diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..333993ef --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,113 @@ +name: Bug report / 错误报告 +description: 游戏崩溃、行为异常或模组冲突?请在这里报告。/ Found a crash or an unexpected behaviour? Report it here. +title: "[Bug] " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + 请在上方标题中概括问题现象。**只报告一个 bug**——多个无关问题请分开提交,便于定位与关闭。 + + - type: checkboxes + id: existing-issue + attributes: + label: 已检查现有问题 / Checked for existing issues + description: 提交前请先搜索 open 与 closed 的 issue,避免重复报告。 + options: + - label: 我已搜索过现有 issue,未发现相同报告。/ I have searched existing issues and found none. + required: true + - label: 我确认这是本模组自身的问题,而非其它模组、启动器或整合包配置导致。/ I confirmed this originates from this mod, not another mod, launcher or modpack config. + required: true + + - type: input + id: mod-version + attributes: + label: 模组版本 / Mod version + description: 见文件名或 mods 列表中显示的版本号。 + placeholder: "例如 1.21.1-2.4.5.8" + validations: + required: true + + - type: dropdown + id: environment + attributes: + label: 运行环境 / Environment + description: 混合端(如 Mohist、CatServer)不受支持。/ Hybrid server cores are unsupported. + options: + - "单人游戏 / Singleplayer" + - "多人游戏 - 局域网 / Multiplayer - Open to LAN" + - "多人游戏 - 专用服务器 / Multiplayer - Dedicated Server" + validations: + required: true + + - type: dropdown + id: side + attributes: + label: 问题出现在哪一侧 / Affected side + options: + - "客户端与服务器均出现 / Both client and server" + - "仅客户端 / Client only" + - "仅服务器 / Server only" + - "不确定 / Not sure" + validations: + required: true + + - type: textarea + id: other-mods + attributes: + label: 其他已安装模组 / Other installed mods + description: 填写整合包名称,或列出关键模组(尤其 AE2、Mekanism、KubeJS、JEI/EMI/REI 及其版本)。 + placeholder: | + - 整合包名称或「自制整合包」 + - AE2: + - Mekanism: + - KubeJS: + - 配方查看器(JEI/EMI/REI): + validations: + required: true + + - type: textarea + id: description + attributes: + label: 问题描述 / Describe the bug + description: 尽可能详细地说明发生了什么,附上相关方块、物品或界面名称。 + validations: + required: true + + - type: textarea + id: expected + attributes: + label: 预期行为 / Expected behavior + description: 你认为应当发生什么。 + validations: + required: true + + - type: textarea + id: result + attributes: + label: 实际行为 / Actual behavior + description: 实际发生了什么。必要时附截图。 + validations: + required: true + + - type: textarea + id: reproduce + attributes: + label: 复现步骤 / Steps to reproduce + description: 缺少复现步骤的报告通常无法修复,请逐步写清。 + value: | + 1. + 2. + 3. + validations: + required: true + + - type: input + id: logs + attributes: + label: 日志或崩溃报告 / Log or crash report + description: | + 请提供链接([Gist](https://gist.github.com/)、[pastebin](https://pastebin.com/) 等),不要直接粘贴大段文本,也不要用会过期的链接。 + 非崩溃问题时可在游戏中按住 F3+C 十秒以上强制生成一份报告。 + validations: + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..044fed1f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: 搜索已有 issue / Search existing issues + url: https://github.com/SorrowMist/UselessMod/issues?q=is%3Aissue + about: 提问或报告前请先搜索 open 与 closed 的 issue,避免重复提交。 + - name: 模组 Wiki / Documentation + url: https://github.com/SorrowMist/UselessMod/tree/1.21/wiki + about: KubeJS 与数据包 API 的使用文档,提交前请先查阅。 diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..c59eaaa4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,52 @@ +name: Feature request / 功能请求 +description: 提出新功能或改进建议。/ Suggest a new feature or an improvement. +title: "[Feature] " +labels: ["feat"] +body: + - type: markdown + attributes: + value: | + 请在上方标题中概括你的建议。提交前请先搜索现有 issue,避免重复提出。 + 本模板用于**功能建议**;使用疑问请先查阅 Wiki 或搜索已有 issue,缺陷请用 Bug report。 + + - type: checkboxes + id: existing-issue + attributes: + label: 已检查现有建议 / Checked for existing suggestions + options: + - label: 我已搜索过现有 issue,未发现相同建议。/ I have searched existing issues and found none. + required: true + + - type: textarea + id: problem + attributes: + label: 想解决的问题 / Problem to solve + description: 说明当前的使用痛点或缺失的能力,而不是直接跳到实现方案。 + placeholder: "例如:批量处理时每条配方都要手动填一遍,效率很低。" + validations: + required: true + + - type: textarea + id: proposal + attributes: + label: 期望的方案 / Proposed solution + description: 你希望模组提供什么功能?若涉及 KubeJS / 数据包 API,请给出期望的调用写法。 + placeholder: | + 例如:希望能这样调用—— + ```js + // 期望的 API 示例 + ``` + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: 考虑过的替代方案 / Alternatives considered + description: 目前你用什么方式绕过?可选填。 + + - type: textarea + id: context + attributes: + label: 补充信息 / Additional context + description: 涉及的具体模组联动、参考实现、截图或上游 issue 链接等。可选填。 \ No newline at end of file diff --git a/.github/labels.yml b/.github/labels.yml new file mode 100644 index 00000000..89f23798 --- /dev/null +++ b/.github/labels.yml @@ -0,0 +1,49 @@ +# GitHub 标签定义(用于 gh label sync / labeler 工具) +# +# 同步方式(需要已登录的 gh CLI,且对仓库有 write 权限): +# gh label sync --file .github/labels.yml +# +# 注意:gh label sync 会以本文件为准,删除仓库中未在此声明的其它标签。 +# 若只想新增而不想删除现有标签,请改用: +# gh label create bug --color D73A4A --description "..." --force +# gh label create feat --color 0E8A16 --description "..." --force + +- name: bug + color: d73a4a + description: 确认或疑似由本模组导致的缺陷、崩溃与行为异常 + +- name: feat + color: 0e8a16 + description: 新功能、功能改进与体验优化建议 + +- name: question + color: d876e3 + description: 使用方式、配置与兼容性疑问(请先查阅 Wiki 与已有 issue) + +- name: documentation + color: 0075ca + description: 文档、Wiki 与指南相关 + +- name: compatibility + color: fbca04 + description: 与其它模组、加载器或服务端的兼容性问题 + +- name: crash + color: b60205 + description: 游戏启动或运行崩溃 + +- name: duplicate + color: cfd3d7 + description: 与已有 issue 重复 + +- name: invalid + color: e4e669 + description: 描述不清、无法复现或不属于本模组范围 + +- name: wontfix + color: ffffff + description: 已确认但不会处理 + +- name: stale + color: ededed + description: 长期无回应,待关闭 \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..9065fc78 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,35 @@ +name: Build + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Setup JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + # This is needed to be able to run ./gradlew below + # You can run `git update-index --chmod +x gradlew` then remove this step. + - name: Make Gradle wrapper executable + run: chmod +x ./gradlew + + - name: Build with Gradle + run: ./gradlew build --console=plain diff --git a/.gitignore b/.gitignore index d8ad0d73..cd3343a9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -<<<<<<< HEAD # eclipse bin *.launch @@ -17,33 +16,40 @@ out # gradle build .gradle +.gradle-tmp + +# vscode +.vscode + +#.dbg +.dbg # other eclipse run - -# Files from Forge MDK -forge*changelog.txt -======= -.gradle -**/build/ -!**/src/**/build/ - -# Ignore Gradle GUI config -gradle-app.setting - -# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) -!gradle-wrapper.jar - -# Avoid ignore Gradle wrappper properties -!gradle-wrapper.properties - -# Cache of project -.gradletasknamecache - -# Eclipse Gradle plugin generated files -# Eclipse Core -.project -# JDT-specific (Eclipse Java Development Tools) -.classpath ->>>>>>> 002d2504eea88f706742ae452f086971844209c3 +runs +run-data +net +repo +.codegraph +othermods +crashreport +.cursor +.cache +.codex +# external reference code +references/ +.crashinfo +.dsh-drop +.workbuddy +.workbuddy-ai + +# Local builds, verification captures and one-off migration artifacts +output/ +verification/ +libs/ +__pycache__/ +*.pyc +*.log +hs_err_pid* +*.cleanup-tmp diff --git a/.vscode/settings.json b/.vscode/settings.json index ebc82519..e0120650 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,4 @@ { "java.compile.nullAnalysis.mode": "automatic", - "java.dependency.packagePresentation": "hierarchical", - "java.jdt.ls.vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx4G -Xms100m -Xlog:disable", - "java.configuration.updateBuildConfiguration": "automatic" + "java.configuration.updateBuildConfiguration": "interactive" } \ No newline at end of file diff --git a/LICENSES/PersonalSpace-LGPL-3.0.txt b/LICENSES/PersonalSpace-LGPL-3.0.txt new file mode 100644 index 00000000..0a041280 --- /dev/null +++ b/LICENSES/PersonalSpace-LGPL-3.0.txt @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/README.md b/README.md new file mode 100644 index 00000000..d5ea713f --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +# Useless Mod + +A Minecraft mod featuring various utilities and enhancements. + +## Forge 1.20.1 + +Version `20.2.4.6` targets Minecraft 1.20.1, Forge 47.3.10+, Java 17, +AE2 15.4.8 and the Create 2 Mekanism: Sky Edition 2 integration profile. +Build with `./gradlew build` (Windows: `gradlew.bat build`); Python 3 is used by +nine release-bytecode/outline regression checks. Dependencies resolve from public Maven repositories. +The `portBridgeTest` suite also runs as part of `check` and is not shipped in the mod jar. + +Optional integrations without a Forge implementation are excluded explicitly in +`gradle/forge1201-pack-profile.json`; this is not a universal backport of every +NeoForge integration. Extended Crafting compiles against official 6.0.10; the +pack's unofficial tier-5/11x11 extension is supported through its native recipe +contract and registry lookup without bundling or replacing the pack's jar. + +## Features + +- Endless Beaf Tool with multiple modes and functionalities +- Enhanced Mekanism upgrades and integrations +- Thermal mod integrations and enhancements +- Various utilities and tools + +## Wiki + +- [KubeJS 万象合金炉配方指南](wiki/KUBEJS_ALLOY_FURNACE_GUIDE_CN.md) +- [KubeJS Advanced Alloy Furnace Recipe Guide](wiki/KUBEJS_ALLOY_FURNACE_GUIDE.md) +- [万象合金炉 BigInteger 发配 API 使用指南](wiki/ALLOY_FURNACE_BIGINT_API_ZH_CN.md) +- [Multiblock Alloy Furnace BigInteger Dispatch API Guide](wiki/ALLOY_FURNACE_BIGINT_API.md) + +## License + +The original Useless Mod code is licensed under the MIT License. Third-party +adaptations remain under their respective licenses described below. + +### Third-party Code + +This mod includes code from several open-source projects under their respective licenses: + +- **Mekanism** - Copyright (c) 2017-2025 Aidan C. Brady +- **Mekanism Upgrades: Reborn** - Copyright (c) Y_Xiao233 +- **PersonalSpace** - Portions of the configurable boundary, road, and + center-marker generation model are adapted from + [GT New Horizons/PersonalSpace](https://github.com/GTNewHorizons/PersonalSpace), + licensed under the GNU LGPL v3.0. The Useless Mod interface, persistence, + networking, and platform-specific extensions are original code. See the + scoped notices in the affected generator/configuration files and + `LICENSES/PersonalSpace-LGPL-3.0.txt`. + +## Contributing + +Contributions are welcome! Please feel free to submit pull requests or open issues. diff --git a/README.txt b/README.txt deleted file mode 100644 index 63809434..00000000 --- a/README.txt +++ /dev/null @@ -1,45 +0,0 @@ -# Useless Mod - -A Minecraft mod featuring various utilities and enhancements. - -## Features - -- Endless Beaf Tool with multiple modes and functionalities -- Enhanced Mekanism upgrades and integrations -- Thermal mod integrations and enhancements -- Various utilities and tools - -## License - -This mod is licensed under the MIT License. See the LICENSE file for details. - -### Third-party Code - -This mod includes code from several open-source projects, all licensed under the MIT License: - -- **Mekanism** - Copyright (c) 2017-2025 Aidan C. Brady -- **Thermal More Integral Components** - Copyright (c) 2024 Elephant_dev -- **Thermal Parallel** - Copyright (c) 2025 EtSH-C2H6S -- **Mekanism Upgrades: Reborn** - Copyright (c) Y_Xiao233 -- **Apotheosis** - Copyright (c) 2023 Brennan Ward - -## Setup - -To set up the development environment: - -### Eclipse -1. Run `./gradlew genEclipseRuns` -2. Import as an existing Gradle project - -### IntelliJ IDEA -1. Import the build.gradle file -2. Run `./gradlew genIntellijRuns` -3. Refresh the Gradle project if needed - -## Building - -Run `./gradlew build` to build the mod. - -## Contributing - -Contributions are welcome! Please feel free to submit pull requests or open issues. diff --git a/build.gradle b/build.gradle index b1fb191d..eb6e1938 100644 --- a/build.gradle +++ b/build.gradle @@ -1,292 +1,113 @@ plugins { - id 'eclipse' - id 'idea' + id 'java-library' id 'maven-publish' - id 'net.minecraftforge.gradle' version '[6.0,6.2)' - id 'org.spongepowered.mixin' version '0.7.+' - id 'org.parchmentmc.librarian.forgegradle' version '1.+' + id 'net.neoforged.moddev.legacyforge' version '2.0.107' + id 'idea' } - version = mod_version group = mod_group_id - -base { - archivesName = mod_id -} - - -// Mojang ships Java 17 to end users in 1.18+, so your mod should target Java 17. +base { archivesName = mod_id } java.toolchain.languageVersion = JavaLanguageVersion.of(17) - -println "Java: ${System.getProperty 'java.version'}, JVM: ${System.getProperty 'java.vm.version'} (${System.getProperty 'java.vendor'}), Arch: ${System.getProperty 'os.arch'}" -minecraft { - // The mappings can be changed at any time and must be in the following format. - // Channel: Version: - // official MCVersion Official field/method names from Mojang mapping files - // parchment YYYY.MM.DD-MCVersion Open community-sourced parameter names and javadocs layered on top of official - // - // You must be aware of the Mojang license when using the 'official' or 'parchment' mappings. - // See more information here: https://github.com/MinecraftForge/MCPConfig/blob/master/Mojang.md - // - // Parchment is an unofficial project maintained by ParchmentMC, separate from MinecraftForge - // Additional setup is needed to use their mappings: https://parchmentmc.org/docs/getting-started - // // Use non-default mappings at your own risk. They may not always work. - // Simply re-run your setup task after changing the mappings to update your workspace. - mappings channel: 'parchment', version: '2023.09.03-1.20.1' - - // When true, this property will have all Eclipse/IntelliJ IDEA run configurations run the "prepareX" task for the given run configuration before launching the game. - // In most cases, it is not necessary to enable. - // enableEclipsePrepareRuns = true - // enableIdeaPrepareRuns = true - - // This property allows configuring Gradle's ProcessResources task(s) to run on IDE output locations before launching the game. - // It is REQUIRED to be set to true for this template to function. - // See https://docs.gradle.org/current/dsl/org.gradle.language.jvm.tasks.ProcessResources.html - copyIdeResources = true - - // When true, this property will add the folder name of all declared run configurations to generated IDE run configurations. - // The folder name can be set on a run configuration using the "folderName" property. - // By default, the folder name of a run configuration is the name of the Gradle project containing it. - // generateRunFolders = true - - // This property enables access transformers for use in development. - // They will be applied to the Minecraft artifact. - // The access transformer file can be anywhere in the project. - // However, it must be at "META-INF/accesstransformer.cfg" in the final mod jar to be loaded by Forge. - // This default location is a best practice to automatically put the file in the right place in the final jar. - // See https://docs.minecraftforge.net/en/latest/advanced/accesstransformers/ for more information. - // accessTransformer = file('src/main/resources/META-INF/accesstransformer.cfg') - - // Default run configurations. - // These can be tweaked, removed, or duplicated as needed. - runs { - // applies to all the run configs below - configureEach { - workingDirectory project.file('run') - - // Recommended logging data for a userdev environment - // The markers can be added/remove as needed separated by commas. - // "SCAN": For mods scan. - // "REGISTRIES": For firing of registry events. - // "REGISTRYDUMP": For getting the contents of all registries. - property 'forge.logging.markers', 'REGISTRIES' - - // Recommended logging level for the console - // You can set various levels here. - // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels - property 'forge.logging.console.level', 'debug' - - mods { - "${mod_id}" { - source sourceSets.main - } - } - } - - client { - // Comma-separated list of namespaces to load gametests from. Empty = all namespaces. - property 'forge.enabledGameTestNamespaces', mod_id - property 'mixin.env.remapRefMap', 'true' - property 'mixin.env.refMapRemappingFile', "${projectDir}/build/createSrgToMcp/output.srg" - } - - server { - property 'forge.enabledGameTestNamespaces', mod_id - args '--nogui' - } - - // This run config launches GameTestServer and runs all registered gametests, then exits. - // By default, the server will crash when no gametests are provided. - // The gametest system is also enabled by default for other run configs under the /test command. - gameTestServer { - property 'forge.enabledGameTestNamespaces', mod_id - } - - data { - // example of overriding the workingDirectory set in configureEach above - workingDirectory project.file('run-data') - - // Specify the modid for data generation, where to output the resulting resource, and where to look for existing resources. - args '--mod', mod_id, '--all', '--output', file('src/generated/resources/'), '--existing', file('src/main/resources/') - } - } -} - -// Include resources generated by data generators. -sourceSets.main.resources { srcDir 'src/generated/resources' } - repositories { - maven { url 'https://modmaven.dev/' } - maven { - url "https://cursemaven.com" + mavenCentral() + exclusiveContent { + forRepository { maven { url = 'https://api.modrinth.com/maven' } } + filter { includeGroup 'maven.modrinth' } } - maven { - name = "Modrinth" - url = "https://api.modrinth.com/maven" + exclusiveContent { + forRepository { maven { url = 'https://cursemaven.com' } } + filter { includeGroup 'curse.maven' } } - // Forge 官方仓库 - maven { - name = "Forge" - url = "https://maven.minecraftforge.net" - } - // Patchouli 仓库 - maven { - name = "BlameJared" - url = "https://maven.blamejared.com" - } - - maven { - name 'Thermal Maven' - url 'https://maven.covers1624.net/' - } - //maven { url 'https://maven.mekanism.io/' } // Mekanism official Maven - - maven { - url "https://maven.shedaniel.me/" - content { - includeGroup "me.shedaniel" - includeGroup "me.shedaniel.cloth" - includeGroup "dev.architectury" - } + maven { url = 'https://maven.createmod.net' } + maven { url = 'https://maven.tterrag.com' } + maven { url = 'https://maven.minecraftforge.net/' } + maven { url = 'https://repo.spongepowered.org/repository/maven-public/' } +} +legacyForge { + accessTransformers = files('src/main/resources/META-INF/accesstransformer.cfg') + version = "${minecraft_version}-${forge_version}" + runs { + client { client(); gameDirectory = file('run/port-client') } + server { server(); gameDirectory = file('run/port-server'); programArgument '--nogui' } } + mods { "${mod_id}" { sourceSet sourceSets.main } } } +sourceSets.main.resources { srcDir 'src/generated/resources' } +sourceSets.test.compileClasspath += sourceSets.main.compileClasspath +sourceSets.test.runtimeClasspath += sourceSets.main.compileClasspath +// Integrations are compile-only; the client/modpack selects its runtime versions. +configurations.modCompileOnly.transitive = false dependencies { - minecraft "net.minecraftforge:forge:${minecraft_version}-${forge_version}" - implementation 'org.spongepowered:mixin:0.8.5' + modCompileOnly 'maven.modrinth:rxYaglEe:orFPvySe' // advancedae-1.2.7-1.20.1 + modCompileOnly 'maven.modrinth:rqFWfVlz:tT3CLYvK' // apotheosis-1.20.1-7.4.8 + modCompileOnly 'maven.modrinth:IiATswDj:d9GM663Q' // applied-mekanistics-1.4.2 + modCompileOnly 'maven.modrinth:XxWD5pD3:Zfyso1sP' // appliedenergistics2-forge-15.4.8 + modCompileOnly 'curse.maven:applied-flux-965012:6755986' // appliedflux-1.20-1.3.2-forge + modCompileOnly 'maven.modrinth:lhGA9TYQ:1MKTLiiG' // architectury-9.2.14-forge + modCompileOnly 'maven.modrinth:uy4Cnpcm:tNwsir0K' // bookshelf-forge-1.20.1-20.2.13 + modCompileOnly 'maven.modrinth:U6BUTZ7K:9bH71LWw' // botanypots-forge-1.20.1-13.0.41 + modCompileOnly 'maven.modrinth:iFDWVIFV:qFTHdrDH' // brandonscore-1.20.1-3.2.1.302-universal + modCompileOnly 'maven.modrinth:LNytGWDc:JjPQsQVw' // create-1.20.1-6.0.6 + modCompileOnly 'maven.modrinth:vvuO3ImH:IPQlZkz1' // curios-forge-5.14.1-1.20.1 + modCompileOnly 'maven.modrinth:nBqivi8H:YR33MHbk' // draconic-evolution-1.20.1-3.1.2.621-universal + modCompileOnly 'maven.modrinth:49ZofO4f:1na1IiE8' // enderio-1.20.1-6.2.13-beta-all + modCompileOnly 'curse.maven:ex-deorum-901420:6614784' // exdeorum-1.46 + modCompileOnly 'curse.maven:extendedae-892005:7005572' // extendedae-1.20-1.4.6-forge + modCompileOnly 'maven.modrinth:5JMG1gql:NtEZsGjO' // extendedcrafting-1.20.1-6.1.5u + modCompileOnly 'curse.maven:flux-networks-248020:5234697' // fluxnetworks-1.20.1-7.2.1.15 + modCompileOnly 'curse.maven:ftb-chunks-314906:6431735' // ftb-chunks-forge-2001.3.6 + modCompileOnly 'curse.maven:ftb-teams-404468:6130786' // ftb-teams-forge-2001.3.1 + modCompileOnly 'curse.maven:ftb-ultimine-386134:6899418' // ftb-ultimine-forge-2001.1.7 + modCompileOnly 'maven.modrinth:8BmcQJ2H:aVW7Z5da' // geckolib-forge-1.20.1-4.8.2 + modCompileOnly 'maven.modrinth:6bLUlbZn:6ByGTKSc' // hostileneuralnetworks-1.20.1-5.3.3 + modCompileOnly 'maven.modrinth:lWxpUd04:O8YO7ORj' // industrial-foregoing-1.20.1-3.5.19 + modCompileOnly 'maven.modrinth:nvQzSEkH:LecuGude' // jade-1.20.1-forge-11.13.2 + modCompileOnly 'maven.modrinth:fRiHVvU7:WtJS5tVw' // emi-1.1.22-1.20.1-forge + modCompileOnly 'maven.modrinth:u6dRKJwZ:zzpNSbJZ' // jei-1.20.1-forge-15.20.0.116 + modCompileOnly 'maven.modrinth:umyGl7zF:g5igndAv' // kubejs-forge-2001.6.5-build.16 + modCompileOnly 'maven.modrinth:Ce6I4WUE:9iboPfea' // mekanism-1.20.1-10.4.15.75 + modCompileOnly 'maven.modrinth:OFVYKsAk:x9re7wt4' // mekanismgenerators-1.20.1-10.4.15.75 + modCompileOnly 'maven.modrinth:tCkE8p2N:6SkuAGoz' // placebo-1.20.1-8.6.3 + modCompileOnly 'maven.modrinth:KZO4S4DO:xDOvxyqP' // powah-5.0.11 + modCompileOnly 'com.tterrag.registrate:Registrate:MC1.20-1.3.11' // registrate-mc1.20-1.3.11 + modCompileOnly 'maven.modrinth:sk9knFPE:uNALdylI' // rhino-forge-2001.2.3-build.10 + modCompileOnly 'maven.modrinth:19smZ71v:zAGXClGI' // summoningrituals-forge-1.20.1-2.0.8 + modCompileOnly 'maven.modrinth:1Ro7m06l:8krhHkrZ' // titanium-1.20.1-3.8.32 + modCompileOnly 'maven.modrinth:Rw1NrDzF:tHDveaxK' // cucumber-1.20.1-7.0.15 + modCompileOnly 'maven.modrinth:UhW5uCKw:eoUaDkZf' // glodium-1.20-1.5-forge + modCompileOnly 'maven.modrinth:HUZkxNTQ:zzqHs32A' // mekanism_extras-1.20.1-1.4.1 + modCompileOnly 'net.createmod.ponder:Ponder-Forge-1.20.1:1.0.80' // ponder-forge-1.20.1-1.0.80 + modCompileOnly 'dev.engine-room.flywheel:flywheel-forge-1.20.1:1.0.4' // flywheel-forge-1.20.1-1.0.4 annotationProcessor 'org.spongepowered:mixin:0.8.5:processor' - implementation fg.deobf("mekanism:Mekanism:${mekanism_version}") - implementation fg.deobf("mekanism:Mekanism:${mekanism_version}:additions") - implementation fg.deobf("mekanism:Mekanism:${mekanism_version}:generators") - implementation fg.deobf("mekanism:Mekanism:${mekanism_version}:tools") - implementation fg.deobf("curse.maven:jei-238222:7037120") // JEI-1.20.1-15.20.0.116 - implementation fg.deobf("curse.maven:ApothicAttributes-898963:5634071") // ApothicAttributes-1.20.1-1.3.7 - implementation fg.deobf("curse.maven:Placebo-283644:6274231") // Placebo-1.20.1-8.6.3 - implementation fg.deobf("curse.maven:Patchouli-306770:6164575") // Patchouli-1.20.1-84.1 - implementation fg.deobf("curse.maven:Apotheosis-313970:6461960") // Apotheosis-1.20.1-7.4.8 - implementation fg.deobf("curse.maven:thermal_foundation-222880:5443583") // thermal_foundation-1.20.1-11.0.6.70 - implementation fg.deobf("curse.maven:thermal_expansion-69163:5372749") // thermal_expansion-1.20.1-11.0.1.29 - implementation fg.deobf("curse.maven:cofh_core-69162:5374122") // cofh_core-1.20.1-11.0.2.56 - implementation fg.deobf("com.teamcofh:thermal_core:1.20.1-11.0.7.25") - implementation fg.deobf("curse.maven:TravelAnchors-413234:4683662") // TravelAnchors-1.20.1-5.0.1 - implementation fg.deobf("curse.maven:LibX-412525:4947474") // LibX-1.20.1-5.0.12 - implementation fg.deobf("curse.maven:jecharacters-250702:6680042") // jecharacters-1.20.1-forge-4.5.16 - implementation fg.deobf("curse.maven:GregTechCEu-890405:6254458") // GregTechCEu-1.20.1-1.6.4 - implementation fg.deobf("curse.maven:BotanyPots-353928:6348581") // BotanyPots-Forge-1.20.1-13.0.41 - implementation fg.deobf("curse.maven:Bookshelf-228525:5423987") // Bookshelf-Forge-1.20.1-20.2.13 - implementation fg.deobf("curse.maven:configured-457570:5180900") // configured-forge-1.20.1-2.2.3 - implementation fg.deobf("curse.maven:extendedae_plus-1337639:7068596") // extendedae_plus-1.4.4 - implementation fg.deobf("curse.maven:mekanism_extras-1026040:6936409") // mekanism_extras-1.20.1-1.4.6 - implementation fg.deobf("curse.maven:ThermalMore-1078644:5704624") // ThermalMore-1.1.2-1.20.1 - implementation fg.deobf("curse.maven:ftb-ultimine-386134:6899418") // ftb-ultimine-forge-2001.1.7 - implementation fg.deobf("curse.maven:Architectury-API-419699:5137938") // Architectury API - implementation fg.deobf("curse.maven:ftb-library-API-404465:6807424") // ftb-library-forge-2001.2.10 - //implementation fg.deobf("curse.maven:thermal_parallel-1.0.0-1350169:7013612") // thermal_parallel-1.0.0 - implementation fg.deobf("curse.maven:spark-361579:4738952") // spark - implementation fg.deobf("curse.maven:Modern-UI-352491:6956345") // Modern UI - implementation fg.deobf("curse.maven:Observable-509575:5643037") // Observable - implementation fg.deobf("curse.maven:Kotlin-for-Forge-4.11.0-351264:5402061") // Kotlin for Forge 4.11.0 - implementation fg.deobf("curse.maven:mob_grinding_utils-1.20.1-1.1.0-254241:5131447") // mob_grinding_utils-1.20.1-1.1.0 - implementation fg.deobf("curse.maven:biggerstacks-1.20.1-1.0.4-all-564896:6458432") // biggerstacks-1.20.1-1.0.4-all - implementation fg.deobf("curse.maven:SkyBreaker-1.20.1-1.0.8-1109012:6645990") // SkyBreaker-1.20.1-1.0.8 - implementation fg.deobf("curse.maven:omnitools-1.0.0-1401075:7314939") // omnitools-1.0.0 - implementation fg.deobf("curse.maven:advancedae-1084104:7159779")//advancedae - implementation fg.deobf("curse.maven:geckolib-388172:7025129")//geckolib - // implementation fg.deobf("curse.maven:emi-580555:6420945")//emi - compileOnly fg.deobf("curse.maven:emi-580555:6420945")//emi - implementation fg.deobf("curse.maven:mega-cells-622112:6175008")//mega-cells - implementation fg.deobf("curse.maven:omni-cells-1359517:7169531")//omni-cells - implementation fg.deobf("curse.maven:applied-flux-965012:7072853")//applied-flux - implementation fg.deobf("curse.maven:cloth-config-348521:5729105")//cloth-config-5.1.48-forge - implementation fg.deobf("curse.maven:sophisticated-core-618298:7369562")//sophisticated-core-6.1.82-forge - implementation fg.deobf("curse.maven:sophisticated-storage-619320:7369594")//sophisticated-storage-6.1.82-forge - implementation fg.deobf("curse.maven:sophisticated-backpacks-422301:7369580")//sophisticated-backpacks-6.1.82-forge - implementation fg.deobf("curse.maven:industrial-foregoing-266515:7068937")//industrial-foregoing-1.20.1-1.1.0 - implementation fg.deobf("curse.maven:titanium-287342:5468426")//titanium-1.20.1-1.0.0 - implementation fg.deobf("curse.maven:blood-magic-224791:7253333")//blood-magic-1.20.1-1.0.0 - implementation fg.deobf("curse.maven:embers-rekindled-300777:7446478")//mbersrekindled-1.20.1-1.4.7 - - - //exendedae前置 - implementation fg.deobf("curse.maven:glodium-957920:5226922") - //extendedAE - implementation fg.deobf("curse.maven:ex-pattern-provider-892005:7144417") - //ae2 - implementation fg.deobf("curse.maven:applied-energistics-2-223794:7148487") - implementation fg.deobf("curse.maven:guideme-1173950:6679102") - //config - implementation fg.deobf("curse.maven:configured-457570:5180900") - - - + compileOnly 'io.github.llamalad7:mixinextras-common:0.5.0' + annotationProcessor 'io.github.llamalad7:mixinextras-common:0.5.0' + testImplementation platform('org.junit:junit-bom:5.11.4') + testImplementation 'org.junit.jupiter:junit-jupiter' + testImplementation 'org.mockito:mockito-core:5.15.2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } - mixin { - add sourceSets.main, "useless_mod.refmap.json" - config "useless_mod.mixins.json" - debug.export = true + add sourceSets.main, "${mod_id}.refmap.json" + ["${mod_id}.mixins.json", "${mod_id}.create.mixins.json", "${mod_id}.mekanism.mixins.json", "${mod_id}.jade.mixins.json"].each { config it } } -// This block of code expands all declared replace properties in the specified resource targets. -// A missing property will result in an error. Properties are expanded using ${} Groovy notation. -// When "copyIdeResources" is enabled, this will also run before the game launches in IDE environments. -// See https://docs.gradle.org/current/dsl/org.gradle.language.jvm.tasks.ProcessResources.html -tasks.named('processResources', ProcessResources).configure { - var replaceProperties = [ - minecraft_version: minecraft_version, minecraft_version_range: minecraft_version_range, - forge_version: forge_version, forge_version_range: forge_version_range, - loader_version_range: loader_version_range, - mod_id: mod_id, mod_name: mod_name, mod_license: mod_license, mod_version: mod_version, - mod_authors: mod_authors, mod_description: mod_description, - ] - inputs.properties replaceProperties - - filesMatching(['META-INF/mods.toml', 'pack.mcmeta']) { - expand replaceProperties + [project: project] - } +tasks.named('jar') { + manifest.attributes('MixinConfigs': ["${mod_id}.mixins.json", "${mod_id}.create.mixins.json", "${mod_id}.mekanism.mixins.json", "${mod_id}.jade.mixins.json"].join(',')) } - -// Example for how to get properties into the manifest for reading at runtime. -tasks.named('jar', Jar).configure { - manifest { - attributes([ - 'Specification-Title' : mod_id, - 'Specification-Vendor' : mod_authors, - 'Specification-Version' : '1', // We are version 1 of ourselves - 'Implementation-Title' : project.name, - 'Implementation-Version' : project.jar.archiveVersion, - 'Implementation-Vendor' : mod_authors, - 'Implementation-Timestamp': new Date().format("yyyy-MM-dd'T'HH:mm:ssZ") - ]) - } - - // This is the preferred method to reobfuscate your jar file - finalizedBy 'reobfJar' +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' + options.compilerArgs += ['-Xmaxerrs', '10000'] } - -// However if you are in a multi-project build, dev time needs unobfed jar files, so you can delay the obfuscation until publishing by doing: -// tasks.named('publish').configure { -// dependsOn 'reobfJar' -// } - -// Example configuration to allow publishing using the maven-publish plugin -publishing { - publications { - register('mavenJava', MavenPublication) { - artifact jar - } - } - repositories { - maven { - url "file://${project.projectDir}/mcmodsrepo" - } +tasks.named('test', Test) { useJUnitPlatform() } +tasks.named('processResources', ProcessResources) { + inputs.property 'mod_version', mod_version + filesMatching('META-INF/mods.toml') { + expand mod_id: mod_id, mod_name: mod_name, mod_version: mod_version } } -tasks.withType(JavaCompile).configureEach { - options.encoding = 'UTF-8' // Use the UTF-8 charset for Java compilation -} +apply from: 'gradle/port-bridge-tests.gradle' +apply from: 'gradle/forge1201-pack-profile.gradle' -compileJava { - options.compilerArgs += ['-Xlint:-deprecation', '-Xlint:-removal', '-Xlint:-unchecked'] -} +apply from: 'gradle/bytecode-contract-tests.gradle' diff --git a/changelog.txt b/changelog.txt deleted file mode 100644 index a32e932a..00000000 --- a/changelog.txt +++ /dev/null @@ -1,1049 +0,0 @@ -1.20.1 Changelog -47.4 -==== - - 47.4.0 1.20.1 RB 4 - https://forums.minecraftforge.net/topic/154387-forge-474-minecraft-1201/ - -47.3 -==== - - 47.3.39 Fix ForgeDev's test runs not working due to dead test mod (#10483) - - 47.3.38 Cache this.useItem before running item break logic, Fixes #10344 (#10376) - - 47.3.37 Speed up mod annotation scanning by ~30% (#10470) - Co-authored-by: LexManos - - 47.3.36 Add missed license headers (#10479) - - 47.3.35 Add '#forge:chorus_additionally_grows_on' tag for similar mechanics to '#minecraft:azalea_grows_on' but for chorus (#10456) - - 47.3.34 Fix cancelling ProjectileImpactEvent still firing onBlockHit (#10481) - - 47.3.33 Honor attacker shield disabling status (#10321) - - 47.3.32 Add fast graphics render type to block model jsons (#10393) - Make modded leaves behave like vanilla leaves by default (Fixes #10389) - - 47.3.31 Fix invalidly symlinked worlds crashing on level select (#10439) - - 47.3.30 Backport even more future ResourceLocation methods (#10428) - - 47.3.29 Ensure NetworkConstants is loaded before mod construction (#10407) - - 47.3.28 Account for problematic mixins in VillagerTrades.EmeraldsForVillagerTypeItem (#10402) - - 47.3.27 Fix incorrect method reference in TntBlock.explode() - - 47.3.26 Fix issues in VillagerTrades.EmeraldsForVillagerTypeItem related to custom Villager Types (#10315) - Add VillagerType#registerBiomeType - - 47.3.25 Add `clientSideOnly` feature to mods.toml (#10085) (backport of #9804 to 1.20.1) - Co-authored-by: Jonathing - - 47.3.24 Fix non-passengers being tickable without checking canUpdate() (#10304) - - 47.3.23 Fix finalizeSpawn's return value not being used correctly (#10301) - - 47.3.22 Bump CoreMods to 5.2.4 (#10263) - - 47.3.21 Allow mipmap lowering to be disabled (#10252) - - 47.3.20 Add optional fix of use item duration, disabled by default (#10246) - - 47.3.19 Backport some Vanilla 1.21 `ResourceLocation` methods (#10241) - Co-authored-by: Paint_Ninja - - 47.3.18 Simplify memory usage display on loading screen (#10233) - Co-authored-by: Paint_Ninja - - 47.3.17 Deprecate `@ObjectHolder`, add a couple of fast-paths (#10228) - Co-authored-by: Paint_Ninja - - 47.3.16 Skip Vanilla classes for the `CapabilityTokenSubclass` transformer (#10221) - Co-authored-by: Paint_Ninja - - 47.3.15 Skip Forge classes in the RuntimeEnumExtender transformer (#10216) - Mod classes are still transformed as usual - Co-authored-by: Paint_Ninja - - 47.3.14 Skip processing Forge classes in `RuntimeDistCleaner` (#10208) - Co-authored-by: Paint_Ninja - - 47.3.13 Disable clean on TeamCity (#10258) - - 47.3.12 Bump CoreMods to 5.2 (#10130) - Full Changelog: - https://gist.github.com/Jonathing/c3ad28b2a048ac839a7baba5417ee870 - The key features are: - - ES6 language support - - Thoroughly updated ASMAPI, with full documentation - - Bug fixes (some optional for backwards-compatibility) - - Partial internal code cleanup - - Request CoreMods to not apply fix for ASMAPI.findFirstInstructionBefore by default - - Updated ASM to 9.7.1 - - Updated Nashorn to 15.4 - - 47.3.11 Remove unneeded boat patch (backport of #10061 to 1.20.1) (#10096) - Co-authored-by: andan42 <49289986+andan42@users.noreply.github.com> - - 47.3.10 Optionally supply FMLJavaModLoadingContext as a param to mod constructors (backport of #10074 to 1.20.1) (#10100) - Co-authored-by: RealMangoRage - - 47.3.9 Minor cleanup to ModListScreen and VersionChecker (backport of #9988 to 1.20.1) (#10095) - - 47.3.8 Cleanup FML Bindings (backport of #10004 to 1.20.1) (#10094) - - 47.3.7 Early display fixes/workarounds for buggy drivers. Backport of #9921 to 1.20.1 (#10073) - - 47.3.6 Add a way to render tooltips from Formatted text and TooltipComponents elements (#10055) - Backport of #10056 for 1.20.1 - - 47.3.5 Make HangingSignBlockEntity useable with custom BlockEntityTypes. #10038 - - 47.3.4 Unlock wrapped registries when firing register events. (#10035) - Co-authored-by: LexManos - - 47.3.3 Choose default JarJar mod file type based on parent JAR (#10023) - Co-authored-by: thedarkcolour <30441001+thedarkcolour@users.noreply.github.com> - - 47.3.2 Fixed falling block entities not rendering as moving blocks (#10006) (#10018) - Co-authored-by: Ven - - 47.3.1 Fix boat travel distance being incorrect. Closes #9997 #9999 - - 47.3.0 1.20.1 RB 3 - https://forums.minecraftforge.net/topic/139825-forge-473-minecraft-1201/ - -47.2 -==== - - 47.2.36 Bump gradle to 8.8 - - 47.2.35 Bump gradle and java runtime version on team city - - 47.2.34 Fix LevelSettings ignoring data configuration. Close #9938 - - 47.2.33 Fix erroneous patch in FireBlock. Closes #9996 - - 47.2.32 Fix early window crash when parsing options.txt (#9934) - Backport of #9933 - - 47.2.31 Prevent mixins from crashing the game when there are missing mods (#9916) - 1.20.1 backport of 49.0.14 - - 47.2.30 Fix NPE when acceptableValues in defineInList() does not allow nulls, backport of #9903 (#9907) - Co-authored-by: J-RAP - - 47.2.29 Fix Crowdin (#9929) - - 47.2.28 Optimise capabilities a tad, backport of #9886 (#9911) - - 47.2.27 Add helper method to `OnDatapackSyncEvent`, backport of #9901 (#9919) - - 47.2.26 Add CPU usage config option to early window, hide by default, backport of #9866 (#9915) - - 47.2.25 Fix slightly offset mods screen link positioning, backport of #9860 (#9861) - Co-authored-by: Dennis C <11262040+XFactHD@users.noreply.github.com> - - 47.2.24 Make common config screen registration tasks easier, backport of #9884 (#9912) - - 47.2.23 Add Leaves method to ModelProvider.java (#9889) - - 47.2.22 [1.20.1] Bump CoreMods and ASM (#9897) - - CoreMods 5.1.2 -> 5.1.6 - - ASM 9.6 -> 9.7 - - 47.2.21 Bump some deps (#9880) - - CoreMods 5.0.1 -> 5.1.2 - - ASM 9.5 -> 9.6 - - Installer 2.1 -> 2.2 - - Installer tools 1.3.0 -> 1.4.1 - - 47.2.20 Fix missing patch for Item.onInventoryTick. Closes #9812 - - 47.2.19 Make common DisplayTest registration tasks easier (#9823) - - 47.2.18 Optimise ForgeConfigSpec and make Range public (#9824) - Backport of #9810 to 1.20.1 - - 47.2.17 Datagen addOptionalTag/s methods that allow passing the TagKey itself instead of passing the location (#9807) (#9808) - - 47.2.16 Update VersionSupportMatrix.java (#9805) - - 47.2.15 Backport of Registries optimization, now uses fastutils collections to minimize boxing - - 47.2.14 Fix patch offset - - 47.2.13 Fix fire related mobs not taking enough freezing damage. Closes #9686 - - 47.2.12 Fix TagLoader error not printing tag name correctly. Closes #9693 - - 47.2.11 Fix LoadingErrorScreen inner headers are not centered. Closes #9687 - - 47.2.10 Rework KeyModifiers system to properly allow keybinds to be triggered when multiple modifiers are pressed. - Fix setting keybinds whel using keyboard inputs to select the menu. Closes #9793 - - 47.2.9 Fix KeyModifiers not being properly taken into account. Closes #9806 - - 47.2.8 Don't turn off VSync when rendering from Minecraft context (#9801) - Co-authored-by: embeddedt <42941056+embeddedt@users.noreply.github.com> - - 47.2.7 Fix rare crash with early display window, fixes MinecraftForge#9673 (#9799) - - 47.2.6 Fix tag loading being unordered. Closes #9774 - - 47.2.5 Fix misaligned patch in RegistryDataLoader - - 47.2.4 Backport CrashReportAnalyser to 1.20.1 (#9757) - - 47.2.3 Minor MDK changes (#9752) - - 47.2.2 Improve mod description formatting in mods screen (#9769) - Co-authored-by: Su5eD - - 47.2.1 [1.20.1] Improve mod loading error message for errors inside mod constructors (#9707) - - 47.2.0 1.20.1 RB - -47.1 -==== - - 47.1.47 Keep order of sources in PackRepository (#9702) - Co-authored-by: dhyces <10985914+dhyces@users.noreply.github.com> - - 47.1.46 Fix DelegatingPackResources searching resource path twice (#9697) - - 47.1.45 Fix `Level` leak in debug HUD (#9699) - Co-authored-by: malte0811 - - 47.1.44 Fix PlayerSpawnPhantomsEvent not being fired (#9689) - - 47.1.43 Enhance LivingBreathEvent and LivingDrownEvent. Closes #9680 - Also remove 3.5MB of useless data from the installer. - - 47.1.42 Partially revert LazyOptional changes, now no longer internally uses weak references. - - 47.1.41 Make LazyOptional's internal references to invalidation listeners use WeakReference, and allow modders to unregister themselves. Closes #8805 - - 47.1.40 Revert EntityEvent.Size changes to before #9018 was called. (#9679) - Kept newly added methods for binary compatibility but deprecated them all for removal. - The entire pose/eye/size system needs to be reevaluated and address some of Mojang's changes. - However this should fix any bugs that pulling that PR may of caused. - - 47.1.39 Add a config option to restore the calculate all normals behavior in case some setups require old broken behavior. (#9670) - - 47.1.38 Fix rounding errors on models with 45 degree normals by favoring one Direction (#9669) - Should fix flickering issues when breaking custom models and having our vanilla solution disabled. - - 47.1.37 Moved ForgeHooksClient.onCreativeModeTabBuildContents to ForgeHooks to fix #9662 - - 47.1.36 Fix tag removal functionality that broke during the 1.19 update. Closes #9053 and #8949 - - 47.1.35 Replace string with forge tag in vanilla recipes. Closes #9062 - - 47.1.34 Fix new brain hooks not persisting active activities. - - 47.1.33 Fix breaking overlay flickering on campfires by using vanilla method to calculate block normals. (#9664) - - 47.1.32 Cleanup usages of static imports and build script so that our build doesn't spam useless error like messages. - So that it is easier to see real errors. - Add compatibility checking to standard testing tasks. - - 47.1.31 Added Action value to PlayerInteractEvent.LeftClickEvent to expose what action fired the event. #9175 - - 47.1.30 Fix parameter names in IForgeDimensionSpecialEffects.adjustLightmapColors to better describe what they are. (#9656) - - 47.1.29 Re-add EntityEvent.Size methods to maintain bincompat - - 47.1.28 Added LivingMakeBrainEvent, to allow a consistent way for modders to manipulate entity Brains. #9292 - - 47.1.27 Add LivingSwapHandItemsEvent - - 47.1.26 Fixed FluidUtil#tryFillContainer returning invalid result when simulating #9358 - - 47.1.25 Re-add in-game mod menu (#9652) - - 47.1.24 Fix Entity eye height for multipart entities. - - 47.1.23 Fix conflicting keybindings not having the correct click count set. #9360 - - 47.1.22 Fix the Emissive Rendering for Experimental Light Pipeline (#9651) - - 47.1.21 Fixed AdvancementsScreen.java.patch buttons not rendering (#9649) - - 47.1.20 Properly Handle Fluid Updates while in a Boat #9428 - - 47.1.19 New hook in IClientBlockExtensions to handle enabling tinting on breaking particles. #9446 - - 47.1.18 Fix invalid index when ticking itemstacks in a player nventory by adding a new onInventoryTick event. Closes #9453 - - 47.1.17 Make the FireworkShapes enum extensible (Closes #9486) - - 47.1.16 Add `EmptyEnergyStorage` (#9487) - - 47.1.15 Support IPv6 address compression for logged IPs - - 47.1.14 Make item name rendering and status bar rendering respect additional gui overlays being rendered by mods (#9648) - - 47.1.13 Fix EyeHeight event being fired twice (#9647) - - 47.1.12 Add PlayerSpawnPhantomsEvent, utilized to block or forcefully allow PhantomSpawner to spawn phantoms (#9644) - - 47.1.11 Fix creative mode screen not correctly using CreativeModeTab::getTabsImage (#9627) - - 47.1.10 Add option to advertise dedicated servers to LAN. - - 47.1.9 Fix entity eye height loop. - - 47.1.8 Particle Description Data Provider. - - 47.1.7 Add LivingBreatheEvent and LivingDrownEvent (#9525) - - 47.1.6 Fix entity size event not being fired, changed it to split eye height and size calculations. (#9535) - - 47.1.5 AlterGroundEvent for modifying block placement performed by AlterGroundDecorator (#9637) - - 47.1.4 Change ProjectileHitEvent to return a result instead of being cancelable. Closes #9642 - - 47.1.3 Replace static import with regular one to fix S2S and non-official mappings. (#9633) - - 47.1.2 Add missing null check for TagsProvider#existingFileHelper (#9638) - - 47.1.1 Add GuiGraphics helpers for blitNineSliced and blitRepeating that support specifying a custom texture size (#9641) - - 47.1.0 1.20.1 Recommended Build - -47.0 -==== - - 47.0.50 Fix FMLOnly loading. Closes #9609 - - 47.0.49 Improve logging for server connections (#9618) - - 47.0.48 Fix placing fluids into waterlogged blocks with FluidUtil::tryPlaceFluid. To behave more like vanilla MC-127110 (#9586) - - 47.0.47 Expose loaded RegistryAccess through AddReloadListenerEvent (#9613) - - 47.0.46 Fix GLM applying to entities when killed. Closes #9551 - - 47.0.45 Add unordered creative tabs after vanilla and perform a second level sorting of tabs by registry name to ensure tabs are ordered the same between game restarts (#9612) - - 47.0.44 Fix Early Loading window FPS snafu where it could spam (#9619) - unlimited screen updates. Probably a good way to gently toast an ancient laptop. - - 47.0.43 Make overloads consistent for defineListAllowEmpty in ForgeConfigSpec.Builder (#9604) - - 47.0.42 Moved GameShuttingDownEvent hook to DedicatedServer class. Fixes #9601 - - 47.0.41 Fix PitcherCropBlock not calling canSustainPlant, not allowing it to be placed on custom farmland. Close #9611 - - 47.0.40 Add null check to NetworkHooks.openScreen. Closes #9597 - - 47.0.39 Fix ShieldBlockEvent not correctly performing damaged functions when not all damage is blocked. (#9615) - - 47.0.38 Add IP address to client disconnect message. Closes #9603 - - 47.0.37 Fix hotbar items not dimming when sleeping in bed. Closes #9616 - - 47.0.36 Bump Eventbus to address NullPointerException when classloading things on some threads. Closes #9570 - - 47.0.35 Try and be a bit nicer about handling bad [feature] definitions (#9606) - * Try and be a bit nicer about handling bad [feature] definitions - they're single square bracket defined and require strings for feature bound values. - * Some more tweaks to the feature system to output useful and well formatted error messages when bounds fail. Requires SPI 7.0.1 and the language string "fml.modloading.feature.missing" has changed. - * Use immutable lists where possible and generally tidy things a bit - - 47.0.34 [HotFix]: Somehow not caught by CI, but there was an issue in the SpawnUtils to handle. - - 47.0.33 Remove amount from FluidStack hashCode to fix the equal/hashCode java contract (#9602) - - 47.0.32 Add example usage for configs to the MDK (#9596) - Demonstrates proper usage of different config value types, validation, transformation/parsing, load/reload handling and direct field access. - - 47.0.31 Demonstrate configureEach in run configs (#9594) - - 47.0.30 Reduce verbosity of prepareRuns doc in MDK (#9593) - - 47.0.29 Lazily configure MDK tasks, improve IDE support (#9592) - - 47.0.28 Fix not dropping xp for player sourced explosions and various other cases when xp should drop (#9588) - - 47.0.27 add item handler capability to chiseled bookshelf (#9587) - - 47.0.26 Fix ignoring maxUpStep method overrides on entities (#9583) - - 47.0.25 Add missing damage type tag provider modid, existing file helper constructor overload (#9581) - - 47.0.24 Expose holder lookup registry provider as a helper in RegistriesDatapackGenerator (#9580) - - 47.0.23 Fix gametest collection causing secondary crash in loading error state (#9568) - - 47.0.22 Fix SpriteCoordinateExpander not working with chained methods (MC-263524) (#9564) - - 47.0.21 Expose EntityRendererProvider.Context to AddLayers event (#9562) - - 47.0.20 [1.20] Add events for checking spawn conditions (SpawnPlacementCheck and PositionCheck) (#9469) - - 47.0.19 Another tweak to the early display. We start a timer process while we create the window, in case it hangs. Also add a skip version config, to try and help in cases where the driver is stupid. (#9595) - - 47.0.18 Auto generate names for modded loot pools. Fixes #9589 (#9591) - - 47.0.17 More tweaks to the mod loading callbacks to make loading bars work better (#9585) - - 47.0.16 Reimplement LootPool name patches and hooks (#9573) - - 47.0.15 Fix experimental lighting pipeline breaking vanilla's emissive rendering. Closes #9552 (#9582) - - 47.0.14 Update early loading default configs to match standard expectations. (#9577) - Also allow a global override env variable for darkmode always. "FML_EARLY_WINDOW_DARK" - - 47.0.13 Add proper duplicate mod error message that includes the mod id(s) and mod files. (#9474) - - 47.0.12 Add missing stack tooltip rendering hooks (#9533) - Fix automatic tooltip wrapping discarding empty lines used as spacers - - 47.0.11 Add piston push reaction overrides to Block class (#9538) - - 47.0.10 Fix missing calls to level-sensitive block SoundType getter (#9553) - - 47.0.9 Fix forge registries that have wrappers/support tags not being in the HolderLookup Provider that is provided to the GatherDataEvent (#9566) - - 47.0.8 Make IItemDecorator a functional interface again. Fixes #9563 (#9574) - - 47.0.7 Make 1.20.x the main TC branch. - - 47.0.6 Fix custom geometry in parent models not being resolved (#9572) - - 47.0.5 Make the scheduled thread factory setDaemon on it's threads. Should allow things to close properly if something deadly happens early on. (#9575) - - 47.0.4 This is an early display window system for forge. (#9558) - - 47.0.3 fix the JIJ break by bumping SJH. apologies. - - 47.0.2 update libs (#9565) - - 47.0.1 Update Armor Layer Implementation to Match Vanilla (#9547) - - 47.0.0 1.20.1 Update - -46.0 -==== - - 46.0.14 Fix JAR compatibility checks for 1.20 (#9556) - - 46.0.13 [1.20] Add `AFTER_LEVEL` render level stage (#9555) - - 46.0.12 Reorder overlay layers to match vanilla (#9550) - - 46.0.11 Re-implement missing level-sensitive block light hook in ChunkAccess (#9536) - - 46.0.10 Fix issues in the deserialization of empty ingredients (#9537) - - 46.0.9 Fix wrong variable passed into EnchantmentClue lookup Fixes #9543 (#9544) - - 46.0.8 Fix incorrect depth test state in debug graph rendering Fixes #9534 (#9539) - - 46.0.7 Fix initCapabilities patch location in ServerLevel Fixes #9526 (#9531) - - 46.0.6 Use Spinning Effect Intensity instead of Partial Tick for Portal Overlay Fixes #9529 (#9530) - - 46.0.5 Fix getArmorModelHook patch, Fixex #9523 (#9528) - - 46.0.4 Fix duplicate Map writes in PlayerList patch. (#9521) - - 46.0.3 Fix Forge Version including MC version in MDK. - - 46.0.2 Fix patch in light propagation (#9532) - - 46.0.1 Attempt to fix jar signing - Gradle 8 is stupid and doesn't (easily) allow in-place tasks, so a temporary fix has been made in ForgeGradle 6 - - 46.0.0 Forge 1.20 - - Creative mode tabs are now a registry; the `BuildContents` event was renamed to `BuildCreativeModeTabContentsEvent` and moved it to its own class - - The pack format is now 15 for both resource packs and data packs - - `ScreenUtils` was deprecated in favor of a `GuiGraphics` extension - - Forge and the MDK were updated to Gradle 8 and FG6 - - The Forge common config file was removed (it only contained the deprecated old fields for resource caching, which was removed in 1.19.3) - - Registry dummy entries were removed - - `RemappingVertexPipeline` was fixed to forward the `endVertex()` call - - Forge tool tags were removed in favor of vanilla ones - Co-authored-by: ChampionAsh5357 - Co-authored-by: coehlrich - Co-authored-by: Dennis C <11262040+XFactHD@users.noreply.github.com> - Co-authored-by: Matyrobbrt - -45.1 -==== - - 45.1.0 1.19.4 Recommended Build - -45.0 -==== - - 45.0.66 Add method to GatherDataEvent to obtain collection of all input paths. (#9499) - - 45.0.65 Log error when Sheets is class-loaded before registration is completed (#9475) - - 45.0.64 [1.19.x] Re-implement RenderTooltipEvent.Color (#9497) - * Reimplement RenderTooltipEvent.Color - * Formatting, comments, EXC - * Deprecate instead of replacing - - 45.0.63 Add API for registering custom world preset editors (#9436) - - 45.0.62 Remove unneeded extra reload of datapacks on world creation screen (#9454) - - 45.0.61 Bump ASM to 9.5 - - 45.0.60 Fix crash when running server from root directory - Fixes #9498 - - 45.0.59 Fix root transform matrix format, allow using all four root transform formats (#9496) - - 45.0.58 Add missing AT lines to allow registering custom game rule boolean/integer types (#9489) - - 45.0.57 [1.19.x] Fix SaplingGrowTreeEvent#setFeature being ignored in FungusBlock (#9485) - Co-authored-by: Brennan Ward <3682588+Shadows-of-Fire@users.noreply.github.com> - - 45.0.56 Restore AccessibilityOnboardingScreen - Fixes #9488 - - 45.0.55 Update documentation on FinalizeSpawn (#9467) - - 45.0.54 Fix fluids without sound event causing exception in tryFillContainer and tryEmptyContainer (#9445) - - 45.0.53 Make FakePlayerFactory respect the given ServerLevel (#9479) - - 45.0.52 Collect and log exceptions occurring in DeferredWorkQueue tasks (#9449) - - 45.0.51 Fix `NamespacedWrapper#wrapAsHolder` (#9450) - - 45.0.50 Fixes ChatScreen calling .setScreen (#9443) - Fix test compile failures also. - - 45.0.49 Determine the Forge version the PR was built against when running PR compat checks (#9374) - - 45.0.48 Add buildscript test to error on deprecated members that should of been removed. (#9460) - - 45.0.47 Remove erroneous brace patch in Inventory (#9462) - Fixes #9459 - - 45.0.46 [1.19.4] Move root transform builder to ModelBuilder to allow use in ItemModelBuilder (#9456) - - 45.0.45 Fix forge grindstone hooks allowing stacks of non-stackable items (#9457) - - 45.0.44 [1.19.4] Fix FMLOnly (#9415) - - 45.0.43 Fix ItemLayerModel erroneously adding particle texture to layer texture list (#9441) - - 45.0.42 Temporary fix for Canceling ProjectileImpactEvents of Piercing ammo. - Event needs to be re-worked to have finer control. #9370 - - 45.0.41 Fix dummy air blocks not being marked as air (#9440) - - 45.0.40 Add support for splitting the login packet (#9367) - It contains full copies of data registries and can easily surpass vanilla's limits - - 45.0.39 Remove Attack Range and Reach Distance and add Block Reach and Entity Reach (#9361) - - 45.0.38 Add default bucket sounds for milk (#9432) - - 45.0.37 Deprecate Item.onUsingTick, as vanilla provides the same function in Item.onUseTick now. Closes #9342 - - 45.0.36 Fix ScreenEvent.Init.[Pre/Post] not working correctly (#9431) - - 45.0.35 Allow FenceGateBlock to be used without a WoodType. Closes #9392 - - 45.0.34 Deprecate duplicate tool tags that vanilla added in 1.19.4 - We will maintain a seperate 'tools' tag until Mojang adds all relevent tool tags. - Closes #9393 - - 45.0.33 Fix BlockEvent.Break not using ItemStack enchantment hooks. - - 45.0.32 Move Block.onCatchFire to above block removal to allow usage of BlockEntity data. Closes #9400 - - 45.0.31 Fix FinalizeSpawn not blocking spawns during worldgen (#9420) - - 45.0.30 Fixed issue with MutableHashedLinkedMap when removing multiple sequential entries in the middle of the map. - Added Unit tests for MutableHashLinkedMap - Added support for removing using the iterator - Added concurrent modification detection to the iterator - Added default constructor with basic hashing strategy. - Closes #9426 - - 45.0.29 Loosen access for BucketItem's canBlockContainFluid (#9421) - - 45.0.28 Update and Regenerate Datapacks (#9419) - Add generation for pack.mcmeta - - 45.0.27 Restore ability to change message in ClientChatEvent (#9377) - - 45.0.26 Remove duplicate line in FoodData patch (#9424) - The line was accidentally duplicated in the 1.19.4 update and patching - process. - Fixes #9422 - - 45.0.25 Rename RegisterParticleProviderEvent's register methods to describe what kind of particle providers they register (deprecating old methods to avoid breaking) and minor docs tweaks (#9388) - - 45.0.24 Update pack versions (#9414) - - 45.0.23 [1.19.4] Revamp and fix spawn events (#9133) - - 45.0.22 [1.19.4] Replace blitOffset parameter with PoseStack in IItemDecorator (#9409) - * Replace blitOffset with PoseStack in IItemDecorator - * Circumvent breaking changes - * Fix blitOffset type - - 45.0.21 Fix JSON model root transforms (#9410) - - 45.0.20 Fix tossed items not being able to be picked up by other players. Closes #9412 (#9404) - - 45.0.19 Fix infinite BE render bounds failing frustum intersection test. Closes #9321 (#9407) - - 45.0.18 Make ForgeSlider use the new vanilla texture (#9406) - - 45.0.17 Add BlockSetType#register to accesstransformer.cfg (#9386) - - 45.0.16 Add option to completely hide a crash-callable depending on a runtime value (#9372) - - 45.0.15 Add isNewChunk to ChunkEvent.Load (#9369) - - 45.0.14 Remove DistExecutor calls in PistonEventTest (#9348) - - 45.0.13 Fix hardcoded precipitation in ClimateSettingsBuilder (#9402) - This effectively caused all biomes to have precipitation, such as - minecraft:desert. - Fixes #9397 - - 45.0.12 Fix incorrect variable used for swimming check (#9403) - Because of the incorrect variable, the check to stop sprinting (and stop - swimming) never fired correctly. - 1.19.3's `flag5` variable was renamed to `flag7` in 1.19.4; however, - this was not caught during patching because of the fuzzy patcher. - Fixes #9399 - - 45.0.11 Fix incorrect boolean used for glint effect (#9401) - The `flag1` variable is ultimately controlled by whether the armor slot - being rendered is for the leggings, which explains this bug where the - leggings always had the enchantment glint but not any other armor piece. - Fixes #9394 - - 45.0.10 Fixed ModMismatchDisconnectedScreen displaying missing mods wrongly (#9398) - - 45.0.9 Fix misaligned text render type patch (#9391) - - 45.0.8 Remove thread filter from processing clientside custom payloads. Closes @9390 - - 45.0.7 Fix LivingEntity patch which caused crash while entities got hurt. Closes #9389 - - 45.0.6 Fix wrong parameters in `Screen#renderTooltipInternal` patch (#9387) - - 45.0.5 Fix misaligned patch in LevelRenderer. Closes #9385 - - 45.0.4 Remove our fix for MC-121048 as it has been fixed by Vanilla (#9381) - - 45.0.3 Fix advancements not loading, bug seems to be fixed by vanilla now. Closes #9384 - - 45.0.2 Fixed patch verifier for cases where patches lowered access levels. Closes #9383 - - 45.0.1 Fix crouching while sprinting stopping the player when step height is modified. Closes #9376 - - 45.0.0 Forge 1.19.4 - Properly move `ServerStatusPing` to codec - Reimplement custom display contexts - Co-authored-by: Matyrobbrt - Co-authored-by: coehlrich - -44.1 -==== - - 44.1.23 Fix experimental world warning screen appearing everytime (#9375) - - 44.1.22 Fix continuing to use items after dropping or when a shield breaks (MC-231097, MC-168573) (#9344) - - 44.1.21 Add onStopUsing hook to IForgeItem (#9343) - - 44.1.20 Document RegisterParticleProvidersEvent's APIs (#9346) - - 44.1.19 Fix incorrect ListTag.getLongArray result (MC-260378) (#9351) - - 44.1.18 Fix missing patch that left TagBuilder#replace unused (#9354) - - 44.1.17 Add 2 new RenderLevelStageEvent.Stage for After Entities and After Block Entities (#9259) - - 44.1.16 Cleanup StemBlock Patch (#9337) - - 44.1.15 Cleanup ItemProperties patch (#9332) - - 44.1.14 Make IForgeIntrinsicHolderTagAppender methods properly chainable (#9331) - - 44.1.13 Fix in custom fluids not respecting max height correctly. (#9319) - - 44.1.12 Fix inconsistent vaporization in BucketItem & FluidType (#9269) - - 44.1.11 Fix reloading event firing during server shutdown and add explicit unloading event instead (#9016) - - 44.1.10 Homogenize and/or holdersets when serializing to prevent serializing to NBT from crashing (#9048) Fixes #9043 - - 44.1.9 [1.19.x] Fix `ForgeSlider` not respecting custom height (#9237) - - 44.1.8 Fix stepsound for blocks in the inside_step_sound_blocks tag. (#9318) - - 44.1.7 Fix missing hanging sign material for modded wood type (#9303) - - 44.1.6 Fire TickEvent.LevelTickEvent on ClientLevel tick (#9299) - - 44.1.5 Add ClientChatReceivedEvent for system messages (#9284) - - 44.1.4 PR Action update (#9274) - - 44.1.3 fix HangingSignEditScreen crash when using custom wood types using modid (#9294) - - 44.1.2 Bump SecureJarHandler version, to help identify invalid mods. - - 44.1.1 [1.19.3] Hotfix missing null check in createUnbakedItemElements (#9285) - - 44.1.0 Mark 1.19.3 Recommended Build - -44.0 -==== - - 44.0.49 [1.19.3] Allow Item and Elements models to specify static color, sky light, and block light values. (#9106) - - 44.0.48 Fix StemBlock not checking canSustainPlant for the correct block, it now checks for Melons/Pumpkins instead of the stem itself. (#9270) - - 44.0.47 Add github shared actions for automation purposes. (#9251) - - 44.0.46 Add translate key for Forge pack.mcmeta description (#9260) - - 44.0.45 Fix broken link for update checker docs in mdk (#9271) - - 44.0.44 Remove duplicate updateNeighbourForOutputSignal call Fixes #9169 (#9234) - - 44.0.43 Add helper methods to access the set of loaded sprite locations (#9223) - - 44.0.42 Disable guiLight3d for generated item models (#9230) - - 44.0.41 Remove resource caching (#9254) - - 44.0.40 Add TradeWithVillagerEvent (#9244) - - 44.0.39 Update link for Parchment "Getting Started" (#9243) - - 44.0.38 Allows DatapackBuiltinEntriesProvider to datagen LevelStems (#9247) - - 44.0.37 Add a method to LootContext.Builder that allows changing the queried loot table id (#9084) - - 44.0.36 [1.19.3] Fix Datagen Tests and Providers (#9212) - - 44.0.35 Fix concrete powder not being hydrated by singular water sources (#9236) - - 44.0.34 [1.19.3] Fix LootTableLoadEvent not getting fired (#9239) - - 44.0.33 Allow using custom factories in button builders (#9238) - - 44.0.32 Fix logspam when a root resource is requested from DelegatingPackResources, fixes #9197 (#9227) - - 44.0.31 [1.19.3] Fix `retrieveRegistryLookup` attempting to get the registry lookup from a `HolderGetter` (#9225) - - 44.0.30 [1.19.3] Add ability to datagen forge specific values in pack.mcmeta (#9221) - Co-authored-by: sciwhiz12 - - 44.0.29 Add block atlas config to register forge:white texture (#9187) - - 44.0.28 Fix ExtendedButton not being highlighted when focused (#9144) - - 44.0.27 Separate checkAndFix from the check* tasks. (#9213) - - 44.0.26 Fix forge resources overriding vanilla ones (#9222) - - 44.0.25 Fix tooltip customization not working for creative inventory (#9218) - - 44.0.24 Fix glowing item frame entity's texture (#9126) - Fixes #9123 - - 44.0.23 Fix datapack registries not being synced to clients (#9219) - - 44.0.22 Fix creatives tabs rendering overlapping tabs if the selected tab isn't on the current page. (#9214) - - 44.0.21 Fix `SidedInvWrapper` not accounting for vanilla stacking special cases in brewing stands and furnaces (#9189) - - 44.0.20 Update to the latest JarJar. (#9217) - - 44.0.19 Specify NetworkHooks#getEntitySpawningPacket Generic Return Type (#9220) - - 44.0.18 Fix using a DeferredRegister on a non-forge wrapped registry. Closes #9199 - - 44.0.17 Add support for custom CreativeModeTab implementations (#9210) - - 44.0.16 Simplify tree grower patches (#9209) - - 44.0.15 Replace AdvancementProvider patch with Forge helper (#9188) - - 44.0.14 Allow using `PackOutput`s in Forge-added datagen classes (#9182) - - 44.0.13 Add simpleBlockWithItem for data gens (#9170) - - 44.0.12 Fix running test mods (#9211) - - 44.0.11 [1.19.3] Fix models nested in custom geometries not resolving parents (#9200) - - 44.0.10 Fix OBJ Loader caches not being thread-safe. (#9204) - - 44.0.9 [1.19.3] Add event before baked models are cached by the BlockModelShaper (#9190) - - 44.0.8 Fix compatibility checker task configuration (#9202) - - 44.0.7 Fix chat offset (#9184) - - 44.0.6 Redesign CreativeTab collection event to be a lot more straight forward. (#9198) - - 44.0.5 Move ICondition patch placement to before MC throws an error. - Disable the explicitly erroring test biome modifier. - - 44.0.4 Fix BlockStateProvider not waiting for models before finishing. (#9196) Fixes #9195: - - 44.0.3 Fix tooltips not rendering on screens. Closes #9191 - - 44.0.2 Fix merged mod resource pack not returning all resources with the same name when asked. Closes #9194 - - 44.0.1 Fix searching using the wrong prefix for items or tags. Fixes #9176 Fixes #9179 (#9177) - - 44.0.0 Forge 1.19.3 - Created a CreativeModeTabEvent to register creative mode tabs and populate entries per tab - Moved datapack registries to DataPackRegistryEvent.NewRegistry event instead of tying them to ForgeRegistry - Made it easier for mods to datagen datapack builtin entries with DatapackBuiltinEntriesProvider - Provided access to lookupProvider for datagen - Updated dependencies to match versions used by vanilla and update JarJar to 0.3.18 - Added a test mod for the new CreativeModeTabEvent - Throws better error message for Forge registries in tag datagen - Deleted ForgeRegistryTagsProvider - Updated ClientChatReceivedEvent and ServerChatEvent for Mojang changes - Added patches for both sign related methods in ModelLayers - Changed RegisterShadersEvent to use ResourceProvider - Migrated old Mojang math types to JOML - Co-authored-by: Marc Hermans - Co-authored-by: LexManos - Co-authored-by: sciwhiz12 - Co-authored-by: coehlrich - -43.2 -==== - - 43.2.0 43.2 Recommended Build. - -43.1 -==== - - 43.1.65 Allow discovering services from the mods folder that use java's modular definition. (#9143) - - 43.1.64 Make Datapack Registries support ICondition(s) (#9113) - - 43.1.63 Enable additional build types to handle pull request validation. (#9159) - - 43.1.62 Check source permission level before selector permission (#9147) - In some situations, such as execution of a function by an advancement as - part of its reward, a command source stack may have a backing source of - a ServerPlayer which may lack the entity selector permission and have an - explicit permission level that should allow the use of entity selectors, - through CommandSourceStack#withPermission. - We now check if the permission level of the command source stack is - sufficient for entity selectors _before_ checking if the source is a - player and if they have the requisite permission. - This means that an operator permission level of 2 will always override - the Forge entity selector permission. - Fixes #9137 - - 43.1.61 Fix fires spreading too/igniting custom portal frames. (#9142) - - 43.1.60 Add supplier to FlowerBlock so it works with custom MobEffects (#9139) - - 43.1.59 Fix some logical bugs related to the Grindstone Event (#9089) - - 43.1.58 Call baked model's `getModelData` before `getRenderTypes` (#9163) - - 43.1.57 Make Util.memoize thread-safe (#9155) - - 43.1.56 Rendering tweaks and fixes: Part 4 (#9065) - - 43.1.55 Fix `Transformation` loading `PoseStack` (#9083) - - 43.1.54 Add simple block appearance API (#9066) - - 43.1.53 Fix invalidated modded packets when on LAN (#9157) - - 43.1.52 Improve extensibility of DetectorRailBlock and PoweredRailBlock (#9130) - - 43.1.51 Fix launch handler minecraft classpath locator (#9120) - - 43.1.50 Add HitResult to `EntityTeleportEvent$EnderPearl` (#9135) - - 43.1.49 Throw aggregate exception for erroneous registry event dispatch (#9111) - This means that exceptions occurring during the dispatch of the registry - events, such as those from the suppliers of RegistryObjects, properly - cause a crash rather than merely being logged and allowing the game to - reach the main menu. - Fixes #8720 - - 43.1.48 Add missing semi-colon near the Dist import statement in example mod. - - 43.1.47 Fix ClientModEvents example not subscribing to client-sided events (#9097) - - 43.1.46 Use GitHub action to lock issues with the `spam` label (#9087) - - 43.1.45 Remove structures slave map to Feature registry (#9091) - - 43.1.44 Improve logging of missing or unsupported dependencies (#9104) - - 43.1.43 [1.19.x] Fix ValueSpec caching the return value incorrectly (#9046) - - 43.1.42 [1.19.x] Add event for registering spawn placements, and modifying existing (#9024) - - 43.1.41 [1.19.x] Add event for items being stacked or swapped in a GUI. (#9050) - - 43.1.40 [1.19.x] Fix PlayerInteractEvent.EntityInteractSpecific not cancelling on a server (#9079) - - 43.1.39 Fix canceling phantom spawns preventing any further attempts that tick. (#9041) - - 43.1.38 Rename fluid type milk translation keys (#9077) - - 43.1.37 Fix minecart speed with water (#9076) - - 43.1.36 Add a cancellable event that gets fired when a Totem of Undying is used (#9069) - - 43.1.35 Fix performance issue and logging when resource caching is enabled (#9029) - - 43.1.34 Fix NPE when feeding wolves and cats (#9074) - - 43.1.33 Fix logically breaking change to ForgeConfigSpec.Builder#comment where modders could not add a empty line to the start of comments. (#9061) - - 43.1.32 Fix ServiceLoader bug - - 43.1.31 Fix ClientChatReceivedEvent for system messages - - 43.1.30 Make ForgeConfigSpec$Builder.comment able to be called multiple times for the same entry. (#9056) - - 43.1.29 Fix control modifier for mac with `KeyMapping`s using Alt instead of Super (#9057) - - 43.1.28 Fix is_desert tag not being applied correctly. (#9051) - - 43.1.27 Fix mob griefing event for SmallFireballs not using owner entity. (#9038) - - 43.1.26 Fix minecarts on rails not properly slowing down in water (#9033) - - 43.1.25 Change codestyle for BookShelves tag. Closes #9027 - Add IS_CAVE tag Closes #8885 - Add IS_DESERT tag Closes #8979 - Simplify Mangrove Swamp tags Closes #8980 - - 43.1.24 Allow faces of an "elements" model to have disabled ambient occlusion (#9019) - - 43.1.23 [1.19.x] Recipe ID-based grouping between modded and vanilla recipes. (#8876) - - 43.1.22 Update fence_gates/wooden (#8936) - - 43.1.21 [1.19.x] Added event for growing fungus (#8981) - - 43.1.20 Added Bookshelves block tag (#8991) - - 43.1.19 Create a Forge EntityType Tag for Bosses (#9017) - - 43.1.18 Allow mods to specify shader import namespace (#9021) - - 43.1.17 Grindstone Events (#8934) - One to modify the output, and one to modify the input. - - 43.1.16 Fix the serialized names of the enum (#9014) - - 43.1.15 Fix `tryEmptyContainerAndStow` duping fluids with stackable containers (#9004) - - 43.1.14 Add mod mismatch event (#8989) - - 43.1.13 [1.19.x] add methods with more context to tree growers (#8956) - - 43.1.12 [1.19.X] Adding more precise events for Advancements (#8360) - - 43.1.11 Default IItemHandler capability for shulker box itemstacks (#8827) - Co-authored-by: LexManos - - 43.1.10 [1.19] Add hook for items to remain in the hotbar when picking blocks/entities (#8872) - - 43.1.9 [1.19.x] Block Model Builder Root Transform Support (#8860) - Co-authored-by: sciwhiz12 - - 43.1.8 [1.19.x] Make LivingSetAttackTargetEvent compatible with the Brain/Behavior system. (Port of PR #8918) (#8954) - - 43.1.7 [1.19.x] Add IForgeBlock#onTreeGrow to replace IForgeBlock#onPlantGrow from 1.16 (#8999) - - 43.1.6 [1.19.x] Moved Player.resetAttackStrengthTicker() to the end of Player.attack() (#9000) - - 43.1.5 fix misplaced patch in sapling block (#9005) - - 43.1.4 Fix failed entity interactions consuming the click. (#9007) - - 43.1.3 Fix entity selector permission check to check original source (#8995) - Permission checks should be against the command source and not the - target entity, as is done in vanilla. - Fixes #8994 - - 43.1.2 Hotfix for 1.19.2 item animation bug (#8987) - * [HOT FIX]: Fixes #8985 by no-oping for vanilla models instead of throwing error - - 43.1.1 Add ability to Auto register capabilities via annotation (#8972) - - 43.1.0 1.19.2 RB - -43.0 -==== - - 43.0.22 Added ItemDecorator API (#8794) - - 43.0.21 [1.19.x] Custom usage animations for items (#8932) - - 43.0.20 Allow registering custom `ColorResolver`s (#8880) - - 43.0.19 [1.19] Allow custom outline rendering on EntityRenderers and BlockEntityRenderers (#8938) - - 43.0.18 Redirect checks for entity selector use to a permission (#8947) - This allows greater flexibility for configuring servers with - operator-like permissions to user groups through the permissions API and - their permissions handler of choice without needing to grant the - vanilla operator permission to any player. - The new permission is "forge:use_entity_selectors", which is granted by - default to players with permission level 2 (GAMEMASTERS) and above. - The hook falls back to checking the permission level if the source of - the command is not a ServerPlayer, such as for command blocks and - functions. - - 43.0.17 Allow FakePlayer to report its position (#8963) - - 43.0.16 Add alternate version of renderEntityInInventory to allow for directly specifying the angles (#8961) - - 43.0.15 Add cancellable ToastAddEvent (#8952) - - 43.0.14 Modify ScreenEvent.RenderInventoryMobEffects to allow moving the effect stack left or right (#8951) - - 43.0.13 Fix Enchantment#doPostHurt and Enchantment#doPostAttack being called twice for players. Fixes MC-248272 (#8948) - - 43.0.12 Remove reflective implementation of ICustomPacket. (#8973) - Make vanilla custom packets able to be sent multiple times. Closes #8969 - - 43.0.11 Filter name spaces to directories only. Closes #8413 - - 43.0.10 Fix a corner case where the UMLB can not extract a version from a library. (#8967) - - 43.0.9 Fix worlds with removed dimension types unable to load. (#8959) Closes #8800 - - 43.0.8 Fix issue where unknown chunk generators would cause DFU to fail. (#8957) - - 43.0.7 Fix comments and documentation that were missed during the review of #8712 (#8945) - - 43.0.6 Make AnvilUpdateEvent fire even if the second input is empty, which means it fires even if only changing the item name. (#8905) - - 43.0.5 Fix `LivingEntity#isBlocking` to use `ToolActions#SHIELD_BLOCK` instead of `UseAnim#BLOCK` (#8933) - - 43.0.4 Add Custom HolderSet Types allowing for logical combining of sets. (#8928) - - 43.0.3 Add values to VersionSupportMatrix to support loading mods that restrict versions to 1.19.1 on 1.19.2 (#8946) - - 43.0.2 Fix certain particles not updating their bounding box when their position changes (#8925) - - 43.0.1 Update EventBus to address concurrency issue in ModLauncher Factory. Closes #8924 - - 43.0.0 1.19.2 - -42.0 -==== - - 42.0.9 Remove calls to getStepHeight in Player#maybeBackOffFromEdge (#8927) - - 42.0.8 Add forge tags for tools and armors, these DO NOT replace ToolActions, and are designed just for Recipes. (#8914) - - 42.0.7 Add Biomes.BEACH to Tags (#8892) - - 42.0.6 Let NetworkInstance.isRemotePresent check minecraft:register for channel IDs. (#8921) - - 42.0.5 Add an event for when the chunk ticket level is updated (#8909) - - 42.0.4 Re-add PotentialSpawns event (#8712) - - 42.0.3 Fix misplaced patch in ItemEntityRenderer breaking ItemEntityRenderer#shouldBob() (#8919) - - 42.0.2 [1.19] [HotFix] Fix the dedicated server not having access to the JiJ filesystems. (#8931) - - 42.0.1 Match Mojang's action bar fix for MC-72687 (#8917) - - 42.0.0 Forge 1.19.1 - Load natives from classpath - Make command argument types a forge registry - Add `EntityMobGriefingEvent` to `Allay#wantsToPickUp` - Overhaul `ServerChatEvent` to use `ChatDecorator` system - Remove `ClientChatEvent#setMessage` for now - Gradle 7.5 - -41.1 -==== - - 41.1.0 Mark 1.19 RB - -41.0 -==== - - 41.0.113 Allow faces of an "elements" model to be made emissive (#8890) - - 41.0.112 Fix invalid channel names sent from the server causing the network thread to error. (#8902) - - 41.0.111 Fix PlayerEvent.BreakSpeed using magic block position to signify invalid position. Closes #8906 - - 41.0.110 Fix cases where URIs would not work properly with JarInJar (#8900) - - 41.0.109 Add new hook to allow modification of lightmap via Dimension special effects (#8863) - - 41.0.108 Fix Forge's packet handling on play messages. (#8875) - - 41.0.107 Add API for tab list header/footer (#8803) - - 41.0.106 Allow modded blocks overriding canStickTo prevent sticking to vanilla blocks/other modded blocks (#8837) - - 41.0.105 Multiple tweaks and fixes to the recent changes in the client refactor PR: Part 3 (#8864) - Fix weighted baked models not respecting children render types - Allow fluid container model to use base texture as particle - Fix inverted behavior in composite model building. Fixes #8871 - - 41.0.104 Fix crossbows not firing ArrowLooseEvent (#8887) - - 41.0.103 Add User-Agent header to requests made by the update checker (#8881) - Format: Java-http-client/ MinecraftForge/ / - - 41.0.102 Output the full path in a crash report so it is easier to find the outer mod when a crash in Jar-In-Jar occurs. (#8856) - - 41.0.101 Clean up the pick item ("middle mouse click") patches (#8870) - - 41.0.100 [1.19.x] Hotfix for test mods while the refactor is ongoing - - 41.0.99 add event to SugarCaneBlock (#8877) - - 41.0.98 Fix Global Loot Modifiers not using Dispatch Codec (#8859) - - 41.0.97 Allow block render types to be set in datagen (#8852) - - 41.0.96 Fix renderBreakingTexture not using the target's model data (#8849) - - 41.0.95 Multiple tweaks and fixes to the recent changes in the client refactor PR: Part 2 (#8854) - * Add getter for the component names in an unbaked geometry - * Fix render type hint not being copied in BlockGeometryBakingContext - * Ensure BlockRenderDispatches's renderSingleBlock uses the correct buffer - - 41.0.94 [1.19.x] Apply general renames, A SRG is provided for modders. (#8840) - See https://gist.github.com/SizableShrimp/882a671ff74256d150776da08c89ef72 - - 41.0.93 Fix mob block breaking AI not working correctly when chunk 0,0 is unloaded. Closes #8853 - - 41.0.92 Fix crash when breaking blocks with multipart models and remove caching. Closes #8850 - - 41.0.91 Fixed `CompositeModel.Baked.Builder.build()` passing arguments in the wrong order (#8846) - - 41.0.90 Make cutout mipmaps explicitly opt-in for item/entity rendering (#8845) - * Make cutout mipmaps explicitly opt-in for item/entity rendering - * Default render type domain to "minecraft" in model datagens - - 41.0.89 Fixed multipart block models not using the new model driven render type system. (#8844) - - 41.0.88 Update to the latest JarJar to fix a collision issue where multiple jars could provide an exact match. (#8847) - - 41.0.87 Add FML config to disable DFU optimizations client-side. (#8842) - * Add client-side command line argument to disable DFU optimizations. - * Switch to using FMLConfig value instead. - - 41.0.86 [1.19] Fixed broken BufferBuilder.putBulkData(ByteBuffer) added by Forge (#8819) - * Fixes BufferBuilder.putBulkData(ByteBuffer) - * use nextElementByte - * Fixed merge conflict - - 41.0.85 [1.19.x] Fix shulker boxes allowing input of items, that return false for Item#canFitInsideContainerItems, through hoppers. (#8823) - * Make ShulkerBoxBlockEntity#canPlaceItemThroughFace delegate to Item#canFitInsideContainerItems. - * Switch to using Or and add comment. - * Switch Or to And. - - 41.0.84 [1.19.x] Added RenderLevelStageEvent to replace RenderLevelLastEvent (#8820) - * Ported RenderLevelStageEvent from 1.18.2 - * Updated to fix merge conflicts - - 41.0.83 [1.19.x] Fix door datagenerator (#8821) - * Fix door datagenerator - Fix datagenerator for door blocks. Successor to #8687, addresses comments made there about statement complexity. - * Fix extra space around parameter - Fix extra space before comma around a parameter. - - 41.0.82 Create PieceBeardifierModifier to re-enable piecewise beardifier definitions (#8798) - - 41.0.81 Allow blocks to provide a dynamic MaterialColor for display on maps (#8812) - - 41.0.80 [1.19.x] BiomeTags Fixes/Improvements (#8711) - * dimension specific tag fix - * remove forge:is_beach cause vanilla has it already - * remove forge tags for new 1.19 vanilla tags (savanna, beach, overworld, end) - Co-authored-by: Flemmli97 - - 41.0.79 1.19 - Remove GlobalLootModifierSerializer and move to Codecs (#8721) - * convert GLM serializer class to codec - * cleanup - * GLM list needs to be sorted - * datagen - * simplify serialization - * fix test mods (oops) - * properly use suppliers for codec as they are registry obj - - 41.0.78 Implement item hooks for potions and enchantments (#8718) - * Implement item hooks for potions and enchantments - * code style fixes - - 41.0.77 Re-apply missing patch to ServerLevel.EntityCallbacks#onTrackingEnd() (#8828) - - 41.0.76 Double Bar Rendering fixed (#8806) (#8807) - * Double Bar Rendering fixed (#8806) - * Added requested changes by sciwhiz12 - - 41.0.75 Multiple tweaks and fixes to the recent changes in the client refactor PR (#8836) - * Add an easy way to get the NamedGuiOverlay from a vanilla overlay - * Fix static member ordering crash in UnitTextureAtlasSprite - * Allow boss bar rendering to be cancelled - * Make fluid container datagen use the new name - - 41.0.74 Add FogMode to ViewportEvent.RenderFog (#8825) - - 41.0.73 Provide additional context to the getFieldOfView event (#8830) - - 41.0.72 Pass renderType to IForgeBakedModel.useAmbientOcclusion (#8834) - - 41.0.71 Load custom ITransformationServices from the classpath in dev (#8818) - * Add a classpath transformer discoverer to load custom transformation services from the classpath - * Update ClasspathTransformerDiscoverer to 1.18 - * Update license year - * Update license header - * Fix the other license headers - * Update ClasspathTransformerDiscoverer to 1.19 - - 41.0.70 Handle modded packets on the network thread (#8703) - * Handle modded packets on the network thread - - On the server we simply need to remove the call to - ensureRunningOnSameThread. - - On the client side, we now handle the packet at the very start of the - call. We make sure we're running from a network thread to prevent - calling the handling code twice. - While this does mean we no longer call .release(), in practice this - doesn't cause any leaks as ClientboundCustomPayloadPacket releases - for us. - * Clarify behaviour a little in the documentation - * Javadoc formatting - * Add a helper method for handling packets on the main thread - Also rename the network thread one. Should make it clearer the expected - behaviour of the two, and make it clearer there's a potentially breaking - change. - * Add back consumer() methods - Also document EventNetworkChannel, to clarify the thread behaviour - there. - * Add since = "1.19" to deprecated annotations - - 41.0.69 Cache resource listing calls in resource packs (#8829) - * Make the resource lookups cached. - * Include configurability and handle patch cleanup. - * Document and comment the cache manager. - * Make thread selection configurable. - * Implement a configurable loading mechanic that falls back to default behaviour when the config is not bound yet. - * Use boolean supplier and fix wildcard import. - * Clean up the VPR since this is more elegant. - * Clean up the VPR since this is more elegant. - * Address review comments. - * Address more review comments. - * Fix formatting on `getSource` - * Address comments by ichtt - * Adapt to pups requests. - * Stupid idea. - * Attempt this again with a copy on write list. - * Fix a concurrency and loading issue. - * Fix #8813 - Checks if the paths are valid resource paths. - * Move the new methods on vanilla Patch. - - 41.0.68 Update SJH and JIJ - - 41.0.67 Fix #8833 (#8835) - - 41.0.66 Fix backwards fabulous check in SimpleBakedModel (#8832) - Yet another blunder we missed during the review of #8786. - - 41.0.65 Make texture atlas in StandaloneGeometryBakingContext configurable (#8831) - - 41.0.64 [1.19.X] Client code cleanup, updates, and other refactors (#8786) - * Revert "Allow safely registering RenderType predicates at any time (#8685)" - This reverts commit be7275443fd939db9c58bcad47079c3767789ac1. - * Renderable API refactors - - Rename "render values" to "context" - - Rename SimpleRenderable to CompositeRenderable to better reflect its use - - Remove IMultipartRenderValues since it doesn't have any real use - - Add extensive customization options to BakedModelRenderable - * ClientRegistry and MinecraftForgeClient refactors - - Add sprite loader manager and registration event - - Add spectator shader manager and registration event - - Add client tooltip factory manager and registration event - - Add recipe book manager and registration event - - Add key mapping registration event - - Remove ClientRegistry, as everything has been moved out of it - - Remove registration methods from MinecraftForgeClient, as they have dedicated events now - * Dimension special effects refactors - - Fold handlers into an extension class and remove public mutable fields - - Add dimension special effects manager and registration event - * HUD overlay refactors - - Rename to IGuiOverlay match vanilla (instead of Ingame) - - Add overlay manager and registration event - - Move vanilla overlays to a standalone enum - * Model loader refactors - - Rename IModelLoader to IGeometryLoader - - Add loader manager and registration event - - Fold all model events into one - - Move registration of additionally loaded models to an event - - Remove ForgeModelBakery and related classes as they served no purpose anymore - * Render properties refactors - - Rename all render properties to client extensions and relocate accordingly - - Move lookups to the respective interfaces - * Model data refactors - - Convert model data to a final class backed by an immutable map and document mutability requirements. This addresses several thread-safety issues in the current implementation which could result in race conditions - - Transfer ownership of the data manager to the client level. This addresses several issues that arise when multiple levels are used at once - * GUI and widget refactors - - Move all widgets to the correct package - - Rename GuiUtils and children to match vanilla naming - * New vertex pipeline API - - Move to vanilla's VertexConsumer - - Roll back recent PR making VertexConsumer format-aware. This is the opposite of what vanilla does, and should not be relevant with the updated lighting pipeline - * Lighting pipeline refactors - - Move to dedicated lighting package - - Separate flat and smooth lighters - - Convert from a vertex pipeline transformer to a pure vertex source (input is baked quads) - * Model geometry API refactors - - Rename IModelGeometry to IUnbakedGeometry - - Rename IModelConfiguration to IGeometryBakingContext - - Rename other elements to match vanilla naming - - Remove current changes to ModelState, as they do not belong there. Transforms should be specified through vanilla's system. ModelState is intended to transfer state from the blockstate JSON - - Remove multipart geometries and geometry parts. After some discussion, these should not be exposed. Instead, geometries should be baked with only the necessary parts enabled - * Make render types a first-class citizen in baked models - - Add named render types (block + entity + fabulous entity) - - Add named render type manager + registration event - - Make BakedModel aware of render types and transfer control over which ones are used to it instead of ItemBlockRenderTypes (fallback) - - (additional) Add concatenated list view. A wrapper for multiple lists that iterates through them in order without the cost of merging them. Useful for merging lists of baked quads - * General event refactors - - Several renames to either match vanilla or improve clarity - - Relocate client chat event dispatching out of common code - * Forge model type refactors - - Rename SeparatePerspectiveModel to SeparateTransformsModel - - Rename ItemModelMesherForge to ForgeItemModelShaper - - Rename DynamicBucketModel to DynamicFluidContainerModel - - Prefix all OBJ-related classes with "Obj" and decouple parsing from construction - - Extract ElementsModel from model loader registry - - Add EmptyModel (baked, unbaked and loader) - - Refactor CompositeModel to take over ItemMultiLayerBakedModel - - Remove FluidModel as it's not used and isn't compatible with the new fluid rendering in modern versions - - Move model loader registration to a proper event handler - - Update names of several JSON fields (backwards-compatible) - - Update datagens to match - * Miscellaneous changes and overlapping patches - - Dispatch all new registration events - - Convert ExtendedServerListData to a record - - Add/remove hooks from ForgeHooksClient as necessary - * Update test mods - * Fix VertexConsumerWrapper returning parent instead of itself - * Additional event cleanup pass - As discussed on Discord: - - Remove "@hidden" and "@see " javadoc annotations from all client events and replace them with @ApiStatus.Internal annotation - - Make all events that shouldn't be fired directly into abstract classes with protected constructors - - Another styling pass, just in case (caught some missed classes) - * Add proper deprecation javadocs and de-dupe some vertex consumer code - * Replace sets of chunk render types with a faster BitSet-backed collection - This largely addresses potential performance concerns that using a plain HashSet might involve by making lookups and iteration as linear as they can likely be (aside from using a plain byte/int/long for bit storage). Further performance concerns related to the implementation may be addressed separately, as all the implementation details are hidden from the end user - * Requested changes - - Remove MinecraftForgeClient and move members to Minecraft, IForgeMinecraft and StencilManager - - Allow non-default elements to be passed into VertexConsumer and add support to derived classes - - Move array instantiation out of quad processing in lighting pipeline - - Fix flipped fluid container model - - Set default UV1 to the correct values in the remapping pipeline - - Minor documentation changes - * Add/update EXC entries and fix AT comment - * Add test mod as per Orion's request - * Additional requested changes - * Allow custom model types to request the particle texture to be loaded - * Even more requested changes - * Improve generics in ConcatenatedListView and add missing fallbacks - * Fix fluid render types being bound to the fluid and not its holder - * Remove non-contractual nullability in ChunkRenderTypeSet and add isEmpty - Additionally, introduce chunk render type checks in ItemBlockRenderTypes - Co-authored-by: Dennis C - - 41.0.63 Implement full support for IPv6 (#8742) - - 41.0.62 Fix certain user-configured options being overwritten incorrectly due to validators. (#8780) - - 41.0.61 Allow safely registering RenderType predicates at any time (#8685) - - 41.0.60 Fix crash after loading error due to fluid texture gathering and config lookup (#8802) - - 41.0.59 Remove the configuration option for handling empty tags in ingredients. (#8799) - Now empty tags are considered broken in all states. - - 41.0.58 Fix MC-105317 Structure blocks do not rotate entities correctly when loading (#8792) - - 41.0.57 Fire ChunkWatchEvents after sending packets (#8747) - - 41.0.56 Add item handler capability to chest boats (#8787) - - 41.0.55 Add getter for correct BiomeSpecialEffectsBuilder to BiomeInfo$Builder (#8781) - - 41.0.54 Fix BlockToolModificationEvent missing cancelable annotation (#8778) - - 41.0.53 Fix ticking chunk tickets from forge's chunk manager not causing chunks to fully tick (#8775) - - 41.0.52 Fix default audio device config loading string comparison issue (#8767) - - 41.0.51 Fix missed vanilla method overrides in ForgeRegistry (#8766) - - 41.0.50 Add MinecraftServer reference to ServerTickEvent (#8765) - - 41.0.49 Fix TagsProviders for datapack registries not recognizing existing files (#8761) - - 41.0.48 Add callback after a BlockState was changed and the neighbors were updated (#8686) - - 41.0.47 Add biome tag entries for 1.19 biomes (#8684) - - 41.0.46 Make fishing rods use tool actions for relevant logic (#8681) - - 41.0.45 Update BootstrapLauncher to 1.1.1 and remove the forced - merge of text2speech since new BSL does it. - - 41.0.44 Merge text2speech libs together so the natives are part of the jar - - 41.0.43 Make Forge ConfigValues implement Supplier. (#8776) - - 41.0.42 Fix merge derp in AbstractModProvider and logic derp in ModDiscoverer - - 41.0.41 Add "send to mods in order" method to ModList and use it (#8759) - * Add "send to mods in order" method to ModList and use it in RegistryEvents and DataGen.. - * Also preserve order in runAll - * Do better comparator thanks @pupnewfster - * postEvent as well. - - 41.0.40 Update SJH to 2.0.2.. (#8774) - * Update SJH to 2.0.3.. - - 41.0.39 Sanity check the version specified in the mod file (#8749) - * Sanity check the version specified in the mod file to - make sure it's compatible with JPMS standards for - version strings. - Closes #8748 - Requires SPI 6 - - 41.0.38 Fix SP-Devtime world loading crash due to missing server configs (#8757) - - 41.0.37 Remove ForgeWorldPreset and related code (#8756) - Vanilla has a working replacement. - - 41.0.36 Change ConfigValue#get() to throw if called before config loaded (#8236) - This prevents silent issues where a mod gets the value of the setting - before configs are loaded, which means the default value is always - returned. - As there may be situations where the getting the config setting before - configs are loaded is needed, and it is not preferable to hardcode the - default value, the original behavior is made available through #getRaw. - Implements and closes #7716 - * Remove getRaw() method - This is effectively replaced with the expression `spec.isLoaded() ? - configValue.get() : configValue.getDefault()`. - * Remove forceSystemNanoTime config setting - As implemented, it never had any effect as any place where the config - value would be queried happens before the configs are loaded. - - 41.0.35 Fix EnumArgument to use enum names for suggestions (#8728) - Previously, the suggestions used the string representation of the enum - through Enum#toString, which can differ from the name of the enum as - required by Enum#valueOf, causing invalid suggestions (both in gui and - through the error message). - - 41.0.34 Jar-In-Jar (#8715) - - 41.0.33 [1.19] Fix data-gen output path of custom data-pack registries (#8724) - - 41.0.32 Fix player dive and surface animations in custom fluids (#8738) - - 41.0.31 [1.19.x] Affect ItemEntity Motion in Custom Fluids (#8737) - - 41.0.30 [1.19] Add support for items to add enchantments without setting them in NBT (#8719) - - 41.0.29 [1.19.x] Add stock biome modifier types for adding features and spawns (#8697) - - 41.0.28 [1.19.x] Fluid API Overhaul (#8695) - - 41.0.27 Replace StructureSpawnListGatherEvent with StructureModifiers (#8717) - - 41.0.26 Use stack sensitive translation key by default for FluidAttributes. (#8707) - - 41.0.25 Delete LootItemRandomChanceCondition which added looting bonus enchantment incorrectly. (#8733) - - 41.0.24 Update EventBus to 6.0, ModLauncher to 10.0.1 and BootstrapLauncher to 1.1 (#8725) - - 41.0.23 Replace support bot with support action (#8700) - - 41.0.22 Fix Reach Distance / Attack Range being clamped at 6.0 (#8699) - - 41.0.21 [1.19.x] Fix mods' worldgen data not being loaded when creating new singleplayer worlds (#8693) - - 41.0.20 [1.19.x] Fix experimental confirmation screen (#8727) - - 41.0.19 Move is_mountain to forge's tag instead of vanilla's (#8726) - - 41.0.18 [1.19.x] Add CommandBuildContext to Register Command Events (#8716) - - 41.0.17 Only rewrite datagen cache when needed (#8709) - - 41.0.16 Implement a simple feature system for Forge (#8670) - * Implement a simple feature system for Forge. Allows mods to demand certain features are available in the loading system. An example for java_version is provided, but not expected to be used widely. This is more targeted to properties of the display, such as GL version and glsl profile. - Requires https://github.com/MinecraftForge/ForgeSPI/pull/13 to be merged first in ForgeSPI, and the SPI to be updated appropriately in build.gradle files. - * rebase onto 1.19 and add in SPI update - - 41.0.15 displayTest option in mods.toml (#8656) - * displayTest option in mods.toml - * "MATCH_VERSION" (or none) is existing match version string behaviour - * "IGNORE_SERVER_VERSION" accepts anything and sends special SERVERONLY string - * "IGNORE_ALL_VERSION" accepts anything and sends an empty string - * "NONE" allows the mod to supply their own displaytest using the IExtensionPoint mechanism. - * Update display test with feedback and added the mods.toml discussion in mdk. - - 41.0.14 Update forgeSPI to v5 (#8696) - - 41.0.13 Make IVertexConsumers such as the lighting pipeline, be aware of which format they are dealing with. (#8692) - Also fix Lighting pipeline ignoring the overlay coords from the block renderer. - - 41.0.12 Fixed misaligned patch to invalidateCaps in Entity (#8705) - - 41.0.11 Fix readAdditionalLevelSaveData (#8704) - - 41.0.10 Fixes setPos to syncPacketPositionCodec (#8702) - - 41.0.9 Fix wrong param passed to PlayLevelSoundEvent.AtEntity (#8688) - - 41.0.8 Override initialize in SlotItemHandler, so it uses the itemhandler instead of container (#8679) - - 41.0.7 Update MDK for 1.19 changes (#8675) - - 41.0.6 Add helper to RecipeType, and fix eclipse compiler error in test class. - - 41.0.5 Update modlauncher to latest (#8691) - - 41.0.4 Fix getting entity data serializer id crashing due to improper port to new registry system (#8678) - - 41.0.3 Fire registry events in the order vanilla registers to registries (#8677) - Custom registries are still fired in alphabetical order, after all vanilla registries. - Move forge's data_serializers registry to forge namespace. - - 41.0.2 Add method with pre/post wrap to allow setting/clearing mod context. (#8682) - Fixes ActiveContainer in ModContext not being present in registry events. Closes #8680 - - 41.0.1 Fix the Curlie oopsie - - 41.0.0 Forge 1.19 - * Bump pack.mcmeta formats - * 1.19 biome modifiers - * Mark ClientPlayerNetworkEvent.LoggedOutEvent's getters as nullable - * Add docs and package-info to client extension interfaces package - * Move RenderBlockOverlayEvent hooks to ForgeHooksClient - * Add package-infos to client events package - * Rename SoundLoadEvent to SoundEngineLoadEvent - This reduces confusion from consumers which may think the - name SoundLoadEvent refers to an individual sound being loaded rather - than the sound engine. - * Document and change SoundLoadEvent to fire on mod bus - Previously, it fired on both the mod bus and the Forge bus, which is - confusing for consumers. - * Delete SoundSetupEvent - Looking at its original implementation shows that there isn't an - appropriate place in the new sound code to reinsert the event, and the - place of 'sound engine/manager initialization event' is taken already by SoundLoadEvent. - * Perform some cleanup on client events - - Removed nullable annotations from ClientPlayerNetworkEvent - - Renamed #getPartialTicks methods to #getPartialTick, to be consistent - with vanilla's naming of the partial tick - - Cleanup documentation to remove line breaks, use the - spelling 'cancelled' over - 'canceled', and improve docs on existing and - new methods. - * Remove EntityEvent.CanUpdate - Closes MinecraftForge/MinecraftForge#6394 - * Switch to Jetbrains nullability annotations - * New PlayLevelSoundEvent; replaces old PlaySoundAtEntityEvent - * Remove ForgeWorldPresetScreens - * Remove IForgeRegistryEntry - * Remove use of List in FML's CompletableFutures - * Add docs to mod loading stages, stages, and phases - * Gradle 7.4.2 - * Use SLF4J in FMLLoader and other subprojects - * Switch dynamic versions in subprojects to pinned ones - * Switch ForgeRoot and MDK to FG plugin markers - * Configure Forge javadoc task - The task now uses a custom stylesheet with MCForge elements, and - configured to combine the generation from the four FML subprojects - (fmlloader, fmlcore, javafmllanguage, mclanguage) and the Forge project - into the javadoc output. - * Update docs/md files, for 1.19 update and the move away from IRC to Discord. - * Make "Potentially dangerous alternative prefix" a debug warning, not info. - Co-authored-by: Curle - Co-authored-by: sciwhiz12 - diff --git a/gradle.properties b/gradle.properties index fcc4fadb..f21babdd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,61 +1,14 @@ -# Sets default memory used for gradle commands. Can be overridden by user or command line properties. -# This is required to provide enough memory for the Minecraft decompilation process. -org.gradle.jvmargs=-Xmx3G -org.gradle.daemon=false - - -## Environment Properties - -# The Minecraft version must agree with the Forge version to get a valid artifact +org.gradle.jvmargs=-Xmx4G +org.gradle.daemon=true +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.configuration-cache=false minecraft_version=1.20.1 -# The Minecraft version range can use any release version of Minecraft as bounds. -# Snapshots, pre-releases, and release candidates are not guaranteed to sort properly -# as they do not follow standard versioning conventions. -minecraft_version_range=[1.20.1,1.21) -# The Forge version must agree with the Minecraft version to get a valid artifact -forge_version=47.4.0 -# The Forge version range can use any version of Forge as bounds or match the loader version range -forge_version_range=[47,) -# The loader version range can only use the major version of Forge/FML as bounds +minecraft_version_range=[1.20.1,1.20.2) +forge_version=47.3.10 loader_version_range=[47,) -# The mapping channel to use for mappings. -# The default set of supported mapping channels are ["official", "snapshot", "snapshot_nodoc", "stable", "stable_nodoc"]. -# Additional mapping channels can be registered through the "channelProviders" extension in a Gradle plugin. -# -# | Channel | Version | | -# |-----------|----------------------|--------------------------------------------------------------------------------| -# | official | MCVersion | Official field/method names from Mojang mapping files | -# | parchment | YYYY.MM.DD-MCVersion | Open community-sourced parameter names and javadocs layered on top of official | -# -# You must be aware of the Mojang license when using the 'official' or 'parchment' mappings. -# See more information here: https://github.com/MinecraftForge/MCPConfig/blob/master/Mojang.md -# -# Parchment is an unofficial project maintained by ParchmentMC, separate from Minecraft Forge. -# Additional setup is needed to use their mappings, see https://parchmentmc.org/docs/getting-started -mapping_channel=parchment -# The mapping version to query from the mapping channel. -# This must match the format required by the mapping channel. -mapping_version=2023.09.03-1.20.1 - -thermal_core_version=11.0.0.15 -thermal_core_range=[11.0,11.1) -## Mod Properties - -# The unique mod identifier for the mod. Must be lowercase in English locale. Must fit the regex [a-z][a-z0-9_]{1,63} -# Must match the String constant located in the main mod class annotated with @Mod. mod_id=useless_mod -# The human-readable display name for the mod. mod_name=Useless Mod -# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. -mod_license=MIT -# The mod version. See https://semver.org/ -mod_version=26.6.16-Forge1.20.1 -# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. -# This should match the base package used for the mod sources. -# See https://maven.apache.org/guides/mini/guide-naming-conventions.html +mod_version=20.2.4.6 mod_group_id=com.sorrowmist.useless -# The authors of the mod. This is a simple text string that is used for display purposes in the mod list. -mod_authors=YourNameHere, OtherNameHere -# The description of the mod. This is a simple multiline text string that is used for display purposes in the mod list. -mod_description=Example mod description.\nNewline characters can be used and will be replaced properly. -mekanism_version=1.20.1-10.4.16.80 +mod_license=MIT diff --git a/gradle/bytecode-contract-tests.gradle b/gradle/bytecode-contract-tests.gradle new file mode 100644 index 00000000..0c51c544 --- /dev/null +++ b/gradle/bytecode-contract-tests.gradle @@ -0,0 +1,25 @@ +// Compare release mixin signatures with the pinned native AE2 and AdvancedAE bytecode. +configurations { contractNative { transitive = false } } +dependencies { + contractNative 'maven.modrinth:XxWD5pD3:Zfyso1sP' + contractNative 'maven.modrinth:rxYaglEe:orFPvySe' +} +def contractInputs = layout.buildDirectory.file('contract-inputs.json') +tasks.register('writeContractInputs') { + inputs.files configurations.contractNative + outputs.file contractInputs + doLast { + def jars = configurations.contractNative.resolvedConfiguration.resolvedArtifacts + def paths = [ae2: jars.find { it.moduleVersion.id.name == 'XxWD5pD3' }.file.absolutePath, + advancedae: jars.find { it.moduleVersion.id.name == 'rxYaglEe' }.file.absolutePath] + contractInputs.get().asFile.text = groovy.json.JsonOutput.toJson(paths) + } +} +tasks.register('bytecodeContractTest', Exec) { + dependsOn 'jar', 'reobfJar', 'writeContractInputs' + workingDir projectDir + commandLine providers.gradleProperty('pythonExecutable').getOrElse( + System.getProperty('os.name').toLowerCase().contains('windows') ? 'python' : 'python3'), + '-m', 'unittest', 'discover', '-s', 'src/test/python', '-v' +} +tasks.named('check') { dependsOn 'bytecodeContractTest' } diff --git a/gradle/forge1201-pack-profile.gradle b/gradle/forge1201-pack-profile.gradle new file mode 100644 index 00000000..8c40efa0 --- /dev/null +++ b/gradle/forge1201-pack-profile.gradle @@ -0,0 +1,11 @@ +// Optional integrations without a Forge 1.20.1 implementation remain outside this build. +def profile = new groovy.json.JsonSlurper().parse(file('gradle/forge1201-pack-profile.json')) +sourceSets.main.java { profile.excludes.each { exclude it } } +tasks.named('processResources', ProcessResources) { + exclude 'META-INF/neoforge.mods.toml', 'META-INF/enumextensions.json' + // These blocks are not registered without NeoEcoAE in the target profile. + exclude 'data/useless_mod/loot_tables/blocks/useless_compact_*.json' +} + +// These tests require the NeoForge-only NeoEcoAE integration excluded above. +sourceSets.test.java.exclude 'com/sorrowmist/useless/compat/neoecoae/**' diff --git a/gradle/forge1201-pack-profile.json b/gradle/forge1201-pack-profile.json new file mode 100644 index 00000000..3c4d8479 --- /dev/null +++ b/gradle/forge1201-pack-profile.json @@ -0,0 +1,117 @@ +{ + "excludes": [ + "com/sorrowmist/useless/content/recipe/adapters/actuallyadditions/**", + "com/sorrowmist/useless/content/recipe/adapters/apothicflux/**", + "com/sorrowmist/useless/content/recipe/adapters/arsnouveau/**", + "com/sorrowmist/useless/content/recipe/adapters/astralsorcery/**", + "com/sorrowmist/useless/content/recipe/adapters/avaritia/**", + "com/sorrowmist/useless/content/recipe/adapters/delight/**", + "com/sorrowmist/useless/content/recipe/adapters/eco/**", + "com/sorrowmist/useless/content/recipe/adapters/extremereactors/**", + "com/sorrowmist/useless/content/recipe/adapters/forbiddenarcanus/**", + "com/sorrowmist/useless/content/recipe/adapters/immersiveengineering/**", + "com/sorrowmist/useless/content/recipe/adapters/ironsspellbooks/**", + "com/sorrowmist/useless/content/recipe/adapters/justdirethings/**", + "com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/**", + "com/sorrowmist/useless/content/recipe/adapters/lychee/**", + "com/sorrowmist/useless/content/recipe/adapters/malum/**", + "com/sorrowmist/useless/content/recipe/adapters/modernindustrialization/**", + "com/sorrowmist/useless/content/recipe/adapters/mysticalagriculture/**", + "com/sorrowmist/useless/content/recipe/adapters/naturesaura/**", + "com/sorrowmist/useless/content/recipe/adapters/neovitae/**", + "com/sorrowmist/useless/content/recipe/adapters/occultism/**", + "com/sorrowmist/useless/content/recipe/adapters/oritech/**", + "com/sorrowmist/useless/content/recipe/adapters/pneumaticcraft/**", + "com/sorrowmist/useless/content/recipe/adapters/productivebees/**", + "com/sorrowmist/useless/content/recipe/adapters/ufo/**", + "com/sorrowmist/useless/content/recipe/adapters/ae/ae2cs/**", + "com/sorrowmist/useless/content/recipe/adapters/ae/ae2lt/**", + "com/sorrowmist/useless/content/recipe/adapters/ae/dataenergistics/**", + "com/sorrowmist/useless/content/recipe/adapters/mekanism/mekanismmoremachine/**", + "com/sorrowmist/useless/compat/apothicflux/**", + "com/sorrowmist/useless/compat/ars/**", + "com/sorrowmist/useless/compat/barbequesdelight/**", + "com/sorrowmist/useless/compat/brewinandchewin/**", + "com/sorrowmist/useless/compat/casualnessdelight/**", + "com/sorrowmist/useless/compat/crabbersdelight/**", + "com/sorrowmist/useless/compat/expandeddelight/**", + "com/sorrowmist/useless/compat/extradelight/**", + "com/sorrowmist/useless/compat/extremereactors/**", + "com/sorrowmist/useless/compat/farmersdelight/**", + "com/sorrowmist/useless/compat/forbiddenarcanus/**", + "com/sorrowmist/useless/compat/immersiveengineering/**", + "com/sorrowmist/useless/compat/ironsspellbooks/**", + "com/sorrowmist/useless/compat/itemobliterator/**", + "com/sorrowmist/useless/compat/kaleidoscope/**", + "com/sorrowmist/useless/compat/lychee/**", + "com/sorrowmist/useless/compat/mekanismmoremachine/**", + "com/sorrowmist/useless/compat/modernindustrialization/**", + "com/sorrowmist/useless/compat/neoecoae/**", + "com/sorrowmist/useless/compat/neovitae/**", + "com/sorrowmist/useless/compat/nomadsdelight/**", + "com/sorrowmist/useless/compat/occultism/**", + "com/sorrowmist/useless/compat/oritech/**", + "com/sorrowmist/useless/compat/pneumaticcraft/**", + "com/sorrowmist/useless/compat/productivebees/**", + "com/sorrowmist/useless/compat/ubesdelight/**", + "com/sorrowmist/useless/compat/ufo/**", + "com/sorrowmist/useless/compat/youkaishomecoming/**", + "com/sorrowmist/useless/integration/dataenergistics/**", + "com/sorrowmist/useless/mixin/malum/**", + "com/sorrowmist/useless/mixin/itemobliterator/**", + "com/sorrowmist/useless/mixin/pneumaticcraft/**", + "com/sorrowmist/useless/mixin/compact/**", + "com/sorrowmist/useless/datagen/**", + "com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/InfuserRecipeAdapter.java", + "com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/PrecisionShapedRecipeAdapter.java", + "com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/PrecisionShapelessRecipeAdapter.java", + "com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/FluidShapedRecipeAdapter.java", + "com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/FluidShapelessRecipeAdapter.java", + "com/sorrowmist/useless/content/recipe/adapters/ae/extendedae/CrystalAssemblerRecipeAdapter.java", + "com/sorrowmist/useless/content/recipe/adapters/mekanism/RecyclerRecipeAdapter.java", + "com/sorrowmist/useless/content/recipe/adapters/enderio/VatFermentingRecipeAdapter.java", + "com/sorrowmist/useless/compat/ae/AeSourceCompat.java", + "com/sorrowmist/useless/compat/ae/AePressureCompat.java", + "com/sorrowmist/useless/compat/extendedae/ExtendedAEPlusJeiCompat.java", + "com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/ExtraUpgradesCraftingRecipeAdapterSupport.java", + "com/sorrowmist/useless/mixin/mek/FuckingPrideHolidayMixin.java", + "com/sorrowmist/useless/mixin/ae2/PatternAccessTerminalPacketSlotLimitMixin.java" + ], + "reason": "Optional integrations not installed in the target pack are preserved as dormant source; not a universal Forge port.", + "absent_addon_files": { + "ifeu": [ + "com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/InfuserRecipeAdapter.java", + "com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/PrecisionShapedRecipeAdapter.java", + "com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/PrecisionShapelessRecipeAdapter.java", + "com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/FluidShapedRecipeAdapter.java", + "com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/FluidShapelessRecipeAdapter.java", + "com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/ExtraUpgradesCraftingRecipeAdapterSupport.java" + ], + "mekmm": [ + "com/sorrowmist/useless/content/recipe/adapters/mekanism/RecyclerRecipeAdapter.java" + ], + "arseng": [ + "com/sorrowmist/useless/compat/ae/AeSourceCompat.java" + ], + "appliedpneumatics": [ + "com/sorrowmist/useless/compat/ae/AePressureCompat.java" + ], + "extendedae_plus": [ + "com/sorrowmist/useless/compat/extendedae/ExtendedAEPlusJeiCompat.java" + ] + }, + "version_absent_features": { + "expatternprovider:1.4.6": [ + "crystal_assembler (no block/item/recipe in installed native jar)" + ], + "enderio:6.2.13-beta": [ + "vat (no machine or recipe classes in native installed jar)" + ], + "mekanism:10.4.15": [ + "Pride holiday does not exist in native HolidayManager; no removal mixin needed." + ] + }, + "native_equivalents": { + "com/sorrowmist/useless/mixin/ae2/PatternAccessTerminalPacketSlotLimitMixin.java": "AE2 15 packet writes a varint-sized slot map with no 128-entry codec limit; native behavior already supports the required capacity." + } +} diff --git a/gradle/port-bridge-tests.gradle b/gradle/port-bridge-tests.gradle new file mode 100644 index 00000000..9f5f97eb --- /dev/null +++ b/gradle/port-bridge-tests.gradle @@ -0,0 +1,108 @@ +// Compatibility regression tests for the Forge 1.20.1 bridges. +// This source set is deliberately NOT packaged in the release jar. +sourceSets { + portBridge { + java.srcDir 'src/main/java' + java.include 'com/sorrowmist/useless/compat/forge1201/**' + java.include 'com/sorrowmist/useless/utils/RateLimitedLogger.java' + java.include 'com/sorrowmist/useless/compat/kubejs/AdvancedAlloyFurnaceRecipeSchema.java' + java.include 'com/sorrowmist/useless/compat/kubejs/GenericStackRecipeComponent.java' + java.include 'com/sorrowmist/useless/compat/kubejs/NativeRecipeComponent.java' + java.include 'com/sorrowmist/useless/content/recipe/adapters/exdeorum/**' + java.include 'com/sorrowmist/useless/content/recipe/adapters/botanypots/**' + java.include 'com/sorrowmist/useless/compat/jei/GenericStackJeiIngredientProvider*.java' + java.include 'com/sorrowmist/useless/compat/jei/AEKeyJeiIngredients.java' + java.include 'com/sorrowmist/useless/compat/jei/WrappedKeyGuiHandler.java' + java.include 'com/sorrowmist/useless/compat/appmek/AppMekJeiChemicalCompat.java' + java.include 'com/sorrowmist/useless/client/gui/PressableAE2Button.java' + java.include 'com/sorrowmist/useless/energy/IEnergyManager.java' + java.include 'com/sorrowmist/useless/energy/EnergyManager.java' + java.include 'com/sorrowmist/useless/init/ModTags.java' + java.include 'com/sorrowmist/useless/world/inventory/ExternalInventorySavedData.java' + java.include 'com/sorrowmist/useless/world/ae/AeConnectLinkSavedData.java' + java.include 'com/sorrowmist/useless/core/component/**' + java.include 'com/sorrowmist/useless/api/enums/tool/**' + java.include 'com/sorrowmist/useless/content/recipe/AdapterUtils.java' + java.include 'com/sorrowmist/useless/content/recipe/ExpectedOutputScaler.java' + java.include 'com/sorrowmist/useless/content/recipe/adapters/create/**' + java.include 'com/sorrowmist/useless/content/machines/advanced_alloy_furnace/chemical/ChemicalStackView.java' + java.include 'com/sorrowmist/useless/content/machines/advanced_alloy_furnace/chemical/FurnaceChemicalStorage.java' + java.include 'com/sorrowmist/useless/compat/mekanism/MekanismChemicalStackView.java' + java.include 'com/sorrowmist/useless/compat/mekanism/MekanismChemicalStorage.java' + java.include 'com/sorrowmist/useless/content/recipe/adapters/mekanism/**' + java.exclude 'com/sorrowmist/useless/content/recipe/adapters/mekanism/RecyclerRecipeAdapter.java' + java.exclude 'com/sorrowmist/useless/content/recipe/adapters/mekanism/mekanismmoremachine/**' + java.include 'com/sorrowmist/useless/content/machines/advanced_alloy_furnace/chemical/ChemicalKeyProvider*.java' + java.include 'com/sorrowmist/useless/compat/mekanism/NativeFurnaceChemicalHandlers.java' + java.include 'com/sorrowmist/useless/content/machines/advanced_alloy_furnace/io/FurnaceFaceAccessor.java' + java.include 'com/sorrowmist/useless/api/enums/FurnaceFace*.java' + java.include 'com/sorrowmist/useless/content/recipe/adapters/ae/extendedae/**' + java.exclude 'com/sorrowmist/useless/content/recipe/adapters/ae/extendedae/CrystalAssemblerRecipeAdapter.java' + java.include 'com/sorrowmist/useless/content/recipe/adapters/ae/advancedae/**' + java.include 'com/sorrowmist/useless/content/recipe/RecipeSourceIds.java' + java.include 'com/sorrowmist/useless/content/recipe/AlloyFurnaceRecipeIdentity.java' + java.include 'com/sorrowmist/useless/content/recipe/adapters/minecraft/BrewingSyntheticRecipe.java' + java.include 'com/sorrowmist/useless/content/recipe/adapters/minecraft/BrewingRecipeAdapter.java' + java.include 'com/sorrowmist/useless/api/recipe/IRecipeAdapter.java' + java.include 'com/sorrowmist/useless/content/recipe/IRecipeAdapter.java' + java.include 'com/sorrowmist/useless/content/blockentities/HighStackItemStackHandler.java' + java.include 'com/sorrowmist/useless/api/enums/AlloyFurnaceMode.java' + java.include 'com/sorrowmist/useless/content/recipe/AdvancedAlloyFurnaceRecipe.java' + java.include 'com/sorrowmist/useless/content/recipe/AdvancedAlloyFurnaceRecipeSerializer.java' + java.include 'com/sorrowmist/useless/content/recipe/AdvancedAlloyFurnaceRecipeBuilder.java' + java.include 'com/sorrowmist/useless/content/recipe/CountedIngredient.java' + java.include 'com/sorrowmist/useless/content/recipe/LongSizedFluidIngredient.java' + java.include 'com/sorrowmist/useless/content/recipe/FluidIngredientAllocator.java' + java.include 'com/sorrowmist/useless/content/recipe/ItemIngredientAllocator.java' + java.include 'com/sorrowmist/useless/content/recipe/AlloyFurnaceRecipeFingerprint.java' + java.include 'com/sorrowmist/useless/content/recipe/adapters/extendedcrafting/**' + java.include 'com/sorrowmist/useless/content/recipe/adapters/enderio/NativeFireLootResults.java' + java.include 'com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/DissolutionChamberRecipeAdapter.java' + java.include 'com/sorrowmist/useless/content/recipe/adapters/summoningrituals/SummoningRitualsAltarRecipeAdapter.java' + java.include 'com/sorrowmist/useless/content/recipe/adapters/enderio/EnderIOAdapterUtils.java' + java.include 'com/sorrowmist/useless/content/recipe/adapters/enderio/AlloySmeltingRecipeAdapter.java' + java.include 'com/sorrowmist/useless/content/recipe/adapters/enderio/SlicingRecipeAdapter.java' + java.include 'com/sorrowmist/useless/content/recipe/adapters/enderio/SagMillingRecipeAdapter.java' + java.include 'com/sorrowmist/useless/content/recipe/adapters/enderio/EnchanterRecipeAdapter.java' + java.include 'com/sorrowmist/useless/content/recipe/adapters/enderio/SoulVialSetIngredient.java' + java.include 'com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/PatternStackView.java' + java.include 'com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/DynamicComponentPattern.java' + java.include 'com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/DynamicComponentPatternDetails.java' + java.include 'com/sorrowmist/useless/content/recipe/adapters/enderio/SoulBindingRecipeAdapter.java' + java.include 'com/sorrowmist/useless/content/recipe/adapters/enderio/TankRecipeAdapter.java' + java.include 'com/sorrowmist/useless/content/recipe/adapters/enderio/SoulVialRecipeAdapter.java' + java.include 'com/sorrowmist/useless/content/recipe/adapters/apotheosis/ApotheosisGemIngredient.java' + java.include 'com/sorrowmist/useless/content/recipe/adapters/hostilenetworks/DataModelRangeIngredient.java' + java.include 'com/sorrowmist/useless/content/recipe/adapters/hostilenetworks/HostileNetworksRecipeAdapter.java' + java.include 'com/sorrowmist/useless/content/recipe/adapters/hostilenetworks/HostileNetworksSyntheticRecipe.java' + + compileClasspath += sourceSets.main.compileClasspath + runtimeClasspath += sourceSets.main.compileClasspath + } + portBridgeTest { + java.srcDir 'src/portBridgeTest/java' + compileClasspath += sourceSets.portBridge.output + sourceSets.portBridge.compileClasspath + runtimeClasspath += sourceSets.portBridge.output + sourceSets.portBridge.runtimeClasspath + } +} +configurations { + portBridgeTestImplementation.extendsFrom testImplementation + portBridgeTestRuntimeOnly.extendsFrom testRuntimeOnly +} +tasks.named('check') { dependsOn 'portBridgeTest' } + +tasks.register('portBridgeTest', Test) { + testClassesDirs = sourceSets.portBridgeTest.output.classesDirs + classpath = sourceSets.portBridgeTest.runtimeClasspath + useJUnitPlatform() + maxHeapSize = '1G' +} + +// ModDev's test logging adapter requires SLF4J 2; this does not affect runtime dependencies. +dependencies { portBridgeTestRuntimeOnly 'org.slf4j:slf4j-api:2.0.9' } +tasks.named('portBridgeTest', Test) { classpath = classpath.filter { !it.name.startsWith('slf4j-api-1.') } } + +// Native bootstrap needs actual Minecraft resources; this jar is never packaged with the mods. +tasks.named('portBridgeTest', Test) { + classpath += files(layout.buildDirectory.file("moddev/artifacts/client-extra-${minecraft_version}-${forge_version}.jar")) +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index c1962a79..1b33c55b 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6ee016a2..d4081da4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index aeb74cbb..23d15a93 100644 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# 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/. @@ -83,7 +85,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +# 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 @@ -111,7 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -130,10 +133,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + 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. @@ -141,7 +147,7 @@ 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=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -149,7 +155,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -198,16 +204,16 @@ 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, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_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 \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 93e3f59f..db3a6ac2 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @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 ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +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 @@ -57,22 +59,22 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +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 +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/lib/EvolvedMekanismExtras-1.20.1-1.3.3_2.jar b/lib/EvolvedMekanismExtras-1.20.1-1.3.3_2.jar deleted file mode 100644 index cb7d6431..00000000 Binary files a/lib/EvolvedMekanismExtras-1.20.1-1.3.3_2.jar and /dev/null differ diff --git a/lib/Mekanism-1.20.1-10.4.16.80.jar b/lib/Mekanism-1.20.1-10.4.16.80.jar deleted file mode 100644 index 439b2fdb..00000000 Binary files a/lib/Mekanism-1.20.1-10.4.16.80.jar and /dev/null differ diff --git a/lib/TravelAnchors-1.20.1-5.0.1.jar b/lib/TravelAnchors-1.20.1-5.0.1.jar deleted file mode 100644 index a318539c..00000000 Binary files a/lib/TravelAnchors-1.20.1-5.0.1.jar and /dev/null differ diff --git a/lib/appliedenergistics2-forge-15.4.8.jar b/lib/appliedenergistics2-forge-15.4.8.jar deleted file mode 100644 index 291e3b1e..00000000 Binary files a/lib/appliedenergistics2-forge-15.4.8.jar and /dev/null differ diff --git a/lib/examplemod-1.0.0.jar b/lib/examplemod-1.0.0.jar deleted file mode 100644 index eb041f30..00000000 Binary files a/lib/examplemod-1.0.0.jar and /dev/null differ diff --git a/lib/mekanismupgradesreborn-1.20.1-5.0.0.jar b/lib/mekanismupgradesreborn-1.20.1-5.0.0.jar deleted file mode 100644 index a0332994..00000000 Binary files a/lib/mekanismupgradesreborn-1.20.1-5.0.0.jar and /dev/null differ diff --git a/lib/thermal_parallel-1.0.0-beta.2.jar b/lib/thermal_parallel-1.0.0-beta.2.jar deleted file mode 100644 index 96adadd7..00000000 Binary files a/lib/thermal_parallel-1.0.0-beta.2.jar and /dev/null differ diff --git a/settings.gradle b/settings.gradle index 001f9722..e5030bce 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,14 +1,3 @@ -pluginManagement { - repositories { - gradlePluginPortal() - maven { - name = 'MinecraftForge' - url = 'https://maven.minecraftforge.net/' - } - maven { url = 'https://maven.parchmentmc.org' } - } -} - -plugins { - id 'org.gradle.toolchains.foojay-resolver-convention' version '0.7.0' -} \ No newline at end of file +pluginManagement { repositories { gradlePluginPortal() } } +plugins { id 'org.gradle.toolchains.foojay-resolver-convention' version '0.9.0' } +rootProject.name = 'useless_mod' diff --git a/src/generated/resources/data/c/tags/blocks/dyed.json b/src/generated/resources/data/c/tags/blocks/dyed.json new file mode 100644 index 00000000..15c141dd --- /dev/null +++ b/src/generated/resources/data/c/tags/blocks/dyed.json @@ -0,0 +1,76 @@ +{ + "values": [ + "useless_mod:black_plastic", + "useless_mod:red_plastic", + "useless_mod:green_plastic", + "useless_mod:brown_plastic", + "useless_mod:blue_plastic", + "useless_mod:purple_plastic", + "useless_mod:cyan_plastic", + "useless_mod:light_gray_plastic", + "useless_mod:gray_plastic", + "useless_mod:pink_plastic", + "useless_mod:lime_plastic", + "useless_mod:yellow_plastic", + "useless_mod:light_blue_plastic", + "useless_mod:magenta_plastic", + "useless_mod:orange_plastic", + "useless_mod:white_plastic", + "useless_mod:dark_red_plastic", + "useless_mod:aqua_plastic", + "useless_mod:black_glow_plastic", + "useless_mod:red_glow_plastic", + "useless_mod:green_glow_plastic", + "useless_mod:brown_glow_plastic", + "useless_mod:blue_glow_plastic", + "useless_mod:purple_glow_plastic", + "useless_mod:cyan_glow_plastic", + "useless_mod:light_gray_glow_plastic", + "useless_mod:gray_glow_plastic", + "useless_mod:pink_glow_plastic", + "useless_mod:lime_glow_plastic", + "useless_mod:yellow_glow_plastic", + "useless_mod:light_blue_glow_plastic", + "useless_mod:magenta_glow_plastic", + "useless_mod:orange_glow_plastic", + "useless_mod:white_glow_plastic", + "useless_mod:dark_red_glow_plastic", + "useless_mod:aqua_glow_plastic", + "useless_mod:black_plastic_ctm", + "useless_mod:red_plastic_ctm", + "useless_mod:green_plastic_ctm", + "useless_mod:brown_plastic_ctm", + "useless_mod:blue_plastic_ctm", + "useless_mod:purple_plastic_ctm", + "useless_mod:cyan_plastic_ctm", + "useless_mod:light_gray_plastic_ctm", + "useless_mod:gray_plastic_ctm", + "useless_mod:pink_plastic_ctm", + "useless_mod:lime_plastic_ctm", + "useless_mod:yellow_plastic_ctm", + "useless_mod:light_blue_plastic_ctm", + "useless_mod:magenta_plastic_ctm", + "useless_mod:orange_plastic_ctm", + "useless_mod:white_plastic_ctm", + "useless_mod:dark_red_plastic_ctm", + "useless_mod:aqua_plastic_ctm", + "useless_mod:black_glow_plastic_ctm", + "useless_mod:red_glow_plastic_ctm", + "useless_mod:green_glow_plastic_ctm", + "useless_mod:brown_glow_plastic_ctm", + "useless_mod:blue_glow_plastic_ctm", + "useless_mod:purple_glow_plastic_ctm", + "useless_mod:cyan_glow_plastic_ctm", + "useless_mod:light_gray_glow_plastic_ctm", + "useless_mod:gray_glow_plastic_ctm", + "useless_mod:pink_glow_plastic_ctm", + "useless_mod:lime_glow_plastic_ctm", + "useless_mod:yellow_glow_plastic_ctm", + "useless_mod:light_blue_glow_plastic_ctm", + "useless_mod:magenta_glow_plastic_ctm", + "useless_mod:orange_glow_plastic_ctm", + "useless_mod:white_glow_plastic_ctm", + "useless_mod:dark_red_glow_plastic_ctm", + "useless_mod:aqua_glow_plastic_ctm" + ] +} diff --git a/src/generated/resources/data/c/tags/blocks/dyed/black.json b/src/generated/resources/data/c/tags/blocks/dyed/black.json new file mode 100644 index 00000000..8560e597 --- /dev/null +++ b/src/generated/resources/data/c/tags/blocks/dyed/black.json @@ -0,0 +1,8 @@ +{ + "values": [ + "useless_mod:black_plastic", + "useless_mod:black_glow_plastic", + "useless_mod:black_plastic_ctm", + "useless_mod:black_glow_plastic_ctm" + ] +} diff --git a/src/generated/resources/data/c/tags/blocks/dyed/blue.json b/src/generated/resources/data/c/tags/blocks/dyed/blue.json new file mode 100644 index 00000000..d20744f6 --- /dev/null +++ b/src/generated/resources/data/c/tags/blocks/dyed/blue.json @@ -0,0 +1,8 @@ +{ + "values": [ + "useless_mod:blue_plastic", + "useless_mod:blue_glow_plastic", + "useless_mod:blue_plastic_ctm", + "useless_mod:blue_glow_plastic_ctm" + ] +} diff --git a/src/generated/resources/data/c/tags/blocks/dyed/brown.json b/src/generated/resources/data/c/tags/blocks/dyed/brown.json new file mode 100644 index 00000000..e6b44ad0 --- /dev/null +++ b/src/generated/resources/data/c/tags/blocks/dyed/brown.json @@ -0,0 +1,8 @@ +{ + "values": [ + "useless_mod:brown_plastic", + "useless_mod:brown_glow_plastic", + "useless_mod:brown_plastic_ctm", + "useless_mod:brown_glow_plastic_ctm" + ] +} diff --git a/src/generated/resources/data/c/tags/blocks/dyed/cyan.json b/src/generated/resources/data/c/tags/blocks/dyed/cyan.json new file mode 100644 index 00000000..2312e5c6 --- /dev/null +++ b/src/generated/resources/data/c/tags/blocks/dyed/cyan.json @@ -0,0 +1,8 @@ +{ + "values": [ + "useless_mod:cyan_plastic", + "useless_mod:cyan_glow_plastic", + "useless_mod:cyan_plastic_ctm", + "useless_mod:cyan_glow_plastic_ctm" + ] +} diff --git a/src/generated/resources/data/c/tags/blocks/dyed/gray.json b/src/generated/resources/data/c/tags/blocks/dyed/gray.json new file mode 100644 index 00000000..e7cd598d --- /dev/null +++ b/src/generated/resources/data/c/tags/blocks/dyed/gray.json @@ -0,0 +1,8 @@ +{ + "values": [ + "useless_mod:gray_plastic", + "useless_mod:gray_glow_plastic", + "useless_mod:gray_plastic_ctm", + "useless_mod:gray_glow_plastic_ctm" + ] +} diff --git a/src/generated/resources/data/c/tags/blocks/dyed/green.json b/src/generated/resources/data/c/tags/blocks/dyed/green.json new file mode 100644 index 00000000..323ac7b7 --- /dev/null +++ b/src/generated/resources/data/c/tags/blocks/dyed/green.json @@ -0,0 +1,8 @@ +{ + "values": [ + "useless_mod:green_plastic", + "useless_mod:green_glow_plastic", + "useless_mod:green_plastic_ctm", + "useless_mod:green_glow_plastic_ctm" + ] +} diff --git a/src/generated/resources/data/c/tags/blocks/dyed/light_blue.json b/src/generated/resources/data/c/tags/blocks/dyed/light_blue.json new file mode 100644 index 00000000..e96c4075 --- /dev/null +++ b/src/generated/resources/data/c/tags/blocks/dyed/light_blue.json @@ -0,0 +1,8 @@ +{ + "values": [ + "useless_mod:light_blue_plastic", + "useless_mod:light_blue_glow_plastic", + "useless_mod:light_blue_plastic_ctm", + "useless_mod:light_blue_glow_plastic_ctm" + ] +} diff --git a/src/generated/resources/data/c/tags/blocks/dyed/light_gray.json b/src/generated/resources/data/c/tags/blocks/dyed/light_gray.json new file mode 100644 index 00000000..503ffbf6 --- /dev/null +++ b/src/generated/resources/data/c/tags/blocks/dyed/light_gray.json @@ -0,0 +1,8 @@ +{ + "values": [ + "useless_mod:light_gray_plastic", + "useless_mod:light_gray_glow_plastic", + "useless_mod:light_gray_plastic_ctm", + "useless_mod:light_gray_glow_plastic_ctm" + ] +} diff --git a/src/generated/resources/data/c/tags/blocks/dyed/lime.json b/src/generated/resources/data/c/tags/blocks/dyed/lime.json new file mode 100644 index 00000000..18f5b321 --- /dev/null +++ b/src/generated/resources/data/c/tags/blocks/dyed/lime.json @@ -0,0 +1,8 @@ +{ + "values": [ + "useless_mod:lime_plastic", + "useless_mod:lime_glow_plastic", + "useless_mod:lime_plastic_ctm", + "useless_mod:lime_glow_plastic_ctm" + ] +} diff --git a/src/generated/resources/data/c/tags/blocks/dyed/magenta.json b/src/generated/resources/data/c/tags/blocks/dyed/magenta.json new file mode 100644 index 00000000..34ad9900 --- /dev/null +++ b/src/generated/resources/data/c/tags/blocks/dyed/magenta.json @@ -0,0 +1,8 @@ +{ + "values": [ + "useless_mod:magenta_plastic", + "useless_mod:magenta_glow_plastic", + "useless_mod:magenta_plastic_ctm", + "useless_mod:magenta_glow_plastic_ctm" + ] +} diff --git a/src/generated/resources/data/c/tags/blocks/dyed/orange.json b/src/generated/resources/data/c/tags/blocks/dyed/orange.json new file mode 100644 index 00000000..a49fc02e --- /dev/null +++ b/src/generated/resources/data/c/tags/blocks/dyed/orange.json @@ -0,0 +1,8 @@ +{ + "values": [ + "useless_mod:orange_plastic", + "useless_mod:orange_glow_plastic", + "useless_mod:orange_plastic_ctm", + "useless_mod:orange_glow_plastic_ctm" + ] +} diff --git a/src/generated/resources/data/c/tags/blocks/dyed/pink.json b/src/generated/resources/data/c/tags/blocks/dyed/pink.json new file mode 100644 index 00000000..ab6f3189 --- /dev/null +++ b/src/generated/resources/data/c/tags/blocks/dyed/pink.json @@ -0,0 +1,8 @@ +{ + "values": [ + "useless_mod:pink_plastic", + "useless_mod:pink_glow_plastic", + "useless_mod:pink_plastic_ctm", + "useless_mod:pink_glow_plastic_ctm" + ] +} diff --git a/src/generated/resources/data/c/tags/blocks/dyed/purple.json b/src/generated/resources/data/c/tags/blocks/dyed/purple.json new file mode 100644 index 00000000..aa28bb42 --- /dev/null +++ b/src/generated/resources/data/c/tags/blocks/dyed/purple.json @@ -0,0 +1,8 @@ +{ + "values": [ + "useless_mod:purple_plastic", + "useless_mod:purple_glow_plastic", + "useless_mod:purple_plastic_ctm", + "useless_mod:purple_glow_plastic_ctm" + ] +} diff --git a/src/generated/resources/data/c/tags/blocks/dyed/red.json b/src/generated/resources/data/c/tags/blocks/dyed/red.json new file mode 100644 index 00000000..759c9cc5 --- /dev/null +++ b/src/generated/resources/data/c/tags/blocks/dyed/red.json @@ -0,0 +1,8 @@ +{ + "values": [ + "useless_mod:red_plastic", + "useless_mod:red_glow_plastic", + "useless_mod:red_plastic_ctm", + "useless_mod:red_glow_plastic_ctm" + ] +} diff --git a/src/generated/resources/data/c/tags/blocks/dyed/white.json b/src/generated/resources/data/c/tags/blocks/dyed/white.json new file mode 100644 index 00000000..99de2ad8 --- /dev/null +++ b/src/generated/resources/data/c/tags/blocks/dyed/white.json @@ -0,0 +1,8 @@ +{ + "values": [ + "useless_mod:white_plastic", + "useless_mod:white_glow_plastic", + "useless_mod:white_plastic_ctm", + "useless_mod:white_glow_plastic_ctm" + ] +} diff --git a/src/generated/resources/data/c/tags/blocks/dyed/yellow.json b/src/generated/resources/data/c/tags/blocks/dyed/yellow.json new file mode 100644 index 00000000..9a7b130c --- /dev/null +++ b/src/generated/resources/data/c/tags/blocks/dyed/yellow.json @@ -0,0 +1,8 @@ +{ + "values": [ + "useless_mod:yellow_plastic", + "useless_mod:yellow_glow_plastic", + "useless_mod:yellow_plastic_ctm", + "useless_mod:yellow_glow_plastic_ctm" + ] +} diff --git a/src/generated/resources/data/c/tags/items/tools/crowbar.json b/src/generated/resources/data/c/tags/items/tools/crowbar.json new file mode 100644 index 00000000..fa16dea1 --- /dev/null +++ b/src/generated/resources/data/c/tags/items/tools/crowbar.json @@ -0,0 +1,7 @@ +{ + "values": [ + "useless_mod:endless_beaf_item", + "useless_mod:endless_beaf_item_no_wrench", + "useless_mod:endless_beaf_crowbar" + ] +} diff --git a/src/generated/resources/data/c/tags/items/tools/file.json b/src/generated/resources/data/c/tags/items/tools/file.json new file mode 100644 index 00000000..47e2cdf5 --- /dev/null +++ b/src/generated/resources/data/c/tags/items/tools/file.json @@ -0,0 +1,11 @@ +{ + "values": [ + "useless_mod:endless_beaf_item", + "useless_mod:endless_beaf_item_no_wrench", + "useless_mod:endless_beaf_wrench", + "useless_mod:endless_beaf_screwdriver", + "useless_mod:endless_beaf_mallet", + "useless_mod:endless_beaf_crowbar", + "useless_mod:endless_beaf_hammer" + ] +} diff --git a/src/generated/resources/data/c/tags/items/tools/hammer.json b/src/generated/resources/data/c/tags/items/tools/hammer.json new file mode 100644 index 00000000..892a5f6a --- /dev/null +++ b/src/generated/resources/data/c/tags/items/tools/hammer.json @@ -0,0 +1,7 @@ +{ + "values": [ + "useless_mod:endless_beaf_item", + "useless_mod:endless_beaf_item_no_wrench", + "useless_mod:endless_beaf_hammer" + ] +} diff --git a/src/generated/resources/data/c/tags/items/tools/knife.json b/src/generated/resources/data/c/tags/items/tools/knife.json new file mode 100644 index 00000000..47e2cdf5 --- /dev/null +++ b/src/generated/resources/data/c/tags/items/tools/knife.json @@ -0,0 +1,11 @@ +{ + "values": [ + "useless_mod:endless_beaf_item", + "useless_mod:endless_beaf_item_no_wrench", + "useless_mod:endless_beaf_wrench", + "useless_mod:endless_beaf_screwdriver", + "useless_mod:endless_beaf_mallet", + "useless_mod:endless_beaf_crowbar", + "useless_mod:endless_beaf_hammer" + ] +} diff --git a/src/generated/resources/data/c/tags/items/tools/mallet.json b/src/generated/resources/data/c/tags/items/tools/mallet.json new file mode 100644 index 00000000..c75a175e --- /dev/null +++ b/src/generated/resources/data/c/tags/items/tools/mallet.json @@ -0,0 +1,7 @@ +{ + "values": [ + "useless_mod:endless_beaf_item", + "useless_mod:endless_beaf_item_no_wrench", + "useless_mod:endless_beaf_mallet" + ] +} diff --git a/src/generated/resources/data/c/tags/items/tools/mining_tool.json b/src/generated/resources/data/c/tags/items/tools/mining_tool.json new file mode 100644 index 00000000..47e2cdf5 --- /dev/null +++ b/src/generated/resources/data/c/tags/items/tools/mining_tool.json @@ -0,0 +1,11 @@ +{ + "values": [ + "useless_mod:endless_beaf_item", + "useless_mod:endless_beaf_item_no_wrench", + "useless_mod:endless_beaf_wrench", + "useless_mod:endless_beaf_screwdriver", + "useless_mod:endless_beaf_mallet", + "useless_mod:endless_beaf_crowbar", + "useless_mod:endless_beaf_hammer" + ] +} diff --git a/src/generated/resources/data/c/tags/items/tools/mortar.json b/src/generated/resources/data/c/tags/items/tools/mortar.json new file mode 100644 index 00000000..47e2cdf5 --- /dev/null +++ b/src/generated/resources/data/c/tags/items/tools/mortar.json @@ -0,0 +1,11 @@ +{ + "values": [ + "useless_mod:endless_beaf_item", + "useless_mod:endless_beaf_item_no_wrench", + "useless_mod:endless_beaf_wrench", + "useless_mod:endless_beaf_screwdriver", + "useless_mod:endless_beaf_mallet", + "useless_mod:endless_beaf_crowbar", + "useless_mod:endless_beaf_hammer" + ] +} diff --git a/src/generated/resources/data/c/tags/items/tools/saw.json b/src/generated/resources/data/c/tags/items/tools/saw.json new file mode 100644 index 00000000..47e2cdf5 --- /dev/null +++ b/src/generated/resources/data/c/tags/items/tools/saw.json @@ -0,0 +1,11 @@ +{ + "values": [ + "useless_mod:endless_beaf_item", + "useless_mod:endless_beaf_item_no_wrench", + "useless_mod:endless_beaf_wrench", + "useless_mod:endless_beaf_screwdriver", + "useless_mod:endless_beaf_mallet", + "useless_mod:endless_beaf_crowbar", + "useless_mod:endless_beaf_hammer" + ] +} diff --git a/src/generated/resources/data/c/tags/items/tools/screwdriver.json b/src/generated/resources/data/c/tags/items/tools/screwdriver.json new file mode 100644 index 00000000..ac68a825 --- /dev/null +++ b/src/generated/resources/data/c/tags/items/tools/screwdriver.json @@ -0,0 +1,7 @@ +{ + "values": [ + "useless_mod:endless_beaf_item", + "useless_mod:endless_beaf_item_no_wrench", + "useless_mod:endless_beaf_screwdriver" + ] +} diff --git a/src/generated/resources/data/c/tags/items/tools/shear.json b/src/generated/resources/data/c/tags/items/tools/shear.json new file mode 100644 index 00000000..47e2cdf5 --- /dev/null +++ b/src/generated/resources/data/c/tags/items/tools/shear.json @@ -0,0 +1,11 @@ +{ + "values": [ + "useless_mod:endless_beaf_item", + "useless_mod:endless_beaf_item_no_wrench", + "useless_mod:endless_beaf_wrench", + "useless_mod:endless_beaf_screwdriver", + "useless_mod:endless_beaf_mallet", + "useless_mod:endless_beaf_crowbar", + "useless_mod:endless_beaf_hammer" + ] +} diff --git a/src/main/resources/data/minecraft/tags/items/tools.json b/src/generated/resources/data/c/tags/items/tools/tools.json similarity index 83% rename from src/main/resources/data/minecraft/tags/items/tools.json rename to src/generated/resources/data/c/tags/items/tools/tools.json index 69f35346..47e2cdf5 100644 --- a/src/main/resources/data/minecraft/tags/items/tools.json +++ b/src/generated/resources/data/c/tags/items/tools/tools.json @@ -1,11 +1,11 @@ { - "replace": false, "values": [ "useless_mod:endless_beaf_item", + "useless_mod:endless_beaf_item_no_wrench", "useless_mod:endless_beaf_wrench", "useless_mod:endless_beaf_screwdriver", "useless_mod:endless_beaf_mallet", "useless_mod:endless_beaf_crowbar", "useless_mod:endless_beaf_hammer" ] -} \ No newline at end of file +} diff --git a/src/generated/resources/data/c/tags/items/tools/wire_cutter.json b/src/generated/resources/data/c/tags/items/tools/wire_cutter.json new file mode 100644 index 00000000..f47b5bb0 --- /dev/null +++ b/src/generated/resources/data/c/tags/items/tools/wire_cutter.json @@ -0,0 +1,9 @@ +{ + "values": [ + "useless_mod:endless_beaf_item", + "useless_mod:endless_beaf_item_no_wrench", + "useless_mod:endless_beaf_wrench", + "useless_mod:endless_beaf_crowbar", + "useless_mod:endless_beaf_hammer" + ] +} diff --git a/src/generated/resources/data/c/tags/items/tools/wrench.json b/src/generated/resources/data/c/tags/items/tools/wrench.json new file mode 100644 index 00000000..ca62d3a4 --- /dev/null +++ b/src/generated/resources/data/c/tags/items/tools/wrench.json @@ -0,0 +1,6 @@ +{ + "values": [ + "useless_mod:endless_beaf_item", + "useless_mod:endless_beaf_wrench" + ] +} diff --git a/src/generated/resources/data/gtceu/tags/items/crafting_tools/crowbar.json b/src/generated/resources/data/gtceu/tags/items/crafting_tools/crowbar.json new file mode 100644 index 00000000..d44913b4 --- /dev/null +++ b/src/generated/resources/data/gtceu/tags/items/crafting_tools/crowbar.json @@ -0,0 +1,5 @@ +{ + "values": [ + "useless_mod:endless_beaf_crowbar" + ] +} diff --git a/src/generated/resources/data/gtceu/tags/items/crafting_tools/file.json b/src/generated/resources/data/gtceu/tags/items/crafting_tools/file.json new file mode 100644 index 00000000..47e2cdf5 --- /dev/null +++ b/src/generated/resources/data/gtceu/tags/items/crafting_tools/file.json @@ -0,0 +1,11 @@ +{ + "values": [ + "useless_mod:endless_beaf_item", + "useless_mod:endless_beaf_item_no_wrench", + "useless_mod:endless_beaf_wrench", + "useless_mod:endless_beaf_screwdriver", + "useless_mod:endless_beaf_mallet", + "useless_mod:endless_beaf_crowbar", + "useless_mod:endless_beaf_hammer" + ] +} diff --git a/src/generated/resources/data/gtceu/tags/items/crafting_tools/hammer.json b/src/generated/resources/data/gtceu/tags/items/crafting_tools/hammer.json new file mode 100644 index 00000000..4402efcb --- /dev/null +++ b/src/generated/resources/data/gtceu/tags/items/crafting_tools/hammer.json @@ -0,0 +1,5 @@ +{ + "values": [ + "useless_mod:endless_beaf_hammer" + ] +} diff --git a/src/generated/resources/data/gtceu/tags/items/crafting_tools/knife.json b/src/generated/resources/data/gtceu/tags/items/crafting_tools/knife.json new file mode 100644 index 00000000..47e2cdf5 --- /dev/null +++ b/src/generated/resources/data/gtceu/tags/items/crafting_tools/knife.json @@ -0,0 +1,11 @@ +{ + "values": [ + "useless_mod:endless_beaf_item", + "useless_mod:endless_beaf_item_no_wrench", + "useless_mod:endless_beaf_wrench", + "useless_mod:endless_beaf_screwdriver", + "useless_mod:endless_beaf_mallet", + "useless_mod:endless_beaf_crowbar", + "useless_mod:endless_beaf_hammer" + ] +} diff --git a/src/generated/resources/data/gtceu/tags/items/crafting_tools/mallet.json b/src/generated/resources/data/gtceu/tags/items/crafting_tools/mallet.json new file mode 100644 index 00000000..b595f5cf --- /dev/null +++ b/src/generated/resources/data/gtceu/tags/items/crafting_tools/mallet.json @@ -0,0 +1,5 @@ +{ + "values": [ + "useless_mod:endless_beaf_mallet" + ] +} diff --git a/src/generated/resources/data/gtceu/tags/items/crafting_tools/mortar.json b/src/generated/resources/data/gtceu/tags/items/crafting_tools/mortar.json new file mode 100644 index 00000000..47e2cdf5 --- /dev/null +++ b/src/generated/resources/data/gtceu/tags/items/crafting_tools/mortar.json @@ -0,0 +1,11 @@ +{ + "values": [ + "useless_mod:endless_beaf_item", + "useless_mod:endless_beaf_item_no_wrench", + "useless_mod:endless_beaf_wrench", + "useless_mod:endless_beaf_screwdriver", + "useless_mod:endless_beaf_mallet", + "useless_mod:endless_beaf_crowbar", + "useless_mod:endless_beaf_hammer" + ] +} diff --git a/src/generated/resources/data/gtceu/tags/items/crafting_tools/saw.json b/src/generated/resources/data/gtceu/tags/items/crafting_tools/saw.json new file mode 100644 index 00000000..47e2cdf5 --- /dev/null +++ b/src/generated/resources/data/gtceu/tags/items/crafting_tools/saw.json @@ -0,0 +1,11 @@ +{ + "values": [ + "useless_mod:endless_beaf_item", + "useless_mod:endless_beaf_item_no_wrench", + "useless_mod:endless_beaf_wrench", + "useless_mod:endless_beaf_screwdriver", + "useless_mod:endless_beaf_mallet", + "useless_mod:endless_beaf_crowbar", + "useless_mod:endless_beaf_hammer" + ] +} diff --git a/src/generated/resources/data/gtceu/tags/items/crafting_tools/screwdriver.json b/src/generated/resources/data/gtceu/tags/items/crafting_tools/screwdriver.json new file mode 100644 index 00000000..99d59cb3 --- /dev/null +++ b/src/generated/resources/data/gtceu/tags/items/crafting_tools/screwdriver.json @@ -0,0 +1,5 @@ +{ + "values": [ + "useless_mod:endless_beaf_screwdriver" + ] +} diff --git a/src/generated/resources/data/gtceu/tags/items/crafting_tools/wire_cutter.json b/src/generated/resources/data/gtceu/tags/items/crafting_tools/wire_cutter.json new file mode 100644 index 00000000..f47b5bb0 --- /dev/null +++ b/src/generated/resources/data/gtceu/tags/items/crafting_tools/wire_cutter.json @@ -0,0 +1,9 @@ +{ + "values": [ + "useless_mod:endless_beaf_item", + "useless_mod:endless_beaf_item_no_wrench", + "useless_mod:endless_beaf_wrench", + "useless_mod:endless_beaf_crowbar", + "useless_mod:endless_beaf_hammer" + ] +} diff --git a/src/generated/resources/data/gtceu/tags/items/crafting_tools/wrench.json b/src/generated/resources/data/gtceu/tags/items/crafting_tools/wrench.json new file mode 100644 index 00000000..d67cca96 --- /dev/null +++ b/src/generated/resources/data/gtceu/tags/items/crafting_tools/wrench.json @@ -0,0 +1,5 @@ +{ + "values": [ + "useless_mod:endless_beaf_wrench" + ] +} diff --git a/src/generated/resources/data/minecraft/tags/blocks/mineable/pickaxe.json b/src/generated/resources/data/minecraft/tags/blocks/mineable/pickaxe.json new file mode 100644 index 00000000..0671a6fc --- /dev/null +++ b/src/generated/resources/data/minecraft/tags/blocks/mineable/pickaxe.json @@ -0,0 +1,97 @@ +{ + "values": [ + "useless_mod:ore_generator_block", + "useless_mod:teleport_block", + "useless_mod:teleport_block_2", + "useless_mod:teleport_block_3", + "useless_mod:advanced_alloy_furnace_block", + "useless_mod:multiblock_alloy_furnace_core", + "useless_mod:me_pattern_assembly", + "useless_mod:omniversal_mold_hub", + "useless_mod:passive_crafting_hatch", + "useless_mod:omniversal_furnace_casing", + "useless_mod:temperature_regulator", + "useless_mod:useless_coil_tier_1", + "useless_mod:useless_coil_tier_2", + "useless_mod:useless_coil_tier_3", + "useless_mod:useless_coil_tier_4", + "useless_mod:useless_coil_tier_5", + "useless_mod:useless_coil_tier_6", + "useless_mod:useless_coil_tier_7", + "useless_mod:useless_coil_tier_8", + "useless_mod:useless_coil_tier_9", + "useless_mod:useful_coil", + "useless_mod:black_plastic", + "useless_mod:red_plastic", + "useless_mod:green_plastic", + "useless_mod:brown_plastic", + "useless_mod:blue_plastic", + "useless_mod:purple_plastic", + "useless_mod:cyan_plastic", + "useless_mod:light_gray_plastic", + "useless_mod:gray_plastic", + "useless_mod:pink_plastic", + "useless_mod:lime_plastic", + "useless_mod:yellow_plastic", + "useless_mod:light_blue_plastic", + "useless_mod:magenta_plastic", + "useless_mod:orange_plastic", + "useless_mod:white_plastic", + "useless_mod:dark_red_plastic", + "useless_mod:aqua_plastic", + "useless_mod:black_glow_plastic", + "useless_mod:red_glow_plastic", + "useless_mod:green_glow_plastic", + "useless_mod:brown_glow_plastic", + "useless_mod:blue_glow_plastic", + "useless_mod:purple_glow_plastic", + "useless_mod:cyan_glow_plastic", + "useless_mod:light_gray_glow_plastic", + "useless_mod:gray_glow_plastic", + "useless_mod:pink_glow_plastic", + "useless_mod:lime_glow_plastic", + "useless_mod:yellow_glow_plastic", + "useless_mod:light_blue_glow_plastic", + "useless_mod:magenta_glow_plastic", + "useless_mod:orange_glow_plastic", + "useless_mod:white_glow_plastic", + "useless_mod:dark_red_glow_plastic", + "useless_mod:aqua_glow_plastic", + "useless_mod:black_plastic_ctm", + "useless_mod:red_plastic_ctm", + "useless_mod:green_plastic_ctm", + "useless_mod:brown_plastic_ctm", + "useless_mod:blue_plastic_ctm", + "useless_mod:purple_plastic_ctm", + "useless_mod:cyan_plastic_ctm", + "useless_mod:light_gray_plastic_ctm", + "useless_mod:gray_plastic_ctm", + "useless_mod:pink_plastic_ctm", + "useless_mod:lime_plastic_ctm", + "useless_mod:yellow_plastic_ctm", + "useless_mod:light_blue_plastic_ctm", + "useless_mod:magenta_plastic_ctm", + "useless_mod:orange_plastic_ctm", + "useless_mod:white_plastic_ctm", + "useless_mod:dark_red_plastic_ctm", + "useless_mod:aqua_plastic_ctm", + "useless_mod:black_glow_plastic_ctm", + "useless_mod:red_glow_plastic_ctm", + "useless_mod:green_glow_plastic_ctm", + "useless_mod:brown_glow_plastic_ctm", + "useless_mod:blue_glow_plastic_ctm", + "useless_mod:purple_glow_plastic_ctm", + "useless_mod:cyan_glow_plastic_ctm", + "useless_mod:light_gray_glow_plastic_ctm", + "useless_mod:gray_glow_plastic_ctm", + "useless_mod:pink_glow_plastic_ctm", + "useless_mod:lime_glow_plastic_ctm", + "useless_mod:yellow_glow_plastic_ctm", + "useless_mod:light_blue_glow_plastic_ctm", + "useless_mod:magenta_glow_plastic_ctm", + "useless_mod:orange_glow_plastic_ctm", + "useless_mod:white_glow_plastic_ctm", + "useless_mod:dark_red_glow_plastic_ctm", + "useless_mod:aqua_glow_plastic_ctm" + ] +} diff --git a/src/generated/resources/data/minecraft/tags/items/axes.json b/src/generated/resources/data/minecraft/tags/items/axes.json new file mode 100644 index 00000000..47e2cdf5 --- /dev/null +++ b/src/generated/resources/data/minecraft/tags/items/axes.json @@ -0,0 +1,11 @@ +{ + "values": [ + "useless_mod:endless_beaf_item", + "useless_mod:endless_beaf_item_no_wrench", + "useless_mod:endless_beaf_wrench", + "useless_mod:endless_beaf_screwdriver", + "useless_mod:endless_beaf_mallet", + "useless_mod:endless_beaf_crowbar", + "useless_mod:endless_beaf_hammer" + ] +} diff --git a/src/main/resources/data/minecraft/tags/items/breaks_decorated_pots.json b/src/generated/resources/data/minecraft/tags/items/breaks_decorated_pots.json similarity index 83% rename from src/main/resources/data/minecraft/tags/items/breaks_decorated_pots.json rename to src/generated/resources/data/minecraft/tags/items/breaks_decorated_pots.json index 69f35346..47e2cdf5 100644 --- a/src/main/resources/data/minecraft/tags/items/breaks_decorated_pots.json +++ b/src/generated/resources/data/minecraft/tags/items/breaks_decorated_pots.json @@ -1,11 +1,11 @@ { - "replace": false, "values": [ "useless_mod:endless_beaf_item", + "useless_mod:endless_beaf_item_no_wrench", "useless_mod:endless_beaf_wrench", "useless_mod:endless_beaf_screwdriver", "useless_mod:endless_beaf_mallet", "useless_mod:endless_beaf_crowbar", "useless_mod:endless_beaf_hammer" ] -} \ No newline at end of file +} diff --git a/src/generated/resources/data/minecraft/tags/items/cluster_max_harvestables.json b/src/generated/resources/data/minecraft/tags/items/cluster_max_harvestables.json new file mode 100644 index 00000000..47e2cdf5 --- /dev/null +++ b/src/generated/resources/data/minecraft/tags/items/cluster_max_harvestables.json @@ -0,0 +1,11 @@ +{ + "values": [ + "useless_mod:endless_beaf_item", + "useless_mod:endless_beaf_item_no_wrench", + "useless_mod:endless_beaf_wrench", + "useless_mod:endless_beaf_screwdriver", + "useless_mod:endless_beaf_mallet", + "useless_mod:endless_beaf_crowbar", + "useless_mod:endless_beaf_hammer" + ] +} diff --git a/src/generated/resources/data/minecraft/tags/items/hoes.json b/src/generated/resources/data/minecraft/tags/items/hoes.json new file mode 100644 index 00000000..47e2cdf5 --- /dev/null +++ b/src/generated/resources/data/minecraft/tags/items/hoes.json @@ -0,0 +1,11 @@ +{ + "values": [ + "useless_mod:endless_beaf_item", + "useless_mod:endless_beaf_item_no_wrench", + "useless_mod:endless_beaf_wrench", + "useless_mod:endless_beaf_screwdriver", + "useless_mod:endless_beaf_mallet", + "useless_mod:endless_beaf_crowbar", + "useless_mod:endless_beaf_hammer" + ] +} diff --git a/src/generated/resources/data/minecraft/tags/items/pickaxes.json b/src/generated/resources/data/minecraft/tags/items/pickaxes.json new file mode 100644 index 00000000..47e2cdf5 --- /dev/null +++ b/src/generated/resources/data/minecraft/tags/items/pickaxes.json @@ -0,0 +1,11 @@ +{ + "values": [ + "useless_mod:endless_beaf_item", + "useless_mod:endless_beaf_item_no_wrench", + "useless_mod:endless_beaf_wrench", + "useless_mod:endless_beaf_screwdriver", + "useless_mod:endless_beaf_mallet", + "useless_mod:endless_beaf_crowbar", + "useless_mod:endless_beaf_hammer" + ] +} diff --git a/src/generated/resources/data/minecraft/tags/items/shovels.json b/src/generated/resources/data/minecraft/tags/items/shovels.json new file mode 100644 index 00000000..47e2cdf5 --- /dev/null +++ b/src/generated/resources/data/minecraft/tags/items/shovels.json @@ -0,0 +1,11 @@ +{ + "values": [ + "useless_mod:endless_beaf_item", + "useless_mod:endless_beaf_item_no_wrench", + "useless_mod:endless_beaf_wrench", + "useless_mod:endless_beaf_screwdriver", + "useless_mod:endless_beaf_mallet", + "useless_mod:endless_beaf_crowbar", + "useless_mod:endless_beaf_hammer" + ] +} diff --git a/src/generated/resources/data/minecraft/tags/items/swords.json b/src/generated/resources/data/minecraft/tags/items/swords.json new file mode 100644 index 00000000..47e2cdf5 --- /dev/null +++ b/src/generated/resources/data/minecraft/tags/items/swords.json @@ -0,0 +1,11 @@ +{ + "values": [ + "useless_mod:endless_beaf_item", + "useless_mod:endless_beaf_item_no_wrench", + "useless_mod:endless_beaf_wrench", + "useless_mod:endless_beaf_screwdriver", + "useless_mod:endless_beaf_mallet", + "useless_mod:endless_beaf_crowbar", + "useless_mod:endless_beaf_hammer" + ] +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/advanced_alloy_furnace_block.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/advanced_alloy_furnace_block.json new file mode 100644 index 00000000..a7eb1135 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/advanced_alloy_furnace_block.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_furnace": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:furnace" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:advanced_alloy_furnace_block" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_furnace" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:advanced_alloy_furnace_block" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/aqua_glow_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/aqua_glow_plastic.json new file mode 100644 index 00000000..7a0d3b48 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/aqua_glow_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_aqua_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:cyan_concrete_powder" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/aqua_glow_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_aqua_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/aqua_glow_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/aqua_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/aqua_glow_plastic_ctm.json new file mode 100644 index 00000000..7431f357 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/aqua_glow_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_aqua_glow_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:aqua_glow_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/aqua_glow_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_aqua_glow_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/aqua_glow_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/aqua_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/aqua_plastic.json new file mode 100644 index 00000000..6fc28c36 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/aqua_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_aqua_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:cyan_concrete_powder" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/aqua_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_aqua_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/aqua_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/aqua_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/aqua_plastic_ctm.json new file mode 100644 index 00000000..4aa7f561 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/aqua_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_aqua_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:aqua_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/aqua_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_aqua_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/aqua_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/black_glow_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/black_glow_plastic.json new file mode 100644 index 00000000..168ad702 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/black_glow_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_black_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:black_concrete" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/black_glow_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_black_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/black_glow_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/black_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/black_glow_plastic_ctm.json new file mode 100644 index 00000000..620a85c3 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/black_glow_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_black_glow_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:black_glow_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/black_glow_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_black_glow_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/black_glow_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/black_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/black_plastic.json new file mode 100644 index 00000000..9c4a11eb --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/black_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_black_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:black_concrete" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/black_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_black_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/black_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/black_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/black_plastic_ctm.json new file mode 100644 index 00000000..54e676e8 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/black_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_black_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:black_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/black_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_black_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/black_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/blue_glow_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/blue_glow_plastic.json new file mode 100644 index 00000000..28946928 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/blue_glow_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_blue_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:blue_concrete" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/blue_glow_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_blue_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/blue_glow_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/blue_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/blue_glow_plastic_ctm.json new file mode 100644 index 00000000..6f5e1f9a --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/blue_glow_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_blue_glow_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:blue_glow_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/blue_glow_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_blue_glow_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/blue_glow_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/blue_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/blue_plastic.json new file mode 100644 index 00000000..e8705a41 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/blue_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_blue_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:blue_concrete" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/blue_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_blue_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/blue_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/blue_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/blue_plastic_ctm.json new file mode 100644 index 00000000..f0564d02 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/blue_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_blue_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:blue_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/blue_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_blue_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/blue_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/brown_glow_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/brown_glow_plastic.json new file mode 100644 index 00000000..193259f6 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/brown_glow_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_brown_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:brown_concrete" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/brown_glow_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_brown_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/brown_glow_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/brown_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/brown_glow_plastic_ctm.json new file mode 100644 index 00000000..10b399a5 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/brown_glow_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_brown_glow_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:brown_glow_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/brown_glow_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_brown_glow_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/brown_glow_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/brown_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/brown_plastic.json new file mode 100644 index 00000000..651cbdfa --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/brown_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_brown_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:brown_concrete" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/brown_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_brown_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/brown_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/brown_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/brown_plastic_ctm.json new file mode 100644 index 00000000..217e7d8a --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/brown_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_brown_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:brown_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/brown_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_brown_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/brown_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/cyan_glow_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/cyan_glow_plastic.json new file mode 100644 index 00000000..a7f2a54f --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/cyan_glow_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_cyan_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:cyan_concrete" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/cyan_glow_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_cyan_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/cyan_glow_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/cyan_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/cyan_glow_plastic_ctm.json new file mode 100644 index 00000000..e5644858 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/cyan_glow_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_cyan_glow_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:cyan_glow_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/cyan_glow_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_cyan_glow_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/cyan_glow_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/cyan_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/cyan_plastic.json new file mode 100644 index 00000000..cb7c59a5 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/cyan_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_cyan_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:cyan_concrete" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/cyan_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_cyan_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/cyan_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/cyan_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/cyan_plastic_ctm.json new file mode 100644 index 00000000..f5fa796e --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/cyan_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_cyan_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:cyan_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/cyan_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_cyan_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/cyan_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/dark_red_glow_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/dark_red_glow_plastic.json new file mode 100644 index 00000000..664e363e --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/dark_red_glow_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_dark_red_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:red_concrete_powder" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/dark_red_glow_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_dark_red_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/dark_red_glow_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/dark_red_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/dark_red_glow_plastic_ctm.json new file mode 100644 index 00000000..841f02a0 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/dark_red_glow_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_dark_red_glow_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:dark_red_glow_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/dark_red_glow_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_dark_red_glow_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/dark_red_glow_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/dark_red_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/dark_red_plastic.json new file mode 100644 index 00000000..bd5d655b --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/dark_red_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_dark_red_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:red_concrete_powder" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/dark_red_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_dark_red_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/dark_red_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/dark_red_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/dark_red_plastic_ctm.json new file mode 100644 index 00000000..0d4e62ce --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/dark_red_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_dark_red_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:dark_red_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/dark_red_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_dark_red_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/dark_red_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/gray_glow_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/gray_glow_plastic.json new file mode 100644 index 00000000..26f75322 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/gray_glow_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_gray_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:gray_concrete" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/gray_glow_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_gray_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/gray_glow_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/gray_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/gray_glow_plastic_ctm.json new file mode 100644 index 00000000..b03c19ea --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/gray_glow_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_gray_glow_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:gray_glow_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/gray_glow_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_gray_glow_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/gray_glow_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/gray_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/gray_plastic.json new file mode 100644 index 00000000..6b405383 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/gray_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_gray_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:gray_concrete" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/gray_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_gray_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/gray_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/gray_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/gray_plastic_ctm.json new file mode 100644 index 00000000..465c5d26 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/gray_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_gray_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:gray_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/gray_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_gray_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/gray_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/green_glow_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/green_glow_plastic.json new file mode 100644 index 00000000..353cda5b --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/green_glow_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_green_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:green_concrete" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/green_glow_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_green_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/green_glow_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/green_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/green_glow_plastic_ctm.json new file mode 100644 index 00000000..b97f9bd6 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/green_glow_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_green_glow_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:green_glow_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/green_glow_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_green_glow_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/green_glow_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/green_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/green_plastic.json new file mode 100644 index 00000000..4f55d007 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/green_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_green_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:green_concrete" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/green_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_green_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/green_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/green_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/green_plastic_ctm.json new file mode 100644 index 00000000..90af4a10 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/green_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_green_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:green_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/green_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_green_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/green_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/light_blue_glow_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/light_blue_glow_plastic.json new file mode 100644 index 00000000..59ff6134 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/light_blue_glow_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_light_blue_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:light_blue_concrete" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/light_blue_glow_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_light_blue_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/light_blue_glow_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/light_blue_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/light_blue_glow_plastic_ctm.json new file mode 100644 index 00000000..923145d9 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/light_blue_glow_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_light_blue_glow_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:light_blue_glow_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/light_blue_glow_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_light_blue_glow_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/light_blue_glow_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/light_blue_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/light_blue_plastic.json new file mode 100644 index 00000000..01635f44 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/light_blue_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_light_blue_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:light_blue_concrete" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/light_blue_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_light_blue_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/light_blue_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/light_blue_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/light_blue_plastic_ctm.json new file mode 100644 index 00000000..fb9ab9c3 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/light_blue_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_light_blue_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:light_blue_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/light_blue_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_light_blue_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/light_blue_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/light_gray_glow_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/light_gray_glow_plastic.json new file mode 100644 index 00000000..c7a8f483 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/light_gray_glow_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_light_gray_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:light_gray_concrete" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/light_gray_glow_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_light_gray_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/light_gray_glow_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/light_gray_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/light_gray_glow_plastic_ctm.json new file mode 100644 index 00000000..ec9def7c --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/light_gray_glow_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_light_gray_glow_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:light_gray_glow_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/light_gray_glow_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_light_gray_glow_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/light_gray_glow_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/light_gray_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/light_gray_plastic.json new file mode 100644 index 00000000..4ec4276c --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/light_gray_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_light_gray_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:light_gray_concrete" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/light_gray_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_light_gray_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/light_gray_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/light_gray_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/light_gray_plastic_ctm.json new file mode 100644 index 00000000..756644e3 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/light_gray_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_light_gray_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:light_gray_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/light_gray_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_light_gray_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/light_gray_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/lime_glow_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/lime_glow_plastic.json new file mode 100644 index 00000000..6ad50ff9 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/lime_glow_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_lime_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:lime_concrete" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/lime_glow_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_lime_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/lime_glow_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/lime_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/lime_glow_plastic_ctm.json new file mode 100644 index 00000000..8b3c43e5 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/lime_glow_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_lime_glow_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:lime_glow_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/lime_glow_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_lime_glow_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/lime_glow_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/lime_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/lime_plastic.json new file mode 100644 index 00000000..3a0e25a3 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/lime_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_lime_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:lime_concrete" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/lime_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_lime_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/lime_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/lime_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/lime_plastic_ctm.json new file mode 100644 index 00000000..d020fcd9 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/lime_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_lime_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:lime_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/lime_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_lime_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/lime_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/magenta_glow_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/magenta_glow_plastic.json new file mode 100644 index 00000000..1a310535 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/magenta_glow_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_magenta_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:magenta_concrete" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/magenta_glow_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_magenta_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/magenta_glow_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/magenta_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/magenta_glow_plastic_ctm.json new file mode 100644 index 00000000..380f81f4 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/magenta_glow_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_magenta_glow_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:magenta_glow_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/magenta_glow_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_magenta_glow_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/magenta_glow_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/magenta_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/magenta_plastic.json new file mode 100644 index 00000000..808c2dca --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/magenta_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_magenta_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:magenta_concrete" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/magenta_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_magenta_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/magenta_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/magenta_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/magenta_plastic_ctm.json new file mode 100644 index 00000000..104aec66 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/magenta_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_magenta_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:magenta_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/magenta_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_magenta_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/magenta_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/me_pattern_assembly.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/me_pattern_assembly.json new file mode 100644 index 00000000..421574ae --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/me_pattern_assembly.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_pattern_provider": { + "conditions": { + "items": [ + { + "items": [ + "ae2:pattern_provider" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/me_pattern_assembly" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_pattern_provider" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/me_pattern_assembly" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/multiblock_alloy_furnace_core.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/multiblock_alloy_furnace_core.json new file mode 100644 index 00000000..ad4b4c84 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/multiblock_alloy_furnace_core.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_alloy_furnace": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:advanced_alloy_furnace_block" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/multiblock_alloy_furnace_core" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_alloy_furnace" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/multiblock_alloy_furnace_core" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/omniversal_furnace_casing.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/omniversal_furnace_casing.json new file mode 100644 index 00000000..6ad2d202 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/omniversal_furnace_casing.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_smooth_stone": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:smooth_stone" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/omniversal_furnace_casing" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_smooth_stone" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/omniversal_furnace_casing" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/omniversal_mold_hub.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/omniversal_mold_hub.json new file mode 100644 index 00000000..9454eed4 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/omniversal_mold_hub.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_hopper": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:hopper" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/omniversal_mold_hub" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_hopper" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/omniversal_mold_hub" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/orange_glow_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/orange_glow_plastic.json new file mode 100644 index 00000000..0ac9a8a8 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/orange_glow_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_orange_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:orange_concrete" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/orange_glow_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_orange_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/orange_glow_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/orange_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/orange_glow_plastic_ctm.json new file mode 100644 index 00000000..f8cf2c33 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/orange_glow_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_orange_glow_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:orange_glow_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/orange_glow_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_orange_glow_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/orange_glow_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/orange_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/orange_plastic.json new file mode 100644 index 00000000..09777dde --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/orange_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_orange_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:orange_concrete" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/orange_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_orange_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/orange_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/orange_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/orange_plastic_ctm.json new file mode 100644 index 00000000..2813055e --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/orange_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_orange_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:orange_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/orange_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_orange_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/orange_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/passive_crafting_hatch.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/passive_crafting_hatch.json new file mode 100644 index 00000000..d20b4d13 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/passive_crafting_hatch.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_pattern_assembly": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:me_pattern_assembly" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/passive_crafting_hatch" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_pattern_assembly" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/passive_crafting_hatch" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/pink_glow_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/pink_glow_plastic.json new file mode 100644 index 00000000..bba4512b --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/pink_glow_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_pink_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:pink_concrete" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/pink_glow_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_pink_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/pink_glow_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/pink_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/pink_glow_plastic_ctm.json new file mode 100644 index 00000000..b31a3a4c --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/pink_glow_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_pink_glow_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:pink_glow_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/pink_glow_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_pink_glow_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/pink_glow_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/pink_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/pink_plastic.json new file mode 100644 index 00000000..5b3cc82d --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/pink_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_pink_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:pink_concrete" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/pink_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_pink_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/pink_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/pink_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/pink_plastic_ctm.json new file mode 100644 index 00000000..b7d113fe --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/pink_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_pink_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:pink_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/pink_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_pink_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/pink_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/purple_glow_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/purple_glow_plastic.json new file mode 100644 index 00000000..6517d248 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/purple_glow_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_purple_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:purple_concrete" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/purple_glow_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_purple_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/purple_glow_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/purple_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/purple_glow_plastic_ctm.json new file mode 100644 index 00000000..afc38696 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/purple_glow_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_purple_glow_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:purple_glow_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/purple_glow_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_purple_glow_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/purple_glow_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/purple_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/purple_plastic.json new file mode 100644 index 00000000..96503f1b --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/purple_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_purple_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:purple_concrete" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/purple_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_purple_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/purple_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/purple_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/purple_plastic_ctm.json new file mode 100644 index 00000000..aaec1d06 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/purple_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_purple_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:purple_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/purple_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_purple_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/purple_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/red_glow_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/red_glow_plastic.json new file mode 100644 index 00000000..94044896 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/red_glow_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_red_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:red_concrete" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/red_glow_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_red_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/red_glow_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/red_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/red_glow_plastic_ctm.json new file mode 100644 index 00000000..cfefb3ff --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/red_glow_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_red_glow_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:red_glow_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/red_glow_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_red_glow_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/red_glow_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/red_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/red_plastic.json new file mode 100644 index 00000000..319f85ac --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/red_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_red_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:red_concrete" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/red_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_red_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/red_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/red_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/red_plastic_ctm.json new file mode 100644 index 00000000..6c705854 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/red_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_red_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:red_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/red_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_red_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/red_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/supervisor.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/supervisor.json new file mode 100644 index 00000000..2e7400cc --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/supervisor.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/supervisor" + }, + "trigger": "minecraft:recipe_unlocked" + }, + "has_useful_ingot": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:useful_ingot" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_useful_ingot" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/supervisor" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/useful_coil.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/useful_coil.json new file mode 100644 index 00000000..1e07d72e --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/useful_coil.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/useful_coil" + }, + "trigger": "minecraft:recipe_unlocked" + }, + "has_tier_ingot": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:useful_ingot" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_tier_ingot" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/useful_coil" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/useless_coil_tier_1.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/useless_coil_tier_1.json new file mode 100644 index 00000000..6b79d37e --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/useless_coil_tier_1.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/useless_coil_tier_1" + }, + "trigger": "minecraft:recipe_unlocked" + }, + "has_tier_ingot": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:useless_ingot_tier_1" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_tier_ingot" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/useless_coil_tier_1" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/useless_coil_tier_2.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/useless_coil_tier_2.json new file mode 100644 index 00000000..e48b4352 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/useless_coil_tier_2.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/useless_coil_tier_2" + }, + "trigger": "minecraft:recipe_unlocked" + }, + "has_tier_ingot": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:useless_ingot_tier_2" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_tier_ingot" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/useless_coil_tier_2" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/useless_coil_tier_3.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/useless_coil_tier_3.json new file mode 100644 index 00000000..d4219001 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/useless_coil_tier_3.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/useless_coil_tier_3" + }, + "trigger": "minecraft:recipe_unlocked" + }, + "has_tier_ingot": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:useless_ingot_tier_3" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_tier_ingot" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/useless_coil_tier_3" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/useless_coil_tier_4.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/useless_coil_tier_4.json new file mode 100644 index 00000000..6e241224 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/useless_coil_tier_4.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/useless_coil_tier_4" + }, + "trigger": "minecraft:recipe_unlocked" + }, + "has_tier_ingot": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:useless_ingot_tier_4" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_tier_ingot" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/useless_coil_tier_4" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/useless_coil_tier_5.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/useless_coil_tier_5.json new file mode 100644 index 00000000..6e3c8ce0 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/useless_coil_tier_5.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/useless_coil_tier_5" + }, + "trigger": "minecraft:recipe_unlocked" + }, + "has_tier_ingot": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:useless_ingot_tier_5" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_tier_ingot" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/useless_coil_tier_5" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/useless_coil_tier_6.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/useless_coil_tier_6.json new file mode 100644 index 00000000..c0670fb5 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/useless_coil_tier_6.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/useless_coil_tier_6" + }, + "trigger": "minecraft:recipe_unlocked" + }, + "has_tier_ingot": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:useless_ingot_tier_6" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_tier_ingot" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/useless_coil_tier_6" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/useless_coil_tier_7.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/useless_coil_tier_7.json new file mode 100644 index 00000000..0fb25a28 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/useless_coil_tier_7.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/useless_coil_tier_7" + }, + "trigger": "minecraft:recipe_unlocked" + }, + "has_tier_ingot": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:useless_ingot_tier_7" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_tier_ingot" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/useless_coil_tier_7" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/useless_coil_tier_8.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/useless_coil_tier_8.json new file mode 100644 index 00000000..815df63d --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/useless_coil_tier_8.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/useless_coil_tier_8" + }, + "trigger": "minecraft:recipe_unlocked" + }, + "has_tier_ingot": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:useless_ingot_tier_8" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_tier_ingot" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/useless_coil_tier_8" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/useless_coil_tier_9.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/useless_coil_tier_9.json new file mode 100644 index 00000000..1c5a2faa --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/useless_coil_tier_9.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/useless_coil_tier_9" + }, + "trigger": "minecraft:recipe_unlocked" + }, + "has_tier_ingot": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:useless_ingot_tier_9" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_tier_ingot" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/useless_coil_tier_9" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/white_glow_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/white_glow_plastic.json new file mode 100644 index 00000000..aa1daa8a --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/white_glow_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/white_glow_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + }, + "has_white_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:white_concrete" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_white_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/white_glow_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/white_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/white_glow_plastic_ctm.json new file mode 100644 index 00000000..68925889 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/white_glow_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/white_glow_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + }, + "has_white_glow_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:white_glow_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_white_glow_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/white_glow_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/white_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/white_plastic.json new file mode 100644 index 00000000..9de849e8 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/white_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/white_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + }, + "has_white_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:white_concrete" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_white_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/white_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/white_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/white_plastic_ctm.json new file mode 100644 index 00000000..03af6d1c --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/white_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/white_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + }, + "has_white_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:white_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_white_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/white_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/yellow_glow_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/yellow_glow_plastic.json new file mode 100644 index 00000000..1e78c887 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/yellow_glow_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/yellow_glow_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + }, + "has_yellow_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:yellow_concrete" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_yellow_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/yellow_glow_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/yellow_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/yellow_glow_plastic_ctm.json new file mode 100644 index 00000000..b8c752c4 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/yellow_glow_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/yellow_glow_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + }, + "has_yellow_glow_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:yellow_glow_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_yellow_glow_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/yellow_glow_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/yellow_plastic.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/yellow_plastic.json new file mode 100644 index 00000000..15375a15 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/yellow_plastic.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/yellow_plastic" + }, + "trigger": "minecraft:recipe_unlocked" + }, + "has_yellow_concrete": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:yellow_concrete" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_yellow_concrete" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/yellow_plastic" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/yellow_plastic_ctm.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/yellow_plastic_ctm.json new file mode 100644 index 00000000..afae0528 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/crafting/yellow_plastic_ctm.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/yellow_plastic_ctm" + }, + "trigger": "minecraft:recipe_unlocked" + }, + "has_yellow_plastic": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:yellow_plastic" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_yellow_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/yellow_plastic_ctm" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/ore_generator_block.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/ore_generator_block.json new file mode 100644 index 00000000..9698331a --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/ore_generator_block.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_ingot": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:useful_ingot" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:ore_generator_block" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_ingot" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:ore_generator_block" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/teleport/teleport_block.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/teleport/teleport_block.json new file mode 100644 index 00000000..662abb3e --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/teleport/teleport_block.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_planks": { + "conditions": { + "items": [ + { + "tag": "minecraft:planks" + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:teleport/teleport_block" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_planks" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:teleport/teleport_block" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/teleport/teleport_block_2.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/teleport/teleport_block_2.json new file mode 100644 index 00000000..6f20c399 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/teleport/teleport_block_2.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_planks": { + "conditions": { + "items": [ + { + "tag": "minecraft:planks" + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:teleport/teleport_block_2" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_planks" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:teleport/teleport_block_2" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/teleport/teleport_block_3.json b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/teleport/teleport_block_3.json new file mode 100644 index 00000000..ecf49eb0 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/building_blocks/teleport/teleport_block_3.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_planks": { + "conditions": { + "items": [ + { + "tag": "minecraft:planks" + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:teleport/teleport_block_3" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_planks" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:teleport/teleport_block_3" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/misc/crafting/ae2_gift_package.json b/src/generated/resources/data/useless_mod/advancements/recipes/misc/crafting/ae2_gift_package.json new file mode 100644 index 00000000..1465e02a --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/misc/crafting/ae2_gift_package.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_planks": { + "conditions": { + "items": [ + { + "tag": "minecraft:planks" + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/ae2_gift_package" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_planks" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/ae2_gift_package" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/misc/crafting/omniversal_pattern_converter.json b/src/generated/resources/data/useless_mod/advancements/recipes/misc/crafting/omniversal_pattern_converter.json new file mode 100644 index 00000000..ae49489a --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/misc/crafting/omniversal_pattern_converter.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_blank_pattern": { + "conditions": { + "items": [ + { + "items": [ + "ae2:blank_pattern" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/omniversal_pattern_converter" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_blank_pattern" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/omniversal_pattern_converter" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/misc/crafting/possible_useful_ingot.json b/src/generated/resources/data/useless_mod/advancements/recipes/misc/crafting/possible_useful_ingot.json new file mode 100644 index 00000000..bc54a64c --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/misc/crafting/possible_useful_ingot.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_ingot": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:useless_ingot_tier_1" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/possible_useful_ingot" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_ingot" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/possible_useful_ingot" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/misc/endless_beaf_item.json b/src/generated/resources/data/useless_mod/advancements/recipes/misc/endless_beaf_item.json new file mode 100644 index 00000000..ae91ba78 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/misc/endless_beaf_item.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_ingot": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:useless_ingot_tier_1" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:endless_beaf_item" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_ingot" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:endless_beaf_item" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/misc/mold/metal_mold_block.json b/src/generated/resources/data/useless_mod/advancements/recipes/misc/mold/metal_mold_block.json new file mode 100644 index 00000000..8e3d9f7d --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/misc/mold/metal_mold_block.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_ingot": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:useless_ingot_tier_1" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:mold/metal_mold_block" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_ingot" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:mold/metal_mold_block" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/misc/mold/metal_mold_gear.json b/src/generated/resources/data/useless_mod/advancements/recipes/misc/mold/metal_mold_gear.json new file mode 100644 index 00000000..0531353f --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/misc/mold/metal_mold_gear.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_ingot": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:useless_ingot_tier_1" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:mold/metal_mold_gear" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_ingot" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:mold/metal_mold_gear" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/misc/mold/metal_mold_plate.json b/src/generated/resources/data/useless_mod/advancements/recipes/misc/mold/metal_mold_plate.json new file mode 100644 index 00000000..51cb9f48 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/misc/mold/metal_mold_plate.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_ingot": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:useless_ingot_tier_1" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:mold/metal_mold_plate" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_ingot" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:mold/metal_mold_plate" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/misc/mold/metal_mold_rod.json b/src/generated/resources/data/useless_mod/advancements/recipes/misc/mold/metal_mold_rod.json new file mode 100644 index 00000000..bd88b3bc --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/misc/mold/metal_mold_rod.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_ingot": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:useless_ingot_tier_1" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:mold/metal_mold_rod" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_ingot" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:mold/metal_mold_rod" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/misc/mold/metal_mold_wire.json b/src/generated/resources/data/useless_mod/advancements/recipes/misc/mold/metal_mold_wire.json new file mode 100644 index 00000000..558ffab8 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/misc/mold/metal_mold_wire.json @@ -0,0 +1,34 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_ingot": { + "conditions": { + "items": [ + { + "items": [ + "useless_mod:useless_ingot_tier_1" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:mold/metal_mold_wire" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_ingot" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:mold/metal_mold_wire" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/advancements/recipes/misc/temperature_regulator.json b/src/generated/resources/data/useless_mod/advancements/recipes/misc/temperature_regulator.json new file mode 100644 index 00000000..8a26da76 --- /dev/null +++ b/src/generated/resources/data/useless_mod/advancements/recipes/misc/temperature_regulator.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_plastic": { + "conditions": { + "items": [ + { + "tag": "useless_mod:plastic_blocks" + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "useless_mod:crafting/temperature_regulator" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_plastic" + ] + ], + "rewards": { + "recipes": [ + "useless_mod:crafting/temperature_regulator" + ] + } +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/advanced_alloy_furnace_block.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/advanced_alloy_furnace_block.json new file mode 100644 index 00000000..72bc5102 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/advanced_alloy_furnace_block.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:advanced_alloy_furnace_block" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/advanced_alloy_furnace_block" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/aqua_glow_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/aqua_glow_plastic.json new file mode 100644 index 00000000..e8fbbed3 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/aqua_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:aqua_glow_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/aqua_glow_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/aqua_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/aqua_glow_plastic_ctm.json new file mode 100644 index 00000000..a468feac --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/aqua_glow_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:aqua_glow_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/aqua_glow_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/aqua_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/aqua_plastic.json new file mode 100644 index 00000000..a46fe9c5 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/aqua_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:aqua_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/aqua_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/aqua_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/aqua_plastic_ctm.json new file mode 100644 index 00000000..32791d2e --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/aqua_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:aqua_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/aqua_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/black_glow_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/black_glow_plastic.json new file mode 100644 index 00000000..3e730f65 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/black_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:black_glow_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/black_glow_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/black_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/black_glow_plastic_ctm.json new file mode 100644 index 00000000..ad926ae9 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/black_glow_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:black_glow_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/black_glow_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/black_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/black_plastic.json new file mode 100644 index 00000000..f71f7364 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/black_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:black_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/black_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/black_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/black_plastic_ctm.json new file mode 100644 index 00000000..7657f505 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/black_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:black_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/black_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/blue_glow_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/blue_glow_plastic.json new file mode 100644 index 00000000..6795aa57 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/blue_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:blue_glow_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/blue_glow_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/blue_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/blue_glow_plastic_ctm.json new file mode 100644 index 00000000..82e11fa6 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/blue_glow_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:blue_glow_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/blue_glow_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/blue_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/blue_plastic.json new file mode 100644 index 00000000..ae460a61 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/blue_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:blue_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/blue_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/blue_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/blue_plastic_ctm.json new file mode 100644 index 00000000..ff91a72c --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/blue_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:blue_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/blue_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/brown_glow_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/brown_glow_plastic.json new file mode 100644 index 00000000..7dd7c40c --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/brown_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:brown_glow_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/brown_glow_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/brown_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/brown_glow_plastic_ctm.json new file mode 100644 index 00000000..d68faa2b --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/brown_glow_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:brown_glow_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/brown_glow_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/brown_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/brown_plastic.json new file mode 100644 index 00000000..6b782a1e --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/brown_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:brown_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/brown_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/brown_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/brown_plastic_ctm.json new file mode 100644 index 00000000..d01c0c50 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/brown_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:brown_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/brown_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/cyan_glow_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/cyan_glow_plastic.json new file mode 100644 index 00000000..eff7b07f --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/cyan_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:cyan_glow_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/cyan_glow_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/cyan_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/cyan_glow_plastic_ctm.json new file mode 100644 index 00000000..5e49cf93 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/cyan_glow_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:cyan_glow_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/cyan_glow_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/cyan_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/cyan_plastic.json new file mode 100644 index 00000000..991acd75 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/cyan_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:cyan_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/cyan_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/cyan_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/cyan_plastic_ctm.json new file mode 100644 index 00000000..60bf0626 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/cyan_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:cyan_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/cyan_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/dark_red_glow_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/dark_red_glow_plastic.json new file mode 100644 index 00000000..c342b088 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/dark_red_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:dark_red_glow_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/dark_red_glow_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/dark_red_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/dark_red_glow_plastic_ctm.json new file mode 100644 index 00000000..64469843 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/dark_red_glow_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:dark_red_glow_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/dark_red_glow_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/dark_red_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/dark_red_plastic.json new file mode 100644 index 00000000..c200e79a --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/dark_red_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:dark_red_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/dark_red_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/dark_red_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/dark_red_plastic_ctm.json new file mode 100644 index 00000000..cd2f6272 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/dark_red_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:dark_red_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/dark_red_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/gray_glow_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/gray_glow_plastic.json new file mode 100644 index 00000000..a9b6238a --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/gray_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:gray_glow_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/gray_glow_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/gray_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/gray_glow_plastic_ctm.json new file mode 100644 index 00000000..dc739bc3 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/gray_glow_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:gray_glow_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/gray_glow_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/gray_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/gray_plastic.json new file mode 100644 index 00000000..249806c9 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/gray_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:gray_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/gray_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/gray_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/gray_plastic_ctm.json new file mode 100644 index 00000000..a951dc1e --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/gray_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:gray_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/gray_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/green_glow_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/green_glow_plastic.json new file mode 100644 index 00000000..36da02ed --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/green_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:green_glow_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/green_glow_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/green_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/green_glow_plastic_ctm.json new file mode 100644 index 00000000..490419f5 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/green_glow_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:green_glow_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/green_glow_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/green_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/green_plastic.json new file mode 100644 index 00000000..fffb6630 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/green_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:green_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/green_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/green_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/green_plastic_ctm.json new file mode 100644 index 00000000..d28cdd05 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/green_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:green_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/green_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/light_blue_glow_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/light_blue_glow_plastic.json new file mode 100644 index 00000000..3a49400a --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/light_blue_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:light_blue_glow_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/light_blue_glow_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/light_blue_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/light_blue_glow_plastic_ctm.json new file mode 100644 index 00000000..d6278d2c --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/light_blue_glow_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:light_blue_glow_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/light_blue_glow_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/light_blue_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/light_blue_plastic.json new file mode 100644 index 00000000..acc283cf --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/light_blue_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:light_blue_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/light_blue_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/light_blue_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/light_blue_plastic_ctm.json new file mode 100644 index 00000000..8a877d94 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/light_blue_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:light_blue_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/light_blue_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/light_gray_glow_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/light_gray_glow_plastic.json new file mode 100644 index 00000000..f414f38e --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/light_gray_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:light_gray_glow_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/light_gray_glow_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/light_gray_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/light_gray_glow_plastic_ctm.json new file mode 100644 index 00000000..259ec85a --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/light_gray_glow_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:light_gray_glow_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/light_gray_glow_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/light_gray_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/light_gray_plastic.json new file mode 100644 index 00000000..c1933e4e --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/light_gray_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:light_gray_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/light_gray_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/light_gray_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/light_gray_plastic_ctm.json new file mode 100644 index 00000000..4326015d --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/light_gray_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:light_gray_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/light_gray_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/lime_glow_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/lime_glow_plastic.json new file mode 100644 index 00000000..1db438ed --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/lime_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:lime_glow_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/lime_glow_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/lime_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/lime_glow_plastic_ctm.json new file mode 100644 index 00000000..2b32a2bb --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/lime_glow_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:lime_glow_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/lime_glow_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/lime_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/lime_plastic.json new file mode 100644 index 00000000..08b3c71c --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/lime_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:lime_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/lime_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/lime_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/lime_plastic_ctm.json new file mode 100644 index 00000000..21cf2c9b --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/lime_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:lime_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/lime_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/magenta_glow_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/magenta_glow_plastic.json new file mode 100644 index 00000000..ddc7f8c4 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/magenta_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:magenta_glow_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/magenta_glow_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/magenta_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/magenta_glow_plastic_ctm.json new file mode 100644 index 00000000..76f7f764 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/magenta_glow_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:magenta_glow_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/magenta_glow_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/magenta_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/magenta_plastic.json new file mode 100644 index 00000000..49bbb3b1 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/magenta_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:magenta_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/magenta_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/magenta_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/magenta_plastic_ctm.json new file mode 100644 index 00000000..cf33c8bc --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/magenta_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:magenta_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/magenta_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/me_pattern_assembly.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/me_pattern_assembly.json new file mode 100644 index 00000000..59a77972 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/me_pattern_assembly.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:me_pattern_assembly" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/me_pattern_assembly" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/multiblock_alloy_furnace_core.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/multiblock_alloy_furnace_core.json new file mode 100644 index 00000000..65563176 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/multiblock_alloy_furnace_core.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:multiblock_alloy_furnace_core" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/multiblock_alloy_furnace_core" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/omniversal_furnace_casing.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/omniversal_furnace_casing.json new file mode 100644 index 00000000..f62e6948 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/omniversal_furnace_casing.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:omniversal_furnace_casing" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/omniversal_furnace_casing" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/omniversal_mold_hub.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/omniversal_mold_hub.json new file mode 100644 index 00000000..a490b471 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/omniversal_mold_hub.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:omniversal_mold_hub" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/omniversal_mold_hub" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/orange_glow_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/orange_glow_plastic.json new file mode 100644 index 00000000..5c7f2d51 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/orange_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:orange_glow_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/orange_glow_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/orange_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/orange_glow_plastic_ctm.json new file mode 100644 index 00000000..b4edc5db --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/orange_glow_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:orange_glow_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/orange_glow_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/orange_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/orange_plastic.json new file mode 100644 index 00000000..0c6ce1e0 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/orange_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:orange_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/orange_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/orange_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/orange_plastic_ctm.json new file mode 100644 index 00000000..edf4763b --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/orange_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:orange_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/orange_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/ore_generator_block.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/ore_generator_block.json new file mode 100644 index 00000000..619c427d --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/ore_generator_block.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:ore_generator_block" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/ore_generator_block" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/passive_crafting_hatch.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/passive_crafting_hatch.json new file mode 100644 index 00000000..b06522b6 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/passive_crafting_hatch.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:passive_crafting_hatch" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/passive_crafting_hatch" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/pink_glow_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/pink_glow_plastic.json new file mode 100644 index 00000000..cac1b027 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/pink_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:pink_glow_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/pink_glow_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/pink_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/pink_glow_plastic_ctm.json new file mode 100644 index 00000000..a5d48991 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/pink_glow_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:pink_glow_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/pink_glow_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/pink_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/pink_plastic.json new file mode 100644 index 00000000..d346605f --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/pink_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:pink_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/pink_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/pink_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/pink_plastic_ctm.json new file mode 100644 index 00000000..b5f91880 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/pink_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:pink_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/pink_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/purple_glow_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/purple_glow_plastic.json new file mode 100644 index 00000000..19e22ba0 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/purple_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:purple_glow_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/purple_glow_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/purple_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/purple_glow_plastic_ctm.json new file mode 100644 index 00000000..444a5638 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/purple_glow_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:purple_glow_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/purple_glow_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/purple_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/purple_plastic.json new file mode 100644 index 00000000..6f81e3d6 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/purple_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:purple_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/purple_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/purple_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/purple_plastic_ctm.json new file mode 100644 index 00000000..df3e4410 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/purple_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:purple_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/purple_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/red_glow_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/red_glow_plastic.json new file mode 100644 index 00000000..32f79b26 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/red_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:red_glow_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/red_glow_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/red_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/red_glow_plastic_ctm.json new file mode 100644 index 00000000..3cea3ffd --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/red_glow_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:red_glow_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/red_glow_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/red_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/red_plastic.json new file mode 100644 index 00000000..1957a571 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/red_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:red_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/red_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/red_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/red_plastic_ctm.json new file mode 100644 index 00000000..dc0ef20a --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/red_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:red_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/red_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/supervisor.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/supervisor.json new file mode 100644 index 00000000..81377417 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/supervisor.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:supervisor" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/supervisor" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/teleport_block.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/teleport_block.json new file mode 100644 index 00000000..e2d07484 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/teleport_block.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:teleport_block" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/teleport_block" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/teleport_block_2.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/teleport_block_2.json new file mode 100644 index 00000000..9cf404a2 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/teleport_block_2.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:teleport_block_2" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/teleport_block_2" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/teleport_block_3.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/teleport_block_3.json new file mode 100644 index 00000000..f4938932 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/teleport_block_3.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:teleport_block_3" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/teleport_block_3" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/temperature_regulator.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/temperature_regulator.json new file mode 100644 index 00000000..2d1e16f1 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/temperature_regulator.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:temperature_regulator" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/temperature_regulator" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/useful_coil.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/useful_coil.json new file mode 100644 index 00000000..cc4120ba --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/useful_coil.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:useful_coil" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/useful_coil" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_coil_tier_1.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_coil_tier_1.json new file mode 100644 index 00000000..8a947834 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_coil_tier_1.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:useless_coil_tier_1" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/useless_coil_tier_1" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_coil_tier_2.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_coil_tier_2.json new file mode 100644 index 00000000..b7fbc4ca --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_coil_tier_2.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:useless_coil_tier_2" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/useless_coil_tier_2" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_coil_tier_3.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_coil_tier_3.json new file mode 100644 index 00000000..c946d8b4 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_coil_tier_3.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:useless_coil_tier_3" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/useless_coil_tier_3" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_coil_tier_4.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_coil_tier_4.json new file mode 100644 index 00000000..0b1b1696 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_coil_tier_4.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:useless_coil_tier_4" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/useless_coil_tier_4" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_coil_tier_5.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_coil_tier_5.json new file mode 100644 index 00000000..a09964ad --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_coil_tier_5.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:useless_coil_tier_5" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/useless_coil_tier_5" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_coil_tier_6.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_coil_tier_6.json new file mode 100644 index 00000000..a458fe12 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_coil_tier_6.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:useless_coil_tier_6" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/useless_coil_tier_6" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_coil_tier_7.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_coil_tier_7.json new file mode 100644 index 00000000..b4c899f0 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_coil_tier_7.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:useless_coil_tier_7" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/useless_coil_tier_7" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_coil_tier_8.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_coil_tier_8.json new file mode 100644 index 00000000..9fd8f3b6 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_coil_tier_8.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:useless_coil_tier_8" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/useless_coil_tier_8" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_coil_tier_9.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_coil_tier_9.json new file mode 100644 index 00000000..690dfe1b --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_coil_tier_9.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:useless_coil_tier_9" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/useless_coil_tier_9" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_compact_c9.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_compact_c9.json new file mode 100644 index 00000000..61e67371 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_compact_c9.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:useless_compact_c9" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/useless_compact_c9" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_compact_f9.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_compact_f9.json new file mode 100644 index 00000000..9e0a513f --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_compact_f9.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:useless_compact_f9" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/useless_compact_f9" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_compact_l9.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_compact_l9.json new file mode 100644 index 00000000..5b9ca1b7 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_compact_l9.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:useless_compact_l9" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/useless_compact_l9" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_glass_tier_1.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_glass_tier_1.json new file mode 100644 index 00000000..6c27199e --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_glass_tier_1.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:useless_glass_tier_1" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/useless_glass_tier_1" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_glass_tier_2.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_glass_tier_2.json new file mode 100644 index 00000000..8b7256e4 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_glass_tier_2.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:useless_glass_tier_2" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/useless_glass_tier_2" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_glass_tier_3.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_glass_tier_3.json new file mode 100644 index 00000000..16ff0ac0 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_glass_tier_3.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:useless_glass_tier_3" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/useless_glass_tier_3" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_glass_tier_4.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_glass_tier_4.json new file mode 100644 index 00000000..4354d929 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_glass_tier_4.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:useless_glass_tier_4" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/useless_glass_tier_4" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_glass_tier_5.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_glass_tier_5.json new file mode 100644 index 00000000..cadae0f5 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_glass_tier_5.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:useless_glass_tier_5" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/useless_glass_tier_5" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_glass_tier_6.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_glass_tier_6.json new file mode 100644 index 00000000..5c5a1158 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_glass_tier_6.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:useless_glass_tier_6" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/useless_glass_tier_6" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_glass_tier_7.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_glass_tier_7.json new file mode 100644 index 00000000..abe097c6 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_glass_tier_7.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:useless_glass_tier_7" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/useless_glass_tier_7" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_glass_tier_8.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_glass_tier_8.json new file mode 100644 index 00000000..523d2a42 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_glass_tier_8.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:useless_glass_tier_8" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/useless_glass_tier_8" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_glass_tier_9.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_glass_tier_9.json new file mode 100644 index 00000000..0317b131 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/useless_glass_tier_9.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:useless_glass_tier_9" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/useless_glass_tier_9" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/white_glow_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/white_glow_plastic.json new file mode 100644 index 00000000..243498e8 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/white_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:white_glow_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/white_glow_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/white_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/white_glow_plastic_ctm.json new file mode 100644 index 00000000..748d28d4 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/white_glow_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:white_glow_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/white_glow_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/white_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/white_plastic.json new file mode 100644 index 00000000..d954967f --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/white_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:white_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/white_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/white_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/white_plastic_ctm.json new file mode 100644 index 00000000..0270322c --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/white_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:white_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/white_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/yellow_glow_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/yellow_glow_plastic.json new file mode 100644 index 00000000..cf1e7238 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/yellow_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:yellow_glow_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/yellow_glow_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/yellow_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/yellow_glow_plastic_ctm.json new file mode 100644 index 00000000..9f4b42ac --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/yellow_glow_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:yellow_glow_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/yellow_glow_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/yellow_plastic.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/yellow_plastic.json new file mode 100644 index 00000000..c55cf204 --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/yellow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:yellow_plastic" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/yellow_plastic" +} diff --git a/src/generated/resources/data/useless_mod/loot_tables/blocks/yellow_plastic_ctm.json b/src/generated/resources/data/useless_mod/loot_tables/blocks/yellow_plastic_ctm.json new file mode 100644 index 00000000..65984c2a --- /dev/null +++ b/src/generated/resources/data/useless_mod/loot_tables/blocks/yellow_plastic_ctm.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "useless_mod:yellow_plastic_ctm" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "useless_mod:blocks/yellow_plastic_ctm" +} diff --git a/src/generated/resources/data/useless_mod/recipes/advanced_alloy/gear/useless_gear_tier_1.json b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/gear/useless_gear_tier_1.json new file mode 100644 index 00000000..7df7969f --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/gear/useless_gear_tier_1.json @@ -0,0 +1,22 @@ +{ + "type": "useless_mod:advanced_alloy_furnace", + "energy": 1000, + "id": "useless_mod:advanced_alloy/gear/useless_gear_tier_1", + "ingredients": [ + { + "count": 4, + "ingredient": { + "item": "useless_mod:useless_ingot_tier_1" + } + } + ], + "mold": { + "item": "useless_mod:metal_mold_gear" + }, + "outputs": [ + { + "id": "useless_mod:useless_gear_tier_1" + } + ], + "process_time": 40 +} diff --git a/src/generated/resources/data/useless_mod/recipes/advanced_alloy/gear/useless_gear_tier_2.json b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/gear/useless_gear_tier_2.json new file mode 100644 index 00000000..4f05dcfa --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/gear/useless_gear_tier_2.json @@ -0,0 +1,21 @@ +{ + "type": "useless_mod:advanced_alloy_furnace", + "id": "useless_mod:advanced_alloy/gear/useless_gear_tier_2", + "ingredients": [ + { + "count": 4, + "ingredient": { + "item": "useless_mod:useless_ingot_tier_2" + } + } + ], + "mold": { + "item": "useless_mod:metal_mold_gear" + }, + "outputs": [ + { + "id": "useless_mod:useless_gear_tier_2" + } + ], + "process_time": 40 +} diff --git a/src/generated/resources/data/useless_mod/recipes/advanced_alloy/gear/useless_gear_tier_3.json b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/gear/useless_gear_tier_3.json new file mode 100644 index 00000000..47fa8551 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/gear/useless_gear_tier_3.json @@ -0,0 +1,22 @@ +{ + "type": "useless_mod:advanced_alloy_furnace", + "energy": 3000, + "id": "useless_mod:advanced_alloy/gear/useless_gear_tier_3", + "ingredients": [ + { + "count": 4, + "ingredient": { + "item": "useless_mod:useless_ingot_tier_3" + } + } + ], + "mold": { + "item": "useless_mod:metal_mold_gear" + }, + "outputs": [ + { + "id": "useless_mod:useless_gear_tier_3" + } + ], + "process_time": 40 +} diff --git a/src/generated/resources/data/useless_mod/recipes/advanced_alloy/gear/useless_gear_tier_4.json b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/gear/useless_gear_tier_4.json new file mode 100644 index 00000000..79e1a7a4 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/gear/useless_gear_tier_4.json @@ -0,0 +1,22 @@ +{ + "type": "useless_mod:advanced_alloy_furnace", + "energy": 4000, + "id": "useless_mod:advanced_alloy/gear/useless_gear_tier_4", + "ingredients": [ + { + "count": 4, + "ingredient": { + "item": "useless_mod:useless_ingot_tier_4" + } + } + ], + "mold": { + "item": "useless_mod:metal_mold_gear" + }, + "outputs": [ + { + "id": "useless_mod:useless_gear_tier_4" + } + ], + "process_time": 40 +} diff --git a/src/generated/resources/data/useless_mod/recipes/advanced_alloy/gear/useless_gear_tier_5.json b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/gear/useless_gear_tier_5.json new file mode 100644 index 00000000..9510a59c --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/gear/useless_gear_tier_5.json @@ -0,0 +1,22 @@ +{ + "type": "useless_mod:advanced_alloy_furnace", + "energy": 5000, + "id": "useless_mod:advanced_alloy/gear/useless_gear_tier_5", + "ingredients": [ + { + "count": 4, + "ingredient": { + "item": "useless_mod:useless_ingot_tier_5" + } + } + ], + "mold": { + "item": "useless_mod:metal_mold_gear" + }, + "outputs": [ + { + "id": "useless_mod:useless_gear_tier_5" + } + ], + "process_time": 40 +} diff --git a/src/generated/resources/data/useless_mod/recipes/advanced_alloy/gear/useless_gear_tier_6.json b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/gear/useless_gear_tier_6.json new file mode 100644 index 00000000..c5935f5a --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/gear/useless_gear_tier_6.json @@ -0,0 +1,22 @@ +{ + "type": "useless_mod:advanced_alloy_furnace", + "energy": 6000, + "id": "useless_mod:advanced_alloy/gear/useless_gear_tier_6", + "ingredients": [ + { + "count": 4, + "ingredient": { + "item": "useless_mod:useless_ingot_tier_6" + } + } + ], + "mold": { + "item": "useless_mod:metal_mold_gear" + }, + "outputs": [ + { + "id": "useless_mod:useless_gear_tier_6" + } + ], + "process_time": 40 +} diff --git a/src/generated/resources/data/useless_mod/recipes/advanced_alloy/gear/useless_gear_tier_7.json b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/gear/useless_gear_tier_7.json new file mode 100644 index 00000000..512b3bc4 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/gear/useless_gear_tier_7.json @@ -0,0 +1,22 @@ +{ + "type": "useless_mod:advanced_alloy_furnace", + "energy": 7000, + "id": "useless_mod:advanced_alloy/gear/useless_gear_tier_7", + "ingredients": [ + { + "count": 4, + "ingredient": { + "item": "useless_mod:useless_ingot_tier_7" + } + } + ], + "mold": { + "item": "useless_mod:metal_mold_gear" + }, + "outputs": [ + { + "id": "useless_mod:useless_gear_tier_7" + } + ], + "process_time": 40 +} diff --git a/src/generated/resources/data/useless_mod/recipes/advanced_alloy/gear/useless_gear_tier_8.json b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/gear/useless_gear_tier_8.json new file mode 100644 index 00000000..bfa8aa8f --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/gear/useless_gear_tier_8.json @@ -0,0 +1,22 @@ +{ + "type": "useless_mod:advanced_alloy_furnace", + "energy": 8000, + "id": "useless_mod:advanced_alloy/gear/useless_gear_tier_8", + "ingredients": [ + { + "count": 4, + "ingredient": { + "item": "useless_mod:useless_ingot_tier_8" + } + } + ], + "mold": { + "item": "useless_mod:metal_mold_gear" + }, + "outputs": [ + { + "id": "useless_mod:useless_gear_tier_8" + } + ], + "process_time": 40 +} diff --git a/src/generated/resources/data/useless_mod/recipes/advanced_alloy/gear/useless_gear_tier_9.json b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/gear/useless_gear_tier_9.json new file mode 100644 index 00000000..86111893 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/gear/useless_gear_tier_9.json @@ -0,0 +1,22 @@ +{ + "type": "useless_mod:advanced_alloy_furnace", + "energy": 9000, + "id": "useless_mod:advanced_alloy/gear/useless_gear_tier_9", + "ingredients": [ + { + "count": 4, + "ingredient": { + "item": "useless_mod:useless_ingot_tier_9" + } + } + ], + "mold": { + "item": "useless_mod:metal_mold_gear" + }, + "outputs": [ + { + "id": "useless_mod:useless_gear_tier_9" + } + ], + "process_time": 40 +} diff --git a/src/generated/resources/data/useless_mod/recipes/advanced_alloy/glass/useless_glass_tier_1.json b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/glass/useless_glass_tier_1.json new file mode 100644 index 00000000..29229517 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/glass/useless_glass_tier_1.json @@ -0,0 +1,27 @@ +{ + "type": "useless_mod:advanced_alloy_furnace", + "energy": 1000, + "id": "useless_mod:advanced_alloy/glass/useless_glass_tier_1", + "ingredients": [ + { + "count": 2, + "ingredient": { + "item": "minecraft:glass" + } + }, + { + "ingredient": { + "item": "useless_mod:useless_ingot_tier_1" + } + } + ], + "mold": { + "item": "useless_mod:metal_mold_block" + }, + "outputs": [ + { + "id": "useless_mod:useless_glass_tier_1" + } + ], + "process_time": 40 +} diff --git a/src/generated/resources/data/useless_mod/recipes/advanced_alloy/glass/useless_glass_tier_2.json b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/glass/useless_glass_tier_2.json new file mode 100644 index 00000000..508c08f9 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/glass/useless_glass_tier_2.json @@ -0,0 +1,26 @@ +{ + "type": "useless_mod:advanced_alloy_furnace", + "id": "useless_mod:advanced_alloy/glass/useless_glass_tier_2", + "ingredients": [ + { + "count": 2, + "ingredient": { + "item": "minecraft:glass" + } + }, + { + "ingredient": { + "item": "useless_mod:useless_ingot_tier_2" + } + } + ], + "mold": { + "item": "useless_mod:metal_mold_block" + }, + "outputs": [ + { + "id": "useless_mod:useless_glass_tier_2" + } + ], + "process_time": 40 +} diff --git a/src/generated/resources/data/useless_mod/recipes/advanced_alloy/glass/useless_glass_tier_3.json b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/glass/useless_glass_tier_3.json new file mode 100644 index 00000000..bb0d5128 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/glass/useless_glass_tier_3.json @@ -0,0 +1,27 @@ +{ + "type": "useless_mod:advanced_alloy_furnace", + "energy": 3000, + "id": "useless_mod:advanced_alloy/glass/useless_glass_tier_3", + "ingredients": [ + { + "count": 2, + "ingredient": { + "item": "minecraft:glass" + } + }, + { + "ingredient": { + "item": "useless_mod:useless_ingot_tier_3" + } + } + ], + "mold": { + "item": "useless_mod:metal_mold_block" + }, + "outputs": [ + { + "id": "useless_mod:useless_glass_tier_3" + } + ], + "process_time": 40 +} diff --git a/src/generated/resources/data/useless_mod/recipes/advanced_alloy/glass/useless_glass_tier_4.json b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/glass/useless_glass_tier_4.json new file mode 100644 index 00000000..ac8835f2 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/glass/useless_glass_tier_4.json @@ -0,0 +1,27 @@ +{ + "type": "useless_mod:advanced_alloy_furnace", + "energy": 4000, + "id": "useless_mod:advanced_alloy/glass/useless_glass_tier_4", + "ingredients": [ + { + "count": 2, + "ingredient": { + "item": "minecraft:glass" + } + }, + { + "ingredient": { + "item": "useless_mod:useless_ingot_tier_4" + } + } + ], + "mold": { + "item": "useless_mod:metal_mold_block" + }, + "outputs": [ + { + "id": "useless_mod:useless_glass_tier_4" + } + ], + "process_time": 40 +} diff --git a/src/generated/resources/data/useless_mod/recipes/advanced_alloy/glass/useless_glass_tier_5.json b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/glass/useless_glass_tier_5.json new file mode 100644 index 00000000..7b251fef --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/glass/useless_glass_tier_5.json @@ -0,0 +1,27 @@ +{ + "type": "useless_mod:advanced_alloy_furnace", + "energy": 5000, + "id": "useless_mod:advanced_alloy/glass/useless_glass_tier_5", + "ingredients": [ + { + "count": 2, + "ingredient": { + "item": "minecraft:glass" + } + }, + { + "ingredient": { + "item": "useless_mod:useless_ingot_tier_5" + } + } + ], + "mold": { + "item": "useless_mod:metal_mold_block" + }, + "outputs": [ + { + "id": "useless_mod:useless_glass_tier_5" + } + ], + "process_time": 40 +} diff --git a/src/generated/resources/data/useless_mod/recipes/advanced_alloy/glass/useless_glass_tier_6.json b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/glass/useless_glass_tier_6.json new file mode 100644 index 00000000..ce73219f --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/glass/useless_glass_tier_6.json @@ -0,0 +1,27 @@ +{ + "type": "useless_mod:advanced_alloy_furnace", + "energy": 6000, + "id": "useless_mod:advanced_alloy/glass/useless_glass_tier_6", + "ingredients": [ + { + "count": 2, + "ingredient": { + "item": "minecraft:glass" + } + }, + { + "ingredient": { + "item": "useless_mod:useless_ingot_tier_6" + } + } + ], + "mold": { + "item": "useless_mod:metal_mold_block" + }, + "outputs": [ + { + "id": "useless_mod:useless_glass_tier_6" + } + ], + "process_time": 40 +} diff --git a/src/generated/resources/data/useless_mod/recipes/advanced_alloy/glass/useless_glass_tier_7.json b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/glass/useless_glass_tier_7.json new file mode 100644 index 00000000..3c882573 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/glass/useless_glass_tier_7.json @@ -0,0 +1,27 @@ +{ + "type": "useless_mod:advanced_alloy_furnace", + "energy": 7000, + "id": "useless_mod:advanced_alloy/glass/useless_glass_tier_7", + "ingredients": [ + { + "count": 2, + "ingredient": { + "item": "minecraft:glass" + } + }, + { + "ingredient": { + "item": "useless_mod:useless_ingot_tier_7" + } + } + ], + "mold": { + "item": "useless_mod:metal_mold_block" + }, + "outputs": [ + { + "id": "useless_mod:useless_glass_tier_7" + } + ], + "process_time": 40 +} diff --git a/src/generated/resources/data/useless_mod/recipes/advanced_alloy/glass/useless_glass_tier_8.json b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/glass/useless_glass_tier_8.json new file mode 100644 index 00000000..777e617d --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/glass/useless_glass_tier_8.json @@ -0,0 +1,27 @@ +{ + "type": "useless_mod:advanced_alloy_furnace", + "energy": 8000, + "id": "useless_mod:advanced_alloy/glass/useless_glass_tier_8", + "ingredients": [ + { + "count": 2, + "ingredient": { + "item": "minecraft:glass" + } + }, + { + "ingredient": { + "item": "useless_mod:useless_ingot_tier_8" + } + } + ], + "mold": { + "item": "useless_mod:metal_mold_block" + }, + "outputs": [ + { + "id": "useless_mod:useless_glass_tier_8" + } + ], + "process_time": 40 +} diff --git a/src/generated/resources/data/useless_mod/recipes/advanced_alloy/glass/useless_glass_tier_9.json b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/glass/useless_glass_tier_9.json new file mode 100644 index 00000000..0fd6856c --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/glass/useless_glass_tier_9.json @@ -0,0 +1,27 @@ +{ + "type": "useless_mod:advanced_alloy_furnace", + "energy": 9000, + "id": "useless_mod:advanced_alloy/glass/useless_glass_tier_9", + "ingredients": [ + { + "count": 2, + "ingredient": { + "item": "minecraft:glass" + } + }, + { + "ingredient": { + "item": "useless_mod:useless_ingot_tier_9" + } + } + ], + "mold": { + "item": "useless_mod:metal_mold_block" + }, + "outputs": [ + { + "id": "useless_mod:useless_glass_tier_9" + } + ], + "process_time": 40 +} diff --git a/src/generated/resources/data/useless_mod/recipes/advanced_alloy/ingot/useful_ingot.json b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/ingot/useful_ingot.json new file mode 100644 index 00000000..c652b326 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/ingot/useful_ingot.json @@ -0,0 +1,57 @@ +{ + "type": "useless_mod:advanced_alloy_furnace", + "energy": 10000000, + "id": "useless_mod:advanced_alloy/ingot/useful_ingot", + "ingredients": [ + { + "count": 64, + "ingredient": { + "item": "useless_mod:possible_useful_ingot" + } + }, + { + "count": 1024, + "ingredient": { + "item": "minecraft:iron_block" + } + }, + { + "count": 1024, + "ingredient": { + "item": "minecraft:gold_block" + } + }, + { + "count": 1024, + "ingredient": { + "item": "minecraft:diamond_block" + } + }, + { + "count": 1024, + "ingredient": { + "item": "minecraft:emerald_block" + } + }, + { + "count": 1024, + "ingredient": { + "item": "minecraft:netherite_block" + } + } + ], + "input_fluids": [ + { + "amount": 10000000, + "ingredient": { + "fluid": "minecraft:lava" + } + } + ], + "outputs": [ + { + "id": "useless_mod:useful_ingot" + } + ], + "process_time": 100 +} diff --git a/src/generated/resources/data/useless_mod/recipes/advanced_alloy/ingot/useless_ingot_tier_1.json b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/ingot/useless_ingot_tier_1.json new file mode 100644 index 00000000..2edf472f --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/ingot/useless_ingot_tier_1.json @@ -0,0 +1,49 @@ +{ + "type": "useless_mod:advanced_alloy_furnace", + "energy": 10000, + "id": "useless_mod:advanced_alloy/ingot/useless_ingot_tier_1", + "ingredients": [ + { + "count": 2, + "ingredient": { + "item": "minecraft:mutton" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:beef" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:chicken" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:iron_ingot" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:gold_ingot" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:diamond" + } + } + ], + "outputs": [ + { + "id": "useless_mod:useless_ingot_tier_1" + } + ], + "process_time": 1 +} diff --git a/src/generated/resources/data/useless_mod/recipes/advanced_alloy/ingot/useless_ingot_tier_2.json b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/ingot/useless_ingot_tier_2.json new file mode 100644 index 00000000..b7f9c037 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/ingot/useless_ingot_tier_2.json @@ -0,0 +1,48 @@ +{ + "type": "useless_mod:advanced_alloy_furnace", + "energy": 50000, + "id": "useless_mod:advanced_alloy/ingot/useless_ingot_tier_2", + "ingredients": [ + { + "count": 2, + "ingredient": { + "item": "minecraft:rotten_flesh" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:gunpowder" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:bone" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:gold_ingot" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:diamond" + } + }, + { + "ingredient": { + "item": "useless_mod:useless_ingot_tier_1" + } + } + ], + "outputs": [ + { + "id": "useless_mod:useless_ingot_tier_2" + } + ], + "process_time": 1 +} diff --git a/src/generated/resources/data/useless_mod/recipes/advanced_alloy/ingot/useless_ingot_tier_3.json b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/ingot/useless_ingot_tier_3.json new file mode 100644 index 00000000..bc549fa2 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/ingot/useless_ingot_tier_3.json @@ -0,0 +1,48 @@ +{ + "type": "useless_mod:advanced_alloy_furnace", + "energy": 100000, + "id": "useless_mod:advanced_alloy/ingot/useless_ingot_tier_3", + "ingredients": [ + { + "count": 2, + "ingredient": { + "item": "minecraft:nether_wart" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:blaze_rod" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:nether_brick" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:gold_ingot" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:diamond" + } + }, + { + "ingredient": { + "item": "useless_mod:useless_ingot_tier_2" + } + } + ], + "outputs": [ + { + "id": "useless_mod:useless_ingot_tier_3" + } + ], + "process_time": 1 +} diff --git a/src/generated/resources/data/useless_mod/recipes/advanced_alloy/ingot/useless_ingot_tier_4.json b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/ingot/useless_ingot_tier_4.json new file mode 100644 index 00000000..0f13be63 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/ingot/useless_ingot_tier_4.json @@ -0,0 +1,48 @@ +{ + "type": "useless_mod:advanced_alloy_furnace", + "energy": 500000, + "id": "useless_mod:advanced_alloy/ingot/useless_ingot_tier_4", + "ingredients": [ + { + "count": 2, + "ingredient": { + "item": "minecraft:ender_pearl" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:chorus_fruit" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:shulker_shell" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:gold_block" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:diamond_block" + } + }, + { + "ingredient": { + "item": "useless_mod:useless_ingot_tier_3" + } + } + ], + "outputs": [ + { + "id": "useless_mod:useless_ingot_tier_4" + } + ], + "process_time": 1 +} diff --git a/src/generated/resources/data/useless_mod/recipes/advanced_alloy/ingot/useless_ingot_tier_5.json b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/ingot/useless_ingot_tier_5.json new file mode 100644 index 00000000..9a997fcb --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/ingot/useless_ingot_tier_5.json @@ -0,0 +1,48 @@ +{ + "type": "useless_mod:advanced_alloy_furnace", + "energy": 1000000, + "id": "useless_mod:advanced_alloy/ingot/useless_ingot_tier_5", + "ingredients": [ + { + "count": 2, + "ingredient": { + "item": "minecraft:wither_skeleton_skull" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:phantom_membrane" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:sea_lantern" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:gold_block" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:diamond_block" + } + }, + { + "ingredient": { + "item": "useless_mod:useless_ingot_tier_4" + } + } + ], + "outputs": [ + { + "id": "useless_mod:useless_ingot_tier_5" + } + ], + "process_time": 1 +} diff --git a/src/generated/resources/data/useless_mod/recipes/advanced_alloy/ingot/useless_ingot_tier_6.json b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/ingot/useless_ingot_tier_6.json new file mode 100644 index 00000000..2466c3cd --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/ingot/useless_ingot_tier_6.json @@ -0,0 +1,48 @@ +{ + "type": "useless_mod:advanced_alloy_furnace", + "energy": 5000000, + "id": "useless_mod:advanced_alloy/ingot/useless_ingot_tier_6", + "ingredients": [ + { + "count": 2, + "ingredient": { + "item": "minecraft:dragon_breath" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:totem_of_undying" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:enchanted_golden_apple" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:gold_block" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:diamond_block" + } + }, + { + "ingredient": { + "item": "useless_mod:useless_ingot_tier_5" + } + } + ], + "outputs": [ + { + "id": "useless_mod:useless_ingot_tier_6" + } + ], + "process_time": 1 +} diff --git a/src/generated/resources/data/useless_mod/recipes/advanced_alloy/ingot/useless_ingot_tier_7.json b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/ingot/useless_ingot_tier_7.json new file mode 100644 index 00000000..0c9880b3 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/ingot/useless_ingot_tier_7.json @@ -0,0 +1,48 @@ +{ + "type": "useless_mod:advanced_alloy_furnace", + "energy": 10000000, + "id": "useless_mod:advanced_alloy/ingot/useless_ingot_tier_7", + "ingredients": [ + { + "count": 2, + "ingredient": { + "item": "minecraft:nether_star" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:wither_rose" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:ghast_tear" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:diamond_block" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:netherite_ingot" + } + }, + { + "ingredient": { + "item": "useless_mod:useless_ingot_tier_6" + } + } + ], + "outputs": [ + { + "id": "useless_mod:useless_ingot_tier_7" + } + ], + "process_time": 1 +} diff --git a/src/generated/resources/data/useless_mod/recipes/advanced_alloy/ingot/useless_ingot_tier_8.json b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/ingot/useless_ingot_tier_8.json new file mode 100644 index 00000000..826007aa --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/ingot/useless_ingot_tier_8.json @@ -0,0 +1,48 @@ +{ + "type": "useless_mod:advanced_alloy_furnace", + "energy": 50000000, + "id": "useless_mod:advanced_alloy/ingot/useless_ingot_tier_8", + "ingredients": [ + { + "count": 2, + "ingredient": { + "item": "minecraft:dragon_head" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:echo_shard" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:conduit" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:diamond_block" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:netherite_ingot" + } + }, + { + "ingredient": { + "item": "useless_mod:useless_ingot_tier_7" + } + } + ], + "outputs": [ + { + "id": "useless_mod:useless_ingot_tier_8" + } + ], + "process_time": 1 +} diff --git a/src/generated/resources/data/useless_mod/recipes/advanced_alloy/ingot/useless_ingot_tier_9.json b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/ingot/useless_ingot_tier_9.json new file mode 100644 index 00000000..f3fd786a --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/advanced_alloy/ingot/useless_ingot_tier_9.json @@ -0,0 +1,48 @@ +{ + "type": "useless_mod:advanced_alloy_furnace", + "energy": 100000000, + "id": "useless_mod:advanced_alloy/ingot/useless_ingot_tier_9", + "ingredients": [ + { + "count": 2, + "ingredient": { + "item": "minecraft:turtle_helmet" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:goat_horn" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:rabbit_foot" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:diamond_block" + } + }, + { + "count": 2, + "ingredient": { + "item": "minecraft:netherite_block" + } + }, + { + "ingredient": { + "item": "useless_mod:useless_ingot_tier_8" + } + } + ], + "outputs": [ + { + "id": "useless_mod:useless_ingot_tier_9" + } + ], + "process_time": 1 +} diff --git a/src/generated/resources/data/useless_mod/recipes/advanced_alloy_furnace_block.json b/src/generated/resources/data/useless_mod/recipes/advanced_alloy_furnace_block.json new file mode 100644 index 00000000..041b348f --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/advanced_alloy_furnace_block.json @@ -0,0 +1,42 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "A": { + "item": "minecraft:emerald_block" + }, + "B": { + "item": "minecraft:diamond_block" + }, + "C": { + "item": "minecraft:lapis_block" + }, + "D": { + "item": "minecraft:iron_block" + }, + "E": { + "item": "minecraft:furnace" + }, + "F": { + "item": "minecraft:gold_block" + }, + "G": { + "item": "minecraft:water_bucket" + }, + "H": { + "item": "minecraft:netherite_block" + }, + "I": { + "item": "minecraft:lava_bucket" + } + }, + "pattern": [ + "ABC", + "DEF", + "GHI" + ], + "result": { + "count": 1, + "item": "useless_mod:advanced_alloy_furnace_block" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/ae2_gift_package.json b/src/generated/resources/data/useless_mod/recipes/crafting/ae2_gift_package.json new file mode 100644 index 00000000..e1df45a6 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/ae2_gift_package.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "P": { + "tag": "minecraft:planks" + }, + "S": { + "item": "minecraft:stick" + } + }, + "pattern": [ + "SPS", + "PPP", + "SPS" + ], + "result": { + "count": 1, + "item": "useless_mod:ae2_gift_package" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/aqua_glow_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/aqua_glow_plastic.json new file mode 100644 index 00000000..b35b7b28 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/aqua_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:cyan_concrete_powder" + }, + "G": { + "item": "minecraft:glowstone_dust" + } + }, + "pattern": [ + "CCC", + "CGC", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:aqua_glow_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/aqua_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/aqua_glow_plastic_ctm.json new file mode 100644 index 00000000..848a62b1 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/aqua_glow_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:aqua_glow_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:aqua_glow_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/aqua_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/aqua_plastic.json new file mode 100644 index 00000000..5c91667e --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/aqua_plastic.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:cyan_concrete_powder" + } + }, + "pattern": [ + "CCC", + "C C", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:aqua_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/aqua_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/aqua_plastic_ctm.json new file mode 100644 index 00000000..7ce2f05c --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/aqua_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:aqua_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:aqua_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/black_glow_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/black_glow_plastic.json new file mode 100644 index 00000000..c9406568 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/black_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:black_concrete" + }, + "G": { + "item": "minecraft:glowstone_dust" + } + }, + "pattern": [ + "CCC", + "CGC", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:black_glow_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/black_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/black_glow_plastic_ctm.json new file mode 100644 index 00000000..f0b92f5e --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/black_glow_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:black_glow_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:black_glow_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/black_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/black_plastic.json new file mode 100644 index 00000000..86a1bb0c --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/black_plastic.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:black_concrete" + } + }, + "pattern": [ + "CCC", + "C C", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:black_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/black_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/black_plastic_ctm.json new file mode 100644 index 00000000..029c1b4d --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/black_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:black_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:black_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/blue_glow_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/blue_glow_plastic.json new file mode 100644 index 00000000..18a37091 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/blue_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:blue_concrete" + }, + "G": { + "item": "minecraft:glowstone_dust" + } + }, + "pattern": [ + "CCC", + "CGC", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:blue_glow_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/blue_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/blue_glow_plastic_ctm.json new file mode 100644 index 00000000..d94353fa --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/blue_glow_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:blue_glow_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:blue_glow_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/blue_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/blue_plastic.json new file mode 100644 index 00000000..af521db0 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/blue_plastic.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:blue_concrete" + } + }, + "pattern": [ + "CCC", + "C C", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:blue_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/blue_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/blue_plastic_ctm.json new file mode 100644 index 00000000..aff82cfe --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/blue_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:blue_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:blue_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/brown_glow_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/brown_glow_plastic.json new file mode 100644 index 00000000..47c4e047 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/brown_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:brown_concrete" + }, + "G": { + "item": "minecraft:glowstone_dust" + } + }, + "pattern": [ + "CCC", + "CGC", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:brown_glow_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/brown_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/brown_glow_plastic_ctm.json new file mode 100644 index 00000000..6a9ef7c8 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/brown_glow_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:brown_glow_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:brown_glow_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/brown_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/brown_plastic.json new file mode 100644 index 00000000..79e8fee5 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/brown_plastic.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:brown_concrete" + } + }, + "pattern": [ + "CCC", + "C C", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:brown_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/brown_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/brown_plastic_ctm.json new file mode 100644 index 00000000..04891af2 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/brown_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:brown_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:brown_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/cyan_glow_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/cyan_glow_plastic.json new file mode 100644 index 00000000..fd776083 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/cyan_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:cyan_concrete" + }, + "G": { + "item": "minecraft:glowstone_dust" + } + }, + "pattern": [ + "CCC", + "CGC", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:cyan_glow_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/cyan_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/cyan_glow_plastic_ctm.json new file mode 100644 index 00000000..ac1bed86 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/cyan_glow_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:cyan_glow_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:cyan_glow_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/cyan_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/cyan_plastic.json new file mode 100644 index 00000000..94fa4919 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/cyan_plastic.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:cyan_concrete" + } + }, + "pattern": [ + "CCC", + "C C", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:cyan_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/cyan_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/cyan_plastic_ctm.json new file mode 100644 index 00000000..05b6fbb9 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/cyan_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:cyan_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:cyan_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/dark_red_glow_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/dark_red_glow_plastic.json new file mode 100644 index 00000000..7ef1d680 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/dark_red_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:red_concrete_powder" + }, + "G": { + "item": "minecraft:glowstone_dust" + } + }, + "pattern": [ + "CCC", + "CGC", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:dark_red_glow_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/dark_red_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/dark_red_glow_plastic_ctm.json new file mode 100644 index 00000000..9649ef7f --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/dark_red_glow_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:dark_red_glow_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:dark_red_glow_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/dark_red_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/dark_red_plastic.json new file mode 100644 index 00000000..2722a12a --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/dark_red_plastic.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:red_concrete_powder" + } + }, + "pattern": [ + "CCC", + "C C", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:dark_red_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/dark_red_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/dark_red_plastic_ctm.json new file mode 100644 index 00000000..b6d2035e --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/dark_red_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:dark_red_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:dark_red_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/gray_glow_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/gray_glow_plastic.json new file mode 100644 index 00000000..02ae2ad8 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/gray_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:gray_concrete" + }, + "G": { + "item": "minecraft:glowstone_dust" + } + }, + "pattern": [ + "CCC", + "CGC", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:gray_glow_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/gray_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/gray_glow_plastic_ctm.json new file mode 100644 index 00000000..a0ef6063 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/gray_glow_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:gray_glow_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:gray_glow_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/gray_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/gray_plastic.json new file mode 100644 index 00000000..1c50f58e --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/gray_plastic.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:gray_concrete" + } + }, + "pattern": [ + "CCC", + "C C", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:gray_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/gray_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/gray_plastic_ctm.json new file mode 100644 index 00000000..c8172812 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/gray_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:gray_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:gray_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/green_glow_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/green_glow_plastic.json new file mode 100644 index 00000000..50bae3a5 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/green_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:green_concrete" + }, + "G": { + "item": "minecraft:glowstone_dust" + } + }, + "pattern": [ + "CCC", + "CGC", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:green_glow_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/green_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/green_glow_plastic_ctm.json new file mode 100644 index 00000000..578ca737 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/green_glow_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:green_glow_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:green_glow_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/green_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/green_plastic.json new file mode 100644 index 00000000..4940bb42 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/green_plastic.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:green_concrete" + } + }, + "pattern": [ + "CCC", + "C C", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:green_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/green_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/green_plastic_ctm.json new file mode 100644 index 00000000..58ebf770 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/green_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:green_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:green_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/light_blue_glow_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/light_blue_glow_plastic.json new file mode 100644 index 00000000..92cce5df --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/light_blue_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:light_blue_concrete" + }, + "G": { + "item": "minecraft:glowstone_dust" + } + }, + "pattern": [ + "CCC", + "CGC", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:light_blue_glow_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/light_blue_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/light_blue_glow_plastic_ctm.json new file mode 100644 index 00000000..ada347dc --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/light_blue_glow_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:light_blue_glow_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:light_blue_glow_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/light_blue_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/light_blue_plastic.json new file mode 100644 index 00000000..5a34fc27 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/light_blue_plastic.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:light_blue_concrete" + } + }, + "pattern": [ + "CCC", + "C C", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:light_blue_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/light_blue_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/light_blue_plastic_ctm.json new file mode 100644 index 00000000..97dfff0a --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/light_blue_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:light_blue_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:light_blue_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/light_gray_glow_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/light_gray_glow_plastic.json new file mode 100644 index 00000000..6aa2335e --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/light_gray_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:light_gray_concrete" + }, + "G": { + "item": "minecraft:glowstone_dust" + } + }, + "pattern": [ + "CCC", + "CGC", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:light_gray_glow_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/light_gray_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/light_gray_glow_plastic_ctm.json new file mode 100644 index 00000000..17f66927 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/light_gray_glow_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:light_gray_glow_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:light_gray_glow_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/light_gray_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/light_gray_plastic.json new file mode 100644 index 00000000..1a7229c6 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/light_gray_plastic.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:light_gray_concrete" + } + }, + "pattern": [ + "CCC", + "C C", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:light_gray_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/light_gray_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/light_gray_plastic_ctm.json new file mode 100644 index 00000000..b4cbeb36 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/light_gray_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:light_gray_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:light_gray_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/lime_glow_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/lime_glow_plastic.json new file mode 100644 index 00000000..5a1536ff --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/lime_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:lime_concrete" + }, + "G": { + "item": "minecraft:glowstone_dust" + } + }, + "pattern": [ + "CCC", + "CGC", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:lime_glow_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/lime_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/lime_glow_plastic_ctm.json new file mode 100644 index 00000000..aa1e0146 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/lime_glow_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:lime_glow_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:lime_glow_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/lime_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/lime_plastic.json new file mode 100644 index 00000000..9cbcd91d --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/lime_plastic.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:lime_concrete" + } + }, + "pattern": [ + "CCC", + "C C", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:lime_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/lime_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/lime_plastic_ctm.json new file mode 100644 index 00000000..5ff1c4c1 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/lime_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:lime_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:lime_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/magenta_glow_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/magenta_glow_plastic.json new file mode 100644 index 00000000..73a05637 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/magenta_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:magenta_concrete" + }, + "G": { + "item": "minecraft:glowstone_dust" + } + }, + "pattern": [ + "CCC", + "CGC", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:magenta_glow_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/magenta_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/magenta_glow_plastic_ctm.json new file mode 100644 index 00000000..d14505fa --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/magenta_glow_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:magenta_glow_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:magenta_glow_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/magenta_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/magenta_plastic.json new file mode 100644 index 00000000..e23611f8 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/magenta_plastic.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:magenta_concrete" + } + }, + "pattern": [ + "CCC", + "C C", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:magenta_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/magenta_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/magenta_plastic_ctm.json new file mode 100644 index 00000000..c735bf22 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/magenta_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:magenta_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:magenta_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/me_pattern_assembly.json b/src/generated/resources/data/useless_mod/recipes/crafting/me_pattern_assembly.json new file mode 100644 index 00000000..b7a8ba29 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/me_pattern_assembly.json @@ -0,0 +1,24 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:chest" + }, + "P": { + "item": "ae2:pattern_provider" + }, + "R": { + "item": "minecraft:redstone" + } + }, + "pattern": [ + " R ", + "CPC", + " R " + ], + "result": { + "count": 1, + "item": "useless_mod:me_pattern_assembly" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/multiblock_alloy_furnace_core.json b/src/generated/resources/data/useless_mod/recipes/crafting/multiblock_alloy_furnace_core.json new file mode 100644 index 00000000..e15530b9 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/multiblock_alloy_furnace_core.json @@ -0,0 +1,24 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "F": { + "item": "useless_mod:advanced_alloy_furnace_block" + }, + "I": { + "item": "minecraft:iron_ingot" + }, + "R": { + "item": "minecraft:redstone" + } + }, + "pattern": [ + "IRI", + "RFR", + "IRI" + ], + "result": { + "count": 1, + "item": "useless_mod:multiblock_alloy_furnace_core" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/omniversal_furnace_casing.json b/src/generated/resources/data/useless_mod/recipes/crafting/omniversal_furnace_casing.json new file mode 100644 index 00000000..19cd78b8 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/omniversal_furnace_casing.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "N": { + "item": "minecraft:iron_nugget" + }, + "S": { + "item": "minecraft:smooth_stone" + } + }, + "pattern": [ + "NNN", + "NSN", + "NNN" + ], + "result": { + "count": 8, + "item": "useless_mod:omniversal_furnace_casing" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/omniversal_mold_hub.json b/src/generated/resources/data/useless_mod/recipes/crafting/omniversal_mold_hub.json new file mode 100644 index 00000000..6293024c --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/omniversal_mold_hub.json @@ -0,0 +1,24 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:chest" + }, + "H": { + "item": "minecraft:hopper" + }, + "R": { + "item": "minecraft:redstone" + } + }, + "pattern": [ + " R ", + "CHR", + " R " + ], + "result": { + "count": 1, + "item": "useless_mod:omniversal_mold_hub" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/omniversal_pattern_converter.json b/src/generated/resources/data/useless_mod/recipes/crafting/omniversal_pattern_converter.json new file mode 100644 index 00000000..34480d11 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/omniversal_pattern_converter.json @@ -0,0 +1,23 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "G": { + "item": "minecraft:yellow_dye" + }, + "L": { + "item": "ae2:logic_processor" + }, + "P": { + "item": "ae2:blank_pattern" + } + }, + "pattern": [ + "GPG", + " L " + ], + "result": { + "count": 1, + "item": "useless_mod:omniversal_pattern_converter" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/orange_glow_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/orange_glow_plastic.json new file mode 100644 index 00000000..f2535eec --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/orange_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:orange_concrete" + }, + "G": { + "item": "minecraft:glowstone_dust" + } + }, + "pattern": [ + "CCC", + "CGC", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:orange_glow_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/orange_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/orange_glow_plastic_ctm.json new file mode 100644 index 00000000..a39d1f26 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/orange_glow_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:orange_glow_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:orange_glow_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/orange_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/orange_plastic.json new file mode 100644 index 00000000..924f7a1a --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/orange_plastic.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:orange_concrete" + } + }, + "pattern": [ + "CCC", + "C C", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:orange_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/orange_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/orange_plastic_ctm.json new file mode 100644 index 00000000..5ea6eeb2 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/orange_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:orange_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:orange_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/passive_crafting_hatch.json b/src/generated/resources/data/useless_mod/recipes/crafting/passive_crafting_hatch.json new file mode 100644 index 00000000..04a9ab28 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/passive_crafting_hatch.json @@ -0,0 +1,27 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:clock" + }, + "E": { + "item": "ae2:export_bus" + }, + "P": { + "item": "useless_mod:me_pattern_assembly" + }, + "R": { + "item": "minecraft:redstone" + } + }, + "pattern": [ + " C ", + "EPE", + " R " + ], + "result": { + "count": 1, + "item": "useless_mod:passive_crafting_hatch" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/pink_glow_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/pink_glow_plastic.json new file mode 100644 index 00000000..3f9e7449 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/pink_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:pink_concrete" + }, + "G": { + "item": "minecraft:glowstone_dust" + } + }, + "pattern": [ + "CCC", + "CGC", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:pink_glow_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/pink_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/pink_glow_plastic_ctm.json new file mode 100644 index 00000000..f5132945 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/pink_glow_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:pink_glow_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:pink_glow_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/pink_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/pink_plastic.json new file mode 100644 index 00000000..973dd49c --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/pink_plastic.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:pink_concrete" + } + }, + "pattern": [ + "CCC", + "C C", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:pink_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/pink_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/pink_plastic_ctm.json new file mode 100644 index 00000000..6c078430 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/pink_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:pink_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:pink_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/possible_useful_ingot.json b/src/generated/resources/data/useless_mod/recipes/crafting/possible_useful_ingot.json new file mode 100644 index 00000000..982a75a3 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/possible_useful_ingot.json @@ -0,0 +1,42 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "1": { + "item": "useless_mod:useless_ingot_tier_1" + }, + "2": { + "item": "useless_mod:useless_ingot_tier_2" + }, + "3": { + "item": "useless_mod:useless_ingot_tier_3" + }, + "4": { + "item": "useless_mod:useless_ingot_tier_4" + }, + "5": { + "item": "useless_mod:useless_ingot_tier_5" + }, + "6": { + "item": "useless_mod:useless_ingot_tier_6" + }, + "7": { + "item": "useless_mod:useless_ingot_tier_7" + }, + "8": { + "item": "useless_mod:useless_ingot_tier_8" + }, + "9": { + "item": "useless_mod:useless_ingot_tier_9" + } + }, + "pattern": [ + "123", + "456", + "789" + ], + "result": { + "count": 1, + "item": "useless_mod:possible_useful_ingot" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/purple_glow_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/purple_glow_plastic.json new file mode 100644 index 00000000..8563f56d --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/purple_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:purple_concrete" + }, + "G": { + "item": "minecraft:glowstone_dust" + } + }, + "pattern": [ + "CCC", + "CGC", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:purple_glow_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/purple_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/purple_glow_plastic_ctm.json new file mode 100644 index 00000000..2784531f --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/purple_glow_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:purple_glow_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:purple_glow_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/purple_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/purple_plastic.json new file mode 100644 index 00000000..131210ea --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/purple_plastic.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:purple_concrete" + } + }, + "pattern": [ + "CCC", + "C C", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:purple_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/purple_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/purple_plastic_ctm.json new file mode 100644 index 00000000..65c0bdc5 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/purple_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:purple_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:purple_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/red_glow_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/red_glow_plastic.json new file mode 100644 index 00000000..7a1f5a4b --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/red_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:red_concrete" + }, + "G": { + "item": "minecraft:glowstone_dust" + } + }, + "pattern": [ + "CCC", + "CGC", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:red_glow_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/red_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/red_glow_plastic_ctm.json new file mode 100644 index 00000000..cedc964a --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/red_glow_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:red_glow_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:red_glow_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/red_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/red_plastic.json new file mode 100644 index 00000000..95e3e5c1 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/red_plastic.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:red_concrete" + } + }, + "pattern": [ + "CCC", + "C C", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:red_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/red_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/red_plastic_ctm.json new file mode 100644 index 00000000..ccc00bea --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/red_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:red_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:red_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/supervisor.json b/src/generated/resources/data/useless_mod/recipes/crafting/supervisor.json new file mode 100644 index 00000000..d070562a --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/supervisor.json @@ -0,0 +1,37 @@ +{ + "type": "minecraft:crafting_shapeless", + "category": "building", + "ingredients": [ + { + "item": "useless_mod:useful_ingot" + }, + { + "item": "minecraft:pink_wool" + }, + { + "item": "minecraft:black_wool" + }, + { + "item": "minecraft:gray_wool" + }, + { + "item": "minecraft:white_wool" + }, + { + "item": "minecraft:green_wool" + }, + { + "item": "minecraft:blue_wool" + }, + { + "item": "minecraft:yellow_wool" + }, + { + "item": "minecraft:orange_wool" + } + ], + "result": { + "count": 1, + "item": "useless_mod:supervisor" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/temperature_regulator.json b/src/generated/resources/data/useless_mod/recipes/crafting/temperature_regulator.json new file mode 100644 index 00000000..d3490378 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/temperature_regulator.json @@ -0,0 +1,13 @@ +{ + "type": "minecraft:crafting_shapeless", + "category": "misc", + "ingredients": [ + { + "tag": "useless_mod:plastic_blocks" + } + ], + "result": { + "count": 1, + "item": "useless_mod:temperature_regulator" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/useful_coil.json b/src/generated/resources/data/useless_mod/recipes/crafting/useful_coil.json new file mode 100644 index 00000000..03d63e67 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/useful_coil.json @@ -0,0 +1,24 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:copper_ingot" + }, + "I": { + "item": "useless_mod:useful_ingot" + }, + "R": { + "item": "minecraft:redstone" + } + }, + "pattern": [ + "CRC", + "RIR", + "CRC" + ], + "result": { + "count": 16, + "item": "useless_mod:useful_coil" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/useless_coil_tier_1.json b/src/generated/resources/data/useless_mod/recipes/crafting/useless_coil_tier_1.json new file mode 100644 index 00000000..c0318de2 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/useless_coil_tier_1.json @@ -0,0 +1,24 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:copper_ingot" + }, + "I": { + "item": "useless_mod:useless_ingot_tier_1" + }, + "R": { + "item": "minecraft:redstone" + } + }, + "pattern": [ + "CRC", + "RIR", + "CRC" + ], + "result": { + "count": 16, + "item": "useless_mod:useless_coil_tier_1" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/useless_coil_tier_2.json b/src/generated/resources/data/useless_mod/recipes/crafting/useless_coil_tier_2.json new file mode 100644 index 00000000..7fbf4588 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/useless_coil_tier_2.json @@ -0,0 +1,24 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:copper_ingot" + }, + "I": { + "item": "useless_mod:useless_ingot_tier_2" + }, + "R": { + "item": "minecraft:redstone" + } + }, + "pattern": [ + "CRC", + "RIR", + "CRC" + ], + "result": { + "count": 16, + "item": "useless_mod:useless_coil_tier_2" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/useless_coil_tier_3.json b/src/generated/resources/data/useless_mod/recipes/crafting/useless_coil_tier_3.json new file mode 100644 index 00000000..ce6e788c --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/useless_coil_tier_3.json @@ -0,0 +1,24 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:copper_ingot" + }, + "I": { + "item": "useless_mod:useless_ingot_tier_3" + }, + "R": { + "item": "minecraft:redstone" + } + }, + "pattern": [ + "CRC", + "RIR", + "CRC" + ], + "result": { + "count": 16, + "item": "useless_mod:useless_coil_tier_3" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/useless_coil_tier_4.json b/src/generated/resources/data/useless_mod/recipes/crafting/useless_coil_tier_4.json new file mode 100644 index 00000000..1602f89d --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/useless_coil_tier_4.json @@ -0,0 +1,24 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:copper_ingot" + }, + "I": { + "item": "useless_mod:useless_ingot_tier_4" + }, + "R": { + "item": "minecraft:redstone" + } + }, + "pattern": [ + "CRC", + "RIR", + "CRC" + ], + "result": { + "count": 16, + "item": "useless_mod:useless_coil_tier_4" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/useless_coil_tier_5.json b/src/generated/resources/data/useless_mod/recipes/crafting/useless_coil_tier_5.json new file mode 100644 index 00000000..7eab7a20 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/useless_coil_tier_5.json @@ -0,0 +1,24 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:copper_ingot" + }, + "I": { + "item": "useless_mod:useless_ingot_tier_5" + }, + "R": { + "item": "minecraft:redstone" + } + }, + "pattern": [ + "CRC", + "RIR", + "CRC" + ], + "result": { + "count": 16, + "item": "useless_mod:useless_coil_tier_5" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/useless_coil_tier_6.json b/src/generated/resources/data/useless_mod/recipes/crafting/useless_coil_tier_6.json new file mode 100644 index 00000000..303c78e0 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/useless_coil_tier_6.json @@ -0,0 +1,24 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:copper_ingot" + }, + "I": { + "item": "useless_mod:useless_ingot_tier_6" + }, + "R": { + "item": "minecraft:redstone" + } + }, + "pattern": [ + "CRC", + "RIR", + "CRC" + ], + "result": { + "count": 16, + "item": "useless_mod:useless_coil_tier_6" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/useless_coil_tier_7.json b/src/generated/resources/data/useless_mod/recipes/crafting/useless_coil_tier_7.json new file mode 100644 index 00000000..7688cf72 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/useless_coil_tier_7.json @@ -0,0 +1,24 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:copper_ingot" + }, + "I": { + "item": "useless_mod:useless_ingot_tier_7" + }, + "R": { + "item": "minecraft:redstone" + } + }, + "pattern": [ + "CRC", + "RIR", + "CRC" + ], + "result": { + "count": 16, + "item": "useless_mod:useless_coil_tier_7" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/useless_coil_tier_8.json b/src/generated/resources/data/useless_mod/recipes/crafting/useless_coil_tier_8.json new file mode 100644 index 00000000..2f46a6d1 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/useless_coil_tier_8.json @@ -0,0 +1,24 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:copper_ingot" + }, + "I": { + "item": "useless_mod:useless_ingot_tier_8" + }, + "R": { + "item": "minecraft:redstone" + } + }, + "pattern": [ + "CRC", + "RIR", + "CRC" + ], + "result": { + "count": 16, + "item": "useless_mod:useless_coil_tier_8" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/useless_coil_tier_9.json b/src/generated/resources/data/useless_mod/recipes/crafting/useless_coil_tier_9.json new file mode 100644 index 00000000..731607c2 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/useless_coil_tier_9.json @@ -0,0 +1,24 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:copper_ingot" + }, + "I": { + "item": "useless_mod:useless_ingot_tier_9" + }, + "R": { + "item": "minecraft:redstone" + } + }, + "pattern": [ + "CRC", + "RIR", + "CRC" + ], + "result": { + "count": 16, + "item": "useless_mod:useless_coil_tier_9" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/white_glow_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/white_glow_plastic.json new file mode 100644 index 00000000..196620fd --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/white_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:white_concrete" + }, + "G": { + "item": "minecraft:glowstone_dust" + } + }, + "pattern": [ + "CCC", + "CGC", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:white_glow_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/white_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/white_glow_plastic_ctm.json new file mode 100644 index 00000000..1fa6da3a --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/white_glow_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:white_glow_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:white_glow_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/white_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/white_plastic.json new file mode 100644 index 00000000..8e07c268 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/white_plastic.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:white_concrete" + } + }, + "pattern": [ + "CCC", + "C C", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:white_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/white_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/white_plastic_ctm.json new file mode 100644 index 00000000..227f33f2 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/white_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:white_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:white_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/yellow_glow_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/yellow_glow_plastic.json new file mode 100644 index 00000000..e91ac983 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/yellow_glow_plastic.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:yellow_concrete" + }, + "G": { + "item": "minecraft:glowstone_dust" + } + }, + "pattern": [ + "CCC", + "CGC", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:yellow_glow_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/yellow_glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/yellow_glow_plastic_ctm.json new file mode 100644 index 00000000..f442d2ab --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/yellow_glow_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:yellow_glow_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:yellow_glow_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/yellow_plastic.json b/src/generated/resources/data/useless_mod/recipes/crafting/yellow_plastic.json new file mode 100644 index 00000000..889c6539 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/yellow_plastic.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "C": { + "item": "minecraft:yellow_concrete" + } + }, + "pattern": [ + "CCC", + "C C", + "CCC" + ], + "result": { + "count": 8, + "item": "useless_mod:yellow_plastic" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/crafting/yellow_plastic_ctm.json b/src/generated/resources/data/useless_mod/recipes/crafting/yellow_plastic_ctm.json new file mode 100644 index 00000000..a473546c --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/crafting/yellow_plastic_ctm.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "P": { + "item": "useless_mod:yellow_plastic" + } + }, + "pattern": [ + "PPP", + "P P", + "PPP" + ], + "result": { + "count": 8, + "item": "useless_mod:yellow_plastic_ctm" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/endless_beaf_item.json b/src/generated/resources/data/useless_mod/recipes/endless_beaf_item.json new file mode 100644 index 00000000..4bcaea69 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/endless_beaf_item.json @@ -0,0 +1,42 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "A": { + "item": "useless_mod:useless_ingot_tier_5" + }, + "B": { + "item": "useless_mod:useless_ingot_tier_4" + }, + "C": { + "item": "useless_mod:useless_ingot_tier_3" + }, + "D": { + "item": "minecraft:diamond_pickaxe" + }, + "E": { + "item": "useless_mod:useless_ingot_tier_2" + }, + "F": { + "item": "minecraft:netherite_pickaxe" + }, + "G": { + "item": "useless_mod:useless_ingot_tier_1" + }, + "H": { + "item": "minecraft:golden_carrot" + }, + "I": { + "item": "minecraft:ghast_tear" + } + }, + "pattern": [ + "ABC", + "DEF", + "IGH" + ], + "result": { + "count": 1, + "item": "useless_mod:endless_beaf_item" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/mold/metal_mold_block.json b/src/generated/resources/data/useless_mod/recipes/mold/metal_mold_block.json new file mode 100644 index 00000000..ce11ecca --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/mold/metal_mold_block.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "X": { + "item": "useless_mod:useless_ingot_tier_1" + } + }, + "pattern": [ + "XXX", + "X X", + "XXX" + ], + "result": { + "count": 1, + "item": "useless_mod:metal_mold_block" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/mold/metal_mold_gear.json b/src/generated/resources/data/useless_mod/recipes/mold/metal_mold_gear.json new file mode 100644 index 00000000..bfe67e55 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/mold/metal_mold_gear.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "X": { + "item": "useless_mod:useless_ingot_tier_1" + } + }, + "pattern": [ + " X ", + "X X", + " X " + ], + "result": { + "count": 1, + "item": "useless_mod:metal_mold_gear" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/mold/metal_mold_plate.json b/src/generated/resources/data/useless_mod/recipes/mold/metal_mold_plate.json new file mode 100644 index 00000000..125fb7e4 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/mold/metal_mold_plate.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "X": { + "item": "useless_mod:useless_ingot_tier_1" + } + }, + "pattern": [ + "XXX" + ], + "result": { + "count": 1, + "item": "useless_mod:metal_mold_plate" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/mold/metal_mold_rod.json b/src/generated/resources/data/useless_mod/recipes/mold/metal_mold_rod.json new file mode 100644 index 00000000..2d718f31 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/mold/metal_mold_rod.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "X": { + "item": "useless_mod:useless_ingot_tier_1" + } + }, + "pattern": [ + " X", + " X ", + "X " + ], + "result": { + "count": 1, + "item": "useless_mod:metal_mold_rod" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/mold/metal_mold_wire.json b/src/generated/resources/data/useless_mod/recipes/mold/metal_mold_wire.json new file mode 100644 index 00000000..bd6dda74 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/mold/metal_mold_wire.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "X": { + "item": "useless_mod:useless_ingot_tier_1" + } + }, + "pattern": [ + " X ", + "XXX", + " X " + ], + "result": { + "count": 1, + "item": "useless_mod:metal_mold_wire" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/ore_generator_block.json b/src/generated/resources/data/useless_mod/recipes/ore_generator_block.json new file mode 100644 index 00000000..3f46ce74 --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/ore_generator_block.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "A": { + "item": "useless_mod:useful_ingot" + } + }, + "pattern": [ + "AAA", + "AAA", + "AAA" + ], + "result": { + "count": 1, + "item": "useless_mod:ore_generator_block" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/teleport/teleport_block.json b/src/generated/resources/data/useless_mod/recipes/teleport/teleport_block.json new file mode 100644 index 00000000..6c832a9e --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/teleport/teleport_block.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "A": { + "tag": "minecraft:planks" + }, + "B": { + "item": "minecraft:dirt" + } + }, + "pattern": [ + "AAA", + "ABA", + "AAA" + ], + "result": { + "count": 1, + "item": "useless_mod:teleport_block" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/teleport/teleport_block_2.json b/src/generated/resources/data/useless_mod/recipes/teleport/teleport_block_2.json new file mode 100644 index 00000000..7f7cb71c --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/teleport/teleport_block_2.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "A": { + "tag": "minecraft:planks" + }, + "B": { + "item": "minecraft:dirt" + } + }, + "pattern": [ + "BBB", + "BAB", + "BBB" + ], + "result": { + "count": 1, + "item": "useless_mod:teleport_block_2" + } +} diff --git a/src/generated/resources/data/useless_mod/recipes/teleport/teleport_block_3.json b/src/generated/resources/data/useless_mod/recipes/teleport/teleport_block_3.json new file mode 100644 index 00000000..620cbbcb --- /dev/null +++ b/src/generated/resources/data/useless_mod/recipes/teleport/teleport_block_3.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "A": { + "item": "minecraft:dirt" + }, + "B": { + "tag": "minecraft:planks" + } + }, + "pattern": [ + "BBB", + "AAA", + "BBB" + ], + "result": { + "count": 1, + "item": "useless_mod:teleport_block_3" + } +} diff --git a/src/generated/resources/data/useless_mod/tags/blocks/beef_tool_tier_locked.json b/src/generated/resources/data/useless_mod/tags/blocks/beef_tool_tier_locked.json new file mode 100644 index 00000000..86dde3df --- /dev/null +++ b/src/generated/resources/data/useless_mod/tags/blocks/beef_tool_tier_locked.json @@ -0,0 +1,3 @@ +{ + "values": [] +} diff --git a/src/generated/resources/data/useless_mod/tags/blocks/glow_plastic.json b/src/generated/resources/data/useless_mod/tags/blocks/glow_plastic.json new file mode 100644 index 00000000..2fbf8443 --- /dev/null +++ b/src/generated/resources/data/useless_mod/tags/blocks/glow_plastic.json @@ -0,0 +1,22 @@ +{ + "values": [ + "useless_mod:black_glow_plastic", + "useless_mod:red_glow_plastic", + "useless_mod:green_glow_plastic", + "useless_mod:brown_glow_plastic", + "useless_mod:blue_glow_plastic", + "useless_mod:purple_glow_plastic", + "useless_mod:cyan_glow_plastic", + "useless_mod:light_gray_glow_plastic", + "useless_mod:gray_glow_plastic", + "useless_mod:pink_glow_plastic", + "useless_mod:lime_glow_plastic", + "useless_mod:yellow_glow_plastic", + "useless_mod:light_blue_glow_plastic", + "useless_mod:magenta_glow_plastic", + "useless_mod:orange_glow_plastic", + "useless_mod:white_glow_plastic", + "useless_mod:dark_red_glow_plastic", + "useless_mod:aqua_glow_plastic" + ] +} diff --git a/src/generated/resources/data/useless_mod/tags/blocks/glow_plastic_ctm.json b/src/generated/resources/data/useless_mod/tags/blocks/glow_plastic_ctm.json new file mode 100644 index 00000000..4de839d8 --- /dev/null +++ b/src/generated/resources/data/useless_mod/tags/blocks/glow_plastic_ctm.json @@ -0,0 +1,22 @@ +{ + "values": [ + "useless_mod:black_glow_plastic_ctm", + "useless_mod:red_glow_plastic_ctm", + "useless_mod:green_glow_plastic_ctm", + "useless_mod:brown_glow_plastic_ctm", + "useless_mod:blue_glow_plastic_ctm", + "useless_mod:purple_glow_plastic_ctm", + "useless_mod:cyan_glow_plastic_ctm", + "useless_mod:light_gray_glow_plastic_ctm", + "useless_mod:gray_glow_plastic_ctm", + "useless_mod:pink_glow_plastic_ctm", + "useless_mod:lime_glow_plastic_ctm", + "useless_mod:yellow_glow_plastic_ctm", + "useless_mod:light_blue_glow_plastic_ctm", + "useless_mod:magenta_glow_plastic_ctm", + "useless_mod:orange_glow_plastic_ctm", + "useless_mod:white_glow_plastic_ctm", + "useless_mod:dark_red_glow_plastic_ctm", + "useless_mod:aqua_glow_plastic_ctm" + ] +} diff --git a/src/generated/resources/data/useless_mod/tags/blocks/omniversal_furnace_casings.json b/src/generated/resources/data/useless_mod/tags/blocks/omniversal_furnace_casings.json new file mode 100644 index 00000000..6a0e0db3 --- /dev/null +++ b/src/generated/resources/data/useless_mod/tags/blocks/omniversal_furnace_casings.json @@ -0,0 +1,6 @@ +{ + "values": [ + "useless_mod:omniversal_furnace_casing", + "useless_mod:passive_crafting_hatch" + ] +} diff --git a/src/generated/resources/data/useless_mod/tags/blocks/plastic.json b/src/generated/resources/data/useless_mod/tags/blocks/plastic.json new file mode 100644 index 00000000..145bb328 --- /dev/null +++ b/src/generated/resources/data/useless_mod/tags/blocks/plastic.json @@ -0,0 +1,22 @@ +{ + "values": [ + "useless_mod:black_plastic", + "useless_mod:red_plastic", + "useless_mod:green_plastic", + "useless_mod:brown_plastic", + "useless_mod:blue_plastic", + "useless_mod:purple_plastic", + "useless_mod:cyan_plastic", + "useless_mod:light_gray_plastic", + "useless_mod:gray_plastic", + "useless_mod:pink_plastic", + "useless_mod:lime_plastic", + "useless_mod:yellow_plastic", + "useless_mod:light_blue_plastic", + "useless_mod:magenta_plastic", + "useless_mod:orange_plastic", + "useless_mod:white_plastic", + "useless_mod:dark_red_plastic", + "useless_mod:aqua_plastic" + ] +} diff --git a/src/generated/resources/data/useless_mod/tags/blocks/plastic_blocks.json b/src/generated/resources/data/useless_mod/tags/blocks/plastic_blocks.json new file mode 100644 index 00000000..930747b2 --- /dev/null +++ b/src/generated/resources/data/useless_mod/tags/blocks/plastic_blocks.json @@ -0,0 +1,8 @@ +{ + "values": [ + "#useless_mod:plastic", + "#useless_mod:glow_plastic", + "#useless_mod:plastic_ctm", + "#useless_mod:glow_plastic_ctm" + ] +} diff --git a/src/generated/resources/data/useless_mod/tags/blocks/plastic_ctm.json b/src/generated/resources/data/useless_mod/tags/blocks/plastic_ctm.json new file mode 100644 index 00000000..b4539056 --- /dev/null +++ b/src/generated/resources/data/useless_mod/tags/blocks/plastic_ctm.json @@ -0,0 +1,22 @@ +{ + "values": [ + "useless_mod:black_plastic_ctm", + "useless_mod:red_plastic_ctm", + "useless_mod:green_plastic_ctm", + "useless_mod:brown_plastic_ctm", + "useless_mod:blue_plastic_ctm", + "useless_mod:purple_plastic_ctm", + "useless_mod:cyan_plastic_ctm", + "useless_mod:light_gray_plastic_ctm", + "useless_mod:gray_plastic_ctm", + "useless_mod:pink_plastic_ctm", + "useless_mod:lime_plastic_ctm", + "useless_mod:yellow_plastic_ctm", + "useless_mod:light_blue_plastic_ctm", + "useless_mod:magenta_plastic_ctm", + "useless_mod:orange_plastic_ctm", + "useless_mod:white_plastic_ctm", + "useless_mod:dark_red_plastic_ctm", + "useless_mod:aqua_plastic_ctm" + ] +} diff --git a/src/generated/resources/data/useless_mod/tags/items/catalysts.json b/src/generated/resources/data/useless_mod/tags/items/catalysts.json new file mode 100644 index 00000000..f0a18110 --- /dev/null +++ b/src/generated/resources/data/useless_mod/tags/items/catalysts.json @@ -0,0 +1,14 @@ +{ + "values": [ + "useless_mod:useless_ingot_tier_1", + "useless_mod:useless_ingot_tier_2", + "useless_mod:useless_ingot_tier_3", + "useless_mod:useless_ingot_tier_4", + "useless_mod:useless_ingot_tier_5", + "useless_mod:useless_ingot_tier_6", + "useless_mod:useless_ingot_tier_7", + "useless_mod:useless_ingot_tier_8", + "useless_mod:useless_ingot_tier_9", + "useless_mod:useful_ingot" + ] +} diff --git a/src/generated/resources/data/useless_mod/tags/items/molds.json b/src/generated/resources/data/useless_mod/tags/items/molds.json new file mode 100644 index 00000000..9973ac4e --- /dev/null +++ b/src/generated/resources/data/useless_mod/tags/items/molds.json @@ -0,0 +1,129 @@ +{ + "values": [ + "useless_mod:metal_mold_plate", + "useless_mod:metal_mold_rod", + "useless_mod:metal_mold_gear", + "useless_mod:metal_mold_wire", + "useless_mod:metal_mold_block", + { + "id": "powah:energizing_orb", + "required": false + }, + { + "id": "extendedcrafting:basic_table", + "required": false + }, + { + "id": "extendedcrafting:advanced_table", + "required": false + }, + { + "id": "extendedcrafting:elite_table", + "required": false + }, + { + "id": "extendedcrafting:ultimate_table", + "required": false + }, + { + "id": "extendedcrafting:compressor", + "required": false + }, + { + "id": "extendedcrafting:crafting_core", + "required": false + }, + { + "id": "extendedcrafting:ender_crafter", + "required": false + }, + { + "id": "extendedcrafting:flux_crafter", + "required": false + }, + { + "id": "neoecoae:integrated_working_station", + "required": false + }, + { + "id": "naturesaura:wood_stand", + "required": false + }, + { + "id": "naturesaura:nature_altar", + "required": false + }, + { + "id": "naturesaura:animal_spawner", + "required": false + }, + { + "id": "naturesaura:offering_table", + "required": false + }, + { + "id": "naturesaura:conversion_catalyst", + "required": false + }, + { + "id": "naturesaura:crushing_catalyst", + "required": false + }, + { + "id": "malum:spirit_crucible", + "required": false + }, + { + "id": "malum:spirit_altar", + "required": false + }, + { + "id": "create:spout", + "required": false + }, + { + "id": "create:mechanical_press", + "required": false + }, + { + "id": "create:mechanical_mixer", + "required": false + }, + { + "id": "create:basin", + "required": false + }, + { + "id": "create:millstone", + "required": false + }, + { + "id": "create:crushing_wheel", + "required": false + }, + { + "id": "create:encased_fan", + "required": false + }, + { + "id": "create:mechanical_crafter", + "required": false + }, + { + "id": "create:deployer", + "required": false + }, + { + "id": "create:mechanical_saw", + "required": false + }, + { + "id": "create:blaze_burner", + "required": false + }, + { + "id": "useless_mod:ritual_blueprint", + "required": false + } + ] +} diff --git a/src/generated/resources/data/useless_mod/tags/items/plastic_blocks.json b/src/generated/resources/data/useless_mod/tags/items/plastic_blocks.json new file mode 100644 index 00000000..bf07edcc --- /dev/null +++ b/src/generated/resources/data/useless_mod/tags/items/plastic_blocks.json @@ -0,0 +1,76 @@ +{ + "values": [ + "useless_mod:aqua_glow_plastic", + "useless_mod:aqua_glow_plastic_ctm", + "useless_mod:aqua_plastic", + "useless_mod:aqua_plastic_ctm", + "useless_mod:black_glow_plastic", + "useless_mod:black_glow_plastic_ctm", + "useless_mod:black_plastic", + "useless_mod:black_plastic_ctm", + "useless_mod:blue_glow_plastic", + "useless_mod:blue_glow_plastic_ctm", + "useless_mod:blue_plastic", + "useless_mod:blue_plastic_ctm", + "useless_mod:brown_glow_plastic", + "useless_mod:brown_glow_plastic_ctm", + "useless_mod:brown_plastic", + "useless_mod:brown_plastic_ctm", + "useless_mod:cyan_glow_plastic", + "useless_mod:cyan_glow_plastic_ctm", + "useless_mod:cyan_plastic", + "useless_mod:cyan_plastic_ctm", + "useless_mod:dark_red_glow_plastic", + "useless_mod:dark_red_glow_plastic_ctm", + "useless_mod:dark_red_plastic", + "useless_mod:dark_red_plastic_ctm", + "useless_mod:gray_glow_plastic", + "useless_mod:gray_glow_plastic_ctm", + "useless_mod:gray_plastic", + "useless_mod:gray_plastic_ctm", + "useless_mod:green_glow_plastic", + "useless_mod:green_glow_plastic_ctm", + "useless_mod:green_plastic", + "useless_mod:green_plastic_ctm", + "useless_mod:light_blue_glow_plastic", + "useless_mod:light_blue_glow_plastic_ctm", + "useless_mod:light_blue_plastic", + "useless_mod:light_blue_plastic_ctm", + "useless_mod:light_gray_glow_plastic", + "useless_mod:light_gray_glow_plastic_ctm", + "useless_mod:light_gray_plastic", + "useless_mod:light_gray_plastic_ctm", + "useless_mod:lime_glow_plastic", + "useless_mod:lime_glow_plastic_ctm", + "useless_mod:lime_plastic", + "useless_mod:lime_plastic_ctm", + "useless_mod:magenta_glow_plastic", + "useless_mod:magenta_glow_plastic_ctm", + "useless_mod:magenta_plastic", + "useless_mod:magenta_plastic_ctm", + "useless_mod:orange_glow_plastic", + "useless_mod:orange_glow_plastic_ctm", + "useless_mod:orange_plastic", + "useless_mod:orange_plastic_ctm", + "useless_mod:pink_glow_plastic", + "useless_mod:pink_glow_plastic_ctm", + "useless_mod:pink_plastic", + "useless_mod:pink_plastic_ctm", + "useless_mod:purple_glow_plastic", + "useless_mod:purple_glow_plastic_ctm", + "useless_mod:purple_plastic", + "useless_mod:purple_plastic_ctm", + "useless_mod:red_glow_plastic", + "useless_mod:red_glow_plastic_ctm", + "useless_mod:red_plastic", + "useless_mod:red_plastic_ctm", + "useless_mod:white_glow_plastic", + "useless_mod:white_glow_plastic_ctm", + "useless_mod:white_plastic", + "useless_mod:white_plastic_ctm", + "useless_mod:yellow_glow_plastic", + "useless_mod:yellow_glow_plastic_ctm", + "useless_mod:yellow_plastic", + "useless_mod:yellow_plastic_ctm" + ] +} diff --git a/src/main/resources/data/useless_mod/tags/items/useless_ingots.json b/src/generated/resources/data/useless_mod/tags/items/useless_ingots.json similarity index 95% rename from src/main/resources/data/useless_mod/tags/items/useless_ingots.json rename to src/generated/resources/data/useless_mod/tags/items/useless_ingots.json index c6ddd00e..f0a18110 100644 --- a/src/main/resources/data/useless_mod/tags/items/useless_ingots.json +++ b/src/generated/resources/data/useless_mod/tags/items/useless_ingots.json @@ -1,5 +1,4 @@ { - "replace": false, "values": [ "useless_mod:useless_ingot_tier_1", "useless_mod:useless_ingot_tier_2", @@ -12,4 +11,4 @@ "useless_mod:useless_ingot_tier_9", "useless_mod:useful_ingot" ] -} \ No newline at end of file +} diff --git a/src/main/java/com/sorrowmist/useless/UselessClient.java b/src/main/java/com/sorrowmist/useless/UselessClient.java new file mode 100644 index 00000000..3bc75cd0 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/UselessClient.java @@ -0,0 +1,18 @@ +package com.sorrowmist.useless; + +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod.EventBusSubscriber; +import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; +import net.minecraftforge.client.ConfigScreenHandler.ConfigScreenFactory; +import net.minecraftforge.fml.ModLoadingContext; +import com.sorrowmist.useless.client.gui.PortConfigScreen; + +@EventBusSubscriber(modid = UselessMod.MODID, value = Dist.CLIENT, bus = EventBusSubscriber.Bus.MOD) +public final class UselessClient { + @SubscribeEvent + public static void onClientSetup(FMLClientSetupEvent event) { + ModLoadingContext.get().registerExtensionPoint(ConfigScreenFactory.class, + () -> new ConfigScreenFactory((minecraft, parent) -> new PortConfigScreen(parent))); + } +} diff --git a/src/main/java/com/sorrowmist/useless/UselessMod.java b/src/main/java/com/sorrowmist/useless/UselessMod.java index d282b781..7d0aa6ea 100644 --- a/src/main/java/com/sorrowmist/useless/UselessMod.java +++ b/src/main/java/com/sorrowmist/useless/UselessMod.java @@ -1,294 +1,241 @@ package com.sorrowmist.useless; -import appeng.api.features.GridLinkables; import com.mojang.logging.LogUtils; -import com.sorrowmist.useless.blocks.ModMenuTypes; -import com.sorrowmist.useless.blocks.advancedalloyfurnace.AdvancedAlloyFurnaceScreen; -import com.sorrowmist.useless.config.ConfigManager; -import com.sorrowmist.useless.items.EndlessBeafItem; -import com.sorrowmist.useless.networking.ChainMiningTogglePacket; -import com.sorrowmist.useless.networking.ClearFluidPacket; -import com.sorrowmist.useless.networking.FluidInteractionPacket; -import com.sorrowmist.useless.networking.ModMessages; -import com.sorrowmist.useless.registry.RegistryHandler; -import net.minecraft.SharedConstants; -import net.minecraft.client.gui.screens.MenuScreens; -import net.minecraft.client.renderer.item.ItemProperties; +import com.sorrowmist.useless.command.UselessCommands; +import com.sorrowmist.useless.content.blocks.GlowPlasticBlock; +import com.sorrowmist.useless.content.items.BeefCropHarvest; +import com.sorrowmist.useless.content.items.EndlessBeafItem; +import com.sorrowmist.useless.content.items.BeefTimeAcceleration; +import com.sorrowmist.useless.utils.mining.RightClickChainer; +import com.sorrowmist.useless.compat.constructionwand.ConstructionWandLogic; +import com.sorrowmist.useless.content.recipe.adapters.RecipeAdapterCompatRegistry; +import com.sorrowmist.useless.core.component.UComponents; +import com.sorrowmist.useless.core.config.ConfigManager; +import com.sorrowmist.useless.init.ModBlockEntities; +import com.sorrowmist.useless.init.ModBlocks; +import com.sorrowmist.useless.init.ModCreativeTabs; +import com.sorrowmist.useless.init.ModEntities; +import com.sorrowmist.useless.init.ModItems; +import com.sorrowmist.useless.init.ModIngredientTypes; +import com.sorrowmist.useless.init.ModMenuType; +import com.sorrowmist.useless.init.ModNetwork; +import com.sorrowmist.useless.init.ModPOIs; +import com.sorrowmist.useless.init.ModRecipeSerializers; +import com.sorrowmist.useless.init.ModRecipeTypes; +import com.sorrowmist.useless.init.ModSounds; +import com.sorrowmist.useless.world.dimension.UselessDimensions; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; import net.minecraft.server.packs.PackType; -import net.minecraft.server.packs.PathPackResources; -import net.minecraft.server.packs.metadata.pack.PackMetadataSection; import net.minecraft.server.packs.repository.Pack; import net.minecraft.server.packs.repository.PackSource; -import net.minecraft.world.flag.FeatureFlagSet; -import net.minecraftforge.api.distmarker.Dist; -import net.minecraftforge.common.MinecraftForge; -import net.minecraftforge.event.AddPackFindersEvent; -import net.minecraftforge.event.BuildCreativeModeTabContentsEvent; -import net.minecraftforge.event.server.ServerStartingEvent; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.EquipmentSlot; +import com.sorrowmist.useless.compat.forge1201.OptionalIntegration; +import net.minecraft.world.entity.ai.attributes.AttributeModifier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraftforge.eventbus.api.EventPriority; import net.minecraftforge.eventbus.api.IEventBus; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.ModList; -import net.minecraftforge.fml.ModLoadingContext; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.config.ModConfig; -import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; -import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; -import net.minecraftforge.network.NetworkRegistry; -import net.minecraftforge.network.simple.SimpleChannel; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.AddPackFindersEvent; +import net.minecraftforge.event.ItemAttributeModifierEvent; +import net.minecraftforge.event.RegisterCommandsEvent; +import net.minecraftforge.event.entity.player.PlayerInteractEvent; +import net.minecraftforge.event.server.ServerStartingEvent; import org.slf4j.Logger; -import org.spongepowered.asm.mixin.Mixins; - -import java.nio.file.Path; -// The value here should match an entry in the META-INF/mods.toml file -@Mod(UselessMod.MOD_ID) +@Mod(UselessMod.MODID) public class UselessMod { - // Define mod id in a common place for everything to reference - public static final String MOD_ID = "useless_mod"; - - // Directly reference a slf4j logger + public static final String MODID = "useless_mod"; public static final Logger LOGGER = LogUtils.getLogger(); public UselessMod() { - IEventBus modEventBus = FMLJavaModLoadingContext.get().getModEventBus(); - - // 初始化所有内容 - RegistryHandler.initAll(modEventBus); - - // 注册配置 - registerConfig(); - modEventBus.addListener(this::onCommonSetup); - // 注册网络消息 - ModMessages.register(); - - // Register the commonSetup method for modloading + com.sorrowmist.useless.compat.forge1201.capability.RegisterCapabilitiesEvent.initialize(); + IEventBus modEventBus=net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext.get().getModEventBus(); + var modContext=net.minecraftforge.fml.ModLoadingContext.get(); + modEventBus.addListener((FMLCommonSetupEvent event) -> event.enqueueWork(() -> { + ModIngredientTypes.register(); + modEventBus.post(new com.sorrowmist.useless.compat.forge1201.capability.RegisterCapabilitiesEvent()); + modEventBus.post(new com.sorrowmist.useless.compat.forge1201.network.RegisterPayloadHandlersEvent(MODID)); + })); modEventBus.addListener(this::commonSetup); + modEventBus.addListener(this::registerBuiltinResourcePacks); + UComponents.init(modEventBus); + + ModBlocks.BLOCKS.register(modEventBus); + ModItems.ITEMS.register(modEventBus); + ModEntities.ENTITY_TYPES.register(modEventBus); + ModBlockEntities.register(modEventBus); + ModMenuType.register(modEventBus); + ModRecipeSerializers.RECIPE_SERIALIZERS.register(modEventBus); + ModRecipeTypes.RECIPE_TYPES.register(modEventBus); + ModSounds.SOUNDS.register(modEventBus); + ModPOIs.POI_TYPES.register(modEventBus); + + ModCreativeTabs.CREATIVE_TAB.register(modEventBus); + + modEventBus.addListener(ModNetwork::registerPayloadHandlers); + + GlowPlasticBlock.BLOCKS.register(modEventBus); + GlowPlasticBlock.ITEMS.register(modEventBus); + + // 无用型紧凑 C9/F9/L9:只在 ECO 存在时注册。 + // 这里的类加载是条件性的——ECO 缺席时 NeoEcoCompactRegistry 永远不会被初始化。 + if (ModList.get().isLoaded("neoecoae")) { + OptionalIntegration.call("neoecoae", "com.sorrowmist.useless.compat.neoecoae.compact.NeoEcoCompactRegistry", + "init", Void.class, null, new Class[]{IEventBus.class}, modEventBus); + } + + // 纬度注册 + UselessDimensions.init(modEventBus); - // Register ourselves for server and other game events we are interested in MinecraftForge.EVENT_BUS.register(this); - // Register EndlessBeafItem for its block break event listeners - MinecraftForge.EVENT_BUS.register(EndlessBeafItem.class); - // Register the item to a creative tab - modEventBus.addListener(this::addCreative); - // 注册内置材质包 - modEventBus.addListener(this::setupBuiltinPack); -// ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, ConfigManager.SPEC); - // 注册 Mixin 配置 - Mixins.addConfiguration("useless_mod.mixins.json"); - - // 在mod构造阶段注册GridLinkableHandler,确保在无线访问点槽位创建之前完成注册 - // 这里使用事件队列来确保在所有物品注册完成后执行 - modEventBus.addListener((FMLCommonSetupEvent event) -> { - event.enqueueWork(() -> { - try { - // 注册EndlessBeafItem的GridLinkableHandler,使其可以与无线访问点绑定 - GridLinkables.register(EndlessBeafItem.ENDLESS_BEAF_ITEM.get(), EndlessBeafItem.LINKABLE_HANDLER); - GridLinkables.register(EndlessBeafItem.ENDLESS_BEAF_WRENCH.get(), EndlessBeafItem.LINKABLE_HANDLER); - GridLinkables.register(EndlessBeafItem.ENDLESS_BEAF_SCREWDRIVER.get(), EndlessBeafItem.LINKABLE_HANDLER); - GridLinkables.register(EndlessBeafItem.ENDLESS_BEAF_MALLET.get(), EndlessBeafItem.LINKABLE_HANDLER); - GridLinkables.register(EndlessBeafItem.ENDLESS_BEAF_CROWBAR.get(), EndlessBeafItem.LINKABLE_HANDLER); - GridLinkables.register(EndlessBeafItem.ENDLESS_BEAF_HAMMER.get(), EndlessBeafItem.LINKABLE_HANDLER); - } catch (Exception e) { - // Ignore exception if GridLinkables registration fails - } - }); - }); + modContext.registerConfig(ModConfig.Type.COMMON, ConfigManager.COMMON_SPEC, "useless_mod-common.toml"); + modContext.registerConfig(ModConfig.Type.CLIENT, ConfigManager.CLIENT_SPEC, "useless_mod-client.toml"); + modContext.registerConfig(ModConfig.Type.SERVER, ConfigManager.SERVER_SPEC, "useless_mod-server.toml"); } - /** - * 注册配置 - */ - private void registerConfig() { - // 注册通用配置 - ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, ConfigManager.SPEC, "useless_mod-common.toml"); + // 便捷 ResourceLocation 工具 + public static ResourceLocation id(String path) { + return new ResourceLocation(MODID, path); } - // 在模组主类中添加网络注册 - public static final SimpleChannel NETWORK = NetworkRegistry.newSimpleChannel( - ResourceLocation.fromNamespaceAndPath(MOD_ID, "main"), - () -> "1.0", - "1.0"::equals, - "1.0"::equals - ); + private void commonSetup(FMLCommonSetupEvent event) { + RecipeAdapterCompatRegistry.init(event); + if (ModList.get().isLoaded("neoecoae")) { + OptionalIntegration.call("neoecoae", "com.sorrowmist.useless.compat.neoecoae.NeoEcoCompat", + "initialize", Void.class, null, new Class[]{}); + } + } + private void registerBuiltinResourcePacks(AddPackFindersEvent event) { + if (event.getPackType() != PackType.CLIENT_RESOURCES) return; - public void onCommonSetup(FMLCommonSetupEvent event) { - event.enqueueWork(() -> { - int packetId = 0; - NETWORK.registerMessage(packetId++, FluidInteractionPacket.class, - FluidInteractionPacket::encode, FluidInteractionPacket::decode, - FluidInteractionPacket::handle); - NETWORK.registerMessage(packetId++, ClearFluidPacket.class, - ClearFluidPacket::encode, ClearFluidPacket::decode, - ClearFluidPacket::handle); - - NETWORK.registerMessage(packetId++, ChainMiningTogglePacket.class, - ChainMiningTogglePacket::encode, ChainMiningTogglePacket::decode, - ChainMiningTogglePacket::handle); - }); - } - /** - * 通用设置 - */ - private void commonSetup(final FMLCommonSetupEvent event) { - // 注册EndlessBeafItem的GridLinkableHandler,使其可以与无线访问点绑定 - event.enqueueWork(() -> { - GridLinkables.register(EndlessBeafItem.ENDLESS_BEAF_ITEM.get(), EndlessBeafItem.LINKABLE_HANDLER); - GridLinkables.register(EndlessBeafItem.ENDLESS_BEAF_WRENCH.get(), EndlessBeafItem.LINKABLE_HANDLER); - GridLinkables.register(EndlessBeafItem.ENDLESS_BEAF_SCREWDRIVER.get(), EndlessBeafItem.LINKABLE_HANDLER); - GridLinkables.register(EndlessBeafItem.ENDLESS_BEAF_MALLET.get(), EndlessBeafItem.LINKABLE_HANDLER); - GridLinkables.register(EndlessBeafItem.ENDLESS_BEAF_CROWBAR.get(), EndlessBeafItem.LINKABLE_HANDLER); - GridLinkables.register(EndlessBeafItem.ENDLESS_BEAF_HAMMER.get(), EndlessBeafItem.LINKABLE_HANDLER); + var root = ModList.get().getModFileById(MODID).getFile().findResource("xia"); + event.addRepositorySource(consumer -> { + Pack pack = Pack.readMetaAndCreate("useless_mod:xia", + Component.translatable("pack.useless_mod.xia.name"), false, + id -> new net.minecraftforge.resource.PathPackResources(id, true, root), + PackType.CLIENT_RESOURCES, Pack.Position.TOP, PackSource.BUILT_IN); + if (pack != null) consumer.accept(pack); }); } - /** - * 添加创造模式标签内容 - */ - private void addCreative(BuildCreativeModeTabContentsEvent event) { - // 这里可以添加物品到创造模式标签 - } + @SubscribeEvent + public void onServerStarting(ServerStartingEvent event) {} - /** - * 服务器启动事件 - */ @SubscribeEvent - public void onServerStarting(ServerStartingEvent event) { - // 服务器启动时执行的操作 + public void onRegisterCommands(RegisterCommandsEvent event) { + UselessCommands.register(event.getDispatcher()); } - /** - * 客户端事件订阅器 - */ - @Mod.EventBusSubscriber(modid = MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD, value = Dist.CLIENT) - public static class ClientModEvents { - @SubscribeEvent - public static void onClientSetup(FMLClientSetupEvent event) { - // 客户端设置 - // 注册物品模型属性 - event.enqueueWork(() -> { - registerItemModelProperties(); - }); + @SubscribeEvent(priority = EventPriority.HIGHEST, receiveCanceled = true) + public void onRightClickBlock(PlayerInteractEvent.RightClickBlock event) { + if (ConstructionWandLogic.handleRightClickBlock(event)) return; + + ItemStack stack = event.getItemStack(); + + // 顺手收菜:先于其它模组的右键收菜逻辑收获成熟作物,并将种子保留在地里, + // 避免部分整合包的收菜功能对造化杖连根拔除作物。 + // 潜行右键时不接管,以便将本次交互让给其它模组。 + // 按住连锁键(Tab)时按连锁范围整片收。 + if (!event.isCanceled() + && stack.getItem() instanceof EndlessBeafItem + && EndlessBeafItem.isCropHarvestEnabled(stack) + && !event.getEntity().isShiftKeyDown() + && BeefCropHarvest.isHarvestable(event.getLevel().getBlockState(event.getPos()))) { + if (event.getLevel() instanceof ServerLevel serverLevel) { + if (RightClickChainer.shouldChain(event.getEntity(), stack)) { + RightClickChainer.harvestCrops(serverLevel, event.getPos(), event.getEntity(), stack); + } else { + BeefCropHarvest.harvest(serverLevel, event.getPos(), event.getEntity(), stack); + } + } + event.setCanceled(true); + event.setCancellationResult(InteractionResult.sidedSuccess(event.getLevel().isClientSide())); + return; + } + + if (stack.getItem() instanceof EndlessBeafItem) { + InteractionResult teleportResult = EndlessBeafItem.tryTeleport( + event.getLevel(), event.getEntity(), stack); + if (teleportResult != InteractionResult.PASS) { + event.setCanceled(true); + event.setCancellationResult(teleportResult); + return; + } - // 注册屏幕 - event.enqueueWork(() -> { - MenuScreens.register(ModMenuTypes.ADVANCED_ALLOY_FURNACE_MENU.get(), AdvancedAlloyFurnaceScreen::new); - }); + InteractionResult timeAccelerationResult = BeefTimeAcceleration.tryUse( + new UseOnContext(event.getEntity(), event.getHand(), event.getHitVec())); + if (timeAccelerationResult != InteractionResult.PASS) { + event.setCanceled(true); + event.setCancellationResult(timeAccelerationResult); + return; + } } - private static void registerItemModelProperties() { - // 为所有EndlessBeafItem注册模型属性,包括主物品和所有子物品 - ResourceLocation silkTouchMode = ResourceLocation.fromNamespaceAndPath(UselessMod.MOD_ID, "silk_touch_mode"); - - // 注册主物品模型属性 - ItemProperties.register(EndlessBeafItem.ENDLESS_BEAF_ITEM.get(), - silkTouchMode, - (stack, level, entity, seed) -> { - if (stack.getItem() instanceof EndlessBeafItem item) { - return item.isSilkTouchMode(stack) ? 1.0F : 0.0F; - } - return 0.0F; - }); - - // 注册扳手模型属性 - ItemProperties.register(EndlessBeafItem.ENDLESS_BEAF_WRENCH.get(), - silkTouchMode, - (stack, level, entity, seed) -> { - if (stack.getItem() instanceof EndlessBeafItem item) { - return item.isSilkTouchMode(stack) ? 1.0F : 0.0F; - } - return 0.0F; - }); - - // 注册螺丝刀模型属性 - ItemProperties.register(EndlessBeafItem.ENDLESS_BEAF_SCREWDRIVER.get(), - silkTouchMode, - (stack, level, entity, seed) -> { - if (stack.getItem() instanceof EndlessBeafItem item) { - return item.isSilkTouchMode(stack) ? 1.0F : 0.0F; - } - return 0.0F; - }); - - // 注册锤子模型属性 - ItemProperties.register(EndlessBeafItem.ENDLESS_BEAF_MALLET.get(), - silkTouchMode, - (stack, level, entity, seed) -> { - if (stack.getItem() instanceof EndlessBeafItem item) { - return item.isSilkTouchMode(stack) ? 1.0F : 0.0F; - } - return 0.0F; - }); - - // 注册撬棍模型属性 - ItemProperties.register(EndlessBeafItem.ENDLESS_BEAF_CROWBAR.get(), - silkTouchMode, - (stack, level, entity, seed) -> { - if (stack.getItem() instanceof EndlessBeafItem item) { - return item.isSilkTouchMode(stack) ? 1.0F : 0.0F; - } - return 0.0F; - }); - - // 注册铁锤模型属性 - ItemProperties.register(EndlessBeafItem.ENDLESS_BEAF_HAMMER.get(), - silkTouchMode, - (stack, level, entity, seed) -> { - if (stack.getItem() instanceof EndlessBeafItem item) { - return item.isSilkTouchMode(stack) ? 1.0F : 0.0F; - } - return 0.0F; - }); + + InteractionResult occultismResult = trySpawnMarkedOccultismSpirit(event); + if (occultismResult != InteractionResult.PASS) { + event.setCanceled(true); + event.setCancellationResult(occultismResult); + return; } - } - // 调试模式 - public static final boolean DEBUG = true; + if (!(stack.getItem() instanceof EndlessBeafItem)) return; - private void setupBuiltinPack(AddPackFindersEvent event) { - if (event.getPackType() == PackType.CLIENT_RESOURCES) { - // 注册god内置材质包 - Path resourcePath = ModList.get().getModFileById(UselessMod.MOD_ID).getFile().findResource("god"); - if (resourcePath != null) { - PathPackResources pack = new PathPackResources( - ModList.get().getModFileById(UselessMod.MOD_ID).getFile().getFileName() + ":" + resourcePath, - resourcePath, - true - ); + InteractionResult result = EndlessBeafItem.trySummonLightningForCollector(event.getLevel(), event.getPos(), event.getEntity()); + if (result == InteractionResult.PASS) return; - PackMetadataSection metadata = new PackMetadataSection( - Component.translatable("pack.useless_mod.god.description"), - SharedConstants.getCurrentVersion().getPackVersion(PackType.CLIENT_RESOURCES) - ); + event.setCanceled(true); + event.setCancellationResult(result); + } - event.addRepositorySource(source -> - source.accept(Pack.create( - "builtin/god", - Component.translatable("pack.useless_mod.god.name"), - false, - string -> pack, - new Pack.Info( - metadata.getDescription(), - metadata.getPackFormat(PackType.SERVER_DATA), - metadata.getPackFormat(PackType.CLIENT_RESOURCES), - FeatureFlagSet.of(), - pack.isHidden() - ), - PackType.CLIENT_RESOURCES, - Pack.Position.TOP, - false, - PackSource.BUILT_IN - )) - ); - } + @SubscribeEvent(priority = EventPriority.HIGHEST, receiveCanceled = true) + public void onRightClickItem(PlayerInteractEvent.RightClickItem event) { + ConstructionWandLogic.handleRightClickItem(event); + } + + private static InteractionResult trySpawnMarkedOccultismSpirit(PlayerInteractEvent.RightClickBlock event) { + if (!ModList.get().isLoaded("occultism")) { + return InteractionResult.PASS; } + return OccultismCompat.trySpawnMarkedSpirit(event); } - public static void logDebug(String message) { - if (DEBUG) { - LOGGER.debug("[USELESS DEBUG] " + message); + /** Isolates the optional Occultism class reference from the main mod entrypoint. */ + private static final class OccultismCompat { + private static InteractionResult trySpawnMarkedSpirit(PlayerInteractEvent.RightClickBlock event) { + return OptionalIntegration.call("occultism", + "com.sorrowmist.useless.content.recipe.adapters.occultism.OccultismSpiritEggHandler", + "trySpawn", InteractionResult.class, InteractionResult.PASS, + new Class[]{PlayerInteractEvent.RightClickBlock.class}, event); } } + + @SubscribeEvent + public void onItemAttributeModifiers(ItemAttributeModifierEvent event) { + if (!(event.getItemStack().getItem() instanceof EndlessBeafItem)) return; + + if (event.getSlotType() != EquipmentSlot.MAINHAND) return; + event.removeAttribute(Attributes.ATTACK_DAMAGE); + event.addModifier(Attributes.ATTACK_DAMAGE, EndlessBeafItem.createAttackDamageModifier()); + event.removeAttribute(Attributes.ATTACK_SPEED); + event.addModifier(Attributes.ATTACK_SPEED, EndlessBeafItem.createAttackSpeedModifier()); + event.addModifier(net.minecraftforge.common.ForgeMod.ENTITY_REACH.get(), + new AttributeModifier(java.util.UUID.nameUUIDFromBytes("useless_mod:beef_entity_reach".getBytes(java.nio.charset.StandardCharsets.UTF_8)), + "useless_mod_beef_entity_reach", ConfigManager.getBeefToolEntityInteractionRange(), + AttributeModifier.Operation.ADDITION)); + event.addModifier(net.minecraftforge.common.ForgeMod.BLOCK_REACH.get(), + new AttributeModifier(java.util.UUID.nameUUIDFromBytes("useless_mod:beef_block_reach".getBytes(java.nio.charset.StandardCharsets.UTF_8)), + "useless_mod_beef_block_reach", ConfigManager.getBeefToolBlockInteractionRange(), + AttributeModifier.Operation.ADDITION)); + } } diff --git a/src/main/java/com/sorrowmist/useless/api/EnumColor.java b/src/main/java/com/sorrowmist/useless/api/EnumColor.java deleted file mode 100644 index 5ee3b916..00000000 --- a/src/main/java/com/sorrowmist/useless/api/EnumColor.java +++ /dev/null @@ -1,144 +0,0 @@ -package com.sorrowmist.useless.api; - -/* - * This file is part of Mekanism. - * - * Copyright (c) 2017-2025 Aidan C. Brady - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import net.minecraft.world.item.DyeColor; -import net.minecraft.world.level.material.MapColor; -import javax.annotation.Nullable; - -public enum EnumColor { - BLACK("Black", "black", new int[]{64, 64, 64}, DyeColor.BLACK), - RED("Red", "red", new int[]{255, 56, 60}, DyeColor.RED), - GREEN("Green", "green", new int[]{89, 193, 95}, DyeColor.GREEN), - BROWN("Brown", "brown", new int[]{161, 118, 73}, DyeColor.BROWN), - BLUE("Blue", "blue", new int[]{54, 107, 208}, DyeColor.BLUE), - PURPLE("Purple", "purple", new int[]{164, 96, 217}, DyeColor.PURPLE), - CYAN("Cyan", "cyan", new int[]{0, 243, 208}, DyeColor.CYAN), - LIGHT_GRAY("Light Gray", "light_gray", new int[]{207, 207, 207}, DyeColor.LIGHT_GRAY), - GRAY("Gray", "gray", new int[]{122, 122, 122}, DyeColor.GRAY), - PINK("Pink", "pink", new int[]{255, 188, 196}, DyeColor.PINK), - LIME("Lime", "lime", new int[]{117, 255, 137}, DyeColor.LIME), - YELLOW("Yellow", "yellow", new int[]{255, 221, 79}, DyeColor.YELLOW), - LIGHT_BLUE("Light Blue", "light_blue", new int[]{85, 158, 255}, DyeColor.LIGHT_BLUE), - MAGENTA("Magenta", "magenta", new int[]{213, 94, 203}, DyeColor.MAGENTA), - ORANGE("Orange", "orange", new int[]{255, 161, 96}, DyeColor.ORANGE), - WHITE("White", "white", new int[]{255, 255, 255}, DyeColor.WHITE), - DARK_RED("Dark Red", "dark_red", new int[]{201, 7, 31}, MapColor.NETHER, null), - AQUA("Aqua", "aqua", new int[]{48, 255, 249}, MapColor.COLOR_LIGHT_BLUE, null); - - private final String englishName; - private final String registryPrefix; - private final int[] rgbCode; - @Nullable - private final DyeColor dyeColor; - private final MapColor mapColor; - - EnumColor(String englishName, String registryPrefix, int[] rgbCode, DyeColor dyeColor) { - this(englishName, registryPrefix, rgbCode, dyeColor.getMapColor(), dyeColor); - } - - EnumColor(String englishName, String registryPrefix, int[] rgbCode, MapColor mapColor, @Nullable DyeColor dyeColor) { - this.englishName = englishName; - this.registryPrefix = registryPrefix; - this.rgbCode = rgbCode; - this.dyeColor = dyeColor; - this.mapColor = mapColor; - } - - public String getRegistryPrefix() { - return registryPrefix; - } - - public String getEnglishName() { - return englishName; - } - - public MapColor getMapColor() { - return mapColor; - } - - @Nullable - public DyeColor getDyeColor() { - return dyeColor; - } - - /** - * 获取RGB整数值(ARGB格式,Alpha为255) - */ - public int getRgb() { - return (0xFF << 24) | (rgbCode[0] << 16) | (rgbCode[1] << 8) | rgbCode[2]; - } - - /** - * 获取RGB数组 [r, g, b] - */ - public int[] getRgbCode() { - return rgbCode.clone(); // 返回副本防止外部修改 - } - - /** - * 获取RGB浮点数组 [r, g, b],范围 0.0-1.0 - */ - public float[] getRgbFloat() { - return new float[] { - rgbCode[0] / 255.0f, - rgbCode[1] / 255.0f, - rgbCode[2] / 255.0f - }; - } - - - /** - * 按定义顺序返回所有颜色 - */ - public static EnumColor[] valuesInOrder() { - return values(); - } - - /** - * 根据名称获取颜色(不区分大小写) - */ - @Nullable - public static EnumColor byName(String name) { - for (EnumColor color : values()) { - if (color.registryPrefix.equalsIgnoreCase(name) || - color.englishName.equalsIgnoreCase(name)) { - return color; - } - } - return null; - } - - /** - * 根据索引获取颜色 - */ - public static EnumColor byIndex(int index) { - if (index < 0 || index >= values().length) { - return WHITE; // 默认返回白色 - } - return values()[index]; - } - -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/api/crafting/SmartDoublingCraftingProvider.java b/src/main/java/com/sorrowmist/useless/api/crafting/SmartDoublingCraftingProvider.java new file mode 100644 index 00000000..513b874b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/crafting/SmartDoublingCraftingProvider.java @@ -0,0 +1,5 @@ +package com.sorrowmist.useless.api.crafting; + +/** Marks an in-mod AE crafting provider as eligible for native smart doubling. */ +public interface SmartDoublingCraftingProvider { +} diff --git a/src/main/java/com/sorrowmist/useless/api/crafting/bigint/AlloyFurnaceBigIntegerApi.java b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/AlloyFurnaceBigIntegerApi.java new file mode 100644 index 00000000..b1a3401c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/AlloyFurnaceBigIntegerApi.java @@ -0,0 +1,118 @@ +package com.sorrowmist.useless.api.crafting.bigint; + +import appeng.api.networking.IGrid; +import appeng.api.networking.IGridNode; +import appeng.core.definitions.AEItems; +import com.sorrowmist.useless.init.ModItems; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +/** + * 多方块万象合金炉 bigint 能力的发现入口。 + * + *

用法(完整示例见 {@code wiki/ALLOY_FURNACE_BIGINT_API_ZH_CN.md}):

+ * + *
{@code
+ * for (AlloyFurnaceBigIntegerTarget target : AlloyFurnaceBigIntegerApi.findTargets(grid)) {
+ *     AlloyFurnaceBigIntegerCapacity capacity =
+ *             target.capacity(pattern, unitPrototype, requestedCount);
+ *     if (!capacity.isAvailable()) {
+ *         continue;
+ *     }
+ *     AlloyFurnaceBigIntegerBatch batch =
+ *             target.admit(pattern, unitPrototype, capacity.accepted(), binding);
+ *     if (batch != null && batch.commit(unitPrototype)) {
+ *         // 材料所有权已转移给机器;产物会切段写回 ME 网络,并通过 binding 回调给你
+ *     }
+ * }
+ * }
+ * + *

为什么用网格节点遍历而不是 {@code ICraftingService}:AE2 19.2.17 的 + * {@code ICraftingService} 只公开了 {@code getCraftingFor(AEKey)} 与 {@code getCpus()}, + * 没有「枚举全部供应器」的公开接口。遍历 {@code IGrid#getNodes()} 并用 + * {@link IGridNode#getOwner()} 判 {@link AlloyFurnaceBigIntegerProvider} 是唯一稳定的公开路径。

+ * + *

仅服务器线程。

+ */ +public final class AlloyFurnaceBigIntegerApi { + private AlloyFurnaceBigIntegerApi() { + } + + /** + * 找出网格里当前可用的全部大数目标,按 {@link AlloyFurnaceBigIntegerTarget#machineIdentity()} 去重。 + * + * @param grid 目标网格;{@code null} 时返回空列表 + * @return 不可变的候选列表;同一台物理机器只会出现一次 + */ + public static @NotNull List findTargets(@Nullable IGrid grid) { + if (grid == null) { + return List.of(); + } + List targets = new ArrayList<>(); + Set seen = new HashSet<>(); + for (IGridNode node : grid.getNodes()) { + if (!(node.getOwner() instanceof AlloyFurnaceBigIntegerProvider provider)) { + continue; + } + AlloyFurnaceBigIntegerTarget target = provider.bigIntegerTarget(); + if (target != null && seen.add(target.machineIdentity())) { + targets.add(target); + } + } + return Collections.unmodifiableList(targets); + } + + /** + * 按物理机器身份查找单个目标。 + * + * @param grid 目标网格 + * @param machineIdentity {@link AlloyFurnaceBigIntegerTarget#machineIdentity()} 的取值({@code 维度@x,y,z}) + * @return 命中的目标;机器不在线或身份不匹配时为空 + */ + public static @NotNull Optional findTarget( + @Nullable IGrid grid, @Nullable String machineIdentity) { + if (machineIdentity == null || machineIdentity.isBlank()) { + return Optional.empty(); + } + for (AlloyFurnaceBigIntegerTarget target : findTargets(grid)) { + if (machineIdentity.equals(target.machineIdentity())) { + return Optional.of(target); + } + } + return Optional.empty(); + } + + /** + * @return 多方块万象合金炉接受的样板物品 id 集合(AE2 合成样板 + 本模组万象样板)。 + * + *

供调用方在昂贵的解码之前先做一次廉价过滤;真正的判定仍以 + * {@link AlloyFurnaceBigIntegerTarget#capacity} 的返回值为准。

+ */ + public static @NotNull Set acceptedPatternKinds() { + Set kinds = new LinkedHashSet<>(2); + addKey(kinds, AEItems.CRAFTING_PATTERN.asItem()); + addKey(kinds, ModItems.OMNIVERSAL_PATTERN.get()); + return Collections.unmodifiableSet(kinds); + } + + /** 注册表未就绪时 {@code getKey} 可能返回 null,这里静默跳过而不是把 null 放进集合。 */ + private static void addKey(Set kinds, @Nullable net.minecraft.world.item.Item item) { + if (item == null) { + return; + } + ResourceLocation key = BuiltInRegistries.ITEM.getKey(item); + if (key != null) { + kinds.add(key); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/crafting/bigint/AlloyFurnaceBigIntegerBatch.java b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/AlloyFurnaceBigIntegerBatch.java new file mode 100644 index 00000000..2b33d80e --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/AlloyFurnaceBigIntegerBatch.java @@ -0,0 +1,47 @@ +package com.sorrowmist.useless.api.crafting.bigint; + +import appeng.api.stacks.KeyCounter; +import org.jetbrains.annotations.NotNull; + +import java.math.BigInteger; + +/** + * 一次性的大数批次准入凭据,由 {@link AlloyFurnaceBigIntegerTarget#admit} 返回。 + * + *

调用契约(务必读完)

+ * + *
    + *
  1. 只能提交一次。{@link #commit} 第二次调用会抛 {@link IllegalStateException}。
  2. + *
  3. 提交必须用「准备时那一份原型」:调用 {@code admit} 时传入的 + * {@code KeyCounter[]} 数组对象本身,不能复制、不能重建。用别的数组提交会抛 + * {@link IllegalArgumentException}。
  4. + *
  5. 我们只消费手里那一份原型。{@code admit} 传入的原型是单次推送的材料, + * 不是整批的 {@code count} 倍。整批里剩下的 {@code count - 1} 份材料由 + * 调用方自己的 BigInteger 账本负责扣除,并在我们提交失败时回滚。 + * 这个账本怎么实现由调用方决定。绝不要把 count 乘进原型。
  6. + *
  7. 提交失败不代表材料丢失:{@link #commit} 返回 {@code false} 时我们不会清空原型, + * 调用方应据此回滚它自己的那 {@code count - 1} 份扣账,并稍后重试或改走别的供应器。
  8. + *
  9. 提交返回 true 即转移所有权:此后原型里的材料归本机所有(我们会清空它), + * 产物由本机按 {@link AlloyFurnaceBigIntegerOutput} 交付回网络 / 回报给已注册的 CPU 适配器。 + * {@link #hasTransferredInputOwnership()} 会变成 {@code true}。
  10. + *
  11. 仅服务器线程。
  12. + *
+ */ +public interface AlloyFurnaceBigIntegerBatch { + + /** @return 已获准的固定份数,恒为正,且在准备与提交之间不会变化 */ + @NotNull BigInteger count(); + + /** + * 提交这批大数合成。 + * + * @param prototype 必须与 {@link AlloyFurnaceBigIntegerTarget#admit} 传入的是同一个数组对象 + * @return 本机是否接收;{@code false} 表示未消费任何材料,调用方需自行回滚其账本 + * @throws IllegalStateException 该凭据已被提交过 + * @throws IllegalArgumentException 提交时用的原型不是准备时那一份 + */ + boolean commit(KeyCounter @NotNull [] prototype); + + /** @return 是否已经发生过一次成功的提交(即输入所有权已转移给本机) */ + boolean hasTransferredInputOwnership(); +} diff --git a/src/main/java/com/sorrowmist/useless/api/crafting/bigint/AlloyFurnaceBigIntegerCapacity.java b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/AlloyFurnaceBigIntegerCapacity.java new file mode 100644 index 00000000..780eb93b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/AlloyFurnaceBigIntegerCapacity.java @@ -0,0 +1,42 @@ +package com.sorrowmist.useless.api.crafting.bigint; + +import org.jetbrains.annotations.NotNull; + +import java.math.BigInteger; +import java.util.Objects; + +/** + * 一次容量查询的结果:本机此刻最多能接受多少份「单次原型」。 + * + *

份数用 {@link BigInteger} 表达,可以远超 {@code long} —— 这是本 API 与 AE2 原生 + * {@code ICraftingProvider#pushPattern} 最本质的区别。

+ * + * @param accepted 可接受份数;{@link BigInteger#ZERO} 表示此刻一份都收不了 + * @param statusKey 为 0 时给出的原因(一个语言键,可直接显示给玩家);非 0 时为空串 + */ +public record AlloyFurnaceBigIntegerCapacity(BigInteger accepted, String statusKey) { + + /** 校验并冻结一次容量结果。 */ + public AlloyFurnaceBigIntegerCapacity { + Objects.requireNonNull(accepted, "BigInteger crafting capacity must not be null"); + if (accepted.signum() < 0) { + throw new IllegalArgumentException("BigInteger crafting capacity must not be negative"); + } + statusKey = statusKey == null ? "" : statusKey; + } + + /** @return 本机此刻一份都收不了,并附带原因 */ + public static @NotNull AlloyFurnaceBigIntegerCapacity none(@NotNull String statusKey) { + return new AlloyFurnaceBigIntegerCapacity(BigInteger.ZERO, statusKey); + } + + /** @return 可接受 {@code accepted} 份(必须为正) */ + public static @NotNull AlloyFurnaceBigIntegerCapacity of(@NotNull BigInteger accepted) { + return new AlloyFurnaceBigIntegerCapacity(accepted, ""); + } + + /** @return 此刻是否至少能收一份 */ + public boolean isAvailable() { + return accepted.signum() > 0; + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/crafting/bigint/AlloyFurnaceBigIntegerOutput.java b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/AlloyFurnaceBigIntegerOutput.java new file mode 100644 index 00000000..29ef97fe --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/AlloyFurnaceBigIntegerOutput.java @@ -0,0 +1,36 @@ +package com.sorrowmist.useless.api.crafting.bigint; + +import appeng.api.stacks.AEKey; + +import java.math.BigInteger; +import java.util.Objects; + +/** + * 一条 BigInteger 规模的 AE 产物。 + * + *

为什么需要它:AE2 自己的存储接口单次最多接受 {@code long} + * ({@code IMEInventory#insert(AEKey, long, Actionable)}),而大数批次的产物总量可以超过 + * {@code long}。本模组内部用 BigInteger 记账、再按 {@code Long.MAX_VALUE} 切段写回网络, + * 对外则用这个不可变记录暴露完整数量,让第三方 CPU 能自己核账,而不必去猜分段。

+ * + *

切段粒度为 {@code Long.MAX_VALUE} —— 这是 AE2 存储接口单次能接受的量。

+ * + * @param what 资源键(物品 / 流体 / 其它 AE 键) + * @param amount 精确数量,必须为正 + */ +public record AlloyFurnaceBigIntegerOutput(AEKey what, BigInteger amount) { + + /** 校验并冻结一条大数产物。 */ + public AlloyFurnaceBigIntegerOutput { + Objects.requireNonNull(what, "BigInteger crafting output key must not be null"); + Objects.requireNonNull(amount, "BigInteger crafting output amount must not be null"); + if (amount.signum() <= 0) { + throw new IllegalArgumentException("BigInteger crafting output amount must be positive"); + } + } + + /** @return 该数量是否已超出单个 {@code long} 能表达的范围 */ + public boolean exceedsLong() { + return amount.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0; + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/crafting/bigint/AlloyFurnaceBigIntegerProvider.java b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/AlloyFurnaceBigIntegerProvider.java new file mode 100644 index 00000000..3bd7511b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/AlloyFurnaceBigIntegerProvider.java @@ -0,0 +1,21 @@ +package com.sorrowmist.useless.api.crafting.bigint; + +import org.jetbrains.annotations.Nullable; + +/** + * 由「支持 BigInteger 批次」的样板供应器方块实体实现的标记接口。 + * + *

{@link AlloyFurnaceBigIntegerApi#findTargets} 就是遍历 AE 网格节点、 + * 用 {@code IGridNode#getOwner() instanceof AlloyFurnaceBigIntegerProvider} 找出候选, + * 再向它索取 {@link #bigIntegerTarget()}。这样调用方不需要认识本模组的具体方块实体类, + * 也不需要做反射或 {@code Class.forName}。

+ * + *

实现方返回的实例必须仅服务器线程可用。

+ */ +public interface AlloyFurnaceBigIntegerProvider { + + /** + * @return 本机当前可用的 bigint 目标;机器未成形、未联网或链接尚未解析时为 {@code null} + */ + @Nullable AlloyFurnaceBigIntegerTarget bigIntegerTarget(); +} diff --git a/src/main/java/com/sorrowmist/useless/api/crafting/bigint/AlloyFurnaceBigIntegerTarget.java b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/AlloyFurnaceBigIntegerTarget.java new file mode 100644 index 00000000..7ec40be8 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/AlloyFurnaceBigIntegerTarget.java @@ -0,0 +1,95 @@ +package com.sorrowmist.useless.api.crafting.bigint; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.networking.IGrid; +import appeng.api.stacks.KeyCounter; +import com.sorrowmist.useless.api.crafting.bigint.cpu.AlloyFurnaceBigIntegerCpuBinding; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.math.BigInteger; +import java.util.Set; + +/** + * 一台支持 BigInteger 批次的多方块万象合金炉,站在「样板供应器」一侧的对外视图。 + * + *

拿到实例的唯一正规途径是 {@link AlloyFurnaceBigIntegerApi#findTargets(IGrid)} / + * {@link AlloyFurnaceBigIntegerApi#findTarget(IGrid, String)}。实例由机器的方块实体持有, + * 不要缓存到跨 tick 的字段里,每 tick 重新查一次即可(查询本身是网格节点遍历,很轻)。

+ * + *

仅服务器线程:本接口的所有方法都必须在服务器线程调用。

+ */ +public interface AlloyFurnaceBigIntegerTarget { + + /** @return provider 局部稳定的路由身份;同一台机器的生命周期内不变 */ + @NotNull String routeIdentity(); + + /** + * @return provider 无关的物理机器身份,格式 {@code 维度@x,y,z}。 + * + *

同一台物理机器无论从哪个 provider 暴露,都必须给出同一个字符串,调用方据此避免超卖 + * 同一台机器。本模组的所有入口都用同一个公式,所以它们认的是同一台机器。

+ */ + @NotNull String machineIdentity(); + + /** @return 本机所在的 AE 网格;机器离线时为 {@code null} */ + @Nullable IGrid grid(); + + /** @return 本机接受的样板种类(AE2 样板物品 id),用于调用方预先过滤 */ + @NotNull Set acceptedPatternKinds(); + + /** + * 查询容量:本机此刻最多能接受多少份「单次推送的原型」。 + * + *

本方法不产生任何副作用,可以随时调用(例如每次 tick 做一次规划)。

+ * + * @param pattern 已解开的样板(不要把倍率包装类传进来;本 API 只认原始样板) + * @param prototype 单次推送的原型材料,不是整批的 count 倍 + * @param requested 期望的份数,必须为正 + * @return 容量结果;为 0 时 {@link AlloyFurnaceBigIntegerCapacity#statusKey()} 给出原因 + */ + @NotNull AlloyFurnaceBigIntegerCapacity capacity(IPatternDetails pattern, + KeyCounter @NotNull [] prototype, + @NotNull BigInteger requested); + + /** + * 本机是否正在动态降频。 + * + *

本模组对每条 bigint 路径都做了「每 tick 时间预算」的动态降频: + * 实测耗时超预算时,后续批次的容量会被按比例收窄。降频状态下 {@link #capacity} 返回的数字 + * 会明显小于机器的真实能力。

+ * + *

这个方法的用途就是让调用方区分这两种情况:

+ *
    + *
  • 容量小且 {@code isThrottled() == false} —— 机器能力就这样(材料窗口 / 能量 / 产物分段 + * 或配置的线程数限制);
  • + *
  • 容量小且 {@code isThrottled() == true} —— 机器正忙(其它机器或本机的耗时占满了全局 + * 预算)。此时可以主动延后非紧急批次,待预算恢复后再提交。
  • + *
+ * + *

注意降频不会将容量降至 0:完全上报「无容量」会引发调度侧「无容量 → 重新提交」的 + * 空转,因此降频只收窄批次规模。是否停止提交由调用方决定。

+ * + * @return 是否正在降频 + */ + boolean isThrottled(); + + /** + * 申请一批大数合成,成功时返回一次性凭据(见 {@link AlloyFurnaceBigIntegerBatch} 的调用契约)。 + * + *

本方法不消费材料、不扣能量,只是做一次快照式检查;真正的检查会在 + * {@code commit} 时重新执行(期间机器状态可能变化,最终以 commit 的返回值为准)。

+ * + * @param pattern 已解开的样板 + * @param prototype 单次推送的原型(提交时必须原样传回同一个数组对象) + * @param requested 期望的份数,必须为正 + * @param cpu CPU 侧绑定,用于接收产物回执;{@code null} 表示不需要回调 + * (产物仍会按段写回 ME 网络) + * @return 准入凭据;{@code null} 表示此刻不可接受 + */ + @Nullable AlloyFurnaceBigIntegerBatch admit(IPatternDetails pattern, + KeyCounter @NotNull [] prototype, + @NotNull BigInteger requested, + @Nullable AlloyFurnaceBigIntegerCpuBinding cpu); +} diff --git a/src/main/java/com/sorrowmist/useless/api/crafting/bigint/cpu/AlloyFurnaceBigIntegerBatchContext.java b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/cpu/AlloyFurnaceBigIntegerBatchContext.java new file mode 100644 index 00000000..f573b5e8 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/cpu/AlloyFurnaceBigIntegerBatchContext.java @@ -0,0 +1,50 @@ +package com.sorrowmist.useless.api.crafting.bigint.cpu; + +import appeng.api.crafting.IPatternDetails; +import com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerOutput; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.math.BigInteger; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +/** + * 一个大数批次的不可变上下文,随每个 CPU 回调一起传入。 + * + *

回调可能发生在提交之后的任意 tick(产物要逐段写回网络),所以请用 + * {@link #batchId()} 而不是「当前正在提交的那个批次」来定位状态。

+ * + * @param batchId 批次唯一标识。存档重载后 {@code cpuToken} 会丢失,请用它重新绑定 + * @param cpuToken 调用方在 {@link AlloyFurnaceBigIntegerCpuBinding#cpuToken()} 里给的上下文, + * 原样回传;存档重载后为 {@code null} + * @param machineIdentity 实际执行本批的机器身份({@code 维度@x,y,z}) + * @param pattern 本批使用的样板(原始样板,不是倍率包装类) + * @param acceptedCount 已获准的份数 + * @param plannedOutputs 样板声明的产物(模板键)。动态产物(id-only 产物槽)的实际键 + * 可能与之不同,实际值见 + * {@link AlloyFurnaceBigIntegerCpuAdapter#onBatchOutputs} + * @param energyCharged 提交时实际扣掉的能量 + */ +public record AlloyFurnaceBigIntegerBatchContext(@NotNull UUID batchId, + @Nullable Object cpuToken, + @NotNull String machineIdentity, + @NotNull IPatternDetails pattern, + @NotNull BigInteger acceptedCount, + @NotNull List plannedOutputs, + long energyCharged) { + + /** 校验并冻结一次批次上下文。 */ + public AlloyFurnaceBigIntegerBatchContext { + Objects.requireNonNull(batchId, "BigInteger crafting batch id must not be null"); + Objects.requireNonNull(machineIdentity, "BigInteger crafting machine identity must not be null"); + Objects.requireNonNull(pattern, "BigInteger crafting batch pattern must not be null"); + Objects.requireNonNull(acceptedCount, "BigInteger crafting batch count must not be null"); + Objects.requireNonNull(plannedOutputs, "BigInteger crafting planned outputs must not be null"); + if (acceptedCount.signum() <= 0) { + throw new IllegalArgumentException("BigInteger crafting batch count must be positive"); + } + plannedOutputs = List.copyOf(plannedOutputs); + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/crafting/bigint/cpu/AlloyFurnaceBigIntegerBatchResult.java b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/cpu/AlloyFurnaceBigIntegerBatchResult.java new file mode 100644 index 00000000..0bfb4602 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/cpu/AlloyFurnaceBigIntegerBatchResult.java @@ -0,0 +1,22 @@ +package com.sorrowmist.useless.api.crafting.bigint.cpu; + +/** + * 一个大数批次的终局状态。 + */ +public enum AlloyFurnaceBigIntegerBatchResult { + + /** 产物已全部写回 ME 网络(或已全部回报给你),批次正常结束。 */ + SUCCESS, + + /** + * 批次被取消:机器被拆、玩家点了取消、或任务被主动中止。 + * + *

产物不会丢失 —— 机器在取消时会把队列里剩余的产物强制写回 ME 网络, + * 只是不再走正常的逐 tick 节奏。收到该状态时请按「这批已由机器交付,但我没拿到逐条回执」 + * 处理,不要重复补记产物。

+ */ + CANCELLED, + + /** 批次在提交阶段就被拒绝,材料未被消费。通常不需要处理(因为 {@code commit} 已经返回了 false)。 */ + REJECTED +} diff --git a/src/main/java/com/sorrowmist/useless/api/crafting/bigint/cpu/AlloyFurnaceBigIntegerCpuAdapter.java b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/cpu/AlloyFurnaceBigIntegerCpuAdapter.java new file mode 100644 index 00000000..06e61944 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/cpu/AlloyFurnaceBigIntegerCpuAdapter.java @@ -0,0 +1,104 @@ +package com.sorrowmist.useless.api.crafting.bigint.cpu; + +import com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerOutput; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * CPU 侧的产物回执通道:实现它并在 mod 初始化阶段注册,即可收到大数批次的产物与终局通知。 + * + *

为什么需要它

+ * + *

机器产出的总量可以超过 {@code long},而 AE2 的存储接口单次最多接受 {@code long}, + * 因此产物按 {@code Long.MAX_VALUE} 切段、逐 tick 写回 ME 网络。若 CPU 自行维护 + * BigInteger 账本(例如为「超长合成任务」记账),仅凭网络中的分段无法还原精确总数 —— + * 本通道直接提供完整的 BigInteger 产物。

+ * + *

回调约定

+ * + *
    + *
  • 全部回调都在服务器线程执行,且都在提交成功之后。
  • + *
  • {@link #onBatchAdmitted} 在 {@code commit} 成功后立即调用(同一 tick)。
  • + *
  • {@link #onBatchOutputs} 在产物全部写回网络之后调用一次; + * {@link #onBatchFinished} 紧接着以 {@link AlloyFurnaceBigIntegerBatchResult#SUCCESS} 调用。
  • + *
  • 批次被取消时只调用 {@link #onBatchFinished}(状态 {@code CANCELLED}),不调用 {@code onBatchOutputs}。
  • + *
  • 实现必须不抛异常:抛出的异常会被记录并吞掉,但你的状态可能因此不一致。 + * 请不要在回调里再调用 {@code admit}/{@code commit}(会造成重入)。
  • + *
  • 不要持有机器引用:上下文里没有任何机器对象,这是刻意的。
  • + *
+ * + *

没有适配器时会怎样

+ * + *

完全没有降级风险:不注册适配器(或 {@code admit} 时传 {@code cpu = null})时, + * 产物仍按段写回 ME 网络,仅不发送这些回调。

+ */ +public interface AlloyFurnaceBigIntegerCpuAdapter { + + /** @return 全局唯一的适配器 id,用于注册去重与持久化 */ + @NotNull ResourceLocation id(); + + /** + * 批次已被机器接受(材料所有权已转移)。 + * + *

适合在这里把「本批要产出什么」记进自己的账本,或标记任务进入等待状态。

+ */ + void onBatchAdmitted(@NotNull AlloyFurnaceBigIntegerBatchContext context); + + /** + * 产物已全部写回 ME 网络,下面是本批的实际产出(BigInteger 精确值)。 + * + *

对动态产物(id-only 产物槽)而言,这里的键可能与 + * {@link AlloyFurnaceBigIntegerBatchContext#plannedOutputs()} 里的模板键不同 —— + * 请以这里为准。

+ */ + void onBatchOutputs(@NotNull AlloyFurnaceBigIntegerBatchContext context, + @NotNull List actualOutputs); + + /** + * 批次终局。 + * + * @param result 见 {@link AlloyFurnaceBigIntegerBatchResult} + */ + void onBatchFinished(@NotNull AlloyFurnaceBigIntegerBatchContext context, + @NotNull AlloyFurnaceBigIntegerBatchResult result); + + /** + * 产物落网之前的「整批接收」机会 —— 这是唯一能跳过逐段插入的通道。 + * + *

为什么需要它

+ *

AE2 的存储接口单次只收 {@code long}({@code IMEInventory#insert(AEKey, long, Actionable)}), + * 所以本机默认要把产物按 {@code Long.MAX_VALUE} 切段、逐段写回网络 + * (实测约 2.6µs/段,一 tick 两万多段就是几十毫秒)。若你愿意直接收下整批, + * 这段插入就完全不需要发生。

+ * + *

调用时机与语义

+ *
    + *
  • 在产物写回网络之前、按键调用;返回你收下的量。
  • + *
  • 机器会把收下的量从待回网账本中扣除,只把余额写回网络。
  • + *
  • 默认返回空 Map ⇒ 行为与不实现本方法完全一致(全部写回网络)。
  • + *
+ * + *

⚠️ 接收即接管:责任随之转移

+ *

收下的物品不会进入 ME 网络,只存在于你自己的账本里。因此:

+ *
    + *
  • 你必须负责它们后续的去向(自行落网、或作为中间产物消耗);
  • + *
  • 机器无法退还 —— 账本在调用返回时已被扣减。若批次随后被取消 + * ({@link #onBatchFinished} 收到 {@code CANCELLED}),已收下的部分需由你自行处理;
  • + *
  • 实现必须不抛异常;抛出的异常会被记录并吞掉,但可能造成产物归属不一致。
  • + *
+ * + *

出于安全考虑,本机只会在「整趟刷写绑定的都是同一个适配器」时调用本方法; + * 多 CPU 混绑同一台机器时不会调用(退回逐段写回)。

+ * + * @param context 本批上下文(与其它回调同一个) + * @param outputs 本次准备写回网络的产物;可只收其中一部分(返回量 ≤ 这里的量) + * @return 你实际收下的量,按 key 计;未列出的 key 视为不收 + */ + default @NotNull java.util.Map claimOutputs( + @NotNull AlloyFurnaceBigIntegerBatchContext context, + @NotNull java.util.List outputs) { + return java.util.Map.of(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/crafting/bigint/cpu/AlloyFurnaceBigIntegerCpuAdapters.java b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/cpu/AlloyFurnaceBigIntegerCpuAdapters.java new file mode 100644 index 00000000..9e4e136e --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/cpu/AlloyFurnaceBigIntegerCpuAdapters.java @@ -0,0 +1,123 @@ +package com.sorrowmist.useless.api.crafting.bigint.cpu; + +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * CPU 适配器的显式注册表。 + * + *

什么时候注册

+ * + *

在你自己的 mod 初始化阶段注册一次即可,推荐放在 + * {@code FMLCommonSetupEvent#enqueueWork}(或等价的公共初始化钩子)里:

+ * + *
{@code
+ * @SubscribeEvent
+ * public static void onCommonSetup(FMLCommonSetupEvent event) {
+ *     event.enqueueWork(() ->
+ *             AlloyFurnaceBigIntegerCpuAdapters.register(new MyCpuAdapter()));
+ * }
+ * }
+ * + *

注册表是全局的、进程级的,不随存档或世界重载清空。若你的 mod 支持运行期禁用, + * 用 {@link #unregister} 摘掉即可。

+ * + *

线程

+ * + *

注册表本身是线程安全的({@code ConcurrentHashMap}),但适配器回调只会在服务器线程发生。 + * 请不要在注册/注销的同时假设回调已停止 —— 注销只影响之后的查找。

+ */ +public final class AlloyFurnaceBigIntegerCpuAdapters { + private static final Map ADAPTERS = + new ConcurrentHashMap<>(); + + private AlloyFurnaceBigIntegerCpuAdapters() { + } + + /** + * 用适配器自己声明的 {@link AlloyFurnaceBigIntegerCpuAdapter#id()} 注册。 + * + * @throws IllegalArgumentException id 已被别的适配器占用 + */ + public static void register(@NotNull AlloyFurnaceBigIntegerCpuAdapter adapter) { + Objects.requireNonNull(adapter, "BigInteger crafting CPU adapter must not be null"); + register(adapter.id(), adapter); + } + + /** + * 显式指定 id 注册,并校验它与适配器声明的一致(防止复制粘贴写错)。 + * + * @throws IllegalArgumentException id 与 {@code adapter.id()} 不一致,或该 id 已被占用 + */ + public static void register(@NotNull ResourceLocation id, + @NotNull AlloyFurnaceBigIntegerCpuAdapter adapter) { + Objects.requireNonNull(id, "BigInteger crafting CPU adapter id must not be null"); + Objects.requireNonNull(adapter, "BigInteger crafting CPU adapter must not be null"); + if (!id.equals(adapter.id())) { + throw new IllegalArgumentException( + "Registered id " + id + " does not match adapter id " + adapter.id()); + } + AlloyFurnaceBigIntegerCpuAdapter previous = ADAPTERS.putIfAbsent(id, adapter); + if (previous != null && previous != adapter) { + throw new IllegalArgumentException("BigInteger crafting CPU adapter id is already registered: " + id); + } + } + + /** + * 注销一个适配器。 + * + * @param id 要摘掉的适配器 id;不存在时静默返回 + */ + public static void unregister(@Nullable ResourceLocation id) { + if (id != null) { + ADAPTERS.remove(id); + } + } + + /** + * @param id 适配器 id + * @return 命中的适配器;未注册或 {@code id} 为 {@code null} 时为空 + */ + public static @NotNull Optional find(@Nullable ResourceLocation id) { + return id == null ? Optional.empty() : Optional.ofNullable(ADAPTERS.get(id)); + } + + /** @return 当前已注册的全部适配器(只读视图) */ + /** + * 是否存在「会整批接走产物」的适配器(即覆写了 {@link AlloyFurnaceBigIntegerCpuAdapter#claimOutputs})。 + * + *

探测该项的原因:本机的「产物交付能力」闸({@code maximumSegmentedCount})假定 + * 产物由本机逐段写回网络。若已有 CPU 适配器整批接走产物,该闸即失去意义 —— + * 交付由对方负责,再按其限制单批规模只会无谓地压小批次。

+ * + *

以「声明类是否仍为接口本身」判断 default 方法是否被覆写,是标准做法, + * 调用方因此无需额外实现声明方法。

+ */ + public static boolean hasBulkOutputAdapter() { + for (AlloyFurnaceBigIntegerCpuAdapter adapter : ADAPTERS.values()) { + try { + if (adapter.getClass() + .getMethod("claimOutputs", AlloyFurnaceBigIntegerBatchContext.class, List.class) + .getDeclaringClass() != AlloyFurnaceBigIntegerCpuAdapter.class) { + return true; + } + } catch (NoSuchMethodException ignored) { + // 老版本接口没有这个方法:视为不支持整批接收。 + } + } + return false; + } + + public static @NotNull Map registered() { + return Collections.unmodifiableMap(ADAPTERS); + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/crafting/bigint/cpu/AlloyFurnaceBigIntegerCpuBinding.java b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/cpu/AlloyFurnaceBigIntegerCpuBinding.java new file mode 100644 index 00000000..1b2cb7af --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/cpu/AlloyFurnaceBigIntegerCpuBinding.java @@ -0,0 +1,32 @@ +package com.sorrowmist.useless.api.crafting.bigint.cpu; + +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +/** + * 把一个大数批次绑定到某个已注册的 CPU 适配器上。 + * + *

由调用方在 {@code admit(...)} 时构造并传入;机器只把它当不透明的标签, + * 用来在产物交付时找到该回调谁。

+ * + * @param adapterId 已通过 {@link AlloyFurnaceBigIntegerCpuAdapters#register} 注册的适配器 id + * @param cpuToken 调用方自己的上下文(合成任务句柄、CPU 引用等),对机器完全不透明, + * 机器只会原样回传,不做任何检查、比较或序列化。 + * 可以传 {@code null}(例如只有全局回调、不需要区分任务时) + */ +public record AlloyFurnaceBigIntegerCpuBinding(@NotNull ResourceLocation adapterId, + @Nullable Object cpuToken) { + + /** 校验并冻结一次 CPU 绑定。 */ + public AlloyFurnaceBigIntegerCpuBinding { + Objects.requireNonNull(adapterId, "BigInteger crafting CPU adapter id must not be null"); + } + + /** @return 只有适配器 id、没有上下文的绑定 */ + public static @NotNull AlloyFurnaceBigIntegerCpuBinding of(@NotNull ResourceLocation adapterId) { + return new AlloyFurnaceBigIntegerCpuBinding(adapterId, null); + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/crafting/bigint/cpu/package-info.java b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/cpu/package-info.java new file mode 100644 index 00000000..1465ff33 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/cpu/package-info.java @@ -0,0 +1,67 @@ +/** + * 多方块万象合金炉的 BigInteger 批次 对外公开 API(CPU 侧 SPI)。 + * + *

这个包解决什么问题

+ * + *

合成 CPU 单次发配的份数可以超过 {@code long},机器也按该数量产出,但 AE2 的 + * 存储接口单次最多接受 {@code long},因此产物按 {@code Long.MAX_VALUE} 切段、逐 tick 写回网络。 + * 若 CPU 自行维护 BigInteger 账本,仅凭网络中的分段无法还原精确总数。

+ * + *

实现 {@link com.sorrowmist.useless.api.crafting.bigint.cpu.AlloyFurnaceBigIntegerCpuAdapter} + * 并注册,即可收到完整的 BigInteger 产物回执与批次终局通知。

+ * + *

包结构

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
CPU 侧 SPI
类型用途
{@link com.sorrowmist.useless.api.crafting.bigint.cpu.AlloyFurnaceBigIntegerCpuAdapter}你实现的回调接口
{@link com.sorrowmist.useless.api.crafting.bigint.cpu.AlloyFurnaceBigIntegerCpuAdapters}显式注册表(在 mod init 阶段调用一次)
{@link com.sorrowmist.useless.api.crafting.bigint.cpu.AlloyFurnaceBigIntegerCpuBinding}提交批次时把你的适配器 id 与自己的上下文绑上去
{@link com.sorrowmist.useless.api.crafting.bigint.cpu.AlloyFurnaceBigIntegerBatchContext}每次回调都会带上的批次上下文
{@link com.sorrowmist.useless.api.crafting.bigint.cpu.AlloyFurnaceBigIntegerBatchResult}批次终局状态
+ * + *

完整流程

+ * + *
{@code
+ * // 1) mod 初始化阶段注册一次
+ * AlloyFurnaceBigIntegerCpuAdapters.register(new MyCpuAdapter());
+ *
+ * // 2) 发配时(服务器线程)
+ * AlloyFurnaceBigIntegerCpuBinding binding =
+ *         new AlloyFurnaceBigIntegerCpuBinding(MyCpuAdapter.ID, myCraftingJobHandle);
+ * for (AlloyFurnaceBigIntegerTarget target : AlloyFurnaceBigIntegerApi.findTargets(grid)) {
+ *     AlloyFurnaceBigIntegerCapacity capacity = target.capacity(pattern, unitPrototype, requested);
+ *     if (!capacity.isAvailable()) {
+ *         continue;
+ *     }
+ *     AlloyFurnaceBigIntegerBatch batch = target.admit(pattern, unitPrototype, capacity.accepted(), binding);
+ *     if (batch == null) {
+ *         continue;
+ *     }
+ *     if (batch.commit(unitPrototype)) {
+ *         break; // 材料所有权已转移;产物会通过 binding 回调给你
+ *     }
+ *     // commit 失败:回滚你自己扣掉的那 count-1 份,然后试下一台
+ * }
+ * }
+ * + *

通用约束

+ * + *
    + *
  1. 回调只在服务器线程发生;实现里不要做跨线程共享的可变状态读写。
  2. + *
  3. 回调里不要抛异常,也不要再调用 {@code admit}/{@code commit}(会重入)。
  4. + *
  5. 不要持有机器引用;上下文里刻意没有任何机器对象。
  6. + *
  7. 存档重载后 {@code cpuToken} 为 {@code null}(它不可序列化)。请用 + * {@link com.sorrowmist.useless.api.crafting.bigint.cpu.AlloyFurnaceBigIntegerBatchContext#batchId()} + * 重新绑定;找不到对应批次就当作「机器仍在替我保管产物」处理。
  8. + *
  9. 未注册适配器不会引起任何降级问题:产物仍按段写回 ME 网络,仅不发送回调。
  10. + *
+ */ +package com.sorrowmist.useless.api.crafting.bigint.cpu; diff --git a/src/main/java/com/sorrowmist/useless/api/crafting/bigint/example/ExampleBigIntegerCpuAdapter.java b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/example/ExampleBigIntegerCpuAdapter.java new file mode 100644 index 00000000..5e969cf7 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/example/ExampleBigIntegerCpuAdapter.java @@ -0,0 +1,186 @@ +package com.sorrowmist.useless.api.crafting.bigint.example; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.networking.IGrid; +import appeng.api.stacks.KeyCounter; +import com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerApi; +import com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerBatch; +import com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerCapacity; +import com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerOutput; +import com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerTarget; +import com.sorrowmist.useless.api.crafting.bigint.cpu.AlloyFurnaceBigIntegerBatchContext; +import com.sorrowmist.useless.api.crafting.bigint.cpu.AlloyFurnaceBigIntegerBatchResult; +import com.sorrowmist.useless.api.crafting.bigint.cpu.AlloyFurnaceBigIntegerCpuAdapter; +import com.sorrowmist.useless.api.crafting.bigint.cpu.AlloyFurnaceBigIntegerCpuAdapters; +import com.sorrowmist.useless.api.crafting.bigint.cpu.AlloyFurnaceBigIntegerCpuBinding; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.math.BigInteger; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 参考实现:一个最小可用的大数 CPU 适配器,演示本 API 的完整用法。 + * + *

这个类不参与运行,只是给第三方 mod 作者照抄的样板。真正接入时请替换掉:

+ *
    + *
  • {@link #ID} —— 改成你自己的命名空间;
  • + *
  • 注册时机 —— 在你自己 mod 的 {@code FMLCommonSetupEvent#enqueueWork} 里调用 + * {@code AlloyFurnaceBigIntegerCpuAdapters.register(new YourAdapter())};
  • + *
  • {@link #onBatchAdmitted} / {@link #onBatchOutputs} / {@link #onBatchFinished} + * —— 接进你自己的合成任务记账;
  • + *
  • {@link #dispatch} —— 接进你自己的 CPU tick 调度。
  • + *
+ * + * @see AlloyFurnaceBigIntegerApi 发现入口 + * @see AlloyFurnaceBigIntegerBatch 提交契约(尤其是 {@code count - 1} 份材料的归属) + */ +@ApiStatus.Experimental +public final class ExampleBigIntegerCpuAdapter implements AlloyFurnaceBigIntegerCpuAdapter { + /** 改成 {@code new ResourceLocation("<你的modid>", "bigint_cpu")}。 */ + public static final ResourceLocation ID = + new ResourceLocation("example_mod", "bigint_cpu"); + + /** 示例状态:batchId → 你自己的任务标识。真实实现请换成你的合成任务表。 */ + private final Map pendingBatches = new ConcurrentHashMap<>(); + + /** 示例账本:把机器回报的实际产出按 BigInteger 累起来(不要累加 long 分段,会溢出)。 */ + private final Map deliveredPerJob = new ConcurrentHashMap<>(); + + /** 示例统计:单条产出里出现过的最大数量,用来证明 BigInteger 确实用上了。 */ + private volatile BigInteger largestSingleOutput = BigInteger.ZERO; + + @Override + public @NotNull ResourceLocation id() { + return ID; + } + + @Override + public void onBatchAdmitted(@NotNull AlloyFurnaceBigIntegerBatchContext context) { + // 材料所有权已转移给机器。这里可以把「本批计划产出多少」记进自己的账本, + // 并让任务进入「等待机器交付」状态。 + String jobKey = jobKey(context); + this.pendingBatches.put(context.batchId(), jobKey); + for (AlloyFurnaceBigIntegerOutput planned : context.plannedOutputs()) { + this.deliveredPerJob.merge(jobKey, planned.amount(), BigInteger::add); + } + } + + @Override + public void onBatchOutputs(@NotNull AlloyFurnaceBigIntegerBatchContext context, + @NotNull List actualOutputs) { + // 产物已经全部写回 ME 网络。actualOutputs 是 BigInteger 精确值 —— + // 对动态产物(id-only 产物槽)来说,这里的键才是真正的键,可能与 plannedOutputs 的模板不同。 + // 累加一律走 BigInteger:long 分段相加会溢出。 + String jobKey = jobKey(context); + BigInteger total = BigInteger.ZERO; + for (AlloyFurnaceBigIntegerOutput actual : actualOutputs) { + total = total.add(actual.amount()); + if (actual.exceedsLong()) { + // 这一条已经超过 long,只有 BigInteger 账本能正确表达它。 + this.largestSingleOutput = this.largestSingleOutput.max(actual.amount()); + } + } + this.deliveredPerJob.merge(jobKey, total, BigInteger::add); + } + + @Override + public void onBatchFinished(@NotNull AlloyFurnaceBigIntegerBatchContext context, + @NotNull AlloyFurnaceBigIntegerBatchResult result) { + String jobKey = this.pendingBatches.remove(context.batchId()); + if (jobKey == null) { + // 存档重载后 cpuToken 会丢,这里可能找不到对应批次。产物仍已入网,按「机器已交付」处理即可。 + return; + } + if (result == AlloyFurnaceBigIntegerBatchResult.CANCELLED) { + // 机器被拆/任务被取消:产物已被强制写回网络(不会丢),只是没走逐条回执。 + // 不要在这里补记产物,否则会重复记账。 + } + } + + // ==================== 发配流程 ==================== + + /** + * 参考的发配流程:找到机器 → 查容量 → 申请 → 提交。 + * + *

请在服务器线程、你自己的 CPU tick 里调用。

+ * + * @param grid 你的 CPU 所在网格 + * @param pattern 已解开的样板(万象样板或 AE2 合成样板) + * @param unitPrototype 单次推送的原型材料,不是整批的 count 倍 + * @param requested 你期望发配的份数,可以超过 {@code long} + * @param jobHandle 你自己的任务句柄,会原样回传给你 + * @param urgent {@code true} 表示紧急批次,即使机器正在降频也照发 + * @return 实际被接受的份数;{@code null} 表示没有任何机器接单 + */ + public static @Nullable BigInteger dispatch(@Nullable IGrid grid, + @NotNull IPatternDetails pattern, + @NotNull KeyCounter @NotNull [] unitPrototype, + @NotNull BigInteger requested, + @Nullable Object jobHandle, + boolean urgent) { + if (grid == null || requested.signum() <= 0) { + return null; + } + AlloyFurnaceBigIntegerCpuBinding binding = new AlloyFurnaceBigIntegerCpuBinding(ID, jobHandle); + for (AlloyFurnaceBigIntegerTarget target : AlloyFurnaceBigIntegerApi.findTargets(grid)) { + // 机器正忙(全局每 tick 时间预算已耗尽)时容量收窄,此处按需延后非紧急批次。 + // 降频不会将容量降至 0,是否停止提交由调用方决定。 + if (target.isThrottled() && !urgent) { + continue; + } + AlloyFurnaceBigIntegerCapacity capacity = target.capacity(pattern, unitPrototype, requested); + if (!capacity.isAvailable()) { + // capacity.statusKey() 是可显示给玩家的原因(档次不够 / 缺模具 / 没能量)。 + continue; + } + AlloyFurnaceBigIntegerBatch batch = + target.admit(pattern, unitPrototype, capacity.accepted(), binding); + if (batch == null) { + continue; + } + if (batch.commit(unitPrototype)) { + // 成功:材料所有权已转移,产物会经回调交回来。 + return batch.count(); + } + // 失败:机器没消费任何材料。请回滚你自己扣掉的那 (count - 1) 份,再试下一台。 + // 注意:只有你自己知道该怎么回滚 —— 机器从不碰你那部分账本。 + } + return null; + } + + // ==================== 示例内部实现 ==================== + + private static String jobKey(AlloyFurnaceBigIntegerBatchContext context) { + Object token = context.cpuToken(); + return token == null ? context.batchId().toString() : token.toString(); + } + + /** @return 示例用:某任务累计收到的产出总量 */ + public @Nullable BigInteger deliveredFor(String jobKey) { + return this.deliveredPerJob.get(jobKey); + } + + /** @return 示例用:单条产出里出现过的最大数量 */ + public @NotNull BigInteger largestSingleOutput() { + return this.largestSingleOutput; + } + + /** @return 示例用:当前仍在等待机器交付的批次数 */ + public int pendingBatchCount() { + return this.pendingBatches.size(); + } + + /** + * 注册示例适配器(仅供演示;真实 mod 请在自己初始化阶段调用)。 + */ + @ApiStatus.Experimental + public static void registerSelf() { + AlloyFurnaceBigIntegerCpuAdapters.register(new ExampleBigIntegerCpuAdapter()); + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/crafting/bigint/package-info.java b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/package-info.java new file mode 100644 index 00000000..890a5078 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/crafting/bigint/package-info.java @@ -0,0 +1,81 @@ +/** + * 多方块万象合金炉的 BigInteger 批次 对外公开 API(供应器侧契约)。 + * + *

这个 API 解决什么问题

+ * + *

AE2 原生的 {@code ICraftingProvider#pushPattern} 只能用 {@code long} 表达数量,一次物理提交 + * 最多 {@code Long.MAX / 每次消耗量} 份。想发配真正的大数(例如 {@code 1e22} 个物品)只有两条路:

+ * + *
    + *
  1. 长版 counted 路径:每 tick 派发一个 {@code long} 窗口,靠多次派发累加到 bigint 总量 + * —— 接口决定的回退路径,慢且占调度;
  2. + *
  3. 原生 bigint 路径(本 API):单次派发即可交付超过 long 的批次。
  4. + *
+ * + *

本 API 把第 2 条做成本模组自己的公开契约:任意第三方合成 CPU 只要实现一个适配器接口, + * 即可实现 bigint 发配,不需要依赖任何第三方调度框架,也不需要往 AE2 的 CPU 逻辑里写 mixin。

+ * + *

包结构

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
供应器侧契约
类型用途
{@link com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerApi}发现网格里的可用目标
{@link com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerTarget}一台机器:查容量 / 申请批次
{@link com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerCapacity}容量查询结果
{@link com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerBatch}一次性准入凭据,{@code commit} 后所有权转移
{@link com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerOutput}BigInteger 规模的产物(可以超过 long)
{@link com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerProvider}由供应器方块实体实现,供发现流程识别(调用方一般不需要碰)
+ * + *

CPU 侧的产物回执通道见 {@link com.sorrowmist.useless.api.crafting.bigint.cpu}。

+ * + *

通用约束

+ * + *
    + *
  1. 只在服务器线程调用。所有方法、所有回调都是如此;客户端调用行为未定义。
  2. + *
  3. 不要缓存内部对象。{@code AlloyFurnaceBigIntegerTarget} 由机器的方块实体持有, + * 跨 tick 缓存会在机器被拆/卸载后变成野指针。每 tick 重新 + * {@link com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerApi#findTargets} 即可。
  4. + *
  5. 不要反射或强转本模组的方块实体类。判定「是不是大数供应器」请用 + * {@code instanceof AlloyFurnaceBigIntegerProvider},方块实体类属于内部实现,随时可能改。
  6. + *
  7. 原型是「单次推送」的量。整批剩下的 {@code count - 1} 份材料由你自己的账本扣, + * 提交失败时要回滚。详见 {@link com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerBatch}。
  8. + *
  9. 一次提交只能用一个凭据一次。凭据是一次性的,不要复用、不要并发提交。
  10. + *
  11. 产物会切段写回 ME 网络。因为 AE2 存储接口单次最多 {@code long},超出部分按 + * {@code Long.MAX_VALUE} 切段、逐 tick 写入。 + * 若你在 CPU 侧注册了适配器,还会额外收到完整的 BigInteger 产物回执。
  12. + *
  13. 容量会被动态降频收窄。本模组对每条 bigint 路径都有「每 tick 时间预算」的降频, + * 实测耗时超预算时后续批次容量按比例收窄。用 + * {@link com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerTarget#isThrottled()} + * 可以区分「机器能力就这么多」与「机器正忙」,从而主动延后非紧急批次。 + * 降频不会将容量压至 0(完全无容量会引发调度侧重提交空转),是否停止提交由调用方决定。
  14. + *
+ * + *

能量闸随线圈档次不同

+ * + *

大数批次的能量计费方式取决于线圈档次,这直接影响容量能有多大:

+ * + * + * + * + * + * + * + * + * + * + *
能量计费
线圈计费方式是否限制份数
有用线圈(tier 10)整批一次固定能耗 {@code ceil(配方能耗 / 1024)},与份数无关不限制(没有能量闸)
其它档次{@code count × 单份能耗}限制:{@code count ≤ 可用能量 / 单份能耗}
+ * + *

所以有用线圈上容量只受材料窗口与产物分段预算限制,能吃到完整 bigint 规模; + * 低档次线圈会被内部能量缓冲(容量随档次增长,只有满级才是 {@code Long.MAX_VALUE})压小。 + * {@link com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerTarget#capacity} + * 会如实反映这一点。

+ */ +package com.sorrowmist.useless.api.crafting.bigint; diff --git a/src/main/java/com/sorrowmist/useless/api/entity/BeeTimerAccess.java b/src/main/java/com/sorrowmist/useless/api/entity/BeeTimerAccess.java new file mode 100644 index 00000000..98858fe3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/entity/BeeTimerAccess.java @@ -0,0 +1,16 @@ +package com.sorrowmist.useless.api.entity; + +/** + * 蜜蜂「是否携带花粉」的写入访问接口。 + * + *

{@code Bee.setHasNectar(boolean)} 是包级私有的,只有公开的 {@code hasNectar()} 可读。 + * 造化杖的「生物加速」需要写入它来加速采蜜归巢,由 mixin 侧的 {@code BeeAccessor} 生成实现。

+ * + *

为什么接口定义在主代码而不是 mixin 包:本项目的主代码不能直接引用 + * {@code com.sorrowmist.useless.mixin.*} 下的类型(编译期解析不到), + * 既有做法是「主代码定义普通接口 → mixin 实现它」。

+ */ +public interface BeeTimerAccess { + + void uselessMod$setBeeHasNectar(boolean value); +} diff --git a/src/main/java/com/sorrowmist/useless/api/entity/TurtleTimerAccess.java b/src/main/java/com/sorrowmist/useless/api/entity/TurtleTimerAccess.java new file mode 100644 index 00000000..7590dab6 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/entity/TurtleTimerAccess.java @@ -0,0 +1,20 @@ +package com.sorrowmist.useless.api.entity; + +/** + * 海龟下蛋倒计时的访问接口。 + * + *

{@code Turtle.layEggCounter} 是包级私有字段,原版也没有公开的 setter + * ({@code setHasEgg} / {@code setLayingEgg} 同样包级私有)。造化杖的「生物加速」 + * 需要直接推进它,因此由 mixin 侧的 {@code TurtleAccessor} 生成访问器实现。

+ * + *

为什么接口定义在主代码而不是 mixin 包:本项目的主代码不能直接引用 + * {@code com.sorrowmist.useless.mixin.*} 下的类型(编译期解析不到), + * 既有做法是「主代码定义普通接口 → mixin 实现它」,见 + * {@code PendingOmniversalPatternHolder} / {@code PatternEncodingLogicMixin}。

+ */ +public interface TurtleTimerAccess { + + int uselessMod$getLayEggCounter(); + + void uselessMod$setLayEggCounter(int value); +} diff --git a/src/main/java/com/sorrowmist/useless/api/enums/AlloyFurnaceMode.java b/src/main/java/com/sorrowmist/useless/api/enums/AlloyFurnaceMode.java new file mode 100644 index 00000000..4f51df28 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/enums/AlloyFurnaceMode.java @@ -0,0 +1,34 @@ +package com.sorrowmist.useless.api.enums; + +import com.mojang.serialization.Codec; + +public enum AlloyFurnaceMode { + NORMAL("normal"), + INSOLATOR("insolator"), + PRESS("press"); + + // 提供给 Codec 的便捷方法 + public static final Codec CODEC = Codec.STRING.xmap( + AlloyFurnaceMode::fromString, + AlloyFurnaceMode::getSerializedName + ); + private final String serializedName; + + AlloyFurnaceMode(String serializedName) { + this.serializedName = serializedName; + } + + // 字符串 → 枚举(用于 codec / 网络) + public static AlloyFurnaceMode fromString(String name) { + if (name == null) return NORMAL; + return switch (name.toLowerCase()) { + case "insolator" -> INSOLATOR; + case "press" -> PRESS; + default -> NORMAL; + }; + } + + public String getSerializedName() { + return this.serializedName; + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/enums/CatalystType.java b/src/main/java/com/sorrowmist/useless/api/enums/CatalystType.java new file mode 100644 index 00000000..01fec01d --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/enums/CatalystType.java @@ -0,0 +1,111 @@ +package com.sorrowmist.useless.api.enums; + +import com.sorrowmist.useless.core.config.ConfigManager; +import com.sorrowmist.useless.init.ModItems; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; + +import java.util.function.Supplier; + +/** + * 高级合金炉可识别的催化剂类型枚举。 + * 统一维护催化剂的默认信息和配置驱动的运行参数。 + */ +public enum CatalystType { + NONE(null, 0, "", 1, false), + USELESS_INGOT_TIER_1(ModItems.USELESS_INGOT_TIER_1, 1, "一阶无用锭", 2, false), + USELESS_INGOT_TIER_2(ModItems.USELESS_INGOT_TIER_2, 2, "二阶无用锭", 4, false), + USELESS_INGOT_TIER_3(ModItems.USELESS_INGOT_TIER_3, 3, "三阶无用锭", 8, false), + USELESS_INGOT_TIER_4(ModItems.USELESS_INGOT_TIER_4, 4, "四阶无用锭", 16, false), + USELESS_INGOT_TIER_5(ModItems.USELESS_INGOT_TIER_5, 5, "五阶无用锭", 32, false), + USELESS_INGOT_TIER_6(ModItems.USELESS_INGOT_TIER_6, 6, "六阶无用锭", 64, false), + USELESS_INGOT_TIER_7(ModItems.USELESS_INGOT_TIER_7, 7, "七阶无用锭", 128, false), + USELESS_INGOT_TIER_8(ModItems.USELESS_INGOT_TIER_8, 8, "八阶无用锭", 256, false), + USELESS_INGOT_TIER_9(ModItems.USELESS_INGOT_TIER_9, 9, "九阶无用锭", 512, false), + USEFUL_INGOT(ModItems.USEFUL_INGOT, Integer.MAX_VALUE, "有用锭", Integer.MAX_VALUE, true); + + private final Supplier itemSupplier; + private final int tier; + private final String displayName; + private final int normalRecipeParallel; + private final boolean infiniteParallel; + + CatalystType(Supplier itemSupplier, int tier, String displayName, int normalRecipeParallel, boolean infiniteParallel) { + this.itemSupplier = itemSupplier; + this.tier = tier; + this.displayName = displayName; + this.normalRecipeParallel = normalRecipeParallel; + this.infiniteParallel = infiniteParallel; + } + + /** + * 根据物品堆解析对应的催化剂类型。 + */ + public static CatalystType fromStack(ItemStack stack) { + if (stack.isEmpty()) { + return NONE; + } + for (CatalystType type : values()) { + if (type.itemSupplier != null && stack.is(type.itemSupplier.get())) { + return type; + } + } + return NONE; + } + + /** + * 根据无用锭等级获取对应的催化剂类型。 + */ + public static CatalystType uselessIngotTier(int tier) { + for (CatalystType type : values()) { + if (type.tier == tier && type.isUselessIngotTier()) { + return type; + } + } + return NONE; + } + + public int getTier() { + return tier; + } + + public String getDisplayName() { + return displayName; + } + + public int getNormalRecipeParallel() { + return this == USEFUL_INGOT + ? normalRecipeParallel + : ConfigManager.getAdvancedAlloyFurnaceCatalystParallel(tier); + } + + public boolean isInfiniteParallel() { + return infiniteParallel; + } + + public boolean isUsefulIngot() { + return this == USEFUL_INGOT; + } + + public boolean isValidCatalyst() { + return this != NONE; + } + + public boolean isUselessIngotTier() { + return tier >= 1 && tier <= 9; + } + + /** + * 计算当前催化剂对基础处理时间的修正结果。 + */ + public int calculateProcessTime(int baseTime) { + if (this == USEFUL_INGOT) { + return 1; + } + if (tier <= 0) { + return baseTime; + } + double multiplier = ConfigManager.getAdvancedAlloyFurnaceCatalystTimeMultiplier(tier); + return Math.max(1, (int) Math.ceil(baseTime * multiplier)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/enums/EnumColor.java b/src/main/java/com/sorrowmist/useless/api/enums/EnumColor.java new file mode 100644 index 00000000..c80a1d17 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/enums/EnumColor.java @@ -0,0 +1,120 @@ +package com.sorrowmist.useless.api.enums; + +import net.minecraft.world.item.DyeColor; +import net.minecraft.world.level.material.MapColor; + +import javax.annotation.Nullable; + +public enum EnumColor { + BLACK("Black", "black", new int[]{64, 64, 64}, DyeColor.BLACK), + RED("Red", "red", new int[]{255, 56, 60}, DyeColor.RED), + GREEN("Green", "green", new int[]{89, 193, 95}, DyeColor.GREEN), + BROWN("Brown", "brown", new int[]{161, 118, 73}, DyeColor.BROWN), + BLUE("Blue", "blue", new int[]{54, 107, 208}, DyeColor.BLUE), + PURPLE("Purple", "purple", new int[]{164, 96, 217}, DyeColor.PURPLE), + CYAN("Cyan", "cyan", new int[]{0, 243, 208}, DyeColor.CYAN), + LIGHT_GRAY("Light Gray", "light_gray", new int[]{207, 207, 207}, DyeColor.LIGHT_GRAY), + GRAY("Gray", "gray", new int[]{122, 122, 122}, DyeColor.GRAY), + PINK("Pink", "pink", new int[]{255, 188, 196}, DyeColor.PINK), + LIME("Lime", "lime", new int[]{117, 255, 137}, DyeColor.LIME), + YELLOW("Yellow", "yellow", new int[]{255, 221, 79}, DyeColor.YELLOW), + LIGHT_BLUE("Light Blue", "light_blue", new int[]{85, 158, 255}, DyeColor.LIGHT_BLUE), + MAGENTA("Magenta", "magenta", new int[]{213, 94, 203}, DyeColor.MAGENTA), + ORANGE("Orange", "orange", new int[]{255, 161, 96}, DyeColor.ORANGE), + WHITE("White", "white", new int[]{255, 255, 255}, DyeColor.WHITE), + DARK_RED("Dark Red", "dark_red", new int[]{201, 7, 31}, MapColor.NETHER, null), + AQUA("Aqua", "aqua", new int[]{48, 255, 249}, MapColor.COLOR_LIGHT_BLUE, null); + + private final String englishName; + private final String registryPrefix; + private final int[] rgbCode; + @Nullable + private final DyeColor dyeColor; + private final MapColor mapColor; + + EnumColor(String englishName, String registryPrefix, int[] rgbCode, DyeColor dyeColor) { + this(englishName, registryPrefix, rgbCode, dyeColor.getMapColor(), dyeColor); + } + + EnumColor(String englishName, String registryPrefix, int[] rgbCode, MapColor mapColor, @Nullable DyeColor dyeColor) { + this.englishName = englishName; + this.registryPrefix = registryPrefix; + this.rgbCode = rgbCode; + this.dyeColor = dyeColor; + this.mapColor = mapColor; + } + + /** + * 按定义顺序返回所有颜色 + */ + public static EnumColor[] valuesInOrder() { + return values(); + } + + /** + * 根据名称获取颜色(不区分大小写) + */ + @Nullable + public static EnumColor byName(String name) { + for (EnumColor color : values()) { + if (color.registryPrefix.equalsIgnoreCase(name) || + color.englishName.equalsIgnoreCase(name)) { + return color; + } + } + return null; + } + + /** + * 根据索引获取颜色 + */ + public static EnumColor byIndex(int index) { + if (index < 0 || index >= values().length) { + return WHITE; // 默认返回白色 + } + return values()[index]; + } + + public String getRegistryPrefix() { + return this.registryPrefix; + } + + public String getEnglishName() { + return this.englishName; + } + + public MapColor getMapColor() { + return this.mapColor; + } + + @Nullable + public DyeColor getDyeColor() { + return this.dyeColor; + } + + /** + * 获取RGB整数值(ARGB格式,Alpha为255) + */ + public int getRgb() { + return (0xFF << 24) | (this.rgbCode[0] << 16) | (this.rgbCode[1] << 8) | this.rgbCode[2]; + } + + /** + * 获取RGB数组 [r, g, b] + */ + public int[] getRgbCode() { + return this.rgbCode.clone(); // 返回副本防止外部修改 + } + + /** + * 获取RGB浮点数组 [r, g, b],范围 0.0-1.0 + */ + public float[] getRgbFloat() { + return new float[] { + this.rgbCode[0] / 255.0f, + this.rgbCode[1] / 255.0f, + this.rgbCode[2] / 255.0f + }; + } + +} diff --git a/src/main/java/com/sorrowmist/useless/api/enums/FurnaceFace.java b/src/main/java/com/sorrowmist/useless/api/enums/FurnaceFace.java new file mode 100644 index 00000000..2b61f278 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/enums/FurnaceFace.java @@ -0,0 +1,55 @@ +package com.sorrowmist.useless.api.enums; + +import net.minecraft.core.Direction; +import org.jetbrains.annotations.Nullable; + +/** + * 高级合金炉的6个逻辑面(相对于方块朝向)。 + *

+ * 用于将GUI中的面配置映射到世界中的实际方向,映射结果随方块水平朝向变化。 + */ +public enum FurnaceFace { + TOP, + LEFT, + FRONT, + RIGHT, + BOTTOM, + BACK; + + public static final int COUNT = values().length; + + /** + * 根据方块的水平朝向,将逻辑面转换为世界方向。 + * + * @param facing 方块的水平朝向(FRONT所指方向) + * @return 世界方向 + */ + public Direction toDirection(Direction facing) { + return switch (this) { + case TOP -> Direction.UP; + case BOTTOM -> Direction.DOWN; + case FRONT -> facing; + case BACK -> facing.getOpposite(); + case LEFT -> facing.getClockWise(); + case RIGHT -> facing.getCounterClockWise(); + }; + } + + /** + * 根据方块的水平朝向,将世界方向转换为逻辑面。 + * + * @param side 世界方向 + * @param facing 方块的水平朝向(FRONT所指方向) + * @return 对应的逻辑面,无法匹配时返回null + */ + @Nullable + public static FurnaceFace fromDirection(Direction side, Direction facing) { + if (side == Direction.UP) return TOP; + if (side == Direction.DOWN) return BOTTOM; + if (side == facing) return FRONT; + if (side == facing.getOpposite()) return BACK; + if (side == facing.getClockWise()) return LEFT; + if (side == facing.getCounterClockWise()) return RIGHT; + return null; + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/enums/FurnaceFaceMode.java b/src/main/java/com/sorrowmist/useless/api/enums/FurnaceFaceMode.java new file mode 100644 index 00000000..1fc81bac --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/enums/FurnaceFaceMode.java @@ -0,0 +1,86 @@ +package com.sorrowmist.useless.api.enums; + +/** + * 高级合金炉每个面的输入输出模式。 + *

+ * 每次点击GUI对应区域时在这些状态间循环,用于控制外部物流手段 + * (漏斗、管道等)是否允许从该面输入/输出物品和流体。 + */ +public enum FurnaceFaceMode { + /** 禁止输入输出,UI无变动 */ + DISABLED(-1), + /** 原材料输入:允许外部向输入槽/输入流体槽输入 */ + MATERIAL_INPUT(0), + /** 原材料输出:允许外部从输出槽/输出流体槽抽取 */ + MATERIAL_OUTPUT(12), + /** 原材料输入和输出 */ + MATERIAL_INPUT_OUTPUT(24), + /** 催化剂输入:允许外部向催化剂槽输入 */ + CATALYST_INPUT(36), + /** 模具输入:允许外部向模具槽输入 */ + MOLD_INPUT(48); + + // UI覆盖图在纹理中的U坐标(V固定为265,尺寸11*12),-1表示无覆盖 + private final int overlayU; + + FurnaceFaceMode(int overlayU) { + this.overlayU = overlayU; + } + + /** + * 获取该模式在纹理中的覆盖图U坐标(V=265)。 + * + * @return U坐标,-1表示无覆盖(DISABLED) + */ + public int getOverlayU() { + return this.overlayU; + } + + public boolean hasOverlay() { + return this.overlayU >= 0; + } + + /** 是否允许原材料输入(物品输入槽/输入流体槽) */ + public boolean allowsMaterialInput() { + return this == MATERIAL_INPUT || this == MATERIAL_INPUT_OUTPUT; + } + + /** 是否允许原材料输出(物品输出槽/输出流体槽) */ + public boolean allowsMaterialOutput() { + return this == MATERIAL_OUTPUT || this == MATERIAL_INPUT_OUTPUT; + } + + /** 是否允许催化剂输入 */ + public boolean allowsCatalystInput() { + return this == CATALYST_INPUT; + } + + /** 是否允许模具输入 */ + public boolean allowsMoldInput() { + return this == MOLD_INPUT; + } + + /** 是否允许任何交互(非禁止) */ + public boolean allowsAny() { + return this != DISABLED; + } + + /** + * 循环到下一个模式。 + */ + public FurnaceFaceMode next() { + FurnaceFaceMode[] values = values(); + return values[(this.ordinal() + 1) % values.length]; + } + + public FurnaceFaceMode previous() { + FurnaceFaceMode[] values = values(); + return values[(this.ordinal() - 1 + values.length) % values.length]; + } + + public static FurnaceFaceMode byIndex(int index) { + FurnaceFaceMode[] values = values(); + if (index < 0 || index >= values.length) return DISABLED; + return values[index]; + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/enums/RarityExtension.java b/src/main/java/com/sorrowmist/useless/api/enums/RarityExtension.java new file mode 100644 index 00000000..be21b60c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/enums/RarityExtension.java @@ -0,0 +1,12 @@ +package com.sorrowmist.useless.api.enums; + +import net.minecraft.ChatFormatting; +import net.minecraft.world.item.Rarity; + +/** Forge 1.20.1 extends Rarity through IExtensibleEnum, not NeoForge EnumProxy. */ +public final class RarityExtension { + public record Entry(Rarity value) { public Rarity getValue() { return value; } } + public static final Entry MYTHIC = new Entry(Rarity.create("USELESS_MYTHIC", ChatFormatting.GOLD)); + public static final Entry LEGENDARY = new Entry(Rarity.create("USELESS_LEGENDARY", ChatFormatting.RED)); + private RarityExtension() {} +} diff --git a/src/main/java/com/sorrowmist/useless/api/enums/RedstoneControlMode.java b/src/main/java/com/sorrowmist/useless/api/enums/RedstoneControlMode.java new file mode 100644 index 00000000..d4756c8c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/enums/RedstoneControlMode.java @@ -0,0 +1,55 @@ +package com.sorrowmist.useless.api.enums; + +/** + * 红石控制模式。 + *

+ * 控制合金炉是否响应红石信号来启用/禁用运行。 + */ +public enum RedstoneControlMode { + /** 无红石控制:始终运行 */ + DISABLED(172), + /** 高电平激活:有红石信号时运行 */ + HIGH_ACTIVE(156), + /** 低电平激活:无红石信号时运行 */ + LOW_ACTIVE(140); + + private final int overlayU; + + RedstoneControlMode(int overlayU) { + this.overlayU = overlayU; + } + + /** 获取UI覆盖图纹理U坐标(V=265),-1表示无覆盖 */ + public int getOverlayU() { + return this.overlayU; + } + + public boolean hasOverlay() { + return this.overlayU >= 0; + } + + public RedstoneControlMode next() { + RedstoneControlMode[] values = values(); + return values[(this.ordinal() + 1) % values.length]; + } + + public static RedstoneControlMode byIndex(int index) { + RedstoneControlMode[] values = values(); + if (index < 0 || index >= values.length) return DISABLED; + return values[index]; + } + + /** + * 判断给定红石信号强度下是否应该启用。 + * + * @param hasRedstoneSignal 是否有红石信号 + * @return 是否允许运行 + */ + public boolean shouldRun(boolean hasRedstoneSignal) { + return switch (this) { + case DISABLED -> true; + case HIGH_ACTIVE -> hasRedstoneSignal; + case LOW_ACTIVE -> !hasRedstoneSignal; + }; + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/enums/tool/ConstructionWandCoreMode.java b/src/main/java/com/sorrowmist/useless/api/enums/tool/ConstructionWandCoreMode.java new file mode 100644 index 00000000..a7c0fa9f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/enums/tool/ConstructionWandCoreMode.java @@ -0,0 +1,19 @@ +package com.sorrowmist.useless.api.enums.tool; + +import net.minecraft.network.chat.Component; + +public enum ConstructionWandCoreMode { + DEFAULT("tooltip.useless_mod.construction_wand_default_core"), + ANGEL("tooltip.useless_mod.construction_wand_angel_core"), + DESTRUCTION("tooltip.useless_mod.construction_wand_destruction_core"); + + private final String tooltipKey; + + ConstructionWandCoreMode(String tooltipKey) { + this.tooltipKey = tooltipKey; + } + + public Component getTooltip() { + return Component.translatable(this.tooltipKey); + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/enums/tool/EnchantMode.java b/src/main/java/com/sorrowmist/useless/api/enums/tool/EnchantMode.java new file mode 100644 index 00000000..01326f34 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/enums/tool/EnchantMode.java @@ -0,0 +1,22 @@ +package com.sorrowmist.useless.api.enums.tool; + +import net.minecraft.network.chat.Component; + +public enum EnchantMode { + SILK_TOUCH("silk_touch", "tooltip.useless_mod.silk_touch_mode"), + FORTUNE("fortune", "tooltip.useless_mod.fortune_mode"); + + private final String name; + private final String tooltipKey; + + EnchantMode(String name, String tooltipKey) { + this.name = name; + this.tooltipKey = tooltipKey; + } + + public static int getTotal() {return values().length;} + + public String getName() {return this.name;} + + public Component getTooltip() {return Component.translatable(this.tooltipKey);} +} diff --git a/src/main/java/com/sorrowmist/useless/api/enums/tool/ModeTypeEnum.java b/src/main/java/com/sorrowmist/useless/api/enums/tool/ModeTypeEnum.java new file mode 100644 index 00000000..709b7951 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/enums/tool/ModeTypeEnum.java @@ -0,0 +1,271 @@ +package com.sorrowmist.useless.api.enums.tool; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; + +public enum ModeTypeEnum { + // 增强连锁挖矿模式开关(true=启用增强连锁,false=使用普通连锁) + ENHANCED_CHAIN_MINING_ENABLED("enhanced_chain_mining_enabled", "tooltip.useless_mod.enhanced_chain_mining_mode"), + ENHANCED_CHAIN_MINING_DISABLED("enhanced_chain_mining_disabled", "tooltip.useless_mod.enhanced_chain_mining_mode"), + + // 强制挖掘模式 + FORCE_MINING_ENABLED("force_mining_enabled", "tooltip.useless_mod.force_mining_mode"), + FORCE_MINING_DISABLED("force_mining_disabled", "tooltip.useless_mod.force_mining_mode"), + + // 自动熔炼:true = 挖掘掉落物按熔炉配方熔炼后再入库 + AUTO_SMELT_ENABLED("auto_smelt_enabled", "tooltip.useless_mod.auto_smelt_mode"), + AUTO_SMELT_DISABLED("auto_smelt_disabled", "tooltip.useless_mod.auto_smelt_mode"), + + // AE存储优先模式 + AE_STORAGE_PRIORITY_ENABLED("ae_storage_priority_enabled", "tooltip.useless_mod.ae_storage_priority_mode"), + AE_STORAGE_PRIORITY_DISABLED("ae_storage_priority_disabled", "tooltip.useless_mod.ae_storage_priority_mode"), + + // AE 连接模式:右键有 AE 节点的机器,把它接入工具绑定的那张网 + AE_NETWORK_CONNECT_ENABLED("ae_network_connect_enabled", "tooltip.useless_mod.ae_network_connect_mode"), + AE_NETWORK_CONNECT_DISABLED("ae_network_connect_disabled", "tooltip.useless_mod.ae_network_connect_mode"), + + WRENCH_TAG_ENABLED("wrench_tag_enabled", "tooltip.useless_mod.wrench_tag_mode"), + WRENCH_TAG_DISABLED("wrench_tag_disabled", "tooltip.useless_mod.wrench_tag_mode"), + + CONSTRUCTION_WAND_ENABLED("construction_wand_enabled", "tooltip.useless_mod.construction_wand_mode"), + CONSTRUCTION_WAND_DISABLED("construction_wand_disabled", "tooltip.useless_mod.construction_wand_mode"), + CONSTRUCTION_WAND_ANGEL_CORE("construction_wand_angel_core", "tooltip.useless_mod.construction_wand_angel_core"), + CONSTRUCTION_WAND_DESTRUCTION_CORE("construction_wand_destruction_core", "tooltip.useless_mod.construction_wand_destruction_core"), + + FORCE_KILL("force_kill", "tooltip.useless_mod.force_kill_enabled_mode"), + + BEEF_MALUM_SPIRIT_ENABLED("beef_malum_spirit_enabled", "tooltip.useless_mod.beef_malum_spirit_mode"), + BEEF_MALUM_SPIRIT_DISABLED("beef_malum_spirit_disabled", "tooltip.useless_mod.beef_malum_spirit_mode"), + + BEEF_MYSTICAL_AGRICULTURE_ENABLED("beef_mystical_agriculture_enabled", + "tooltip.useless_mod.beef_mystical_agriculture_mode"), + BEEF_MYSTICAL_AGRICULTURE_DISABLED("beef_mystical_agriculture_disabled", + "tooltip.useless_mod.beef_mystical_agriculture_mode"), + + BEEF_BEHEADING_ENABLED("beef_beheading_enabled", "tooltip.useless_mod.beef_beheading_mode"), + BEEF_BEHEADING_DISABLED("beef_beheading_disabled", "tooltip.useless_mod.beef_beheading_mode"), + + BEEF_TIME_ACCELERATION_ENABLED("beef_time_acceleration_enabled", "tooltip.useless_mod.time_acceleration_mode"), + BEEF_TIME_ACCELERATION_DISABLED("beef_time_acceleration_disabled", "tooltip.useless_mod.time_acceleration_mode"), + + BEEF_INVULNERABILITY_ENABLED("beef_invulnerability_enabled", "tooltip.useless_mod.beef_invulnerability_mode"), + BEEF_INVULNERABILITY_DISABLED("beef_invulnerability_disabled", "tooltip.useless_mod.beef_invulnerability_mode"), + + BEEF_CAPTURE_ENABLED("beef_capture_enabled", "tooltip.useless_mod.beef_capture_mode"), + BEEF_CAPTURE_DISABLED("beef_capture_disabled", "tooltip.useless_mod.beef_capture_mode"), + + BEEF_TELEPORT_ENABLED("beef_teleport_enabled", "tooltip.useless_mod.beef_teleport_mode"), + BEEF_TELEPORT_DISABLED("beef_teleport_disabled", "tooltip.useless_mod.beef_teleport_mode"), + + BEEF_AOE_DAMAGE_ENABLED("beef_aoe_damage_enabled", "tooltip.useless_mod.beef_aoe_damage_mode"), + BEEF_AOE_DAMAGE_DISABLED("beef_aoe_damage_disabled", "tooltip.useless_mod.beef_aoe_damage_mode"), + + BEEF_MAGNET_ENABLED("beef_magnet_enabled", "tooltip.useless_mod.beef_magnet_mode"), + BEEF_MAGNET_DISABLED("beef_magnet_disabled", "tooltip.useless_mod.beef_magnet_mode"), + + BEEF_ADVANCED_STEALTH_ENABLED("beef_advanced_stealth_enabled", "tooltip.useless_mod.beef_advanced_stealth_mode"), + BEEF_ADVANCED_STEALTH_DISABLED("beef_advanced_stealth_disabled", "tooltip.useless_mod.beef_advanced_stealth_mode"), + + // 土壤右键模式:true = 锄头优先(耕地),false = 铲子优先(草径) + BEEF_FARMLAND_MODE_ENABLED("beef_farmland_mode_enabled", "tooltip.useless_mod.beef_farmland_mode"), + BEEF_FARMLAND_MODE_DISABLED("beef_farmland_mode_disabled", "tooltip.useless_mod.beef_farmland_mode"), + + // 「顺手收菜」:true = 右键成熟作物时收获并保留种子于耕地 + BEEF_CROP_HARVEST_ENABLED("beef_crop_harvest_enabled", "tooltip.useless_mod.beef_crop_harvest_mode"), + BEEF_CROP_HARVEST_DISABLED("beef_crop_harvest_disabled", "tooltip.useless_mod.beef_crop_harvest_mode"), + + // 剪刀功能:true = 造化杖可剪羊毛/剪掉落,并对外声明剪刀能力 + BEEF_SHEARS_ENABLED("beef_shears_enabled", "tooltip.useless_mod.beef_shears_mode"), + BEEF_SHEARS_DISABLED("beef_shears_disabled", "tooltip.useless_mod.beef_shears_mode"), + + // 打火石功能:true = 右键可点燃营火/蜡烛,或在可点火位置放火 + BEEF_FLINT_AND_STEEL_ENABLED("beef_flint_and_steel_enabled", "tooltip.useless_mod.beef_flint_and_steel_mode"), + BEEF_FLINT_AND_STEEL_DISABLED("beef_flint_and_steel_disabled", "tooltip.useless_mod.beef_flint_and_steel_mode"), + + // 匠心仪式挎包:true = 右键魔典预览的五芒星,从绑定的 AE 网络取方块摆出整座仪式 + BEEF_RITUAL_SATCHEL_ENABLED("beef_ritual_satchel_enabled", "tooltip.useless_mod.beef_ritual_satchel_mode"), + BEEF_RITUAL_SATCHEL_DISABLED("beef_ritual_satchel_disabled", "tooltip.useless_mod.beef_ritual_satchel_mode"), + + // 催熟:true = 右键可骨粉方块一键催到成熟,右键幼年动物直接催至成年 + BEEF_RIPEN_ENABLED("beef_ripen_enabled", "tooltip.useless_mod.beef_ripen_mode"), + BEEF_RIPEN_DISABLED("beef_ripen_disabled", "tooltip.useless_mod.beef_ripen_mode"), + + // 连点:true = 手持造化杖时以最快速度重复触发右键 + BEEF_AUTO_CLICK_ENABLED("beef_auto_click_enabled", "tooltip.useless_mod.beef_auto_click_mode"), + BEEF_AUTO_CLICK_DISABLED("beef_auto_click_disabled", "tooltip.useless_mod.beef_auto_click_mode"), + + // 强制生长:true = 催熟时额外用随机刻推进(可作用于甘蔗/仙人掌/竹子等骨粉无效的方块) + BEEF_FORCE_GROW_ENABLED("beef_force_grow_enabled", "tooltip.useless_mod.beef_force_grow_mode"), + BEEF_FORCE_GROW_DISABLED("beef_force_grow_disabled", "tooltip.useless_mod.beef_force_grow_mode"), + + // 无线物流:潜行右键容器绑定/解绑,界面里配置搬运规则 + BEEF_WIRELESS_LOGISTICS_ENABLED("beef_wireless_logistics_enabled", + "tooltip.useless_mod.beef_wireless_logistics_mode"), + BEEF_WIRELESS_LOGISTICS_DISABLED("beef_wireless_logistics_disabled", + "tooltip.useless_mod.beef_wireless_logistics_mode"), + + // Ex Deorum 钩子:true = 挖掘时按 exdeorum 的钩子配方追加掉落 + EXDEORUM_CROOK_ENABLED("exdeorum_crook_enabled", "tooltip.useless_mod.exdeorum_crook_mode"), + EXDEORUM_CROOK_DISABLED("exdeorum_crook_disabled", "tooltip.useless_mod.exdeorum_crook_mode"), + + // Ex Deorum 锤子:true = 挖掘时按 exdeorum 的锤子配方改写掉落 + EXDEORUM_HAMMER_ENABLED("exdeorum_hammer_enabled", "tooltip.useless_mod.exdeorum_hammer_mode"), + EXDEORUM_HAMMER_DISABLED("exdeorum_hammer_disabled", "tooltip.useless_mod.exdeorum_hammer_mode"), + + // Ex Deorum 压缩锤:配方体系与普通锤相互独立,单列一项开关 + EXDEORUM_COMPRESSED_HAMMER_ENABLED("exdeorum_compressed_hammer_enabled", + "tooltip.useless_mod.exdeorum_compressed_hammer_mode"), + EXDEORUM_COMPRESSED_HAMMER_DISABLED("exdeorum_compressed_hammer_disabled", + "tooltip.useless_mod.exdeorum_compressed_hammer_mode"), + + // 杀戮光环:手持造化杖每 20 tick 对「范围伤害」配置范围内生物结算一次攻击伤害 + BEEF_KILL_AURA_ENABLED("beef_kill_aura_enabled", "tooltip.useless_mod.beef_kill_aura_mode"), + BEEF_KILL_AURA_DISABLED("beef_kill_aura_disabled", "tooltip.useless_mod.beef_kill_aura_mode"), + + // 保护名单模式:开启时暂停光环与范围伤害,方便从容添加保护名单 + BEEF_PROTECT_MODE_ENABLED("beef_protect_mode_enabled", "tooltip.useless_mod.beef_protect_toggle_mode"), + BEEF_PROTECT_MODE_DISABLED("beef_protect_mode_disabled", "tooltip.useless_mod.beef_protect_toggle_mode"), + + // 生物加速:潜行右键生物加速其「计时器」(生长/繁殖冷却/下蛋/补货/长毛…),不跑 AI 与移动 + BEEF_ENTITY_TIME_ACCELERATION_ENABLED("beef_entity_time_acceleration_enabled", + "tooltip.useless_mod.beef_entity_time_acceleration_mode"), + BEEF_ENTITY_TIME_ACCELERATION_DISABLED("beef_entity_time_acceleration_disabled", + "tooltip.useless_mod.beef_entity_time_acceleration_mode"); + + private final String name; + private final String tooltipKey; + + ModeTypeEnum(String name, String tooltipKey) { + this.name = name; + this.tooltipKey = tooltipKey; + } + + public static int getTotal() {return values().length;} + + // 辅助方法:根据模式类型和状态获取对应的枚举值 + public static ModeTypeEnum getEnhancedChainMiningMode(boolean enabled) { + return enabled ? ENHANCED_CHAIN_MINING_ENABLED : ENHANCED_CHAIN_MINING_DISABLED; + } + + public static ModeTypeEnum getForceMiningMode(boolean enabled) { + return enabled ? FORCE_MINING_ENABLED : FORCE_MINING_DISABLED; + } + + public static ModeTypeEnum getAutoSmeltMode(boolean enabled) { + return enabled ? AUTO_SMELT_ENABLED : AUTO_SMELT_DISABLED; + } + + public static ModeTypeEnum getAEStoragePriorityMode(boolean enabled) { + return enabled ? AE_STORAGE_PRIORITY_ENABLED : AE_STORAGE_PRIORITY_DISABLED; + } + + public static ModeTypeEnum getAeNetworkConnectMode(boolean enabled) { + return enabled ? AE_NETWORK_CONNECT_ENABLED : AE_NETWORK_CONNECT_DISABLED; + } + + public static ModeTypeEnum getWrenchTagMode(boolean enabled) { + return enabled ? WRENCH_TAG_ENABLED : WRENCH_TAG_DISABLED; + } + + public static ModeTypeEnum getConstructionWandMode(boolean enabled) { + return enabled ? CONSTRUCTION_WAND_ENABLED : CONSTRUCTION_WAND_DISABLED; + } + + public static ModeTypeEnum getBeefInvulnerabilityMode(boolean enabled) { + return enabled ? BEEF_INVULNERABILITY_ENABLED : BEEF_INVULNERABILITY_DISABLED; + } + + public static ModeTypeEnum getBeefTimeAccelerationMode(boolean enabled) { + return enabled ? BEEF_TIME_ACCELERATION_ENABLED : BEEF_TIME_ACCELERATION_DISABLED; + } + + public static ModeTypeEnum getBeefMalumSpiritMode(boolean enabled) { + return enabled ? BEEF_MALUM_SPIRIT_ENABLED : BEEF_MALUM_SPIRIT_DISABLED; + } + + public static ModeTypeEnum getBeefMysticalAgricultureMode(boolean enabled) { + return enabled ? BEEF_MYSTICAL_AGRICULTURE_ENABLED : BEEF_MYSTICAL_AGRICULTURE_DISABLED; + } + + public static ModeTypeEnum getBeefBeheadingMode(boolean enabled) { + return enabled ? BEEF_BEHEADING_ENABLED : BEEF_BEHEADING_DISABLED; + } + + public static ModeTypeEnum getBeefCaptureMode(boolean enabled) { + return enabled ? BEEF_CAPTURE_ENABLED : BEEF_CAPTURE_DISABLED; + } + + public static ModeTypeEnum getBeefTeleportMode(boolean enabled) { + return enabled ? BEEF_TELEPORT_ENABLED : BEEF_TELEPORT_DISABLED; + } + + public static ModeTypeEnum getBeefAoeDamageMode(boolean enabled) { + return enabled ? BEEF_AOE_DAMAGE_ENABLED : BEEF_AOE_DAMAGE_DISABLED; + } + + public static ModeTypeEnum getBeefMagnetMode(boolean enabled) { + return enabled ? BEEF_MAGNET_ENABLED : BEEF_MAGNET_DISABLED; + } + + public static ModeTypeEnum getBeefAdvancedStealthMode(boolean enabled) { + return enabled ? BEEF_ADVANCED_STEALTH_ENABLED : BEEF_ADVANCED_STEALTH_DISABLED; + } + + public static ModeTypeEnum getBeefFarmlandMode(boolean enabled) { + return enabled ? BEEF_FARMLAND_MODE_ENABLED : BEEF_FARMLAND_MODE_DISABLED; + } + + public static ModeTypeEnum getBeefCropHarvestMode(boolean enabled) { + return enabled ? BEEF_CROP_HARVEST_ENABLED : BEEF_CROP_HARVEST_DISABLED; + } + + public static ModeTypeEnum getBeefShearsMode(boolean enabled) { + return enabled ? BEEF_SHEARS_ENABLED : BEEF_SHEARS_DISABLED; + } + + public static ModeTypeEnum getBeefFlintAndSteelMode(boolean enabled) { + return enabled ? BEEF_FLINT_AND_STEEL_ENABLED : BEEF_FLINT_AND_STEEL_DISABLED; + } + + public static ModeTypeEnum getBeefRitualSatchelMode(boolean enabled) { + return enabled ? BEEF_RITUAL_SATCHEL_ENABLED : BEEF_RITUAL_SATCHEL_DISABLED; + } + + public static ModeTypeEnum getBeefRipenMode(boolean enabled) { + return enabled ? BEEF_RIPEN_ENABLED : BEEF_RIPEN_DISABLED; + } + + public static ModeTypeEnum getBeefAutoClickMode(boolean enabled) { + return enabled ? BEEF_AUTO_CLICK_ENABLED : BEEF_AUTO_CLICK_DISABLED; + } + + public static ModeTypeEnum getBeefForceGrowMode(boolean enabled) { + return enabled ? BEEF_FORCE_GROW_ENABLED : BEEF_FORCE_GROW_DISABLED; + } + + public static ModeTypeEnum getBeefWirelessLogisticsMode(boolean enabled) { + return enabled ? BEEF_WIRELESS_LOGISTICS_ENABLED : BEEF_WIRELESS_LOGISTICS_DISABLED; + } + + public static ModeTypeEnum getBeefKillAuraMode(boolean enabled) { + return enabled ? BEEF_KILL_AURA_ENABLED : BEEF_KILL_AURA_DISABLED; + } + + public static ModeTypeEnum getBeefProtectMode(boolean enabled) { + return enabled ? BEEF_PROTECT_MODE_ENABLED : BEEF_PROTECT_MODE_DISABLED; + } + + public static ModeTypeEnum getBeefEntityTimeAccelerationMode(boolean enabled) { + return enabled ? BEEF_ENTITY_TIME_ACCELERATION_ENABLED : BEEF_ENTITY_TIME_ACCELERATION_DISABLED; + } + + public String getName() {return this.name;} + + public Component getTooltip() {return Component.translatable(this.tooltipKey);} + + public Component getStatusComponent(boolean active) { + return Component.translatable( + active ? "tooltip.useless_mod.enable" : "tooltip.useless_mod.disable" + ).withStyle(active ? ChatFormatting.GREEN : ChatFormatting.GRAY); + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/enums/tool/ToolTypeMode.java b/src/main/java/com/sorrowmist/useless/api/enums/tool/ToolTypeMode.java new file mode 100644 index 00000000..414ec3da --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/enums/tool/ToolTypeMode.java @@ -0,0 +1,32 @@ +package com.sorrowmist.useless.api.enums.tool; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; + +public enum ToolTypeMode { + NONE_MODE("none_mode", "tooltip.useless_mod.none_mode"), + WRENCH_MODE("wrench_mode", "tooltip.useless_mod.wrench_mode"), + SCREWDRIVER_MODE("screwdriver_mode", "tooltip.useless_mod.screwdriver_mode"), + MALLET_MODE("mallet_mode", "tooltip.useless_mod.mallet_mode"), + CROWBAR_MODE("crowbar_mode", "tooltip.useless_mod.crowbar_mode"), + HAMMER_MODE("hammer_mode", "tooltip.useless_mod.hammer_mode"), + OMNITOOL_MODE("omnitool_mode", "tooltip.useless_mod.omnitool_mode"); + + private final String name; + private final String tooltipKey; + + ToolTypeMode(String name, String tooltipKey) { + this.name = name; + this.tooltipKey = tooltipKey; + } + + public static int getTotal() {return values().length;} + + public String getName() {return this.name;} + + public Component getTooltip() {return Component.translatable(this.tooltipKey);} + + public Component getTooltip(ChatFormatting... styles) { + return Component.translatable(this.tooltipKey).withStyle(styles); + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/logistics/LongEnergyHandler.java b/src/main/java/com/sorrowmist/useless/api/logistics/LongEnergyHandler.java new file mode 100644 index 00000000..286636b7 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/logistics/LongEnergyHandler.java @@ -0,0 +1,33 @@ +package com.sorrowmist.useless.api.logistics; + +/** + * long 级能量处理器。 + * + *

MinecraftForge 的 {@code IEnergyStorage} 只有 int 面。本模组自己的 {@code IEnergyManager} + * 从设计之初就是 long 契约,因此会原生长途直通:不存在任何 int 中转,也没有补循环。

+ * + *

外部模组的 int 级能量能力由适配器包装成这一层,内部分片细节被封在适配器里, + * 调用方看不到也不需要知道。

+ */ +public interface LongEnergyHandler extends LongResourceHandler { + + /** 抽出最多 {@code amount} 能量,返回实际抽出量。 */ + long extract(long amount, boolean simulate); + + /** 接收最多 {@code amount} 能量,返回实际接收量。 */ + long receive(long amount, boolean simulate); + + /** 当前储量。 */ + long stored(); + + /** 容量。 */ + long capacity(); + + default boolean canExtract() { + return true; + } + + default boolean canReceive() { + return true; + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/logistics/LongFluidHandler.java b/src/main/java/com/sorrowmist/useless/api/logistics/LongFluidHandler.java new file mode 100644 index 00000000..91bd6bb0 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/logistics/LongFluidHandler.java @@ -0,0 +1,92 @@ +package com.sorrowmist.useless.api.logistics; + +import net.minecraftforge.fluids.FluidStack; + +/** + * long 级流体处理器。 + * + *

MinecraftForge 的 {@code IFluidHandler} 单次只能 {@code fill}/{@code drain} 一个 int, + * 且 {@code FluidStack} 的 amount 本身就是 int。这里把「类型」和「数量」拆开表达: + * 数量走 {@code long} 参数与返回值,类型由 {@link FluidStack} 单独承载(其 amount 视为占位)。

+ */ +public interface LongFluidHandler extends LongResourceHandler { + + /** 储罐数量。 */ + int getTanks(); + + /** + * 查看某个储罐里的流体类型。只读:返回栈的 amount 不代表罐内真实数量, + * 真实数量请用 {@link #amountIn(int)}。 + */ + FluidStack getFluidInTank(int tank); + + /** 储罐内实际流体量(long 语义)。空罐返回 0。 */ + long amountIn(int tank); + + /** 储罐容量(long 语义)。 */ + long capacityOf(int tank); + + /** + * 该处理器里一共有多少这种流体(跨罐累加,long 语义)。 + * + *

语义与 {@link LongItemHandler#amountOf(net.minecraft.world.item.ItemStack)} 完全对称: + * 给过滤器的「源端保留」「接收端上限」提供「现在有多少」这个基准。

+ * + *

默认实现 = {@link #findTank(FluidStack)} + {@link #amountIn(int)}; + * AE 端点必须覆写(否则要抓整网快照,且按类型跨罐累加才算得对)。

+ */ + default long amountOf(FluidStack type) { + if (type == null || type.isEmpty()) { + return 0L; + } + int tank = findTank(type); + return tank < 0 ? 0L : amountIn(tank); + } + + /** 从指定储罐抽出 {@code amount}。返回值是实际抽出量;{@code simulate} 为真时只试算。 */ + long drain(int tank, long amount, boolean simulate); + + /** + * 向该处理器注入 {@code type} 流体 {@code amount}。 + * {@code type} 只取其流体类型与组件,其 amount 被忽略。 + * + * @return 实际注入量;{@code simulate} 为真时只试算 + */ + long fill(FluidStack type, long amount, boolean simulate); + + /** + * 找出「和给定流体同类型」的储罐;找不到返回 -1。 + * + *

理由同 {@link LongItemHandler#findSlot(net.minecraft.world.item.ItemStack)}: + * 过滤器驱动时按类型直接定位,不要靠扫描——AE 端点的 {@code getTanks()} 是网络里的 + * 流体种类数,扫描会被调用方的预算截断,排在后面的那些永远找不到。

+ */ + default int findTank(FluidStack type) { + if (type == null || type.isEmpty()) { + return -1; + } + for (int tank = 0; tank < getTanks(); tank++) { + if (amountIn(tank) > 0L + && com.sorrowmist.useless.compat.forge1201.fluid.FluidStacks.same(getFluidInTank(tank), type)) { + return tank; + } + } + return -1; + } + + /** + * 按类型直接抽走 {@code amount},不经过「第几号罐」。语义与 + * {@link LongItemHandler#extractMatching(net.minecraft.world.item.ItemStack, long, boolean)} + * 完全对称:一次调用同时回答「有没有这一种」和「能抽多少」,默认实现 = + * {@link #findTank(FluidStack)} + {@link #drain(int, long, boolean)}。 + * + *

AE 端点必须覆写它 —— 否则每次都会先抓一份整网快照(遍历 ME 网络里的每一种流体)。

+ */ + default long drainMatching(FluidStack type, long amount, boolean simulate) { + if (type == null || type.isEmpty() || amount <= 0L) { + return 0L; + } + int tank = findTank(type); + return tank < 0 ? 0L : drain(tank, amount, simulate); + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/logistics/LongItemHandler.java b/src/main/java/com/sorrowmist/useless/api/logistics/LongItemHandler.java new file mode 100644 index 00000000..f6a08ee3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/logistics/LongItemHandler.java @@ -0,0 +1,151 @@ +package com.sorrowmist.useless.api.logistics; + +import net.minecraft.world.item.ItemStack; + +/** + * long 级物品处理器。 + * + *

和 MinecraftForge 的 {@code IItemHandler} 相比,这里的「数量」参数与返回值都是 {@code long}: + * 一次调用要搬的数量不会被截断到 {@link Integer#MAX_VALUE},调用方也不需要在外面补循环。

+ * + *

槽内堆叠上限仍然是物理事实(原版 64),所以底层是 int 槽位的实现一次能抽出的数量受堆叠 + * 限制;但接口本身不设 int 上限,超大容量或多槽聚合的实现可以一次性给出更多。

+ */ +public interface LongItemHandler extends LongResourceHandler { + + /** 槽位数。 */ + int getSlots(); + + /** + * 查看槽里的物品。只读:返回的栈仅用于判断类型,其 count 不代表槽内真实数量, + * 真实数量请用 {@link #amountIn(int)}。 + */ + ItemStack getStackInSlot(int slot); + + /** + * 扫描路径专用的读取入口:只用于判断类型,数量看 {@link #amountIn(int)}。 + * + *

与 {@link #getStackInSlot(int)} 的唯一区别是返回值不保证是副本——实现可以直接 + * 交出自己内部的对象。因此调用方不得长期持有它,也不得在 extract 之后依赖它: + * 槽位可能被就地清空,手里那个对象会跟着一起变空(典型是原生 {@code IItemHandler} 的 + * 引用型返回值)。需要在抽取之后继续用的话,先自己 {@code copy()} 一份。

+ * + *

存在的理由:有的实现(例如 AE2 的 {@code GenericStackItemStorage})每次 + * {@code getStackInSlot} 都新建一个 {@code ItemStack},外层再 copy 一遍纯属浪费—— + * 在 1.21 的组件化物品栈上,一次 copy 要连带复制整份 DataComponent 映射,实测占无线物流 + * 总耗时的 13%(见内部无线物流性能分析报告第十二节)。

+ * + *

默认实现委托 {@link #getStackInSlot(int)},因此旧实现的行为逐字节不变。

+ */ + default ItemStack peekStack(int slot) { + return getStackInSlot(slot); + } + + /** 槽内实际数量(long 语义)。空槽返回 0。 */ + long amountIn(int slot); + + /** + * 扫描路径的优化入口:调用方手里已经有 {@link #getStackInSlot(int)} 的结果。 + * + *

默认实现忽略 {@code stackFromSlot},直接走 {@link #amountIn(int)}——这是正确的 + * 默认值,因为 {@link #getStackInSlot(int)} 的返回值「count 不代表槽内真实数量」(见其说明)。 + * 只有「槽内栈的 count 就是真实数量」的实现才该覆写它。

+ * + *

存在的理由:扫描路径本来就要读一次 {@code getStackInSlot} 拿类型,紧接着又要 + * {@code amountIn} 拿数量。对底层是 int 槽位的实现,后者等于把同一个槽位再读一遍; + * 对 AE 端点则是一次完整的 {@code AEKey → ItemStack} 物化(要连带复制整份组件映射)。 + * 覆写它就能把这一次省掉。

+ * + *

调用方必须保证 {@code stackFromSlot} 确实取自同一个 {@code slot}: + * 覆写实现会直接拿它的 count 当答案。

+ */ + default long amountIn(int slot, ItemStack stackFromSlot) { + return amountIn(slot); + } + + /** + * 该处理器里一共有多少这种物品(跨槽累加,long 语义)。 + * + *

存在的理由:过滤器的「源端保留 X 个」「接收端最多 Y 个」需要知道「现在有多少」, + * 而这既不是某个槽位的数字(可能分散在多个槽),也不是 {@code extract(..., simulate)} + * 的返回值(那个还叠了容量上限)。

+ * + *

默认实现 = {@link #findSlot(ItemStack)} + {@link #amountIn(int)},只回答「第一个 + * 同类型槽位的存量」。对「同一种物品最多出现在一个槽位」的实现(原版容器、本模组的 + * 大容量容器)这已经是正确答案;对可能分散的实现应覆写。

+ * + *

调用方只在真的需要限制时才调它:AE 端点覆写后要抓一次整网快照, + * 那是线性开销,不能当成免费查询。

+ */ + default long amountOf(ItemStack template) { + if (template == null || template.isEmpty()) { + return 0L; + } + int slot = findSlot(template); + return slot < 0 ? 0L : amountIn(slot); + } + + /** + * 从指定槽位抽走 {@code amount} 个物品。 + * + * @return 实际抽出的数量(可能小于请求量);{@code simulate} 为真时只试算不改动 + */ + long extract(int slot, long amount, boolean simulate); + + /** + * 插入物品。{@code template} 只取其类型,其 count 被忽略,实际插入量由 {@code amount} 决定。 + * + * @return 实际插入的数量(可能小于请求量);{@code simulate} 为真时只试算不改动 + */ + long insert(ItemStack template, long amount, boolean simulate); + + /** + * 找出「和给定物品同类型」的槽位;找不到返回 -1。 + * + *

过滤器驱动的搬运靠它直接定位:有过滤器时「要搬什么」已经完全确定,直接问 + * 「源端有没有这一种」即可,不必扫全表。

+ * + *

对条目数很大的实现必须覆写。默认实现是线性扫描,而 AE 端点的 + * {@link #getSlots()} 是整个 ME 网络的资源种类数(动辄上千),靠扫描找某一种既慢, + * 又会因为调用方的扫描预算而永远找不到排在后面的那些。AE 端点按资源键定位。

+ */ + default int findSlot(ItemStack template) { + if (template == null || template.isEmpty()) { + return -1; + } + for (int slot = 0; slot < getSlots(); slot++) { + if (amountIn(slot) > 0L + && ItemStack.isSameItemSameTags(getStackInSlot(slot), template)) { + return slot; + } + } + return -1; + } + + /** + * 按类型直接抽走 {@code amount} 个 {@code template},不经过「第几号槽」。 + * + *

过滤器驱动的搬运只走这一个入口。它把「源端有没有这一种」和「能抽多少」 + * 合并成一次调用({@code simulate} 为真时返回值就是可抽量),调用方因此不必先 + * {@link #findSlot(ItemStack)} 探一次、再 {@code amountIn} 问一次。

+ * + *

默认实现 = {@link #findSlot(ItemStack)} + {@link #extract(int, long, boolean)}, + * 对槽位有限的实现完全够用(多一次槽位扫描,代价可忽略)。

+ * + *

条目数很大的实现必须覆写。默认实现要先 {@code findSlot},而 AE 端点的 + * {@code findSlot} 需要先抓一份整网快照 —— 那是一次完整的 + * {@code MEStorage.getAvailableStacks()},把 ME 网络里的每一种资源都遍历一遍。 + * 覆写成「拿 {@code template} 直接构造资源键、问一次 {@code MEStorage.extract}」即可 + * 把这一步变成一次哈希查找。AE2 的 {@code extract} 本身就是 long 签名, + * 「先模拟后提交」的时序也完全不变。

+ * + * @return 实际抽出的数量;{@code simulate} 为真时只试算不改动 + */ + default long extractMatching(ItemStack template, long amount, boolean simulate) { + if (template == null || template.isEmpty() || amount <= 0L) { + return 0L; + } + int slot = findSlot(template); + return slot < 0 ? 0L : extract(slot, amount, simulate); + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/logistics/LongPressureHandler.java b/src/main/java/com/sorrowmist/useless/api/logistics/LongPressureHandler.java new file mode 100644 index 00000000..0a7b7eb7 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/logistics/LongPressureHandler.java @@ -0,0 +1,62 @@ +package com.sorrowmist.useless.api.logistics; + +/** + * long 级气压处理器。 + * + *

与 {@link LongEnergyHandler} 同一层契约,但多带两个「气压」特有的量:{@link #volume()} 与 + * {@link #pressure()}。气压与能量、流体都不同:它是状态量而不是纯流量——「搬多少」取决于 + * 目标离目标气压还差多少,而不是每轮固定搬一批。所以端点必须能报出自己的体积与当前空气量, + * 调用方才算得出「要补多少 / 要抽多少」。

+ * + *

空气量可以为负。 气动工艺用 {@code air = pressure × volume} 表示空气量,真空就是负值 + * (下限 {@code -volume},即 -1 bar 的绝对真空)。因此 {@link #stored()} 是有符号的, + * {@link #capacity()} 只表示正压侧的上限。

+ */ +public interface LongPressureHandler extends LongResourceHandler { + + /** + * 移除最多 {@code amount} mL 空气,返回实际移除量。 + * + *

下限是 0,不制造真空。 这条是「把源端的气搬到别处」用的——抽到 0 bar(无压)为止, + * 不会把源端抽成负压。要主动降成真空请用 {@link #setAirTo}。

+ */ + long extract(long amount, boolean simulate); + + /** 接收最多 {@code amount} mL 空气,返回实际接收量。 */ + long receive(long amount, boolean simulate); + + /** 当前空气量(mL)。可为负:负值表示真空。 */ + long stored(); + + /** + * 正压侧容量(mL);AE 网络没有上限,取 {@link Long#MAX_VALUE}。 + * + *

方块侧取的是「危险压力 × 体积」——即开始有爆炸风险的那个压力对应的空气量, + * 而不是实现自己报的某个常量上限(气动的 {@code maxPressure()} 恒为 10 bar,不反映真实阈值)。

+ */ + long capacity(); + + /** + * 有效体积(mL)。 + * + *

AE 网络这类「只有一堆空气、没有容器」的承载返回 {@code 0}——它没有压力概念, + * 因此目标气压对它无意义。

+ */ + long volume(); + + /** 当前压力(bar);无体积的承载恒为 0。 */ + default double pressure() { + long volume = volume(); + return volume > 0L ? (double) stored() / (double) volume : 0.0D; + } + + /** + * 把空气量直接调整到 {@code targetAir}(mL,可为负 = 真空),返回实际变化量。 + * + *

供「把多余空气排到环境」这类没有源端的调节使用:降真空时空气量会越过 0 继续下降, + * 因此不能用 {@link #extract}。无体积的承载(AE 网络)不支持该操作,返回 0。

+ */ + default long setAirTo(long targetAir, boolean simulate) { + return 0L; + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/logistics/LongResourceHandler.java b/src/main/java/com/sorrowmist/useless/api/logistics/LongResourceHandler.java new file mode 100644 index 00000000..9dc8ca23 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/logistics/LongResourceHandler.java @@ -0,0 +1,13 @@ +package com.sorrowmist.useless.api.logistics; + +/** + * long 级资源处理器的公共标记。 + * + *

{@link LongItemHandler}、{@link LongFluidHandler}、{@link LongEnergyHandler} 都实现它, + * 让「解析出来的处理器」有一个统一的静态类型,调用方不必先 cast 成 {@code Object} 再试。

+ * + *

化学品刻意不在这个家族里:它的视图接口({@code ChemicalHandlerView})从设计之初就以 long + * 计数,本身已经是 long 级契约,再包一层只会多一次无意义的转发。

+ */ +public interface LongResourceHandler { +} diff --git a/src/main/java/com/sorrowmist/useless/api/logistics/package-info.java b/src/main/java/com/sorrowmist/useless/api/logistics/package-info.java new file mode 100644 index 00000000..06fe45ad --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/logistics/package-info.java @@ -0,0 +1,13 @@ +/** + * long 级资源搬运接口。 + * + *

MinecraftForge 原生的槽位能力({@code IItemHandler} / {@code IFluidHandler} / + * {@code IEnergyStorage})每一步都收 {@code int}。于是「一次搬 5×10^9 个」这种请求会被先截断成 + * {@link Integer#MAX_VALUE},剩下的只能靠调用方在外面反复循环补足——既慢,又让调用方必须自己 + * 维护「还剩多少没搬」的状态。

+ * + *

这一层把 long 语义提到接口本身:调用方说一次「最多搬这么多」,由实现决定用多少次底层 + * {@code int} 调用把它凑出来。本模组自己的 long 能力(如 {@code IEnergyManager})会被直接识别并 + * 原生长途直通,不再经过任何 int 中转。

+ */ +package com.sorrowmist.useless.api.logistics; diff --git a/src/main/java/com/sorrowmist/useless/api/recipe/AlloyFurnaceCompatApi.java b/src/main/java/com/sorrowmist/useless/api/recipe/AlloyFurnaceCompatApi.java new file mode 100644 index 00000000..a2a407ab --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/recipe/AlloyFurnaceCompatApi.java @@ -0,0 +1,76 @@ +package com.sorrowmist.useless.api.recipe; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import org.slf4j.Logger; + +import java.util.Objects; +import java.util.function.BooleanSupplier; + +/** + * Entry point for third-party alloy-furnace compatibility modules. + * + *
{@code
+ * event.enqueueWork(() -> AlloyFurnaceCompatApi.register(
+ *         "example_compat", Config.ENABLED::get, new ExampleRecipeAdapter()));
+ * }
+ */ +public final class AlloyFurnaceCompatApi { + private static final Logger LOGGER = LogUtils.getLogger(); + + private AlloyFurnaceCompatApi() { + } + + /** Registers adapters with a source that is always enabled. */ + public static void register(String sourceId, IRecipeAdapter... adapters) { + register(sourceId, () -> true, adapters); + } + + /** + * Registers adapters when {@code enabled} is true. + * + *

The supplier is evaluated once during registration. Configuration changes should take + * effect after the normal restart required by the owning mod's configuration.

+ */ + public static void register(String sourceId, BooleanSupplier enabled, + IRecipeAdapter... adapters) { + Objects.requireNonNull(sourceId, "sourceId"); + Objects.requireNonNull(enabled, "enabled"); + if (sourceId.isBlank()) { + throw new IllegalArgumentException("sourceId cannot be blank"); + } + if (adapters == null || adapters.length == 0) { + return; + } + + final boolean isEnabled; + try { + isEnabled = enabled.getAsBoolean(); + } catch (RuntimeException exception) { + RateLimitedLogger.error(LOGGER, "Failed to evaluate alloy-furnace compatibility switch: source={}", + sourceId, exception); + return; + } + if (!isEnabled) { + return; + } + + String normalizedSource = RecipeSourceIds.normalize(sourceId); + AlloyFurnaceRecipeManager manager = AlloyFurnaceRecipeManager.getInstance(); + for (IRecipeAdapter adapter : adapters) { + if (adapter == null) { + RateLimitedLogger.warn(LOGGER, "Skipping null alloy-furnace compatibility adapter: source={}", normalizedSource); + continue; + } + try { + manager.registerAdapter(adapter, normalizedSource); + } catch (RuntimeException | LinkageError exception) { + RateLimitedLogger.error(LOGGER, "Failed to register alloy-furnace compatibility adapter: source={}, adapter={}", + normalizedSource, adapter.getClass().getName(), exception); + } + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/recipe/AlloyFurnaceRecipeBuilder.java b/src/main/java/com/sorrowmist/useless/api/recipe/AlloyFurnaceRecipeBuilder.java new file mode 100644 index 00000000..b6331165 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/recipe/AlloyFurnaceRecipeBuilder.java @@ -0,0 +1,138 @@ +package com.sorrowmist.useless.api.recipe; + +import appeng.api.stacks.GenericStack; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipeBuilder; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.ItemLike; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; + +import java.util.List; + +/** Public builder for recipes returned by an alloy-furnace compatibility adapter. */ +public final class AlloyFurnaceRecipeBuilder extends AdvancedAlloyFurnaceRecipeBuilder { + private AlloyFurnaceRecipeBuilder() { + } + + public static AlloyFurnaceRecipeBuilder create() { + return new AlloyFurnaceRecipeBuilder(); + } + + @Override + public AlloyFurnaceRecipeBuilder input(Ingredient ingredient, long count) { + super.input(ingredient, count); + return this; + } + + @Override + public AlloyFurnaceRecipeBuilder input(ItemLike item, long count) { + super.input(item, count); + return this; + } + + @Override + public AlloyFurnaceRecipeBuilder fluidInput(FluidStack fluid) { + super.fluidInput(fluid); + return this; + } + + @Override + public AlloyFurnaceRecipeBuilder fluidInput(SizedFluidIngredient fluid) { + super.fluidInput(fluid); + return this; + } + + @Override + public AlloyFurnaceRecipeBuilder fluidInput(LongSizedFluidIngredient fluid) { + super.fluidInput(fluid); + return this; + } + + @Override + public AlloyFurnaceRecipeBuilder keyInput(GenericStack keyInput) { + super.keyInput(keyInput); + return this; + } + + @Override + public AlloyFurnaceRecipeBuilder output(ItemLike item, int count) { + super.output(item, count); + return this; + } + + @Override + public AlloyFurnaceRecipeBuilder output(ItemStack output) { + super.output(output); + return this; + } + + @Override + public AlloyFurnaceRecipeBuilder fluidOutput(FluidStack output) { + super.fluidOutput(output); + return this; + } + + @Override + public AlloyFurnaceRecipeBuilder keyOutput(GenericStack keyOutput) { + super.keyOutput(keyOutput); + return this; + } + + @Override + public AlloyFurnaceRecipeBuilder energy(long energy) { + super.energy(energy); + return this; + } + + @Override + public AlloyFurnaceRecipeBuilder processTime(int ticks) { + super.processTime(ticks); + return this; + } + + @Override + public AlloyFurnaceRecipeBuilder catalyst(Ingredient catalyst, int uses) { + super.catalyst(catalyst, uses); + return this; + } + + @Override + public AlloyFurnaceRecipeBuilder mold(Ingredient mold) { + super.mold(mold); + return this; + } + + @Override + public AlloyFurnaceRecipeBuilder molds(List molds) { + super.molds(molds); + return this; + } + + @Override + public AlloyFurnaceRecipeBuilder molds(Ingredient... molds) { + super.molds(molds); + return this; + } + + @Override + public AlloyFurnaceRecipeBuilder addMold(Ingredient mold) { + super.addMold(mold); + return this; + } + + @Override + public AlloyFurnaceRecipeBuilder mode(AlloyFurnaceMode mode) { + super.mode(mode); + return this; + } + + @Override + public AdvancedAlloyFurnaceRecipe build(ResourceLocation id) { + return super.build(id); + } +} diff --git a/src/main/java/com/sorrowmist/useless/api/recipe/IRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/api/recipe/IRecipeAdapter.java new file mode 100644 index 00000000..35cb33a5 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/api/recipe/IRecipeAdapter.java @@ -0,0 +1,125 @@ +package com.sorrowmist.useless.api.recipe; + +import appeng.api.stacks.AEKey; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.Recipe; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Map; + +/** + * Public contract for adapting a source recipe type to the alloy furnace. + * + *

The manager supplies merged inputs to the matching methods. Adapters should keep the + * ingredient and component semantics represented by those maps instead of reducing them to a + * representative stack.

+ * + * @param source recipe type + */ +public interface IRecipeAdapter> { + String DEFAULT_SOURCE_ID = "compatibility"; + + /** Stable source identifier used when an adapter is registered without an explicit source. */ + default String sourceId() { + return DEFAULT_SOURCE_ID; + } + + /** Returns the source recipe class enumerated from the level's RecipeManager. */ + Class getRecipeClass(); + + /** Returns recipes synthesized from runtime data instead of RecipeManager entries. */ + default List> getGeneratedRecipes(Level level) { + return List.of(); + } + + /** Converts one source recipe, or returns {@code null} when it is unsupported. */ + @Nullable + default AdvancedAlloyFurnaceRecipe convert(RecipeHolder holder, Level level) { + List recipes = convertAll(holder, level); + return recipes.isEmpty() ? null : recipes.get(0); + } + + /** Converts one source recipe into zero or more alloy-furnace recipes. */ + default List convertAll(RecipeHolder holder, Level level) { + AdvancedAlloyFurnaceRecipe recipe = convert(holder, level); + return recipe == null ? List.of() : List.of(recipe); + } + + /** + * Converts a source recipe using the concrete item stacks currently in the machine. + * Implementations must not mutate {@code actualInputs}. + */ + default List convertAll(RecipeHolder holder, Level level, + List actualInputs) { + return convertAll(holder, level); + } + + /** Returns the fixed mold used to pre-index this adapter, or {@code null} for a dynamic mold. */ + @Nullable + ItemStack getMoldItem(); + + /** Tests whether this adapter may handle the current mold. */ + default boolean matchesMold(@Nullable ItemStack mold) { + ItemStack fixedMold = getMoldItem(); + if (fixedMold == null || fixedMold.isEmpty()) { + return true; + } + return mold != null && !mold.isEmpty() && ItemStack.isSameItem(fixedMold, mold); + } + + /** Legacy four-argument lookup for adapters without AEKey inputs. */ + @Nullable + default RecipeHolder findMatchingRecipe(Level level, Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + return null; + } + + /** Returns all matches for the legacy four-argument lookup. */ + default List> findMatchingRecipes(Level level, Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + RecipeHolder holder = findMatchingRecipe(level, mergedInputs, mergedFluids, mold); + return holder == null ? List.of() : List.of(holder); + } + + /** Lookup including merged AEKey inputs. */ + @Nullable + default RecipeHolder findMatchingRecipe(Level level, Map mergedInputs, + Map mergedFluids, + Map mergedKeys, + @Nullable ItemStack mold) { + return findMatchingRecipe(level, mergedInputs, mergedFluids, mold); + } + + /** Returns all source recipes matching the merged item, fluid and AEKey inputs. */ + default List> findMatchingRecipes(Level level, Map mergedInputs, + Map mergedFluids, + Map mergedKeys, + @Nullable ItemStack mold) { + List> matches = findMatchingRecipes(level, mergedInputs, mergedFluids, mold); + if (!matches.isEmpty()) { + return matches; + } + RecipeHolder holder = findMatchingRecipe(level, mergedInputs, mergedFluids, mergedKeys, mold); + return holder == null ? List.of() : List.of(holder); + } + + /** + * Lookup using both merged inputs and the concrete item stacks. Dynamic adapters may override + * this when source recipe selection depends on components or another property of a stack. + */ + default List> findMatchingRecipes(Level level, Map mergedInputs, + Map mergedFluids, + Map mergedKeys, + @Nullable ItemStack mold, + List actualInputs) { + return findMatchingRecipes(level, mergedInputs, mergedFluids, mergedKeys, mold); + } +} diff --git a/src/main/java/com/sorrowmist/useless/blocks/CreatorDollBlock.java b/src/main/java/com/sorrowmist/useless/blocks/CreatorDollBlock.java deleted file mode 100644 index 2bcf7fa8..00000000 --- a/src/main/java/com/sorrowmist/useless/blocks/CreatorDollBlock.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.sorrowmist.useless.blocks; - -import com.sorrowmist.useless.UselessMod; -import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; -import net.minecraft.world.item.BlockItem; -import net.minecraft.world.item.Item; -import net.minecraft.world.level.BlockGetter; -import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.block.state.BlockBehaviour; -import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.phys.shapes.CollisionContext; -import net.minecraft.world.phys.shapes.BooleanOp; -import net.minecraft.world.phys.shapes.Shapes; -import net.minecraft.world.phys.shapes.VoxelShape; -import net.minecraftforge.registries.DeferredRegister; -import net.minecraftforge.registries.ForgeRegistries; -import net.minecraftforge.registries.RegistryObject; - -public class CreatorDollBlock extends Block { - - // 注册器 - public static final DeferredRegister BLOCKS = DeferredRegister.create(ForgeRegistries.BLOCKS, UselessMod.MOD_ID); - public static final DeferredRegister ITEMS = DeferredRegister.create(ForgeRegistries.ITEMS, UselessMod.MOD_ID); - - // 定义自定义碰撞箱,根据坐着的人形模型创建更精确的碰撞箱 - private static final VoxelShape COLLISION_SHAPE; - - static { - // 头部碰撞箱(4.35-11.65 x 6-13.3 x 7.35-14.65) - VoxelShape head = Shapes.box( - 4.35/16.0D, 6.0/16.0D, 7.35/16.0D, - 11.65/16.0D, 13.3/16.0D, 14.65/16.0D - ); - - // 身体碰撞箱(5.5-10.5 x 0-6 x 9.5-12.5) - VoxelShape body = Shapes.box( - 5.5/16.0D, 0.0/16.0D, 9.5/16.0D, - 10.5/16.0D, 6.0/16.0D, 12.5/16.0D - ); - - // 右臂碰撞箱(简化版,不考虑旋转) - VoxelShape rightArm = Shapes.box( - 10.65/16.0D, 0.34/16.0D, 10.0/16.0D, - 13.15/16.0D, 6.34/16.0D, 12.0/16.0D - ); - - // 左臂碰撞箱(简化版,不考虑旋转) - VoxelShape leftArm = Shapes.box( - 2.67/16.0D, 0.42/16.0D, 10.0/16.0D, - 6.17/16.0D, 6.42/16.0D, 12.0/16.0D - ); - - // 右腿碰撞箱(简化版,不考虑旋转) - VoxelShape rightLeg = Shapes.box( - 3.7/16.0D, 0.0/16.0D, 4.0/16.0D, - 8.0/16.0D, 2.0/16.0D, 10.0/16.0D - ); - - // 左腿碰撞箱(简化版,不考虑旋转) - VoxelShape leftLeg = Shapes.box( - 8.0/16.0D, 0.0/16.0D, 4.0/16.0D, - 12.3/16.0D, 2.0/16.0D, 10.0/16.0D - ); - - // 组合所有碰撞箱 - COLLISION_SHAPE = Shapes.or( - head, body, rightArm, leftArm, rightLeg, leftLeg - ); - } - - // 注册方块和物品 - public static final RegistryObject CREATOR_DOLL_BLOCK = BLOCKS.register("creator_doll_block", - () -> new CreatorDollBlock(BlockBehaviour.Properties.of() - .strength(0.5F, 0.5F) - .dynamicShape() - .noOcclusion())); - - public static final RegistryObject CREATOR_DOLL_BLOCK_ITEM = ITEMS.register("creator_doll_block", - () -> new BlockItem(CREATOR_DOLL_BLOCK.get(), new Item.Properties())); - - public CreatorDollBlock(Properties properties) { - super(properties); - } - - // 重写碰撞箱方法,使用自定义碰撞箱 - @Override - public VoxelShape getCollisionShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { - return COLLISION_SHAPE; - } - - // 重写选择箱方法,使用自定义碰撞箱 - @Override - public VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { - return COLLISION_SHAPE; - } - - // 重写光线遮挡方法,确保光线能正确穿透 - @Override - public VoxelShape getOcclusionShape(BlockState state, BlockGetter level, BlockPos pos) { - return COLLISION_SHAPE; - } -} diff --git a/src/main/java/com/sorrowmist/useless/blocks/GlowPlasticBlock.java b/src/main/java/com/sorrowmist/useless/blocks/GlowPlasticBlock.java deleted file mode 100644 index 0f01ac27..00000000 --- a/src/main/java/com/sorrowmist/useless/blocks/GlowPlasticBlock.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.sorrowmist.useless.blocks; -/* - * This file is part of Mekanism. - * - * Copyright (c) 2017-2025 Aidan C. Brady - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import com.sorrowmist.useless.UselessMod; -import com.sorrowmist.useless.api.EnumColor; -import net.minecraft.world.item.BlockItem; -import net.minecraft.world.item.Item; -import net.minecraft.world.level.block.Block; -import net.minecraftforge.eventbus.api.IEventBus; -import net.minecraftforge.registries.DeferredRegister; -import net.minecraftforge.registries.ForgeRegistries; -import net.minecraftforge.registries.RegistryObject; - -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.function.UnaryOperator; - -public class GlowPlasticBlock extends Block implements IColoredBlock { - - // 在方块类内部直接定义注册器 - public static final DeferredRegister BLOCKS = DeferredRegister.create(ForgeRegistries.BLOCKS, UselessMod.MOD_ID); - public static final DeferredRegister ITEMS = DeferredRegister.create(ForgeRegistries.ITEMS, UselessMod.MOD_ID); - - // 存储所有颜色的方块和物品 - public static final Map> GLOW_PLASTIC_BLOCKS = new LinkedHashMap<>(); - public static final Map> GLOW_PLASTIC_BLOCK_ITEMS = new LinkedHashMap<>(); - - private final EnumColor color; - - public GlowPlasticBlock(EnumColor color, UnaryOperator propertyModifier) { - super(applyLightLevelAdjustments(propertyModifier.apply(Properties.of() - .mapColor(color.getMapColor()) - .strength(5F, 6F) - .requiresCorrectToolForDrops()))); - this.color = color; - } - - @Override - public EnumColor getColor() { - return color; - } - - private static Properties applyLightLevelAdjustments(Properties properties) { - return properties.lightLevel(state -> 15); - } - - // 静态初始化块 - 注册所有颜色的方块和物品 - static { - UnaryOperator glowPlasticProperties = properties -> properties; - - // 为每种颜色注册方块和物品 - for (EnumColor color : EnumColor.valuesInOrder()) { - String registryName = color.getRegistryPrefix() + "_glow_plastic"; - - // 注册方块 - RegistryObject block = BLOCKS.register(registryName, - () -> new GlowPlasticBlock(color, glowPlasticProperties)); - GLOW_PLASTIC_BLOCKS.put(color, block); - - // 注册对应的物品 - RegistryObject item = ITEMS.register(registryName, - () -> new BlockItem(block.get(), new Item.Properties())); - GLOW_PLASTIC_BLOCK_ITEMS.put(color, item); - } - } - -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/blocks/IColoredBlock.java b/src/main/java/com/sorrowmist/useless/blocks/IColoredBlock.java deleted file mode 100644 index 76635e8b..00000000 --- a/src/main/java/com/sorrowmist/useless/blocks/IColoredBlock.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.sorrowmist.useless.blocks; - -/* - * This file is part of Mekanism. - * - * Copyright (c) 2017-2025 Aidan C. Brady - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import com.sorrowmist.useless.api.EnumColor; - -public interface IColoredBlock { - EnumColor getColor(); -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/blocks/ModBlockEntities.java b/src/main/java/com/sorrowmist/useless/blocks/ModBlockEntities.java deleted file mode 100644 index a3f8e108..00000000 --- a/src/main/java/com/sorrowmist/useless/blocks/ModBlockEntities.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.sorrowmist.useless.blocks; - -import com.sorrowmist.useless.UselessMod; -import com.sorrowmist.useless.blocks.advancedalloyfurnace.AdvancedAlloyFurnaceBlock; -import com.sorrowmist.useless.blocks.advancedalloyfurnace.AdvancedAlloyFurnaceBlockEntity; -import com.sorrowmist.useless.blocks.oregenerator.OreGeneratorBlock; -import com.sorrowmist.useless.blocks.oregenerator.OreGeneratorBlockEntity; -import net.minecraft.world.level.block.entity.BlockEntityType; -import net.minecraftforge.registries.DeferredRegister; -import net.minecraftforge.registries.ForgeRegistries; -import net.minecraftforge.registries.RegistryObject; - -public class ModBlockEntities { - public static final DeferredRegister> BLOCK_ENTITIES = - DeferredRegister.create(ForgeRegistries.BLOCK_ENTITY_TYPES, UselessMod.MOD_ID); - - public static final RegistryObject> ORE_GENERATOR = - BLOCK_ENTITIES.register("ore_generator", - () -> BlockEntityType.Builder.of(OreGeneratorBlockEntity::new, - OreGeneratorBlock.ORE_GENERATOR_BLOCK.get()).build(null)); - - // 在 ModBlockEntities.java 中添加注册 -// 在现有的 ModBlockEntities.java 文件中添加以下内容: - - public static final RegistryObject> ADVANCED_ALLOY_FURNACE = - BLOCK_ENTITIES.register("advanced_alloy_furnace", - () -> BlockEntityType.Builder.of(AdvancedAlloyFurnaceBlockEntity::new, - AdvancedAlloyFurnaceBlock.ADVANCED_ALLOY_FURNACE_BLOCK.get()).build(null)); -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/blocks/ModMenuTypes.java b/src/main/java/com/sorrowmist/useless/blocks/ModMenuTypes.java deleted file mode 100644 index 30e40ff0..00000000 --- a/src/main/java/com/sorrowmist/useless/blocks/ModMenuTypes.java +++ /dev/null @@ -1,50 +0,0 @@ -// ModMenuTypes.java -package com.sorrowmist.useless.blocks; - -import com.sorrowmist.useless.UselessMod; -import com.sorrowmist.useless.blocks.advancedalloyfurnace.AdvancedAlloyFurnaceMenu; -import net.minecraft.core.BlockPos; -import net.minecraft.world.inventory.MenuType; -import net.minecraft.world.level.block.entity.BlockEntity; -import net.minecraftforge.common.extensions.IForgeMenuType; -import net.minecraftforge.registries.DeferredRegister; -import net.minecraftforge.registries.ForgeRegistries; -import net.minecraftforge.registries.RegistryObject; - -public class ModMenuTypes { - public static final DeferredRegister> MENUS = - DeferredRegister.create(ForgeRegistries.MENU_TYPES, UselessMod.MOD_ID); - - public static final RegistryObject> ADVANCED_ALLOY_FURNACE_MENU = - MENUS.register("advanced_alloy_furnace_menu", - () -> IForgeMenuType.create((windowId, inv, data) -> { - // 添加更健壮的数据检查 - if (data == null) { - // 返回一个安全的默认菜单,而不是null - return new AdvancedAlloyFurnaceMenu(windowId, inv, null); - } - - try { - BlockPos pos = data.readBlockPos(); - if (pos == null) { - return new AdvancedAlloyFurnaceMenu(windowId, inv, null); - } - - // 检查世界和方块实体是否存在 - if (inv.player == null || inv.player.level() == null) { - return new AdvancedAlloyFurnaceMenu(windowId, inv, null); - } - - BlockEntity blockEntity = inv.player.level().getBlockEntity(pos); - if (blockEntity == null) { - // 仍然创建菜单,但方块实体为null - return new AdvancedAlloyFurnaceMenu(windowId, inv, null); - } - - return new AdvancedAlloyFurnaceMenu(windowId, inv, blockEntity); - } catch (Exception e) { - // 发生异常时返回安全的默认菜单 - return new AdvancedAlloyFurnaceMenu(windowId, inv, null); - } - })); -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/blocks/advancedalloyfurnace/AdvancedAlloyFurnaceBlock.java b/src/main/java/com/sorrowmist/useless/blocks/advancedalloyfurnace/AdvancedAlloyFurnaceBlock.java deleted file mode 100644 index 599770ba..00000000 --- a/src/main/java/com/sorrowmist/useless/blocks/advancedalloyfurnace/AdvancedAlloyFurnaceBlock.java +++ /dev/null @@ -1,124 +0,0 @@ -// AdvancedAlloyFurnaceBlock.java - 修改部分 -package com.sorrowmist.useless.blocks.advancedalloyfurnace; - -import com.sorrowmist.useless.UselessMod; -import com.sorrowmist.useless.blocks.advancedalloyfurnace.AdvancedAlloyFurnaceBlockItem; -import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.world.InteractionHand; -import net.minecraft.world.InteractionResult; -import net.minecraft.world.MenuProvider; -import net.minecraft.world.entity.item.ItemEntity; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.item.BlockItem; -import net.minecraft.world.item.Item; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.context.BlockPlaceContext; -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.BlockBehaviour; -import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.level.block.state.StateDefinition; -import net.minecraft.world.level.block.state.properties.BlockStateProperties; -import net.minecraft.world.level.block.state.properties.BooleanProperty; -import net.minecraft.world.level.block.state.properties.DirectionProperty; -import net.minecraft.world.phys.BlockHitResult; -import net.minecraftforge.registries.DeferredRegister; -import net.minecraftforge.registries.ForgeRegistries; -import net.minecraftforge.registries.RegistryObject; -import org.jetbrains.annotations.Nullable; - -public class AdvancedAlloyFurnaceBlock extends Block implements EntityBlock { - // 注册器 - public static final DeferredRegister BLOCKS = DeferredRegister.create(ForgeRegistries.BLOCKS, UselessMod.MOD_ID); - public static final DeferredRegister ITEMS = DeferredRegister.create(ForgeRegistries.ITEMS, UselessMod.MOD_ID); - - // 状态属性 - 添加ACTIVE属性 - public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING; - public static final BooleanProperty ACTIVE = BooleanProperty.create("active"); - - // 注册方块和物品 - public static final RegistryObject ADVANCED_ALLOY_FURNACE_BLOCK = BLOCKS.register("advanced_alloy_furnace_block", - () -> new AdvancedAlloyFurnaceBlock()); - - public static final RegistryObject ADVANCED_ALLOY_FURNACE_BLOCK_ITEM = ITEMS.register("advanced_alloy_furnace_block", - () -> new AdvancedAlloyFurnaceBlockItem(ADVANCED_ALLOY_FURNACE_BLOCK.get(), new Item.Properties())); - - // 方块构造函数 - public AdvancedAlloyFurnaceBlock() { - super(BlockBehaviour.Properties.of() - .strength(5.0F, 32768.0F) - .requiresCorrectToolForDrops() - .lightLevel(state -> state.getValue(ACTIVE) ? 15 : 7)); // 活动时更亮 - // 注册默认状态 - this.registerDefaultState(this.stateDefinition.any() - .setValue(FACING, Direction.NORTH) - .setValue(ACTIVE, false)); - } - - // 添加状态定义 - @Override - protected void createBlockStateDefinition(StateDefinition.Builder builder) { - builder.add(FACING, ACTIVE); - } - - // 放置时设置朝向 - @Override - public BlockState getStateForPlacement(BlockPlaceContext context) { - return this.defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite()); - } - - @Override - public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { - if (!level.isClientSide) { - BlockEntity blockEntity = level.getBlockEntity(pos); - if (blockEntity instanceof AdvancedAlloyFurnaceBlockEntity) { - // 使用 NetworkHooks 来确保数据正确传输 - if (player instanceof net.minecraft.server.level.ServerPlayer serverPlayer) { - net.minecraftforge.network.NetworkHooks.openScreen(serverPlayer, (MenuProvider) blockEntity, pos); - } - } - } - return InteractionResult.sidedSuccess(level.isClientSide); - } - - @Override - public void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean isMoving) { - if (!state.is(newState.getBlock())) { - BlockEntity blockEntity = level.getBlockEntity(pos); - if (blockEntity instanceof AdvancedAlloyFurnaceBlockEntity furnace) { - // 确保方块实体的数据已保存,以便在精准采集模式下能正确获取NBT数据 - furnace.setChanged(); - - // 对于有方块实体的方块,需要特殊处理以确保数据正确保存 - // 这里不直接创建物品实体,而是让掉落物处理系统来处理 - // BlockBreakUtils.getBlockDrops方法会正确处理战利品表 - // ForceBreakUtils.createSilkTouchDrop方法会保存方块实体的NBT数据 - } - } - // 调用父类方法,让方块正常破坏并触发掉落事件 - // 现在战利品表已修复,方块会有正常的掉落物 - super.onRemove(state, level, pos, newState, isMoving); - } - - @Nullable - @Override - public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { - return new AdvancedAlloyFurnaceBlockEntity(pos, state); - } - - @Nullable - @Override - public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType type) { - return level.isClientSide ? null : (lvl, pos, st, be) -> { - if (be instanceof AdvancedAlloyFurnaceBlockEntity furnace) { - furnace.tick(); - } - }; - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/blocks/advancedalloyfurnace/AdvancedAlloyFurnaceBlockEntity.java b/src/main/java/com/sorrowmist/useless/blocks/advancedalloyfurnace/AdvancedAlloyFurnaceBlockEntity.java deleted file mode 100644 index bca43dfb..00000000 --- a/src/main/java/com/sorrowmist/useless/blocks/advancedalloyfurnace/AdvancedAlloyFurnaceBlockEntity.java +++ /dev/null @@ -1,2277 +0,0 @@ -package com.sorrowmist.useless.blocks.advancedalloyfurnace; - -import com.sorrowmist.useless.UselessMod; -import com.sorrowmist.useless.blocks.ModBlockEntities; -import com.sorrowmist.useless.common.inventory.MultiSlotItemHandler; -import com.sorrowmist.useless.recipes.advancedalloyfurnace.AdvancedAlloyFurnaceRecipe; -import com.sorrowmist.useless.recipes.advancedalloyfurnace.AdvancedAlloyFurnaceRecipeManager; -import com.sorrowmist.useless.utils.MoldIdentifier; -import com.sorrowmist.useless.registry.CatalystManager; -import com.sorrowmist.useless.registry.ModIngots; -import com.sorrowmist.useless.registry.ModMolds; -import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.network.chat.Component; -import net.minecraft.network.protocol.Packet; -import net.minecraft.network.protocol.game.ClientGamePacketListener; -import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.world.Containers; -import net.minecraft.world.MenuProvider; -import net.minecraft.world.SimpleContainer; -import net.minecraft.world.entity.player.Inventory; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.inventory.AbstractContainerMenu; -import net.minecraft.world.inventory.ContainerData; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.crafting.Ingredient; -import net.minecraft.world.level.block.entity.BlockEntity; -import net.minecraft.world.level.block.state.BlockState; -import net.minecraftforge.common.capabilities.Capability; -import net.minecraftforge.common.capabilities.ForgeCapabilities; -import net.minecraftforge.common.util.LazyOptional; -import net.minecraftforge.energy.IEnergyStorage; -import net.minecraftforge.fluids.FluidStack; -import net.minecraftforge.fluids.capability.IFluidHandler; -import net.minecraftforge.fluids.capability.templates.FluidTank; -import net.minecraftforge.items.IItemHandler; -import net.minecraftforge.items.IItemHandlerModifiable; -import net.minecraftforge.registries.ForgeRegistries; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.ArrayList; -import java.util.List; - -// AE2 Integration -import appeng.api.config.Actionable; -import appeng.api.networking.*; -import appeng.api.networking.energy.IEnergySource; -import appeng.api.networking.security.IActionSource; -import appeng.api.networking.storage.IStorageService; -import appeng.api.stacks.*; -import appeng.api.storage.*; -import appeng.capabilities.Capabilities; - -public class AdvancedAlloyFurnaceBlockEntity extends BlockEntity implements MenuProvider, IGridNodeListener, appeng.api.networking.IInWorldGridNodeHost, appeng.api.networking.security.IActionHost { - - // 能量系统 - private final CustomEnergyStorage energyStorage = new CustomEnergyStorage(Integer.MAX_VALUE, Integer.MAX_VALUE, 0); - private final LazyOptional lazyEnergyHandler = LazyOptional.of(() -> energyStorage); - - // 状态管理 - 重构版本 - private volatile boolean isActive = false; // 是否活跃 - private volatile int process = 0; // 处理进度 - private int processMax = 200; // 最大处理进度 - private int processTick = 10; // 每tick消耗的能量 - private boolean wasActive = false; // 上一次是否活跃 - private int syncCounter = 0; // 同步计数器 - - // 并行数相关 - private int currentParallel = 1; // 当前并行数 - private int maxParallel = 1; // 最大并行数 - private boolean hasCatalyst = false; // 是否有催化剂 - private boolean requiresMold = false; // 是否需要模具 - - // 配方管理 - private AdvancedAlloyFurnaceRecipe currentRecipe; // 当前配方 - - // 输入缓存,用于检测输入变化 - private List lastInputItems = new ArrayList<>(); - private FluidStack lastInputFluid = FluidStack.EMPTY; - private ItemStack lastCatalyst = ItemStack.EMPTY; - private ItemStack lastMold = ItemStack.EMPTY; - - // 自动输出相关 - private int autoOutputCounter = 0; - private static final int AUTO_OUTPUT_INTERVAL = 20; // 每20tick尝试输出一次,主动检测输出槽 - - // 输出暂存相关 - private List pendingOutputs = new ArrayList<>(); - private boolean isOutputPending = false; - - // 新增槽位索引定义 - private static final int INPUT_SLOTS = 9; // 物品输入槽数量 - private static final int OUTPUT_SLOTS = 9; // 物品输出槽数量 - private static final int CATALYST_SLOT = INPUT_SLOTS + OUTPUT_SLOTS; // 无用锭催化槽 - private static final int MOLD_SLOT = CATALYST_SLOT + 1; // 金属模具槽 - - // 总槽位数:9输入 + 9输出 + 1催化剂 + 1模具 = 20 - private static final int TOTAL_SLOTS = INPUT_SLOTS + OUTPUT_SLOTS + 2; - - // AE2 Integration - private final IManagedGridNode gridNode; - private final IActionSource actionSource; - private boolean isConnectedToAE = false; - - // 容器数据同步 - 重构版本 - private final ContainerData data = new ContainerData() { - @Override - public int get(int index) { - switch (index) { - case 0 -> { return energyStorage.getEnergyStored(); } - case 1 -> { return energyStorage.getMaxEnergyStored(); } - case 2 -> { return process; } - case 3 -> { return processMax; } - case 4 -> { return isActive ? 1 : 0; } - case 5 -> { return processTick; } - case 6 -> { return currentParallel; } - case 7 -> { return maxParallel; } - case 8 -> { return requiresMold ? 1 : 0; } - default -> { return 0; } - } - } - - @Override - public void set(int index, int value) { - // 只允许客户端修改能量,其他状态由服务端控制 - if (index == 0) { - energyStorage.setEnergy(value); - } - } - - @Override - public int getCount() { - return 9; - } - }; - - // 修复催化剂状态检测方法 - public boolean hasCatalyst() { - // 检查是否有配方需要催化剂 - if (currentRecipe == null || !currentRecipe.requiresCatalyst()) { - return false; - } - - // 检查催化剂槽位是否有匹配的催化剂 - ItemStack catalyst = itemHandler.getStackInSlot(CATALYST_SLOT); - if (catalyst.isEmpty()) { - return false; - } - - // 检查催化剂是否匹配配方要求 - return currentRecipe.getCatalyst().test(catalyst) && catalyst.getCount() >= currentRecipe.getCatalystCount(); - } - - - // 修复:使用自定义的高堆叠物品处理器,不再使用匿名内部类 - private final MultiSlotItemHandler itemHandler; - private final LazyOptional lazyItemHandler; - - // 流体槽配置 - private static final int FLUID_INPUT_COUNT = 6; - private static final int FLUID_OUTPUT_COUNT = 6; - private final List inputFluidTanks = new ArrayList<>(); - private final List outputFluidTanks = new ArrayList<>(); - - // 修复流体处理器 - private final LazyOptional lazyFluidHandler; - - public AdvancedAlloyFurnaceBlockEntity(BlockPos pos, BlockState state) { - super(ModBlockEntities.ADVANCED_ALLOY_FURNACE.get(), pos, state); - - // 初始化物品处理器,槽位数改为14 - this.itemHandler = new MultiSlotItemHandler(TOTAL_SLOTS) { - @Override - public boolean isItemValid(int slot, @Nonnull ItemStack stack) { - // 检查是否为无用锭或模具 - boolean isUselessIngot = MoldIdentifier.isUselessIngot(stack); - boolean isMetalMold = MoldIdentifier.isMetalMold(stack); - - if (slot < INPUT_SLOTS) { - // 输入槽(0-8)可以接受无用锭,但不能接受模具 - return !isMetalMold; - // 输入槽可以接受其他有效输入物品 - } else if (slot >= INPUT_SLOTS && slot < INPUT_SLOTS + OUTPUT_SLOTS) { - // 输出槽(9-17)不能手动放置物品 - return false; - } else if (slot == CATALYST_SLOT) { - // 催化剂槽只能接受无用锭 - return MoldIdentifier.isUselessIngot(stack); - } else if (slot == MOLD_SLOT) { - // 模具槽可以接受任何物品 - return true; - } - return false; - } - - @Nonnull - @Override - public ItemStack insertItem(int slot, @Nonnull ItemStack stack, boolean simulate) { - if (!simulate) { - // 记录插入前的状态 - ItemStack oldStack = getStackInSlot(slot); - ItemStack result = super.insertItem(slot, stack, simulate); - // 检查是否有变化 - if (!ItemStack.matches(oldStack, getStackInSlot(slot))) { - onContentsChanged(slot); - } - return result; - } - return super.insertItem(slot, stack, simulate); - } - - @Nonnull - @Override - public ItemStack extractItem(int slot, int amount, boolean simulate) { - if (!simulate) { - // 记录提取前的状态 - ItemStack oldStack = getStackInSlot(slot); - ItemStack result = super.extractItem(slot, amount, simulate); - // 检查是否有变化 - if (!ItemStack.matches(oldStack, getStackInSlot(slot))) { - onContentsChanged(slot); - } - return result; - } - return super.extractItem(slot, amount, simulate); - } - - @Override - public void setStackInSlot(int slot, @Nonnull ItemStack stack) { - // 记录设置前的状态 - ItemStack oldStack = getStackInSlot(slot); - super.setStackInSlot(slot, stack); - // 检查是否有变化 - if (!ItemStack.matches(oldStack, stack)) { - onContentsChanged(slot); - } - } - - // 物品变化回调 - protected void onContentsChanged(int slot) { - setChanged(); - markForClientUpdate(); - - // 检查是否为输入槽、催化剂槽或模具槽 - if (slot < INPUT_SLOTS || slot == CATALYST_SLOT || slot == MOLD_SLOT) { - // 输入物品、催化剂或模具变化时,触发检测 - triggerInputChangeDetection(); - } else if (slot >= INPUT_SLOTS && slot < INPUT_SLOTS + OUTPUT_SLOTS) { - // 输出槽变化且有物品时,触发自动输出 - ItemStack stack = getStackInSlot(slot); - if (!stack.isEmpty()) { - tryAutoOutput(); - } - } - } - }; - - // 初始化流体槽 - for (int i = 0; i < FLUID_INPUT_COUNT; i++) { - final int tankIndex = i; - inputFluidTanks.add(new FluidTank(Integer.MAX_VALUE) { - @Override - protected void onContentsChanged() { - setChanged(); - markForClientUpdate(); - // 输入流体变化时,触发检测 - triggerInputChangeDetection(); - } - - @Override - public boolean isFluidValid(FluidStack stack) { - // 允许任何流体被放入输入槽,不限制流体类型 - return true; - } - }); - } - - for (int i = 0; i < FLUID_OUTPUT_COUNT; i++) { - outputFluidTanks.add(new FluidTank(Integer.MAX_VALUE) { - @Override - protected void onContentsChanged() { - setChanged(); - markForClientUpdate(); - } - }); - } - - // 初始化流体处理器 - this.lazyFluidHandler = LazyOptional.of(() -> new IFluidHandler() { - @Override - public int getTanks() { - return FLUID_INPUT_COUNT + FLUID_OUTPUT_COUNT; - } - - @Nonnull - @Override - public FluidStack getFluidInTank(int tank) { - if (tank < FLUID_INPUT_COUNT) { - return inputFluidTanks.get(tank).getFluid(); - } else { - return outputFluidTanks.get(tank - FLUID_INPUT_COUNT).getFluid(); - } - } - - @Override - public int getTankCapacity(int tank) { - if (tank < FLUID_INPUT_COUNT) { - return inputFluidTanks.get(tank).getCapacity(); - } else { - return outputFluidTanks.get(tank - FLUID_INPUT_COUNT).getCapacity(); - } - } - - @Override - public boolean isFluidValid(int tank, @Nonnull FluidStack stack) { - if (tank < FLUID_INPUT_COUNT) { - return inputFluidTanks.get(tank).isFluidValid(stack); - } - return false; // 输出槽不接受输入 - } - - @Override - public int fill(FluidStack resource, FluidAction action) { - if (resource.isEmpty()) { - return 0; - } - - int totalFilled = 0; - for (int i = 0; i < FLUID_INPUT_COUNT; i++) { - FluidTank tank = inputFluidTanks.get(i); - if (tank.isFluidValid(resource)) { - int filled = tank.fill(resource, action); - totalFilled += filled; - if (filled > 0) { - // 只填充到第一个可用槽 - break; - } - } - } - return totalFilled; - } - - @Nonnull - @Override - public FluidStack drain(FluidStack resource, FluidAction action) { - if (resource.isEmpty()) { - return FluidStack.EMPTY; - } - - for (int i = 0; i < FLUID_OUTPUT_COUNT; i++) { - FluidTank tank = outputFluidTanks.get(i); - FluidStack drained = tank.drain(resource, action); - if (!drained.isEmpty()) { - return drained; - } - } - return FluidStack.EMPTY; - } - - @Nonnull - @Override - public FluidStack drain(int maxDrain, FluidAction action) { - for (int i = 0; i < FLUID_OUTPUT_COUNT; i++) { - FluidTank tank = outputFluidTanks.get(i); - FluidStack drained = tank.drain(maxDrain, action); - if (!drained.isEmpty()) { - return drained; - } - } - return FluidStack.EMPTY; - } - }); - - // 设置所有槽位支持高堆叠 - this.itemHandler.setAllSlotCapacity(Integer.MAX_VALUE); - this.lazyItemHandler = LazyOptional.of(() -> itemHandler); - - // AE2 Integration - // 创建动作源 - this.actionSource = appeng.api.networking.security.IActionSource.ofMachine(this); - - // 创建网格节点 - this.gridNode = GridHelper.createManagedNode(this, this) - .setInWorldNode(true) - .setTagName("node"); // 设置为in-world节点并添加标签 - } - - // IGridNodeListener implementation - @Override - public void onSaveChanges(AdvancedAlloyFurnaceBlockEntity nodeOwner, IGridNode node) { - // 当需要保存节点变化时的处理 - setChanged(); - } - - @Override - public void onGridChanged(AdvancedAlloyFurnaceBlockEntity nodeOwner, IGridNode node) { - // 当网格变化时,更新连接状态 - isConnectedToAE = node.isActive(); - setChanged(); - markForClientUpdate(); - } - - @Override - public void onStateChanged(AdvancedAlloyFurnaceBlockEntity nodeOwner, IGridNode node, State state) { - // 节点状态变化时的处理 - isConnectedToAE = node.isActive(); - setChanged(); - markForClientUpdate(); - } - - // 数据同步管理 - private boolean needsClientUpdate = false; - - // AE2 Integration - 生命周期管理 - @Override - public void clearRemoved() { - super.clearRemoved(); - // 当方块实体不再被移除时,创建网格节点 - GridHelper.onFirstTick(this, (be) -> { - be.gridNode.create(getLevel(), getBlockPos()); - }); - } - - @Override - public void onLoad() { - super.onLoad(); - // 节点创建由clearRemoved中的GridHelper.onFirstTick处理,确保只创建一次 - } - - @Override - public void onChunkUnloaded() { - super.onChunkUnloaded(); - // 区块卸载时销毁节点 - this.gridNode.destroy(); - } - - @Override - public void setRemoved() { - super.setRemoved(); - // 方块被移除时销毁节点 - this.gridNode.destroy(); - } - - // 重构的tick方法,简洁可靠的配方运行逻辑 - public void tick() { - if (level == null) return; - - if (!level.isClientSide) { - // 服务端逻辑 - - // 减少数据同步频率 - syncCounter++; - - // 处理暂存输出 - if (isOutputPending) { - processPendingOutputs(); - } - - // 主动检测输出槽并尝试自动输出 - 每20tick执行一次 - autoOutputCounter++; - if (autoOutputCounter >= AUTO_OUTPUT_INTERVAL) { - // 检查输出槽是否有物品或流体,如果有则尝试自动输出 - boolean hasOutputItems = false; - for (int slot = INPUT_SLOTS; slot < INPUT_SLOTS + OUTPUT_SLOTS; slot++) { - if (!itemHandler.getStackInSlot(slot).isEmpty()) { - hasOutputItems = true; - break; - } - } - boolean hasOutputFluid = false; - for (FluidTank tank : outputFluidTanks) { - if (!tank.isEmpty()) { - hasOutputFluid = true; - break; - } - } - - if (hasOutputItems || hasOutputFluid) { - tryAutoOutput(); - } - autoOutputCounter = 0; // 重置计数器 - } - - // 核心配方运行逻辑 - handleRecipeLogic(); - - // 数据同步(减少频率) - if (needsClientUpdate || syncCounter >= 20) { - syncToClient(); - needsClientUpdate = false; - syncCounter = 0; - } - } - } - - // 核心配方运行逻辑 - 重构版本 - private void handleRecipeLogic() { - // 如果没有活跃的配方,尝试查找并开始新配方 - if (!isActive || currentRecipe == null) { - startNewRecipe(); - return; - } - - // 活跃状态下的处理 - processActiveRecipe(); - } - - // 开始新配方 - private void startNewRecipe() { - // 查找匹配的配方 - AdvancedAlloyFurnaceRecipe foundRecipe = findMatchingRecipe(); - if (foundRecipe == null) { - return; - } - - // 验证输入是否满足要求 - if (!validateInputs(foundRecipe)) { - return; - } - - // 验证输出空间是否足够 - if (!validateOutputs(foundRecipe, currentParallel)) { - return; - } - - // 更新并行数 - updateParallelNumbersForRecipe(foundRecipe); - - // 设置当前配方并开始处理 - currentRecipe = foundRecipe; - processMax = foundRecipe.getProcessTime(); - process = processMax; - - // 计算能量消耗 - ItemStack catalyst = itemHandler.getStackInSlot(CATALYST_SLOT); - String catalystId = ForgeRegistries.ITEMS.getKey(catalyst.getItem()).toString(); - boolean isUsefulIngot = catalystId.equals("useless_mod:useful_ingot"); - int energyToUse = isUsefulIngot ? foundRecipe.getEnergy() : foundRecipe.getEnergy() * currentParallel; - processTick = Math.max(1, energyToUse / foundRecipe.getProcessTime()); - - // 激活机器 - isActive = true; - updateActiveState(false); - markForClientUpdate(); - } - - // 处理活跃的配方 - private void processActiveRecipe() { - // 检查当前配方是否仍然有效 - if (!validateInputs(currentRecipe)) { - stopRecipe(); - return; - } - - // 检查能量是否足够 - if (energyStorage.getEnergyStored() < processTick) { - return; - } - - // 消耗能量并更新进度 - energyStorage.modifyEnergy(-processTick); - process--; - - // 检查工艺是否完成 - if (process <= 0) { - completeRecipe(); - } else { - markForClientUpdate(); - } - } - - // 完成配方 - private void completeRecipe() { - // 确保当前配方不为null - if (currentRecipe == null) { - stopRecipe(); - return; - } - - // 处理配方输出 - resolveRecipe(currentRecipe); - - // 重置当前配方 - currentRecipe = null; - - // 尝试开始下一个配方 - startNewRecipe(); - - // 如果没有新配方,停止当前配方 - if (!isActive || currentRecipe == null) { - stopRecipe(); - } - } - - // 停止配方 - private void stopRecipe() { - // 重置状态 - isActive = false; - process = 0; - processMax = 200; - processTick = 10; - wasActive = true; - - // 通知客户端更新 - updateActiveState(true); - markForClientUpdate(); - } - - // 更新并行数 - private void updateParallelNumbersForRecipe(AdvancedAlloyFurnaceRecipe recipe) { - // 获取当前输入和催化剂状态 - List inputItems = new ArrayList<>(); - for (int i = 0; i < INPUT_SLOTS; i++) { - inputItems.add(itemHandler.getStackInSlot(i).copy()); - } - - ItemStack catalyst = itemHandler.getStackInSlot(CATALYST_SLOT).copy(); - ItemStack mold = itemHandler.getStackInSlot(MOLD_SLOT).copy(); - - // 找到与配方匹配的流体 - FluidStack matchingFluid = FluidStack.EMPTY; - List requiredFluids = recipe.getInputFluids(); - if (!requiredFluids.isEmpty()) { - for (FluidStack requiredFluid : requiredFluids) { - for (FluidTank tank : inputFluidTanks) { - FluidStack tankFluid = tank.getFluid(); - if (!tankFluid.isEmpty() && tankFluid.getFluid().isSame(requiredFluid.getFluid())) { - matchingFluid = tankFluid; - break; - } - } - if (!matchingFluid.isEmpty()) { - break; - } - } - } - - // 更新并行数 - updateParallelNumbers(catalyst, recipe, inputItems, matchingFluid); - - // 更新需要模具的状态 - requiresMold = recipe.requiresMold(); - } - - private void markForClientUpdate() { - needsClientUpdate = true; - } - - // 触发输入变化检测 - private void triggerInputChangeDetection() { - // 获取输入物品和流体 - List inputItems = new ArrayList<>(); - for (int i = 0; i < INPUT_SLOTS; i++) { - inputItems.add(itemHandler.getStackInSlot(i)); - } - - // 获取所有输入流体槽中的流体,取第一个非空流体作为当前输入流体 - FluidStack inputFluid = FluidStack.EMPTY; - for (FluidTank tank : inputFluidTanks) { - FluidStack tankFluid = tank.getFluid(); - if (!tankFluid.isEmpty()) { - inputFluid = tankFluid; - break; - } - } - - ItemStack catalyst = itemHandler.getStackInSlot(CATALYST_SLOT); - ItemStack mold = itemHandler.getStackInSlot(MOLD_SLOT); - - // 查找匹配的配方 - AdvancedAlloyFurnaceRecipe foundRecipe = findMatchingRecipe(); - - // 如果当前配方无效,使用null作为recipe参数 - if (foundRecipe != null && !validateInputs(foundRecipe)) { - foundRecipe = null; - // 直接更新当前配方为null - currentRecipe = null; - } - - // 更新并行数 - updateParallelNumbers(catalyst, foundRecipe, inputItems, inputFluid); - - // 更新需要模具的状态 - requiresMold = foundRecipe != null && foundRecipe.requiresMold(); - - // 标记需要同步 - markForClientUpdate(); - - // 更新输入缓存 - lastInputItems = new ArrayList<>(inputItems); - lastInputFluid = inputFluid.copy(); - lastCatalyst = catalyst.copy(); - lastMold = mold.copy(); - } - - // 检查输入是否发生变化 - private boolean isInputChanged(List inputItems, FluidStack inputFluid, ItemStack catalyst, ItemStack mold) { - // 快速检查:如果输入数量变化,直接返回true - if (inputItems.size() != lastInputItems.size()) { - return true; - } - - // 检查输入物品是否变化,只比较非空物品 - boolean hasItemChange = false; - for (int i = 0; i < inputItems.size(); i++) { - ItemStack current = inputItems.get(i); - ItemStack last = lastInputItems.get(i); - - // 只有当物品非空时才比较,或者一个为空一个非空时 - if ((!current.isEmpty() || !last.isEmpty()) && - (!ItemStack.matches(current, last) || current.getCount() != last.getCount())) { - hasItemChange = true; - break; - } - } - - if (hasItemChange) { - return true; - } - - // 检查输入流体是否变化,只比较非空流体 - if ((!inputFluid.isEmpty() || !lastInputFluid.isEmpty()) && - (!inputFluid.isFluidEqual(lastInputFluid) || inputFluid.getAmount() != lastInputFluid.getAmount())) { - return true; - } - - // 检查催化剂是否变化,只比较非空催化剂 - if ((!catalyst.isEmpty() || !lastCatalyst.isEmpty()) && - (!ItemStack.matches(catalyst, lastCatalyst) || catalyst.getCount() != lastCatalyst.getCount())) { - return true; - } - - // 检查模具是否变化,只比较非空模具 - if ((!mold.isEmpty() || !lastMold.isEmpty()) && - (!ItemStack.matches(mold, lastMold) || mold.getCount() != lastMold.getCount())) { - return true; - } - - return false; - } - - // 自动输出功能 - private void tryAutoOutput() { - boolean outputtedAny = false; - - // 优先输出物品到AE网络 - outputtedAny |= tryOutputItemsToAE(); - - // 然后输出流体到AE网络 - outputtedAny |= tryOutputFluidsToAE(); - - // 然后输出物品到周围容器 - outputtedAny |= tryOutputItems(); - - // 然后输出流体到周围容器 - outputtedAny |= tryOutputFluids(); - - if (outputtedAny) { - setChanged(); - markForClientUpdate(); - } - } - - // AE2 Integration - 输出物品到AE网络 - private boolean tryOutputItemsToAE() { - boolean outputtedAny = false; - - if (!isConnectedToAE) { - return false; - } - - // 检查所有输出槽位(9-17) - for (int slot = INPUT_SLOTS; slot < INPUT_SLOTS + OUTPUT_SLOTS; slot++) { - ItemStack stackInSlot = itemHandler.getStackInSlot(slot); - if (stackInSlot.isEmpty()) continue; - - // 尝试输出到AE网络 - long inserted = tryOutputToAE(stackInSlot); - if (inserted > 0) { - // 更新槽位内容 - if (inserted >= stackInSlot.getCount()) { - itemHandler.setStackInSlot(slot, ItemStack.EMPTY); - } else { - ItemStack remainingStack = stackInSlot.copy(); - remainingStack.setCount((int) (stackInSlot.getCount() - inserted)); - itemHandler.setStackInSlot(slot, remainingStack); - } - outputtedAny = true; - } - } - - return outputtedAny; - } - - // AE2 Integration - 输出流体到AE网络 - private boolean tryOutputFluidsToAE() { - if (!isConnectedToAE) { - return false; - } - - boolean outputtedAny = false; - for (FluidTank tank : outputFluidTanks) { - FluidStack fluidInTank = tank.getFluid(); - if (fluidInTank.isEmpty()) continue; - - // 尝试输出到AE网络 - long inserted = tryOutputFluidToAE(fluidInTank); - if (inserted > 0) { - // 从输出槽抽取相应数量的流体 - tank.drain((int) inserted, IFluidHandler.FluidAction.EXECUTE); - outputtedAny = true; - } - } - - return outputtedAny; - } - - private boolean tryOutputItems() { - boolean outputtedAny = false; - - // 检查所有输出槽位(9-17) - for (int slot = INPUT_SLOTS; slot < INPUT_SLOTS + OUTPUT_SLOTS; slot++) { - ItemStack stackInSlot = itemHandler.getStackInSlot(slot); - if (stackInSlot.isEmpty()) continue; - - ItemStack remainingStack = stackInSlot.copy(); - boolean slotOutputted = false; - - // 恢复全向输出 - for (Direction direction : Direction.values()) { - if (remainingStack.isEmpty()) break; - - BlockPos neighborPos = worldPosition.relative(direction); - var neighborEntity = level.getBlockEntity(neighborPos); - - if (neighborEntity != null) { - // 检查目标方块是否在黑名单中(暂时只包含自身方块) - BlockState neighborState = level.getBlockState(neighborPos); - if (neighborState.getBlock() instanceof AdvancedAlloyFurnaceBlock) { - continue; // 跳过自身方块 - } - - // 只获取一次能力,避免重复调用 - var itemHandlerOpt = neighborEntity.getCapability(ForgeCapabilities.ITEM_HANDLER, direction.getOpposite()); - if (itemHandlerOpt.isPresent()) { - IItemHandler neighborHandler = itemHandlerOpt.resolve().get(); - - // 尝试插入到邻居容器 - for (int neighborSlot = 0; neighborSlot < neighborHandler.getSlots(); neighborSlot++) { - if (remainingStack.isEmpty()) break; - - // 模拟插入 - ItemStack simulatedRemaining = neighborHandler.insertItem(neighborSlot, remainingStack.copy(), true); - if (simulatedRemaining.getCount() < remainingStack.getCount()) { - // 实际插入 - remainingStack = neighborHandler.insertItem(neighborSlot, remainingStack, false); - slotOutputted = true; - } - } - } - } - } - - // 更新槽位内容 - if (slotOutputted) { - if (remainingStack.isEmpty()) { - itemHandler.setStackInSlot(slot, ItemStack.EMPTY); - } else { - itemHandler.setStackInSlot(slot, remainingStack); - } - outputtedAny = true; - } - } - - return outputtedAny; - } - - private boolean tryOutputFluids() { - boolean outputtedAny = false; - - // 恢复全向输出 - for (FluidTank fluidTank : outputFluidTanks) { - FluidStack fluidInTank = fluidTank.getFluid(); - if (fluidInTank.isEmpty()) continue; - - FluidStack remainingFluid = fluidInTank.copy(); - - for (Direction direction : Direction.values()) { - if (remainingFluid.isEmpty()) break; - - BlockPos neighborPos = worldPosition.relative(direction); - var neighborEntity = level.getBlockEntity(neighborPos); - - if (neighborEntity != null) { - // 检查目标方块是否在黑名单中(暂时只包含自身方块) - BlockState neighborState = level.getBlockState(neighborPos); - if (neighborState.getBlock() instanceof AdvancedAlloyFurnaceBlock) { - continue; // 跳过自身方块 - } - - // 只获取一次能力,避免重复调用 - var fluidHandlerOpt = neighborEntity.getCapability(ForgeCapabilities.FLUID_HANDLER, direction.getOpposite()); - if (fluidHandlerOpt.isPresent()) { - IFluidHandler neighborHandler = fluidHandlerOpt.resolve().get(); - - // 尝试填充到邻居容器 - int filled = neighborHandler.fill(remainingFluid.copy(), IFluidHandler.FluidAction.SIMULATE); - if (filled > 0) { - // 实际填充 - FluidStack fluidToDrain = new FluidStack(remainingFluid, filled); - int actuallyFilled = neighborHandler.fill(fluidToDrain, IFluidHandler.FluidAction.EXECUTE); - - if (actuallyFilled > 0) { - // 从输出槽抽取相应数量的流体 - fluidTank.drain(actuallyFilled, IFluidHandler.FluidAction.EXECUTE); - remainingFluid.shrink(actuallyFilled); - outputtedAny = true; - } - } - } - } - } - } - - return outputtedAny; - } - - // 简化canProcessStart方法,适应新的runRecipeLogic逻辑 - // 调试方法已移除,使用更详细的日志记录替代 - - - // 配方查找和验证 - // 修改配方查找方法,计算并行数 - // 修改配方查找方法,确保正确更新并行数 - private AdvancedAlloyFurnaceRecipe findMatchingRecipe() { - List inputItems = new ArrayList<>(); - for (int i = 0; i < INPUT_SLOTS; i++) { - inputItems.add(itemHandler.getStackInSlot(i)); - } - - // 获取所有非空的输入流体槽 - List inputFluidsList = new ArrayList<>(); - for (FluidTank tank : inputFluidTanks) { - FluidStack tankFluid = tank.getFluid(); - if (!tankFluid.isEmpty()) { - inputFluidsList.add(tankFluid); - } - } - - ItemStack catalyst = itemHandler.getStackInSlot(CATALYST_SLOT); - ItemStack mold = itemHandler.getStackInSlot(MOLD_SLOT); - - // 使用支持多种流体输入的配方查找方法 - AdvancedAlloyFurnaceRecipe recipe = AdvancedAlloyFurnaceRecipeManager.getInstance() - .getRecipeWithCatalystOrMold(level, inputItems, inputFluidsList, catalyst, mold); - - // 更新当前配方引用 - currentRecipe = recipe; - - return recipe; - } - // 修复并行数更新逻辑 - // 修复并行数更新逻辑 - // 修改并行数更新方法,确保正确更新催化剂状态 - // 在 AdvancedAlloyFurnaceBlockEntity.java 的 updateParallelNumbers 方法中修改并行数计算 - private void updateParallelNumbers(ItemStack catalyst, AdvancedAlloyFurnaceRecipe recipe, List inputItems, FluidStack inputFluid) { - // 检查配方是否允许催化剂 - boolean catalystAllowed = recipe != null && recipe.isCatalystAllowed(); - - // 计算最大并行数(基于催化剂和黑名单) - int newMaxParallel = catalystAllowed ? CatalystManager.getCatalystParallel(catalyst) : 1; - - // 如果有配方,计算实际并行数 - int newCurrentParallel; - if (recipe != null) { - newCurrentParallel = calculateActualParallel(recipe, inputItems, inputFluid, newMaxParallel); - } else { - newCurrentParallel = 1; - } - - // 更新催化剂状态(仅当配方允许催化剂时) - boolean newHasCatalyst; - if (recipe != null && recipe.requiresCatalyst() && catalystAllowed) { - newHasCatalyst = !catalyst.isEmpty() && recipe.getCatalyst().test(catalyst) && catalyst.getCount() >= recipe.getCatalystCount(); - } else { - newHasCatalyst = false; - } - - // 更新需要模具的状态 - boolean newRequiresMold = recipe != null && recipe.requiresMold(); - - // 仅当值变化时才更新并标记需要同步 - boolean changed = false; - if (newMaxParallel != maxParallel) { - maxParallel = newMaxParallel; - changed = true; - } - if (newCurrentParallel != currentParallel) { - currentParallel = newCurrentParallel; - changed = true; - } - if (newHasCatalyst != hasCatalyst) { - hasCatalyst = newHasCatalyst; - changed = true; - } - if (newRequiresMold != requiresMold) { - requiresMold = newRequiresMold; - changed = true; - } - - if (changed) { - markForClientUpdate(); - } - } - - - // 在 AdvancedAlloyFurnaceBlockEntity.java 中添加 - public AdvancedAlloyFurnaceRecipe getCurrentRecipe() { - return currentRecipe; - } - - // 计算实际并行数(优化版本) - private int calculateActualParallel(AdvancedAlloyFurnaceRecipe recipe, List inputItems, - FluidStack inputFluid, int maxParallel) { - if (maxParallel <= 1) { - return 1; - } - - // 计算输入物品支持的并行数 - int itemParallel = Integer.MAX_VALUE; - List inputIngredients = recipe.getInputItems(); - List inputCounts = recipe.getInputItemCounts(); - - // 检查配方是否有物品输入需求 - boolean hasItemInputs = !inputIngredients.isEmpty(); - - if (hasItemInputs) { - // 提前检查是否有输入物品 - boolean hasItems = false; - for (ItemStack stack : inputItems) { - if (!stack.isEmpty()) { - hasItems = true; - break; - } - } - - if (hasItems) { - for (int i = 0; i < inputIngredients.size(); i++) { - // 对于冲压机配方,忽略底部输入栏对应的物品(索引为1),只考虑顶部输入栏(索引为0) - if (recipe.isPressRecipe() && i == 1) { - continue; // 跳过底部输入栏物品 - } - - Ingredient ingredient = inputIngredients.get(i); - long requiredCount = inputCounts.get(i); - - if (requiredCount <= 0) { - continue; - } - - // 使用long类型存储可用数量,避免整数溢出 - long availableCount = 0; - for (ItemStack stack : inputItems) { - if (!stack.isEmpty() && ingredient.test(stack)) { - availableCount += stack.getCount(); - } - } - - // 计算可能的并行数,确保结果不会超过Integer.MAX_VALUE - long possibleParallelLong = availableCount / requiredCount; - int possibleParallel = (int) Math.min(possibleParallelLong, Integer.MAX_VALUE); - itemParallel = Math.min(itemParallel, possibleParallel); - - // 如果已经是0,提前返回 - if (itemParallel == 0) { - return 1; - } - } - } else { - itemParallel = 0; - } - } - - // 计算输入流体支持的并行数 - int fluidParallel = Integer.MAX_VALUE; - List requiredFluids = recipe.getInputFluids(); - boolean hasFluidInputs = !requiredFluids.isEmpty(); - - if (hasFluidInputs) { - // 检查所有输入流体槽,找到支持的最大并行数 - long totalFluidAvailable = 0; - boolean hasMatchingFluid = false; - - for (FluidStack requiredFluid : requiredFluids) { - for (FluidTank tank : inputFluidTanks) { - FluidStack tankFluid = tank.getFluid(); - if (!tankFluid.isEmpty() && tankFluid.getFluid().isSame(requiredFluid.getFluid())) { - totalFluidAvailable += tankFluid.getAmount(); - hasMatchingFluid = true; - } - } - } - - if (hasMatchingFluid) { - // 使用long类型计算流体并行数,避免整数溢出 - long fluidRequired = requiredFluids.get(0).getAmount(); - long fluidParallelLong = totalFluidAvailable / fluidRequired; - fluidParallel = (int) Math.min(fluidParallelLong, Integer.MAX_VALUE); - } else { - fluidParallel = 0; - } - } - - // 取最小并行数,但不超过最大并行数 - // 当配方没有物品输入时,只考虑fluidParallel和maxParallel - // 当配方没有流体输入时,只考虑itemParallel和maxParallel - int actualParallel; - if (hasItemInputs && hasFluidInputs) { - // 有物品和流体输入,取三者最小值 - actualParallel = Math.min(Math.min(itemParallel, fluidParallel), maxParallel); - } else if (hasItemInputs) { - // 只有物品输入,取itemParallel和maxParallel的最小值 - actualParallel = Math.min(itemParallel, maxParallel); - } else if (hasFluidInputs) { - // 只有流体输入,取fluidParallel和maxParallel的最小值 - actualParallel = Math.min(fluidParallel, maxParallel); - } else { - // 没有输入,返回1 - actualParallel = 1; - } - - return Math.max(1, actualParallel); - } - - // 用于验证输入的复用列表,避免频繁创建ArrayList - private final List validationInputItems = new ArrayList<>(INPUT_SLOTS); - - // AE2 Integration - 获取存储服务 - private MEStorage getStorageService() { - if (!isConnectedToAE) { - return null; - } - - IGridNode node = this.gridNode.getNode(); - if (node == null || !node.isActive()) { - return null; - } - - IGrid grid = node.getGrid(); - if (grid == null) { - return null; - } - - IStorageService storageService = grid.getService(IStorageService.class); - if (storageService == null) { - return null; - } - - return storageService.getInventory(); - } - - // AE2 Integration - 获取能量源 - private IEnergySource getEnergySource() { - if (!isConnectedToAE) { - return null; - } - - IGridNode node = this.gridNode.getNode(); - if (node == null || !node.isActive()) { - return null; - } - - IGrid grid = node.getGrid(); - if (grid == null) { - return null; - } - - return grid.getEnergyService(); - } - - private boolean validateInputs(AdvancedAlloyFurnaceRecipe recipe) { - // 每次验证都创建新的输入列表,使用ItemStack副本,避免被matches方法修改 - List inputCopies = new ArrayList<>(); - for (int i = 0; i < INPUT_SLOTS; i++) { - inputCopies.add(itemHandler.getStackInSlot(i).copy()); - } - - ItemStack catalyst = itemHandler.getStackInSlot(CATALYST_SLOT).copy(); - ItemStack mold = itemHandler.getStackInSlot(MOLD_SLOT).copy(); - - // 获取所有非空的输入流体槽 - List inputFluidsList = new ArrayList<>(); - for (FluidTank tank : inputFluidTanks) { - FluidStack tankFluid = tank.getFluid(); - if (!tankFluid.isEmpty()) { - inputFluidsList.add(tankFluid); - } - } - - // 检查配方是否需要流体输入 - List requiredFluids = recipe.getInputFluids(); - if (requiredFluids.isEmpty()) { - // 如果配方不需要流体,直接使用空流体列表进行匹配 - return recipe.matches(inputCopies, inputFluidsList, catalyst, mold); - } else { - // 如果配方需要流体,检查所有配方流体是否都能在输入流体槽中找到匹配 - return recipe.matches(inputCopies, inputFluidsList, catalyst, mold); - } - } - - private boolean validateOutputs(AdvancedAlloyFurnaceRecipe recipe, int parallel) { - // 直接检查自身输出空间,不再优先检查外部容器 - return hasInternalOutputSpace(recipe, parallel); - } - - // 检查外部容器是否有输出空间 - private boolean hasExternalOutputSpace(AdvancedAlloyFurnaceRecipe recipe, int parallel) { - // 检查输出物品在外部容器的空间 - for (ItemStack output : recipe.getOutputItems()) { - if (!output.isEmpty()) { - ItemStack testOutput = output.copy(); - testOutput.setCount(testOutput.getCount() * parallel); - if (!canOutputItemToExternal(testOutput)) { - return false; - } - } - } - - // 检查输出流体在外部容器的空间 - List outputFluids = recipe.getOutputFluids(); - if (!outputFluids.isEmpty()) { - for (FluidStack outputFluid : outputFluids) { - FluidStack testOutput = outputFluid.copy(); - testOutput.setAmount(testOutput.getAmount() * parallel); - if (!canOutputFluidToExternal(testOutput)) { - return false; - } - } - } - - return true; - } - - // 检查自身输出空间 - private boolean hasInternalOutputSpace(AdvancedAlloyFurnaceRecipe recipe, int parallel) { - // 检查输出物品空间 - 修改:不再检查堆叠限制,只检查是否有空槽位 - for (ItemStack output : recipe.getOutputItems()) { - if (!output.isEmpty()) { - ItemStack testOutput = output.copy(); - testOutput.setCount(testOutput.getCount() * parallel); - boolean hasSpace = false; - for (int i = INPUT_SLOTS; i < INPUT_SLOTS + OUTPUT_SLOTS; i++) { - ItemStack slot = itemHandler.getStackInSlot(i); - // 修改:允许堆叠到已有相同物品的槽位,或者使用空槽位 - if (slot.isEmpty() || (ItemStack.isSameItemSameTags(slot, testOutput) && - slot.getCount() + testOutput.getCount() <= Integer.MAX_VALUE)) { - hasSpace = true; - break; - } - } - if (!hasSpace) return false; - } - } - - // 检查输出流体空间 - List outputFluids = recipe.getOutputFluids(); - if (!outputFluids.isEmpty()) { - for (FluidStack outputFluid : outputFluids) { - FluidStack testOutput = outputFluid.copy(); - testOutput.setAmount(testOutput.getAmount() * parallel); - boolean hasSpace = false; - for (FluidTank tank : outputFluidTanks) { - if (tank.getSpace() >= testOutput.getAmount() || - (tank.getFluid().isFluidEqual(testOutput) && - tank.getFluid().getAmount() + testOutput.getAmount() <= Integer.MAX_VALUE)) { - hasSpace = true; - break; - } - } - if (!hasSpace) return false; - } - } - - return true; - } - - // 检查物品是否能输出到外部容器 - private boolean canOutputItemToExternal(ItemStack output) { - for (Direction direction : Direction.values()) { - BlockPos neighborPos = worldPosition.relative(direction); - var neighborEntity = level.getBlockEntity(neighborPos); - - if (neighborEntity != null) { - var itemHandlerCap = neighborEntity.getCapability(ForgeCapabilities.ITEM_HANDLER, direction.getOpposite()).resolve(); - if (itemHandlerCap.isPresent()) { - IItemHandler neighborHandler = itemHandlerCap.get(); - - for (int slot = 0; slot < neighborHandler.getSlots(); slot++) { - ItemStack simulatedRemaining = neighborHandler.insertItem(slot, output.copy(), true); - if (simulatedRemaining.getCount() < output.getCount()) { - return true; - } - } - } - } - } - return false; - } - - // 检查流体是否能输出到外部容器 - private boolean canOutputFluidToExternal(FluidStack output) { - for (Direction direction : Direction.values()) { - BlockPos neighborPos = worldPosition.relative(direction); - var neighborEntity = level.getBlockEntity(neighborPos); - - if (neighborEntity != null) { - var fluidHandlerCap = neighborEntity.getCapability(ForgeCapabilities.FLUID_HANDLER, direction.getOpposite()).resolve(); - if (fluidHandlerCap.isPresent()) { - IFluidHandler neighborHandler = fluidHandlerCap.get(); - - int filled = neighborHandler.fill(output.copy(), IFluidHandler.FluidAction.SIMULATE); - if (filled > 0) { - return true; - } - } - } - } - return false; - } - - // 修复配方处理方法 - private void resolveRecipe(AdvancedAlloyFurnaceRecipe recipe) { - if (recipe == null) { - return; - } - - // 保存当前并行数到局部变量,避免后续更新槽位时被修改 - int parallel = currentParallel; - - // 创建输入物品的副本用于消耗计算 - List inputItems = new ArrayList<>(); - for (int i = 0; i < INPUT_SLOTS; i++) { - inputItems.add(itemHandler.getStackInSlot(i).copy()); - } - - ItemStack catalystSlot = itemHandler.getStackInSlot(CATALYST_SLOT).copy(); - - // 获取所有非空的输入流体槽的副本用于消耗计算 - List inputFluidsList = new ArrayList<>(); - for (FluidTank tank : inputFluidTanks) { - FluidStack tankFluid = tank.getFluid(); - if (!tankFluid.isEmpty()) { - inputFluidsList.add(tankFluid.copy()); - } - } - - // 使用并行数消耗输入(支持多种流体) - recipe.consumeInputs(inputItems, inputFluidsList, catalystSlot, parallel); - - // 先输出配方结果,使用保存的并行数 - outputRecipeResults(recipe, parallel); - - // 然后更新实际的输入槽位 - for (int i = 0; i < INPUT_SLOTS; i++) { - itemHandler.setStackInSlot(i, inputItems.get(i)); - } - - // 更新催化剂槽位(如果被消耗了) - if (!catalystSlot.equals(itemHandler.getStackInSlot(CATALYST_SLOT))) { - itemHandler.setStackInSlot(CATALYST_SLOT, catalystSlot); - } - - // 注意:模具槽位的物品不会被消耗,保持不变 - - // 更新所有输入流体槽 - int fluidIndex = 0; - for (FluidTank tank : inputFluidTanks) { - FluidStack tankFluid = tank.getFluid(); - if (!tankFluid.isEmpty()) { - // 确保我们没有超出流体列表的范围 - if (fluidIndex < inputFluidsList.size()) { - // 更新流体槽的内容 - tank.setFluid(inputFluidsList.get(fluidIndex)); - fluidIndex++; - } - } - } - - // 强制立即同步到客户端 - setChanged(); - syncToClient(); - } - - // 直接输出到自身容器的配方结果处理方法 - private void outputRecipeResults(AdvancedAlloyFurnaceRecipe recipe, int parallel) { - // 对于有机灌注机配方,使用催化剂支持的最大并行数,而不是当前并行数 - int outputParallel = parallel; - if (recipe.isInsolatorRecipe()) { - // 获取催化剂 - ItemStack catalyst = itemHandler.getStackInSlot(CATALYST_SLOT); - // 获取催化剂支持的最大并行数 - int catalystMaxParallel = CatalystManager.getCatalystParallel(catalyst); - outputParallel = Math.max(1, catalystMaxParallel); - } - - // 直接输出物品到AE网络(如果连接),否则输出到自身容器 - for (ItemStack output : recipe.getOutputItems()) { - if (!output.isEmpty()) { - // 使用long类型计算总输出数量,避免整数溢出 - long totalOutputCount = (long) output.getCount() * outputParallel; - - // 如果总输出数量超过Integer.MAX_VALUE,分多次创建ItemStack - ItemStack baseOutputStack = output.copy(); - long remainingToProcess = totalOutputCount; - - while (remainingToProcess > 0) { - // 每次输出不超过Integer.MAX_VALUE - int batchSize = (int) Math.min(remainingToProcess, Integer.MAX_VALUE); - ItemStack outputStack = baseOutputStack.copy(); - outputStack.setCount(batchSize); - - // 尝试优先输出到AE网络 - long inserted = tryOutputToAE(outputStack); - long actualInserted = Math.min(inserted, remainingToProcess); - remainingToProcess -= actualInserted; - - // 如果AE网络没存下,输出到自身容器 - if (remainingToProcess > 0) { - // 计算本次需要输出到自身容器的数量 - int toOutputToContainer = (int) Math.min(remainingToProcess, Integer.MAX_VALUE); - ItemStack containerOutputStack = baseOutputStack.copy(); - containerOutputStack.setCount(toOutputToContainer); - - // 检查pendingOutputs列表中是否已经有相同的物品,如果有就不再添加,避免无限增长 - boolean alreadyExists = false; - for (ItemStack existingItem : pendingOutputs) { - if (ItemStack.isSameItemSameTags(existingItem, containerOutputStack)) { - // 如果已经存在,就增加现有物品的数量,而不是添加新物品 - long newCount = (long) existingItem.getCount() + toOutputToContainer; - if (newCount <= (long) Integer.MAX_VALUE) { - existingItem.setCount((int) newCount); - } else { - existingItem.setCount(Integer.MAX_VALUE); - } - alreadyExists = true; - break; - } - } - - if (!alreadyExists) { - addOutputItem(containerOutputStack); - } - - // 从剩余数量中减去本次输出到容器的数量 - remainingToProcess -= toOutputToContainer; - } - - // 输出物品到自身后,触发自动输出 - tryAutoOutput(); - - // 避免无限循环,每次循环至少减少1 - if (inserted == 0 && remainingToProcess > 0) { - // 如果AE网络没存下,且已经输出到容器,继续处理剩余数量 - continue; - } - - // 只有当没有剩余数量或AE网络插入失败时才退出循环 - if (remainingToProcess <= 0 || inserted == 0) { - break; - } - } - } - } - - // 直接输出流体到AE网络(如果连接),否则输出到自身容器 - List outputFluids = recipe.getOutputFluids(); - if (!outputFluids.isEmpty()) { - for (FluidStack outputFluid : outputFluids) { - // 使用long类型计算总输出数量,避免整数溢出 - long totalFluidAmount = (long) outputFluid.getAmount() * outputParallel; - - // 如果总流体数量超过Integer.MAX_VALUE,分多次填充 - FluidStack baseOutputStack = outputFluid.copy(); - long remainingAmount = totalFluidAmount; - - while (remainingAmount > 0) { - FluidStack outputStack = baseOutputStack.copy(); - // 每次输出不超过Integer.MAX_VALUE - int stackAmount = (int) Math.min(remainingAmount, Integer.MAX_VALUE); - outputStack.setAmount(stackAmount); - - // 尝试优先输出到AE网络 - long inserted = tryOutputFluidToAE(outputStack); - long actualInserted = Math.min(inserted, remainingAmount); - remainingAmount -= actualInserted; - - // 如果AE网络没存下,输出到自身流体槽(寻找第一个适合的槽位) - if (remainingAmount > 0) { - FluidStack remainingFluid = outputStack.copy(); - remainingFluid.setAmount((int) remainingAmount); - - boolean fluidAdded = false; - for (FluidTank tank : outputFluidTanks) { - if (tank.isEmpty() || tank.getFluid().isFluidEqual(remainingFluid)) { - int filled = tank.fill(remainingFluid, IFluidHandler.FluidAction.EXECUTE); - if (filled > 0) { - fluidAdded = true; - // 如果没有完全填充,继续寻找下一个槽位 - if (filled < remainingFluid.getAmount()) { - remainingFluid.shrink(filled); - } else { - break; - } - } - } - } - } - - // 输出流体到自身后,触发自动输出 - tryAutoOutput(); - - // 避免无限循环,每次循环至少减少1 - if (inserted == 0) { - break; - } - } - } - } - } - - // AE2 Integration - 尝试输出物品到AE网络 - private long tryOutputToAE(ItemStack stack) { - if (stack.isEmpty() || !isConnectedToAE || actionSource == null) { - return 0; - } - - MEStorage storage = getStorageService(); - - if (storage == null) { - return 0; - } - - // 将ItemStack转换为AEKey - AEItemKey key = AEItemKey.of(stack); - if (key == null) { - return 0; - } - - // 尝试插入到AE网络 - long amount = stack.getCount(); - long inserted = storage.insert(key, amount, Actionable.MODULATE, actionSource); - - return inserted; - } - - // AE2 Integration - 尝试输出流体到AE网络 - private long tryOutputFluidToAE(FluidStack stack) { - if (stack.isEmpty() || !isConnectedToAE || actionSource == null) { - return 0; - } - - MEStorage storage = getStorageService(); - - if (storage == null) { - return 0; - } - - // 将FluidStack转换为AEKey - AEFluidKey key = AEFluidKey.of(stack); - if (key == null) { - return 0; - } - - // 尝试插入到AE网络 - long amount = stack.getAmount(); - long inserted = storage.insert(key, amount, Actionable.MODULATE, actionSource); - - return inserted; - } - - // 尝试将物品输出到外部容器 - private ItemStack tryOutputItemToExternal(ItemStack output) { - ItemStack remaining = output.copy(); - - for (Direction direction : Direction.values()) { - if (remaining.isEmpty()) break; - - BlockPos neighborPos = worldPosition.relative(direction); - var neighborEntity = level.getBlockEntity(neighborPos); - - if (neighborEntity != null) { - var itemHandlerCap = neighborEntity.getCapability(ForgeCapabilities.ITEM_HANDLER, direction.getOpposite()).resolve(); - if (itemHandlerCap.isPresent()) { - IItemHandler neighborHandler = itemHandlerCap.get(); - - for (int slot = 0; slot < neighborHandler.getSlots(); slot++) { - if (remaining.isEmpty()) break; - - ItemStack simulatedRemaining = neighborHandler.insertItem(slot, remaining.copy(), true); - if (simulatedRemaining.getCount() < remaining.getCount()) { - ItemStack beforeInsert = remaining.copy(); - remaining = neighborHandler.insertItem(slot, remaining, false); - } - } - } - } - } - - return remaining; - } - - // 尝试将流体输出到外部容器 - private FluidStack tryOutputFluidToExternal(FluidStack output) { - FluidStack remaining = output.copy(); - - for (Direction direction : Direction.values()) { - if (remaining.isEmpty()) break; - - BlockPos neighborPos = worldPosition.relative(direction); - var neighborEntity = level.getBlockEntity(neighborPos); - - if (neighborEntity != null) { - var fluidHandlerCap = neighborEntity.getCapability(ForgeCapabilities.FLUID_HANDLER, direction.getOpposite()).resolve(); - if (fluidHandlerCap.isPresent()) { - IFluidHandler neighborHandler = fluidHandlerCap.get(); - - int filled = neighborHandler.fill(remaining.copy(), IFluidHandler.FluidAction.SIMULATE); - if (filled > 0) { - FluidStack fluidToDrain = new FluidStack(remaining, filled); - int actuallyFilled = neighborHandler.fill(fluidToDrain, IFluidHandler.FluidAction.EXECUTE); - - if (actuallyFilled > 0) { - remaining.shrink(actuallyFilled); - } - } - } - } - } - - return remaining; - } - - // 清空输入流体 - public void clearInputFluid() { - for (FluidTank tank : inputFluidTanks) { - tank.setFluid(FluidStack.EMPTY); - } - setChanged(); - syncToClient(); - } - - // 清空指定索引的输入流体槽 - public void clearInputFluid(int tankIndex) { - if (tankIndex >= 0 && tankIndex < inputFluidTanks.size()) { - inputFluidTanks.get(tankIndex).setFluid(FluidStack.EMPTY); - setChanged(); - syncToClient(); - } - } - - // 清空输出流体 - public void clearOutputFluid() { - for (FluidTank tank : outputFluidTanks) { - tank.setFluid(FluidStack.EMPTY); - } - setChanged(); - syncToClient(); - } - - // 清空指定索引的输出流体槽 - public void clearOutputFluid(int tankIndex) { - if (tankIndex >= 0 && tankIndex < outputFluidTanks.size()) { - outputFluidTanks.get(tankIndex).setFluid(FluidStack.EMPTY); - setChanged(); - syncToClient(); - } - } - - // 修改:改进的输出物品添加方法,将物品添加到暂存列表 - private void addOutputItem(ItemStack output) { - ItemStack outputStack = output.copy(); - pendingOutputs.add(outputStack); - isOutputPending = true; - } - - // 处理暂存物品的输出 - private void processPendingOutputs() { - if (pendingOutputs.isEmpty()) { - isOutputPending = false; - return; - } - - // 遍历暂存物品列表,尝试输出 - List remainingOutputs = new ArrayList<>(); - - for (ItemStack pendingItem : pendingOutputs) { - ItemStack outputStack = pendingItem.copy(); - boolean outputProcessed = false; - - // 首先尝试堆叠到已有相同物品的槽位,但避免整数溢出 - for (int i = INPUT_SLOTS; i < INPUT_SLOTS + OUTPUT_SLOTS; i++) { - ItemStack slotStack = itemHandler.getStackInSlot(i); - if (ItemStack.isSameItemSameTags(slotStack, outputStack)) { - // 检查当前数量加上要添加的数量是否会超过Integer.MAX_VALUE - if ((long) slotStack.getCount() + (long) outputStack.getCount() <= (long) Integer.MAX_VALUE) { - // 安全堆叠,不会导致溢出 - slotStack.grow(outputStack.getCount()); - itemHandler.setStackInSlot(i, slotStack); - outputProcessed = true; - break; - } - // 如果会导致溢出,就尝试下一个槽位 - } - } - - // 如果没有堆叠成功,尝试放入空槽位 - if (!outputProcessed) { - for (int i = INPUT_SLOTS; i < INPUT_SLOTS + OUTPUT_SLOTS; i++) { - ItemStack slotStack = itemHandler.getStackInSlot(i); - if (slotStack.isEmpty()) { - itemHandler.setStackInSlot(i, outputStack.copy()); - outputProcessed = true; - break; - } - } - } - - // 如果还是没有成功输出,将物品保留在暂存列表中 - if (!outputProcessed) { - remainingOutputs.add(outputStack); - } - } - - // 保存原始大小 - int originalSize = pendingOutputs.size(); - // 更新暂存列表 - pendingOutputs = remainingOutputs; - isOutputPending = !pendingOutputs.isEmpty(); - - // 如果有物品被输出,触发自动输出 - if (pendingOutputs.size() < originalSize) { - tryAutoOutput(); - } - } - - // 修复的流体交互方法 - public boolean interactWithFluid(Player player, ItemStack stack, boolean isInputTank, boolean isFill) { - return interactWithFluid(player, stack, isInputTank, isFill, 0); - } - - public boolean interactWithFluid(Player player, ItemStack stack, boolean isInputTank, boolean isFill, int tankIndex) { - if (level == null || level.isClientSide) return false; - - // 首先尝试处理普通流体容器 - boolean success = handleFluidContainerInteraction(stack, isInputTank, isFill, tankIndex); - - - - if (success) { - setChanged(); - syncToClient(); - // 更新玩家手持物品 - if (player instanceof ServerPlayer serverPlayer) { - serverPlayer.containerMenu.setCarried(stack); - } - } - - return success; - } - - /** - * 处理普通流体容器的交互 - */ - private boolean handleFluidContainerInteraction(ItemStack stack, boolean isInputTank, boolean isFill, int tankIndex) { - return stack.getCapability(ForgeCapabilities.FLUID_HANDLER_ITEM).map(itemHandler -> { - boolean success = false; - - if (isFill) { - // 向机器填充流体 - 只允许输入槽 - if (isInputTank) { - // 确保tankIndex在有效范围内 - if (tankIndex >= 0 && tankIndex < inputFluidTanks.size()) { - FluidTank targetTank = inputFluidTanks.get(tankIndex); - FluidStack drained = itemHandler.drain(Integer.MAX_VALUE, IFluidHandler.FluidAction.SIMULATE); - if (!drained.isEmpty()) { - int filled = targetTank.fill(drained, IFluidHandler.FluidAction.SIMULATE); - if (filled > 0) { - FluidStack actualDrained = itemHandler.drain(filled, IFluidHandler.FluidAction.EXECUTE); - targetTank.fill(actualDrained, IFluidHandler.FluidAction.EXECUTE); - success = true; - } - } - } - } - } else { - // 从机器抽取流体 - 只允许输出槽 - if (!isInputTank) { - // 确保tankIndex在有效范围内 - if (tankIndex >= 0 && tankIndex < outputFluidTanks.size()) { - FluidTank targetTank = outputFluidTanks.get(tankIndex); - FluidStack drained = targetTank.drain(Integer.MAX_VALUE, IFluidHandler.FluidAction.SIMULATE); - if (!drained.isEmpty()) { - int filled = itemHandler.fill(drained, IFluidHandler.FluidAction.SIMULATE); - if (filled > 0) { - FluidStack actualDrained = targetTank.drain(filled, IFluidHandler.FluidAction.EXECUTE); - itemHandler.fill(actualDrained, IFluidHandler.FluidAction.EXECUTE); - success = true; - } - } - } - } - } - - return success; - }).orElse(false); - } - - - - // 添加便捷方法供UI使用 - public boolean fillInputTank(Player player, ItemStack container) { - return interactWithFluid(player, container, true, true); - } - - public boolean drainOutputTank(Player player, ItemStack container) { - return interactWithFluid(player, container, false, false); - } - - // 状态管理 - private void updateActiveState(boolean prevActive) { - if (!isActive && prevActive) { - wasActive = true; - if (level != null) { - // 更新方块状态为不活动 - BlockState currentState = getBlockState(); - if (currentState.hasProperty(AdvancedAlloyFurnaceBlock.ACTIVE) && currentState.getValue(AdvancedAlloyFurnaceBlock.ACTIVE)) { - level.setBlockAndUpdate(worldPosition, currentState.setValue(AdvancedAlloyFurnaceBlock.ACTIVE, false)); - } - } - return; - } - - if (prevActive != isActive || (wasActive && syncCounter % 40 == 0)) { - wasActive = false; - if (level != null) { - // 更新方块状态 - BlockState currentState = getBlockState(); - if (currentState.hasProperty(AdvancedAlloyFurnaceBlock.ACTIVE)) { - level.setBlockAndUpdate(worldPosition, currentState.setValue(AdvancedAlloyFurnaceBlock.ACTIVE, isActive)); - } - } - syncToClient(); - } - } - - private void syncToClient() { - if (level != null && !level.isClientSide) { - setChanged(); - level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3); - } - } - - // Getters - 用于UI显示 - public int getEnergyStored() { - return energyStorage.getEnergyStored(); - } - - public int getMaxEnergyStored() { - return energyStorage.getMaxEnergyStored(); - } - - public int getProgress() { - return process; - } - - public int getMaxProgress() { - return processMax; - } - - public boolean isActive() { - return isActive; - } - - public int getProcessTick() { - return processTick; - } - - // 新增:获取并行数 - public int getCurrentParallel() { - return currentParallel; - } - - public int getMaxParallel() { - return maxParallel; - } - - // 暴露给菜单的方法:检查是否为无用锭 - public boolean isUselessIngot(ItemStack stack) { - return MoldIdentifier.isUselessIngot(stack); - } - - // 暴露给菜单的方法:检查是否为金属模具 - public boolean isMetalMold(ItemStack stack) { - return MoldIdentifier.isMetalMold(stack); - } - - // 暴露给菜单的方法:检查是否为可接受的标志物 - public boolean isAcceptableMarker(ItemStack stack) { - return MoldIdentifier.isAcceptableMarker(stack); - } - - // 公共访问方法 - public MultiSlotItemHandler getItemHandler() { - return itemHandler; - } - - public ItemStack getItemInSlot(int slot) { - if (slot >= 0 && slot < itemHandler.getSlots()) { - return itemHandler.getStackInSlot(slot); - } - return ItemStack.EMPTY; - } - - // 兼容旧方法:返回第一个输入流体槽 - public FluidTank getInputFluidTank() { - return inputFluidTanks.isEmpty() ? new FluidTank(0) : inputFluidTanks.get(0); - } - - // 兼容旧方法:返回第一个输出流体槽 - public FluidTank getOutputFluidTank() { - return outputFluidTanks.isEmpty() ? new FluidTank(0) : outputFluidTanks.get(0); - } - - // 获取输入流体槽列表 - public List getInputFluidTanks() { - return inputFluidTanks; - } - - // 获取输出流体槽列表 - public List getOutputFluidTanks() { - return outputFluidTanks; - } - - // 获取指定索引的输入流体槽 - public FluidTank getInputFluidTank(int index) { - if (index >= 0 && index < inputFluidTanks.size()) { - return inputFluidTanks.get(index); - } - return new FluidTank(0); // 返回空的流体槽作为默认值 - } - - // 获取指定索引的输出流体槽 - public FluidTank getOutputFluidTank(int index) { - if (index >= 0 && index < outputFluidTanks.size()) { - return outputFluidTanks.get(index); - } - return new FluidTank(0); // 返回空的流体槽作为默认值 - } - - public ContainerData getContainerData() { - return data; - } - - // 数据持久化 - @Override - public void load(CompoundTag tag) { - super.load(tag); - if (tag.contains("Inventory")) { - itemHandler.deserializeNBT(tag.getCompound("Inventory")); - } - energyStorage.deserializeNBT(tag.get("Energy")); - - // 加载输入流体槽 - for (int i = 0; i < inputFluidTanks.size(); i++) { - String key = "InputFluid" + i; - if (tag.contains(key)) { - inputFluidTanks.get(i).readFromNBT(tag.getCompound(key)); - } - } - - // 加载输出流体槽 - for (int i = 0; i < outputFluidTanks.size(); i++) { - String key = "OutputFluid" + i; - if (tag.contains(key)) { - outputFluidTanks.get(i).readFromNBT(tag.getCompound(key)); - } - } - - // 兼容旧格式 - if (tag.contains("InputFluid") && !inputFluidTanks.isEmpty()) { - inputFluidTanks.get(0).readFromNBT(tag.getCompound("InputFluid")); - } - if (tag.contains("OutputFluid") && !outputFluidTanks.isEmpty()) { - outputFluidTanks.get(0).readFromNBT(tag.getCompound("OutputFluid")); - } - - process = tag.getInt("Process"); - processMax = tag.getInt("ProcessMax"); - processTick = tag.getInt("ProcessTick"); - isActive = tag.getBoolean("IsActive"); - wasActive = tag.getBoolean("WasActive"); - autoOutputCounter = tag.getInt("AutoOutputCounter"); - // 新增:加载并行数 - currentParallel = tag.getInt("CurrentParallel"); - maxParallel = tag.getInt("MaxParallel"); - // AE2 Integration - 加载节点数据 - this.gridNode.loadFromNBT(tag); - } - - @Override - protected void saveAdditional(CompoundTag tag) { - super.saveAdditional(tag); - tag.put("Inventory", itemHandler.serializeNBT()); - tag.put("Energy", energyStorage.serializeNBT()); - - // 保存输入流体槽 - for (int i = 0; i < inputFluidTanks.size(); i++) { - String key = "InputFluid" + i; - tag.put(key, inputFluidTanks.get(i).writeToNBT(new CompoundTag())); - } - - // 保存输出流体槽 - for (int i = 0; i < outputFluidTanks.size(); i++) { - String key = "OutputFluid" + i; - tag.put(key, outputFluidTanks.get(i).writeToNBT(new CompoundTag())); - } - - tag.putInt("Process", process); - tag.putInt("ProcessMax", processMax); - tag.putInt("ProcessTick", processTick); - tag.putBoolean("IsActive", isActive); - tag.putBoolean("WasActive", wasActive); - tag.putInt("AutoOutputCounter", autoOutputCounter); - // 新增:保存并行数 - tag.putInt("CurrentParallel", currentParallel); - tag.putInt("MaxParallel", maxParallel); - // AE2 Integration - 保存节点数据 - this.gridNode.saveToNBT(tag); - } - - // Capabilities - 修复方向处理 - @Nonnull - @Override - public LazyOptional getCapability(@Nonnull Capability cap, @Nullable Direction side) { - // AE2 Integration - 暴露网络节点能力 - if (cap == Capabilities.IN_WORLD_GRID_NODE_HOST) { - return LazyOptional.of(() -> this).cast(); - } - if (cap == ForgeCapabilities.ENERGY) { - return lazyEnergyHandler.cast(); - } - if (cap == ForgeCapabilities.ITEM_HANDLER) { - // 根据方向返回不同的物品处理器 - return LazyOptional.of(() -> new DirectionalItemHandler(itemHandler, side)).cast(); - } - if (cap == ForgeCapabilities.FLUID_HANDLER) { - return lazyFluidHandler.cast(); - } - - - - return super.getCapability(cap, side); - } - - // IInWorldGridNodeHost implementation - @Nullable - @Override - public IGridNode getGridNode(Direction dir) { - return this.gridNode.getNode(); - } - - @Override - public appeng.api.util.AECableType getCableConnectionType(Direction dir) { - return appeng.api.util.AECableType.GLASS; - } - - // IActionHost implementation - @Nullable - @Override - public IGridNode getActionableNode() { - return this.gridNode.getNode(); - } - - // 方向感知的物品处理器包装类 - private class DirectionalItemHandler implements IItemHandlerModifiable { - private final MultiSlotItemHandler originalHandler; - private final Direction accessDirection; - - public DirectionalItemHandler(MultiSlotItemHandler originalHandler, Direction accessDirection) { - this.originalHandler = originalHandler; - this.accessDirection = accessDirection; - } - - @Override - public int getSlots() { - return originalHandler.getSlots(); - } - - @Nonnull - @Override - public ItemStack getStackInSlot(int slot) { - return originalHandler.getStackInSlot(slot); - } - - @Nonnull - @Override - public ItemStack insertItem(int slot, @Nonnull ItemStack stack, boolean simulate) { - // 基于方向和槽位类型限制插入 - if (accessDirection == Direction.DOWN) { - // 后方访问:只能插入催化剂槽(12) - if (slot == CATALYST_SLOT) { - return originalHandler.insertItem(slot, stack, simulate); - } - } else { - // 非后方访问:只能插入输入槽(0-8) - if (slot < INPUT_SLOTS) { - return originalHandler.insertItem(slot, stack, simulate); - } - } - // 不允许插入的情况,返回原物品 - return stack; - } - - @Nonnull - @Override - public ItemStack extractItem(int slot, int amount, boolean simulate) { - // 提取不受方向限制 - return originalHandler.extractItem(slot, amount, simulate); - } - - @Override - public int getSlotLimit(int slot) { - return originalHandler.getSlotLimit(slot); - } - - @Override - public boolean isItemValid(int slot, @Nonnull ItemStack stack) { - return originalHandler.isItemValid(slot, stack); - } - - @Override - public void setStackInSlot(int slot, @Nonnull ItemStack stack) { - // 转发设置操作到原始处理器 - originalHandler.setStackInSlot(slot, stack); - } - } - - @Override - public void invalidateCaps() { - super.invalidateCaps(); - lazyEnergyHandler.invalidate(); - lazyItemHandler.invalidate(); - lazyFluidHandler.invalidate(); - } - - // 自定义能量存储类 - private class CustomEnergyStorage implements IEnergyStorage { - private int energy; - private int capacity; - private int maxReceive; - private int maxExtract; - - public CustomEnergyStorage(int capacity, int maxReceive, int maxExtract) { - this.capacity = capacity; - this.maxReceive = maxReceive; - this.maxExtract = maxExtract; - } - - @Override - public int receiveEnergy(int maxReceive, boolean simulate) { - if (!canReceive()) return 0; - - int energyReceived = Math.min(capacity - energy, Math.min(this.maxReceive, maxReceive)); - if (!simulate) { - energy += energyReceived; - setChanged(); - markForClientUpdate(); - } - return energyReceived; - } - - @Override - public int extractEnergy(int maxExtract, boolean simulate) { - if (!canExtract()) return 0; - - int energyExtracted = Math.min(energy, Math.min(this.maxExtract, maxExtract)); - if (!simulate) { - energy -= energyExtracted; - setChanged(); - markForClientUpdate(); - } - return energyExtracted; - } - - @Override - public int getEnergyStored() { - return energy; - } - - @Override - public int getMaxEnergyStored() { - return capacity; - } - - @Override - public boolean canExtract() { - return maxExtract > 0; - } - - @Override - public boolean canReceive() { - return maxReceive > 0; - } - - public void modifyEnergy(int amount) { - energy = Math.max(0, Math.min(capacity, energy + amount)); - setChanged(); - markForClientUpdate(); - } - - public void setEnergy(int energy) { - this.energy = Math.max(0, Math.min(capacity, energy)); - } - - public void deserializeNBT(net.minecraft.nbt.Tag nbt) { - if (nbt instanceof CompoundTag tag) { - energy = tag.getInt("Energy"); - capacity = tag.getInt("Capacity"); - maxReceive = tag.getInt("MaxReceive"); - maxExtract = tag.getInt("MaxExtract"); - } - } - - public CompoundTag serializeNBT() { - CompoundTag tag = new CompoundTag(); - tag.putInt("Energy", energy); - tag.putInt("Capacity", capacity); - tag.putInt("MaxReceive", maxReceive); - tag.putInt("MaxExtract", maxExtract); - return tag; - } - } - - @Override - public Component getDisplayName() { - return Component.translatable("block.useless_mod.advanced_alloy_furnace_block"); - } - - @Nullable - @Override - public AbstractContainerMenu createMenu(int containerId, Inventory playerInventory, Player player) { - return new AdvancedAlloyFurnaceMenu(containerId, playerInventory, this, data); - } - - // 客户端数据同步 - @Override - public CompoundTag getUpdateTag() { - CompoundTag tag = super.getUpdateTag(); - saveAdditional(tag); - return tag; - } - - @Override - public void handleUpdateTag(CompoundTag tag) { - load(tag); - } - - @Nullable - @Override - public Packet getUpdatePacket() { - return ClientboundBlockEntityDataPacket.create(this); - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/blocks/advancedalloyfurnace/AdvancedAlloyFurnaceBlockItem.java b/src/main/java/com/sorrowmist/useless/blocks/advancedalloyfurnace/AdvancedAlloyFurnaceBlockItem.java deleted file mode 100644 index 189b7236..00000000 --- a/src/main/java/com/sorrowmist/useless/blocks/advancedalloyfurnace/AdvancedAlloyFurnaceBlockItem.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.sorrowmist.useless.blocks.advancedalloyfurnace; - -import net.minecraft.core.BlockPos; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.item.BlockItem; -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.entity.BlockEntity; -import net.minecraft.world.level.block.state.BlockState; -import org.jetbrains.annotations.Nullable; - -public class AdvancedAlloyFurnaceBlockItem extends BlockItem { - - public AdvancedAlloyFurnaceBlockItem(Block block, Properties properties) { - super(block, properties); - } - - @Override - protected boolean updateCustomBlockEntityTag(BlockPos pos, Level level, @Nullable Player player, ItemStack stack, BlockState state) { - // 从物品的NBT中恢复方块实体数据 - if (level.isClientSide) { - return false; - } - - BlockEntity blockEntity = level.getBlockEntity(pos); - if (blockEntity instanceof AdvancedAlloyFurnaceBlockEntity furnace) { - CompoundTag blockEntityTag = stack.getTagElement("BlockEntityTag"); - if (blockEntityTag != null) { - furnace.load(blockEntityTag); - furnace.setChanged(); - } - } - - return super.updateCustomBlockEntityTag(pos, level, player, stack, state); - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/blocks/advancedalloyfurnace/AdvancedAlloyFurnaceMenu.java b/src/main/java/com/sorrowmist/useless/blocks/advancedalloyfurnace/AdvancedAlloyFurnaceMenu.java deleted file mode 100644 index 0bb9e25b..00000000 --- a/src/main/java/com/sorrowmist/useless/blocks/advancedalloyfurnace/AdvancedAlloyFurnaceMenu.java +++ /dev/null @@ -1,508 +0,0 @@ -package com.sorrowmist.useless.blocks.advancedalloyfurnace; - -import com.sorrowmist.useless.UselessMod; -import com.sorrowmist.useless.blocks.ModMenuTypes; -import com.sorrowmist.useless.menu.slot.HighStackSlot; -import net.minecraft.world.entity.player.Inventory; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.inventory.AbstractContainerMenu; -import net.minecraft.world.inventory.ContainerData; -import net.minecraft.world.inventory.SimpleContainerData; -import net.minecraft.world.inventory.Slot; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.level.block.entity.BlockEntity; -import net.minecraftforge.common.capabilities.ForgeCapabilities; -import net.minecraftforge.items.SlotItemHandler; - -public class AdvancedAlloyFurnaceMenu extends AbstractContainerMenu { - private final AdvancedAlloyFurnaceBlockEntity blockEntity; - private final Player player; - private final ContainerData data; - - // 新的槽位布局常量 - 根据新贴图调整 - // 输入槽位 (9个,三行三列) - private static final int INPUT_SLOTS_X = 8; - private static final int INPUT_SLOTS_FIRST_Y = 18; - private static final int INPUT_SLOT_WIDTH = 16; - private static final int INPUT_SLOT_HEIGHT = 16; - private static final int INPUT_SLOT_SPACING = 2; // 槽位间隔2像素 - - // 输出槽位 (9个,三行三列) - private static final int OUTPUT_SLOTS_X = 116; - private static final int OUTPUT_SLOTS_FIRST_Y = 113; - private static final int OUTPUT_SLOT_WIDTH = 16; - private static final int OUTPUT_SLOT_HEIGHT = 16; - private static final int OUTPUT_SLOT_SPACING = 2; // 槽位间隔2像素 - - // 流体槽位 - 根据新贴图调整 - private static final int FLUID_INPUT_X = 10; - private static final int FLUID_OUTPUT_X = 154; - private static final int FLUID_Y = 143; - - // 新增:催化剂和模具槽位位置 - private static final int CATALYST_SLOT_X = 61; - private static final int CATALYST_SLOT_Y = 87; - private static final int MOLD_SLOT_X = 99; - private static final int MOLD_SLOT_Y = 87; - - // 指示灯位置 - private static final int INDICATOR_Y_OFFSET = -8; // 槽位上方2像素 - private static final int INDICATOR_SIZE = 4; - - // 清空按钮位置 - 需要重新定位 - private static final int CLEAR_FLUID_BUTTON_X = 30; - private static final int CLEAR_FLUID_BUTTON_Y = 145; - private static final int CLEAR_FLUID_BUTTON_WIDTH = 20; - private static final int CLEAR_FLUID_BUTTON_HEIGHT = 20; - - // 获取清空按钮位置信息(供Screen使用) - public int getClearButtonX() { - return CLEAR_FLUID_BUTTON_X; - } - - public int getClearButtonY() { - return CLEAR_FLUID_BUTTON_Y; - } - - public int getClearButtonWidth() { - return CLEAR_FLUID_BUTTON_WIDTH; - } - - public int getClearButtonHeight() { - return CLEAR_FLUID_BUTTON_HEIGHT; - } - - public AdvancedAlloyFurnaceMenu(int windowId, Inventory playerInventory, BlockEntity blockEntity, ContainerData data) { - super(ModMenuTypes.ADVANCED_ALLOY_FURNACE_MENU.get(), windowId); - this.player = playerInventory.player; - this.data = data; - - // 安全地处理方块实体 - if (blockEntity instanceof AdvancedAlloyFurnaceBlockEntity) { - this.blockEntity = (AdvancedAlloyFurnaceBlockEntity) blockEntity; - } else { - this.blockEntity = null; - - } - - // 只有在方块实体存在时才添加物品槽位 - if (this.blockEntity != null) { - // 在添加机器槽位时使用HighStackSlot - this.blockEntity.getCapability(ForgeCapabilities.ITEM_HANDLER).ifPresent(handler -> { - // 输入槽位 (0-8) - 使用HighStackSlot,三行三列布局 - for (int row = 0; row < 3; row++) { - for (int col = 0; col < 3; col++) { - int slotIndex = row * 3 + col; - int x = INPUT_SLOTS_X + col * (INPUT_SLOT_WIDTH + INPUT_SLOT_SPACING); - int y = INPUT_SLOTS_FIRST_Y + row * (INPUT_SLOT_HEIGHT + INPUT_SLOT_SPACING); - this.addSlot(new HighStackSlot(handler, slotIndex, x, y)); - } - } - - // 输出槽位 (9-17) - 使用HighStackSlot,三行三列布局 - for (int row = 0; row < 3; row++) { - for (int col = 0; col < 3; col++) { - int slotIndex = 9 + row * 3 + col; - int x = OUTPUT_SLOTS_X + col * (OUTPUT_SLOT_WIDTH + OUTPUT_SLOT_SPACING); - int y = OUTPUT_SLOTS_FIRST_Y + row * (OUTPUT_SLOT_HEIGHT + OUTPUT_SLOT_SPACING); - this.addSlot(new HighStackSlot(handler, slotIndex, x, y) { - @Override - public boolean mayPlace(ItemStack stack) { - return false; // 输出槽不能手动放置物品 - } - }); - } - } - - // 催化剂槽 (18) - 使用HighStackSlot - this.addSlot(new HighStackSlot(handler, 18, CATALYST_SLOT_X, CATALYST_SLOT_Y) { - @Override - public boolean mayPlace(ItemStack stack) { - if (blockEntity instanceof AdvancedAlloyFurnaceBlockEntity furnace) { - return furnace.isUselessIngot(stack); - } - return false; - } - }); - - // 模具槽 (19) - 使用HighStackSlot - this.addSlot(new HighStackSlot(handler, 19, MOLD_SLOT_X, MOLD_SLOT_Y) { - @Override - public boolean mayPlace(ItemStack stack) { - // 模具槽可以接受任何物品 - return true; - } - }); - }); - } - - addPlayerInventory(playerInventory); - addPlayerHotbar(playerInventory); - - // 添加数据槽 - addDataSlots(data); - } - - public AdvancedAlloyFurnaceMenu(int windowId, Inventory playerInventory, BlockEntity blockEntity) { - this(windowId, playerInventory, blockEntity, - blockEntity instanceof AdvancedAlloyFurnaceBlockEntity furnace ? - furnace.getContainerData() : new SimpleContainerData(9)); // 修改:从8改为9 - } - - private void addPlayerInventory(Inventory playerInventory) { - // 主物品栏 (27个) - 3行9列,根据新贴图调整位置:起点8,174,大小16*16,间隔2 - for (int i = 0; i < 3; ++i) { - for (int j = 0; j < 9; ++j) { - int x = 8 + j * (16 + 2); - int y = 174 + i * (16 + 2); - this.addSlot(new Slot(playerInventory, j + i * 9 + 9, x, y)); - } - } - } - - private void addPlayerHotbar(Inventory playerInventory) { - // 快捷栏 (9个) - 根据新贴图调整位置:起点8,232,大小16*16,间隔2 - for (int i = 0; i < 9; ++i) { - int x = 8 + i * (16 + 2); - int y = 232; - this.addSlot(new Slot(playerInventory, i, x, y)); - } - } - - @Override - public ItemStack quickMoveStack(Player playerIn, int index) { - if (blockEntity == null) { - return ItemStack.EMPTY; - } - - ItemStack itemstack = ItemStack.EMPTY; - Slot slot = this.slots.get(index); - - if (slot != null && slot.hasItem()) { - ItemStack stackInSlot = slot.getItem(); - itemstack = stackInSlot.copy(); - - // 定义槽位范围 - final int MACHINE_INPUT_START = 0; - final int MACHINE_INPUT_END = 8; - final int MACHINE_OUTPUT_START = 9; - final int MACHINE_OUTPUT_END = 17; - final int CATALYST_SLOT = 18; - final int MOLD_SLOT = 19; - final int PLAYER_INVENTORY_START = 20; - final int PLAYER_INVENTORY_END = 46; - final int HOTBAR_START = 47; - final int HOTBAR_END = 55; - - // 从机器输出槽转移到玩家物品栏 - if (index >= MACHINE_OUTPUT_START && index <= MACHINE_OUTPUT_END) { - // 输出槽可以转移到玩家物品栏的任何位置 - ItemStack originalStack = stackInSlot.copy(); - if (!this.moveItemStackTo(stackInSlot, PLAYER_INVENTORY_START, HOTBAR_END + 1, true)) { - return ItemStack.EMPTY; - } - // 修复:根据转移后的剩余数量更新实际槽位 - slot.set(stackInSlot); - slot.onQuickCraft(originalStack, itemstack); - } - // 从催化剂槽转移到玩家物品栏 - else if (index == CATALYST_SLOT) { - ItemStack originalStack = stackInSlot.copy(); - if (!this.moveItemStackTo(stackInSlot, PLAYER_INVENTORY_START, HOTBAR_END + 1, true)) { - return ItemStack.EMPTY; - } - // 修复:根据转移后的剩余数量更新实际槽位 - slot.set(stackInSlot); - slot.onQuickCraft(originalStack, itemstack); - } - // 从模具槽转移到玩家物品栏 - else if (index == MOLD_SLOT) { - ItemStack originalStack = stackInSlot.copy(); - if (!this.moveItemStackTo(stackInSlot, PLAYER_INVENTORY_START, HOTBAR_END + 1, true)) { - return ItemStack.EMPTY; - } - // 修复:根据转移后的剩余数量更新实际槽位 - slot.set(stackInSlot); - slot.onQuickCraft(originalStack, itemstack); - } - // 从机器输入槽转移到玩家物品栏 - else if (index >= MACHINE_INPUT_START && index <= MACHINE_INPUT_END) { - // 输入槽转移到玩家物品栏的任何位置 - ItemStack originalStack = stackInSlot.copy(); - if (!this.moveItemStackTo(stackInSlot, PLAYER_INVENTORY_START, HOTBAR_END + 1, true)) { - return ItemStack.EMPTY; - } - // 修复:根据转移后的剩余数量更新实际槽位 - slot.set(stackInSlot); - slot.onQuickCraft(originalStack, itemstack); - } - // 从玩家物品栏转移到机器 - else if (index >= PLAYER_INVENTORY_START && index <= HOTBAR_END) { - boolean isUselessIngot = blockEntity.isUselessIngot(stackInSlot); - - // 根据物品类型决定转移目标 - if (isUselessIngot) { - // 无用锭只能转移到催化剂槽 - if (!this.moveItemStackTo(stackInSlot, CATALYST_SLOT, CATALYST_SLOT + 1, false)) { - return ItemStack.EMPTY; - } - } else { - // 先尝试转移到输入槽 - if (!this.moveItemStackTo(stackInSlot, MACHINE_INPUT_START, MACHINE_INPUT_END + 1, false)) { - // 如果无法转移到输入槽,尝试转移到模具槽 - if (!this.moveItemStackTo(stackInSlot, MOLD_SLOT, MOLD_SLOT + 1, false)) { - // 如果仍无法转移,尝试转移到玩家物品栏的其他位置 - if (index >= PLAYER_INVENTORY_START && index <= PLAYER_INVENTORY_END) { - // 从主物品栏转移到快捷栏 - if (!this.moveItemStackTo(stackInSlot, HOTBAR_START, HOTBAR_END + 1, false)) { - return ItemStack.EMPTY; - } - } else if (index >= HOTBAR_START && index <= HOTBAR_END) { - // 从快捷栏转移到主物品栏 - if (!this.moveItemStackTo(stackInSlot, PLAYER_INVENTORY_START, PLAYER_INVENTORY_END + 1, false)) { - return ItemStack.EMPTY; - } - } - } - } - } - } - - if (stackInSlot.isEmpty()) { - slot.set(ItemStack.EMPTY); - } else { - slot.setChanged(); - } - - if (stackInSlot.getCount() == itemstack.getCount()) { - return ItemStack.EMPTY; - } - - slot.onTake(playerIn, stackInSlot); - } - - return itemstack; - } - - // 重写 moveItemStackTo 方法,修复转移方向并支持高堆叠 - @Override - protected boolean moveItemStackTo(ItemStack stack, int startIndex, int endIndex, boolean reverseDirection) { - boolean flag = false; - int i = reverseDirection ? endIndex - 1 : startIndex; - - // 堆叠到已有物品 - if (stack.isStackable()) { - while (!stack.isEmpty()) { - if (reverseDirection) { - if (i < startIndex) break; - } else { - if (i >= endIndex) break; - } - - Slot slot = this.slots.get(i); - ItemStack itemstack = slot.getItem(); - - if (!itemstack.isEmpty() && ItemStack.isSameItemSameTags(stack, itemstack) && slot.mayPlace(stack)) { - // 修复:使用槽位的实际最大堆叠大小,支持高堆叠 - int maxStackSize = slot.getMaxStackSize(stack); - int j = itemstack.getCount() + stack.getCount(); - - if (j <= maxStackSize) { - // 将所有物品堆叠到已有物品上 - itemstack.setCount(j); - stack.setCount(0); - slot.set(itemstack); // 修复:必须调用set方法来更新槽位 - slot.setChanged(); - flag = true; - } else if (itemstack.getCount() < maxStackSize) { - // 只堆叠部分物品到已有物品上 - int transferAmount = maxStackSize - itemstack.getCount(); - itemstack.setCount(maxStackSize); - stack.shrink(transferAmount); - slot.set(itemstack); // 修复:必须调用set方法来更新槽位 - slot.setChanged(); - flag = true; - } - } - - if (reverseDirection) { - --i; - } else { - ++i; - } - } - } - - // 放入空槽位 - if (!stack.isEmpty()) { - i = reverseDirection ? endIndex - 1 : startIndex; - - while (true) { - if (reverseDirection) { - if (i < startIndex) break; - } else { - if (i >= endIndex) break; - } - - Slot slot1 = this.slots.get(i); - ItemStack itemstack1 = slot1.getItem(); - - if (itemstack1.isEmpty() && slot1.mayPlace(stack)) { - // 修复:使用槽位的实际最大堆叠大小,支持高堆叠 - int maxStackSize = slot1.getMaxStackSize(stack); - if (stack.getCount() > maxStackSize) { - slot1.set(stack.split(maxStackSize)); - } else { - slot1.set(stack.split(stack.getCount())); - } - - slot1.setChanged(); - flag = true; - break; - } - - if (reverseDirection) { - --i; - } else { - ++i; - } - } - } - - return flag; - } - - @Override - public boolean stillValid(Player player) { - // 如果方块实体不存在,菜单无效 - if (this.blockEntity == null) { - return false; - } - - return player.distanceToSqr(this.blockEntity.getBlockPos().getX() + 0.5D, - this.blockEntity.getBlockPos().getY() + 0.5D, - this.blockEntity.getBlockPos().getZ() + 0.5D) <= 64.0D; - } - - // 获取流体槽位置信息(供Screen使用) - public int getFluidInputX() { - return FLUID_INPUT_X; - } - - public int getFluidOutputX() { - return FLUID_OUTPUT_X; - } - - public int getFluidY() { - return FLUID_Y; - } - - public AdvancedAlloyFurnaceBlockEntity getBlockEntity() { - return blockEntity; - } - - // 添加安全检查方法 - public boolean isValid() { - return blockEntity != null; - } - - // 数据访问方法 - public int getEnergy() { - return data.get(0); - } - - public int getMaxEnergy() { - return data.get(1); - } - - public int getProgress() { - return data.get(2); - } - - public int getMaxProgress() { - return data.get(3); - } - - public boolean isActive() { - return data.get(4) == 1; - } - - // 新增:获取当前配方的能量消耗 - public int getProcessTick() { - return data.get(5); - } - - // 修改:获取当前并行数 - public int getCurrentParallel() { - return data.get(6); - } - - // 修改:获取最大并行数 - public int getMaxParallel() { - return data.get(7); - } - - // 添加获取需要模具状态的方法 - public boolean requiresMold() { - return data.get(8) == 1; - } - - // 新增:获取催化剂和模具槽位位置信息 - public int getCatalystSlotX() { - return CATALYST_SLOT_X; - } - - public int getCatalystSlotY() { - return CATALYST_SLOT_Y; - } - - public int getMoldSlotX() { - return MOLD_SLOT_X; - } - - public int getMoldSlotY() { - return MOLD_SLOT_Y; - } - - public int getIndicatorYOffset() { - return INDICATOR_Y_OFFSET; - } - - public int getIndicatorSize() { - return INDICATOR_SIZE; - } - - // 修复:自定义槽位类,支持高堆叠 - public static class HighStackSlotItemHandler extends SlotItemHandler { - public HighStackSlotItemHandler(net.minecraftforge.items.IItemHandler itemHandler, int index, int xPosition, int yPosition) { - super(itemHandler, index, xPosition, yPosition); - } - - @Override - public int getMaxStackSize() { - return Integer.MAX_VALUE; - } - - @Override - public int getMaxStackSize(ItemStack stack) { - // 修复:返回实际的最大堆叠大小,而不是受限于物品的默认堆叠限制 - return Integer.MAX_VALUE; - } - - // 修复:重写 mayPlace 方法,确保高堆叠物品可以被放置 - @Override - public boolean mayPlace(ItemStack stack) { - if (stack.isEmpty()) { - return false; - } - - // 检查物品处理器是否允许放置 - if (!getItemHandler().isItemValid(getSlotIndex(), stack)) { - return false; - } - - // 允许高堆叠物品放置 - return true; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/blocks/advancedalloyfurnace/AdvancedAlloyFurnaceScreen.java b/src/main/java/com/sorrowmist/useless/blocks/advancedalloyfurnace/AdvancedAlloyFurnaceScreen.java deleted file mode 100644 index fb4dcc1c..00000000 --- a/src/main/java/com/sorrowmist/useless/blocks/advancedalloyfurnace/AdvancedAlloyFurnaceScreen.java +++ /dev/null @@ -1,1247 +0,0 @@ -package com.sorrowmist.useless.blocks.advancedalloyfurnace; - -import com.mojang.blaze3d.systems.RenderSystem; -import com.sorrowmist.useless.UselessMod; -import com.sorrowmist.useless.menu.slot.HighStackSlot; -import com.sorrowmist.useless.networking.ClearFluidPacket; -import com.sorrowmist.useless.networking.FluidInteractionPacket; -import com.sorrowmist.useless.recipes.advancedalloyfurnace.AdvancedAlloyFurnaceRecipe; -import com.sorrowmist.useless.registry.CatalystManager; -import com.sorrowmist.useless.utils.NumberFormatUtil; -import com.sorrowmist.useless.utils.SlotRendererUtil; -import net.minecraft.ChatFormatting; -import net.minecraft.client.gui.Font; -import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; -import net.minecraft.client.renderer.texture.TextureAtlasSprite; -import net.minecraft.network.chat.Component; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.entity.player.Inventory; -import net.minecraft.world.inventory.InventoryMenu; -import net.minecraft.world.inventory.Slot; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.TooltipFlag; -import net.minecraftforge.client.extensions.common.IClientFluidTypeExtensions; -import net.minecraftforge.fluids.FluidStack; - -import java.util.ArrayList; -import java.util.List; - -public class AdvancedAlloyFurnaceScreen extends AbstractContainerScreen { - private static final ResourceLocation TEXTURE = - ResourceLocation.fromNamespaceAndPath("useless_mod", "textures/gui/advanced_alloy_furnace_gui.png"); - - private static final ResourceLocation COMPONENTS_TEXTURE = - ResourceLocation.fromNamespaceAndPath("useless_mod", "textures/gui/advanced_alloy_furnace_zu_jian.png"); - - // 新增:提示图片纹理 - private static final ResourceLocation TIPS_TEXTURE = - ResourceLocation.fromNamespaceAndPath("useless_mod", "textures/gui/advanced_alloy_furnace_tips.png"); - - // GUI显示尺寸(新贴图尺寸) - private static final int DISPLAY_WIDTH = 176; - private static final int DISPLAY_HEIGHT = 256; - - // 贴图实际尺寸(与显示尺寸相同) - private static final int TEXTURE_WIDTH = 256; - private static final int TEXTURE_HEIGHT = 512; - - // 进度条位置和尺寸 - 改为两段 - private static final int PROGRESS_LEFT_X = 32; - private static final int PROGRESS_LEFT_Y = 72; - private static final int PROGRESS_LEFT_WIDTH = 24; - private static final int PROGRESS_LEFT_HEIGHT = 25; - - private static final int PROGRESS_RIGHT_X = 120; - private static final int PROGRESS_RIGHT_Y = 90; - private static final int PROGRESS_RIGHT_WIDTH = 27; - private static final int PROGRESS_RIGHT_HEIGHT = 21; - - // 能量条位置和尺寸 - 根据新贴图调整 - private static final int ENERGY_BAR_X = 58; - private static final int ENERGY_BAR_Y = 3; - private static final int ENERGY_BAR_WIDTH = 60; - private static final int ENERGY_BAR_HEIGHT = 6; - - // 能量条遮罩位置 - private static final int ENERGY_MASK_X = 57; - private static final int ENERGY_MASK_Y = 3; - private static final int ENERGY_MASK_WIDTH = 62; - private static final int ENERGY_MASK_HEIGHT = 7; - - // 流体槽位置和尺寸 - 根据新贴图调整 - private static final int FLUID_INPUT_X = 10; - private static final int FLUID_OUTPUT_X = 154; - private static final int FLUID_Y = 142; - private static final int FLUID_WIDTH = 16; - private static final int FLUID_HEIGHT = 32; - - // 流体输入区域 - private static final int FLUID_INPUT_AREA_X = 74; - private static final int FLUID_INPUT_AREA_Y = 20; - private static final int FLUID_INPUT_AREA_WIDTH = 86; - private static final int FLUID_INPUT_AREA_HEIGHT = 50; - - // 流体输出区域 - private static final int FLUID_OUTPUT_AREA_X = 8; - private static final int FLUID_OUTPUT_AREA_Y = 115; - private static final int FLUID_OUTPUT_AREA_WIDTH = 86; - private static final int FLUID_OUTPUT_AREA_HEIGHT = 50; - - // 流体槽通用属性 - private static final int FLUID_TANK_WIDTH = 55; - private static final int FLUID_TANK_HEIGHT = 17; - private static final int FLUID_TANK_SPACING = 2; - - // 滑块相关 - private static final int SLIDER_SLOT_X = 161; - private static final int SLIDER_SLOT_Y = 24; - private static final int SLIDER_SLOT_WIDTH = 3; - private static final int SLIDER_SLOT_HEIGHT = 42; // 修改为42 - private static final int SLIDER_WIDTH = 7; - private static final int SLIDER_HEIGHT = 15; - - // 输出区域滑块槽 - private static final int OUTPUT_SLIDER_SLOT_X = 95; - private static final int OUTPUT_SLIDER_SLOT_Y = 119; - private static final int OUTPUT_SLIDER_SLOT_WIDTH = 3; - private static final int OUTPUT_SLIDER_SLOT_HEIGHT = 42; - - // 催化剂和模具槽位位置 - private static final int CATALYST_SLOT_X = 61; - private static final int CATALYST_SLOT_Y = 87; - private static final int MOLD_SLOT_X = 99; - private static final int MOLD_SLOT_Y = 87; - - // 指示灯位置 - private static final int CATALYST_INDICATOR_X = 67; - private static final int CATALYST_INDICATOR_Y = 80; - private static final int MOLD_INDICATOR_X = 105; - private static final int MOLD_INDICATOR_Y = 80; - private static final int INDICATOR_WIDTH = 4; - private static final int INDICATOR_HEIGHT = 5; - - // 提示?区域 - 主图内置 - private static final int TIPS_AREA_X = 80; - private static final int TIPS_AREA_Y = 87; - private static final int TIPS_AREA_WIDTH = 16; - private static final int TIPS_AREA_HEIGHT = 16; - - // 标题位置 - 根据新要求添加 - private static final int TITLE_LABEL_X = 66; - private static final int TITLE_LABEL_Y = 52; - private static final int INVENTORY_LABEL_X = 10; - private static final int INVENTORY_LABEL_Y = 181; - - // 组件纹理坐标 - 根据新素材图调整 - private static final int ENERGY_BAR_U = 0; - private static final int ENERGY_BAR_V = 94; // 修改为正确的能量条本体位置 - private static final int ENERGY_MASK_U = 0; - private static final int ENERGY_MASK_V = 101; - - private static final int PROGRESS_LEFT_MASK_U = 0; - private static final int PROGRESS_LEFT_MASK_V = 109; - private static final int PROGRESS_RIGHT_MASK_U = 0; - private static final int PROGRESS_RIGHT_MASK_V = 135; - - private static final int FLUID_TANK_U = 0; - private static final int FLUID_TANK_V = 0; - private static final int FLUID_TANK_MASK_U = 0; - private static final int FLUID_TANK_MASK_V = 18; - - // 指示灯纹理坐标 - private static final int LIT_INDICATOR_U = 0; - private static final int LIT_INDICATOR_V = 88; - - // 催化剂和模具槽位纹理坐标 - private static final int CATALYST_SLOT_U = 0; - private static final int CATALYST_SLOT_V = 0; - private static final int MOLD_SLOT_U = 0; - private static final int MOLD_SLOT_V = 0; - - // 滑块纹理坐标 - private static final int SLIDER_DEFAULT_U = 0; - private static final int SLIDER_DEFAULT_V = 72; - private static final int SLIDER_PRESSED_U = 18; - private static final int SLIDER_PRESSED_V = 72; - - // 流体遮罩纹理坐标 - private static final int FLUID_MASK_OUTPUT_X = 152; - private static final int FLUID_MASK_OUTPUT_Y = 140; - private static final int FLUID_MASK_OUTPUT_U = 0; - private static final int FLUID_MASK_OUTPUT_V = 0; - private static final int FLUID_MASK_WIDTH = 19; - private static final int FLUID_MASK_HEIGHT = 36; - - // 提示区域变量 - private static final int CATALYST_MOLD_INFO_X = 80; - private static final int CATALYST_MOLD_INFO_Y = 87; - private static final int CATALYST_MOLD_INFO_WIDTH = 16; - private static final int CATALYST_MOLD_INFO_HEIGHT = 16; - - // 清空按钮位置 - 重新定位 - private static final int CLEAR_FLUID_BUTTON_X = 29; - private static final int CLEAR_FLUID_BUTTON_Y = 150; - private static final int CLEAR_FLUID_BUTTON_WIDTH = 17; - private static final int CLEAR_FLUID_BUTTON_HEIGHT = 17; - - // 清空按钮纹理(单槽清空按钮) - private static final int TANK_CLEAR_BUTTON_U = 0; - private static final int TANK_CLEAR_BUTTON_V = 36; - private static final int TANK_CLEAR_BUTTON_PRESSED_U = 18; - private static final int TANK_CLEAR_BUTTON_PRESSED_V = 36; - private static final int TANK_CLEAR_BUTTON_WIDTH = 17; - private static final int TANK_CLEAR_BUTTON_HEIGHT = 17; - - // 输入区域清空按钮状态 - 存储每个按钮的按下状态 - private final boolean[] inputTankClearButtonsPressed = new boolean[6]; - - // 输出区域清空按钮状态 - 存储每个按钮的按下状态 - private final boolean[] outputTankClearButtonsPressed = new boolean[6]; - - // 流体输入区域滚动功能 - private int inputFluidScrollOffset = 0; - private boolean isDraggingInputSlider = false; - private int draggedInputSliderY = 0; - - // 流体输出区域滚动功能 - private int outputFluidScrollOffset = 0; - private boolean isDraggingOutputSlider = false; - private int draggedOutputSliderY = 0; - - public AdvancedAlloyFurnaceScreen(AdvancedAlloyFurnaceMenu menu, Inventory playerInventory, Component title) { - super(menu, playerInventory, title); - this.imageWidth = DISPLAY_WIDTH; - this.imageHeight = DISPLAY_HEIGHT; - - // 设置标题位置 - this.titleLabelX = TITLE_LABEL_X; - this.titleLabelY = TITLE_LABEL_Y; - this.inventoryLabelX = INVENTORY_LABEL_X; - this.inventoryLabelY = INVENTORY_LABEL_Y; - } - - @Override - protected void renderBg(GuiGraphics guiGraphics, float partialTick, int mouseX, int mouseY) { - RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); - int x = (width - imageWidth) / 2; - int y = (height - imageHeight) / 2; - - // 渲染GUI主贴图 - guiGraphics.blit(TEXTURE, x, y, 0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT, TEXTURE_WIDTH, TEXTURE_HEIGHT); - - // 渲染能量条 - renderEnergyBar(guiGraphics, x, y); - - // 渲染进度条 - renderProgressBar(guiGraphics, x, y); - - // 渲染流体输入区域 - renderFluidInputArea(guiGraphics, x, y); - - // 渲染输出流体槽 - renderOutputFluidTanks(guiGraphics, x, y); - - // 渲染催化剂和模具槽位 - renderCatalystAndMoldSlots(guiGraphics, x, y); - - // 渲染指示灯 - renderIndicators(guiGraphics, x, y); - - // 移除旧UI的清空按钮,只使用新UI的每槽清空按钮 - - // 渲染滑块 - renderSlider(guiGraphics, x, y); - } - - // 渲染提示图片(已内置在主图中,不再需要额外渲染) - private void renderTipsImage(GuiGraphics guiGraphics, int x, int y) { - // 提示区域已内置在主图中,无需额外渲染 - } - - // 渲染能量条 - private void renderEnergyBar(GuiGraphics guiGraphics, int x, int y) { - if (menu == null) return; - - int energyStored = menu.getEnergy(); - int maxEnergy = menu.getMaxEnergy(); - - if (maxEnergy > 0) { - // 渲染能量条本体,从组件图抠取,起点0,24,大小60*6,覆盖主图58,3,60*6区域 - float energyRatio = (float) energyStored / maxEnergy; - int energyWidth = (int) (ENERGY_BAR_WIDTH * energyRatio); - - if (energyWidth > 0) { - guiGraphics.blit(COMPONENTS_TEXTURE, - x + ENERGY_BAR_X, y + ENERGY_BAR_Y, - ENERGY_BAR_U, ENERGY_BAR_V, - energyWidth, ENERGY_BAR_HEIGHT); - } - - // 渲染能量条遮罩,从组件图抠取,起点0,101,大小62*7,覆盖主图57,3,62*7位置 - guiGraphics.blit(COMPONENTS_TEXTURE, - x + ENERGY_MASK_X, y + ENERGY_MASK_Y, - ENERGY_MASK_U, ENERGY_MASK_V, - ENERGY_MASK_WIDTH, ENERGY_MASK_HEIGHT); - } - } - - // 渲染进度条 - private void renderProgressBar(GuiGraphics guiGraphics, int x, int y) { - if (menu == null) return; - - int progress = menu.getProgress(); - int maxProgress = menu.getMaxProgress(); - - if (maxProgress > 0 && progress > 0) { - float progressRatio = 1 - (float) progress / maxProgress; - - // 渲染左进度条 - int leftProgressHeight = (int) Math.ceil(PROGRESS_LEFT_HEIGHT * progressRatio); - guiGraphics.blit(COMPONENTS_TEXTURE, - x + PROGRESS_LEFT_X, y + PROGRESS_LEFT_Y, - PROGRESS_LEFT_MASK_U, PROGRESS_LEFT_MASK_V, - PROGRESS_LEFT_WIDTH, leftProgressHeight); - - // 渲染右进度条 - int rightProgressHeight = (int) Math.ceil(PROGRESS_RIGHT_HEIGHT * progressRatio); - guiGraphics.blit(COMPONENTS_TEXTURE, - x + PROGRESS_RIGHT_X, y + PROGRESS_RIGHT_Y, - PROGRESS_RIGHT_MASK_U, PROGRESS_RIGHT_MASK_V, - PROGRESS_RIGHT_WIDTH, rightProgressHeight); - } - } - - // 渲染流体槽(已被renderFluidInputArea和renderOutputFluidTanks替代) - private void renderFluidTanks(GuiGraphics guiGraphics, int x, int y) { - // 流体渲染逻辑已迁移到renderFluidInputArea和renderOutputFluidTanks方法 - // 保留此方法以保持兼容性 - } - - // 渲染流体输入区域,支持滚动 - private void renderFluidInputArea(GuiGraphics guiGraphics, int x, int y) { - if (menu == null || menu.getBlockEntity() == null) return; - - // 计算可见的流体槽数量 - int visibleTanks = FLUID_INPUT_AREA_HEIGHT / (FLUID_TANK_HEIGHT + FLUID_TANK_SPACING); - int totalTanks = 6; // 固定6个输入流体槽 - - // 计算流体槽和按钮的总宽度(流体槽宽度 + 1像素间隔 + 按钮宽度) - int totalWidth = FLUID_TANK_WIDTH + 1 + TANK_CLEAR_BUTTON_WIDTH; - - // 限制滚动偏移 - int maxOffset = Math.max(0, totalTanks - visibleTanks); - inputFluidScrollOffset = Math.max(0, Math.min(inputFluidScrollOffset, maxOffset)); - - // 渲染可见的流体槽 - if (visibleTanks > 0) { - // 计算每个槽位的垂直间距,使上下边距和槽位间隔相等 - int totalTankHeight = visibleTanks * FLUID_TANK_HEIGHT; - int totalSpacing = FLUID_INPUT_AREA_HEIGHT - totalTankHeight; - int spacing = visibleTanks > 1 ? totalSpacing / (visibleTanks + 1) : totalSpacing / 2; - - for (int i = inputFluidScrollOffset; i < Math.min(totalTanks, inputFluidScrollOffset + visibleTanks); i++) { - int tankIndex = i; - // 计算流体槽Y坐标,使上下边距和槽位间隔相等 - int tankY = y + FLUID_INPUT_AREA_Y + spacing + (i - inputFluidScrollOffset) * (FLUID_TANK_HEIGHT + spacing); - // 整体横向居中:(区域宽度 - 总宽度) / 2 - int startX = x + FLUID_INPUT_AREA_X + (FLUID_INPUT_AREA_WIDTH - totalWidth) / 2; - int tankX = startX; - int buttonX = startX + FLUID_TANK_WIDTH + 1; // 流体槽右边空1像素 - - // 渲染流体槽背景 - guiGraphics.blit(COMPONENTS_TEXTURE, - tankX, tankY, - FLUID_TANK_U, FLUID_TANK_V, - FLUID_TANK_WIDTH, FLUID_TANK_HEIGHT); - - // 渲染流体 - FluidStack fluid = menu.getBlockEntity().getInputFluidTank(tankIndex).getFluid(); - int capacity = menu.getBlockEntity().getInputFluidTank(tankIndex).getCapacity(); - renderFluidTank(guiGraphics, tankX, tankY, fluid, capacity); - - // 渲染流体槽遮罩 - guiGraphics.blit(COMPONENTS_TEXTURE, - tankX, tankY, - FLUID_TANK_MASK_U, FLUID_TANK_MASK_V, - FLUID_TANK_WIDTH, FLUID_TANK_HEIGHT); - - // 渲染清空流体按钮 - renderTankClearButton(guiGraphics, buttonX, tankY, tankIndex, true); - } - } - } - - // 渲染单个流体槽的清空按钮 - private void renderTankClearButton(GuiGraphics guiGraphics, int x, int y, int tankIndex, boolean isInput) { - // 根据按钮状态选择纹理,区分输入输出区域 - boolean isPressed = isInput ? inputTankClearButtonsPressed[tankIndex] : outputTankClearButtonsPressed[tankIndex]; - int u = isPressed ? TANK_CLEAR_BUTTON_PRESSED_U : TANK_CLEAR_BUTTON_U; - int v = isPressed ? TANK_CLEAR_BUTTON_PRESSED_V : TANK_CLEAR_BUTTON_V; - - // 渲染按钮 - guiGraphics.blit(COMPONENTS_TEXTURE, x, y, - u, v, - TANK_CLEAR_BUTTON_WIDTH, TANK_CLEAR_BUTTON_HEIGHT); - } - - // 渲染输出流体槽 - private void renderOutputFluidTanks(GuiGraphics guiGraphics, int x, int y) { - if (menu == null || menu.getBlockEntity() == null) return; - - // 计算可见的流体槽数量 - int visibleTanks = FLUID_OUTPUT_AREA_HEIGHT / (FLUID_TANK_HEIGHT + FLUID_TANK_SPACING); - int totalTanks = 6; // 固定6个输出流体槽 - - // 计算流体槽和按钮的总宽度(流体槽宽度 + 1像素间隔 + 按钮宽度) - int totalWidth = FLUID_TANK_WIDTH + 1 + TANK_CLEAR_BUTTON_WIDTH; - - // 渲染可见的流体槽 - if (visibleTanks > 0) { - // 计算每个槽位的垂直间距,使上下边距和槽位间隔相等 - int totalTankHeight = visibleTanks * FLUID_TANK_HEIGHT; - int totalSpacing = FLUID_OUTPUT_AREA_HEIGHT - totalTankHeight; - int spacing = visibleTanks > 1 ? totalSpacing / (visibleTanks + 1) : totalSpacing / 2; - - for (int i = outputFluidScrollOffset; i < Math.min(totalTanks, outputFluidScrollOffset + visibleTanks); i++) { - int tankIndex = i; - // 计算流体槽Y坐标,使上下边距和槽位间隔相等 - int tankY = y + FLUID_OUTPUT_AREA_Y + spacing + (i - outputFluidScrollOffset) * (FLUID_TANK_HEIGHT + spacing); - // 整体横向居中:(区域宽度 - 总宽度) / 2 - int startX = x + FLUID_OUTPUT_AREA_X + (FLUID_OUTPUT_AREA_WIDTH - totalWidth) / 2; - int tankX = startX; - int buttonX = startX + FLUID_TANK_WIDTH + 1; // 流体槽右边空1像素 - - // 渲染流体槽背景 - guiGraphics.blit(COMPONENTS_TEXTURE, - tankX, tankY, - FLUID_TANK_U, FLUID_TANK_V, - FLUID_TANK_WIDTH, FLUID_TANK_HEIGHT); - - // 渲染流体 - FluidStack fluid = menu.getBlockEntity().getOutputFluidTank(tankIndex).getFluid(); - int capacity = menu.getBlockEntity().getOutputFluidTank(tankIndex).getCapacity(); - renderFluidTank(guiGraphics, tankX, tankY, fluid, capacity); - - // 渲染流体槽遮罩 - guiGraphics.blit(COMPONENTS_TEXTURE, - tankX, tankY, - FLUID_TANK_MASK_U, FLUID_TANK_MASK_V, - FLUID_TANK_WIDTH, FLUID_TANK_HEIGHT); - - // 渲染清空流体按钮 - renderTankClearButton(guiGraphics, buttonX, tankY, tankIndex, false); - } - } - } - - // 新增:渲染催化剂和模具槽位 - 不渲染流体槽相关贴图 - private void renderCatalystAndMoldSlots(GuiGraphics guiGraphics, int x, int y) { - // 槽位背景已在主GUI贴图中,不需要额外渲染流体槽贴图 - } - - // 修改:渲染指示灯,现在显示并行数状态 - private void renderIndicators(GuiGraphics guiGraphics, int x, int y) { - if (menu == null) return; - - int currentParallel = menu.getCurrentParallel(); - int maxParallel = menu.getMaxParallel(); - - // 渲染催化剂指示灯 - 根据并行数状态显示不同颜色 - // 熄灭状态主图自带,不需要额外渲染,只渲染亮起状态 - if (currentParallel > 1) { - guiGraphics.blit(COMPONENTS_TEXTURE, - x + CATALYST_INDICATOR_X, y + CATALYST_INDICATOR_Y, - LIT_INDICATOR_U, LIT_INDICATOR_V, - INDICATOR_WIDTH, INDICATOR_HEIGHT); - } - - // 模具指示灯 - 使用menu中的方法获取需要模具的状态 - // 熄灭状态主图自带,不需要额外渲染,只渲染亮起状态 - boolean requiresMold = menu.requiresMold(); - if (requiresMold) { - guiGraphics.blit(COMPONENTS_TEXTURE, - x + MOLD_INDICATOR_X, y + MOLD_INDICATOR_Y, - LIT_INDICATOR_U, LIT_INDICATOR_V, - INDICATOR_WIDTH, INDICATOR_HEIGHT); - } - } - - // 修改流体渲染方法,从左向右填充,使用完整单位,流体大小为53*15,离边缘1像素 - private void renderFluidTank(GuiGraphics guiGraphics, int x, int y, FluidStack fluid, int capacity) { - if (!fluid.isEmpty() && capacity > 0) { - // 流体渲染尺寸:离边缘1像素,大小53*15 - final int FLUID_RENDER_WIDTH = 53; - final int FLUID_RENDER_HEIGHT = 15; - final int FLUID_RENDER_X_OFFSET = 1; - final int FLUID_RENDER_Y_OFFSET = 1; - - float fluidRatio = (float) fluid.getAmount() / capacity; - int fluidWidth = (int) (FLUID_RENDER_WIDTH * fluidRatio); // 从左向右填充,所以计算宽度 - if (fluidWidth > 0) { - IClientFluidTypeExtensions fluidAttributes = IClientFluidTypeExtensions.of(fluid.getFluid()); - ResourceLocation fluidStillTexture = fluidAttributes.getStillTexture(fluid); - int fluidColor = fluidAttributes.getTintColor(fluid); - - if (fluidStillTexture != null) { - TextureAtlasSprite fluidSprite = minecraft.getTextureAtlas(InventoryMenu.BLOCK_ATLAS).apply(fluidStillTexture); - - float r = ((fluidColor >> 16) & 0xFF) / 255.0F; - float g = ((fluidColor >> 8) & 0xFF) / 255.0F; - float b = (fluidColor & 0xFF) / 255.0F; - float a = ((fluidColor >> 24) & 0xFF) / 255.0F; - - RenderSystem.setShaderColor(r, g, b, a); - RenderSystem.setShaderTexture(0, InventoryMenu.BLOCK_ATLAS); - - int renderX = x + FLUID_RENDER_X_OFFSET; - int renderY = y + FLUID_RENDER_Y_OFFSET; - int renderHeight = FLUID_RENDER_HEIGHT; - - // 渲染流体 - 从左向右填充 - for (int i = 0; i < Math.ceil((double) fluidWidth / 16); i++) { - for (int j = 0; j < Math.ceil((double) renderHeight / 16); j++) { - int texWidth = Math.min(16, fluidWidth - i * 16); - int texHeight = Math.min(16, renderHeight - j * 16); - - if (texWidth > 0 && texHeight > 0) { - guiGraphics.blit( - renderX + i * 16, - renderY + j * 16, - 0, - texWidth, - texHeight, - fluidSprite - ); - } - } - } - - RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); - } - } - - // 渲染流体数量文本 - 使用完整单位 - SlotRendererUtil.renderFluidAmount(guiGraphics, fluid.getAmount(), x, y, FLUID_TANK_WIDTH, FLUID_TANK_HEIGHT); - } - } - - // 渲染清空按钮 - - // 渲染滑块 - private void renderSlider(GuiGraphics guiGraphics, int x, int y) { - // 滑块槽不需要纹理坐标,主图自带,直接渲染滑块即可 - - // 计算输入区域滑块位置 - int totalTanks = 6; - int visibleInputTanks = FLUID_INPUT_AREA_HEIGHT / (FLUID_TANK_HEIGHT + FLUID_TANK_SPACING); - int maxInputOffset = Math.max(0, totalTanks - visibleInputTanks); - - // 计算输入滑块可移动范围 - int inputSliderRange = SLIDER_SLOT_HEIGHT - SLIDER_HEIGHT; - int inputSliderY = y + SLIDER_SLOT_Y; - if (maxInputOffset > 0) { - inputSliderY += (int) ((float) inputFluidScrollOffset / maxInputOffset * inputSliderRange); - } - - // 渲染输入区域滑块 - guiGraphics.blit(COMPONENTS_TEXTURE, - x + SLIDER_SLOT_X - 2, inputSliderY, - isDraggingInputSlider ? SLIDER_PRESSED_U : SLIDER_DEFAULT_U, - isDraggingInputSlider ? SLIDER_PRESSED_V : SLIDER_DEFAULT_V, - SLIDER_WIDTH, SLIDER_HEIGHT); - - // 计算输出区域滑块位置 - int visibleOutputTanks = FLUID_OUTPUT_AREA_HEIGHT / (FLUID_TANK_HEIGHT + FLUID_TANK_SPACING); - int maxOutputOffset = Math.max(0, totalTanks - visibleOutputTanks); - - // 计算输出滑块可移动范围 - int outputSliderRange = OUTPUT_SLIDER_SLOT_HEIGHT - SLIDER_HEIGHT; - int outputSliderY = y + OUTPUT_SLIDER_SLOT_Y; - if (maxOutputOffset > 0) { - outputSliderY += (int) ((float) outputFluidScrollOffset / maxOutputOffset * outputSliderRange); - } - - // 渲染输出区域滑块,滑块纹理起点0,72,大小7*15 - guiGraphics.blit(COMPONENTS_TEXTURE, - x + OUTPUT_SLIDER_SLOT_X - 2, outputSliderY, - isDraggingOutputSlider ? SLIDER_PRESSED_U : SLIDER_DEFAULT_U, - isDraggingOutputSlider ? SLIDER_PRESSED_V : SLIDER_DEFAULT_V, - SLIDER_WIDTH, SLIDER_HEIGHT); - } - - @Override - public boolean mouseScrolled(double mouseX, double mouseY, double delta) { - // 检查鼠标是否在流体输入区域内 - int x = (width - imageWidth) / 2; - int y = (height - imageHeight) / 2; - - // 检查输入区域滚动 - if (isMouseOverArea((int) mouseX, (int) mouseY, x + FLUID_INPUT_AREA_X, y + FLUID_INPUT_AREA_Y, - FLUID_INPUT_AREA_WIDTH, FLUID_INPUT_AREA_HEIGHT)) { - // 滚动流体输入区域 - int scrollAmount = delta > 0 ? -1 : 1; - int visibleTanks = FLUID_INPUT_AREA_HEIGHT / (FLUID_TANK_HEIGHT + FLUID_TANK_SPACING); - inputFluidScrollOffset = Math.max(0, Math.min(6 - visibleTanks, - inputFluidScrollOffset + scrollAmount)); - return true; - } - - // 检查输出区域滚动 - if (isMouseOverArea((int) mouseX, (int) mouseY, x + FLUID_OUTPUT_AREA_X, y + FLUID_OUTPUT_AREA_Y, - FLUID_OUTPUT_AREA_WIDTH, FLUID_OUTPUT_AREA_HEIGHT)) { - // 滚动流体输出区域 - int scrollAmount = delta > 0 ? -1 : 1; - int visibleTanks = FLUID_OUTPUT_AREA_HEIGHT / (FLUID_TANK_HEIGHT + FLUID_TANK_SPACING); - outputFluidScrollOffset = Math.max(0, Math.min(6 - visibleTanks, - outputFluidScrollOffset + scrollAmount)); - return true; - } - - return super.mouseScrolled(mouseX, mouseY, delta); - } - - @Override - public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) { - if (isDraggingInputSlider) { - int x = (width - imageWidth) / 2; - int y = (height - imageHeight) / 2; - - // 计算新的滑块位置 - int sliderY = (int) mouseY - draggedInputSliderY; - int minSliderY = y + SLIDER_SLOT_Y; - int maxSliderY = minSliderY + SLIDER_SLOT_HEIGHT - SLIDER_HEIGHT; - - // 限制滑块位置 - sliderY = Math.max(minSliderY, Math.min(maxSliderY, sliderY)); - - // 计算对应的滚动偏移 - int sliderRange = SLIDER_SLOT_HEIGHT - SLIDER_HEIGHT; - int totalTanks = 6; - int visibleTanks = FLUID_INPUT_AREA_HEIGHT / (FLUID_TANK_HEIGHT + FLUID_TANK_SPACING); - int maxOffset = Math.max(0, totalTanks - visibleTanks); - - if (maxOffset > 0) { - inputFluidScrollOffset = (int) (((double) (sliderY - minSliderY) / sliderRange) * maxOffset); - inputFluidScrollOffset = Math.max(0, Math.min(maxOffset, inputFluidScrollOffset)); - } - - return true; - } - - // 处理输出滑块拖拽 - if (isDraggingOutputSlider) { - int x = (width - imageWidth) / 2; - int y = (height - imageHeight) / 2; - - // 计算新的滑块位置 - int sliderY = (int) mouseY - draggedOutputSliderY; - int minSliderY = y + OUTPUT_SLIDER_SLOT_Y; - int maxSliderY = minSliderY + OUTPUT_SLIDER_SLOT_HEIGHT - SLIDER_HEIGHT; - - // 限制滑块位置 - sliderY = Math.max(minSliderY, Math.min(maxSliderY, sliderY)); - - // 计算对应的滚动偏移 - int sliderRange = OUTPUT_SLIDER_SLOT_HEIGHT - SLIDER_HEIGHT; - int totalTanks = 6; - int visibleTanks = FLUID_OUTPUT_AREA_HEIGHT / (FLUID_TANK_HEIGHT + FLUID_TANK_SPACING); - int maxOffset = Math.max(0, totalTanks - visibleTanks); - - if (maxOffset > 0) { - outputFluidScrollOffset = (int) (((double) (sliderY - minSliderY) / sliderRange) * maxOffset); - outputFluidScrollOffset = Math.max(0, Math.min(maxOffset, outputFluidScrollOffset)); - } - - return true; - } - - return super.mouseDragged(mouseX, mouseY, button, dragX, dragY); - } - - @Override - protected void renderLabels(GuiGraphics guiGraphics, int mouseX, int mouseY) { - // 取消渲染标题和物品栏文字,根据用户要求 - } - - private void renderRainbowText(GuiGraphics guiGraphics, Font font, Component text, int x, int y) { - String string = text.getString(); - int currentX = x; - - for (int i = 0; i < string.length(); i++) { - char c = string.charAt(i); - int color = getRainbowColor(i, string.length()); - guiGraphics.drawString(font, String.valueOf(c), currentX, y, color, false); - currentX += font.width(String.valueOf(c)); - } - } - - private int getRainbowColor(int index, int total) { - float hue = (float) index / total; - return java.awt.Color.HSBtoRGB(hue, 1.0f, 1.0f) | 0xFF000000; - } - - @Override - public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { - renderBackground(guiGraphics); - - // 使用父类的渲染逻辑(这会正常渲染所有槽位,包括玩家物品栏) - super.render(guiGraphics, mouseX, mouseY, partialTick); - - // 为HighStackSlot添加自定义数量渲染 - renderHighStackSlotCounts(guiGraphics); - - // 渲染自定义工具提示 - renderCustomTooltips(guiGraphics, mouseX, mouseY); - } - - /** - * 为HighStackSlot渲染自定义数量文本 - */ - private void renderHighStackSlotCounts(GuiGraphics guiGraphics) { - int x = (width - imageWidth) / 2; - int y = (height - imageHeight) / 2; - - for (Slot slot : this.menu.slots) { - if (slot instanceof HighStackSlot && slot.hasItem() && slot.getItem().getCount() > 1) { - renderHighStackCount(guiGraphics, slot, x, y); - } - } - } - - /** - * 渲染单个HighStackSlot的数量文本 - */ - private void renderHighStackCount(GuiGraphics guiGraphics, Slot slot, int x, int y) { - ItemStack stack = slot.getItem(); - String text = NumberFormatUtil.formatItemCount(stack.getCount()); - Font font = minecraft.font; - - guiGraphics.pose().pushPose(); - float scale = 0.65f; - guiGraphics.pose().translate(0, 0, 300.0F); - guiGraphics.pose().scale(scale, scale, 1.0F); - - int textX = Math.round((x + slot.x + 16 - font.width(text) * scale) / scale); - int textY = Math.round((y + slot.y + 16 - 8 * scale) / scale); - - guiGraphics.drawString(font, text, textX, textY, 0xFFFFFF, true); - guiGraphics.pose().popPose(); - } - - /** - * 渲染自定义工具提示 - */ - // 修改工具提示渲染方法,实时显示并行数 - // 在 renderCustomTooltips 方法中修改催化剂和模具信息区域悬停提示 - private void renderCustomTooltips(GuiGraphics guiGraphics, int mouseX, int mouseY) { - if (menu == null) return; - - int x = (width - imageWidth) / 2; - int y = (height - imageHeight) / 2; - - // 首先渲染物品的工具提示 - for (Slot slot : menu.slots) { - if (isHovering(slot.x, slot.y, 16, 16, mouseX, mouseY)) { - ItemStack stack = slot.getItem(); - if (!stack.isEmpty()) { - // 渲染物品的工具提示 - guiGraphics.renderComponentTooltip(this.font, stack.getTooltipLines(minecraft.player, minecraft.options.advancedItemTooltips ? TooltipFlag.Default.ADVANCED : TooltipFlag.Default.NORMAL), mouseX, mouseY); - } - } - } - - // 能量条悬停提示 - if (isMouseOverArea(mouseX, mouseY, x + ENERGY_BAR_X, y + ENERGY_BAR_Y, - ENERGY_BAR_WIDTH, ENERGY_BAR_HEIGHT)) { - List tooltip = new ArrayList<>(); - tooltip.add(Component.literal("能量: " + menu.getEnergy() + " / " + menu.getMaxEnergy() + " FE")); - tooltip.add(Component.literal("消耗: " + menu.getProcessTick() + " FE/t")); - guiGraphics.renderComponentTooltip(this.font, tooltip, mouseX, mouseY); - } - - // 进度条悬停提示 - if (isMouseOverProgressArea(mouseX, mouseY, x, y)) { - List tooltip = new ArrayList<>(); - int progress = menu.getProgress(); - int maxProgress = menu.getMaxProgress(); - - if (maxProgress > 0) { - float progressPercent = (float) progress / maxProgress * 100; - tooltip.add(Component.literal("进度: " + progress + "/" + maxProgress + " (" + String.format("%.1f", progressPercent) + "%)")); - tooltip.add(Component.literal("状态: " + (menu.isActive() ? "工作中" : "空闲"))); - // 添加并行数信息 - tooltip.add(Component.literal("本次并行数: " + menu.getCurrentParallel())); - tooltip.add(Component.literal("当前最大并行数: " + menu.getMaxParallel())); - } else { - tooltip.add(Component.literal("没有活动进程")); - } - guiGraphics.renderComponentTooltip(this.font, tooltip, mouseX, mouseY); - } - - // 移除旧UI清空按钮的悬停提示 - - // 修复:催化剂和模具信息区域悬停提示,始终显示并行数信息 - if (isMouseOverArea(mouseX, mouseY, - x + CATALYST_MOLD_INFO_X, y + CATALYST_MOLD_INFO_Y, - CATALYST_MOLD_INFO_WIDTH, CATALYST_MOLD_INFO_HEIGHT)) { - List tooltip = new ArrayList<>(); - - int currentParallel = menu.getCurrentParallel(); - int maxParallel = menu.getMaxParallel(); - - tooltip.add(Component.literal("本次并行数: " + currentParallel).withStyle(ChatFormatting.YELLOW)); - tooltip.add(Component.literal("当前最大并行数: " + maxParallel).withStyle(ChatFormatting.BLUE)); - - // 检查配方是否允许催化剂 - if (menu.getBlockEntity() != null) { - ItemStack catalyst = menu.getBlockEntity().getItemInSlot(12); // 催化剂槽位 - - // 获取当前配方(需要添加获取方法) - AdvancedAlloyFurnaceRecipe currentRecipe = menu.getBlockEntity().getCurrentRecipe(); - boolean catalystAllowed = currentRecipe == null || currentRecipe.isCatalystAllowed(); - - if (menu.getBlockEntity().hasCatalyst() && catalystAllowed) { - String catalystName = CatalystManager.getCatalystName(catalyst); - int catalystParallel = CatalystManager.getCatalystParallel(catalyst); - tooltip.add(Component.literal("催化剂: " + catalystName + " (" + catalystParallel + "倍)").withStyle(ChatFormatting.GREEN)); - } else if (!catalystAllowed) { - tooltip.add(Component.literal("该配方不支持催化剂并行").withStyle(ChatFormatting.RED)); - } - } - - tooltip.add(Component.literal("")); - tooltip.add(Component.literal("并行数说明:").withStyle(ChatFormatting.GOLD)); - tooltip.add(Component.literal("• 消耗和产出乘以并行数").withStyle(ChatFormatting.GRAY)); - tooltip.add(Component.literal("• 处理时间保持不变").withStyle(ChatFormatting.GRAY)); - tooltip.add(Component.literal("• 能量消耗乘以并行数(有用锭除外)").withStyle(ChatFormatting.GRAY)); - tooltip.add(Component.literal("• 催化剂为可选项,可提高并行数").withStyle(ChatFormatting.GRAY)); - tooltip.add(Component.literal("• 有用锭作为催化剂时不会被消耗").withStyle(ChatFormatting.GRAY)); - tooltip.add(Component.literal("⚠ 普通催化剂会被消耗").withStyle(ChatFormatting.RED)); - - guiGraphics.renderComponentTooltip(this.font, tooltip, mouseX, mouseY); - } - - // 催化剂槽位悬停提示 - 当槽内有物品时不再显示 - if (isMouseOverArea(mouseX, mouseY, - x + menu.getCatalystSlotX(), y + menu.getCatalystSlotY(), 18, 18)) { - // 检查催化剂槽是否有物品 - boolean hasCatalyst = false; - if (menu.getBlockEntity() != null) { - ItemStack catalyst = menu.getBlockEntity().getItemInSlot(12); // 催化剂槽位 - hasCatalyst = !catalyst.isEmpty(); - } - - // 只有当槽内没有物品时才显示提示 - if (!hasCatalyst) { - List tooltip = new ArrayList<>(); - tooltip.add(Component.literal("催化剂槽位").withStyle(ChatFormatting.GOLD)); - tooltip.add(Component.literal("放入无用锭可提高并行数").withStyle(ChatFormatting.GRAY)); - - // 使用 CatalystManager 获取格式化的催化剂列表 - tooltip.addAll(CatalystManager.getFormattedCatalystList()); - - tooltip.add(Component.literal("")); - tooltip.add(Component.literal("催化剂为可选项,不放入时并行数为1").withStyle(ChatFormatting.YELLOW)); - tooltip.add(Component.literal("注意: 合成无用锭时催化剂无效").withStyle(ChatFormatting.RED)); - tooltip.add(Component.literal("⚠ 催化剂会被消耗").withStyle(ChatFormatting.RED)); - - guiGraphics.renderComponentTooltip(this.font, tooltip, mouseX, mouseY); - } - } - - // 流体槽悬停提示 - renderFluidTooltips(guiGraphics, mouseX, mouseY, x, y); - } - - private boolean isMouseOverProgressArea(int mouseX, int mouseY, int x, int y) { - return isMouseOverArea(mouseX, mouseY, x + PROGRESS_LEFT_X, y + PROGRESS_LEFT_Y, PROGRESS_LEFT_WIDTH, PROGRESS_LEFT_HEIGHT) || - isMouseOverArea(mouseX, mouseY, x + PROGRESS_RIGHT_X, y + PROGRESS_RIGHT_Y, PROGRESS_RIGHT_WIDTH, PROGRESS_RIGHT_HEIGHT); - } - - // 渲染流体槽悬浮提示 - private void renderFluidTooltips(GuiGraphics guiGraphics, int mouseX, int mouseY, int x, int y) { - if (menu == null || menu.getBlockEntity() == null) return; - - // 计算可见的流体槽数量 - int visibleInputTanks = FLUID_INPUT_AREA_HEIGHT / (FLUID_TANK_HEIGHT + FLUID_TANK_SPACING); - int visibleOutputTanks = FLUID_OUTPUT_AREA_HEIGHT / (FLUID_TANK_HEIGHT + FLUID_TANK_SPACING); - int totalTanks = 6; - - // 计算流体槽和按钮的总宽度 - int totalWidth = FLUID_TANK_WIDTH + 1 + TANK_CLEAR_BUTTON_WIDTH; - - // 检查输入区域的流体槽 - if (visibleInputTanks > 0) { - // 计算每个槽位的垂直间距,与渲染时保持一致 - int inputTotalTankHeight = visibleInputTanks * FLUID_TANK_HEIGHT; - int inputTotalSpacing = FLUID_INPUT_AREA_HEIGHT - inputTotalTankHeight; - int inputSpacing = visibleInputTanks > 1 ? inputTotalSpacing / (visibleInputTanks + 1) : inputTotalSpacing / 2; - - for (int i = inputFluidScrollOffset; i < Math.min(totalTanks, inputFluidScrollOffset + visibleInputTanks); i++) { - int tankY = y + FLUID_INPUT_AREA_Y + inputSpacing + (i - inputFluidScrollOffset) * (FLUID_TANK_HEIGHT + inputSpacing); - int startX = x + FLUID_INPUT_AREA_X + (FLUID_INPUT_AREA_WIDTH - totalWidth) / 2; - int tankX = startX; - - if (isMouseOverArea(mouseX, mouseY, tankX, tankY, FLUID_TANK_WIDTH, FLUID_TANK_HEIGHT)) { - FluidStack fluid = menu.getBlockEntity().getInputFluidTank(i).getFluid(); - if (!fluid.isEmpty()) { - renderFluidTooltip(guiGraphics, mouseX, mouseY, fluid, menu.getBlockEntity().getInputFluidTank(i).getCapacity()); - return; - } - } - } - } - - // 检查输出区域的流体槽 - if (visibleOutputTanks > 0) { - // 计算每个槽位的垂直间距,与渲染时保持一致 - int outputTotalTankHeight = visibleOutputTanks * FLUID_TANK_HEIGHT; - int outputTotalSpacing = FLUID_OUTPUT_AREA_HEIGHT - outputTotalTankHeight; - int outputSpacing = visibleOutputTanks > 1 ? outputTotalSpacing / (visibleOutputTanks + 1) : outputTotalSpacing / 2; - - for (int i = outputFluidScrollOffset; i < Math.min(totalTanks, outputFluidScrollOffset + visibleOutputTanks); i++) { - int tankY = y + FLUID_OUTPUT_AREA_Y + outputSpacing + (i - outputFluidScrollOffset) * (FLUID_TANK_HEIGHT + outputSpacing); - int startX = x + FLUID_OUTPUT_AREA_X + (FLUID_OUTPUT_AREA_WIDTH - totalWidth) / 2; - int tankX = startX; - - if (isMouseOverArea(mouseX, mouseY, tankX, tankY, FLUID_TANK_WIDTH, FLUID_TANK_HEIGHT)) { - FluidStack fluid = menu.getBlockEntity().getOutputFluidTank(i).getFluid(); - if (!fluid.isEmpty()) { - renderFluidTooltip(guiGraphics, mouseX, mouseY, fluid, menu.getBlockEntity().getOutputFluidTank(i).getCapacity()); - return; - } - } - } - } - } - - // 渲染单个流体槽的悬浮提示 - private void renderFluidTooltip(GuiGraphics guiGraphics, int mouseX, int mouseY, FluidStack fluid, int capacity) { - List tooltip = new ArrayList<>(); - - // 添加流体名称 - tooltip.add(fluid.getDisplayName().copy().withStyle(ChatFormatting.AQUA)); - - // 添加流体数量 - tooltip.add(Component.literal(fluid.getAmount() + " / " + capacity + " mB").withStyle(ChatFormatting.WHITE)); - - guiGraphics.renderComponentTooltip(this.font, tooltip, mouseX, mouseY); - } - - private void renderFluidInteractions(GuiGraphics guiGraphics, int mouseX, int mouseY, int x, int y) { - // 移除旧的流体槽交互提示,因为现在使用的是新的流体输入输出区域 - } - - @Override - public boolean mouseClicked(double mouseX, double mouseY, int button) { - if (menu != null && menu.getBlockEntity() != null) { - int x = (width - imageWidth) / 2; - int y = (height - imageHeight) / 2; - - // 移除旧UI的全局清空按钮检测 - - // 检查是否点击了单个流体槽的清空按钮 - Object[] clickedInfo = getClickedTankClearButtonIndex(mouseX, mouseY); - if (clickedInfo != null) { - int clickedTankIndex = (int) clickedInfo[0]; - boolean isInput = (boolean) clickedInfo[1]; - // 更新对应区域的按钮状态 - if (isInput) { - inputTankClearButtonsPressed[clickedTankIndex] = true; - } else { - outputTankClearButtonsPressed[clickedTankIndex] = true; - } - handleTankClearButton(clickedTankIndex, isInput); - return true; - } - - // 检查是否点击了输入流体槽区域 - if (isMouseOverFluidInputArea((int)mouseX, (int)mouseY)) { - handleFluidInputAreaInteraction(mouseX, mouseY, button); - return true; - } - - // 检查是否点击了输出流体槽区域 - if (isMouseOverArea((int)mouseX, (int)mouseY, x + FLUID_OUTPUT_AREA_X, y + FLUID_OUTPUT_AREA_Y, - FLUID_OUTPUT_AREA_WIDTH, FLUID_OUTPUT_AREA_HEIGHT)) { - handleFluidOutputAreaInteraction(mouseX, mouseY, button); - return true; - } - - // 检查是否点击了输入滑块 - if (isMouseOverInputSlider((int)mouseX, (int)mouseY)) { - isDraggingInputSlider = true; - int totalTanks = 6; - int visibleTanks = FLUID_INPUT_AREA_HEIGHT / (FLUID_TANK_HEIGHT + FLUID_TANK_SPACING); - int maxOffset = Math.max(0, totalTanks - visibleTanks); - int sliderRange = SLIDER_SLOT_HEIGHT - SLIDER_HEIGHT; - int sliderY = y + SLIDER_SLOT_Y; - if (maxOffset > 0) { - sliderY += (int) ((float) inputFluidScrollOffset / maxOffset * sliderRange); - } - draggedInputSliderY = (int) mouseY - sliderY; - return true; - } - - // 检查是否点击了输出滑块 - if (isMouseOverOutputSlider((int)mouseX, (int)mouseY)) { - isDraggingOutputSlider = true; - int totalTanks = 6; - int visibleTanks = FLUID_OUTPUT_AREA_HEIGHT / (FLUID_TANK_HEIGHT + FLUID_TANK_SPACING); - int maxOffset = Math.max(0, totalTanks - visibleTanks); - int sliderRange = OUTPUT_SLIDER_SLOT_HEIGHT - SLIDER_HEIGHT; - int sliderY = y + OUTPUT_SLIDER_SLOT_Y; - if (maxOffset > 0) { - sliderY += (int) ((float) outputFluidScrollOffset / maxOffset * sliderRange); - } - draggedOutputSliderY = (int) mouseY - sliderY; - return true; - } - } - - return super.mouseClicked(mouseX, mouseY, button); - } - - @Override - public boolean mouseReleased(double mouseX, double mouseY, int button) { - // 移除旧UI的清空按钮释放处理,只处理新UI的每槽清空按钮 - - // 检查是否释放了输入区域单个流体槽的清空按钮 - for (int i = 0; i < inputTankClearButtonsPressed.length; i++) { - if (inputTankClearButtonsPressed[i]) { - inputTankClearButtonsPressed[i] = false; - return true; - } - } - - // 检查是否释放了输出区域单个流体槽的清空按钮 - for (int i = 0; i < outputTankClearButtonsPressed.length; i++) { - if (outputTankClearButtonsPressed[i]) { - outputTankClearButtonsPressed[i] = false; - return true; - } - } - - // 处理输入滑块释放 - if (isDraggingInputSlider) { - isDraggingInputSlider = false; - return true; - } - - // 处理输出滑块释放 - if (isDraggingOutputSlider) { - isDraggingOutputSlider = false; - return true; - } - - return super.mouseReleased(mouseX, mouseY, button); - } - - // 获取被点击的流体槽清空按钮索引,返回数组 [tankIndex, isInput] - private Object[] getClickedTankClearButtonIndex(double mouseX, double mouseY) { - int x = (width - imageWidth) / 2; - int y = (height - imageHeight) / 2; - - // 计算可见的流体槽数量 - int visibleInputTanks = FLUID_INPUT_AREA_HEIGHT / (FLUID_TANK_HEIGHT + FLUID_TANK_SPACING); - int visibleOutputTanks = FLUID_OUTPUT_AREA_HEIGHT / (FLUID_TANK_HEIGHT + FLUID_TANK_SPACING); - int totalTanks = 6; - - // 计算流体槽和按钮的总宽度 - int totalWidth = FLUID_TANK_WIDTH + 1 + TANK_CLEAR_BUTTON_WIDTH; - - // 检查输入区域的清空按钮 - if (visibleInputTanks > 0) { - // 计算每个槽位的垂直间距,与渲染时保持一致 - int inputTotalTankHeight = visibleInputTanks * FLUID_TANK_HEIGHT; - int inputTotalSpacing = FLUID_INPUT_AREA_HEIGHT - inputTotalTankHeight; - int inputSpacing = visibleInputTanks > 1 ? inputTotalSpacing / (visibleInputTanks + 1) : inputTotalSpacing / 2; - - for (int i = inputFluidScrollOffset; i < Math.min(totalTanks, inputFluidScrollOffset + visibleInputTanks); i++) { - int tankY = y + FLUID_INPUT_AREA_Y + inputSpacing + (i - inputFluidScrollOffset) * (FLUID_TANK_HEIGHT + inputSpacing); - int startX = x + FLUID_INPUT_AREA_X + (FLUID_INPUT_AREA_WIDTH - totalWidth) / 2; - int buttonX = startX + FLUID_TANK_WIDTH + 1; - - if (isMouseOverArea((int)mouseX, (int)mouseY, buttonX, tankY, TANK_CLEAR_BUTTON_WIDTH, TANK_CLEAR_BUTTON_HEIGHT)) { - return new Object[]{i, true}; // 返回槽位索引和输入区域标记 - } - } - } - - // 检查输出区域的清空按钮 - if (visibleOutputTanks > 0) { - // 计算每个槽位的垂直间距,与渲染时保持一致 - int outputTotalTankHeight = visibleOutputTanks * FLUID_TANK_HEIGHT; - int outputTotalSpacing = FLUID_OUTPUT_AREA_HEIGHT - outputTotalTankHeight; - int outputSpacing = visibleOutputTanks > 1 ? outputTotalSpacing / (visibleOutputTanks + 1) : outputTotalSpacing / 2; - - for (int i = outputFluidScrollOffset; i < Math.min(totalTanks, outputFluidScrollOffset + visibleOutputTanks); i++) { - int tankY = y + FLUID_OUTPUT_AREA_Y + outputSpacing + (i - outputFluidScrollOffset) * (FLUID_TANK_HEIGHT + outputSpacing); - int startX = x + FLUID_OUTPUT_AREA_X + (FLUID_OUTPUT_AREA_WIDTH - totalWidth) / 2; - int buttonX = startX + FLUID_TANK_WIDTH + 1; - - if (isMouseOverArea((int)mouseX, (int)mouseY, buttonX, tankY, TANK_CLEAR_BUTTON_WIDTH, TANK_CLEAR_BUTTON_HEIGHT)) { - return new Object[]{i, false}; // 返回槽位索引和输出区域标记 - } - } - } - return null; // 未点击任何按钮 - } - - // 检查鼠标是否在流体输入区域内 - private boolean isMouseOverFluidInputArea(int mouseX, int mouseY) { - int x = (width - imageWidth) / 2; - int y = (height - imageHeight) / 2; - return isMouseOverArea(mouseX, mouseY, x + FLUID_INPUT_AREA_X, y + FLUID_INPUT_AREA_Y, - FLUID_INPUT_AREA_WIDTH, FLUID_INPUT_AREA_HEIGHT); - } - - // 检查鼠标是否在输入滑块上 - private boolean isMouseOverInputSlider(int mouseX, int mouseY) { - int x = (width - imageWidth) / 2; - int y = (height - imageHeight) / 2; - - // 计算输入滑块位置 - int totalTanks = 6; - int visibleTanks = FLUID_INPUT_AREA_HEIGHT / (FLUID_TANK_HEIGHT + FLUID_TANK_SPACING); - int maxOffset = Math.max(0, totalTanks - visibleTanks); - - // 计算滑块可移动范围 - int sliderRange = SLIDER_SLOT_HEIGHT - SLIDER_HEIGHT; - int sliderY = y + SLIDER_SLOT_Y; - if (maxOffset > 0) { - sliderY += (int) ((float) inputFluidScrollOffset / maxOffset * sliderRange); - } - - // 检查鼠标是否在滑块上 - return isMouseOverArea(mouseX, mouseY, x + SLIDER_SLOT_X - 2, sliderY, SLIDER_WIDTH, SLIDER_HEIGHT); - } - - // 检查鼠标是否在输出滑块上 - private boolean isMouseOverOutputSlider(int mouseX, int mouseY) { - int x = (width - imageWidth) / 2; - int y = (height - imageHeight) / 2; - - // 计算输出滑块位置 - int totalTanks = 6; - int visibleTanks = FLUID_OUTPUT_AREA_HEIGHT / (FLUID_TANK_HEIGHT + FLUID_TANK_SPACING); - int maxOffset = Math.max(0, totalTanks - visibleTanks); - - // 计算滑块可移动范围 - int sliderRange = OUTPUT_SLIDER_SLOT_HEIGHT - SLIDER_HEIGHT; - int sliderY = y + OUTPUT_SLIDER_SLOT_Y; - if (maxOffset > 0) { - sliderY += (int) ((float) outputFluidScrollOffset / maxOffset * sliderRange); - } - - // 检查鼠标是否在滑块上 - return isMouseOverArea(mouseX, mouseY, x + OUTPUT_SLIDER_SLOT_X - 2, sliderY, SLIDER_WIDTH, SLIDER_HEIGHT); - } - - // 处理流体输入区域的交互 - private void handleFluidInputAreaInteraction(double mouseX, double mouseY, int button) { - int x = (width - imageWidth) / 2; - int y = (height - imageHeight) / 2; - - // 计算可见的流体槽数量 - int visibleTanks = FLUID_INPUT_AREA_HEIGHT / (FLUID_TANK_HEIGHT + FLUID_TANK_SPACING); - int totalTanks = 6; - - // 计算流体槽和按钮的总宽度 - int totalWidth = FLUID_TANK_WIDTH + 1 + TANK_CLEAR_BUTTON_WIDTH; - - if (visibleTanks > 0) { - // 计算每个槽位的垂直间距,与渲染时保持一致 - int totalTankHeight = visibleTanks * FLUID_TANK_HEIGHT; - int totalSpacing = FLUID_INPUT_AREA_HEIGHT - totalTankHeight; - int spacing = visibleTanks > 1 ? totalSpacing / (visibleTanks + 1) : totalSpacing / 2; - - for (int i = inputFluidScrollOffset; i < Math.min(totalTanks, inputFluidScrollOffset + visibleTanks); i++) { - int tankIndex = i; - int tankY = y + FLUID_INPUT_AREA_Y + spacing + (i - inputFluidScrollOffset) * (FLUID_TANK_HEIGHT + spacing); - int startX = x + FLUID_INPUT_AREA_X + (FLUID_INPUT_AREA_WIDTH - totalWidth) / 2; - int tankX = startX; - - if (isMouseOverArea((int)mouseX, (int)mouseY, tankX, tankY, FLUID_TANK_WIDTH, FLUID_TANK_HEIGHT)) { - handleFluidInteraction(true, button, tankIndex); - return; - } - } - } - } - - // 处理流体输出区域的交互 - private void handleFluidOutputAreaInteraction(double mouseX, double mouseY, int button) { - int x = (width - imageWidth) / 2; - int y = (height - imageHeight) / 2; - - // 计算可见的流体槽数量 - int visibleTanks = FLUID_OUTPUT_AREA_HEIGHT / (FLUID_TANK_HEIGHT + FLUID_TANK_SPACING); - int totalTanks = 6; - - // 计算流体槽和按钮的总宽度 - int totalWidth = FLUID_TANK_WIDTH + 1 + TANK_CLEAR_BUTTON_WIDTH; - - if (visibleTanks > 0) { - // 计算每个槽位的垂直间距,与渲染时保持一致 - int totalTankHeight = visibleTanks * FLUID_TANK_HEIGHT; - int totalSpacing = FLUID_OUTPUT_AREA_HEIGHT - totalTankHeight; - int spacing = visibleTanks > 1 ? totalSpacing / (visibleTanks + 1) : totalSpacing / 2; - - for (int i = outputFluidScrollOffset; i < Math.min(totalTanks, outputFluidScrollOffset + visibleTanks); i++) { - int tankIndex = i; - int tankY = y + FLUID_OUTPUT_AREA_Y + spacing + (i - outputFluidScrollOffset) * (FLUID_TANK_HEIGHT + spacing); - int startX = x + FLUID_OUTPUT_AREA_X + (FLUID_OUTPUT_AREA_WIDTH - totalWidth) / 2; - int tankX = startX; - - if (isMouseOverArea((int)mouseX, (int)mouseY, tankX, tankY, FLUID_TANK_WIDTH, FLUID_TANK_HEIGHT)) { - handleFluidInteraction(false, button, tankIndex); - return; - } - } - } - } - - // 处理单个流体槽的清空按钮点击 - private void handleTankClearButton(int tankIndex, boolean isInput) { - if (minecraft == null || minecraft.player == null) return; - - // 发送数据包清空指定槽位的流体,区分输入输出区域 - UselessMod.NETWORK.sendToServer(new ClearFluidPacket( - menu.getBlockEntity().getBlockPos(), - isInput, - tankIndex - )); - } - - - private void handleFluidInteraction(boolean isInputTank, int button) { - handleFluidInteraction(isInputTank, button, 0); - } - - private void handleFluidInteraction(boolean isInputTank, int button, int tankIndex) { - if (minecraft == null || minecraft.player == null) return; - - ItemStack carried = menu.getCarried(); - if (carried.isEmpty()) return; - - boolean isFill = (button == 1); - - if (!isInputTank && isFill) { - return; - } - - UselessMod.NETWORK.sendToServer(new FluidInteractionPacket( - menu.getBlockEntity().getBlockPos(), - isInputTank, - isFill, - button, - tankIndex - )); - } - - private boolean isMouseOverArea(int mouseX, int mouseY, int x, int y, int width, int height) { - return mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height; - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/blocks/oregenerator/OreGeneratorBlock.java b/src/main/java/com/sorrowmist/useless/blocks/oregenerator/OreGeneratorBlock.java deleted file mode 100644 index 2d43bf91..00000000 --- a/src/main/java/com/sorrowmist/useless/blocks/oregenerator/OreGeneratorBlock.java +++ /dev/null @@ -1,144 +0,0 @@ -package com.sorrowmist.useless.blocks.oregenerator; - -import com.sorrowmist.useless.UselessMod; -import net.minecraft.world.Containers; -import net.minecraft.world.InteractionHand; -import net.minecraft.world.InteractionResult; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.item.BlockItem; -import net.minecraft.world.item.Item; -import net.minecraft.world.item.ItemStack; -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.state.BlockBehaviour; -import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.level.block.entity.BlockEntityTicker; -import net.minecraft.world.level.block.entity.BlockEntityType; -import net.minecraft.core.BlockPos; -import net.minecraft.world.level.Level; -import net.minecraft.world.phys.BlockHitResult; -import net.minecraftforge.common.capabilities.ForgeCapabilities; -import net.minecraftforge.registries.DeferredRegister; -import net.minecraftforge.registries.ForgeRegistries; -import net.minecraftforge.registries.RegistryObject; -import org.jetbrains.annotations.Nullable; - -public class OreGeneratorBlock extends Block implements EntityBlock { - // 在方块类内部直接定义注册器 - public static final DeferredRegister BLOCKS = DeferredRegister.create(ForgeRegistries.BLOCKS, UselessMod.MOD_ID); - public static final DeferredRegister ITEMS = DeferredRegister.create(ForgeRegistries.ITEMS, UselessMod.MOD_ID); - - // 注册方块和物品 - public static final RegistryObject ORE_GENERATOR_BLOCK = BLOCKS.register("ore_generator_block", - () -> new OreGeneratorBlock()); - - public static final RegistryObject ORE_GENERATOR_BLOCK_ITEM = ITEMS.register("ore_generator_block", - () -> new BlockItem(ORE_GENERATOR_BLOCK.get(), new Item.Properties())); - - - // 方块构造函数 - public OreGeneratorBlock() { - super(BlockBehaviour.Properties.of() - .strength(3.0F, 32768.0F) - .requiresCorrectToolForDrops()); - } - - - - - - @Override - public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { - if (!level.isClientSide) { - BlockEntity blockEntity = level.getBlockEntity(pos); - if (blockEntity instanceof OreGeneratorBlockEntity oreGenerator) { - - ItemStack handItem = player.getItemInHand(hand); - // 检查是否空手且按住Shift - if (player.isShiftKeyDown() && player.getItemInHand(hand).isEmpty()) { - // 取出物品 - oreGenerator.getCapability(ForgeCapabilities.ITEM_HANDLER).ifPresent(handler -> { - ItemStack stackInSlot = handler.getStackInSlot(0); - if (!stackInSlot.isEmpty()) { - // 将物品给玩家 - if (!player.getInventory().add(stackInSlot)) { - // 如果玩家物品栏满了,掉落在地上 - Containers.dropItemStack(level, pos.getX(), pos.getY(), pos.getZ(), stackInSlot); - } - // 清空槽位 - handler.extractItem(0, stackInSlot.getCount(), false); - } - }); - return InteractionResult.SUCCESS; - - } - else if (!handItem.isEmpty() && !player.isShiftKeyDown()) { - oreGenerator.getCapability(ForgeCapabilities.ITEM_HANDLER).ifPresent(handler -> { - ItemStack stackInSlot = handler.getStackInSlot(0); - - // 如果槽位为空,可以放入物品 - if (stackInSlot.isEmpty()) { - // 复制手持物品,但限制最大堆叠数为64 - ItemStack toInsert = handItem.copy(); - toInsert.setCount(Math.min(toInsert.getCount(), 64)); - - // 放入物品 - handler.insertItem(0, toInsert, false); - - // 减少玩家手持物品数量 - handItem.shrink(toInsert.getCount()); - } - // 如果槽位已有物品且是同一种类,可以合并 - else if (ItemStack.isSameItemSameTags(stackInSlot, handItem) && stackInSlot.getCount() < 64) { - int spaceLeft = 64 - stackInSlot.getCount(); - int toAdd = Math.min(handItem.getCount(), spaceLeft); - - if (toAdd > 0) { - // 增加槽位物品数量 - stackInSlot.grow(toAdd); - handler.extractItem(0, stackInSlot.getCount(), false); // 先取出 - handler.insertItem(0, stackInSlot, false); // 再放入更新后的 - - // 减少玩家手持物品数量 - handItem.shrink(toAdd); - } - } - }); - return InteractionResult.SUCCESS; - } - } - } - return InteractionResult.sidedSuccess(level.isClientSide); - } - - - @Override - public void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean isMoving) { - if (!state.is(newState.getBlock())) { - BlockEntity blockEntity = level.getBlockEntity(pos); - if (blockEntity instanceof OreGeneratorBlockEntity generator) { - // 掉落方块实体中的物品 - generator.drops(); - } - } - super.onRemove(state, level, pos, newState, isMoving); - } - - - @Nullable - @Override - public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { - return new OreGeneratorBlockEntity(pos, state); - } - - @Nullable - @Override - public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType type) { - return level.isClientSide ? null : (lvl, pos, st, be) -> { - if (be instanceof OreGeneratorBlockEntity generator) { - generator.tick(); - } - }; - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/blocks/oregenerator/OreGeneratorBlockEntity.java b/src/main/java/com/sorrowmist/useless/blocks/oregenerator/OreGeneratorBlockEntity.java deleted file mode 100644 index 13da536c..00000000 --- a/src/main/java/com/sorrowmist/useless/blocks/oregenerator/OreGeneratorBlockEntity.java +++ /dev/null @@ -1,148 +0,0 @@ -package com.sorrowmist.useless.blocks.oregenerator; - -import com.sorrowmist.useless.blocks.ModBlockEntities; -import net.minecraft.world.Containers; -import net.minecraft.world.SimpleContainer; -import net.minecraft.world.level.block.entity.BlockEntity; -import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.item.ItemStack; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.core.Direction; -import net.minecraft.core.BlockPos; -import net.minecraftforge.common.capabilities.Capability; -import net.minecraftforge.common.capabilities.ForgeCapabilities; -import net.minecraftforge.common.util.LazyOptional; -import net.minecraftforge.items.IItemHandler; -import net.minecraftforge.items.ItemStackHandler; -import net.minecraftforge.registries.ForgeRegistries; -import net.minecraft.tags.TagKey; -import net.minecraft.resources.ResourceLocation; -import net.minecraftforge.registries.tags.ITagManager; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.Optional; - -public class OreGeneratorBlockEntity extends BlockEntity { - - private final ItemStackHandler itemHandler = new ItemStackHandler(1) { - @Override - protected void onContentsChanged(int slot) { - setChanged(); - } - }; - - private final LazyOptional lazyItemHandler = LazyOptional.of(() -> itemHandler); - - public OreGeneratorBlockEntity(BlockPos pos, BlockState state) { - super(ModBlockEntities.ORE_GENERATOR.get(), pos, state); - } - - public void tick() { - if (level == null || level.isClientSide) return; - - ItemStack inputStack = itemHandler.getStackInSlot(0); - if (inputStack.isEmpty()) return; - - // 检查是否为 raw_materials 或 ores - if (!isValidInput(inputStack)) return; - - int inputCount = inputStack.getCount(); - long outputCount = (long) Math.pow(inputCount, 3); - - // 防止数量过大 - if (outputCount > Integer.MAX_VALUE) outputCount = Integer.MAX_VALUE; - - // 创建新的物品堆,而不是使用输入槽的物品 - ItemStack outputStack = new ItemStack(inputStack.getItem(), (int) outputCount); - - // 尝试输出到相邻容器 - if (tryExportItem(outputStack)) { - // 输出成功,但不清空输入槽(输入槽只是模板) - // 这样输入槽的物品会一直存在,持续生成输出 - } - } - - private boolean isValidInput(ItemStack stack) { - ITagManager tags = ForgeRegistries.ITEMS.tags(); - if (tags == null) return false; - - TagKey rawMaterialsTag = TagKey.create(ForgeRegistries.ITEMS.getRegistryKey(),ResourceLocation.fromNamespaceAndPath("forge", "raw_materials")); - TagKey oresTag = TagKey.create(ForgeRegistries.ITEMS.getRegistryKey(), ResourceLocation.fromNamespaceAndPath("forge", "ores")); - TagKey GTTag = TagKey.create(ForgeRegistries.ITEMS.getRegistryKey(), ResourceLocation.fromNamespaceAndPath("gt", "raw_materials")); - - return stack.is(rawMaterialsTag) || stack.is(oresTag) || stack.is(GTTag); - } - - private boolean tryExportItem(ItemStack stack) { - if (level == null) return false; - - for (Direction direction : Direction.values()) { - BlockPos adjacentPos = worldPosition.relative(direction); - BlockEntity adjacentBE = level.getBlockEntity(adjacentPos); - - if (adjacentBE != null) { - // 黑名单检查:跳过相邻的OreGeneratorBlockEntity,防止物品在矿石生成器之间无限循环 - if (adjacentBE instanceof OreGeneratorBlockEntity) { - continue; - } - - Optional handler = adjacentBE.getCapability(ForgeCapabilities.ITEM_HANDLER, direction.getOpposite()).resolve(); - if (handler.isPresent()) { - IItemHandler targetHandler = handler.get(); - // 尝试将新生成的物品插入相邻容器 - ItemStack remainder = stack.copy(); - for (int i = 0; i < targetHandler.getSlots(); i++) { - remainder = targetHandler.insertItem(i, remainder, false); - if (remainder.isEmpty()) { - return true; - } - } - // 如果部分插入成功,继续尝试其他方向 - if (remainder.getCount() < stack.getCount()) { - // 部分成功,继续处理剩余物品 - stack.setCount(remainder.getCount()); - } - } - } - } - return false; - } - - @Override - public void load(CompoundTag tag) { - super.load(tag); - itemHandler.deserializeNBT(tag.getCompound("Inventory")); - } - - @Override - protected void saveAdditional(CompoundTag tag) { - super.saveAdditional(tag); - tag.put("Inventory", itemHandler.serializeNBT()); - } - - @Nonnull - @Override - public LazyOptional getCapability(@Nonnull Capability cap, @Nullable Direction side) { - if (cap == ForgeCapabilities.ITEM_HANDLER) { - return lazyItemHandler.cast(); - } - return super.getCapability(cap, side); - } - - @Override - public void invalidateCaps() { - super.invalidateCaps(); - lazyItemHandler.invalidate(); - } - - - public void drops() { - SimpleContainer inventory = new SimpleContainer(itemHandler.getSlots()); - for (int i = 0; i < itemHandler.getSlots(); i++) { - inventory.setItem(i, itemHandler.getStackInSlot(i)); - } - - Containers.dropContents(this.level, this.worldPosition, inventory); - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/blocks/teleport/TeleportBlock.java b/src/main/java/com/sorrowmist/useless/blocks/teleport/TeleportBlock.java deleted file mode 100644 index a2288af8..00000000 --- a/src/main/java/com/sorrowmist/useless/blocks/teleport/TeleportBlock.java +++ /dev/null @@ -1,228 +0,0 @@ -package com.sorrowmist.useless.blocks.teleport; - -import com.sorrowmist.useless.UselessMod; -import net.minecraft.core.BlockPos; -import net.minecraft.resources.ResourceKey; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.server.level.TicketType; -import net.minecraft.world.InteractionHand; -import net.minecraft.world.InteractionResult; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.item.BlockItem; -import net.minecraft.world.item.Item; -import net.minecraft.world.level.ChunkPos; -import net.minecraft.world.level.Level; -import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.block.state.BlockBehaviour; -import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.phys.BlockHitResult; -import net.minecraftforge.registries.DeferredRegister; -import net.minecraftforge.registries.ForgeRegistries; -import net.minecraftforge.registries.RegistryObject; - -public class TeleportBlock { - public static final DeferredRegister BLOCKS = DeferredRegister.create(ForgeRegistries.BLOCKS, UselessMod.MOD_ID); - public static final DeferredRegister ITEMS = DeferredRegister.create(ForgeRegistries.ITEMS, UselessMod.MOD_ID); - - // 维度ID - private static final ResourceLocation USELESSDIM_ID = ResourceLocation.fromNamespaceAndPath(UselessMod.MOD_ID, "uselessdim"); - private static final ResourceKey USELESSDIM_KEY = ResourceKey.create(net.minecraft.core.registries.Registries.DIMENSION, USELESSDIM_ID); - - - public static final RegistryObject TELEPORT_BLOCK = BLOCKS.register("teleport_block", - () -> new Block(BlockBehaviour.Properties.of() - .strength(2.0f,65536.0f) - .requiresCorrectToolForDrops()) { - @Override - public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { - if (!level.isClientSide() && player instanceof ServerPlayer serverPlayer) { - // 异步处理传送,避免阻塞主线程 - level.getServer().execute(() -> { - handleTeleport(serverPlayer, pos); - }); - } - return InteractionResult.sidedSuccess(level.isClientSide()); - } - }); - - public static final RegistryObject TELEPORT_BLOCK_ITEM = ITEMS.register("teleport_block", - () -> new BlockItem(TELEPORT_BLOCK.get(), new Item.Properties())); - - private static void handleTeleport(ServerPlayer player, BlockPos sourcePos) { - Level currentLevel = player.level(); - ResourceKey currentDimension = currentLevel.dimension(); - - // 判断当前维度 - boolean isInUselessDim = currentDimension.location().equals(USELESSDIM_ID); - ResourceKey targetDimension = isInUselessDim ? Level.OVERWORLD : USELESSDIM_KEY; - - ServerLevel targetLevel = player.server.getLevel(targetDimension); - if (targetLevel == null) { - player.displayClientMessage(net.minecraft.network.chat.Component.literal("目标维度未加载"), false); - return; - } - - processTeleport(player, sourcePos, targetLevel); - } - - private static void processTeleport(ServerPlayer player, BlockPos sourcePos, ServerLevel targetLevel) { - // 预加载目标区块(但不强制保持加载) - ChunkPos targetChunkPos = new ChunkPos(sourcePos); - - // 只预加载区块,不强制保持 - targetLevel.getChunk(sourcePos.getX() >> 4, sourcePos.getZ() >> 4); - - // 查找或创建目标传送方块 - BlockPos targetPos = findOrCreateTeleportBlock(targetLevel, sourcePos); - if (targetPos == null) { - player.displayClientMessage(net.minecraft.network.chat.Component.literal("传送失败,请在其他位置尝试"), false); - return; - } - - // 使用轻量级的区块票证(只在传送瞬间保持加载) - targetLevel.getChunkSource().addRegionTicket( - TicketType.PORTAL, - new ChunkPos(targetPos), - 1, // 减少加载范围 - targetPos - ); - - // 立即执行传送 - player.teleportTo(targetLevel, - targetPos.getX() + 0.5, - targetPos.getY() + 1.0, - targetPos.getZ() + 0.5, - player.getYRot(), - player.getXRot()); - - player.displayClientMessage(net.minecraft.network.chat.Component.literal("传送成功!"), false); - - // 不再需要强制加载清理,让Minecraft自然管理区块 - } - - private static BlockPos findOrCreateTeleportBlock(ServerLevel level, BlockPos sourcePos) { - int searchRadius = 16; - BlockPos.MutableBlockPos searchPos = new BlockPos.MutableBlockPos(); - - // 优先搜索同一Y层和相邻层 - int[] priorityYLevels = { - sourcePos.getY(), // 同一层 (最高优先级) - sourcePos.getY() - 1, // 下一层 (高优先级) - sourcePos.getY() + 1, // 上一层 (高优先级) - sourcePos.getY() - 2, // 下两层 - sourcePos.getY() + 2, // 上两层 - sourcePos.getY() - 3, // 下三层 - sourcePos.getY() + 3 // 上三层 - }; - - // 先搜索优先级高的层次 - for (int y : priorityYLevels) { - if (y < level.getMinBuildHeight() || y >= level.getMaxBuildHeight()) { - continue; - } - - BlockPos found = searchAtYLevel(level, sourcePos, y, searchRadius); - if (found != null) { - return found; - } - } - - // 如果优先级层次没找到,向下扩展搜索(地下可能有很多传送方块) - for (int y = sourcePos.getY() - 4; y >= level.getMinBuildHeight() + 1; y--) { - BlockPos found = searchAtYLevel(level, sourcePos, y, searchRadius); - if (found != null) { - return found; - } - } - - // 最后向上搜索(作为备选) - for (int y = sourcePos.getY() + 4; y < level.getMaxBuildHeight() - 10; y++) { - BlockPos found = searchAtYLevel(level, sourcePos, y, searchRadius); - if (found != null) { - return found; - } - } - - return createTeleportBlockFast(level, sourcePos); - } - - private static BlockPos searchAtYLevel(ServerLevel level, BlockPos center, int y, int radius) { - BlockPos.MutableBlockPos searchPos = new BlockPos.MutableBlockPos(); - - // 从内向外搜索 - for (int currentRadius = 0; currentRadius <= radius; currentRadius++) { - // 搜索当前半径的边界 - for (int x = -currentRadius; x <= currentRadius; x++) { - for (int z = -currentRadius; z <= currentRadius; z++) { - // 只搜索边界,避免重复搜索内部 - if (Math.abs(x) != currentRadius && Math.abs(z) != currentRadius) { - continue; - } - - searchPos.set(center.getX() + x, y, center.getZ() + z); - - if (isValidTeleportBlock(level, searchPos)) { - return searchPos.immutable(); - } - } - } - } - - return null; - } - - private static boolean isValidTeleportBlock(ServerLevel level, BlockPos pos) { - // 确保区块已加载 - if (!level.isLoaded(pos)) { - // 如果区块未加载,尝试快速加载 - ChunkPos chunkPos = new ChunkPos(pos); - if (!level.getChunkSource().hasChunk(chunkPos.x, chunkPos.z)) { - return false; // 不强制加载未加载的区块 - } - } - - return level.getBlockState(pos).getBlock() == TELEPORT_BLOCK.get(); - } - - private static BlockPos createTeleportBlockFast(ServerLevel level, BlockPos sourcePos) { - // 尝试几个已知的安全高度 - int[] safeHeights = {64, 80, 96, 112, 128}; - - for (int height : safeHeights) { - BlockPos testPos = new BlockPos(sourcePos.getX(), height, sourcePos.getZ()); - - if (canPlaceTeleportBlockFast(level, testPos)) { - level.setBlock(testPos, TELEPORT_BLOCK.get().defaultBlockState(), 3); - return testPos; - } - } - - // 如果预设高度都不行,使用原版的世界生成高度 - BlockPos worldSurfacePos = level.getHeightmapPos(net.minecraft.world.level.levelgen.Heightmap.Types.MOTION_BLOCKING, sourcePos); - BlockPos placementPos = worldSurfacePos.above(2); // 在地表上方2格 - - if (canPlaceTeleportBlockFast(level, placementPos)) { - level.setBlock(placementPos, TELEPORT_BLOCK.get().defaultBlockState(), 3); - return placementPos; - } - - // 最后尝试:直接在地表创建 - level.setBlock(placementPos, TELEPORT_BLOCK.get().defaultBlockState(), 3); - // 确保玩家不会掉下去 - level.setBlock(placementPos.below(), net.minecraft.world.level.block.Blocks.STONE.defaultBlockState(), 3); - - return placementPos; - } - - private static boolean canPlaceTeleportBlockFast(ServerLevel level, BlockPos pos) { - if (pos.getY() < level.getMinBuildHeight() || pos.getY() >= level.getMaxBuildHeight()) { - return false; - } - - // 快速检查:位置是否可替换且下方有支撑 - return level.getBlockState(pos).canBeReplaced() && - level.getBlockState(pos.below()).isSolid(); - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/blocks/teleport/TeleportBlock2.java b/src/main/java/com/sorrowmist/useless/blocks/teleport/TeleportBlock2.java deleted file mode 100644 index 7e04c1fa..00000000 --- a/src/main/java/com/sorrowmist/useless/blocks/teleport/TeleportBlock2.java +++ /dev/null @@ -1,228 +0,0 @@ -package com.sorrowmist.useless.blocks.teleport; - -import com.sorrowmist.useless.UselessMod; -import net.minecraft.core.BlockPos; -import net.minecraft.resources.ResourceKey; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.server.level.TicketType; -import net.minecraft.world.InteractionHand; -import net.minecraft.world.InteractionResult; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.item.BlockItem; -import net.minecraft.world.item.Item; -import net.minecraft.world.level.ChunkPos; -import net.minecraft.world.level.Level; -import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.block.state.BlockBehaviour; -import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.phys.BlockHitResult; -import net.minecraftforge.registries.DeferredRegister; -import net.minecraftforge.registries.ForgeRegistries; -import net.minecraftforge.registries.RegistryObject; - -public class TeleportBlock2 { - public static final DeferredRegister BLOCKS = DeferredRegister.create(ForgeRegistries.BLOCKS, UselessMod.MOD_ID); - public static final DeferredRegister ITEMS = DeferredRegister.create(ForgeRegistries.ITEMS, UselessMod.MOD_ID); - - // 维度ID - private static final ResourceLocation USELESSDIM_ID = ResourceLocation.fromNamespaceAndPath(UselessMod.MOD_ID, "uselessdim2"); - private static final ResourceKey USELESSDIM_KEY = ResourceKey.create(net.minecraft.core.registries.Registries.DIMENSION, USELESSDIM_ID); - - - public static final RegistryObject TELEPORT_BLOCK_2 = BLOCKS.register("teleport_block_2", - () -> new Block(BlockBehaviour.Properties.of() - .strength(2.0f,65536.0f) - .requiresCorrectToolForDrops()) { - @Override - public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { - if (!level.isClientSide() && player instanceof ServerPlayer serverPlayer) { - // 异步处理传送,避免阻塞主线程 - level.getServer().execute(() -> { - handleTeleport(serverPlayer, pos); - }); - } - return InteractionResult.sidedSuccess(level.isClientSide()); - } - }); - - public static final RegistryObject TELEPORT_BLOCK_ITEM_2 = ITEMS.register("teleport_block_2", - () -> new BlockItem(TELEPORT_BLOCK_2.get(), new Item.Properties())); - - private static void handleTeleport(ServerPlayer player, BlockPos sourcePos) { - Level currentLevel = player.level(); - ResourceKey currentDimension = currentLevel.dimension(); - - // 判断当前维度 - boolean isInUselessDim = currentDimension.location().equals(USELESSDIM_ID); - ResourceKey targetDimension = isInUselessDim ? Level.OVERWORLD : USELESSDIM_KEY; - - ServerLevel targetLevel = player.server.getLevel(targetDimension); - if (targetLevel == null) { - player.displayClientMessage(net.minecraft.network.chat.Component.literal("目标维度未加载"), false); - return; - } - - processTeleport(player, sourcePos, targetLevel); - } - - private static void processTeleport(ServerPlayer player, BlockPos sourcePos, ServerLevel targetLevel) { - // 预加载目标区块(但不强制保持加载) - ChunkPos targetChunkPos = new ChunkPos(sourcePos); - - // 只预加载区块,不强制保持 - targetLevel.getChunk(sourcePos.getX() >> 4, sourcePos.getZ() >> 4); - - // 查找或创建目标传送方块 - BlockPos targetPos = findOrCreateTeleportBlock(targetLevel, sourcePos); - if (targetPos == null) { - player.displayClientMessage(net.minecraft.network.chat.Component.literal("传送失败,请在其他位置尝试"), false); - return; - } - - // 使用轻量级的区块票证(只在传送瞬间保持加载) - targetLevel.getChunkSource().addRegionTicket( - TicketType.PORTAL, - new ChunkPos(targetPos), - 1, // 减少加载范围 - targetPos - ); - - // 立即执行传送 - player.teleportTo(targetLevel, - targetPos.getX() + 0.5, - targetPos.getY() + 1.0, - targetPos.getZ() + 0.5, - player.getYRot(), - player.getXRot()); - - player.displayClientMessage(net.minecraft.network.chat.Component.literal("传送成功!"), false); - - // 不再需要强制加载清理,让Minecraft自然管理区块 - } - - private static BlockPos findOrCreateTeleportBlock(ServerLevel level, BlockPos sourcePos) { - int searchRadius = 16; - BlockPos.MutableBlockPos searchPos = new BlockPos.MutableBlockPos(); - - // 优先搜索同一Y层和相邻层 - int[] priorityYLevels = { - sourcePos.getY(), // 同一层 (最高优先级) - sourcePos.getY() - 1, // 下一层 (高优先级) - sourcePos.getY() + 1, // 上一层 (高优先级) - sourcePos.getY() - 2, // 下两层 - sourcePos.getY() + 2, // 上两层 - sourcePos.getY() - 3, // 下三层 - sourcePos.getY() + 3 // 上三层 - }; - - // 先搜索优先级高的层次 - for (int y : priorityYLevels) { - if (y < level.getMinBuildHeight() || y >= level.getMaxBuildHeight()) { - continue; - } - - BlockPos found = searchAtYLevel(level, sourcePos, y, searchRadius); - if (found != null) { - return found; - } - } - - // 如果优先级层次没找到,向下扩展搜索(地下可能有很多传送方块) - for (int y = sourcePos.getY() - 4; y >= level.getMinBuildHeight() + 1; y--) { - BlockPos found = searchAtYLevel(level, sourcePos, y, searchRadius); - if (found != null) { - return found; - } - } - - // 最后向上搜索(作为备选) - for (int y = sourcePos.getY() + 4; y < level.getMaxBuildHeight() - 10; y++) { - BlockPos found = searchAtYLevel(level, sourcePos, y, searchRadius); - if (found != null) { - return found; - } - } - - return createTeleportBlockFast(level, sourcePos); - } - - private static BlockPos searchAtYLevel(ServerLevel level, BlockPos center, int y, int radius) { - BlockPos.MutableBlockPos searchPos = new BlockPos.MutableBlockPos(); - - // 从内向外搜索 - for (int currentRadius = 0; currentRadius <= radius; currentRadius++) { - // 搜索当前半径的边界 - for (int x = -currentRadius; x <= currentRadius; x++) { - for (int z = -currentRadius; z <= currentRadius; z++) { - // 只搜索边界,避免重复搜索内部 - if (Math.abs(x) != currentRadius && Math.abs(z) != currentRadius) { - continue; - } - - searchPos.set(center.getX() + x, y, center.getZ() + z); - - if (isValidTeleportBlock(level, searchPos)) { - return searchPos.immutable(); - } - } - } - } - - return null; - } - - private static boolean isValidTeleportBlock(ServerLevel level, BlockPos pos) { - // 确保区块已加载 - if (!level.isLoaded(pos)) { - // 如果区块未加载,尝试快速加载 - ChunkPos chunkPos = new ChunkPos(pos); - if (!level.getChunkSource().hasChunk(chunkPos.x, chunkPos.z)) { - return false; // 不强制加载未加载的区块 - } - } - - return level.getBlockState(pos).getBlock() == TELEPORT_BLOCK_2.get(); - } - - private static BlockPos createTeleportBlockFast(ServerLevel level, BlockPos sourcePos) { - // 尝试几个已知的安全高度 - int[] safeHeights = {64, 80, 96, 112, 128}; - - for (int height : safeHeights) { - BlockPos testPos = new BlockPos(sourcePos.getX(), height, sourcePos.getZ()); - - if (canPlaceTeleportBlockFast(level, testPos)) { - level.setBlock(testPos, TELEPORT_BLOCK_2.get().defaultBlockState(), 3); - return testPos; - } - } - - // 如果预设高度都不行,使用原版的世界生成高度 - BlockPos worldSurfacePos = level.getHeightmapPos(net.minecraft.world.level.levelgen.Heightmap.Types.MOTION_BLOCKING, sourcePos); - BlockPos placementPos = worldSurfacePos.above(2); // 在地表上方2格 - - if (canPlaceTeleportBlockFast(level, placementPos)) { - level.setBlock(placementPos, TELEPORT_BLOCK_2.get().defaultBlockState(), 3); - return placementPos; - } - - // 最后尝试:直接在地表创建 - level.setBlock(placementPos, TELEPORT_BLOCK_2.get().defaultBlockState(), 3); - // 确保玩家不会掉下去 - level.setBlock(placementPos.below(), net.minecraft.world.level.block.Blocks.STONE.defaultBlockState(), 3); - - return placementPos; - } - - private static boolean canPlaceTeleportBlockFast(ServerLevel level, BlockPos pos) { - if (pos.getY() < level.getMinBuildHeight() || pos.getY() >= level.getMaxBuildHeight()) { - return false; - } - - // 快速检查:位置是否可替换且下方有支撑 - return level.getBlockState(pos).canBeReplaced() && - level.getBlockState(pos.below()).isSolid(); - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/blocks/teleport/TeleportBlock3.java b/src/main/java/com/sorrowmist/useless/blocks/teleport/TeleportBlock3.java deleted file mode 100644 index 590411c1..00000000 --- a/src/main/java/com/sorrowmist/useless/blocks/teleport/TeleportBlock3.java +++ /dev/null @@ -1,228 +0,0 @@ -package com.sorrowmist.useless.blocks.teleport; - -import com.sorrowmist.useless.UselessMod; -import net.minecraft.core.BlockPos; -import net.minecraft.resources.ResourceKey; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.server.level.TicketType; -import net.minecraft.world.InteractionHand; -import net.minecraft.world.InteractionResult; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.item.BlockItem; -import net.minecraft.world.item.Item; -import net.minecraft.world.level.ChunkPos; -import net.minecraft.world.level.Level; -import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.block.state.BlockBehaviour; -import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.phys.BlockHitResult; -import net.minecraftforge.registries.DeferredRegister; -import net.minecraftforge.registries.ForgeRegistries; -import net.minecraftforge.registries.RegistryObject; - -public class TeleportBlock3 { - public static final DeferredRegister BLOCKS = DeferredRegister.create(ForgeRegistries.BLOCKS, UselessMod.MOD_ID); - public static final DeferredRegister ITEMS = DeferredRegister.create(ForgeRegistries.ITEMS, UselessMod.MOD_ID); - - // 维度ID - private static final ResourceLocation USELESSDIM_ID = ResourceLocation.fromNamespaceAndPath(UselessMod.MOD_ID, "uselessdim3"); - private static final ResourceKey USELESSDIM_KEY = ResourceKey.create(net.minecraft.core.registries.Registries.DIMENSION, USELESSDIM_ID); - - - public static final RegistryObject TELEPORT_BLOCK_3 = BLOCKS.register("teleport_block_3", - () -> new Block(BlockBehaviour.Properties.of() - .strength(2.0f,65536.0f) - .requiresCorrectToolForDrops()) { - @Override - public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { - if (!level.isClientSide() && player instanceof ServerPlayer serverPlayer) { - // 异步处理传送,避免阻塞主线程 - level.getServer().execute(() -> { - handleTeleport(serverPlayer, pos); - }); - } - return InteractionResult.sidedSuccess(level.isClientSide()); - } - }); - - public static final RegistryObject TELEPORT_BLOCK_ITEM_3 = ITEMS.register("teleport_block_3", - () -> new BlockItem(TELEPORT_BLOCK_3.get(), new Item.Properties())); - - private static void handleTeleport(ServerPlayer player, BlockPos sourcePos) { - Level currentLevel = player.level(); - ResourceKey currentDimension = currentLevel.dimension(); - - // 判断当前维度 - boolean isInUselessDim = currentDimension.location().equals(USELESSDIM_ID); - ResourceKey targetDimension = isInUselessDim ? Level.OVERWORLD : USELESSDIM_KEY; - - ServerLevel targetLevel = player.server.getLevel(targetDimension); - if (targetLevel == null) { - player.displayClientMessage(net.minecraft.network.chat.Component.literal("目标维度未加载"), false); - return; - } - - processTeleport(player, sourcePos, targetLevel); - } - - private static void processTeleport(ServerPlayer player, BlockPos sourcePos, ServerLevel targetLevel) { - // 预加载目标区块(但不强制保持加载) - ChunkPos targetChunkPos = new ChunkPos(sourcePos); - - // 只预加载区块,不强制保持 - targetLevel.getChunk(sourcePos.getX() >> 4, sourcePos.getZ() >> 4); - - // 查找或创建目标传送方块 - BlockPos targetPos = findOrCreateTeleportBlock(targetLevel, sourcePos); - if (targetPos == null) { - player.displayClientMessage(net.minecraft.network.chat.Component.literal("传送失败,请在其他位置尝试"), false); - return; - } - - // 使用轻量级的区块票证(只在传送瞬间保持加载) - targetLevel.getChunkSource().addRegionTicket( - TicketType.PORTAL, - new ChunkPos(targetPos), - 1, // 减少加载范围 - targetPos - ); - - // 立即执行传送 - player.teleportTo(targetLevel, - targetPos.getX() + 0.5, - targetPos.getY() + 1.0, - targetPos.getZ() + 0.5, - player.getYRot(), - player.getXRot()); - - player.displayClientMessage(net.minecraft.network.chat.Component.literal("传送成功!"), false); - - // 不再需要强制加载清理,让Minecraft自然管理区块 - } - - private static BlockPos findOrCreateTeleportBlock(ServerLevel level, BlockPos sourcePos) { - int searchRadius = 16; - BlockPos.MutableBlockPos searchPos = new BlockPos.MutableBlockPos(); - - // 优先搜索同一Y层和相邻层 - int[] priorityYLevels = { - sourcePos.getY(), // 同一层 (最高优先级) - sourcePos.getY() - 1, // 下一层 (高优先级) - sourcePos.getY() + 1, // 上一层 (高优先级) - sourcePos.getY() - 2, // 下两层 - sourcePos.getY() + 2, // 上两层 - sourcePos.getY() - 3, // 下三层 - sourcePos.getY() + 3 // 上三层 - }; - - // 先搜索优先级高的层次 - for (int y : priorityYLevels) { - if (y < level.getMinBuildHeight() || y >= level.getMaxBuildHeight()) { - continue; - } - - BlockPos found = searchAtYLevel(level, sourcePos, y, searchRadius); - if (found != null) { - return found; - } - } - - // 如果优先级层次没找到,向下扩展搜索(地下可能有很多传送方块) - for (int y = sourcePos.getY() - 4; y >= level.getMinBuildHeight() + 1; y--) { - BlockPos found = searchAtYLevel(level, sourcePos, y, searchRadius); - if (found != null) { - return found; - } - } - - // 最后向上搜索(作为备选) - for (int y = sourcePos.getY() + 4; y < level.getMaxBuildHeight() - 10; y++) { - BlockPos found = searchAtYLevel(level, sourcePos, y, searchRadius); - if (found != null) { - return found; - } - } - - return createTeleportBlockFast(level, sourcePos); - } - - private static BlockPos searchAtYLevel(ServerLevel level, BlockPos center, int y, int radius) { - BlockPos.MutableBlockPos searchPos = new BlockPos.MutableBlockPos(); - - // 从内向外搜索 - for (int currentRadius = 0; currentRadius <= radius; currentRadius++) { - // 搜索当前半径的边界 - for (int x = -currentRadius; x <= currentRadius; x++) { - for (int z = -currentRadius; z <= currentRadius; z++) { - // 只搜索边界,避免重复搜索内部 - if (Math.abs(x) != currentRadius && Math.abs(z) != currentRadius) { - continue; - } - - searchPos.set(center.getX() + x, y, center.getZ() + z); - - if (isValidTeleportBlock(level, searchPos)) { - return searchPos.immutable(); - } - } - } - } - - return null; - } - - private static boolean isValidTeleportBlock(ServerLevel level, BlockPos pos) { - // 确保区块已加载 - if (!level.isLoaded(pos)) { - // 如果区块未加载,尝试快速加载 - ChunkPos chunkPos = new ChunkPos(pos); - if (!level.getChunkSource().hasChunk(chunkPos.x, chunkPos.z)) { - return false; // 不强制加载未加载的区块 - } - } - - return level.getBlockState(pos).getBlock() == TELEPORT_BLOCK_3.get(); - } - - private static BlockPos createTeleportBlockFast(ServerLevel level, BlockPos sourcePos) { - // 尝试几个已知的安全高度 - int[] safeHeights = {64, 80, 96, 112, 128}; - - for (int height : safeHeights) { - BlockPos testPos = new BlockPos(sourcePos.getX(), height, sourcePos.getZ()); - - if (canPlaceTeleportBlockFast(level, testPos)) { - level.setBlock(testPos, TELEPORT_BLOCK_3.get().defaultBlockState(), 3); - return testPos; - } - } - - // 如果预设高度都不行,使用原版的世界生成高度 - BlockPos worldSurfacePos = level.getHeightmapPos(net.minecraft.world.level.levelgen.Heightmap.Types.MOTION_BLOCKING, sourcePos); - BlockPos placementPos = worldSurfacePos.above(2); // 在地表上方2格 - - if (canPlaceTeleportBlockFast(level, placementPos)) { - level.setBlock(placementPos, TELEPORT_BLOCK_3.get().defaultBlockState(), 3); - return placementPos; - } - - // 最后尝试:直接在地表创建 - level.setBlock(placementPos, TELEPORT_BLOCK_3.get().defaultBlockState(), 3); - // 确保玩家不会掉下去 - level.setBlock(placementPos.below(), net.minecraft.world.level.block.Blocks.STONE.defaultBlockState(), 3); - - return placementPos; - } - - private static boolean canPlaceTeleportBlockFast(ServerLevel level, BlockPos pos) { - if (pos.getY() < level.getMinBuildHeight() || pos.getY() >= level.getMaxBuildHeight()) { - return false; - } - - // 快速检查:位置是否可替换且下方有支撑 - return level.getBlockState(pos).canBeReplaced() && - level.getBlockState(pos.below()).isSolid(); - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/client/BeefAutoClicker.java b/src/main/java/com/sorrowmist/useless/client/BeefAutoClicker.java new file mode 100644 index 00000000..d41eb97f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/BeefAutoClicker.java @@ -0,0 +1,139 @@ +package com.sorrowmist.useless.client; + +import com.sorrowmist.useless.core.config.ConfigManager; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.EntityHitResult; +import net.minecraft.world.phys.HitResult; + +/** + * 「连点模式」的客户端实现。 + * + *

开关是客户端会话级状态({@link #isEnabled()}),不绑定任何物品 —— 空手或拿着 + * 任意物品都能开启,开启后每个客户端 tick 按配置次数重复触发右键。判定顺序完全复刻 + * {@code Minecraft#startUseItem}:先主手、后副手,每只手先看视线命中(实体 → 方块), + * 命中被消费就结束这一次;都没消费时才兜底走物品自身的 {@code use}(空手没有 {@code use},跳过)。 + * 这样连点触发的是真正的右键交互,造化杖自己的行为链、以及其它模组的 + * 右键功能都会照常生效(例如同时开启催熟模式即可自动反复催熟)。

+ * + *

速率由客户端配置 {@code beef_autoclick_clicks_per_tick} 控制(默认 4 次/tick)。 + * 之所以不复用原版的 {@code rightClickDelay},是因为它会把速率压到约 4 次/秒, + * 达不到「尽可能快」的要求。

+ */ +public final class BeefAutoClicker { + + /** 与 {@code Minecraft#startUseItem} 一致的尝试顺序:主手 → 副手。 */ + private static final InteractionHand[] HANDS = InteractionHand.values(); + + /** 连点开关:客户端会话级,登录时由 {@code ClientEventBusSubscriber} 重置为关。 */ + private static volatile boolean enabled; + + private BeefAutoClicker() { + } + + public static boolean isEnabled() { + return enabled; + } + + public static void setEnabled(boolean value) { + enabled = value; + } + + /** 翻转开关并返回新状态。 */ + public static boolean toggle() { + enabled = !enabled; + return enabled; + } + + /** 登录 / 断线时把开关重置为关,避免状态跨会话残留。 */ + public static void reset() { + enabled = false; + } + + /** 客户端 tick 入口。 */ + public static void tick(Minecraft mc) { + if (!enabled) { + return; + } + LocalPlayer player = mc.player; + if (player == null || mc.level == null || mc.gameMode == null) { + return; + } + // 开着界面 / 游戏暂停时不连点,避免误操作 + if (mc.screen != null || mc.isPaused()) { + return; + } + // 正在吃东西/拉弓/用刷子,或正在挖方块时让位给玩家自己 + if (player.isUsingItem() || player.isHandsBusy() || mc.gameMode.isDestroying()) { + return; + } + + int clicks = ConfigManager.getBeefAutoClickClicksPerTick(); + for (int i = 0; i < clicks; i++) { + if (!performRightClick(mc, player)) { + break; + } + } + } + + /** + * 执行一次右键交互。 + * + *

与 {@code Minecraft#startUseItem} 一致:依次尝试主手、副手。每只手先看视线命中 + * (实体走 {@code interact}、方块走 {@code useItemOn}),命中被消费就结束;都没消费时才兜底走 + * 该手物品自身的 {@code use}。空手不调用 {@code use}(原版亦然),但空手依然会触发 + * 方块 / 实体的交互 —— 这正是「空手连点」的意义所在。

+ * + *

副手一定要试:主手空着、副手拿着物品时,若只走主手就完全不会用到副手那件物品 + * (2026-10-07 用户报的「副手持有物品开启连点没生效」)。

+ * + * @return 本次是否真的触发了一次交互(false 表示当前状态不适合连点,应停止本 tick 的循环) + */ + private static boolean performRightClick(Minecraft mc, LocalPlayer player) { + for (InteractionHand hand : HANDS) { + ItemStack stack = player.getItemInHand(hand); + + HitResult hit = mc.hitResult; + if (hit != null) { + // 1. 命中实体:优先交给实体交互 + if (hit.getType() == HitResult.Type.ENTITY && hit instanceof EntityHitResult entityHit) { + Entity entity = entityHit.getEntity(); + if (!mc.level.getWorldBorder().isWithinBounds(entity.blockPosition())) { + return false; + } + InteractionResult result = mc.gameMode.interact(player, entity, hand); + if (result.consumesAction()) { + player.swing(hand); + return true; + } + } else if (hit.getType() == HitResult.Type.BLOCK && hit instanceof BlockHitResult blockHit) { + // 2. 命中方块:空气方块不触发(与原版一致) + BlockState state = mc.level.getBlockState(blockHit.getBlockPos()); + if (!state.isAir()) { + InteractionResult result = mc.gameMode.useItemOn(player, hand, blockHit); + if (result.consumesAction()) { + player.swing(hand); + return true; + } + } + } + } + + // 3. 兜底:走该手物品自身的 use(例如对空气右键、或方块交互未消费时)。空手没有 use 可用。 + if (!stack.isEmpty()) { + InteractionResult result = mc.gameMode.useItem(player, hand); + if (result.consumesAction()) { + player.swing(hand); + return true; + } + } + } + return true; + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/BeefTooltipKeyHandler.java b/src/main/java/com/sorrowmist/useless/client/BeefTooltipKeyHandler.java new file mode 100644 index 00000000..e115911d --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/BeefTooltipKeyHandler.java @@ -0,0 +1,54 @@ +package com.sorrowmist.useless.client; + +import com.sorrowmist.useless.content.items.EndlessBeafItem; +import net.minecraft.client.Minecraft; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod.EventBusSubscriber; +import net.minecraftforge.client.event.InputEvent; +import org.lwjgl.glfw.GLFW; + +/** + * 造化杖提示翻页的按键入口。 + * + *

翻页键 {@code [} 与 {@code ]} 不注册为 {@code KeyMapping}:它们只在物品提示框 + * 显示期间生效,注册为常规绑定会被原版按键系统在游戏内持续分发,而并不存在对应的游戏内行为。

+ * + *

{@link InputEvent.Key} 不提供取消机制,因此按键仍会继续传递。这两个键在游戏中未绑定 + * 任何行为,且分页只在提示框可见时改写页码,无提示框时处理逻辑提前返回,故不产生副作用。

+ */ +@OnlyIn(Dist.CLIENT) +@EventBusSubscriber(Dist.CLIENT) +public final class BeefTooltipKeyHandler { + + private BeefTooltipKeyHandler() { + } + + @SubscribeEvent + public static void onKey(InputEvent.Key event) { + if (event.getAction() != GLFW.GLFW_PRESS) return; + + int delta; + if (event.getKey() == GLFW.GLFW_KEY_RIGHT_BRACKET) { + delta = 1; + } else if (event.getKey() == GLFW.GLFW_KEY_LEFT_BRACKET) { + delta = -1; + } else { + return; + } + + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null || !TooltipPageState.isTooltipVisible()) return; + + // 必须使用提示框渲染时记录的那个物品栈,而不是重新按主手/副手推断: + // 在物品栏中悬停时,被查看的是槽位内的物品栈实例,与快捷栏的 ItemStack + // 并非同一对象,两者的标识哈希不同。若按键阶段改用主手物品翻页,页码会写到 + // 渲染阶段从不读取的键上,表现为按键无效。 + ItemStack staff = TooltipPageState.getViewedStack(); + if (staff == null || !(staff.getItem() instanceof EndlessBeafItem)) return; + + BeefTooltipPager.turnPage(staff, delta); + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/BeefTooltipPager.java b/src/main/java/com/sorrowmist/useless/client/BeefTooltipPager.java new file mode 100644 index 00000000..b8486a28 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/BeefTooltipPager.java @@ -0,0 +1,82 @@ +package com.sorrowmist.useless.client; + +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +/** + * 造化杖提示分页状态。 + * + *

造化杖的提示行数已超过屏幕可用高度,超出部分在原版提示框中被裁掉。此处把提示内容 + * 按页切分:渲染时只取当前页对应的行,玩家用 {@code [} 与 {@code ]} 翻页。

+ * + *

页码按物品栈缓存(键为 {@link System#identityHashCode(Object)}),而非全局单值: + * 切换所持物品、物品被消耗或切换存档后,原键不再出现,页码自然回到首页,无需在事件中清理。

+ */ +@OnlyIn(Dist.CLIENT) +public final class BeefTooltipPager { + + /** 单页最多行数。 */ + public static final int LINES_PER_PAGE = 12; + + private BeefTooltipPager() { + } + + /** 读取当前物品栈的页码(从 0 开始)。 */ + public static int getPage(ItemStack stack) { + return TooltipPageState.pageOf(identityOf(stack)); + } + + /** + * 取物品栈的标识哈希。 + * + *

{@link ItemStack} 未覆写 {@code equals} 与 {@code hashCode},其默认实现即按对象身份比较, + * 因此直接沿用 {@link System#identityHashCode(Object)}。该值在一次游戏会话内唯一且不复用。

+ */ + private static int identityOf(ItemStack stack) { + return System.identityHashCode(stack); + } + + /** + * 翻页。 + * + * @param delta 正值向后翻页,负值向前翻页 + */ + public static void turnPage(ItemStack stack, int delta) { + TooltipPageState.turnPage(identityOf(stack), delta); + } + + /** 把行数换算成总页数,至少为 1。 */ + public static int pageCount(int lineCount) { + return Math.max(1, (lineCount + LINES_PER_PAGE - 1) / LINES_PER_PAGE); + } + + /** + * 裁取当前页对应的行区间,并把回绕后的页码写回存储。 + * + *

页码在按键阶段只能单调累加,真正的回绕需要总行数,因此推迟到渲染阶段在此完成。 + * 连续按同一方向键时页码可无界增长,取模后等价于循环翻页。

+ * + * @return 长度恰为 2 的数组 {@code [fromIndex, toIndex)},可直接用于 {@code List#subList} + */ + public static int[] pageRange(ItemStack stack, int lineCount, int totalPages) { + int page = Math.floorMod(getPage(stack), totalPages); + TooltipPageState.setPage(identityOf(stack), page); + int from = Math.min(page * LINES_PER_PAGE, lineCount); + int to = Math.min(from + LINES_PER_PAGE, lineCount); + return new int[]{from, to}; + } + + /** 生成翻页指示行,形如 {@code 1/3 [ ] 翻页}。 */ + public static MutableComponent pageIndicator(int page, int totalPages) { + return Component.translatable( + "tooltip.useless_mod.tooltip_page", + page + 1, + totalPages, + "[", + "]" + ); + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/ClientEventBusSubscriber.java b/src/main/java/com/sorrowmist/useless/client/ClientEventBusSubscriber.java deleted file mode 100644 index c237443c..00000000 --- a/src/main/java/com/sorrowmist/useless/client/ClientEventBusSubscriber.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.sorrowmist.useless.client; - -import com.sorrowmist.useless.UselessMod; -import com.sorrowmist.useless.items.EndlessBeafItem; -import com.sorrowmist.useless.networking.ChainMiningTogglePacket; -import com.sorrowmist.useless.networking.EnchantmentSwitchPacket; -import com.sorrowmist.useless.networking.ModMessages; -import com.sorrowmist.useless.networking.TriggerForceMiningPacket; -import net.minecraft.client.Minecraft; -import net.minecraft.world.item.ItemStack; -import net.minecraftforge.api.distmarker.Dist; -import net.minecraftforge.client.event.InputEvent; -import net.minecraftforge.client.event.RegisterKeyMappingsEvent; -import net.minecraftforge.eventbus.api.SubscribeEvent; -import net.minecraftforge.fml.common.Mod; - -/** - * 客户端事件订阅器 - * 重构:ForceBreakKey发送时检查Tab键状态 - */ -@Mod.EventBusSubscriber(modid = UselessMod.MOD_ID, bus = Mod.EventBusSubscriber.Bus.FORGE, value = Dist.CLIENT) -public class ClientEventBusSubscriber { - - @SubscribeEvent - public static void onKeyInput(InputEvent.Key event) { - Minecraft minecraft = Minecraft.getInstance(); - // 检查玩家是否打开了GUI或正在使用物品 - if (minecraft.screen != null) { - return; - } - - if (KeyBindings.SWITCH_SILK_TOUCH_KEY.consumeClick()) { - if (minecraft.player != null) { - ItemStack mainHandItem = minecraft.player.getMainHandItem(); - if (mainHandItem.getItem() instanceof EndlessBeafItem) { - ModMessages.sendToServer(new EnchantmentSwitchPacket(true)); - } - } - } - - if (KeyBindings.SWITCH_FORTUNE_KEY.consumeClick()) { - if (minecraft.player != null) { - ItemStack mainHandItem = minecraft.player.getMainHandItem(); - if (mainHandItem.getItem() instanceof EndlessBeafItem) { - ModMessages.sendToServer(new EnchantmentSwitchPacket(false)); - } - } - } - - // 处理连锁挖掘按键的按下和松开事件 - if (KeyBindings.SWITCH_CHAIN_MINING_KEY.isDown() != KeyBindings.SWITCH_CHAIN_MINING_KEY_WAS_DOWN) { - KeyBindings.SWITCH_CHAIN_MINING_KEY_WAS_DOWN = KeyBindings.SWITCH_CHAIN_MINING_KEY.isDown(); - if (minecraft.player != null) { - ItemStack mainHandItem = minecraft.player.getMainHandItem(); - if (mainHandItem.getItem() instanceof EndlessBeafItem) { - UselessMod.NETWORK.sendToServer(new ChainMiningTogglePacket(KeyBindings.SWITCH_CHAIN_MINING_KEY.isDown())); - } - } - } - - // 处理强制挖掘触发按键(R键)- 重构:传入Tab键状态以支持R+Tab连锁强制破坏 - if (KeyBindings.TRIGGER_FORCE_MINING_KEY.consumeClick()) { - if (minecraft.player != null) { - ItemStack mainHandItem = minecraft.player.getMainHandItem(); - if (mainHandItem.getItem() instanceof EndlessBeafItem) { - boolean tabPressed = KeyBindings.SWITCH_CHAIN_MINING_KEY.isDown(); - ModMessages.sendToServer(new TriggerForceMiningPacket(tabPressed)); - } - } - } - } -} - -@Mod.EventBusSubscriber(modid = UselessMod.MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD, value = Dist.CLIENT) -class ClientModBusSubscriber { - @SubscribeEvent - public static void onKeyRegister(RegisterKeyMappingsEvent event) { - event.register(KeyBindings.SWITCH_SILK_TOUCH_KEY); - event.register(KeyBindings.SWITCH_FORTUNE_KEY); - event.register(KeyBindings.SWITCH_CHAIN_MINING_KEY); - event.register(KeyBindings.SWITCH_ENHANCED_CHAIN_MINING_KEY); - event.register(KeyBindings.SWITCH_MODE_WHEEL_KEY); - event.register(KeyBindings.SWITCH_FORCE_MINING_KEY); - event.register(KeyBindings.TRIGGER_FORCE_MINING_KEY); - event.register(KeyBindings.SET_MASTER_PATTERN_KEY); - event.register(KeyBindings.SET_SLAVE_PATTERN_KEY); - } -} diff --git a/src/main/java/com/sorrowmist/useless/client/ClientSetup.java b/src/main/java/com/sorrowmist/useless/client/ClientSetup.java deleted file mode 100644 index d941c356..00000000 --- a/src/main/java/com/sorrowmist/useless/client/ClientSetup.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.sorrowmist.useless.client; - -import com.sorrowmist.useless.blocks.GlowPlasticBlock; -import com.sorrowmist.useless.api.EnumColor; -import net.minecraftforge.api.distmarker.Dist; -import net.minecraftforge.client.event.RegisterColorHandlersEvent; -import net.minecraftforge.eventbus.api.SubscribeEvent; -import net.minecraftforge.fml.common.Mod; - -@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD, value = Dist.CLIENT, modid = "useless_mod") -public class ClientSetup { - - @SubscribeEvent - public static void onItemColor(RegisterColorHandlersEvent.Item event) { - // 注册所有颜色的物品染色器 - for (EnumColor color : EnumColor.valuesInOrder()) { - var block = GlowPlasticBlock.GLOW_PLASTIC_BLOCKS.get(color).get(); - event.register((stack, tintIndex) -> { - // 只对 tintIndex 0 应用颜色染色 - return tintIndex == 0 ? color.getRgb() : 0xFFFFFFFF; - }, block.asItem()); - } - } - - @SubscribeEvent - public static void onBlockColor(RegisterColorHandlersEvent.Block event) { - // 注册所有颜色的方块染色器 - for (EnumColor color : EnumColor.valuesInOrder()) { - var block = GlowPlasticBlock.GLOW_PLASTIC_BLOCKS.get(color).get(); - event.register((state, world, pos, tintIndex) -> { - // 只对 tintIndex 0 应用颜色染色 - return tintIndex == 0 ? color.getRgb() : 0xFFFFFFFF; - }, block); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/client/KeyBindings.java b/src/main/java/com/sorrowmist/useless/client/KeyBindings.java deleted file mode 100644 index f8af32f6..00000000 --- a/src/main/java/com/sorrowmist/useless/client/KeyBindings.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.sorrowmist.useless.client; - -import com.mojang.blaze3d.platform.InputConstants; -import net.minecraft.client.KeyMapping; -import net.minecraftforge.client.settings.KeyConflictContext; -import org.lwjgl.glfw.GLFW; - -public class KeyBindings { - public static final String KEY_CATEGORY_USELESS = "key.category.useless_mod.useless"; - public static final String KEY_SWITCH_SILK_TOUCH = "key.useless_mod.switch_silk_touch"; - public static final String KEY_SWITCH_FORTUNE = "key.useless_mod.switch_fortune"; - public static final String KEY_SWITCH_CHAIN_MINING = "key.useless_mod.chain_mining"; - public static final String KEY_SWITCH_ENHANCED_CHAIN_MINING = "key.useless_mod.enhanced_chain_mining"; - public static final String KEY_SWITCH_MODE_WHEEL = "key.useless_mod.switch_mode_wheel"; - public static final String KEY_SWITCH_FORCE_MINING = "key.useless_mod.force_mining"; - public static final String KEY_TRIGGER_FORCE_MINING = "key.useless_mod.trigger_force_mining"; - public static final String KEY_SET_MASTER_PATTERN = "key.useless_mod.set_master_pattern"; - public static final String KEY_SET_SLAVE_PATTERN = "key.useless_mod.set_slave_pattern"; - - public static final KeyMapping SWITCH_SILK_TOUCH_KEY = new KeyMapping(KEY_SWITCH_SILK_TOUCH, - KeyConflictContext.IN_GAME, - InputConstants.Type.KEYSYM, - GLFW.GLFW_KEY_PAGE_DOWN, - KEY_CATEGORY_USELESS); - - public static final KeyMapping SWITCH_FORTUNE_KEY = new KeyMapping(KEY_SWITCH_FORTUNE, - KeyConflictContext.IN_GAME, - InputConstants.Type.KEYSYM, - GLFW.GLFW_KEY_PAGE_UP, - KEY_CATEGORY_USELESS); - - public static final KeyMapping SWITCH_CHAIN_MINING_KEY = new KeyMapping(KEY_SWITCH_CHAIN_MINING, - KeyConflictContext.IN_GAME, - InputConstants.Type.KEYSYM, - GLFW.GLFW_KEY_TAB, - KEY_CATEGORY_USELESS); - - public static final KeyMapping SWITCH_ENHANCED_CHAIN_MINING_KEY = new KeyMapping(KEY_SWITCH_ENHANCED_CHAIN_MINING, - KeyConflictContext.IN_GAME, - InputConstants.Type.KEYSYM, - GLFW.GLFW_KEY_KP_8, - KEY_CATEGORY_USELESS); - - public static final KeyMapping SWITCH_MODE_WHEEL_KEY = new KeyMapping(KEY_SWITCH_MODE_WHEEL, - KeyConflictContext.IN_GAME, - InputConstants.Type.KEYSYM, - GLFW.GLFW_KEY_G, - KEY_CATEGORY_USELESS); - - public static final KeyMapping SWITCH_FORCE_MINING_KEY = new KeyMapping(KEY_SWITCH_FORCE_MINING, - KeyConflictContext.IN_GAME, - InputConstants.Type.KEYSYM, - GLFW.GLFW_KEY_KP_9, - KEY_CATEGORY_USELESS); - - public static final KeyMapping TRIGGER_FORCE_MINING_KEY = new KeyMapping(KEY_TRIGGER_FORCE_MINING, - KeyConflictContext.IN_GAME, - InputConstants.Type.KEYSYM, - GLFW.GLFW_KEY_R, - KEY_CATEGORY_USELESS); - - public static final KeyMapping SET_MASTER_PATTERN_KEY = new KeyMapping(KEY_SET_MASTER_PATTERN, - KeyConflictContext.IN_GAME, - InputConstants.Type.KEYSYM, - GLFW.GLFW_KEY_K, - KEY_CATEGORY_USELESS); - - public static final KeyMapping SET_SLAVE_PATTERN_KEY = new KeyMapping(KEY_SET_SLAVE_PATTERN, - KeyConflictContext.IN_GAME, - InputConstants.Type.KEYSYM, - GLFW.GLFW_KEY_L, - KEY_CATEGORY_USELESS); - - // 跟踪连锁挖掘按键的按下状态 - public static boolean SWITCH_CHAIN_MINING_KEY_WAS_DOWN = false; -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/client/ModeWheelHandler.java b/src/main/java/com/sorrowmist/useless/client/ModeWheelHandler.java deleted file mode 100644 index b1027404..00000000 --- a/src/main/java/com/sorrowmist/useless/client/ModeWheelHandler.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.sorrowmist.useless.client; - -import com.sorrowmist.useless.UselessMod; -import com.sorrowmist.useless.items.EndlessBeafItem; -import com.sorrowmist.useless.modes.ModeManager; -import net.minecraft.client.Minecraft; -import net.minecraft.world.item.ItemStack; -import net.minecraftforge.api.distmarker.Dist; -import net.minecraftforge.client.event.InputEvent; -import net.minecraftforge.eventbus.api.SubscribeEvent; -import net.minecraftforge.fml.common.Mod; - -/** - * 模式轮盘处理器,负责处理模式轮盘的显示、隐藏和交互 - */ -@Mod.EventBusSubscriber(modid = UselessMod.MOD_ID, bus = Mod.EventBusSubscriber.Bus.FORGE, value = Dist.CLIENT) -public class ModeWheelHandler { - /** - * 处理键盘按下事件 - */ - @SubscribeEvent - public static void onKeyPressed(InputEvent.Key event) { - Minecraft minecraft = Minecraft.getInstance(); - if (minecraft.player == null) { - return; - } - - // 检查是否是G键按下事件 (GLFW_PRESS = 1) - if (event.getKey() == KeyBindings.SWITCH_MODE_WHEEL_KEY.getKey().getValue() && event.getAction() == 1) { - // 检查主手和副手物品是否是永恒牛排工具或包含我们NBT数据的Omnitool扳手 - ItemStack mainHandItem = minecraft.player.getMainHandItem(); - ItemStack offHandItem = minecraft.player.getOffhandItem(); - - ItemStack targetItem = null; - - // 检查主手 - net.minecraft.resources.ResourceLocation mainItemId = net.minecraftforge.registries.ForgeRegistries.ITEMS.getKey(mainHandItem.getItem()); - if (mainHandItem.getItem() instanceof EndlessBeafItem || - (mainItemId != null && mainItemId.toString().equals("omnitools:omni_wrench") && - mainHandItem.hasTag() && mainHandItem.getTag().contains("ToolModes"))) { - targetItem = mainHandItem; - } - // 检查副手 - else { - net.minecraft.resources.ResourceLocation offItemId = net.minecraftforge.registries.ForgeRegistries.ITEMS.getKey(offHandItem.getItem()); - if (offHandItem.getItem() instanceof EndlessBeafItem || - (offItemId != null && offItemId.toString().equals("omnitools:omni_wrench") && - offHandItem.hasTag() && offHandItem.getTag().contains("ToolModes"))) { - targetItem = offHandItem; - } - } - - if (targetItem != null && minecraft.screen == null) { - ModeManager modeManager = new ModeManager(); - modeManager.loadFromStack(targetItem); - ModeWheelScreen.show(modeManager, targetItem); - } - } - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/client/ModeWheelScreen.java b/src/main/java/com/sorrowmist/useless/client/ModeWheelScreen.java deleted file mode 100644 index d5a54e89..00000000 --- a/src/main/java/com/sorrowmist/useless/client/ModeWheelScreen.java +++ /dev/null @@ -1,495 +0,0 @@ -package com.sorrowmist.useless.client; - -import com.mojang.blaze3d.platform.InputConstants; -import com.mojang.blaze3d.systems.RenderSystem; -import com.mojang.blaze3d.vertex.*; -import com.sorrowmist.useless.modes.ModeManager; -import com.sorrowmist.useless.modes.ToolMode; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.client.renderer.GameRenderer; -import net.minecraft.network.chat.Component; -import net.minecraft.util.Mth; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.Items; - -import java.util.ArrayList; -import java.util.List; - -/** - * 模式轮盘屏幕,使用Minecraft官方Screen类实现 - */ -public class ModeWheelScreen extends Screen { - // 模式管理器 - private final ModeManager modeManager; - // 主手物品栈 - private final ItemStack mainHandItem; - - // 圆盘菜单参数 - private static final float PRECISION = 5.0f; - private static final int MAX_SLOTS = 18; - private static final float OPEN_ANIMATION_LENGTH = 0.25f; - - private float totalTime; - private float prevTick; - private float extraTick; - private boolean closing = false; - - // 模式数据分组 - private final List leftModes = new ArrayList<>(); - private final List middleModes = new ArrayList<>(); - private final List rightModes = new ArrayList<>(); - - public record ModeData(ToolMode mode, Component name, boolean isActive) {} - - // 圆盘配置 - private static final float DISC_RADIUS = 60.0f; - private static final float DISC_SPACING = 150.0f; - - /** - * 构造模式轮盘屏幕 - * - * @param modeManager 模式管理器 - * @param mainHandItem 主手物品栈 - */ - public ModeWheelScreen(ModeManager modeManager, ItemStack mainHandItem) { - super(Component.literal("Mode Wheel")); - this.modeManager = modeManager; - this.mainHandItem = mainHandItem; - this.minecraft = Minecraft.getInstance(); - loadModes(); - } - - private void loadModes() { - leftModes.clear(); - middleModes.clear(); - rightModes.clear(); - - for (ToolMode mode : ToolMode.values()) { - if (mode != ToolMode.CHAIN_MINING) { - boolean shouldAddMode = true; - if (mode == ToolMode.OMNITOOL_MODE) { - net.minecraft.resources.ResourceLocation omnitoolId = new net.minecraft.resources.ResourceLocation("omnitools:omni_wrench"); - shouldAddMode = net.minecraftforge.registries.ForgeRegistries.ITEMS.containsKey(omnitoolId); - } - - if (shouldAddMode) { - boolean isActive = modeManager.isModeActive(mode); - Component name = isActive ? Component.translatable("tooltip.useless_mod.mode_with_status", mode.getTooltip(), Component.translatable("text.useless_mod.active")) : mode.getTooltip(); - ModeData modeData = new ModeData(mode, name, isActive); - - // 根据模式类型分配到不同分组 - switch (mode) { - case SILK_TOUCH: - case FORTUNE: - leftModes.add(modeData); - break; - case WRENCH_MODE: - case MALLET_MODE: - case CROWBAR_MODE: - case HAMMER_MODE: - case SCREWDRIVER_MODE: - case OMNITOOL_MODE: - middleModes.add(modeData); - break; - case FORCE_MINING: - case AE_STORAGE_PRIORITY: - case ENHANCED_CHAIN_MINING: - rightModes.add(modeData); - break; - default: - // 其他模式暂时不处理 - break; - } - } - } - } - } - - @Override - public void tick() { - if (totalTime < OPEN_ANIMATION_LENGTH) { - extraTick++; - } - - // 检查G键是否被松开,如果松开则关闭菜单 - if (!InputConstants.isKeyDown(Minecraft.getInstance().getWindow().getWindow(), InputConstants.KEY_G)) { - onClose(); - } - } - - @Override - public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { - if (leftModes.isEmpty() && middleModes.isEmpty() && rightModes.isEmpty()) { - graphics.drawCenteredString(font, Component.translatable("message.useless_mod.no_modes"), width / 2, height / 2, 0xFFFFFF); - return; - } - - PoseStack ms = graphics.pose(); - float openAnimation = closing ? 1.0f - totalTime / OPEN_ANIMATION_LENGTH : totalTime / OPEN_ANIMATION_LENGTH; - float currTick = minecraft.getFrameTime(); - totalTime += (currTick + extraTick - prevTick) / 20f; - extraTick = 0; - prevTick = currTick; - - float animProgress = Mth.clamp(openAnimation, 0, 1); - animProgress = (float) (1 - Math.pow(1 - animProgress, 3)); - - // 计算三个圆盘的中心位置 - int centerY = height / 2; - int leftCenterX = (int) (width / 2 - DISC_SPACING); - int middleCenterX = width / 2; - int rightCenterX = (int) (width / 2 + DISC_SPACING); - - // 渲染准备 - ms.pushPose(); - RenderSystem.enableBlend(); - RenderSystem.defaultBlendFunc(); - RenderSystem.disableDepthTest(); - RenderSystem.disableCull(); - RenderSystem.setShader(GameRenderer::getPositionColorShader); - RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); - - // 绘制三个圆盘 - Tesselator tessellator = Tesselator.getInstance(); - BufferBuilder buffer = tessellator.getBuilder(); - buffer.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR); - - // 绘制左圆盘 - drawDisc(buffer, leftCenterX, centerY, DISC_RADIUS, leftModes, mouseX, mouseY, animProgress); - - // 绘制中间圆盘 - drawDisc(buffer, middleCenterX, centerY, DISC_RADIUS, middleModes, mouseX, mouseY, animProgress); - - // 绘制右圆盘 - drawDisc(buffer, rightCenterX, centerY, DISC_RADIUS, rightModes, mouseX, mouseY, animProgress); - - BufferUploader.drawWithShader(buffer.end()); - - // 绘制分隔线 - buffer = tessellator.getBuilder(); - buffer.begin(VertexFormat.Mode.DEBUG_LINES, DefaultVertexFormat.POSITION_COLOR); - - // 左圆盘分隔线 - drawDiscDividers(buffer, leftCenterX, centerY, DISC_RADIUS, leftModes.size(), animProgress); - - // 中间圆盘分隔线 - drawDiscDividers(buffer, middleCenterX, centerY, DISC_RADIUS, middleModes.size(), animProgress); - - // 右圆盘分隔线 - drawDiscDividers(buffer, rightCenterX, centerY, DISC_RADIUS, rightModes.size(), animProgress); - - BufferUploader.drawWithShader(buffer.end()); - - // 恢复渲染状态 - RenderSystem.enableDepthTest(); - RenderSystem.enableCull(); - RenderSystem.disableBlend(); - - // 绘制模式名称 - drawModeNames(graphics, leftCenterX, centerY, DISC_RADIUS, leftModes, animProgress); - drawModeNames(graphics, middleCenterX, centerY, DISC_RADIUS, middleModes, animProgress); - drawModeNames(graphics, rightCenterX, centerY, DISC_RADIUS, rightModes, animProgress); - - // 绘制悬停提示 - drawHoverText(graphics, leftCenterX, centerY, DISC_RADIUS, leftModes, mouseX, mouseY); - drawHoverText(graphics, middleCenterX, centerY, DISC_RADIUS, middleModes, mouseX, mouseY); - drawHoverText(graphics, rightCenterX, centerY, DISC_RADIUS, rightModes, mouseX, mouseY); - - ms.popPose(); - } - - /** - * 绘制单个圆盘 - */ - private void drawDisc(BufferBuilder buffer, int centerX, int centerY, float radius, - List modes, int mouseX, int mouseY, float animProgress) { - if (modes.isEmpty()) return; - - float radiusIn = Math.max(0.1f, radius * 0.4f * animProgress); - float radiusOut = Math.max(0.1f, radius * animProgress); - int numberOfSlices = modes.size(); - - // 绘制灰色背景环 - drawSlice(buffer, centerX, centerY, 9, radiusIn, radiusOut, 0, 360, 80, 80, 80, 120); - - // 确定选中的项目 - double mouseAngle = Math.toDegrees(Math.atan2(mouseY - centerY, mouseX - centerX)); - double mouseDistance = Math.sqrt(Math.pow(mouseX - centerX, 2) + Math.pow(mouseY - centerY, 2)); - float slot0 = (((0 - 0.5f) / (float) numberOfSlices) + 0.25f) * 360; - if (mouseAngle < slot0) { - mouseAngle += 360; - } - - int selectedItem = -1; - for (int i = 0; i < numberOfSlices; i++) { - float sliceBorderLeft = (((i - 0.5f) / (float) numberOfSlices) + 0.25f) * 360; - float sliceBorderRight = (((i + 0.5f) / (float) numberOfSlices) + 0.25f) * 360; - if (mouseAngle >= sliceBorderLeft && mouseAngle < sliceBorderRight && - mouseDistance >= radiusIn && mouseDistance < radiusOut) { - selectedItem = i; - break; - } - } - - // 绘制高亮切片 - for (int i = 0; i < numberOfSlices; i++) { - float sliceBorderLeft = (((i - 0.5f) / (float) numberOfSlices) + 0.25f) * 360; - float sliceBorderRight = (((i + 0.5f) / (float) numberOfSlices) + 0.25f) * 360; - - if (selectedItem == i) { - // 悬停切片 - 蓝色高亮 - drawSlice(buffer, centerX, centerY, 10, radiusIn, radiusOut, sliceBorderLeft, sliceBorderRight, 63, 161, 191, 150); - } else { - // 调整索引以匹配实际显示的模式 - int adjusted = ((i + (numberOfSlices / 2 + 1)) % numberOfSlices) - 1; - adjusted = adjusted == -1 ? numberOfSlices - 1 : adjusted; - - if (adjusted >= 0 && adjusted < modes.size() && modes.get(adjusted).isActive()) { - // 当前激活的模式 - 绿色高亮 - drawSlice(buffer, centerX, centerY, 10, radiusIn, radiusOut, sliceBorderLeft, sliceBorderRight, 80, 180, 80, 130); - } - } - } - } - - /** - * 绘制圆盘分隔线 - */ - private void drawDiscDividers(BufferBuilder buffer, int centerX, int centerY, float radius, - int sliceCount, float animProgress) { - if (sliceCount <= 0) return; - - float radiusIn = radius * 0.4f * animProgress; - float radiusOut = radius * animProgress; - - for (int i = 0; i < sliceCount; i++) { - float angle = (float) Math.toRadians((((i - 0.5f) / (float) sliceCount) + 0.25f) * 360); - float x1 = centerX + radiusIn * (float) Math.cos(angle); - float y1 = centerY + radiusIn * (float) Math.sin(angle); - float x2 = centerX + radiusOut * (float) Math.cos(angle); - float y2 = centerY + radiusOut * (float) Math.sin(angle); - buffer.vertex(x1, y1, 11).color(200, 200, 200, 100).endVertex(); - buffer.vertex(x2, y2, 11).color(200, 200, 200, 100).endVertex(); - } - } - - /** - * 绘制模式名称 - */ - private void drawModeNames(GuiGraphics graphics, int centerX, int centerY, float radius, - List modes, float animProgress) { - if (modes.isEmpty()) return; - - float textRadius = radius * 0.7f * animProgress; - int numberOfSlices = modes.size(); - - for (int i = 0; i < numberOfSlices; i++) { - float angle = ((i / (float) numberOfSlices) - 0.25f) * 2 * (float) Math.PI; - if (numberOfSlices % 2 != 0) { - angle += Math.PI / numberOfSlices; - } - - Component name = modes.get(i).mode.getTooltip(); - int nameWidth = font.width(name); - float textX = centerX - nameWidth / 2 + textRadius * (float) Math.cos(angle); - float textY = centerY - font.lineHeight / 2 + textRadius * (float) Math.sin(angle); - - graphics.drawString(font, name, (int) textX, (int) textY, 0xFFFFFF, false); - } - } - - /** - * 绘制悬停提示文字 - */ - private void drawHoverText(GuiGraphics graphics, int centerX, int centerY, float radius, - List modes, int mouseX, int mouseY) { - if (modes.isEmpty()) return; - - float radiusIn = radius * 0.4f; - float radiusOut = radius; - int numberOfSlices = modes.size(); - - // 计算鼠标角度和距离 - double mouseAngle = Math.toDegrees(Math.atan2(mouseY - centerY, mouseX - centerX)); - double mouseDistance = Math.sqrt(Math.pow(mouseX - centerX, 2) + Math.pow(mouseY - centerY, 2)); - float slot0 = (((0 - 0.5f) / (float) numberOfSlices) + 0.25f) * 360; - if (mouseAngle < slot0) { - mouseAngle += 360; - } - - // 确定选中的项目 - int selectedItem = -1; - for (int i = 0; i < numberOfSlices; i++) { - float sliceBorderLeft = (((i - 0.5f) / (float) numberOfSlices) + 0.25f) * 360; - float sliceBorderRight = (((i + 0.5f) / (float) numberOfSlices) + 0.25f) * 360; - if (mouseAngle >= sliceBorderLeft && mouseAngle < sliceBorderRight && - mouseDistance >= radiusIn && mouseDistance < radiusOut) { - selectedItem = i; - break; - } - } - - // 绘制悬停提示文字 - if (selectedItem >= 0 && selectedItem < modes.size()) { - // 调整索引以匹配实际显示的模式 - int adjusted = ((selectedItem + (numberOfSlices / 2 + 1)) % numberOfSlices) - 1; - adjusted = adjusted == -1 ? numberOfSlices - 1 : adjusted; - - if (adjusted >= 0 && adjusted < modes.size()) { - Component name = modes.get(adjusted).name; - int nameWidth = font.width(name); - graphics.drawString(font, name, centerX - nameWidth / 2, centerY - font.lineHeight / 2, 0xFFFFFF, false); - } - } - } - - @Override - public boolean mouseClicked(double mouseX, double mouseY, int button) { - int centerY = height / 2; - int leftCenterX = (int) (width / 2 - DISC_SPACING); - int middleCenterX = width / 2; - int rightCenterX = (int) (width / 2 + DISC_SPACING); - - // 检查左圆盘点击 - if (checkDiscClick((int) mouseX, (int) mouseY, leftCenterX, centerY, DISC_RADIUS, leftModes)) { - return true; - } - - // 检查中间圆盘点击 - if (checkDiscClick((int) mouseX, (int) mouseY, middleCenterX, centerY, DISC_RADIUS, middleModes)) { - return true; - } - - // 检查右圆盘点击 - if (checkDiscClick((int) mouseX, (int) mouseY, rightCenterX, centerY, DISC_RADIUS, rightModes)) { - return true; - } - - return super.mouseClicked(mouseX, mouseY, button); - } - - /** - * 检查是否点击了某个圆盘,并处理点击事件 - */ - private boolean checkDiscClick(int mouseX, int mouseY, int centerX, int centerY, - float radius, List modes) { - if (modes.isEmpty()) return false; - - float radiusIn = radius * 0.4f; - float radiusOut = radius; - int numberOfSlices = modes.size(); - - // 计算鼠标角度和距离 - double mouseAngle = Math.toDegrees(Math.atan2(mouseY - centerY, mouseX - centerX)); - double mouseDistance = Math.sqrt(Math.pow(mouseX - centerX, 2) + Math.pow(mouseY - centerY, 2)); - float slot0 = (((0 - 0.5f) / (float) numberOfSlices) + 0.25f) * 360; - if (mouseAngle < slot0) { - mouseAngle += 360; - } - - // 检查是否在圆盘范围内 - if (mouseDistance < radiusIn || mouseDistance > radiusOut) { - return false; - } - - // 确定点击的切片 - for (int i = 0; i < numberOfSlices; i++) { - float sliceBorderLeft = (((i - 0.5f) / (float) numberOfSlices) + 0.25f) * 360; - float sliceBorderRight = (((i + 0.5f) / (float) numberOfSlices) + 0.25f) * 360; - - if (mouseAngle >= sliceBorderLeft && mouseAngle < sliceBorderRight) { - // 调整索引以匹配实际显示的模式 - int adjusted = ((i + (numberOfSlices / 2 + 1)) % numberOfSlices) - 1; - adjusted = adjusted == -1 ? numberOfSlices - 1 : adjusted; - - if (adjusted >= 0 && adjusted < modes.size()) { - handleModeClick(modes.get(adjusted).mode); - } - return true; - } - } - - return false; - } - - @Override - public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - // 只允许ESC键关闭屏幕,G键通过松开自动关闭 - if (keyCode == InputConstants.KEY_ESCAPE) { - onClose(); - return true; - } - - return super.keyPressed(keyCode, scanCode, modifiers); - } - - @Override - public boolean isPauseScreen() { - // 不是暂停屏幕,游戏继续运行 - return false; - } - - @Override - public boolean shouldCloseOnEsc() { - // 允许按ESC关闭 - return true; - } - - /** - * 处理模式选择 - * - * @param mode 被选中的模式 - */ - private void handleModeClick(ToolMode mode) { - // 发送数据包到服务端处理模式切换 - com.sorrowmist.useless.networking.ModMessages.sendToServer( - new com.sorrowmist.useless.networking.ToolModeSwitchPacket(mode) - ); - - // 仅在客户端更新UI显示(不修改实际状态) - modeManager.toggleMode(mode); - - // 关闭模式轮盘 - onClose(); - } - - /** - * 绘制圆盘切片 - */ - private void drawSlice(BufferBuilder buffer, float x, float y, float z, float radiusIn, float radiusOut, - float startAngle, float endAngle, int r, int g, int b, int a) { - float angle = endAngle - startAngle; - int sections = Math.max(1, Mth.ceil(angle / PRECISION)); - - for (int i = 0; i < sections; i++) { - float angle1 = (float) Math.toRadians(startAngle + (i / (float) sections) * angle); - float angle2 = (float) Math.toRadians(startAngle + ((i + 1) / (float) sections) * angle); - - float x1In = x + radiusIn * (float) Math.cos(angle1); - float y1In = y + radiusIn * (float) Math.sin(angle1); - float x1Out = x + radiusOut * (float) Math.cos(angle1); - float y1Out = y + radiusOut * (float) Math.sin(angle1); - float x2In = x + radiusIn * (float) Math.cos(angle2); - float y2In = y + radiusIn * (float) Math.sin(angle2); - float x2Out = x + radiusOut * (float) Math.cos(angle2); - float y2Out = y + radiusOut * (float) Math.sin(angle2); - - buffer.vertex(x1In, y1In, z).color(r, g, b, a).endVertex(); - buffer.vertex(x1Out, y1Out, z).color(r, g, b, a).endVertex(); - buffer.vertex(x2Out, y2Out, z).color(r, g, b, a).endVertex(); - buffer.vertex(x2In, y2In, z).color(r, g, b, a).endVertex(); - } - } - - /** - * 显示模式轮盘屏幕 - * - * @param modeManager 模式管理器 - * @param mainHandItem 主手物品栈 - */ - public static void show(ModeManager modeManager, ItemStack mainHandItem) { - Minecraft.getInstance().setScreen(new ModeWheelScreen(modeManager, mainHandItem)); - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/client/StaffLinkClientHooks.java b/src/main/java/com/sorrowmist/useless/client/StaffLinkClientHooks.java new file mode 100644 index 00000000..689af6a9 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/StaffLinkClientHooks.java @@ -0,0 +1,21 @@ +package com.sorrowmist.useless.client; + +import net.minecraft.client.gui.screens.Screen; + +/** + * 无线物流里需要客户端状态的判断。 + * + *

单独放一个客户端类,是为了让公共代码({@code EventHandler})不直接引用 + * {@code net.minecraft.client.*}:专用服务器上没有这些类,common 类里出现它们的 + * 符号会在类加载阶段炸掉。这里由调用方保证只在客户端分支里调用。

+ */ +public final class StaffLinkClientHooks { + + private StaffLinkClientHooks() { + } + + /** 按住 Ctrl:批量绑定 / 批量编辑。 */ + public static boolean isBatchModifierDown() { + return Screen.hasControlDown(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/TooltipPageState.java b/src/main/java/com/sorrowmist/useless/client/TooltipPageState.java new file mode 100644 index 00000000..f39d6bc6 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/TooltipPageState.java @@ -0,0 +1,91 @@ +package com.sorrowmist.useless.client; + +import net.minecraft.world.item.ItemStack; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 物品提示分页页码的存储。 + * + *

页码以物品栈的标识哈希为键。标识哈希在一次游戏会话内唯一且不复用, + * 因此不同物品栈、乃至同一物品的不同堆叠都不会互相干扰。

+ */ +public final class TooltipPageState { + + private static final Map PAGES = new ConcurrentHashMap<>(); + + /** + * 提示框可见性标记。 + * + *

由 {@code EndlessBeafItem#appendHoverText} 在渲染提示框时置位,由客户端渲染帧末复位。 + * 按键事件无法得知提示框是否真的绘制,只能依赖此标记判断翻页键当前是否有效。

+ */ + private static volatile boolean tooltipVisible; + + /** + * 本次渲染提示框所对应的物品栈。 + * + *

按键处理与提示框渲染不在同一阶段,按键期间无法回溯到「正在展示哪一份提示」; + * 记录该引用后,按键入口直接对同一物品栈翻页,不必再按主手/副手推断。 + * 持有的是物品栈引用而非副本,与页码缓存使用同一标识哈希,两者键一致。

+ */ + private static volatile ItemStack viewedStack; + + private TooltipPageState() { + } + + /** 记录本次渲染提示框的物品栈。 */ + public static void setViewedStack(ItemStack stack) { + viewedStack = stack; + } + + /** 读取本次渲染提示框的物品栈,无提示框时为 null。 */ + public static ItemStack getViewedStack() { + return viewedStack; + } + + /** 标记提示框本轮已渲染。 */ + public static void markTooltipVisible() { + tooltipVisible = true; + } + + /** 清除提示框可见标记,应在每帧渲染结束后调用。 */ + public static void clearTooltipVisible() { + tooltipVisible = false; + } + + /** 提示框本轮是否已渲染。 */ + public static boolean isTooltipVisible() { + return tooltipVisible; + } + + /** + * 读取页码,不存在时返回 0。 + * + *

只读取值而不创建条目:提示渲染每帧都会调用,若此处写入缓存, + * 未翻页的物品栈会长期占用内存。

+ */ + public static int pageOf(int stackIdentity) { + Integer page = PAGES.get(stackIdentity); + return page == null ? 0 : page; + } + + /** + * 翻页并把结果写入存储。 + * + *

此处不接收总页数:按键处理发生在提示框渲染之前,调用时无法得知本次提示将产生多少行。 + * 页码只做非负夹紧并单调累加,由 {@code BeefTooltipPager} 在渲染时按总页数取模回绕。

+ * + * @param delta 正值向后翻页,负值向前翻页 + */ + public static void turnPage(int stackIdentity, int delta) { + int current = pageOf(stackIdentity); + PAGES.put(stackIdentity, Math.max(0, current + delta)); + } + + /** 覆写页码,供渲染阶段回绕后写回。 */ + public static void setPage(int stackIdentity, int page) { + PAGES.put(stackIdentity, Math.max(0, page)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/TooltipVisibilityReset.java b/src/main/java/com/sorrowmist/useless/client/TooltipVisibilityReset.java new file mode 100644 index 00000000..f39df85e --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/TooltipVisibilityReset.java @@ -0,0 +1,28 @@ +package com.sorrowmist.useless.client; + +import com.sorrowmist.useless.UselessMod; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod.EventBusSubscriber; +import net.minecraftforge.client.event.RenderGuiEvent; + +/** + * 提示框可见性标记的帧末复位。 + * + *

标记由物品的提示框渲染路径置位,按键事件读取;两者并不在同一阶段,因此需要在每帧 + * 渲染收尾时清除,使标记只反映「刚刚结束的这一帧是否绘制过提示框」。

+ * + *

复位放在 {@code RenderGuiEvent.Post}:该项事件的 {@code Post} 阶段处于整帧 GUI 绘制完成之后, + * 早于下一帧的按键分发,不会清除掉同一帧内刚置位的标记。

+ */ +@EventBusSubscriber(modid = UselessMod.MODID, value = Dist.CLIENT) +public final class TooltipVisibilityReset { + + private TooltipVisibilityReset() { + } + + @SubscribeEvent + public static void onRenderGuiPost(RenderGuiEvent.Post event) { + TooltipPageState.clearTooltipVisible(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/gui/AE2StyleTextField.java b/src/main/java/com/sorrowmist/useless/client/gui/AE2StyleTextField.java new file mode 100644 index 00000000..ec79c9cd --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/gui/AE2StyleTextField.java @@ -0,0 +1,109 @@ +package com.sorrowmist.useless.client.gui; + +import net.minecraft.Util; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; + +/** + * 采用 AE2 文本框外观的输入框。 + * + *

AE2 的文本框由 {@code ae2:textures/guis/text_field.png} 一张 128×128 贴图绘制:横向分三段, + * 左端 1 像素、可平铺中段、右端 1 像素;纵向每 12 像素一行,依次对应普通、聚焦与只读三种状态。 + * 该绘制方式仅依赖贴图与 {@code GuiGraphics},不依赖 AE2 的 {@code ScreenStyle} 配置体系, + * 因此可直接复用其外观而不引入 AE2 界面 JSON 依赖。

+ * + *

原版 {@link EditBox} 的字体与提示文本字段均为私有且无可用的公开读取方法, + * 因此本类自行保存这两项,绘制时不访问父类私有状态。

+ */ +final class AE2StyleTextField extends EditBox { + /** AE2 文本框贴图,尺寸 128×128。 */ + private static final ResourceLocation TEXTURE = + new ResourceLocation("ae2", "textures/guis/text_field.png"); + /** 贴图中可平铺中段的横向起点,位于左端盖之后。 */ + private static final int SOURCE_BODY_X = 1; + /** 可平铺中段的像素宽度,用作平铺步长。 */ + private static final int SOURCE_BODY_WIDTH = 126; + /** 贴图总宽度,作为 blit 的纹理宽度参数。 */ + private static final int TEXTURE_SIZE = 128; + /** 贴图每行的高度,与三种状态的行距一致。 */ + private static final int ROW_HEIGHT = 12; + /** 文本相对输入框左缘的水平内缩。 */ + private static final int PADDING = 4; + /** 提示文本颜色。 */ + private static final int HINT_COLOR = 0x808080; + /** 正文颜色。 */ + private static final int TEXT_COLOR = 0xE0E0E0; + + /** 绘制用字体,父类字段私有,此处另行保存。 */ + private final Font textFont; + /** 空值且未聚焦时显示的提示文本。 */ + private String hintText = ""; + + AE2StyleTextField(Font font, int x, int y, int width, int height, Component narration) { + super(font, x, y, width, height, narration); + this.textFont = font; + setBordered(false); + } + + /** + * 设置空值提示文本。 + * + *

父类的提示由原版在聚焦时绘制,本类改为自绘,故需同时保存一份文本。

+ */ + @Override + public void setHint(Component hint) { + super.setHint(hint); + this.hintText = hint.getString(); + } + + @Override + public void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + if (!isVisible()) return; + + // 贴图纵向三行依次为普通、聚焦、只读:第 0 行为未聚焦底纹,第 1 行为聚焦高亮底纹。 + // 本类不用于只读展示,因此不涉及第 2 行。 + int sourceY = (isFocused() ? 1 : 0) * ROW_HEIGHT; + int x = getX(); + int y = getY(); + int right = x + getWidth(); + // 目标高度取控件实际高度:贴图源行高固定为 ROW_HEIGHT,纵向按需缩放以铺满控件, + // 避免控件高度大于贴图行高时底部露出空隙。 + int height = getHeight(); + + // 左端盖。 + graphics.blit(TEXTURE, x, y, 0, sourceY, 1, height, TEXTURE_SIZE, TEXTURE_SIZE); + // 中段按中段宽度为步长平铺,末段按剩余宽度裁剪,避免贴图被拉伸。 + int bodyX = x + 1; + while (bodyX < right - 1) { + int piece = Math.min(SOURCE_BODY_WIDTH, right - 1 - bodyX); + graphics.blit(TEXTURE, bodyX, y, SOURCE_BODY_X, sourceY, piece, height, + TEXTURE_SIZE, TEXTURE_SIZE); + bodyX += piece; + } + // 右端盖。 + graphics.blit(TEXTURE, right - 1, y, 127, sourceY, 1, height, TEXTURE_SIZE, TEXTURE_SIZE); + + int textY = y + (getHeight() - 8) / 2; + int maxTextWidth = getWidth() - PADDING * 2; + String value = getValue(); + if (value.isEmpty()) { + if (!hintText.isEmpty()) { + graphics.drawString(textFont, textFont.plainSubstrByWidth(hintText, maxTextWidth), + x + PADDING, textY, HINT_COLOR, false); + } + return; + } + + String visible = textFont.plainSubstrByWidth(value, maxTextWidth); + graphics.drawString(textFont, visible, x + PADDING, textY, TEXT_COLOR, false); + + // 光标仅在聚焦时绘制,位置紧随已显示文本。 + if (isFocused() && (Util.getMillis() / 500L) % 2L == 0L) { + int cursorX = x + PADDING + textFont.width(visible); + graphics.fill(cursorX, textY - 1, cursorX + 1, textY + 9, 0xFFD0D0D0); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/gui/AdvancedAlloyFurnaceScreen.java b/src/main/java/com/sorrowmist/useless/client/gui/AdvancedAlloyFurnaceScreen.java new file mode 100644 index 00000000..97789a5b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/gui/AdvancedAlloyFurnaceScreen.java @@ -0,0 +1,1368 @@ +package com.sorrowmist.useless.client.gui; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.BufferBuilder; +import com.mojang.blaze3d.vertex.BufferUploader; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.Tesselator; +import com.mojang.blaze3d.vertex.VertexFormat; +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.api.enums.FurnaceFace; +import com.sorrowmist.useless.api.enums.FurnaceFaceMode; +import com.sorrowmist.useless.api.enums.RedstoneControlMode; +import com.sorrowmist.useless.content.blockentities.AdvancedAlloyFurnaceBlockEntity; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalStackView; +import com.sorrowmist.useless.content.menus.AdvancedAlloyFurnaceMenu; +import com.sorrowmist.useless.inventory.slot.PatternSlotItemHandler; +import com.sorrowmist.useless.client.render.PatternSlotRenderer; +import com.sorrowmist.useless.network.AutoIOChangePacket; +import com.sorrowmist.useless.network.AECancelPacket; +import com.sorrowmist.useless.network.AEReturnOutputTogglePacket; +import com.sorrowmist.useless.network.FaceModeChangePacket; +import com.sorrowmist.useless.network.PatternPageChangePacket; +import com.sorrowmist.useless.network.RedstoneControlPacket; +import com.sorrowmist.useless.network.TankClearPacket; +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.InventoryMenu; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.client.extensions.common.IClientFluidTypeExtensions; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import appeng.api.stacks.GenericStack; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class AdvancedAlloyFurnaceScreen extends AbstractContainerScreen { + + // UI背景纹理(从locate_picture.png抠取) + private static final ResourceLocation TEXTURE = + new ResourceLocation(UselessMod.MODID, "textures/gui/locate_picture.png"); + + // UI尺寸:243*260 + private static final int DISPLAY_WIDTH = 243; + private static final int DISPLAY_HEIGHT = 260; + // 纹理实际尺寸:256*480 + private static final int TEXTURE_WIDTH = 256; + private static final int TEXTURE_HEIGHT = 480; + + // 槽位尺寸 + private static final int SLOT_SIZE = 16; + private static final int SLOT_SPACING = 2; + + // 物品输入槽位置:起点75,16,横向9个 + private static final int INPUT_SLOTS_X = 75; + private static final int INPUT_SLOTS_Y = 16; + + // 物品输出槽位置:起点75,93,横向9个 + private static final int OUTPUT_SLOTS_X = 75; + private static final int OUTPUT_SLOTS_Y = 93; + + // 流体输入槽位置:起点75,36,大小16*31,横向9个 + private static final int FLUID_INPUT_X = 75; + private static final int FLUID_INPUT_Y = 36; + private static final int FLUID_TANK_WIDTH = 16; + private static final int FLUID_TANK_HEIGHT = 31; + + // 流体输出槽位置:起点75,113,大小16*31,横向9个 + private static final int FLUID_OUTPUT_X = 75; + private static final int FLUID_OUTPUT_Y = 113; + + // The three reserved positions after the six fluid tanks are chemical tanks. + private static final int CHEMICAL_INPUT_X = FLUID_INPUT_X + + AdvancedAlloyFurnaceLayout.FLUID_TANK_COUNT * (FLUID_TANK_WIDTH + SLOT_SPACING); + private static final int CHEMICAL_OUTPUT_X = FLUID_OUTPUT_X + + AdvancedAlloyFurnaceLayout.FLUID_TANK_COUNT * (FLUID_TANK_WIDTH + SLOT_SPACING); + + // 样板槽位置:起点8,5,横向3个纵向9个 + private static final int PATTERN_SLOTS_X = 8; + private static final int PATTERN_SLOTS_Y = 5; + private static final int PATTERN_SLOTS_COLS = 3; + private static final int PATTERN_SLOTS_ROWS = 9; + + // 翻页按钮位置 + private static final int PREV_PAGE_BUTTON_X = 11; + private static final int PREV_PAGE_BUTTON_Y = 172; + private static final int PREV_PAGE_BUTTON_WIDTH = 4; + private static final int PREV_PAGE_BUTTON_HEIGHT = 7; + + private static final int NEXT_PAGE_BUTTON_X = 53; + private static final int NEXT_PAGE_BUTTON_Y = 172; + private static final int NEXT_PAGE_BUTTON_WIDTH = 4; + private static final int NEXT_PAGE_BUTTON_HEIGHT = 7; + + // 翻页按钮纹理坐标(按下状态) + private static final int PREV_PAGE_PRESSED_U = 103; + private static final int PREV_PAGE_PRESSED_V = 402; + private static final int NEXT_PAGE_PRESSED_U = 145; + private static final int NEXT_PAGE_PRESSED_V = 402; + + // 催化剂槽位置:起点78,154 + private static final int CATALYST_SLOT_X = 78; + private static final int CATALYST_SLOT_Y = 154; + + // 模具槽位置:起点128,154 + private static final int MOLD_SLOT_X = 128; + private static final int MOLD_SLOT_Y = 154; + + // 催化剂指示灯位置:起点97,160,大小4*4 + private static final int CATALYST_INDICATOR_X = 97; + private static final int CATALYST_INDICATOR_Y = 160; + private static final int INDICATOR_SIZE = 4; + + // 模具指示灯位置:起点121,160,大小4*4 + private static final int MOLD_INDICATOR_X = 121; + private static final int MOLD_INDICATOR_Y = 160; + + // 指示灯亮起纹理坐标:起点2,401 + private static final int INDICATOR_LIT_U = 2; + private static final int INDICATOR_LIT_V = 401; + + // 进度条位置:起点78,71,大小154*18 + private static final int PROGRESS_BAR_X = 78; + private static final int PROGRESS_BAR_Y = 71; + private static final int PROGRESS_BAR_WIDTH = 154; + private static final int PROGRESS_BAR_HEIGHT = 18; + + // 进度条纹理坐标:起点1,373 + private static final int PROGRESS_BAR_U = 1; + private static final int PROGRESS_BAR_V = 373; + + // 能量槽位置:起点153,169,大小84*4 + private static final int ENERGY_BAR_X = 153; + private static final int ENERGY_BAR_Y = 169; + private static final int ENERGY_BAR_WIDTH = 84; + private static final int ENERGY_BAR_HEIGHT = 4; + + // 能量槽纹理坐标:起点12,402 + private static final int ENERGY_BAR_U = 12; + private static final int ENERGY_BAR_V = 402; + + // 帮助提示区域位置:起点103,154,大小16*16 + private static final int TIPS_AREA_X = 103; + private static final int TIPS_AREA_Y = 154; + private static final int TIPS_AREA_SIZE = 16; + + // ==================== 面模式控制区域 ==================== + // 覆盖图尺寸 + private static final int OVERLAY_WIDTH = 11; + private static final int OVERLAY_HEIGHT = 12; + private static final int OVERLAY_V = 265; + + // 六个面的控制区域坐标(x, y, 对应的FurnaceFace) + // 顶部:26,197 + private static final int FACE_TOP_X = 26; + private static final int FACE_TOP_Y = 197; + + // 左侧:14,210 + private static final int FACE_LEFT_X = 14; + private static final int FACE_LEFT_Y = 210; + + // 前面:26,210 + private static final int FACE_FRONT_X = 26; + private static final int FACE_FRONT_Y = 210; + + // 右侧:38,210 + private static final int FACE_RIGHT_X = 38; + private static final int FACE_RIGHT_Y = 210; + + // 下部:26,223 + private static final int FACE_BOTTOM_X = 26; + private static final int FACE_BOTTOM_Y = 223; + + // 后部:38,223 + private static final int FACE_BACK_X = 38; + private static final int FACE_BACK_Y = 223; + + // 面模式区域定义(按FurnaceFace顺序存储x, y) + private static final int[] FACE_XS = { + FACE_TOP_X, // TOP + FACE_LEFT_X, // LEFT + FACE_FRONT_X, // FRONT + FACE_RIGHT_X, // RIGHT + FACE_BOTTOM_X,// BOTTOM + FACE_BACK_X, // BACK + }; + private static final int[] FACE_YS = { + FACE_TOP_Y, + FACE_LEFT_Y, + FACE_FRONT_Y, + FACE_RIGHT_Y, + FACE_BOTTOM_Y, + FACE_BACK_Y, + }; + + // ==================== 自动输入输出配置区域 ==================== + // 自动输出:10,237,15*12 + private static final int AUTO_OUTPUT_X = 10; + private static final int AUTO_OUTPUT_Y = 237; + private static final int AUTO_OUTPUT_WIDTH = 15; + private static final int AUTO_OUTPUT_HEIGHT = 12; + + // 自动输入:43,237,15*12 + private static final int AUTO_INPUT_X = 43; + private static final int AUTO_INPUT_Y = 237; + private static final int AUTO_INPUT_WIDTH = 15; + private static final int AUTO_INPUT_HEIGHT = 12; + + // 自动输入输出覆盖图在纹理中的坐标(V=265) + private static final int AUTO_OUTPUT_OVERLAY_U = 107; + private static final int AUTO_INPUT_OVERLAY_U = 123; + private static final int AUTO_IO_OVERLAY_V = 265; + + // ==================== 红石控制区域 ==================== + // 红石控制:152,151,14*15 + private static final int REDSTONE_CONTROL_X = 152; + private static final int REDSTONE_CONTROL_Y = 151; + private static final int REDSTONE_CONTROL_WIDTH = AlloyFurnaceControlIcons.WIDTH; + private static final int REDSTONE_CONTROL_HEIGHT = AlloyFurnaceControlIcons.HEIGHT; + + // ==================== AE 任务控制区域 ==================== + // 取消 AE 合成任务按钮:169,151,14*15,常态[140,283],按下[156,283] + private static final int CANCEL_AE_X = 169; + private static final int CANCEL_AE_Y = 151; + private static final int CANCEL_AE_WIDTH = AlloyFurnaceControlIcons.WIDTH; + private static final int CANCEL_AE_HEIGHT = AlloyFurnaceControlIcons.HEIGHT; + + // 产物回AE切换按钮:186,151,14*15,关闭[140,301],开启[156,301] + private static final int AE_RETURN_X = 186; + private static final int AE_RETURN_Y = 151; + private static final int AE_RETURN_WIDTH = 14; + private static final int AE_RETURN_HEIGHT = 15; + private static final int AE_RETURN_OFF_U = 140; + private static final int AE_RETURN_ON_U = 156; + private static final int AE_RETURN_V = 301; + + // 翻页按钮状态 + private boolean prevPageButtonPressed = false; + private boolean nextPageButtonPressed = false; + // 取消AE任务按钮状态 + private boolean cancelAePressed = false; + + // Cache both successful and failed pattern display lookups for this screen instance. + + public AdvancedAlloyFurnaceScreen(AdvancedAlloyFurnaceMenu menu, Inventory playerInventory, Component title) { + super(menu, playerInventory, title); + this.imageWidth = DISPLAY_WIDTH; + this.imageHeight = DISPLAY_HEIGHT; + // 不显示标题和玩家背包标签 + this.titleLabelX = 0; + this.titleLabelY = 0; + this.inventoryLabelX = 0; + this.inventoryLabelY = 0; + } + + /** + * 渲染自动输入输出配置的覆盖图。 + */ + private void renderAutoIOOverlays(GuiGraphics guiGraphics, int x, int y) { + if (this.menu.getBlockEntity() == null) return; + + // 自动输出 + if (this.menu.getBlockEntity().isAutoOutputEnabled()) { + guiGraphics.blit(TEXTURE, + x + AUTO_OUTPUT_X, y + AUTO_OUTPUT_Y, + AUTO_OUTPUT_OVERLAY_U, AUTO_IO_OVERLAY_V, + AUTO_OUTPUT_WIDTH, AUTO_OUTPUT_HEIGHT, + TEXTURE_WIDTH, TEXTURE_HEIGHT); + } + + // 自动输入 + if (this.menu.getBlockEntity().isAutoInputEnabled()) { + guiGraphics.blit(TEXTURE, + x + AUTO_INPUT_X, y + AUTO_INPUT_Y, + AUTO_INPUT_OVERLAY_U, AUTO_IO_OVERLAY_V, + AUTO_INPUT_WIDTH, AUTO_INPUT_HEIGHT, + TEXTURE_WIDTH, TEXTURE_HEIGHT); + } + } + + /** + * 渲染红石控制模式的覆盖图。 + */ + private void renderRedstoneControlOverlay(GuiGraphics guiGraphics, int x, int y) { + if (this.menu.getBlockEntity() == null) return; + RedstoneControlMode mode = this.menu.getBlockEntity().getRedstoneControlMode(); + if (!mode.hasOverlay()) return; + AlloyFurnaceControlIcons.drawRedstone( + guiGraphics, x + REDSTONE_CONTROL_X, y + REDSTONE_CONTROL_Y, mode); + } + + /** + * 渲染取消AE任务按钮。 + */ + private void renderCancelAeButton(GuiGraphics guiGraphics, int x, int y) { + AlloyFurnaceControlIcons.drawCancel( + guiGraphics, x + CANCEL_AE_X, y + CANCEL_AE_Y, cancelAePressed); + } + + /** + * 渲染产物回AE切换按钮。 + */ + private void renderAeReturnButton(GuiGraphics guiGraphics, int x, int y) { + boolean isOn = this.menu.isReturnOutputToAe(); + int u = isOn ? AE_RETURN_ON_U : AE_RETURN_OFF_U; + guiGraphics.blit(TEXTURE, + x + AE_RETURN_X, y + AE_RETURN_Y, + u, AE_RETURN_V, + AE_RETURN_WIDTH, AE_RETURN_HEIGHT, + TEXTURE_WIDTH, TEXTURE_HEIGHT); + } + + @Override + public void render(@NotNull GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + this.renderBackground(guiGraphics); + super.render(guiGraphics, mouseX, mouseY, partialTick); + this.renderTooltip(guiGraphics, mouseX, mouseY); + } + + @Override + protected void renderTooltip(@NotNull GuiGraphics guiGraphics, int mouseX, int mouseY) { + super.renderTooltip(guiGraphics, mouseX, mouseY); + + int x = (this.width - this.imageWidth) / 2; + int y = (this.height - this.imageHeight) / 2; + + this.renderFluidTankTooltip(guiGraphics, mouseX, mouseY, x, y, true); + this.renderFluidTankTooltip(guiGraphics, mouseX, mouseY, x, y, false); + this.renderChemicalTankTooltip(guiGraphics, mouseX, mouseY, x, y, true); + this.renderChemicalTankTooltip(guiGraphics, mouseX, mouseY, x, y, false); + this.renderEnergyTooltip(guiGraphics, mouseX, mouseY, x, y); + this.renderProgressTooltip(guiGraphics, mouseX, mouseY, x, y); + this.renderTipsTooltip(guiGraphics, mouseX, mouseY, x, y); + this.renderFaceModeTooltip(guiGraphics, mouseX, mouseY, x, y); + this.renderAutoIOTooltip(guiGraphics, mouseX, mouseY, x, y); + this.renderRedstoneControlTooltip(guiGraphics, mouseX, mouseY, x, y); + this.renderCancelAeTooltip(guiGraphics, mouseX, mouseY, x, y); + this.renderAeReturnTooltip(guiGraphics, mouseX, mouseY, x, y); + } + + @Override + protected void renderLabels(GuiGraphics guiGraphics, int mouseX, int mouseY) { + // 不渲染任何标签 + } + + /** + * 重写槽位渲染,支持自定义槽位的背景图标 + */ + @Override + protected void renderSlot(GuiGraphics guiGraphics, @NotNull Slot slot) { + int x = slot.x; + int y = slot.y; + ItemStack stack = slot.getItem(); + + boolean isPatternSlot = slot instanceof PatternSlotItemHandler; + // 如果是自定义的PatternSlotItemHandler槽位,检查是否激活 + if (isPatternSlot) { + PatternSlotItemHandler patternSlot = (PatternSlotItemHandler) slot; + if (!patternSlot.isActive()) { + return; + } + var icon = patternSlot.getIcon(); + if ((patternSlot.renderIconWithItem() || stack.isEmpty()) && patternSlot.isSlotEnabled() && icon != null) { + icon.getBlitter() + .dest(x, y) + .opacity(patternSlot.getOpacityOfIcon()) + .blit(guiGraphics); + } + } + + GenericStack patternOutput = isPatternSlot ? getPatternOutput(stack) : null; + if (PatternSlotRenderer.renderPattern( + guiGraphics, this.font, patternOutput, x, y, slot.x + slot.y * this.imageWidth)) { + return; + } + + guiGraphics.renderItem(stack, x, y, slot.x + slot.y * this.imageWidth); + + if (slot.index < AdvancedAlloyFurnaceLayout.TOTAL_SLOTS && !stack.isEmpty() && stack.getCount() > 1) { + this.renderCustomItemCount(guiGraphics, stack, x, y); + } else { + guiGraphics.renderItemDecorations(this.font, stack, x, y, null); + } + } + + private void renderCustomItemCount(GuiGraphics guiGraphics, ItemStack stack, int x, int y) { + int count = stack.getCount(); + if (count <= 1) return; + + String text = formatAeCount(count); + + final float scaleFactor = 0.666f; + + guiGraphics.pose().pushPose(); + guiGraphics.pose().translate(0.0F, 0.0F, 200.0F); + guiGraphics.pose().scale(scaleFactor, scaleFactor, scaleFactor); + + renderSizeLabel(guiGraphics.pose().last().pose(), this.font, x, y, text); + + guiGraphics.pose().popPose(); + } + + private void renderSizeLabel(org.joml.Matrix4f matrix, Font font, float xPos, float yPos, String text) { + final float scaleFactor = 0.666f; + final float inverseScaleFactor = 1.0f / scaleFactor; + final int offset = -1; + + RenderSystem.disableBlend(); + final int X = (int) ((xPos + offset + 16.0f + 2.0f - font.width(text) * scaleFactor) * inverseScaleFactor); + final int Y = (int) ((yPos + offset + 16.0f - 5.0f * scaleFactor) * inverseScaleFactor); + var buffer = Minecraft.getInstance().renderBuffers().bufferSource(); + font.drawInBatch(text, X + 1, Y + 1, 0x413f54, false, matrix, buffer, Font.DisplayMode.NORMAL, 0, 15728880); + font.drawInBatch(text, X, Y, 0xffffff, false, matrix, buffer, Font.DisplayMode.NORMAL, 0, 15728880); + buffer.endBatch(); + RenderSystem.enableBlend(); + } + + private String formatAeCount(int count) { + if (count < 1000) { + return String.valueOf(count); + } else if (count < 1000000) { + int k = count / 1000; + return k + "K"; + } else if (count < 1000000000) { + int m = count / 1000000; + return m + "M"; + } else { + int g = count / 1000000000; + return g + "G"; + } + } + + /** + * 解析编码样板,返回主产物(用于在样板槽上直接显示产物图标) + */ + @Nullable + private GenericStack getPatternOutput(ItemStack stack) { + return stack.isEmpty() + ? null + : PatternSlotRenderer.getPrimaryOutput(stack, Minecraft.getInstance().level); + } + + @Override + protected void renderBg(GuiGraphics guiGraphics, float partialTick, int mouseX, int mouseY) { + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + int x = (this.width - this.imageWidth) / 2; + int y = (this.height - this.imageHeight) / 2; + + // 渲染UI背景(从locate_picture.png抠取0,0到242,259区域) + guiGraphics.blit(TEXTURE, x, y, 0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT, TEXTURE_WIDTH, TEXTURE_HEIGHT); + + // 渲染进度条 + this.renderProgressBar(guiGraphics, x, y); + + // 渲染能量槽 + this.renderEnergyBar(guiGraphics, x, y); + + // 渲染流体槽 + this.renderFluidTanks(guiGraphics, x, y, true); + this.renderFluidTanks(guiGraphics, x, y, false); + this.renderChemicalTanks(guiGraphics, x, y, true); + this.renderChemicalTanks(guiGraphics, x, y, false); + + // 渲染指示灯 + this.renderIndicators(guiGraphics, x, y); + + // 渲染翻页按钮 + this.renderPageButtons(guiGraphics, x, y); + + // 渲染面模式覆盖图 + this.renderFaceModeOverlays(guiGraphics, x, y); + + // 渲染自动输入输出覆盖图 + this.renderAutoIOOverlays(guiGraphics, x, y); + + // 渲染红石控制覆盖图 + this.renderRedstoneControlOverlay(guiGraphics, x, y); + + // 渲染取消AE任务按钮 + this.renderCancelAeButton(guiGraphics, x, y); + + // 渲染产物回AE切换按钮 + this.renderAeReturnButton(guiGraphics, x, y); + } + + /** + * 渲染进度条 + * 根据合成进度从上到下渲染 + */ + private void renderProgressBar(GuiGraphics guiGraphics, int x, int y) { + int progress = this.menu.getProgress(); + int maxProgress = this.menu.getMaxProgress(); + + if (maxProgress <= 0 || progress <= 0) return; + + float progressRatio = (float) progress / maxProgress; + int progressHeight = (int) (PROGRESS_BAR_HEIGHT * progressRatio); + + if (progressHeight > 0) { + // 从上到下渲染进度条 + guiGraphics.blit(TEXTURE, + x + PROGRESS_BAR_X, + y + PROGRESS_BAR_Y, + PROGRESS_BAR_U, + PROGRESS_BAR_V, + PROGRESS_BAR_WIDTH, + progressHeight, + TEXTURE_WIDTH, + TEXTURE_HEIGHT); + } + } + + /** + * 渲染能量槽 + * 根据电量百分比从左到右渲染 + */ + private void renderEnergyBar(GuiGraphics guiGraphics, int x, int y) { + long energyStored = this.menu.getEnergy(); + long maxEnergy = this.menu.getMaxEnergy(); + + if (maxEnergy <= 0) return; + + double energyRatio = (double) energyStored / maxEnergy; + int energyWidth = (int) (ENERGY_BAR_WIDTH * energyRatio); + + if (energyWidth > 0) { + // 从左到右渲染能量槽 + guiGraphics.blit(TEXTURE, + x + ENERGY_BAR_X, + y + ENERGY_BAR_Y, + ENERGY_BAR_U, + ENERGY_BAR_V, + energyWidth, + ENERGY_BAR_HEIGHT, + TEXTURE_WIDTH, + TEXTURE_HEIGHT); + } + } + + /** + * 渲染流体槽 + */ + private void renderFluidTanks(GuiGraphics guiGraphics, int x, int y, boolean isInput) { + if (this.menu.getBlockEntity() == null) return; + + int startX = isInput ? FLUID_INPUT_X : FLUID_OUTPUT_X; + int startY = isInput ? FLUID_INPUT_Y : FLUID_OUTPUT_Y; + + for (int i = 0; i < AdvancedAlloyFurnaceLayout.FLUID_TANK_COUNT; i++) { + int tankX = x + startX + i * (FLUID_TANK_WIDTH + SLOT_SPACING); + int tankY = y + startY; + + FluidStack fluid = isInput ? + this.menu.getInputFluidTank(i).getFluid() : + this.menu.getOutputFluidTank(i).getFluid(); + int capacity = isInput ? + this.menu.getInputFluidTank(i).getCapacity() : + this.menu.getOutputFluidTank(i).getCapacity(); + + this.renderFluidTank(guiGraphics, tankX, tankY, fluid, capacity); + } + } + + /** + * 渲染单个流体槽 + */ + private void renderFluidTank(GuiGraphics guiGraphics, int x, int y, FluidStack fluidStack, int capacity) { + if (fluidStack.isEmpty() || capacity <= 0) return; + + Fluid fluid = fluidStack.getFluid(); + if (fluid == Fluids.EMPTY) return; + + IClientFluidTypeExtensions fluidTypeExtensions = IClientFluidTypeExtensions.of(fluid); + ResourceLocation stillTexture = fluidTypeExtensions.getStillTexture(fluidStack); + + TextureAtlasSprite sprite = null; + if (this.minecraft != null) { + sprite = this.minecraft.getTextureAtlas(InventoryMenu.BLOCK_ATLAS).apply(stillTexture); + } + if (sprite == null) return; + + int fluidHeight = (int) ((float) fluidStack.getAmount() / capacity * FLUID_TANK_HEIGHT); + if (fluidHeight <= 0) return; + + int fluidColor = fluidTypeExtensions.getTintColor(fluidStack); + + guiGraphics.pose().pushPose(); + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + + float r = ((fluidColor >> 16) & 0xFF) / 255.0F; + float g = ((fluidColor >> 8) & 0xFF) / 255.0F; + float b = (fluidColor & 0xFF) / 255.0F; + float a = ((fluidColor >> 24) & 0xFF) / 255.0F; + if (a == 0) a = 1.0F; + + RenderSystem.setShaderColor(r, g, b, a); + + int remainingHeight = fluidHeight; + int currentY = y + FLUID_TANK_HEIGHT - fluidHeight; // 从底部开始填充 + int tileSize = 16; + + RenderSystem.setShaderTexture(0, sprite.atlasLocation()); + RenderSystem.setShader(GameRenderer::getPositionTexShader); + + Tesselator tesselator = Tesselator.getInstance(); + BufferBuilder bufferBuilder = tesselator.getBuilder(); + bufferBuilder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX); + + while (remainingHeight > 0) { + int drawHeight = Math.min(tileSize, remainingHeight); + + float u0 = sprite.getU0(); + float v0 = sprite.getV0(); + float u1 = sprite.getU1(); + float v1 = sprite.getV(drawHeight); + + float x1 = x; + float x2 = x + FLUID_TANK_WIDTH; + float y1 = currentY; + float y2 = currentY + drawHeight; + + bufferBuilder.vertex(x1, y2, 0).uv(u0, v1).endVertex(); + bufferBuilder.vertex(x2, y2, 0).uv(u1, v1).endVertex(); + bufferBuilder.vertex(x2, y1, 0).uv(u1, v0).endVertex(); + bufferBuilder.vertex(x1, y1, 0).uv(u0, v0).endVertex(); + + remainingHeight -= drawHeight; + currentY += drawHeight; + } + + BufferUploader.drawWithShader(bufferBuilder.end()); + + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + RenderSystem.disableBlend(); + guiGraphics.pose().popPose(); + } + + private void renderChemicalTanks(GuiGraphics guiGraphics, int x, int y, boolean isInput) { + if (!this.menu.hasChemicalSupport()) return; + + int startX = isInput ? CHEMICAL_INPUT_X : CHEMICAL_OUTPUT_X; + int startY = isInput ? FLUID_INPUT_Y : FLUID_OUTPUT_Y; + for (int i = 0; i < AdvancedAlloyFurnaceLayout.CHEMICAL_TANK_COUNT; i++) { + int tankX = x + startX + i * (FLUID_TANK_WIDTH + SLOT_SPACING); + ChemicalStackView chemical = isInput + ? this.menu.getInputChemical(i) : this.menu.getOutputChemical(i); + this.renderChemicalTank(guiGraphics, tankX, y + startY, chemical, + this.menu.getChemicalTankCapacity()); + } + } + + private void renderChemicalTank(GuiGraphics guiGraphics, int x, int y, + ChemicalStackView chemical, long capacity) { + if (chemical == null || chemical.isEmpty() || capacity <= 0L) return; + + int fillHeight = (int) Math.min(FLUID_TANK_HEIGHT, + Math.max(1L, Math.min((long) FLUID_TANK_HEIGHT, + (long) ((double) chemical.amount() / (double) capacity * FLUID_TANK_HEIGHT)))); + ResourceLocation icon = chemical.icon(); + TextureAtlasSprite sprite = icon == null || this.minecraft == null ? null + : this.minecraft.getTextureAtlas(InventoryMenu.BLOCK_ATLAS).apply(icon); + if (sprite == null) return; + + int color = chemical.tintColor(); + float r = ((color >> 16) & 0xFF) / 255.0F; + float g = ((color >> 8) & 0xFF) / 255.0F; + float b = (color & 0xFF) / 255.0F; + + guiGraphics.pose().pushPose(); + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + RenderSystem.setShaderColor(r, g, b, 1.0F); + RenderSystem.setShaderTexture(0, sprite.atlasLocation()); + RenderSystem.setShader(GameRenderer::getPositionTexShader); + + int remainingHeight = fillHeight; + int currentY = y + FLUID_TANK_HEIGHT - fillHeight; + Tesselator tesselator = Tesselator.getInstance(); + BufferBuilder bufferBuilder = tesselator.getBuilder(); + bufferBuilder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX); + while (remainingHeight > 0) { + int drawHeight = Math.min(16, remainingHeight); + float u0 = sprite.getU0(); + float v0 = sprite.getV0(); + float u1 = sprite.getU1(); + float v1 = sprite.getV(drawHeight); + bufferBuilder.vertex(x, currentY + drawHeight, 0).uv(u0, v1).endVertex(); + bufferBuilder.vertex(x + FLUID_TANK_WIDTH, currentY + drawHeight, 0).uv(u1, v1).endVertex(); + bufferBuilder.vertex(x + FLUID_TANK_WIDTH, currentY, 0).uv(u1, v0).endVertex(); + bufferBuilder.vertex(x, currentY, 0).uv(u0, v0).endVertex(); + remainingHeight -= drawHeight; + currentY += drawHeight; + } + BufferUploader.drawWithShader(bufferBuilder.end()); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + RenderSystem.disableBlend(); + guiGraphics.pose().popPose(); + } + + /** + * 渲染指示灯 + */ + private void renderIndicators(GuiGraphics guiGraphics, int x, int y) { + int currentParallel = this.menu.getCurrentParallel(); + + // 催化剂指示灯:当并行数大于1时亮起 + if (currentParallel > 1) { + guiGraphics.blit(TEXTURE, + x + CATALYST_INDICATOR_X, + y + CATALYST_INDICATOR_Y, + INDICATOR_LIT_U, + INDICATOR_LIT_V, + INDICATOR_SIZE, + INDICATOR_SIZE, + TEXTURE_WIDTH, + TEXTURE_HEIGHT); + } + + // 模具指示灯:当有模具时亮起 + if (this.menu.hasMold()) { + guiGraphics.blit(TEXTURE, + x + MOLD_INDICATOR_X, + y + MOLD_INDICATOR_Y, + INDICATOR_LIT_U, + INDICATOR_LIT_V, + INDICATOR_SIZE, + INDICATOR_SIZE, + TEXTURE_WIDTH, + TEXTURE_HEIGHT); + } + } + + /** + * 渲染翻页按钮和页码提示 + */ + private void renderPageButtons(GuiGraphics guiGraphics, int x, int y) { + int currentPage = this.menu.getPatternPage(); + int maxPage = this.menu.getMaxPatternPage(); + + // 渲染左翻页按钮(按下状态时覆盖纹理) + if (this.prevPageButtonPressed && currentPage > 0) { + guiGraphics.blit(TEXTURE, + x + PREV_PAGE_BUTTON_X, + y + PREV_PAGE_BUTTON_Y, + PREV_PAGE_PRESSED_U, + PREV_PAGE_PRESSED_V, + PREV_PAGE_BUTTON_WIDTH, + PREV_PAGE_BUTTON_HEIGHT, + TEXTURE_WIDTH, + TEXTURE_HEIGHT); + } + + // 渲染右翻页按钮(按下状态时覆盖纹理) + if (this.nextPageButtonPressed && currentPage < maxPage) { + guiGraphics.blit(TEXTURE, + x + NEXT_PAGE_BUTTON_X, + y + NEXT_PAGE_BUTTON_Y, + NEXT_PAGE_PRESSED_U, + NEXT_PAGE_PRESSED_V, + NEXT_PAGE_BUTTON_WIDTH, + NEXT_PAGE_BUTTON_HEIGHT, + TEXTURE_WIDTH, + TEXTURE_HEIGHT); + } + + // 渲染页码提示文字(在左右翻页键之间居中显示) + String pageText = (currentPage + 1) + "/" + (maxPage + 1); + int textWidth = this.font.width(pageText); + int centerX = x + PREV_PAGE_BUTTON_X + PREV_PAGE_BUTTON_WIDTH + + (NEXT_PAGE_BUTTON_X - PREV_PAGE_BUTTON_X - PREV_PAGE_BUTTON_WIDTH) / 2; + int textX = centerX - textWidth / 2; + int textY = y + PREV_PAGE_BUTTON_Y + 1; + guiGraphics.drawString(this.font, pageText, textX, textY, 0xFFFFFF, false); + } + + /** + * 渲染六个面的模式覆盖图。 + * 仅当模式不是DISABLED时才渲染对应的覆盖纹理。 + *

+ * 纹理坐标:U从FurnaceFaceMode.getOverlayU()取,V=265,尺寸11*12。 + */ + private void renderFaceModeOverlays(GuiGraphics guiGraphics, int x, int y) { + if (this.menu.getBlockEntity() == null) return; + FurnaceFaceMode[] modes = this.menu.getBlockEntity().getFaceModes(); + for (int faceIdx = 0; faceIdx < FurnaceFace.COUNT; faceIdx++) { + FurnaceFaceMode mode = modes[faceIdx]; + if (!mode.hasOverlay()) continue; + int u = mode.getOverlayU(); + int faceX = x + FACE_XS[faceIdx]; + int faceY = y + FACE_YS[faceIdx]; + guiGraphics.blit(TEXTURE, + faceX, faceY, + u, OVERLAY_V, + OVERLAY_WIDTH, OVERLAY_HEIGHT, + TEXTURE_WIDTH, TEXTURE_HEIGHT); + } + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + int x = (this.width - this.imageWidth) / 2; + int y = (this.height - this.imageHeight) / 2; + + if (this.handlePatternPageClick(mouseX, mouseY, x, y)) return true; + if (this.handleProgressClick(mouseX, mouseY, x, y)) return true; + if (this.handleFluidTankClick(mouseX, mouseY, x, y)) return true; + if (this.handleChemicalTankClick(mouseX, mouseY, x, y)) return true; + if (this.handleFaceModeClick(mouseX, mouseY, button, x, y)) return true; + if (this.handleAutoIOClick(mouseX, mouseY, x, y)) return true; + if (this.handleRedstoneControlClick(mouseX, mouseY, x, y)) return true; + if (this.handleCancelAeClick(mouseX, mouseY, x, y)) return true; + if (this.handleAeReturnClick(mouseX, mouseY, x, y)) return true; + + return super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + this.prevPageButtonPressed = false; + this.nextPageButtonPressed = false; + this.cancelAePressed = false; + return super.mouseReleased(mouseX, mouseY, button); + } + + /** + * 处理翻页按钮点击 + */ + private boolean handlePatternPageClick(double mouseX, double mouseY, int x, int y) { + int currentPage = this.menu.getPatternPage(); + int maxPage = this.menu.getMaxPatternPage(); + + // 左翻页按钮 + if (currentPage > 0 && isInArea(mouseX, mouseY, + x + PREV_PAGE_BUTTON_X, y + PREV_PAGE_BUTTON_Y, + PREV_PAGE_BUTTON_WIDTH, PREV_PAGE_BUTTON_HEIGHT)) { + this.prevPageButtonPressed = true; + int newPage = currentPage - 1; + this.menu.setPatternPage(newPage); + this.updatePatternSlotVisibility(); + PacketDistributor.sendToServer(new PatternPageChangePacket(newPage)); + return true; + } + + // 右翻页按钮 + if (currentPage < maxPage && isInArea(mouseX, mouseY, + x + NEXT_PAGE_BUTTON_X, y + NEXT_PAGE_BUTTON_Y, + NEXT_PAGE_BUTTON_WIDTH, NEXT_PAGE_BUTTON_HEIGHT)) { + this.nextPageButtonPressed = true; + int newPage = currentPage + 1; + this.menu.setPatternPage(newPage); + this.updatePatternSlotVisibility(); + PacketDistributor.sendToServer(new PatternPageChangePacket(newPage)); + return true; + } + + return false; + } + + private boolean handleCancelAeClick(double mouseX, double mouseY, int x, int y) { + if (isInArea(mouseX, mouseY, x + CANCEL_AE_X, y + CANCEL_AE_Y, + CANCEL_AE_WIDTH, CANCEL_AE_HEIGHT)) { + this.cancelAePressed = true; + PacketDistributor.sendToServer(new AECancelPacket( + this.menu.getBlockEntity().getBlockPos())); + return true; + } + return false; + } + + private boolean handleAeReturnClick(double mouseX, double mouseY, int x, int y) { + if (isInArea(mouseX, mouseY, x + AE_RETURN_X, y + AE_RETURN_Y, + AE_RETURN_WIDTH, AE_RETURN_HEIGHT)) { + if (this.menu.getBlockEntity() != null) { + PacketDistributor.sendToServer(new AEReturnOutputTogglePacket( + this.menu.getBlockEntity().getBlockPos())); + } + return true; + } + return false; + } + + /** + * 处理进度条点击(查看配方) + */ + private boolean handleProgressClick(double mouseX, double mouseY, int x, int y) { + if (!isInArea(mouseX, mouseY, + x + PROGRESS_BAR_X, y + PROGRESS_BAR_Y, + PROGRESS_BAR_WIDTH, PROGRESS_BAR_HEIGHT)) { + return false; + } + + if (ModList.get().isLoaded("jei")) { + com.sorrowmist.useless.compat.jei.JEIPlugin.showAdvancedAlloyFurnaceRecipes(); + } + return true; + } + + /** + * 处理流体槽点击(清空) + */ + private boolean handleFluidTankClick(double mouseX, double mouseY, int x, int y) { + // 检查输入流体槽 + for (int i = 0; i < AdvancedAlloyFurnaceLayout.FLUID_TANK_COUNT; i++) { + int tankX = x + FLUID_INPUT_X + i * (FLUID_TANK_WIDTH + SLOT_SPACING); + int tankY = y + FLUID_INPUT_Y; + + if (isInArea(mouseX, mouseY, tankX, tankY, FLUID_TANK_WIDTH, FLUID_TANK_HEIGHT)) { + FluidStack fluid = this.menu.getInputFluidTank(i).getFluid(); + if (!fluid.isEmpty()) { + PacketDistributor.sendToServer(new TankClearPacket( + this.menu.getBlockEntity().getBlockPos(), i, true)); + } + return true; + } + } + + // 检查输出流体槽 + for (int i = 0; i < AdvancedAlloyFurnaceLayout.FLUID_TANK_COUNT; i++) { + int tankX = x + FLUID_OUTPUT_X + i * (FLUID_TANK_WIDTH + SLOT_SPACING); + int tankY = y + FLUID_OUTPUT_Y; + + if (isInArea(mouseX, mouseY, tankX, tankY, FLUID_TANK_WIDTH, FLUID_TANK_HEIGHT)) { + FluidStack fluid = this.menu.getOutputFluidTank(i).getFluid(); + if (!fluid.isEmpty()) { + PacketDistributor.sendToServer(new TankClearPacket( + this.menu.getBlockEntity().getBlockPos(), i, false)); + } + return true; + } + } + + return false; + } + + private boolean handleChemicalTankClick(double mouseX, double mouseY, int x, int y) { + if (!this.menu.hasChemicalSupport()) return false; + + for (int i = 0; i < AdvancedAlloyFurnaceLayout.CHEMICAL_TANK_COUNT; i++) { + int tankX = x + CHEMICAL_INPUT_X + i * (FLUID_TANK_WIDTH + SLOT_SPACING); + if (isInArea(mouseX, mouseY, tankX, y + FLUID_INPUT_Y, + FLUID_TANK_WIDTH, FLUID_TANK_HEIGHT)) { + if (!this.menu.getInputChemical(i).isEmpty()) { + PacketDistributor.sendToServer(new TankClearPacket( + this.menu.getBlockEntity().getBlockPos(), i, true, true)); + } + return true; + } + } + + for (int i = 0; i < AdvancedAlloyFurnaceLayout.CHEMICAL_TANK_COUNT; i++) { + int tankX = x + CHEMICAL_OUTPUT_X + i * (FLUID_TANK_WIDTH + SLOT_SPACING); + if (isInArea(mouseX, mouseY, tankX, y + FLUID_OUTPUT_Y, + FLUID_TANK_WIDTH, FLUID_TANK_HEIGHT)) { + if (!this.menu.getOutputChemical(i).isEmpty()) { + PacketDistributor.sendToServer(new TankClearPacket( + this.menu.getBlockEntity().getBlockPos(), i, false, true)); + } + return true; + } + } + return false; + } + + /** + * 处理面模式控制区域点击。 + *

+ * 点击时循环该面的输入输出模式: + * 客户端本地立即更新显示,同时发送网络包到服务器。 + */ + private boolean handleFaceModeClick(double mouseX, double mouseY, int button, int x, int y) { + for (int faceIdx = 0; faceIdx < FurnaceFace.COUNT; faceIdx++) { + int faceX = x + FACE_XS[faceIdx]; + int faceY = y + FACE_YS[faceIdx]; + if (isInArea(mouseX, mouseY, faceX, faceY, OVERLAY_WIDTH, OVERLAY_HEIGHT)) { + FurnaceFace face = FurnaceFace.values()[faceIdx]; + // 客户端本地立即更新 + boolean reverse = button == 1; + FurnaceFaceMode currentMode = this.menu.getBlockEntity().getFaceMode(face); + FurnaceFaceMode newMode = reverse ? currentMode.previous() : currentMode.next(); + this.menu.getBlockEntity().setFaceMode(face, newMode); + // 发送网络包到服务器 + PacketDistributor.sendToServer(new FaceModeChangePacket( + this.menu.getBlockEntity().getBlockPos(), faceIdx, reverse)); + return true; + } + } + return false; + } + + /** + * 处理自动输入输出配置区域点击。 + */ + private boolean handleAutoIOClick(double mouseX, double mouseY, int x, int y) { + if (this.menu.getBlockEntity() == null) return false; + + // 自动输出区域 + if (isInArea(mouseX, mouseY, x + AUTO_OUTPUT_X, y + AUTO_OUTPUT_Y, + AUTO_OUTPUT_WIDTH, AUTO_OUTPUT_HEIGHT)) { + boolean newState = !this.menu.getBlockEntity().isAutoOutputEnabled(); + this.menu.getBlockEntity().setAutoOutputEnabled(newState); + PacketDistributor.sendToServer(new AutoIOChangePacket( + this.menu.getBlockEntity().getBlockPos(), true)); + return true; + } + + // 自动输入区域 + if (isInArea(mouseX, mouseY, x + AUTO_INPUT_X, y + AUTO_INPUT_Y, + AUTO_INPUT_WIDTH, AUTO_INPUT_HEIGHT)) { + boolean newState = !this.menu.getBlockEntity().isAutoInputEnabled(); + this.menu.getBlockEntity().setAutoInputEnabled(newState); + PacketDistributor.sendToServer(new AutoIOChangePacket( + this.menu.getBlockEntity().getBlockPos(), false)); + return true; + } + + return false; + } + + /** + * 处理红石控制区域点击。 + */ + private boolean handleRedstoneControlClick(double mouseX, double mouseY, int x, int y) { + if (this.menu.getBlockEntity() == null) return false; + + if (isInArea(mouseX, mouseY, x + REDSTONE_CONTROL_X, y + REDSTONE_CONTROL_Y, + REDSTONE_CONTROL_WIDTH, REDSTONE_CONTROL_HEIGHT)) { + RedstoneControlMode newMode = this.menu.getBlockEntity().getRedstoneControlMode().next(); + this.menu.getBlockEntity().setRedstoneControlMode(newMode); + PacketDistributor.sendToServer(new RedstoneControlPacket( + this.menu.getBlockEntity().getBlockPos())); + return true; + } + + return false; + } + + private void updatePatternSlotVisibility() { + int currentPage = this.menu.getPatternPage(); + int slotsPerPage = this.menu.getPatternSlotsPerPage(); + int base = currentPage * slotsPerPage; + int end = Math.min(base + slotsPerPage, AdvancedAlloyFurnaceLayout.PATTERN_SLOTS_COUNT); + + for (Slot slot : this.menu.slots) { + if (slot instanceof com.sorrowmist.useless.inventory.slot.PatternSlotItemHandler patternSlot) { + int slotIndex = slot.getSlotIndex(); + int relativeIndex = slotIndex - AdvancedAlloyFurnaceLayout.PATTERN_SLOTS_START; + boolean isActive = relativeIndex >= base && relativeIndex < end; + patternSlot.setActive(isActive); + } + } + } + + private static boolean isInArea(double mouseX, double mouseY, int areaX, int areaY, int width, int height) { + return mouseX >= areaX && mouseX < areaX + width && mouseY >= areaY && mouseY < areaY + height; + } + + private void renderFluidTankTooltip(GuiGraphics guiGraphics, int mouseX, int mouseY, int x, int y, + boolean isInput) { + int startX = isInput ? FLUID_INPUT_X : FLUID_OUTPUT_X; + int startY = isInput ? FLUID_INPUT_Y : FLUID_OUTPUT_Y; + + for (int i = 0; i < AdvancedAlloyFurnaceLayout.FLUID_TANK_COUNT; i++) { + int tankX = x + startX + i * (FLUID_TANK_WIDTH + SLOT_SPACING); + int tankY = y + startY; + + if (!isInArea(mouseX, mouseY, tankX, tankY, FLUID_TANK_WIDTH, FLUID_TANK_HEIGHT)) continue; + + FluidStack fluid = isInput ? + this.menu.getInputFluidTank(i).getFluid() : + this.menu.getOutputFluidTank(i).getFluid(); + int capacity = isInput ? + this.menu.getInputFluidTank(i).getCapacity() : + this.menu.getOutputFluidTank(i).getCapacity(); + + if (!fluid.isEmpty()) { + Component fluidName = fluid.getDisplayName(); + Component amountText = Component.translatable("gui.useless_mod.advanced_alloy_furnace.fluid_amount", + String.format("%,d", fluid.getAmount()), + String.format("%,d", capacity)); + + guiGraphics.renderTooltip(this.font, List.of(fluidName, amountText), + Optional.empty(), mouseX, mouseY); + } + break; + } + } + + private void renderChemicalTankTooltip(GuiGraphics guiGraphics, int mouseX, int mouseY, + int x, int y, boolean isInput) { + if (!this.menu.hasChemicalSupport()) return; + int startX = isInput ? CHEMICAL_INPUT_X : CHEMICAL_OUTPUT_X; + int startY = isInput ? FLUID_INPUT_Y : FLUID_OUTPUT_Y; + for (int i = 0; i < AdvancedAlloyFurnaceLayout.CHEMICAL_TANK_COUNT; i++) { + int tankX = x + startX + i * (FLUID_TANK_WIDTH + SLOT_SPACING); + int tankY = y + startY; + if (!isInArea(mouseX, mouseY, tankX, tankY, FLUID_TANK_WIDTH, FLUID_TANK_HEIGHT)) continue; + + ChemicalStackView chemical = isInput + ? this.menu.getInputChemical(i) : this.menu.getOutputChemical(i); + if (!chemical.isEmpty()) { + Component amountText = Component.translatable( + "gui.useless_mod.advanced_alloy_furnace.chemical_amount", + formatLong(chemical.amount()), formatLong(this.menu.getChemicalTankCapacity())); + guiGraphics.renderTooltip(this.font, + List.of(chemical.displayName(), amountText), Optional.empty(), mouseX, mouseY); + } + break; + } + } + + private static String formatLong(long amount) { + return String.format("%,d", amount); + } + + private void renderEnergyTooltip(GuiGraphics guiGraphics, int mouseX, int mouseY, int x, int y) { + if (!isInArea(mouseX, mouseY, + x + ENERGY_BAR_X, y + ENERGY_BAR_Y, + ENERGY_BAR_WIDTH, ENERGY_BAR_HEIGHT)) { + return; + } + + List tooltip = new ArrayList<>(); + tooltip.add(Component.translatable("gui.useless_mod.advanced_alloy_furnace.energy", + this.menu.getEnergy(), this.menu.getMaxEnergy())); + tooltip.add(Component.empty()); + tooltip.add(Component.translatable("tooltip.useless_mod.furnace_tier", this.menu.getFurnaceTier()) + .withStyle(ChatFormatting.GOLD)); + tooltip.add(Component.translatable("tooltip.useless_mod.furnace_upgrade_hint")); + tooltip.add(Component.translatable("tooltip.useless_mod.furnace_upgrade_effect")); + guiGraphics.renderTooltip(this.font, tooltip, Optional.empty(), mouseX, mouseY); + } + + private void renderProgressTooltip(GuiGraphics guiGraphics, int mouseX, int mouseY, int x, int y) { + if (!isInArea(mouseX, mouseY, + x + PROGRESS_BAR_X, y + PROGRESS_BAR_Y, + PROGRESS_BAR_WIDTH, PROGRESS_BAR_HEIGHT)) { + return; + } + + List tooltip = new ArrayList<>(); + int progress = this.menu.getProgress(); + int maxProgress = this.menu.getMaxProgress(); + + if (maxProgress > 0) { + float progressPercent = (float) progress / maxProgress * 100; + tooltip.add(Component.translatable("gui.useless_mod.advanced_alloy_furnace.progress", + progress, maxProgress, String.format("%.1f", progressPercent))); + + boolean isActive = progress > 0 && progress < maxProgress; + String statusKey = isActive + ? "gui.useless_mod.advanced_alloy_furnace.status.active" + : "gui.useless_mod.advanced_alloy_furnace.status.idle"; + tooltip.add(Component.translatable("gui.useless_mod.advanced_alloy_furnace.status", + Component.translatable(statusKey)) + .withStyle(isActive ? ChatFormatting.GREEN : ChatFormatting.GRAY)); + + tooltip.add(Component.empty()); + tooltip.add(Component.translatable("gui.useless_mod.advanced_alloy_furnace.current_parallel", + this.menu.getCurrentParallel()) + .withStyle(ChatFormatting.YELLOW)); + tooltip.add(Component.translatable("gui.useless_mod.advanced_alloy_furnace.max_parallel", + this.menu.getCatalystMaxParallel()) + .withStyle(ChatFormatting.BLUE)); + } else { + tooltip.add(Component.translatable("gui.useless_mod.advanced_alloy_furnace.no_process")); + } + + int aeTotalProgress = this.menu.getTotalAEProgress(); + int aeTotalMaxProgress = this.menu.getTotalAEMaxProgress(); + var taskProgressList = this.menu.getAETaskProgressList(); + + tooltip.add(Component.empty()); + tooltip.add(Component.translatable("gui.useless_mod.advanced_alloy_furnace.ae_tasks", + taskProgressList.size()) + .withStyle(ChatFormatting.DARK_PURPLE)); + + if (!taskProgressList.isEmpty()) { + for (var taskProgress : taskProgressList) { + String productName = taskProgress.getProductName(); + int taskProgressVal = taskProgress.getProgress(); + int taskMaxProgress = taskProgress.getMaxProgress(); + long totalOutputCount = taskProgress.getTotalOutputCount(); + String statusKey = taskProgress.getStatusKey(); + String statusDetail = taskProgress.getStatusDetail(); + + Component nameComponent = Component.literal(productName); + Component statusComponent = Component.translatable(statusKey); + + if (taskMaxProgress > 1) { + tooltip.add(Component.translatable("gui.useless_mod.advanced_alloy_furnace.ae_task_progress_ticks", + nameComponent, totalOutputCount, taskProgressVal, taskMaxProgress, statusComponent) + .withStyle(ChatFormatting.LIGHT_PURPLE)); + } else if (statusDetail != null && !statusDetail.isBlank()) { + tooltip.add(Component.translatable("gui.useless_mod.advanced_alloy_furnace.ae_task_waiting_detail", + nameComponent, totalOutputCount, statusComponent, createTaskDetailComponent(statusDetail)) + .withStyle(ChatFormatting.YELLOW)); + } else { + tooltip.add(Component.translatable("gui.useless_mod.advanced_alloy_furnace.ae_task_waiting", + nameComponent, totalOutputCount, statusComponent) + .withStyle(ChatFormatting.YELLOW)); + } + } + } else if (aeTotalMaxProgress > 0) { + float aeProgressPercent = (float) aeTotalProgress / aeTotalMaxProgress * 100; + tooltip.add(Component.translatable("gui.useless_mod.advanced_alloy_furnace.ae_progress", + aeTotalProgress, aeTotalMaxProgress, String.format("%.1f", aeProgressPercent)) + .withStyle(ChatFormatting.LIGHT_PURPLE)); + } + + tooltip.add(Component.empty()); + tooltip.add(Component.translatable("gui.useless_mod.advanced_alloy_furnace.view_recipes") + .withStyle(ChatFormatting.AQUA)); + + guiGraphics.renderTooltip(this.font, tooltip, Optional.empty(), mouseX, mouseY); + } + + private Component createTaskDetailComponent(String statusDetail) { + if (statusDetail.startsWith("gui.useless_mod.")) { + return Component.translatable(statusDetail); + } + return Component.literal(statusDetail); + } + + private void renderTipsTooltip(GuiGraphics guiGraphics, int mouseX, int mouseY, int x, int y) { + if (!isInArea(mouseX, mouseY, + x + TIPS_AREA_X, y + TIPS_AREA_Y, + TIPS_AREA_SIZE, TIPS_AREA_SIZE)) { + return; + } + + List tooltip = new ArrayList<>(); + + int currentParallel = this.menu.getCurrentParallel(); + int catalystMaxParallel = this.menu.getCatalystMaxParallel(); + + tooltip.add(Component.translatable("gui.useless_mod.advanced_alloy_furnace.parallel_info") + .withStyle(ChatFormatting.GOLD, ChatFormatting.BOLD)); + tooltip.add(Component.empty()); + tooltip.add(Component.translatable("gui.useless_mod.advanced_alloy_furnace.current_parallel", currentParallel) + .withStyle(ChatFormatting.YELLOW)); + tooltip.add(Component.translatable("gui.useless_mod.advanced_alloy_furnace.max_parallel", catalystMaxParallel) + .withStyle(ChatFormatting.BLUE)); + + if (this.menu.getBlockEntity() != null) { + AdvancedAlloyFurnaceBlockEntity entity = this.menu.getBlockEntity(); + ItemStack catalyst = entity.getItemHandler().getStackInSlot(AdvancedAlloyFurnaceLayout.CATALYST_SLOT); + + if (!catalyst.isEmpty()) { + tooltip.add(Component.empty()); + tooltip.add(Component.translatable("gui.useless_mod.advanced_alloy_furnace.catalyst", + catalyst.getDisplayName().getString(), catalystMaxParallel) + .withStyle(ChatFormatting.GREEN)); + } + } + + tooltip.add(Component.empty()); + tooltip.add(Component.translatable("gui.useless_mod.advanced_alloy_furnace.parallel_description.title") + .withStyle(ChatFormatting.GOLD)); + tooltip.add(Component.translatable("gui.useless_mod.advanced_alloy_furnace.parallel_description.1") + .withStyle(ChatFormatting.GRAY)); + tooltip.add(Component.translatable("gui.useless_mod.advanced_alloy_furnace.parallel_description.2") + .withStyle(ChatFormatting.GRAY)); + tooltip.add(Component.translatable("gui.useless_mod.advanced_alloy_furnace.parallel_description.3") + .withStyle(ChatFormatting.GRAY)); + tooltip.add(Component.translatable("gui.useless_mod.advanced_alloy_furnace.parallel_description.4") + .withStyle(ChatFormatting.GRAY)); + tooltip.add(Component.translatable("gui.useless_mod.advanced_alloy_furnace.catalyst_warning") + .withStyle(ChatFormatting.RED)); + + guiGraphics.renderTooltip(this.font, tooltip, Optional.empty(), mouseX, mouseY); + } + + private void renderFaceModeTooltip(GuiGraphics guiGraphics, int mouseX, int mouseY, int x, int y) { + if (this.menu.getBlockEntity() == null) return; + + for (int faceIdx = 0; faceIdx < FurnaceFace.COUNT; faceIdx++) { + int faceX = x + FACE_XS[faceIdx]; + int faceY = y + FACE_YS[faceIdx]; + if (!isInArea(mouseX, mouseY, faceX, faceY, OVERLAY_WIDTH, OVERLAY_HEIGHT)) continue; + + FurnaceFace face = FurnaceFace.values()[faceIdx]; + FurnaceFaceMode mode = this.menu.getBlockEntity().getFaceMode(face); + + Component faceName = Component.translatable("gui.useless_mod.advanced_alloy_furnace.face." + face.name().toLowerCase()); + Component modeName = Component.translatable("gui.useless_mod.advanced_alloy_furnace.face_mode." + mode.name().toLowerCase()); + + guiGraphics.renderTooltip(this.font, List.of(faceName, modeName), Optional.empty(), mouseX, mouseY); + break; + } + } + + private void renderAutoIOTooltip(GuiGraphics guiGraphics, int mouseX, int mouseY, int x, int y) { + if (this.menu.getBlockEntity() == null) return; + + // 自动输出 + if (isInArea(mouseX, mouseY, x + AUTO_OUTPUT_X, y + AUTO_OUTPUT_Y, + AUTO_OUTPUT_WIDTH, AUTO_OUTPUT_HEIGHT)) { + Component title = Component.translatable("gui.useless_mod.advanced_alloy_furnace.auto_output"); + Component status = this.menu.getBlockEntity().isAutoOutputEnabled() + ? Component.translatable("gui.useless_mod.enabled") + : Component.translatable("gui.useless_mod.disabled"); + guiGraphics.renderTooltip(this.font, List.of(title, status), Optional.empty(), mouseX, mouseY); + return; + } + + // 自动输入 + if (isInArea(mouseX, mouseY, x + AUTO_INPUT_X, y + AUTO_INPUT_Y, + AUTO_INPUT_WIDTH, AUTO_INPUT_HEIGHT)) { + Component title = Component.translatable("gui.useless_mod.advanced_alloy_furnace.auto_input"); + Component status = this.menu.getBlockEntity().isAutoInputEnabled() + ? Component.translatable("gui.useless_mod.enabled") + : Component.translatable("gui.useless_mod.disabled"); + guiGraphics.renderTooltip(this.font, List.of(title, status), Optional.empty(), mouseX, mouseY); + } + } + + private void renderRedstoneControlTooltip(GuiGraphics guiGraphics, int mouseX, int mouseY, int x, int y) { + if (this.menu.getBlockEntity() == null) return; + + if (isInArea(mouseX, mouseY, x + REDSTONE_CONTROL_X, y + REDSTONE_CONTROL_Y, + REDSTONE_CONTROL_WIDTH, REDSTONE_CONTROL_HEIGHT)) { + RedstoneControlMode mode = this.menu.getBlockEntity().getRedstoneControlMode(); + Component title = Component.translatable("gui.useless_mod.advanced_alloy_furnace.redstone_control"); + Component status = Component.translatable("gui.useless_mod.advanced_alloy_furnace.redstone_control." + mode.name().toLowerCase()); + guiGraphics.renderTooltip(this.font, List.of(title, status), Optional.empty(), mouseX, mouseY); + } + } + + private void renderCancelAeTooltip(GuiGraphics guiGraphics, int mouseX, int mouseY, int x, int y) { + if (isInArea(mouseX, mouseY, x + CANCEL_AE_X, y + CANCEL_AE_Y, + CANCEL_AE_WIDTH, CANCEL_AE_HEIGHT)) { + Component title = Component.translatable("gui.useless_mod.advanced_alloy_furnace.cancel_ae_tasks"); + Component desc = Component.translatable("gui.useless_mod.advanced_alloy_furnace.cancel_ae_tasks.desc"); + guiGraphics.renderTooltip(this.font, List.of(title, desc), Optional.empty(), mouseX, mouseY); + } + } + + private void renderAeReturnTooltip(GuiGraphics guiGraphics, int mouseX, int mouseY, int x, int y) { + if (!isInArea(mouseX, mouseY, x + AE_RETURN_X, y + AE_RETURN_Y, + AE_RETURN_WIDTH, AE_RETURN_HEIGHT)) { + return; + } + if (this.menu.getBlockEntity() == null) return; + Component title = Component.translatable("gui.useless_mod.advanced_alloy_furnace.ae_return_output"); + Component status = this.menu.getBlockEntity().isReturnOutputToAe() + ? Component.translatable("gui.useless_mod.enabled") + : Component.translatable("gui.useless_mod.disabled"); + guiGraphics.renderTooltip(this.font, List.of(title, status), Optional.empty(), mouseX, mouseY); + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/gui/AlloyFurnaceControlButton.java b/src/main/java/com/sorrowmist/useless/client/gui/AlloyFurnaceControlButton.java new file mode 100644 index 00000000..59726d52 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/gui/AlloyFurnaceControlButton.java @@ -0,0 +1,58 @@ +package com.sorrowmist.useless.client.gui; + +import com.sorrowmist.useless.api.enums.RedstoneControlMode; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.network.chat.Component; + +import java.util.function.Supplier; + +final class AlloyFurnaceControlButton extends Button { + private final Supplier redstoneMode; + private final boolean cancel; + private boolean pressed; + + private AlloyFurnaceControlButton(int x, int y, Component message, OnPress onPress, + Supplier redstoneMode, boolean cancel) { + super(x, y, AlloyFurnaceControlIcons.WIDTH, AlloyFurnaceControlIcons.HEIGHT, + message, onPress, DEFAULT_NARRATION); + this.redstoneMode = redstoneMode; + this.cancel = cancel; + } + + static AlloyFurnaceControlButton redstone(int x, int y, + Supplier mode, + OnPress onPress) { + return new AlloyFurnaceControlButton(x, y, + Component.translatable("gui.useless_mod.advanced_alloy_furnace.redstone_control"), + onPress, mode, false); + } + + static AlloyFurnaceControlButton cancel(int x, int y, OnPress onPress) { + return new AlloyFurnaceControlButton(x, y, + Component.translatable("gui.useless_mod.advanced_alloy_furnace.cancel_ae_tasks"), + onPress, () -> RedstoneControlMode.DISABLED, true); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + boolean handled = super.mouseClicked(mouseX, mouseY, button); + if (handled && cancel) { + pressed = true; + } + return handled; + } + + void releaseVisualState() { + pressed = false; + } + + @Override + protected void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + if (cancel) { + AlloyFurnaceControlIcons.drawCancel(graphics, getX(), getY(), pressed); + } else { + AlloyFurnaceControlIcons.drawRedstone(graphics, getX(), getY(), redstoneMode.get()); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/gui/AlloyFurnaceControlIcons.java b/src/main/java/com/sorrowmist/useless/client/gui/AlloyFurnaceControlIcons.java new file mode 100644 index 00000000..c499d7f3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/gui/AlloyFurnaceControlIcons.java @@ -0,0 +1,33 @@ +package com.sorrowmist.useless.client.gui; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.api.enums.RedstoneControlMode; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.resources.ResourceLocation; + +final class AlloyFurnaceControlIcons { + static final int WIDTH = 14; + static final int HEIGHT = 15; + + private static final ResourceLocation TEXTURE = new ResourceLocation( + UselessMod.MODID, "textures/gui/locate_picture.png"); + private static final int TEXTURE_WIDTH = 256; + private static final int TEXTURE_HEIGHT = 480; + private static final int REDSTONE_V = 265; + private static final int CANCEL_NORMAL_U = 140; + private static final int CANCEL_PRESSED_U = 156; + private static final int CANCEL_V = 283; + + private AlloyFurnaceControlIcons() { + } + + static void drawRedstone(GuiGraphics graphics, int x, int y, RedstoneControlMode mode) { + graphics.blit(TEXTURE, x, y, mode.getOverlayU(), REDSTONE_V, + WIDTH, HEIGHT, TEXTURE_WIDTH, TEXTURE_HEIGHT); + } + + static void drawCancel(GuiGraphics graphics, int x, int y, boolean pressed) { + graphics.blit(TEXTURE, x, y, pressed ? CANCEL_PRESSED_U : CANCEL_NORMAL_U, CANCEL_V, + WIDTH, HEIGHT, TEXTURE_WIDTH, TEXTURE_HEIGHT); + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/gui/ChainGroupScreen.java b/src/main/java/com/sorrowmist/useless/client/gui/ChainGroupScreen.java new file mode 100644 index 00000000..ae91ae1e --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/gui/ChainGroupScreen.java @@ -0,0 +1,692 @@ +package com.sorrowmist.useless.client.gui; + +import com.sorrowmist.useless.content.menus.ChainGroupMenu; +import com.sorrowmist.useless.core.config.ChainGroupManager; +import com.sorrowmist.useless.data.BeefToolLayout; +import com.sorrowmist.useless.network.BeefToolLayoutUpdatePacket; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.Component; +import net.minecraft.util.FormattedCharSequence; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.glfw.GLFW; + +import java.util.ArrayList; +import java.util.List; + +/** + * 连锁挖掘「等价组」编辑界面。 + * + *

从模式轮盘(G)→「连锁挖掘」模块右端的齿轮进入。等价组是玩家个人设置, + * 与造化杖布局存在同一份 {@link BeefToolLayout} 里,所以编辑直接走 + * {@link BeefToolLayoutUpdatePacket}——与轮盘的导入导出是同一条校验/落库路径。

+ * + *

本界面是 {@link AbstractContainerScreen} 而非普通 {@code Screen}:JEI 与 EMI 的原料侧栏 + * 默认只画在容器界面旁边(JEI 内置的 handler 只覆盖 {@code AbstractContainerScreen}), + * 普通 {@code Screen} 拿不到侧栏也就没法拖拽。菜单是空的、也不下发, + * 数据仍然走服务端权威的布局更新包。

+ */ +public final class ChainGroupScreen extends AbstractContainerScreen { + + /** + * 面板宽度:JEI 的原料侧栏只会画在 {@code guiRight} 右侧,而且至少要放得下 2 列 + * (每列 18px,两侧各留 6px 边距,共需 48px)才会显示,所以面板不能太宽。 + */ + private static final int IMAGE_WIDTH = 288; + private static final int IMAGE_HEIGHT = 222; + + /** + * 给 JEI / EMI 原料侧栏预留的右侧宽度。 + * + *

面板默认居中;只有当居中会把右侧挤到不足这个宽度时,才自动往左挪。 + * 取 120 是让侧栏至少能出 6 列(12px 边距 + 6×18px),而不是勉强压着 48px 的下限。

+ */ + private static final int SIDEBAR_RESERVE = 120; + + /** 内容区左边缘。 */ + private static final int CONTENT_LEFT = 8; + /** 右边缘对齐线 R1:所有底部按钮、输入框、拖拽槽、条目行尾的删除 x 都落在这一条线上。 */ + private static final int CONTENT_RIGHT = 280; + /** 左列(组列表)右边缘 R2,也是组行拖拽槽的右边缘。 */ + private static final int GROUP_COLUMN_RIGHT = 126; + /** 右列(条目列表)左边缘。 */ + private static final int ENTRY_COLUMN_LEFT = 134; + + private static final int TITLE_Y = 6; + private static final int COLUMN_HEADER_Y = 18; + private static final int LIST_TOP = 28; + private static final int LIST_ROW_HEIGHT = 16; + private static final int LIST_VISIBLE_ROWS = 10; + private static final int LIST_BOTTOM = LIST_TOP + LIST_VISIBLE_ROWS * LIST_ROW_HEIGHT; + + private static final int FOOTER_Y = 196; + private static final int FOOTER_HEIGHT = 14; + private static final int STATUS_Y = 210; + + private static final int ADD_GROUP_X = 8; + private static final int REMOVE_GROUP_X = 64; + private static final int FOOTER_BUTTON_WIDTH = 52; + private static final int ENTRY_FIELD_WIDTH = 78; + private static final int ADD_ENTRY_WIDTH = 64; + private static final int ADD_ENTRY_X = CONTENT_RIGHT - ADD_ENTRY_WIDTH; + + private static final int DROP_ZONE_SIZE = 16; + private static final int DELETE_HIT_WIDTH = 8; + + private static final int SELECTED_BAR_COLOR = 0xFF3E8E8E; + + private final ItemStack targetItem; + private final List buttons = new ArrayList<>(); + + private BeefToolLayout layout; + private int selectedGroup = -1; + private int groupScroll; + private int entryScroll; + + /** + * 用 {@link AE2StyleTextField} 而不是原版 {@code EditBox}:原版画提示文本时既不截断也不裁剪, + * 提示一长就会溢出到右边的「添加」按钮上;这个子类自己按框宽截断。 + */ + private AE2StyleTextField entryField; + + private String statusKey; + private int statusTicks; + + + public ChainGroupScreen(ChainGroupMenu menu, Inventory inventory, ItemStack targetItem, + BeefToolLayout layout) { + super(menu, inventory, Component.translatable("gui.useless_mod.chain_group.title")); + this.imageWidth = IMAGE_WIDTH; + this.imageHeight = IMAGE_HEIGHT; + this.targetItem = targetItem; + this.layout = layout; + if (!layout.chainGroups().isEmpty()) { + this.selectedGroup = 0; + } + } + + @Override + protected void init() { + super.init(); + // 默认居中;只有当居中会让右侧放不下 JEI / EMI 的原料侧栏时才自动左移, + // 否则侧栏会因为挤不出 2 列而整个不显示(GUI 越窄越容易触发)。 + int centered = (width - imageWidth) / 2; + leftPos = Math.max(0, Math.min(centered, width - imageWidth - SIDEBAR_RESERVE)); + // 面板比 GUI 还高时(极端 GUI 缩放)顶部对齐,免得底部按钮跑出屏幕。 + topPos = Math.max(0, (height - imageHeight) / 2); + + buttons.clear(); + addButton(ADD_GROUP_X, FOOTER_BUTTON_WIDTH, + Component.translatable("gui.useless_mod.chain_group.add_group"), this::addGroup); + addButton(REMOVE_GROUP_X, FOOTER_BUTTON_WIDTH, + Component.translatable("gui.useless_mod.chain_group.remove_group"), this::removeSelectedGroup); + addButton(ADD_ENTRY_X, ADD_ENTRY_WIDTH, + Component.translatable("gui.useless_mod.chain_group.add_entry"), this::commitField); + + Component hint = Component.translatable("gui.useless_mod.chain_group.entry_hint"); + entryField = new AE2StyleTextField(font, leftPos + ENTRY_COLUMN_LEFT, topPos + FOOTER_Y + 1, + ENTRY_FIELD_WIDTH, FOOTER_HEIGHT - 2, hint); + entryField.setMaxLength(BeefToolLayout.MAX_CHAIN_ENTRY_LENGTH); + entryField.setHint(hint); + addRenderableWidget(entryField); + + clampSelection(); + clampScroll(); + } + + private void addButton(int x, int width, Component message, Runnable action) { + PressableAE2Button button = addRenderableWidget(new PressableAE2Button( + leftPos + x, topPos + FOOTER_Y, width, FOOTER_HEIGHT, message, ignored -> action.run())); + buttons.add(button); + } + + // ------------------------------------------------------------------ 服务端交互 + + /** + * 界面收到服务端校验后的布局快照。 + * + *

输入框里还有没提交的内容时整份跳过:这些内容只会在玩家按回车或点「添加」时 + * 才变成条目,被快照覆盖掉就等于把玩家打的字吞了。

+ */ + public void receiveSync(BeefToolLayout synced) { + if (entryField != null && !entryField.getValue().isBlank()) { + return; + } + this.layout = synced; + clampSelection(); + clampScroll(); + } + + /** 服务端拒绝了这次改动。 */ + public void receiveError(BeefToolLayout.Error error) { + setStatus(errorKey(error)); + } + + private void sendLayout() { + try { + ChainGroupManager.validateEntries(layout.chainGroups()); + } catch (BeefToolLayout.LayoutException exception) { + setStatus(errorKey(exception.error())); + return; + } + PacketDistributor.sendToServer(new BeefToolLayoutUpdatePacket(layout.toJson())); + clampScroll(); + } + + // ------------------------------------------------------------------ 编辑动作 + + private void addGroup() { + if (layout.chainGroups().size() >= BeefToolLayout.MAX_CHAIN_GROUPS) { + setStatus("gui.useless_mod.mode_config.error.limit"); + return; + } + layout.chainGroups().add(new ArrayList<>()); + selectedGroup = layout.chainGroups().size() - 1; + entryScroll = 0; + sendLayout(); + } + + private void removeSelectedGroup() { + if (selectedGroup < 0 || selectedGroup >= layout.chainGroups().size()) { + return; + } + layout.chainGroups().remove(selectedGroup); + clampSelection(); + entryScroll = 0; + sendLayout(); + } + + /** 把输入框里的内容作为一个新条目加进当前选中的组。 */ + private void commitField() { + if (entryField == null) { + return; + } + String raw = entryField.getValue(); + String entry = raw == null ? "" : raw.trim(); + if (entry.isEmpty()) { + return; + } + if (!addEntry(selectedGroup, entry)) { + return; + } + entryField.setValue(""); + } + + /** JEI 拖拽入口:把一个方块加进指定组。 */ + public void addEntryFromBlock(int groupIndex, ResourceLocation blockId) { + if (blockId != null) { + addEntry(groupIndex, blockId.toString()); + } + } + + private boolean addEntry(int groupIndex, String entry) { + if (groupIndex < 0 || groupIndex >= layout.chainGroups().size()) { + setStatus("gui.useless_mod.chain_group.error.no_group"); + return false; + } + if (!ChainGroupManager.isValidEntrySyntax(entry)) { + setStatus("gui.useless_mod.chain_group.error.invalid"); + return false; + } + List group = layout.chainGroups().get(groupIndex); + if (group.contains(entry)) { + setStatus("gui.useless_mod.chain_group.error.duplicate"); + return false; + } + if (group.size() >= BeefToolLayout.MAX_CHAIN_ENTRIES_PER_GROUP + || totalEntries() >= BeefToolLayout.MAX_TOTAL_CHAIN_ENTRIES) { + setStatus("gui.useless_mod.mode_config.error.limit"); + return false; + } + group.add(entry); + selectedGroup = groupIndex; + sendLayout(); + return true; + } + + private void removeEntry(int groupIndex, int entryIndex) { + if (groupIndex < 0 || groupIndex >= layout.chainGroups().size()) { + return; + } + List group = layout.chainGroups().get(groupIndex); + if (entryIndex < 0 || entryIndex >= group.size()) { + return; + } + group.remove(entryIndex); + clampScroll(); + sendLayout(); + } + + private int totalEntries() { + int total = 0; + for (List group : layout.chainGroups()) { + total += group.size(); + } + return total; + } + + // ------------------------------------------------------------------ JEI 拖拽接口 + + public int chainGroupCount() { + return layout.chainGroups().size(); + } + + public int dropZoneSize() { + return DROP_ZONE_SIZE; + } + + /** 该组当前是否可见(被滚动出可视区时不该给出拖拽目标)。 */ + public boolean isGroupRowVisible(int groupIndex) { + int row = groupIndex - groupScroll; + return row >= 0 && row < LIST_VISIBLE_ROWS; + } + + public int groupDropZoneScreenX(int groupIndex) { + return leftPos + GROUP_COLUMN_RIGHT - DROP_ZONE_SIZE; + } + + public int groupDropZoneScreenY(int groupIndex) { + return topPos + LIST_TOP + (groupIndex - groupScroll) * LIST_ROW_HEIGHT; + } + + // ------------------------------------------------------------------ 输入 + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + // 回车提交输入框内容,提交后不关闭界面。 + if ((keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) + && entryField != null && entryField.isFocused()) { + commitField(); + return true; + } + // 输入框聚焦期间不把按键转交父类:AbstractContainerScreen 在子控件未消费按键时 + // 会比对容器快捷键(默认 E 打开物品栏),命中即调用 onClose 关闭本界面, + // 导致该字母无法输入。Esc 仍然放行,保持「Esc 关闭界面」的既有行为。 + if (keyCode != GLFW.GLFW_KEY_ESCAPE && entryField != null + && (entryField.keyPressed(keyCode, scanCode, modifiers) || entryField.canConsumeInput())) { + return true; + } + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + double localX = mouseX - leftPos; + double localY = mouseY - topPos; + + if (button == GLFW.GLFW_MOUSE_BUTTON_LEFT) { + int groupRow = groupRowAt(localX, localY); + // 命中区域是整列,点在列表下方的空白行时不能把选中索引顶到越界。 + if (groupRow >= 0 && groupScroll + groupRow < layout.chainGroups().size()) { + selectedGroup = groupScroll + groupRow; + entryScroll = 0; + return true; + } + int entryRow = entryRowAt(localX, localY); + if (entryRow >= 0 && localX >= CONTENT_RIGHT - DELETE_HIT_WIDTH) { + removeEntry(selectedGroup, entryScroll + entryRow); + return true; + } + } + return super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + for (PressableAE2Button widget : buttons) { + widget.releaseVisualState(); + } + return super.mouseReleased(mouseX, mouseY, button); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double scrollY) { + double localX = mouseX - leftPos; + double localY = mouseY - topPos; + if (localY >= LIST_TOP && localY <= LIST_BOTTOM) { + int step = scrollY > 0 ? -1 : 1; + if (localX >= CONTENT_LEFT && localX <= GROUP_COLUMN_RIGHT) { + groupScroll = Mth.clamp(groupScroll + step, 0, maxGroupScroll()); + return true; + } + if (localX >= ENTRY_COLUMN_LEFT && localX <= CONTENT_RIGHT) { + entryScroll = Mth.clamp(entryScroll + step, 0, maxEntryScroll()); + return true; + } + } + return super.mouseScrolled(mouseX, mouseY, scrollY); + } + + @Override + protected void containerTick() { + super.containerTick(); + // 这里刻意不做任何自动提交:输入框里的是「待新增的条目」, + // 只有玩家按回车或点「添加」才该产生记录。停手一会儿就自动加一条, + // 或者点一下别处就自动加一条,都是替玩家做了不可逆的决定。 + if (statusTicks > 0 && --statusTicks == 0) { + statusKey = null; + } + } + + @Override + public void onClose() { + // 输入框里没提交的内容直接丢弃——按回车或点「添加」才算数。 + // 回到轮盘,玩家可以接着调其它模式;轮盘 init 会重新请求一份最新布局。 + Minecraft.getInstance().setScreen(new ModeWheelScreen(targetItem)); + } + + @Override + public boolean isPauseScreen() { + return false; + } + + // ------------------------------------------------------------------ 渲染 + + /** + * 面板与列表画在这里。 + * + *

父类 {@code renderBackground} 先铺半透明底、再调本方法;之后才是 + * JEI 的侧栏背景、控件(renderables)、槽位与标签,最后是 JEI 的物品格子。

+ */ + @Override + protected void renderBg(GuiGraphics graphics, float partialTick, int mouseX, int mouseY) { + MachineScreenStyle.drawPanel(graphics, leftPos, topPos, imageWidth, imageHeight); + + graphics.drawString(font, title, leftPos + CONTENT_LEFT, topPos + TITLE_Y, + MachineScreenStyle.TEXT_COLOR, false); + graphics.drawString(font, Component.translatable("gui.useless_mod.chain_group.group_label"), + leftPos + CONTENT_LEFT, topPos + COLUMN_HEADER_Y, + MachineScreenStyle.MUTED_TEXT_COLOR, false); + graphics.drawString(font, Component.translatable("gui.useless_mod.chain_group.entry_label"), + leftPos + ENTRY_COLUMN_LEFT, topPos + COLUMN_HEADER_Y, + MachineScreenStyle.MUTED_TEXT_COLOR, false); + + // 两列各垫一层内衬,把「组列表 / 条目列表」分开,不然一片平底看不出是两个区域。 + MachineScreenStyle.drawInset(graphics, leftPos + CONTENT_LEFT - 2, topPos + LIST_TOP - 2, + leftPos + GROUP_COLUMN_RIGHT, topPos + LIST_BOTTOM); + MachineScreenStyle.drawInset(graphics, leftPos + ENTRY_COLUMN_LEFT - 2, topPos + LIST_TOP - 2, + leftPos + CONTENT_RIGHT, topPos + LIST_BOTTOM); + + renderGroupList(graphics); + renderEntryList(graphics); + drawStatus(graphics); + } + + /** + * 父类默认会画容器标题与「物品栏」标签,这里全都不要——本界面的文字已经在 + * {@link #renderBg} 里画好了,没有物品栏。 + */ + @Override + protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) { + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + // 控件(按钮 / 输入框)由父类的 renderables 循环渲染,天然压在面板之上。 + super.render(graphics, mouseX, mouseY, partialTick); + renderHoverTooltip(graphics, mouseX, mouseY); + } + + + private void renderGroupList(GuiGraphics graphics) { + List> groups = layout.chainGroups(); + if (groups.isEmpty()) { + // 与正常条目行同基线(LIST_TOP + 4),否则会比列表内衬顶边高出 4px 而压在那条边框线上。 + // 提示按列宽折行:该文案长于列宽,截断会丢失后半句语义。 + int budget = GROUP_COLUMN_RIGHT - (CONTENT_LEFT + 2) - 2; + drawWrapped(graphics, Component.translatable("gui.useless_mod.chain_group.empty"), + leftPos + CONTENT_LEFT + 2, topPos + LIST_TOP + 4, budget); + return; + } + + for (int row = 0; row < LIST_VISIBLE_ROWS; row++) { + int index = groupScroll + row; + if (index >= groups.size()) { + break; + } + List group = groups.get(index); + int y = topPos + LIST_TOP + row * LIST_ROW_HEIGHT; + int left = leftPos + CONTENT_LEFT - 2; + int right = leftPos + GROUP_COLUMN_RIGHT; + + // 选中态用「底色 + 左侧竖条」:只靠一层底色太淡。 + if (index == selectedGroup) { + graphics.fill(left, y - 1, right, y + LIST_ROW_HEIGHT - 1, MachineScreenStyle.HIGHLIGHT_COLOR); + graphics.fill(left, y - 1, left + 2, y + LIST_ROW_HEIGHT - 1, SELECTED_BAR_COLOR); + } + + // 行尾拖拽槽:JEI 拖进来的方块会追加到这一组。 + int zoneX = leftPos + GROUP_COLUMN_RIGHT - DROP_ZONE_SIZE; + MachineScreenStyle.drawInset(graphics, zoneX, y, zoneX + DROP_ZONE_SIZE, y + DROP_ZONE_SIZE); + + String count = "(" + group.size() + ")"; + int countX = zoneX - font.width(count) - 3; + String preview = group.isEmpty() ? "-" : entryDisplayName(group.get(0)); + int textBudget = Math.max(16, countX - 4 - (leftPos + CONTENT_LEFT + 2)); + graphics.drawString(font, + font.plainSubstrByWidth((index + 1) + ". " + preview, textBudget), + leftPos + CONTENT_LEFT + 2, y + 4, + index == selectedGroup ? MachineScreenStyle.TEXT_COLOR : MachineScreenStyle.SUBTLE_TEXT_COLOR, + false); + graphics.drawString(font, count, countX, y + 4, MachineScreenStyle.MUTED_TEXT_COLOR, false); + } + } + + private void renderEntryList(GuiGraphics graphics) { + List entries = selectedEntries(); + if (entries.isEmpty()) { + // 与正常条目行同基线(LIST_TOP + 4),否则会比列表内衬顶边高出 4px 而压在那条边框线上。 + // 提示按列宽折行:该文案在窄列下可能放不下,截断会丢失后半句语义。 + int budget = CONTENT_RIGHT - (ENTRY_COLUMN_LEFT + 2) - 2; + drawWrapped(graphics, Component.translatable("gui.useless_mod.chain_group.entry_empty"), + leftPos + ENTRY_COLUMN_LEFT + 2, topPos + LIST_TOP + 4, budget); + return; + } + + int deleteX = leftPos + CONTENT_RIGHT - 6; + for (int row = 0; row < LIST_VISIBLE_ROWS; row++) { + int index = entryScroll + row; + if (index >= entries.size()) { + break; + } + String entry = entries.get(index); + Block block = resolveExactBlock(entry); + int y = topPos + LIST_TOP + row * LIST_ROW_HEIGHT; + int textX = leftPos + ENTRY_COLUMN_LEFT; + + if (block != null && block.asItem() != Items.AIR) { + graphics.renderItem(new ItemStack(block), textX, y); + textX += 18; + } + + int budget = deleteX - 6 - textX; + if (isResolvable(entry)) { + String display = entryDisplayName(entry); + String name = font.plainSubstrByWidth(display, budget); + graphics.drawString(font, name, textX, y + 4, MachineScreenStyle.TEXT_COLOR, false); + // 名字后面缀上原始 ID:不同模组的方块重名很常见,光看名字分不出来。 + // 但标签 / 通配符没有方块名,显示名就是条目原文,这时只画一次。 + if (!display.equals(entry)) { + int idX = textX + font.width(name) + 4; + int idBudget = deleteX - 6 - idX; + if (idBudget > 12) { + graphics.drawString(font, font.plainSubstrByWidth(entry, idBudget), idX, y + 4, + MachineScreenStyle.MUTED_TEXT_COLOR, false); + } + } + } else { + // 语法合法但当前解析不到(未知方块 ID / 尚未加载的标签)标红提示。 + graphics.drawString(font, font.plainSubstrByWidth(entry, budget), textX, y + 4, + MachineScreenStyle.ERROR_TEXT_COLOR, false); + } + graphics.drawString(font, "x", deleteX, y + 4, MachineScreenStyle.ERROR_TEXT_COLOR, false); + } + } + + private void drawStatus(GuiGraphics graphics) { + if (statusKey == null) { + return; + } + graphics.drawString(font, Component.translatable(statusKey), leftPos + CONTENT_LEFT, + topPos + STATUS_Y, MachineScreenStyle.ERROR_TEXT_COLOR, false); + } + + private void renderHoverTooltip(GuiGraphics graphics, int mouseX, int mouseY) { + double localX = mouseX - leftPos; + double localY = mouseY - topPos; + + int groupRow = groupRowAt(localX, localY); + if (groupRow >= 0) { + int index = groupScroll + groupRow; + if (index < layout.chainGroups().size() + && localX >= GROUP_COLUMN_RIGHT - DROP_ZONE_SIZE) { + graphics.renderTooltip(font, + Component.translatable("gui.useless_mod.chain_group.drop_hint"), + (int) mouseX, (int) mouseY); + return; + } + } + + int entryRow = entryRowAt(localX, localY); + if (entryRow >= 0) { + List entries = selectedEntries(); + int index = entryScroll + entryRow; + if (index < entries.size()) { + graphics.renderTooltip(font, Component.literal(entries.get(index)), + (int) mouseX, (int) mouseY); + } + } + } + + // ------------------------------------------------------------------ 工具方法 + + /** + * 按给定宽度折行绘制提示文本。 + * + *

列表空态提示长于列宽,直接截断会丢失后半句语义,因此折行完整呈现。 + * 行距取字体行高加 1 像素,避免相邻两行贴连;不使用列表行高,否则行距过疏。

+ */ + private void drawWrapped(GuiGraphics graphics, Component text, int x, int y, int width) { + List lines = font.split(text, width); + for (int i = 0; i < lines.size(); i++) { + graphics.drawString(font, lines.get(i), x, y + i * (font.lineHeight + 1), + MachineScreenStyle.MUTED_TEXT_COLOR, false); + } + } + + private List selectedEntries() { + if (selectedGroup < 0 || selectedGroup >= layout.chainGroups().size()) { + return List.of(); + } + return layout.chainGroups().get(selectedGroup); + } + + /** 条目对应的方块显示名;解析不到时退回条目原文。 */ + private static String entryDisplayName(String entry) { + Block block = resolveExactBlock(entry); + return block == null ? entry : block.getName().getString(); + } + + /** 只解析「精确方块 ID」形式的条目;标签与通配符没有单一对应方块。 */ + @Nullable + private static Block resolveExactBlock(String entry) { + if (entry.startsWith("#") || entry.indexOf('*') >= 0) { + return null; + } + ResourceLocation id = ResourceLocation.tryParse(entry); + if (id == null || !BuiltInRegistries.BLOCK.containsKey(id)) { + return null; + } + Block block = BuiltInRegistries.BLOCK.get(id); + return block == Blocks.AIR ? null : block; + } + + /** 条目当前能否解析到任何方块(精确 ID 查注册表,标签查标签集,通配符不判)。 */ + private static boolean isResolvable(String entry) { + if (entry.startsWith("#")) { + ResourceLocation id = ResourceLocation.tryParse(entry.substring(1)); + if (id == null) { + return false; + } + return BuiltInRegistries.BLOCK.getTag(TagKey.create(Registries.BLOCK, id)) + .map(set -> set.size() > 0) + .orElse(false); + } + if (entry.indexOf('*') >= 0) { + return true; + } + return resolveExactBlock(entry) != null; + } + + private int groupRowAt(double localX, double localY) { + if (localX < CONTENT_LEFT || localX > GROUP_COLUMN_RIGHT) { + return -1; + } + if (localY < LIST_TOP || localY > LIST_BOTTOM) { + return -1; + } + int row = (int) ((localY - LIST_TOP) / LIST_ROW_HEIGHT); + return row >= 0 && row < LIST_VISIBLE_ROWS ? row : -1; + } + + private int entryRowAt(double localX, double localY) { + if (localX < ENTRY_COLUMN_LEFT || localX > CONTENT_RIGHT) { + return -1; + } + if (localY < LIST_TOP || localY > LIST_BOTTOM) { + return -1; + } + int row = (int) ((localY - LIST_TOP) / LIST_ROW_HEIGHT); + return row >= 0 && row < LIST_VISIBLE_ROWS ? row : -1; + } + + private int maxGroupScroll() { + return Math.max(0, layout.chainGroups().size() - LIST_VISIBLE_ROWS); + } + + private int maxEntryScroll() { + return Math.max(0, selectedEntries().size() - LIST_VISIBLE_ROWS); + } + + private void clampSelection() { + if (layout.chainGroups().isEmpty()) { + selectedGroup = -1; + } else { + selectedGroup = Mth.clamp(selectedGroup, 0, layout.chainGroups().size() - 1); + } + } + + private void clampScroll() { + groupScroll = Mth.clamp(groupScroll, 0, maxGroupScroll()); + entryScroll = Mth.clamp(entryScroll, 0, maxEntryScroll()); + } + + private void setStatus(String key) { + statusKey = key; + statusTicks = 100; + } + + private static String errorKey(BeefToolLayout.Error error) { + return switch (error) { + case INVALID_TEXT -> "gui.useless_mod.mode_config.error.invalid_text"; + case UNSUPPORTED_VERSION -> "gui.useless_mod.mode_config.error.unsupported_version"; + case INVALID_STRUCTURE -> "gui.useless_mod.mode_config.error.invalid_structure"; + case INVALID_NAME -> "gui.useless_mod.mode_config.error.invalid_name"; + case DUPLICATE_MODULE -> "gui.useless_mod.mode_config.error.duplicate_module"; + case UNKNOWN_MODULE -> "gui.useless_mod.mode_config.error.unknown_module"; + case LIMIT -> "gui.useless_mod.mode_config.error.limit"; + }; + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/gui/DimensionConfigScreen.java b/src/main/java/com/sorrowmist/useless/client/gui/DimensionConfigScreen.java new file mode 100644 index 00000000..f2930a51 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/gui/DimensionConfigScreen.java @@ -0,0 +1,853 @@ +package com.sorrowmist.useless.client.gui; + +import com.sorrowmist.useless.content.menus.DimensionConfigMenu; +import com.sorrowmist.useless.network.DimensionConfigSubmitPacket; +import com.sorrowmist.useless.world.dimension.DimensionGenerationConfig; +import com.sorrowmist.useless.world.dimension.PlatformStyle; +import com.sorrowmist.useless.world.dimension.UselessDimensions; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.level.block.state.BlockState; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; +import org.lwjgl.glfw.GLFW; + +public final class DimensionConfigScreen extends AbstractContainerScreen { + private static final int PANEL_WIDTH = 420; + private static final int PANEL_HEIGHT = 346; + + /* 生成预览弹窗:不常驻占位,点「预览」按钮后以全屏覆盖层显示,避免撑宽配置面板; + 显示期间底层配置界面与玩家背包均不渲染。 */ + private static final int POPUP_WIDTH = 420; + private static final int POPUP_HEIGHT = 330; + private static final int POPUP_PAD = 10; + private static final int VIEW_W = POPUP_WIDTH - POPUP_PAD * 2; + private static final int TOP_VIEW_H = 170; + private static final int SIDE_VIEW_H = 52; + private static final int INFO_LINE_HEIGHT = 11; + private static final int CLOSE_BUTTON_W = 80; + private static final int CLOSE_BUTTON_H = 18; + private static final int DIALOG_BUTTON_W = 84; + private static final int DIALOG_BUTTON_H = 18; + /** + * 预览弹窗的全屏底板颜色:不透明黑,用于完全遮挡底层界面。 + * 预览弹窗是窗口内的全屏覆盖层,其底板与内容按普通渲染类型提交,无法覆盖已提交的 + * 底层内容,因此底层槽位中的方块与玩家背包物品只能在绘制底层界面之前被阻断。 + */ + private static final int PREVIEW_BACKDROP_COLOR = 0xFF000000; + + private final PlatformPreview preview = + new PlatformPreview(VIEW_W, TOP_VIEW_H, VIEW_W, SIDE_VIEW_H); + + private EditBox layersField; + private EditBox startYField; + private EditBox boundaryXField; + private EditBox boundaryZField; + private EditBox roadWidthField; + private PressableAE2Button layersDown; + private PressableAE2Button layersUp; + private PressableAE2Button startYDown; + private PressableAE2Button startYUp; + private PressableAE2Button boundaryXDown; + private PressableAE2Button boundaryXUp; + private PressableAE2Button boundaryZDown; + private PressableAE2Button boundaryZUp; + private PressableAE2Button roadWidthDown; + private PressableAE2Button roadWidthUp; + private PressableAE2Button bedrockButton; + private PressableAE2Button bottomButton; + private PressableAE2Button modeButton; + private PressableAE2Button centerEnabledButton; + private PressableAE2Button applyButton; + private PressableAE2Button teleportButton; + private PressableAE2Button cancelButton; + private PressableAE2Button exportButton; + private PressableAE2Button importButton; + private PressableAE2Button previewButton; + /* 覆盖层按钮只用 addWidget 进 children,不放进 renderables:它们由覆盖层自己渲染, + 否则会在 super.render() 阶段被画到底层 UI 下面。 */ + private PressableAE2Button closePreviewButton; + private PressableAE2Button confirmImportButton; + private PressableAE2Button cancelImportButton; + private Slot pressedSlot; + private boolean updatingFields; + /** 预览弹窗是否打开;打开时独占点击与 ESC。 */ + private boolean previewOpen; + /** 待确认导入的预设;非空时界面显示二次确认层。 */ + private DimensionGenerationConfig pendingImport; + /** 状态提示的语言键与剩余显示时间(tick)。 */ + private String statusKey; + private int statusTicks; + + public DimensionConfigScreen(DimensionConfigMenu menu, Inventory inventory, Component title) { + super(menu, inventory, title); + imageWidth = PANEL_WIDTH; + imageHeight = PANEL_HEIGHT; + inventoryLabelX = 16; + inventoryLabelY = 242; + titleLabelX = 8; + titleLabelY = 7; + } + + @Override + protected void init() { + super.init(); + layersField = createNumberField(106, 36, 52, + Component.translatable("gui.useless_mod.dimension_config.layers"), + menu.getPlatformLayers(), menu::setPlatformLayers, false); + startYField = createNumberField(178, 36, 52, + Component.translatable("gui.useless_mod.dimension_config.start_y"), + menu.getPlatformStartY(), menu::setPlatformStartY, true); + boundaryXField = createNumberField(106, 82, 52, + Component.translatable("gui.useless_mod.dimension_config.boundary_interval_x"), + menu.getBoundaryIntervalX(), menu::setBoundaryIntervalX, false); + boundaryZField = createNumberField(250, 82, 52, + Component.translatable("gui.useless_mod.dimension_config.boundary_interval_z"), + menu.getBoundaryIntervalZ(), menu::setBoundaryIntervalZ, false); + roadWidthField = createNumberField(250, 128, 52, + Component.translatable("gui.useless_mod.dimension_config.road_width"), + menu.getRoadWidth(), value -> { + menu.setRoadWidth(value); + // 输入值低于下限时菜单会钳制,此处把输入框同步回实际生效值。 + if (menu.getRoadWidth() != value) syncFieldsFromMenu(); + }, false); + + layersDown = addStepButton(106, 54, "-", layersField, -1, 1, 256, + "gui.useless_mod.dimension_config.tooltip.decrease"); + layersUp = addStepButton(134, 54, "+", layersField, 1, 1, 256, + "gui.useless_mod.dimension_config.tooltip.increase"); + startYDown = addStepButton(178, 54, "-", startYField, -1, -64, 256, + "gui.useless_mod.dimension_config.tooltip.decrease"); + startYUp = addStepButton(206, 54, "+", startYField, 1, -64, 256, + "gui.useless_mod.dimension_config.tooltip.increase"); + + boundaryXDown = addStepButton(106, 100, "-", boundaryXField, -1, 0, 256, + "gui.useless_mod.dimension_config.tooltip.decrease"); + boundaryXUp = addStepButton(134, 100, "+", boundaryXField, 1, 0, 256, + "gui.useless_mod.dimension_config.tooltip.increase"); + boundaryZDown = addStepButton(250, 100, "-", boundaryZField, -1, 0, 256, + "gui.useless_mod.dimension_config.tooltip.decrease"); + boundaryZUp = addStepButton(278, 100, "+", boundaryZField, 1, 0, 256, + "gui.useless_mod.dimension_config.tooltip.increase"); + roadWidthDown = addStepButton(250, 146, "-", roadWidthField, -1, + DimensionConfigMenu.MIN_ROAD_WIDTH, DimensionConfigMenu.MAX_ROAD_WIDTH, + "gui.useless_mod.dimension_config.tooltip.decrease"); + roadWidthUp = addStepButton(278, 146, "+", roadWidthField, 1, + DimensionConfigMenu.MIN_ROAD_WIDTH, DimensionConfigMenu.MAX_ROAD_WIDTH, + "gui.useless_mod.dimension_config.tooltip.increase"); + + bedrockButton = addButton(236, 36, 84, 16, bedrockText(), + "gui.useless_mod.dimension_config.tooltip.bedrock", button -> { + menu.toggleGenerateBedrock(); + updateToggleButtons(); + }); + bottomButton = addButton(326, 36, 84, 16, bottomText(), + "gui.useless_mod.dimension_config.tooltip.bottom", button -> { + menu.toggleBedrockAtBottom(); + updateToggleButtons(); + }); + + modeButton = addButton(106, 128, 100, 16, modeText(), + "gui.useless_mod.dimension_config.tooltip.mode", button -> { + menu.cycleMode(); + // 切回马路模式时菜单会把道路宽度提升到下限,输入框需同步显示该值。 + syncFieldsFromMenu(); + updateFeatureButtons(); + updateControls(); + }); + centerEnabledButton = addButton(106, 176, 100, 16, centerEnabledText(), + "gui.useless_mod.dimension_config.tooltip.center_marker", button -> { + menu.toggleCenterMarker(); + updateFeatureButtons(); + }); + // 主配置区 y>=196 是空的,正好放预览入口,不必撑宽面板。 + previewButton = addButton(106, 200, 100, 18, + Component.translatable("gui.useless_mod.dimension_config.preview"), + "gui.useless_mod.dimension_config.tooltip.preview", + button -> setPreviewOpen(true)); + + applyButton = addButton(224, 258, 88, 18, + Component.translatable("gui.useless_mod.dimension_config.apply"), + "gui.useless_mod.dimension_config.tooltip.apply", button -> submit(false)); + teleportButton = addButton(318, 258, 88, 18, + Component.translatable("gui.useless_mod.dimension_config.apply_and_teleport"), + "gui.useless_mod.dimension_config.tooltip.apply_and_teleport", button -> submit(true)); + cancelButton = addButton(224, 280, 182, 18, + Component.translatable("gui.useless_mod.dimension_config.cancel"), + "gui.useless_mod.dimension_config.tooltip.cancel", button -> onClose()); + // 右下角:把当前配置导出到剪贴板,或从剪贴板导入一份预设。 + exportButton = addButton(224, 302, 88, 18, + Component.translatable("gui.useless_mod.dimension_config.export"), + "gui.useless_mod.dimension_config.tooltip.export", button -> exportPreset()); + importButton = addButton(318, 302, 88, 18, + Component.translatable("gui.useless_mod.dimension_config.import"), + "gui.useless_mod.dimension_config.tooltip.import", button -> importPreset()); + // 覆盖层按钮只进 children(addWidget),由覆盖层自行渲染;若放进 renderables,会被 + // super.render() 绘制到底层 UI 之下,导致不可见。 + closePreviewButton = addWidget(new PressableAE2Button( + closeButtonX(), closeButtonY(), CLOSE_BUTTON_W, CLOSE_BUTTON_H, + Component.translatable("gui.useless_mod.dimension_config.preview.close"), + button -> setPreviewOpen(false))); + confirmImportButton = addWidget(new PressableAE2Button( + confirmX(), confirmY(), DIALOG_BUTTON_W, DIALOG_BUTTON_H, + Component.translatable("gui.useless_mod.dimension_config.confirm"), + button -> confirmImport(true))); + cancelImportButton = addWidget(new PressableAE2Button( + cancelImportX(), cancelImportY(), DIALOG_BUTTON_W, DIALOG_BUTTON_H, + Component.translatable("gui.useless_mod.dimension_config.cancel_import"), + button -> confirmImport(false))); + syncOverlayButtons(); + + teleportButton.visible = menu.canTeleport(); + updateToggleButtons(); + updateFeatureButtons(); + updateControls(); + refreshPreview(); + } + + /** 覆盖层按钮的 visible 同时决定是否响应点击,所以每次开关覆盖层都要同步。 */ + private void syncOverlayButtons() { + closePreviewButton.visible = previewOpen; + boolean importing = pendingImport != null; + confirmImportButton.visible = importing; + cancelImportButton.visible = importing; + } + + /** + * 把界面当前的编辑状态喂给预览。样式按传送方块所属维度取,与配置写入的目标维度一致。 + * {@code createConfiguration()} 在方块槽位不全时返回空,此时预览显示提示而不是旧图。 + */ + private void refreshPreview() { + PlatformStyle style = UselessDimensions.styleFor(menu.getTargetDimension()); + DimensionGenerationConfig config = menu.createConfiguration() + .map(DimensionGenerationConfig::normalized) + .orElse(null); + preview.refresh(style, config); + } + + private PressableAE2Button addStepButton(int x, int y, String text, EditBox field, + int delta, int min, int max, String tooltipKey) { + return addButton(x, y, 24, 14, Component.literal(text), tooltipKey, + button -> adjust(field, delta, min, max)); + } + + private PressableAE2Button addButton(int x, int y, int width, int height, + Component message, String tooltipKey, + Button.OnPress onPress) { + PressableAE2Button button = new PressableAE2Button( + leftPos + x, topPos + y, width, height, message, onPress); + button.setTooltip(Tooltip.create(Component.translatable(tooltipKey))); + return addRenderableWidget(button); + } + + private EditBox createNumberField(int x, int y, int width, Component message, + int initialValue, java.util.function.IntConsumer setter, + boolean signed) { + EditBox field = new EditBox(font, leftPos + x, topPos + y, width, 14, message); + field.setMaxLength(4); + field.setFilter(value -> value.isEmpty() + || (signed && value.equals("-")) + || value.matches(signed ? "-?\\d+" : "\\d+")); + field.setValue(Integer.toString(initialValue)); + field.setResponder(value -> { + if (updatingFields || value.isEmpty() || value.equals("-")) return; + try { + setter.accept(Integer.parseInt(value)); + } catch (NumberFormatException ignored) { + } + }); + addRenderableWidget(field); + return field; + } + + private Component bedrockText() { + return Component.translatable(menu.isGenerateBedrock() + ? "gui.useless_mod.dimension_config.bedrock_on" + : "gui.useless_mod.dimension_config.bedrock_off"); + } + + private Component bottomText() { + return Component.translatable(menu.isBedrockAtBottom() + ? "gui.useless_mod.dimension_config.bottom_on" + : "gui.useless_mod.dimension_config.bottom_off"); + } + + /** 模式按钮文本:马路 / 多联。标签 "模式" 已单独绘制,按钮内不再重复。 */ + private Component modeText() { + String key = menu.getMode() == DimensionGenerationConfig.Mode.MULTI + ? "multi" : "road"; + return Component.translatable("gui.useless_mod.dimension_config.mode." + key); + } + + private Component centerEnabledText() { + return Component.translatable(menu.isCenterMarkerEnabled() + ? "gui.useless_mod.dimension_config.center_marker_on" + : "gui.useless_mod.dimension_config.center_marker_off"); + } + + private void updateToggleButtons() { + bedrockButton.setMessage(bedrockText()); + bottomButton.setMessage(bottomText()); + } + + private void updateFeatureButtons() { + modeButton.setMessage(modeText()); + centerEnabledButton.setMessage(centerEnabledText()); + // 边界间隔在多联模式下表示合并尺寸,因此始终保留;道路宽度只在马路模式出现。 + boolean boundary = menu.isBoundaryIntervalVisible(); + boolean roadWidthVisible = menu.isRoadWidthVisible(); + boundaryXField.visible = boundary; + boundaryZField.visible = boundary; + boundaryXDown.visible = boundary; + boundaryXUp.visible = boundary; + boundaryZDown.visible = boundary; + boundaryZUp.visible = boundary; + roadWidthField.visible = roadWidthVisible; + roadWidthDown.visible = roadWidthVisible; + roadWidthUp.visible = roadWidthVisible; + centerEnabledButton.visible = menu.isCenterMarkerVisible(); + centerEnabledButton.active = menu.isCenterMarkerVisible(); + } + + private void updateControls() { + boolean complete = menu.isCompleteConfiguration(); + applyButton.active = complete; + teleportButton.active = complete && menu.canTeleport(); + } + + private void submit(boolean teleport) { + if (!menu.isCompleteConfiguration()) { + if (minecraft != null && minecraft.player != null) { + minecraft.player.displayClientMessage( + Component.translatable("gui.useless_mod.dimension_config.invalid"), true); + } + return; + } + menu.createConfiguration().ifPresent(config -> PacketDistributor.sendToServer( + new DimensionConfigSubmitPacket(menu.containerId, config, teleport))); + } + + /** 把当前界面上的配置序列化后写入系统剪贴板。 */ + private void exportPreset() { + DimensionGenerationConfig config = menu.createConfiguration().orElse(null); + if (config == null) { + setStatus("gui.useless_mod.dimension_config.export_invalid"); + return; + } + String text = config.toPresetJson(); + if (text.getBytes(java.nio.charset.StandardCharsets.UTF_8).length + > DimensionGenerationConfig.MAX_PRESET_BYTES) { + setStatus("gui.useless_mod.dimension_config.error.limit"); + return; + } + if (minecraft != null) minecraft.keyboardHandler.setClipboard(text); + setStatus("gui.useless_mod.dimension_config.exported"); + } + + /** 读取剪贴板并解析预设;成功时先进入二次确认,避免误覆盖当前配置。 */ + private void importPreset() { + if (minecraft == null) return; + String text = minecraft.keyboardHandler.getClipboard(); + try { + pendingImport = DimensionGenerationConfig.fromPresetJson(text); + setStatus("gui.useless_mod.dimension_config.import_confirm"); + } catch (DimensionGenerationConfig.PresetException exception) { + setStatus(presetErrorKey(exception.error())); + } + syncOverlayButtons(); + } + + /** 确认导入:套用到界面编辑状态,仍需点「应用」才会保存到世界。 */ + private void confirmImport(boolean accepted) { + if (accepted && pendingImport != null) { + menu.applyPreset(pendingImport); + syncFieldsFromMenu(); + updateToggleButtons(); + updateFeatureButtons(); + updateControls(); + setStatus("gui.useless_mod.dimension_config.imported"); + } + pendingImport = null; + syncOverlayButtons(); + } + + /** 将菜单当前数值同步回输入框,避免导入后界面与配置不一致。 */ + private void syncFieldsFromMenu() { + updatingFields = true; + layersField.setValue(Integer.toString(menu.getPlatformLayers())); + startYField.setValue(Integer.toString(menu.getPlatformStartY())); + boundaryXField.setValue(Integer.toString(menu.getBoundaryIntervalX())); + boundaryZField.setValue(Integer.toString(menu.getBoundaryIntervalZ())); + roadWidthField.setValue(Integer.toString(menu.getRoadWidth())); + updatingFields = false; + } + + private static String presetErrorKey(DimensionGenerationConfig.PresetError error) { + return switch (error) { + case INVALID_TEXT -> "gui.useless_mod.dimension_config.error.invalid_text"; + case UNSUPPORTED_VERSION -> "gui.useless_mod.dimension_config.error.unsupported_version"; + case INVALID_STRUCTURE -> "gui.useless_mod.dimension_config.error.invalid_structure"; + case BLOCKED_BLOCK -> "gui.useless_mod.dimension_config.error.blocked_block"; + case LIMIT -> "gui.useless_mod.dimension_config.error.limit"; + }; + } + + private void setStatus(String key) { + statusKey = key; + statusTicks = 100; + } + + private void adjust(EditBox field, int delta, int min, int max) { + int value; + try { + value = Integer.parseInt(field.getValue()); + } catch (NumberFormatException exception) { + value = min; + } + value = Math.max(min, Math.min(max, value + delta)); + updatingFields = true; + field.setValue(Integer.toString(value)); + updatingFields = false; + if (field == layersField) menu.setPlatformLayers(value); + else if (field == startYField) menu.setPlatformStartY(value); + else if (field == boundaryXField) menu.setBoundaryIntervalX(value); + else if (field == boundaryZField) menu.setBoundaryIntervalZ(value); + else if (field == roadWidthField) menu.setRoadWidth(value); + updateControls(); + } + + @Override + protected void containerTick() { + super.containerTick(); + if (statusTicks > 0) statusTicks--; + updateControls(); + refreshPreview(); + } + + /** 界面移除时释放预览占用的动态贴图,避免泄漏。 */ + @Override + public void removed() { + super.removed(); + preview.close(); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + // 预览以全屏覆盖层呈现,此时不渲染底层界面。底层槽位中的预设方块与玩家背包物品 + // 在提交时进入 RenderBuffers 的固定缓冲,无法被随后绘制的覆盖层遮挡,因此只能从 + // 绘制阶段跳过;缺少该分支时两者会显示在预览内容之上。 + if (previewOpen) { + graphics.fill(0, 0, width, height, PREVIEW_BACKDROP_COLOR); + graphics.flush(); + renderPreviewPopup(graphics, mouseX, mouseY, partialTick); + return; + } + super.render(graphics, mouseX, mouseY, partialTick); + if (pendingImport != null) { + // 必须先 flush:底层 UI 里的方块物品走的是 RenderBuffers 的固定缓冲 + // (solid / cutout / translucent),不主动提交的话它们会在帧末才绘制, + // 结果盖在覆盖层上面。 + graphics.flush(); + renderImportConfirm(graphics, mouseX, mouseY, partialTick); + } + } + + /* ================= 生成预览弹窗 ================= */ + + private int popupX() { + return (width - POPUP_WIDTH) / 2; + } + + private int popupY() { + return Math.max(4, (height - POPUP_HEIGHT) / 2); + } + + private int closeButtonX() { + return popupX() + POPUP_WIDTH - POPUP_PAD - CLOSE_BUTTON_W; + } + + private int closeButtonY() { + return popupY() + POPUP_HEIGHT - 24; + } + + private void setPreviewOpen(boolean open) { + previewOpen = open; + syncOverlayButtons(); + if (open) refreshPreview(); + } + + /** 居中显示俯视示意图与侧视剖面;全屏底板由 {@link #render} 绘制。 */ + private void renderPreviewPopup(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + int x = popupX(); + int y = popupY(); + MachineScreenStyle.drawPanel(graphics, x, y, POPUP_WIDTH, POPUP_HEIGHT); + graphics.drawString(font, Component.translatable("gui.useless_mod.dimension_config.preview"), + x + POPUP_PAD, y + 7, MachineScreenStyle.TEXT_COLOR, false); + + int viewX = x + POPUP_PAD; + int topY = y + 30; + int sideY = topY + TOP_VIEW_H + 14; + int infoY = sideY + SIDE_VIEW_H + 7; + + if (!preview.isValid()) { + drawPopupLine(graphics, Component.translatable( + "gui.useless_mod.dimension_config.preview.empty"), + viewX, topY + 4, MachineScreenStyle.MUTED_TEXT_COLOR); + graphics.flush(); + closePreviewButton.render(graphics, mouseX, mouseY, partialTick); + return; + } + + drawPopupLabel(graphics, "gui.useless_mod.dimension_config.preview.top", viewX, topY - 10); + drawPopupLabel(graphics, "gui.useless_mod.dimension_config.preview.side", viewX, sideY - 10); + graphics.renderOutline(viewX - 1, topY - 1, VIEW_W + 2, TOP_VIEW_H + 2, + MachineScreenStyle.SLOT_SHADOW_COLOR); + graphics.renderOutline(viewX - 1, sideY - 1, VIEW_W + 2, SIDE_VIEW_H + 2, + MachineScreenStyle.SLOT_SHADOW_COLOR); + // 预览贴图走的是立即绘制(GuiGraphics 的 blit 内部直接 drawWithShader), + // 先把上面这些排队的内容提交掉,否则它们会在帧末提交时压住预览。 + graphics.flush(); + preview.renderTop(graphics, viewX, topY); + preview.renderSide(graphics, viewX, sideY); + + int line = 0; + drawPopupLine(graphics, Component.translatable( + "gui.useless_mod.dimension_config.preview.period", + preview.periodBlocksX(), preview.periodBlocksZ()), + viewX, infoY + line++ * INFO_LINE_HEIGHT, MachineScreenStyle.SUBTLE_TEXT_COLOR); + drawPopupLine(graphics, renderModeText(), + viewX, infoY + line++ * INFO_LINE_HEIGHT, MachineScreenStyle.SUBTLE_TEXT_COLOR); + drawPopupLine(graphics, Component.translatable( + "gui.useless_mod.dimension_config.preview.range", + preview.bottomY(), preview.topY(), preview.topY() - preview.bottomY()), + viewX, infoY + line++ * INFO_LINE_HEIGHT, MachineScreenStyle.SUBTLE_TEXT_COLOR); + if (preview.bedrockY() != Integer.MIN_VALUE) { + drawPopupLine(graphics, Component.translatable( + "gui.useless_mod.dimension_config.preview.bedrock", preview.bedrockY()), + viewX, infoY + line * INFO_LINE_HEIGHT, MachineScreenStyle.SUBTLE_TEXT_COLOR); + } + graphics.flush(); + closePreviewButton.render(graphics, mouseX, mouseY, partialTick); + renderPreviewTooltip(graphics, mouseX, mouseY, viewX, topY); + } + + private void drawPopupLabel(GuiGraphics graphics, String key, int x, int y) { + graphics.drawString(font, Component.translatable(key), x, y, + MachineScreenStyle.MUTED_TEXT_COLOR, false); + } + + private void drawPopupLine(GuiGraphics graphics, Component text, int x, int y, int color) { + String value = text.getString(); + if (font.width(value) > VIEW_W) { + value = font.plainSubstrByWidth(value, VIEW_W - 3) + "..."; + } + graphics.drawString(font, value, x, y, color, false); + } + + /** 说明当前是真实材质还是降级成了色块;色块时顺便给出一个像素代表多少格。 */ + private Component renderModeText() { + if (preview.isTextured()) { + return Component.translatable("gui.useless_mod.dimension_config.preview.textured"); + } + int blocksPerPixel = preview.blocksPerPixel(); + if (blocksPerPixel <= 1) { + return Component.translatable("gui.useless_mod.dimension_config.preview.color"); + } + return Component.translatable("gui.useless_mod.dimension_config.preview.blocks", + blocksPerPixel); + } + + /** 悬停俯视图时提示所指格子的坐标、方块与角色。 */ + private void renderPreviewTooltip(GuiGraphics graphics, int mouseX, int mouseY, + int viewX, int viewY) { + if (!preview.isValid()) return; + int localX = mouseX - viewX; + int localY = mouseY - viewY; + if (localX < 0 || localY < 0 || localX >= VIEW_W || localY >= TOP_VIEW_H) return; + int[] block = preview.blockAt(localX, localY); + if (block == null) return; + BlockState state = preview.stateAtBlock(block[0], block[1]); + if (state == null) return; + + Component tooltip = Component.literal("X=" + block[0] + ", Z=" + block[1]) + .append(Component.literal("\n")) + .append(state.getBlock().getName()); + String role = preview.roleKey(state); + if (role != null) { + tooltip = tooltip.copy().append(Component.literal("\n")) + .append(Component.translatable( + "gui.useless_mod.dimension_config.preview.role." + role)); + } + graphics.renderTooltip(font, tooltip, mouseX, mouseY); + } + + /** 底部状态提示,导入导出后短暂显示结果;画在标签层以免盖住物品提示。 */ + private void renderStatus(GuiGraphics graphics) { + if (statusTicks <= 0 || statusKey == null) return; + String text = Component.translatable(statusKey).getString(); + if (font.width(text) > 404) text = font.plainSubstrByWidth(text, 401) + "..."; + graphics.drawString(font, text, 8, 336, MachineScreenStyle.TEXT_COLOR, false); + } + + /** 导入二次确认层:遮住整个界面,底板与按钮都用本模组统一的面板与按钮样式。 */ + private void renderImportConfirm(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + graphics.fill(0, 0, width, height, 0x99000000); + graphics.flush(); + int boxLeft = leftPos + 90; + int boxTop = topPos + 140; + MachineScreenStyle.drawPanel(graphics, boxLeft, boxTop, imageWidth - 180, 60); + graphics.drawCenteredString(font, Component.translatable( + "gui.useless_mod.dimension_config.import_confirm"), + leftPos + imageWidth / 2, boxTop + 12, MachineScreenStyle.TEXT_COLOR); + graphics.flush(); + confirmImportButton.render(graphics, mouseX, mouseY, partialTick); + cancelImportButton.render(graphics, mouseX, mouseY, partialTick); + } + + private int confirmX() { + return leftPos + imageWidth / 2 - 90; + } + + private int confirmY() { + return topPos + 168; + } + + private int cancelImportX() { + return leftPos + imageWidth / 2 + 6; + } + + private int cancelImportY() { + return topPos + 168; + } + + @Override + protected void renderBg(GuiGraphics graphics, float partialTick, int mouseX, int mouseY) { + MachineScreenStyle.drawPanel(graphics, leftPos, topPos, imageWidth, imageHeight); + MachineScreenStyle.drawInset(graphics, leftPos + 4, topPos + 18, leftPos + 94, topPos + 230); + MachineScreenStyle.drawInset(graphics, leftPos + 98, topPos + 18, leftPos + imageWidth - 4, topPos + 230); + MachineScreenStyle.drawSlotGroup(graphics, leftPos, topPos, 16, 34, 1, 3); + // 多联模式隐藏边界交替、道路主体与中心标记槽位,连同其槽位底板一起隐藏。 + if (menu.isBoundarySlotVisible()) { + MachineScreenStyle.drawSlotGroup(graphics, leftPos, topPos, 16, 100, 1, 2); + } + if (menu.isRoadPatternSelected()) { + MachineScreenStyle.drawSlotGroup(graphics, leftPos, topPos, 16, 148, 1, 3, 18, 17); + } + if (menu.isCenterMarkerVisible()) { + MachineScreenStyle.drawSlotGroup(graphics, leftPos, topPos, 16, 212, 1, 1); + } + MachineScreenStyle.drawInset(graphics, leftPos + 4, topPos + 236, + leftPos + 198, topPos + 334); + MachineScreenStyle.drawInset(graphics, leftPos + 220, topPos + 236, + leftPos + imageWidth - 4, topPos + 334); + for (Slot slot : menu.slots) { + if (!slot.isActive()) continue; + if (isRoadSlot(slot)) { + MachineScreenStyle.drawRoadSlotBackground(graphics, leftPos, topPos, slot, + isRoadSlotActive(slot), slot == pressedSlot); + } else { + MachineScreenStyle.drawSlotBackground(graphics, leftPos, topPos, slot); + } + } + } + + @Override + protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) { + String titleText = title.getString(); + if (font.width(titleText) > 84) titleText = font.plainSubstrByWidth(titleText, 81) + "..."; + graphics.drawString(font, titleText, titleLabelX, titleLabelY, + MachineScreenStyle.TEXT_COLOR, false); + String target = menu.getTargetDimension().location().toString(); + if (font.width(target) > 86) target = font.plainSubstrByWidth(target, 83) + "..."; + graphics.drawString(font, target, 8, 19, MachineScreenStyle.MUTED_TEXT_COLOR, false); + + drawLeftLabel(graphics, "gui.useless_mod.dimension_config.border_block", 36); + drawLeftLabel(graphics, "gui.useless_mod.dimension_config.fill_block", 54); + drawLeftLabel(graphics, "gui.useless_mod.dimension_config.center_block", 72); + boolean boundarySlot = menu.isBoundarySlotVisible(); + boolean centerMarkerVisible = menu.isCenterMarkerVisible(); + boolean roadPattern = menu.isRoadPatternSelected(); + if (boundarySlot) { + drawLeftLabel(graphics, "gui.useless_mod.dimension_config.boundary_alternate_1", 102); + drawLeftLabel(graphics, "gui.useless_mod.dimension_config.boundary_alternate_2", 120); + } + if (roadPattern) { + drawLeftLabel(graphics, "gui.useless_mod.dimension_config.road_surface", 150); + drawLeftLabel(graphics, "gui.useless_mod.dimension_config.road_edge", 167); + drawLeftLabel(graphics, "gui.useless_mod.dimension_config.road_center_marking", 184); + } + if (centerMarkerVisible) { + drawLeftLabel(graphics, "gui.useless_mod.dimension_config.center_marker", 214); + } + + drawLabel(graphics, "gui.useless_mod.dimension_config.layers", 106, 26); + drawLabel(graphics, "gui.useless_mod.dimension_config.start_y", 178, 26); + drawLabel(graphics, "gui.useless_mod.dimension_config.boundary_interval_x", 106, 72); + drawLabel(graphics, "gui.useless_mod.dimension_config.boundary_interval_z", 250, 72); + drawLabel(graphics, "gui.useless_mod.dimension_config.mode", 106, 118); + if (roadPattern) { + drawLabel(graphics, "gui.useless_mod.dimension_config.road_width", 250, 118); + } + if (centerMarkerVisible) { + drawLabel(graphics, "gui.useless_mod.dimension_config.center_options", 106, 166); + } + graphics.drawString(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, + MachineScreenStyle.TEXT_COLOR, false); + renderStatus(graphics); + } + + private void drawLabel(GuiGraphics graphics, String key, int x, int y) { + graphics.drawString(font, Component.translatable(key), x, y, + MachineScreenStyle.MUTED_TEXT_COLOR, false); + } + + private void drawLeftLabel(GuiGraphics graphics, String key, int y) { + Component label = Component.translatable(key); + String text = label.getString(); + if (font.width(text) > 58) text = font.plainSubstrByWidth(text, 58); + graphics.drawString(font, text, 34, y, MachineScreenStyle.MUTED_TEXT_COLOR, false); + } + + private boolean isRoadSlot(Slot slot) { + return slot == menu.getGhostSlot(DimensionConfigMenu.BOUNDARY_A_SLOT) + || slot == menu.getGhostSlot(DimensionConfigMenu.BOUNDARY_B_SLOT) + || slot == menu.getGhostSlot(DimensionConfigMenu.ROAD_A_SLOT) + || slot == menu.getGhostSlot(DimensionConfigMenu.ROAD_B_SLOT) + || slot == menu.getGhostSlot(DimensionConfigMenu.ROAD_C_SLOT); + } + + private boolean isRoadSlotActive(Slot slot) { + int index = roadSlotIndex(slot); + return index < 0 || menu.isGhostSlotActive(index); + } + + private int roadSlotIndex(Slot slot) { + if (slot == menu.getGhostSlot(DimensionConfigMenu.BOUNDARY_A_SLOT)) { + return DimensionConfigMenu.BOUNDARY_A_SLOT; + } + if (slot == menu.getGhostSlot(DimensionConfigMenu.BOUNDARY_B_SLOT)) { + return DimensionConfigMenu.BOUNDARY_B_SLOT; + } + if (slot == menu.getGhostSlot(DimensionConfigMenu.ROAD_A_SLOT)) { + return DimensionConfigMenu.ROAD_A_SLOT; + } + if (slot == menu.getGhostSlot(DimensionConfigMenu.ROAD_B_SLOT)) { + return DimensionConfigMenu.ROAD_B_SLOT; + } + if (slot == menu.getGhostSlot(DimensionConfigMenu.ROAD_C_SLOT)) { + return DimensionConfigMenu.ROAD_C_SLOT; + } + return -1; + } + + private Slot slotAt(double mouseX, double mouseY) { + double localX = mouseX - leftPos; + double localY = mouseY - topPos; + for (Slot slot : menu.slots) { + if (slot instanceof DimensionConfigMenu.GhostSlot ghost && ghost.isHidden()) continue; + if (slot.isActive() && localX >= slot.x && localX < slot.x + 16 + && localY >= slot.y && localY < slot.y + 16) { + return slot; + } + } + return null; + } + + @Override + protected void renderSlot(GuiGraphics graphics, Slot slot) { + super.renderSlot(graphics, slot); + if (isRoadSlot(slot) && !isRoadSlotActive(slot)) { + graphics.fill(slot.x, slot.y, slot.x + 16, slot.y + 16, 0x66000000); + } + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + // 确认层存在时,ESC 只取消导入,不关闭整个界面。 + if (pendingImport != null && keyCode == GLFW.GLFW_KEY_ESCAPE) { + confirmImport(false); + return true; + } + // 预览弹窗同理:ESC 只关弹窗;弹窗期间独占按键,数字键等不再作用于底层槽位。 + if (previewOpen) { + if (keyCode == GLFW.GLFW_KEY_ESCAPE) { + setPreviewOpen(false); + } + return true; + } + if (keyCode != GLFW.GLFW_KEY_ESCAPE) { + EditBox[] fields = {layersField, startYField, + boundaryXField, boundaryZField, roadWidthField}; + for (EditBox field : fields) { + if (field != null && (field.keyPressed(keyCode, scanCode, modifiers) + || field.canConsumeInput())) return true; + } + } + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + // 覆盖层独占点击:只转发给覆盖层自己的按钮,避免穿透到底下的槽位与控件。 + if (pendingImport != null) { + confirmImportButton.mouseClicked(mouseX, mouseY, button); + cancelImportButton.mouseClicked(mouseX, mouseY, button); + return true; + } + if (previewOpen) { + closePreviewButton.mouseClicked(mouseX, mouseY, button); + return true; + } + Slot slot = slotAt(mouseX, mouseY); + if (slot == null || !slot.isActive()) return super.mouseClicked(mouseX, mouseY, button); + if (isRoadSlot(slot) && !isRoadSlotActive(slot)) return true; + pressedSlot = isRoadSlot(slot) && isRoadSlotActive(slot) ? slot : null; + boolean handled = super.mouseClicked(mouseX, mouseY, button); + if (!handled) pressedSlot = null; + return handled; + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + // 覆盖层按钮不在 renderables 里,事件也要手动转发,否则按下后松手不会触发。 + if (pendingImport != null) { + confirmImportButton.mouseReleased(mouseX, mouseY, button); + confirmImportButton.releaseVisualState(); + cancelImportButton.mouseReleased(mouseX, mouseY, button); + cancelImportButton.releaseVisualState(); + return true; + } + if (previewOpen) { + closePreviewButton.mouseReleased(mouseX, mouseY, button); + closePreviewButton.releaseVisualState(); + return true; + } + pressedSlot = null; + layersDown.releaseVisualState(); + layersUp.releaseVisualState(); + startYDown.releaseVisualState(); + startYUp.releaseVisualState(); + boundaryXDown.releaseVisualState(); + boundaryXUp.releaseVisualState(); + boundaryZDown.releaseVisualState(); + boundaryZUp.releaseVisualState(); + roadWidthDown.releaseVisualState(); + roadWidthUp.releaseVisualState(); + bedrockButton.releaseVisualState(); + bottomButton.releaseVisualState(); + modeButton.releaseVisualState(); + centerEnabledButton.releaseVisualState(); + previewButton.releaseVisualState(); + applyButton.releaseVisualState(); + teleportButton.releaseVisualState(); + cancelButton.releaseVisualState(); + exportButton.releaseVisualState(); + importButton.releaseVisualState(); + return super.mouseReleased(mouseX, mouseY, button); + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/gui/MachineScreenStyle.java b/src/main/java/com/sorrowmist/useless/client/gui/MachineScreenStyle.java new file mode 100644 index 00000000..ce138a76 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/gui/MachineScreenStyle.java @@ -0,0 +1,89 @@ +package com.sorrowmist.useless.client.gui; + +import appeng.client.gui.style.BackgroundGenerator; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.world.inventory.Slot; + +final class MachineScreenStyle { + static final int PANEL_COLOR = 0xFFCBCCD4; + static final int HIGHLIGHT_COLOR = 0xFFF2F2F2; + static final int SLOT_COLOR = 0xFFADB0C4; + static final int SLOT_SHADOW_COLOR = 0xFF9A9FB4; + /** + * 未启用槽位底板的透明度。 + * + *

取值沿用 AE2 对未启用可选槽位的淡化系数:未放入容量卡而不可用的区域仍保留底板轮廓, + * 但与面板按该透明度混合后明显浅于可用槽位。混合交由渲染管线完成,面板色调整时 + * 无需重新推导常量。

+ */ + private static final int DISABLED_SLOT_ALPHA = 0x33; + private static final int ROAD_SLOT_PRESSED = 0xFF8F93A7; + private static final int ROAD_SLOT_DISABLED = 0xFF777B8D; + static final int TEXT_COLOR = 0xFF413F54; + static final int MUTED_TEXT_COLOR = 0xFF878FA5; + static final int SUBTLE_TEXT_COLOR = 0xFF6D7287; + static final int ERROR_TEXT_COLOR = 0xFFCE2401; + /** 输入框底 / 描边:比 PANEL_COLOR 深一档,用来在浅色面板上圈出「这里能填」。 */ + static final int FIELD_BORDER_COLOR = 0xFF8B8FA3; + static final int FIELD_FILL_COLOR = 0xFFE4E5EA; + + private MachineScreenStyle() { + } + + static void drawPanel(GuiGraphics graphics, int left, int top, int width, int height) { + BackgroundGenerator.draw(width, height, graphics, left, top); + } + + static void drawInset(GuiGraphics graphics, int left, int top, int right, int bottom) { + graphics.fill(left, top, right, bottom, HIGHLIGHT_COLOR); + graphics.fill(left + 1, top + 1, right - 1, bottom - 1, PANEL_COLOR); + } + + static void drawSlotGroup(GuiGraphics graphics, int leftPos, int topPos, + int x, int y, int columns, int rows) { + drawSlotGroup(graphics, leftPos, topPos, x, y, columns, rows, 18, 18); + } + + static void drawSlotGroup(GuiGraphics graphics, int leftPos, int topPos, + int x, int y, int columns, int rows, + int columnSpacing, int rowSpacing) { + int left = leftPos + x - 1; + int top = topPos + y - 1; + int right = leftPos + x + (columns - 1) * columnSpacing + 17; + int bottom = topPos + y + (rows - 1) * rowSpacing + 17; + drawInset(graphics, left, top, right, bottom); + } + + static void drawSlotBackground(GuiGraphics graphics, int leftPos, int topPos, Slot slot) { + int left = leftPos + slot.x; + int top = topPos + slot.y; + graphics.fill(left, top, left + 16, top + 16, SLOT_COLOR); + graphics.fill(left, top, left + 16, top + 1, SLOT_SHADOW_COLOR); + } + + /** + * 绘制未启用槽位的底板:保留与可用槽位相同的轮廓,仅整体淡化。 + * + *

淡化由渲染管线的颜色混合完成,而非改用更浅的字面量颜色,使高亮边框与底部暗边在 + * 同一系数下一起变淡;颜色字面量各自减淡会让两层失去原有的相对明暗关系。

+ */ + static void drawDisabledSlotBackground(GuiGraphics graphics, int leftPos, int topPos, Slot slot) { + int left = leftPos + slot.x; + int top = topPos + slot.y; + graphics.fill(left, top, left + 16, top + 16, withAlpha(SLOT_COLOR)); + graphics.fill(left, top, left + 16, top + 1, withAlpha(SLOT_SHADOW_COLOR)); + } + + /** 以 {@link #DISABLED_SLOT_ALPHA} 替换颜色的透明度分量。 */ + private static int withAlpha(int color) { + return color & 0x00FFFFFF | DISABLED_SLOT_ALPHA << 24; + } + + static void drawRoadSlotBackground(GuiGraphics graphics, int leftPos, int topPos, + Slot slot, boolean active, boolean pressed) { + int left = leftPos + slot.x; + int top = topPos + slot.y; + int color = !active ? ROAD_SLOT_DISABLED : pressed ? ROAD_SLOT_PRESSED : SLOT_COLOR; + graphics.fill(left, top, left + 16, top + 16, color); + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/gui/MiningStatusGui.java b/src/main/java/com/sorrowmist/useless/client/gui/MiningStatusGui.java new file mode 100644 index 00000000..672a3999 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/gui/MiningStatusGui.java @@ -0,0 +1,176 @@ +package com.sorrowmist.useless.client.gui; + +import com.sorrowmist.useless.content.items.BeefToolVariants; +import com.sorrowmist.useless.core.common.KeyBindings; +import com.sorrowmist.useless.data.PlayerMiningData; +import com.sorrowmist.useless.utils.UComponentUtils; +import com.sorrowmist.useless.utils.mining.MiningDispatcher; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.network.chat.Component; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Nullable; + +public class MiningStatusGui { + private static final int BG_MAIN = 0xB0202020; + private static final int BG_SHADOW = 0x40000000; + private static final int BORDER_LIGHT = 0x40FFFFFF; + + private static final int COLOR_ENHANCED = 0xFF4DD0E1; + private static final int COLOR_NORMAL = 0xFF66BB6A; + private static final int COLOR_FORCE_ON = 0xFFFF7043; + private static final int COLOR_MUTED = 0xFF9E9E9E; + + public static void render(GuiGraphics g, float partialTick) { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + if (player == null || mc.screen != null) return; + if (!KeyBindings.isChainMiningKeyDown()) return; + + // 该面板描述的是造化杖自身的连锁状态,未持有该工具时不存在可展示的内容, + // 因此在绘制前直接返回,避免出现与手持物品无关的状态读数。 + ItemStack stack = resolveHeldBeafTool(player); + if (stack == null) return; + + boolean enhancedChainMining = UComponentUtils.isEnhancedChainMiningEnabled(stack); + boolean forceMiningEnabled = UComponentUtils.isForceMiningEnabled(stack); + + String statusKey; + int statusColor; + + if (enhancedChainMining) { + statusKey = "gui.useless_mod.status.enhanced"; + statusColor = COLOR_ENHANCED; + } else { + statusKey = "gui.useless_mod.status.normal"; + statusColor = COLOR_NORMAL; + } + + Component statusValue = Component.translatable(statusKey); + // 翻译键与显示名均使用本项目自有命名,不沿用 FTB Ultimine 的名称,以避免与该外部项目产生关联。 + Component statusLine = Component.translatable( + "gui.useless_mod.chain_mining_status", + statusValue + ); + + Component forceLabel = Component.translatable("gui.useless_mod.force_mining_label"); + Component forceValue = Component.translatable( + forceMiningEnabled ? "gui.useless_mod.force.enabled" : "gui.useless_mod.force.disabled" + ); + + PlayerMiningData data = MiningDispatcher.getPlayerData(player); + int count = data != null ? data.getCachedBlocks().size() : 0; + + // 名称后附方向说明:隧道与对角类形状的走向取决于点击面与玩家朝向,仅凭名称无法预判。 + Component shapeText = data != null + ? Component.translatable("gui.useless_mod.shape_label", + Component.translatable(data.getShape().getTranslationKey()), + Component.translatable(data.getShape().getDescriptionKey())) + : Component.empty(); + + // 切换提示行:说明滚轮组合键,缺少该提示时玩家没有任何途径得知形状可以切换。 + Component hintText = Component.translatable("gui.useless_mod.shape_hint"); + + // 数量行显示「本次预览 / 配置上限」。上限为 0 表示客户端尚未收到同步, + // 此时只显示当前数量,避免出现「N / 0」这类无意义读数。 + int maxBlocks = data != null ? data.getMaxBlocks() : 0; + Component countText = maxBlocks > 0 + ? Component.translatable("gui.useless_mod.mining_count_limit", count, maxBlocks) + : Component.translatable("gui.useless_mod.mining_count", count); + + int forceColor = forceMiningEnabled ? COLOR_FORCE_ON : COLOR_MUTED; + + /* ========= 尺寸计算 ========= */ + int padding = 6; + int lineSpacing = 4; + int lineHeight = mc.font.lineHeight; + + int width = Math.max( + mc.font.width(statusLine), + Math.max( + mc.font.width(forceLabel) + mc.font.width(forceValue), + Math.max(mc.font.width(countText), + Math.max(mc.font.width(shapeText), mc.font.width(hintText))) + ) + ) + padding * 2 + 6; + + // 形状行与切换提示行在持有造化杖期间恒定存在,面板高度固定包含这两行。 + int height = padding * 2 + lineHeight * 5 + lineSpacing * 4 + 1; + + int x = 0; + int y = 0; + + /* ========= 背景 ========= */ + g.fill(x + 2, y + 2, x + width + 2, y + height + 2, BG_SHADOW); + g.fill(x, y, x + width, y + height, BG_MAIN); + + // 左侧状态强调条 + g.fill(x, y, x + 3, y + height, statusColor); + + /* ========= 文本 ========= */ + int textX = x + padding + 3; + int textY = y + padding; + + // 第一行:状态 + g.drawString(mc.font, statusLine, textX, textY, statusColor, true); + + // 分割线 + int separatorY = textY + lineHeight + lineSpacing; + g.fill( + x + 3, + separatorY, + x + width, + separatorY + 1, + BORDER_LIGHT + ); + + // 第二行:强制挖掘 + int line2Y = separatorY + 1 + lineSpacing; + g.drawString(mc.font, forceLabel, + textX, + line2Y, + 0xFFFFFFFF, + true + ); + + g.drawString(mc.font, forceValue, + textX + mc.font.width(forceLabel), + line2Y, + forceColor, + true + ); + + // 第三行:挖掘数量 + int line3Y = line2Y + lineHeight + lineSpacing; + g.drawString(mc.font, countText, + textX, + line3Y, + 0xFFFFFFFF, + true + ); + + // 第四、五行:连锁形状与切换提示 + int shapeY = line3Y + lineHeight + lineSpacing; + g.drawString(mc.font, shapeText, textX, shapeY, 0xFFFFFFFF, true); + g.drawString(mc.font, hintText, textX, shapeY + lineHeight + lineSpacing, COLOR_MUTED, true); + } + + /** + * 取玩家当前持有的造化杖。 + *

+ * 主手与副手依次查找,任一持有即返回对应物品;双手均未持有时返回 {@code null}。 + * 判定范围与连锁高亮一致,覆盖基础形态与全部工具模式变体。 + */ + @Nullable + private static ItemStack resolveHeldBeafTool(LocalPlayer player) { + for (InteractionHand hand : InteractionHand.values()) { + ItemStack stack = player.getItemInHand(hand); + if (BeefToolVariants.isBeafTool(stack)) { + return stack; + } + } + return null; + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/gui/ModeWheelScreen.java b/src/main/java/com/sorrowmist/useless/client/gui/ModeWheelScreen.java new file mode 100644 index 00000000..44b17f5f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/gui/ModeWheelScreen.java @@ -0,0 +1,1745 @@ +package com.sorrowmist.useless.client.gui; + +import com.mojang.blaze3d.platform.InputConstants; +import com.mojang.blaze3d.systems.RenderSystem; +import com.sorrowmist.useless.api.enums.tool.ConstructionWandCoreMode; +import com.sorrowmist.useless.api.enums.tool.EnchantMode; +import com.sorrowmist.useless.api.enums.tool.ToolTypeMode; +import com.sorrowmist.useless.client.BeefAutoClicker; +import com.sorrowmist.useless.content.menus.ChainGroupMenu; +import com.sorrowmist.useless.core.common.KeyBindings; +import com.sorrowmist.useless.core.component.UComponents; +import com.sorrowmist.useless.core.config.BeefToolProtectionManager; +import com.sorrowmist.useless.core.config.ChainGroupManager; +import com.sorrowmist.useless.content.items.EndlessBeafItem; +import com.sorrowmist.useless.data.BeefToolLayout; +import com.sorrowmist.useless.data.BeefToolModuleRegistry; +import com.sorrowmist.useless.network.BeefToolLayoutRequestPacket; +import com.sorrowmist.useless.network.BeefToolLayoutUpdatePacket; +import com.sorrowmist.useless.network.ConstructionWandCorePacket; +import com.sorrowmist.useless.network.EnchantmentSwitchPacket; +import com.sorrowmist.useless.network.ModeTogglePacket; +import com.sorrowmist.useless.network.ToolTypeModeSwitchPacket; +import com.sorrowmist.useless.utils.UselessItemUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.Renderable; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.ItemStack; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; +import org.lwjgl.glfw.GLFW; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +public class ModeWheelScreen extends Screen { + private static final int PANEL_MAX_WIDTH = 480; + private static final int PANEL_MARGIN = 6; + private static final int HEADER_HEIGHT = 42; + private static final int NORMAL_FOOTER_HEIGHT = 20; + private static final int EDITOR_FOOTER_HEIGHT = 38; + private static final int CONTENT_PADDING = 4; + private static final int CARD_GAP = 5; + private static final int CARD_HEADER_HEIGHT = 18; + private static final int CARD_BOTTOM_PADDING = 6; + private static final int MODULE_HEIGHT = 18; + private static final int MODULE_GAP = 2; + /** 连锁等价组齿轮图标的边长(画在连锁挖掘模块按钮右端)。 */ + private static final int GEAR_SIZE = 12; + private static final int PAGE_TAB_HEIGHT = 15; + private static final int PAGE_TAB_WIDTH = 82; + private static final int DRAG_THRESHOLD = 4; + private static final int DESIGN_HEIGHT = 320; + + private final List toolbarButtons = new ArrayList<>(); + private final List modeButtons = new ArrayList<>(); + private final List groupNameFields = new ArrayList<>(); + private final List cardLayouts = new ArrayList<>(); + private final List moduleLayouts = new ArrayList<>(); + private final List pageTabRects = new ArrayList<>(); + + private ItemStack targetItem; + private BeefToolLayout layout = BeefToolModuleRegistry.defaultLayout(); + private EditBox pageNameField; + private DragState drag; + private String lastAvailabilitySignature = ""; + private String statusKey; + private int statusTicks; + private String pendingImportText; + private boolean confirmImport; + private boolean editing; + private boolean awaitingLayout = true; + private boolean layoutRequested; + private int pageTabOffset; + private int scrollOffset; + private int maxScroll; + private int contentLeft; + private int contentTop; + private int contentRight; + private int contentBottom; + private int panelLeft; + private int panelTop; + private int panelWidth; + private int panelHeight; + private int uiWidth; + private int uiHeight; + private float uiScale = 1.0F; + private float uiOffsetX; + private float uiOffsetY; + + public ModeWheelScreen(ItemStack targetItem) { + super(Component.translatable("gui.useless_mod.mode_config.title")); + this.targetItem = targetItem == null ? ItemStack.EMPTY : targetItem; + } + + @Override + protected void init() { + super.init(); + toolbarButtons.clear(); + modeButtons.clear(); + groupNameFields.clear(); + pageNameField = null; + + calculatePanelBounds(); + calculateGeometry(); + createToolbarWidgets(); + if (!awaitingLayout && editing) { + createEditorNameFields(); + } + if (!awaitingLayout && !editing) { + createModeButtons(); + } + + if (!layoutRequested && minecraft != null && minecraft.player != null) { + layoutRequested = true; + PacketDistributor.sendToServer(new BeefToolLayoutRequestPacket()); + } + } + + private void calculatePanelBounds() { + calculateUiScale(); + int availableWidth = Math.max(1, uiWidth - PANEL_MARGIN * 2); + int availableHeight = Math.max(1, uiHeight - PANEL_MARGIN * 2); + int desiredWidth = desiredContentWidth() + PANEL_MARGIN * 2; + int desiredHeight = HEADER_HEIGHT + desiredContentHeight() + + (editing ? EDITOR_FOOTER_HEIGHT : NORMAL_FOOTER_HEIGHT); + panelWidth = Math.max(1, Math.min(PANEL_MAX_WIDTH, Math.min(availableWidth, desiredWidth))); + panelHeight = Math.max(1, Math.min(DESIGN_HEIGHT, Math.min(availableHeight, desiredHeight))); + panelLeft = (uiWidth - panelWidth) / 2; + panelTop = Math.max(PANEL_MARGIN, (uiHeight - panelHeight) / 2); + + contentLeft = panelLeft + PANEL_MARGIN; + contentRight = panelLeft + panelWidth - PANEL_MARGIN; + contentTop = panelTop + HEADER_HEIGHT; + int footerHeight = editing ? EDITOR_FOOTER_HEIGHT : NORMAL_FOOTER_HEIGHT; + contentBottom = Math.max(contentTop + 1, panelTop + panelHeight - footerHeight); + } + + private void calculateUiScale() { + float widthScale = Math.max(0.1F, (width - PANEL_MARGIN * 2.0F) / PANEL_MAX_WIDTH); + float heightScale = Math.max(0.1F, (height - PANEL_MARGIN * 2.0F) / DESIGN_HEIGHT); + uiScale = Math.min(1.0F, Math.min(widthScale, heightScale)); + uiWidth = Math.max(1, Math.round(width / uiScale)); + uiHeight = Math.max(1, Math.round(height / uiScale)); + uiOffsetX = (width - uiWidth * uiScale) / 2.0F; + uiOffsetY = (height - uiHeight * uiScale) / 2.0F; + } + + private int desiredContentWidth() { + BeefToolLayout.Page page = currentPageObject(); + int[] groupWidths = sumGroupColumnWidths(page); + int groupWidth = groupWidths[0] + groupWidths[1]; + if (groupWidth > 0 && groupWidths[1] > 0) groupWidth += CARD_GAP; + if (editing) { + groupWidth = Math.max(groupWidth, naturalModuleRowWidth(layout.unassignedModules())); + groupWidth = Math.max(groupWidth, editorFooterWidth()); + } + return Math.max(Math.max(140, groupWidth), minimumContentWidth()); + } + + private int editorFooterWidth() { + return 54 + 4 + 54 + 4 + 58 + 4 + 58 + 4 + 58; + } + + private int minimumContentWidth() { + int titleWidth = font == null ? 100 : font.width(title); + int actionWidth = editing ? 50 : 70; + int minimumPanelWidth = titleWidth + actionWidth + (editing ? 138 : 64); + return Math.max(0, minimumPanelWidth - PANEL_MARGIN * 2); + } + + private int desiredContentHeight() { + BeefToolLayout.Page page = currentPageObject(); + int rawY = CONTENT_PADDING; + if (editing) { + rawY += cardHeight(1, visibleModules(layout.unassignedModules()).size(), + moduleColumns(visibleModules(layout.unassignedModules()).size())) + CARD_GAP; + } + int[] bottoms = {rawY, rawY}; + for (BeefToolLayout.Group group : page.groups()) { + if (BeefToolModuleRegistry.isGroupHidden(group)) continue; + List modules = visibleModules(group.modules()); + int column = bottoms[0] <= bottoms[1] ? 0 : 1; + bottoms[column] += cardHeight(1, modules.size(), moduleColumns(modules.size())) + CARD_GAP; + } + return Math.max(rawY, Math.max(bottoms[0], bottoms[1])) + CONTENT_PADDING; + } + + private int[] sumGroupColumnWidths(BeefToolLayout.Page page) { + int left = 0; + int right = 0; + int visibleIndex = 0; + for (BeefToolLayout.Group group : page.groups()) { + if (BeefToolModuleRegistry.isGroupHidden(group)) continue; + int width = naturalGroupWidth(group); + if ((visibleIndex++ & 1) == 0) left = Math.max(left, width); + else right = Math.max(right, width); + } + return new int[]{left, right}; + } + + private int naturalGroupWidth(BeefToolLayout.Group group) { + List modules = visibleModules(group.modules()); + int titleWidth = font == null ? 80 : font.width(group.name()) + 30; + if (modules.isEmpty()) return Math.max(100, titleWidth); + int buttonWidth = modules.size() == 1 + ? moduleButtonWidth(group.modules()) + : commonModuleWidth(currentPageObject()); + int columns = moduleColumns(modules.size()); + int contentWidth = columns == 1 + ? buttonWidth + : buttonWidth * 2 + MODULE_GAP; + return Math.max(titleWidth, contentWidth + 10); + } + + private int naturalModuleRowWidth(List moduleIds) { + List modules = visibleModules(moduleIds); + if (modules.isEmpty()) return 140; + int buttonWidth = modules.size() == 1 + ? moduleButtonWidth(moduleIds) + : commonModuleWidth(currentPageObject()); + int columns = moduleColumns(modules.size()); + return (columns == 1 ? buttonWidth : buttonWidth * 2 + MODULE_GAP) + 10; + } + + private int moduleButtonWidth(List moduleIds) { + int width = 50; + for (String id : visibleModules(moduleIds)) { + width = Math.max(width, font == null ? 80 : font.width(buttonMessage(id)) + 8); + } + return width; + } + + + + /** 整页统一的按钮宽度:所有含两个以上按钮的分组取齐,保证各分组外观一致。 */ + private int commonModuleWidth(BeefToolLayout.Page page) { + int width = 50; + for (BeefToolLayout.Group group : page.groups()) { + if (BeefToolModuleRegistry.isGroupHidden(group)) continue; + List modules = visibleModules(group.modules()); + if (modules.size() < 2) continue; + for (String id : modules) { + width = Math.max(width, font == null ? 80 : font.width(buttonMessage(id)) + 8); + } + } + if (editing) { + List modules = visibleModules(layout.unassignedModules()); + if (modules.size() >= 2) { + for (String id : modules) { + width = Math.max(width, font == null ? 80 : font.width(buttonMessage(id)) + 8); + } + } + } + return width; + } + + private int moduleColumns(int moduleCount) { + return moduleCount <= 1 ? 1 : 2; + } + + private int pageNavigationLeft() { + int right = panelLeft + panelWidth - PANEL_MARGIN; + int actionWidth = editing ? 50 : 70; + return right - actionWidth - 44; + } + + private void createToolbarWidgets() { + int right = panelLeft + panelWidth - PANEL_MARGIN; + int y = panelTop + 4; + int actionWidth = editing ? 50 : 70; + int actionX = right - actionWidth; + int nextX = actionX - 22; + int previousX = nextX - 22; + + PressableAE2Button previous = addToolbarButton(new PressableAE2Button( + previousX, y, 18, 14, Component.literal("<"), ignored -> changePage(-1))); + PressableAE2Button next = addToolbarButton(new PressableAE2Button( + nextX, y, 18, 14, Component.literal(">"), ignored -> changePage(1))); + PressableAE2Button action = addToolbarButton(new PressableAE2Button( + actionX, y, actionWidth, 14, + Component.translatable(editing + ? "gui.useless_mod.mode_config.done" + : "gui.useless_mod.mode_config.edit"), + ignored -> { + if (editing) finishEditing(); + else startEditing(); + })); + + boolean ready = !awaitingLayout; + previous.visible = ready && layout.pages().size() > 1; + next.visible = ready && layout.pages().size() > 1; + previous.active = ready && currentPage() > 0; + next.active = ready && currentPage() + 1 < layout.pages().size(); + action.active = ready; + + if (!editing || !ready) return; + + int footerY = panelTop + panelHeight - 18; + int footerX = panelLeft + PANEL_MARGIN; + addToolbarButton(new PressableAE2Button( + footerX, footerY, 54, 14, + Component.translatable("gui.useless_mod.mode_config.add_page"), ignored -> addPage())); + footerX += 58; + addToolbarButton(new PressableAE2Button( + footerX, footerY, 54, 14, + Component.translatable("gui.useless_mod.mode_config.delete_page"), ignored -> deletePage())); + footerX += 58; + addToolbarButton(new PressableAE2Button( + footerX, footerY, 58, 14, + Component.translatable("gui.useless_mod.mode_config.add_group"), ignored -> addGroup())); + footerX += 62; + addToolbarButton(new PressableAE2Button( + footerX, footerY, 58, 14, + Component.translatable("gui.useless_mod.mode_config.export"), ignored -> exportLayout())); + footerX += 62; + addToolbarButton(new PressableAE2Button( + footerX, footerY, 58, 14, + Component.translatable("gui.useless_mod.mode_config.import"), ignored -> importLayout())); + } + + private PressableAE2Button addToolbarButton(PressableAE2Button button) { + toolbarButtons.add(addRenderableWidget(button)); + return button; + } + + private void createModeButtons() { + for (ModuleLayout module : moduleLayouts) { + PressableAE2Button button = addRenderableWidget(new PressableAE2Button( + module.rect().left(), module.rect().top(), module.rect().width(), module.rect().height(), + buttonMessage(module.id()), ignored -> onModeSelected(module.id()))); + modeButtons.add(new ModeButton(module.id(), button)); + } + positionModeButtons(); + } + + private void createEditorNameFields() { + BeefToolLayout.Page page = currentPageObject(); + int pageFieldX = panelLeft + PANEL_MARGIN + font.width(title) + 10; + int pageFieldWidth = Math.min(150, Math.max(64, pageNavigationLeft() - pageFieldX - 8)); + pageNameField = new EditBox(font, pageFieldX, panelTop + 4, pageFieldWidth, 14, + Component.translatable("gui.useless_mod.mode_config.page_name")); + configureNameField(pageNameField); + pageNameField.setValue(page.name()); + addRenderableWidget(pageNameField); + + for (CardLayout card : cardLayouts) { + if (card.unassigned() || card.groupIndex() < 0) continue; + BeefToolLayout.Group group = groupAt(card.pageIndex(), card.groupIndex()); + if (group == null) continue; + EditBox field = new EditBox(font, card.left() + 10, card.top() + 2, + Math.max(20, card.width() - 31), 14, + Component.translatable("gui.useless_mod.mode_config.group_name")); + configureNameField(field); + field.setValue(group.name()); + addRenderableWidget(field); + groupNameFields.add(new GroupNameField(card.pageIndex(), card.groupIndex(), field)); + } + updateEditorFieldPositions(); + } + + private static void configureNameField(EditBox field) { + field.setMaxLength(BeefToolLayout.MAX_NAME_LENGTH); + field.setFilter(value -> value.codePointCount(0, value.length()) <= BeefToolLayout.MAX_NAME_LENGTH + && value.indexOf('\n') < 0 && value.indexOf('\r') < 0); + } + + private void calculateGeometry() { + cardLayouts.clear(); + moduleLayouts.clear(); + pageTabRects.clear(); + if (layout.pages().isEmpty()) { + layout.pages().add(new BeefToolLayout.Page("Page 1")); + layout.setSelectedPage(0); + } + + List rawCards = new ArrayList<>(); + int rawY = contentTop + CONTENT_PADDING; + if (editing) { + List unassigned = visibleModules(layout.unassignedModules()); + int height = cardHeight(contentRight - contentLeft, unassigned.size(), moduleColumns(unassigned.size())); + rawCards.add(new RawCard(-1, -1, contentLeft, rawY, + contentRight - contentLeft, height, true, unassigned)); + rawY += height + CARD_GAP; + } + + BeefToolLayout.Page page = currentPageObject(); + int[] columnWidths = fitColumnWidths(sumGroupColumnWidths(page), contentRight - contentLeft); + int[] columnBottoms = {rawY, rawY}; + for (int groupIndex = 0; groupIndex < page.groups().size(); groupIndex++) { + BeefToolLayout.Group group = page.groups().get(groupIndex); + if (BeefToolModuleRegistry.isGroupHidden(group)) continue; + List visibleModules = visibleModules(group.modules()); + int column = columnBottoms[0] <= columnBottoms[1] ? 0 : 1; + int left = contentLeft + (column == 0 ? 0 : columnWidths[0] + CARD_GAP); + int cardWidth = columnWidths[column]; + int height = cardHeight(cardWidth, visibleModules.size(), moduleColumns(visibleModules.size())); + int top = columnBottoms[column]; + rawCards.add(new RawCard(currentPage(), groupIndex, left, top, + cardWidth, height, false, visibleModules)); + columnBottoms[column] = top + height + CARD_GAP; + } + + int rawBottom = rawY; + for (int bottom : columnBottoms) rawBottom = Math.max(rawBottom, bottom); + if (rawCards.isEmpty()) rawBottom = rawY; + maxScroll = Math.max(0, rawBottom - contentBottom + CONTENT_PADDING); + scrollOffset = Mth.clamp(scrollOffset, 0, maxScroll); + + for (RawCard raw : rawCards) { + CardLayout card = new CardLayout(raw.pageIndex(), raw.groupIndex(), raw.left(), + raw.top() - scrollOffset, raw.width(), raw.height(), raw.unassigned(), raw.modules()); + cardLayouts.add(card); + for (int moduleIndex = 0; moduleIndex < card.modules().size(); moduleIndex++) { + Rect rect = moduleRect(card, moduleIndex); + moduleLayouts.add(new ModuleLayout(card.pageIndex(), card.groupIndex(), + card.unassigned(), card.modules().get(moduleIndex), moduleIndex, rect)); + } + } + } + + private int[] fitColumnWidths(int[] naturalWidths, int availableWidth) { + int left = naturalWidths[0]; + int right = naturalWidths[1]; + if (right == 0) return new int[]{Math.max(1, Math.min(left, availableWidth)), 0}; + int usable = Math.max(2, availableWidth - CARD_GAP); + int naturalTotal = Math.max(1, left + right); + if (naturalTotal <= usable) return new int[]{left, right}; + int fittedLeft = Math.max(1, usable * left / naturalTotal); + int fittedRight = Math.max(1, usable - fittedLeft); + return new int[]{fittedLeft, fittedRight}; + } + + private int cardHeight(int width, int moduleCount, int columns) { + int rows = Math.max(1, (moduleCount + columns - 1) / columns); + return CARD_HEADER_HEIGHT + rows * MODULE_HEIGHT + + Math.max(0, rows - 1) * MODULE_GAP + CARD_BOTTOM_PADDING; + } + + private Rect moduleRect(CardLayout card, int index) { + int columns = moduleColumns(card.modules().size()); + List sourceModules = card.unassigned() + ? layout.unassignedModules() + : groupAt(card.pageIndex(), card.groupIndex()).modules(); + int buttonWidth = visibleModules(sourceModules).size() == 1 + ? moduleButtonWidth(sourceModules) + : commonModuleWidth(currentPageObject()); + int column = index % columns; + int row = index / columns; + return new Rect(card.left() + 5 + column * (buttonWidth + MODULE_GAP), + card.top() + CARD_HEADER_HEIGHT + row * (MODULE_HEIGHT + MODULE_GAP), + buttonWidth, MODULE_HEIGHT); + } + + private List visibleModules(List moduleIds) { + List visible = new ArrayList<>(); + for (String id : moduleIds) { + if (BeefToolModuleRegistry.isAvailable(id, targetItem)) visible.add(id); + } + return List.copyOf(visible); + } + + private void positionModeButtons() { + for (int i = 0; i < modeButtons.size() && i < moduleLayouts.size(); i++) { + ModeButton modeButton = modeButtons.get(i); + ModuleLayout module = moduleLayouts.get(i); + modeButton.button().setX(module.rect().left()); + modeButton.button().setY(module.rect().top()); + modeButton.button().setWidth(module.rect().width()); + modeButton.button().setHeight(module.rect().height()); + modeButton.button().visible = intersectsContent(module.rect()); + modeButton.button().active = !awaitingLayout; + modeButton.button().setMessage(buttonMessage(modeButton.id())); + } + } + + private void updateEditorFieldPositions() { + if (pageNameField != null) { + pageNameField.visible = editing && !awaitingLayout; + if (!pageNameField.isFocused() && !pageNameField.getValue().equals(currentPageObject().name())) { + pageNameField.setValue(currentPageObject().name()); + } + } + for (GroupNameField field : groupNameFields) { + CardLayout card = findCard(field.pageIndex(), field.groupIndex()); + if (card == null) { + field.field().visible = false; + continue; + } + field.field().setX(card.left() + 10); + field.field().setY(card.top() + 2); + field.field().setWidth(Math.max(20, card.width() - 31)); + field.field().visible = intersectsContent(card.rect()); + } + } + + private boolean intersectsContent(Rect rect) { + return rect.right() > contentLeft && rect.left() < contentRight + && rect.bottom() > contentTop && rect.top() < contentBottom; + } + + private CardLayout findCard(int pageIndex, int groupIndex) { + for (CardLayout card : cardLayouts) { + if (!card.unassigned() && card.pageIndex() == pageIndex && card.groupIndex() == groupIndex) { + return card; + } + } + return null; + } + + private BeefToolLayout.Page currentPageObject() { + if (layout.pages().isEmpty()) { + layout.pages().add(new BeefToolLayout.Page("Page 1")); + layout.setSelectedPage(0); + } + int page = currentPage(); + return layout.pages().get(page); + } + + private int currentPage() { + if (layout.pages().isEmpty()) return 0; + return Mth.clamp(layout.selectedPage(), 0, layout.pages().size() - 1); + } + + private BeefToolLayout.Group groupAt(int pageIndex, int groupIndex) { + if (pageIndex < 0 || pageIndex >= layout.pages().size()) return null; + List groups = layout.pages().get(pageIndex).groups(); + return groupIndex >= 0 && groupIndex < groups.size() ? groups.get(groupIndex) : null; + } + + private Component buttonMessage(String id) { + Component name = BeefToolModuleRegistry.name(id); + if (BeefToolModuleRegistry.isExclusive(id)) { + return isActive(id) + ? Component.translatable("gui.useless_mod.mode_config.current", name) + : name; + } + return Component.translatable("gui.useless_mod.mode_config.state", name, stateMessage(id)); + } + + /** + * 模块的状态文案。绝大多数模块是「开启 / 关闭」, + * 但「右键泥土」这类模块是在两种行为之间切换,会用工具优先级的说法(锄头优先 / 铲子优先)。 + */ + private Component stateMessage(String id) { + if (BeefToolModuleRegistry.BEEF_FARMLAND_MODE.equals(id)) { + return EndlessBeafItem.farmlandStateText(targetItem); + } + return Component.translatable(isActive(id) + ? "tooltip.useless_mod.enable" + : "tooltip.useless_mod.disable"); + } + + private boolean isActive(String id) { + if (targetItem == null || targetItem.isEmpty()) return false; + return switch (id) { + case BeefToolModuleRegistry.ENCHANT_SILK_TOUCH -> + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(targetItem, UComponents.EnchantModeComponent) == EnchantMode.SILK_TOUCH; + case BeefToolModuleRegistry.ENCHANT_FORTUNE -> + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(targetItem, UComponents.EnchantModeComponent) == EnchantMode.FORTUNE; + case BeefToolModuleRegistry.TOOL_NONE -> currentTool() == ToolTypeMode.NONE_MODE; + case BeefToolModuleRegistry.TOOL_WRENCH -> currentTool() == ToolTypeMode.WRENCH_MODE; + case BeefToolModuleRegistry.TOOL_SCREWDRIVER -> currentTool() == ToolTypeMode.SCREWDRIVER_MODE; + case BeefToolModuleRegistry.TOOL_MALLET -> currentTool() == ToolTypeMode.MALLET_MODE; + case BeefToolModuleRegistry.TOOL_CROWBAR -> currentTool() == ToolTypeMode.CROWBAR_MODE; + case BeefToolModuleRegistry.TOOL_HAMMER -> currentTool() == ToolTypeMode.HAMMER_MODE; + case BeefToolModuleRegistry.TOOL_OMNITOOL -> currentTool() == ToolTypeMode.OMNITOOL_MODE; + case BeefToolModuleRegistry.CONSTRUCTION_WAND -> bool(UComponents.ConstructionWandEnabledComponent, false); + case BeefToolModuleRegistry.CONSTRUCTION_WAND_ANGEL -> + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(targetItem, UComponents.ConstructionWandCoreComponent, ConstructionWandCoreMode.DEFAULT) + == ConstructionWandCoreMode.ANGEL; + case BeefToolModuleRegistry.CONSTRUCTION_WAND_DESTRUCTION -> + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(targetItem, UComponents.ConstructionWandCoreComponent, ConstructionWandCoreMode.DEFAULT) + == ConstructionWandCoreMode.DESTRUCTION; + case BeefToolModuleRegistry.ENHANCED_CHAIN_MINING -> bool(UComponents.EnhancedChainMiningComponent, false); + case BeefToolModuleRegistry.FORCE_MINING -> bool(UComponents.ForceMiningComponent, false); + case BeefToolModuleRegistry.AUTO_SMELT -> bool(UComponents.AutoSmeltComponent, false); + case BeefToolModuleRegistry.EXDEORUM_CROOK -> bool(UComponents.ExDeorumCrookComponent, false); + case BeefToolModuleRegistry.EXDEORUM_HAMMER -> bool(UComponents.ExDeorumHammerComponent, false); + case BeefToolModuleRegistry.EXDEORUM_COMPRESSED_HAMMER -> + bool(UComponents.ExDeorumCompressedHammerComponent, false); + case BeefToolModuleRegistry.AE_STORAGE_PRIORITY -> bool(UComponents.AEStoragePriorityComponent, false); + case BeefToolModuleRegistry.AE_NETWORK_CONNECT -> bool(UComponents.AeNetworkConnectComponent, false); + case BeefToolModuleRegistry.WRENCH_TAG -> bool(UComponents.WrenchTagEnabledComponent, true); + case BeefToolModuleRegistry.FORCE_KILL -> bool(UComponents.ForceKillEnabledComponent, false); + case BeefToolModuleRegistry.BEEF_MALUM_SPIRIT -> + bool(UComponents.BeefMalumSpiritEnabledComponent, false); + case BeefToolModuleRegistry.BEEF_MYSTICAL_AGRICULTURE -> + bool(UComponents.BeefMysticalAgricultureEnabledComponent, false); + case BeefToolModuleRegistry.BEEF_BEHEADING -> + bool(UComponents.BeefBeheadingEnabledComponent, false); + case BeefToolModuleRegistry.BEEF_TIME_ACCELERATION -> + bool(UComponents.BeefTimeAccelerationEnabledComponent, false); + case BeefToolModuleRegistry.BEEF_INVULNERABILITY -> + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(targetItem, UComponents.BeefInvulnerabilityEnabledComponent, + targetItem.getItem() instanceof com.sorrowmist.useless.content.items.EndlessBeafItem); + case BeefToolModuleRegistry.BEEF_ADVANCED_STEALTH -> + bool(UComponents.BeefAdvancedStealthEnabledComponent, false); + case BeefToolModuleRegistry.BEEF_CAPTURE -> bool(UComponents.BeefCaptureEnabledComponent, false); + case BeefToolModuleRegistry.BEEF_TELEPORT -> bool(UComponents.BeefTeleportEnabledComponent, false); + case BeefToolModuleRegistry.BEEF_AOE_DAMAGE -> bool(UComponents.BeefAoeDamageEnabledComponent, false); + case BeefToolModuleRegistry.BEEF_MAGNET -> bool(UComponents.BeefMagnetEnabledComponent, true); + case BeefToolModuleRegistry.BEEF_FARMLAND_MODE -> bool(UComponents.BeefFarmlandModeComponent, false); + case BeefToolModuleRegistry.BEEF_CROP_HARVEST -> bool(UComponents.BeefCropHarvestComponent, true); + case BeefToolModuleRegistry.BEEF_SHEARS -> bool(UComponents.BeefShearsComponent, true); + case BeefToolModuleRegistry.BEEF_FLINT_AND_STEEL -> + bool(UComponents.BeefFlintAndSteelComponent, true); + case BeefToolModuleRegistry.BEEF_RITUAL_SATCHEL -> + bool(UComponents.BeefRitualSatchelComponent, false); + case BeefToolModuleRegistry.BEEF_RIPEN -> bool(UComponents.BeefRipenComponent, false); + case BeefToolModuleRegistry.BEEF_FORCE_GROW -> bool(UComponents.BeefForceGrowComponent, false); + case BeefToolModuleRegistry.BEEF_AUTO_CLICK -> BeefAutoClicker.isEnabled(); + case BeefToolModuleRegistry.BEEF_WIRELESS_LOGISTICS -> + bool(UComponents.StaffLinkEnabledComponent, false); + case BeefToolModuleRegistry.BEEF_KILL_AURA -> bool(UComponents.BeefKillAuraComponent, false); + case BeefToolModuleRegistry.BEEF_PROTECT_MODE -> bool(UComponents.BeefProtectModeComponent, false); + case BeefToolModuleRegistry.BEEF_ENTITY_TIME_ACCELERATION -> + bool(UComponents.BeefEntityTimeAccelerationComponent, false); + default -> false; + }; + } + + private ToolTypeMode currentTool() { + return com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(targetItem, UComponents.CurrentToolTypeComponent, ToolTypeMode.NONE_MODE); + } + + private boolean bool(com.sorrowmist.useless.compat.forge1201.component.DataComponentType type, boolean fallback) { + return com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(targetItem, type, fallback); + } + + private boolean bool( + com.sorrowmist.useless.compat.forge1201.registry.DeferredHolder, + com.sorrowmist.useless.compat.forge1201.component.DataComponentType> holder, + boolean fallback) { + return bool(holder.get(), fallback); + } + + @Override + public void tick() { + super.tick(); + if (statusTicks > 0) { + statusTicks--; + if (statusTicks == 0) statusKey = null; + } + refreshTargetAndModes(); + } + + private void refreshTargetAndModes() { + if (minecraft == null || minecraft.player == null) return; + var target = UselessItemUtils.findTargetToolInHands(minecraft.player); + if (target.isEmpty()) { + onClose(); + return; + } + + targetItem = target.get().getKey(); + String signature = availabilitySignature(targetItem); + if (!signature.equals(lastAvailabilitySignature)) { + lastAvailabilitySignature = signature; + if (!awaitingLayout) rebuildWidgets(); + } else { + for (ModeButton button : modeButtons) button.button().setMessage(buttonMessage(button.id())); + } + } + + private String availabilitySignature(ItemStack target) { + StringBuilder signature = new StringBuilder(); + for (BeefToolModuleRegistry.Definition definition : BeefToolModuleRegistry.definitions()) { + if (BeefToolModuleRegistry.isAvailable(definition.id(), target)) { + signature.append('|').append(definition.id()).append(':').append(buttonMessage(definition.id())); + } + } + return signature.toString(); + } + + public void receiveLayout(BeefToolLayout received) { + this.layout = received.copy(); + this.layout.setSelectedPage(currentPage()); + this.awaitingLayout = false; + this.lastAvailabilitySignature = availabilitySignature(targetItem); + if (minecraft != null && minecraft.screen == this) rebuildWidgets(); + } + + public void receiveLayoutError(BeefToolLayout.Error error) { + setStatus(errorKey(error)); + if (awaitingLayout) { + awaitingLayout = false; + layout = BeefToolModuleRegistry.defaultLayout(); + if (minecraft != null && minecraft.screen == this) rebuildWidgets(); + } + } + + private void startEditing() { + if (awaitingLayout) return; + editing = true; + scrollOffset = 0; + rebuildWidgets(); + } + + private void finishEditing() { + boolean changed = commitNameFields(); + editing = false; + scrollOffset = 0; + if (changed) sendLayout(); + rebuildWidgets(); + } + + private void changePage(int delta) { + if (awaitingLayout || drag != null) return; + boolean changed = editing && commitNameFields(); + int oldPage = currentPage(); + int next = Mth.clamp(oldPage + delta, 0, layout.pages().size() - 1); + if (next == oldPage) { + if (changed) sendLayout(); + return; + } + layout.setSelectedPage(next); + scrollOffset = 0; + sendLayout(); + rebuildWidgets(); + } + + private void selectPage(int page, boolean persist, boolean rebuild) { + if (page < 0 || page >= layout.pages().size()) return; + if (editing && drag == null) commitNameFields(); + layout.setSelectedPage(page); + scrollOffset = 0; + ensurePageTabVisible(); + if (persist) sendLayout(); + if (rebuild) rebuildWidgets(); + } + + private void addPage() { + if (layout.pages().size() >= BeefToolLayout.MAX_PAGES) { + setStatus("gui.useless_mod.mode_config.error.limit"); + return; + } + commitNameFields(); + layout.pages().add(new BeefToolLayout.Page("Page " + (layout.pages().size() + 1))); + layout.setSelectedPage(layout.pages().size() - 1); + scrollOffset = 0; + sendLayout(); + rebuildWidgets(); + } + + private void deletePage() { + commitNameFields(); + BeefToolLayout.Page page = currentPageObject(); + movePageModulesToUnassigned(page); + if (layout.pages().size() == 1) { + page.groups().clear(); + page.setName("Page 1"); + layout.setSelectedPage(0); + } else { + layout.pages().remove(page); + layout.setSelectedPage(Math.min(currentPage(), layout.pages().size() - 1)); + } + scrollOffset = 0; + sendLayout(); + rebuildWidgets(); + } + + private void movePageModulesToUnassigned(BeefToolLayout.Page page) { + for (BeefToolLayout.Group group : page.groups()) { + layout.unassignedModules().addAll(group.modules()); + } + } + + private void addGroup() { + commitNameFields(); + BeefToolLayout.Page page = currentPageObject(); + if (page.groups().size() >= BeefToolLayout.MAX_GROUPS_PER_PAGE) { + setStatus("gui.useless_mod.mode_config.error.limit"); + return; + } + page.groups().add(new BeefToolLayout.Group("Group " + (page.groups().size() + 1))); + sendLayout(); + rebuildWidgets(); + } + + private void deleteGroup(CardLayout card) { + if (card == null || card.groupIndex() < 0) return; + commitNameFields(); + BeefToolLayout.Group group = groupAt(card.pageIndex(), card.groupIndex()); + if (group == null) return; + layout.unassignedModules().addAll(group.modules()); + layout.pages().get(card.pageIndex()).groups().remove(group); + sendLayout(); + rebuildWidgets(); + } + + private boolean commitNameFields() { + boolean changed = false; + if (pageNameField != null && !layout.pages().isEmpty()) { + BeefToolLayout.Page page = currentPageObject(); + String name = acceptedName(pageNameField.getValue(), page.name()); + if (!name.equals(page.name())) { + page.setName(name); + changed = true; + } + } + for (GroupNameField field : groupNameFields) { + BeefToolLayout.Group group = groupAt(field.pageIndex(), field.groupIndex()); + if (group == null) continue; + String name = acceptedName(field.field().getValue(), group.name()); + if (!name.equals(group.name())) { + group.setName(name); + changed = true; + } + } + return changed; + } + + private static String acceptedName(String value, String fallback) { + String trimmed = value == null ? "" : value.trim(); + if (trimmed.isEmpty() || trimmed.codePointCount(0, trimmed.length()) > BeefToolLayout.MAX_NAME_LENGTH + || trimmed.indexOf('\n') >= 0 || trimmed.indexOf('\r') >= 0) { + return fallback; + } + return trimmed; + } + + private void exportLayout() { + if (commitNameFields()) sendLayout(); + String text = layout.toJson(); + if (text.getBytes(StandardCharsets.UTF_8).length > BeefToolLayout.MAX_TEXT_LENGTH) { + setStatus("gui.useless_mod.mode_config.error.limit"); + return; + } + Minecraft.getInstance().keyboardHandler.setClipboard(text); + setStatus("gui.useless_mod.mode_config.exported"); + } + + private void importLayout() { + String text = Minecraft.getInstance().keyboardHandler.getClipboard(); + try { + BeefToolLayout candidate = BeefToolLayout.fromJson(text); + validateKnownModules(candidate); + pendingImportText = text; + confirmImport = true; + } catch (BeefToolLayout.LayoutException exception) { + setStatus(errorKey(exception.error())); + } + } + + private void confirmImport(boolean accepted) { + if (accepted && pendingImportText != null) { + PacketDistributor.sendToServer(new BeefToolLayoutUpdatePacket(pendingImportText)); + setStatus("gui.useless_mod.mode_config.importing"); + } + pendingImportText = null; + confirmImport = false; + } + + private void sendLayout() { + if (awaitingLayout) return; + try { + validateKnownModules(layout); + layout.setSelectedPage(currentPage()); + PacketDistributor.sendToServer(new BeefToolLayoutUpdatePacket(layout.toJson())); + } catch (BeefToolLayout.LayoutException exception) { + setStatus(errorKey(exception.error())); + } + } + + private static void validateKnownModules(BeefToolLayout candidate) throws BeefToolLayout.LayoutException { + candidate.validate(); + // 等价组与布局同进同出,导入时也要在本地先拦下非法内容, + // 免得等服务器回一个笼统的 INVALID_STRUCTURE。 + ChainGroupManager.validateEntries(candidate.chainGroups()); + // 保护名单同样随布局导入导出,本地先拦下非法内容。 + BeefToolProtectionManager.validateEntries(candidate.protectedTypes(), candidate.protectedEntities()); + for (BeefToolLayout.Page page : candidate.pages()) { + for (BeefToolLayout.Group group : page.groups()) { + for (String id : group.modules()) validateKnown(id); + } + } + for (String id : candidate.unassignedModules()) validateKnown(id); + } + + private static void validateKnown(String id) throws BeefToolLayout.LayoutException { + if (!BeefToolModuleRegistry.isKnown(id)) { + throw new BeefToolLayout.LayoutException(BeefToolLayout.Error.UNKNOWN_MODULE); + } + } + + private static String errorKey(BeefToolLayout.Error error) { + return switch (error) { + case INVALID_TEXT -> "gui.useless_mod.mode_config.error.invalid_text"; + case UNSUPPORTED_VERSION -> "gui.useless_mod.mode_config.error.unsupported_version"; + case INVALID_STRUCTURE -> "gui.useless_mod.mode_config.error.invalid_structure"; + case INVALID_NAME -> "gui.useless_mod.mode_config.error.invalid_name"; + case DUPLICATE_MODULE -> "gui.useless_mod.mode_config.error.duplicate_module"; + case UNKNOWN_MODULE -> "gui.useless_mod.mode_config.error.unknown_module"; + case LIMIT -> "gui.useless_mod.mode_config.error.limit"; + }; + } + + private void setStatus(String key) { + statusKey = key; + statusTicks = 100; + } + + private void onModeSelected(String id) { + if (targetItem == null || targetItem.isEmpty()) return; + switch (id) { + case BeefToolModuleRegistry.ENCHANT_SILK_TOUCH -> + PacketDistributor.sendToServer(new EnchantmentSwitchPacket(EnchantMode.SILK_TOUCH)); + case BeefToolModuleRegistry.ENCHANT_FORTUNE -> + PacketDistributor.sendToServer(new EnchantmentSwitchPacket(EnchantMode.FORTUNE)); + case BeefToolModuleRegistry.TOOL_NONE -> sendToolType(ToolTypeMode.NONE_MODE); + case BeefToolModuleRegistry.TOOL_WRENCH -> sendToolType(ToolTypeMode.WRENCH_MODE); + case BeefToolModuleRegistry.TOOL_SCREWDRIVER -> sendToolType(ToolTypeMode.SCREWDRIVER_MODE); + case BeefToolModuleRegistry.TOOL_MALLET -> sendToolType(ToolTypeMode.MALLET_MODE); + case BeefToolModuleRegistry.TOOL_CROWBAR -> sendToolType(ToolTypeMode.CROWBAR_MODE); + case BeefToolModuleRegistry.TOOL_HAMMER -> sendToolType(ToolTypeMode.HAMMER_MODE); + case BeefToolModuleRegistry.TOOL_OMNITOOL -> sendToolType( + currentTool() == ToolTypeMode.OMNITOOL_MODE + ? ToolTypeMode.NONE_MODE + : ToolTypeMode.OMNITOOL_MODE); + case BeefToolModuleRegistry.CONSTRUCTION_WAND -> toggle(ModeTogglePacket.ModeType.CONSTRUCTION_WAND, + UComponents.ConstructionWandEnabledComponent, false); + case BeefToolModuleRegistry.CONSTRUCTION_WAND_ANGEL -> toggleCore(ConstructionWandCoreMode.ANGEL); + case BeefToolModuleRegistry.CONSTRUCTION_WAND_DESTRUCTION -> toggleCore(ConstructionWandCoreMode.DESTRUCTION); + case BeefToolModuleRegistry.ENHANCED_CHAIN_MINING -> toggle(ModeTogglePacket.ModeType.CHAIN_MINING, + UComponents.EnhancedChainMiningComponent, false); + case BeefToolModuleRegistry.FORCE_MINING -> toggle(ModeTogglePacket.ModeType.FORCE_MINING, + UComponents.ForceMiningComponent, false); + case BeefToolModuleRegistry.AUTO_SMELT -> toggle(ModeTogglePacket.ModeType.AUTO_SMELT, + UComponents.AutoSmeltComponent, false); + case BeefToolModuleRegistry.EXDEORUM_CROOK -> toggle(ModeTogglePacket.ModeType.EXDEORUM_CROOK, + UComponents.ExDeorumCrookComponent, false); + case BeefToolModuleRegistry.EXDEORUM_HAMMER -> toggle(ModeTogglePacket.ModeType.EXDEORUM_HAMMER, + UComponents.ExDeorumHammerComponent, false); + case BeefToolModuleRegistry.EXDEORUM_COMPRESSED_HAMMER -> + toggle(ModeTogglePacket.ModeType.EXDEORUM_COMPRESSED_HAMMER, + UComponents.ExDeorumCompressedHammerComponent, false); + case BeefToolModuleRegistry.AE_STORAGE_PRIORITY -> toggle(ModeTogglePacket.ModeType.AE_STORAGE_PRIORITY, + UComponents.AEStoragePriorityComponent, false); + case BeefToolModuleRegistry.AE_NETWORK_CONNECT -> toggle(ModeTogglePacket.ModeType.AE_NETWORK_CONNECT, + UComponents.AeNetworkConnectComponent, false); + case BeefToolModuleRegistry.WRENCH_TAG -> toggle(ModeTogglePacket.ModeType.WRENCH_TAG, + UComponents.WrenchTagEnabledComponent, true); + case BeefToolModuleRegistry.FORCE_KILL -> toggle(ModeTogglePacket.ModeType.FORCE_KILL, + UComponents.ForceKillEnabledComponent, false); + case BeefToolModuleRegistry.BEEF_MALUM_SPIRIT -> + toggle(ModeTogglePacket.ModeType.BEEF_MALUM_SPIRIT, + UComponents.BeefMalumSpiritEnabledComponent, false); + case BeefToolModuleRegistry.BEEF_MYSTICAL_AGRICULTURE -> + toggle(ModeTogglePacket.ModeType.BEEF_MYSTICAL_AGRICULTURE, + UComponents.BeefMysticalAgricultureEnabledComponent, false); + case BeefToolModuleRegistry.BEEF_BEHEADING -> + toggle(ModeTogglePacket.ModeType.BEEF_BEHEADING, + UComponents.BeefBeheadingEnabledComponent, false); + case BeefToolModuleRegistry.BEEF_TIME_ACCELERATION -> toggle(ModeTogglePacket.ModeType.BEEF_TIME_ACCELERATION, + UComponents.BeefTimeAccelerationEnabledComponent, false); + case BeefToolModuleRegistry.BEEF_INVULNERABILITY -> toggle(ModeTogglePacket.ModeType.BEEF_INVULNERABILITY, + UComponents.BeefInvulnerabilityEnabledComponent, + targetItem.getItem() instanceof com.sorrowmist.useless.content.items.EndlessBeafItem); + case BeefToolModuleRegistry.BEEF_ADVANCED_STEALTH -> toggle(ModeTogglePacket.ModeType.BEEF_ADVANCED_STEALTH, + UComponents.BeefAdvancedStealthEnabledComponent, false); + case BeefToolModuleRegistry.BEEF_CAPTURE -> toggle(ModeTogglePacket.ModeType.BEEF_CAPTURE, + UComponents.BeefCaptureEnabledComponent, false); + case BeefToolModuleRegistry.BEEF_TELEPORT -> toggle(ModeTogglePacket.ModeType.BEEF_TELEPORT, + UComponents.BeefTeleportEnabledComponent, false); + case BeefToolModuleRegistry.BEEF_AOE_DAMAGE -> toggle(ModeTogglePacket.ModeType.BEEF_AOE_DAMAGE, + UComponents.BeefAoeDamageEnabledComponent, false); + case BeefToolModuleRegistry.BEEF_MAGNET -> toggle(ModeTogglePacket.ModeType.BEEF_MAGNET, + UComponents.BeefMagnetEnabledComponent, true); + case BeefToolModuleRegistry.BEEF_FARMLAND_MODE -> toggle(ModeTogglePacket.ModeType.BEEF_FARMLAND_MODE, + UComponents.BeefFarmlandModeComponent, false); + case BeefToolModuleRegistry.BEEF_CROP_HARVEST -> toggle(ModeTogglePacket.ModeType.BEEF_CROP_HARVEST, + UComponents.BeefCropHarvestComponent, true); + case BeefToolModuleRegistry.BEEF_SHEARS -> toggle(ModeTogglePacket.ModeType.BEEF_SHEARS, + UComponents.BeefShearsComponent, true); + case BeefToolModuleRegistry.BEEF_FLINT_AND_STEEL -> + toggle(ModeTogglePacket.ModeType.BEEF_FLINT_AND_STEEL, + UComponents.BeefFlintAndSteelComponent, true); + case BeefToolModuleRegistry.BEEF_RITUAL_SATCHEL -> + toggle(ModeTogglePacket.ModeType.BEEF_RITUAL_SATCHEL, + UComponents.BeefRitualSatchelComponent, false); + case BeefToolModuleRegistry.BEEF_RIPEN -> toggle(ModeTogglePacket.ModeType.BEEF_RIPEN, + UComponents.BeefRipenComponent, false); + case BeefToolModuleRegistry.BEEF_FORCE_GROW -> toggle(ModeTogglePacket.ModeType.BEEF_FORCE_GROW, + UComponents.BeefForceGrowComponent, false); + case BeefToolModuleRegistry.BEEF_AUTO_CLICK -> BeefAutoClicker.toggle(); + case BeefToolModuleRegistry.BEEF_WIRELESS_LOGISTICS -> + toggle(ModeTogglePacket.ModeType.BEEF_WIRELESS_LOGISTICS, + UComponents.StaffLinkEnabledComponent, false); + case BeefToolModuleRegistry.BEEF_KILL_AURA -> + toggle(ModeTogglePacket.ModeType.BEEF_KILL_AURA, + UComponents.BeefKillAuraComponent, false); + case BeefToolModuleRegistry.BEEF_PROTECT_MODE -> + toggle(ModeTogglePacket.ModeType.BEEF_PROTECT_MODE, + UComponents.BeefProtectModeComponent, false); + case BeefToolModuleRegistry.BEEF_ENTITY_TIME_ACCELERATION -> + toggle(ModeTogglePacket.ModeType.BEEF_ENTITY_TIME_ACCELERATION, + UComponents.BeefEntityTimeAccelerationComponent, false); + default -> { + } + } + } + + private void sendToolType(ToolTypeMode mode) { + PacketDistributor.sendToServer(new ToolTypeModeSwitchPacket(mode)); + } + + private void toggle(ModeTogglePacket.ModeType type, + com.sorrowmist.useless.compat.forge1201.component.DataComponentType component, + boolean fallback) { + PacketDistributor.sendToServer(new ModeTogglePacket(type, !bool(component, fallback))); + } + + private void toggle( + ModeTogglePacket.ModeType type, + com.sorrowmist.useless.compat.forge1201.registry.DeferredHolder, + com.sorrowmist.useless.compat.forge1201.component.DataComponentType> component, + boolean fallback) { + toggle(type, component.get(), fallback); + } + + private void toggleCore(ConstructionWandCoreMode requested) { + ConstructionWandCoreMode current = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(targetItem, + UComponents.ConstructionWandCoreComponent, ConstructionWandCoreMode.DEFAULT); + PacketDistributor.sendToServer(new ConstructionWandCorePacket( + current == requested ? ConstructionWandCoreMode.DEFAULT : requested)); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + graphics.fill(0, 0, width, height, 0x66000000); + graphics.pose().pushPose(); + graphics.pose().translate(uiOffsetX, uiOffsetY, 0.0F); + graphics.pose().scale(uiScale, uiScale, 1.0F); + mouseX = (int) toUiX(mouseX); + mouseY = (int) toUiY(mouseY); + MachineScreenStyle.drawPanel(graphics, panelLeft, panelTop, panelWidth, panelHeight); + graphics.drawString(font, title, panelLeft + PANEL_MARGIN, panelTop + 7, + MachineScreenStyle.TEXT_COLOR, false); + drawPageTabs(graphics); + + if (awaitingLayout) { + graphics.drawString(font, Component.translatable("gui.useless_mod.mode_config.loading"), + panelLeft + PANEL_MARGIN, contentTop + 8, MachineScreenStyle.MUTED_TEXT_COLOR, false); + } else { + enableContentScissor(graphics); + try { + drawCards(graphics, mouseX, mouseY); + if (editing) { + drawEditorModules(graphics, mouseX, mouseY); + for (GroupNameField field : groupNameFields) { + if (field.field().visible) field.field().render(graphics, mouseX, mouseY, partialTick); + } + } else { + for (ModeButton modeButton : modeButtons) { + if (modeButton.button().visible) modeButton.button().render(graphics, mouseX, mouseY, partialTick); + } + drawChainGroupGear(graphics, mouseX, mouseY); + } + } finally { + graphics.flush(); + RenderSystem.disableScissor(); + } + } + + drawChainGroupGearTooltip(graphics, mouseX, mouseY); + drawModeButtonTooltips(graphics, mouseX, mouseY); + + if (pageNameField != null && pageNameField.visible) { + pageNameField.render(graphics, mouseX, mouseY, partialTick); + } + for (Renderable button : toolbarButtons) { + if (button instanceof net.minecraft.client.gui.components.AbstractWidget widget && widget.visible) { + widget.render(graphics, mouseX, mouseY, partialTick); + } + } + drawStatus(graphics); + if (confirmImport) drawImportConfirmation(graphics, mouseX, mouseY); + graphics.pose().popPose(); + } + + private void enableContentScissor(GuiGraphics graphics) { + graphics.flush(); + var window = Minecraft.getInstance().getWindow(); + double guiScale = window.getGuiScale(); + double screenLeft = uiOffsetX + contentLeft * uiScale; + double screenTop = uiOffsetY + contentTop * uiScale; + double screenRight = uiOffsetX + contentRight * uiScale; + double screenBottom = uiOffsetY + contentBottom * uiScale; + RenderSystem.enableScissor( + (int) (screenLeft * guiScale), + (int) (window.getHeight() - screenBottom * guiScale), + Math.max(0, (int) ((screenRight - screenLeft) * guiScale)), + Math.max(0, (int) ((screenBottom - screenTop) * guiScale))); + } + + private void drawCards(GuiGraphics graphics, int mouseX, int mouseY) { + for (CardLayout card : cardLayouts) { + if (!intersectsContent(card.rect())) continue; + MachineScreenStyle.drawInset(graphics, card.left(), card.top(), + card.left() + card.width(), card.top() + card.height()); + if (card.unassigned()) { + graphics.drawString(font, + Component.translatable("gui.useless_mod.mode_config.unassigned"), + card.left() + 6, card.top() + 4, MachineScreenStyle.TEXT_COLOR, false); + } else if (!editing) { + graphics.drawString(font, groupAt(card.pageIndex(), card.groupIndex()).name(), + card.left() + 6, card.top() + 4, MachineScreenStyle.TEXT_COLOR, false); + } else { + graphics.drawString(font, Component.literal("::"), + card.left() + 2, card.top() + 4, MachineScreenStyle.MUTED_TEXT_COLOR, false); + graphics.drawString(font, Component.literal("X"), + card.left() + card.width() - 14, card.top() + 4, + MachineScreenStyle.ERROR_TEXT_COLOR, false); + } + if (card.modules().isEmpty()) { + graphics.drawString(font, + Component.translatable("gui.useless_mod.mode_config.empty"), + card.left() + 6, card.top() + CARD_HEADER_HEIGHT + 4, + MachineScreenStyle.MUTED_TEXT_COLOR, false); + } + } + } + + private void drawEditorModules(GuiGraphics graphics, int mouseX, int mouseY) { + for (ModuleLayout module : moduleLayouts) { + if (!intersectsContent(module.rect())) continue; + boolean hovered = module.rect().contains(mouseX, mouseY); + int color = hovered ? MachineScreenStyle.HIGHLIGHT_COLOR : MachineScreenStyle.PANEL_COLOR; + graphics.fill(module.rect().left(), module.rect().top(), + module.rect().right(), module.rect().bottom(), color); + graphics.fill(module.rect().left(), module.rect().top(), + module.rect().right(), module.rect().top() + 1, MachineScreenStyle.SLOT_SHADOW_COLOR); + Component message = buttonMessage(module.id()); + String text = font.plainSubstrByWidth(message.getString(), Math.max(1, module.rect().width() - 6)); + int textColor = hovered ? MachineScreenStyle.TEXT_COLOR : MachineScreenStyle.SUBTLE_TEXT_COLOR; + graphics.drawString(font, text, module.rect().left() + 3, + module.rect().top() + 5, textColor, false); + } + if (drag != null && drag.moved) { + String text = drag.kind == DragKind.MODULE + ? buttonMessage(drag.moduleId).getString() + : drag.kind == DragKind.GROUP ? drag.group.name() : drag.page.name(); + int ghostWidth = Math.min(150, font.width(text) + 8); + int left = Mth.clamp((int) (drag.mouseX - ghostWidth / 2), 0, Math.max(0, uiWidth - ghostWidth)); + int top = Mth.clamp((int) (drag.mouseY - 10), 0, Math.max(0, uiHeight - 20)); + graphics.fill(left, top, left + ghostWidth, top + 18, 0xEE20242C); + graphics.drawString(font, font.plainSubstrByWidth(text, ghostWidth - 6), left + 3, top + 5, + 0xFFFFFFFF, false); + } + } + + private void drawPageTabs(GuiGraphics graphics) { + pageTabRects.clear(); + if (layout.pages().isEmpty()) return; + ensurePageTabVisible(); + int left = panelLeft + PANEL_MARGIN; + int right = panelLeft + panelWidth - PANEL_MARGIN; + int y = panelTop + 24; + int visibleCount = Math.max(1, (right - left) / PAGE_TAB_WIDTH); + for (int index = pageTabOffset; + index < Math.min(layout.pages().size(), pageTabOffset + visibleCount); index++) { + int x = left + (index - pageTabOffset) * PAGE_TAB_WIDTH; + Rect rect = new Rect(x, y, PAGE_TAB_WIDTH - 2, PAGE_TAB_HEIGHT); + pageTabRects.add(rect); + boolean selected = index == currentPage(); + MachineScreenStyle.drawInset(graphics, rect.left(), rect.top(), rect.right(), rect.bottom()); + if (selected) { + graphics.fill(rect.left() + 2, rect.top() + 2, rect.right() - 2, rect.bottom() - 2, + MachineScreenStyle.HIGHLIGHT_COLOR); + } + String name = font.plainSubstrByWidth(layout.pages().get(index).name(), rect.width() - 6); + graphics.drawString(font, name, rect.left() + 3, rect.top() + 4, + selected ? MachineScreenStyle.TEXT_COLOR : MachineScreenStyle.MUTED_TEXT_COLOR, false); + } + } + + private void ensurePageTabVisible() { + int visibleCount = Math.max(1, (panelWidth - PANEL_MARGIN * 2) / PAGE_TAB_WIDTH); + int selected = currentPage(); + if (selected < pageTabOffset) pageTabOffset = selected; + if (selected >= pageTabOffset + visibleCount) pageTabOffset = selected - visibleCount + 1; + pageTabOffset = Mth.clamp(pageTabOffset, 0, Math.max(0, layout.pages().size() - visibleCount)); + } + + private void drawStatus(GuiGraphics graphics) { + if (statusKey == null) return; + int y = editing ? panelTop + panelHeight - 34 : panelTop + panelHeight - 15; + Component status = Component.translatable(statusKey); + String text = font.plainSubstrByWidth(status.getString(), Math.max(1, panelWidth - 12)); + graphics.drawString(font, text, panelLeft + PANEL_MARGIN, y, + MachineScreenStyle.MUTED_TEXT_COLOR, false); + } + + private void drawImportConfirmation(GuiGraphics graphics, int mouseX, int mouseY) { + int modalWidth = Math.min(320, uiWidth - 12); + int modalHeight = 82; + int left = (uiWidth - modalWidth) / 2; + int top = (uiHeight - modalHeight) / 2; + graphics.fill(0, 0, uiWidth, uiHeight, 0x99000000); + MachineScreenStyle.drawPanel(graphics, left, top, modalWidth, modalHeight); + graphics.drawString(font, + font.plainSubstrByWidth( + Component.translatable("gui.useless_mod.mode_config.import_confirm").getString(), + modalWidth - 16), + left + 8, top + 10, MachineScreenStyle.TEXT_COLOR, false); + drawManualButton(graphics, left + 10, top + 48, 110, 18, + Component.translatable("gui.useless_mod.mode_config.confirm"), + new Rect(left + 10, top + 48, 110, 18), mouseX, mouseY); + drawManualButton(graphics, left + modalWidth - 120, top + 48, 110, 18, + Component.translatable("gui.useless_mod.mode_config.cancel"), + new Rect(left + modalWidth - 120, top + 48, 110, 18), mouseX, mouseY); + } + + private void drawManualButton(GuiGraphics graphics, int x, int y, int width, int height, + Component message, Rect rect, int mouseX, int mouseY) { + int color = rect.contains(mouseX, mouseY) + ? MachineScreenStyle.HIGHLIGHT_COLOR : MachineScreenStyle.PANEL_COLOR; + graphics.fill(x, y, x + width, y + height, color); + graphics.drawString(font, font.plainSubstrByWidth(message.getString(), width - 6), + x + 3, y + 5, MachineScreenStyle.TEXT_COLOR, false); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + mouseX = toUiX(mouseX); + mouseY = toUiY(mouseY); + if (confirmImport) { + int modalWidth = Math.min(320, uiWidth - 12); + int modalHeight = 82; + int left = (uiWidth - modalWidth) / 2; + int top = (uiHeight - modalHeight) / 2; + if (new Rect(left + 10, top + 48, 110, 18).contains(mouseX, mouseY)) { + confirmImport(true); + return true; + } + if (new Rect(left + modalWidth - 120, top + 48, 110, 18).contains(mouseX, mouseY)) { + confirmImport(false); + return true; + } + return true; + } + if (awaitingLayout) return true; + + // 连锁挖掘模块右端的齿轮:进入「连锁等价组」编辑子界面。 + // 必须在 super.mouseClicked 之前拦截,否则点击会被模块按钮先吃掉。 + if (!editing && button == GLFW.GLFW_MOUSE_BUTTON_LEFT) { + ModuleLayout chainModule = findModule(BeefToolModuleRegistry.ENHANCED_CHAIN_MINING); + if (chainModule != null && intersectsContent(chainModule.rect()) + && chainGroupGearRect(chainModule).contains(mouseX, mouseY)) { + // 菜单是空的、只在客户端本地建,不下发;界面继承容器屏是为了让 JEI/EMI 的侧栏出现。 + Inventory playerInventory = Minecraft.getInstance().player.getInventory(); + Minecraft.getInstance().setScreen(new ChainGroupScreen( + new ChainGroupMenu(0, playerInventory), playerInventory, targetItem, layout.copy())); + return true; + } + } + + int pageTab = pageAt(mouseX, mouseY); + if (pageTab >= 0 && button == GLFW.GLFW_MOUSE_BUTTON_LEFT) { + if (editing) { + drag = DragState.page(layout.pages().get(pageTab), mouseX, mouseY); + } else { + selectPage(pageTab, true, true); + } + return true; + } + + if (editing && button == GLFW.GLFW_MOUSE_BUTTON_LEFT) { + CardLayout deleteCard = cardDeleteAt(mouseX, mouseY); + if (deleteCard != null) { + deleteGroup(deleteCard); + return true; + } + CardLayout handleCard = groupHandleAt(mouseX, mouseY); + if (handleCard != null) { + BeefToolLayout.Group group = groupAt(handleCard.pageIndex(), handleCard.groupIndex()); + if (group != null) { + drag = DragState.group(layout.pages().get(handleCard.pageIndex()), group, + mouseX, mouseY); + return true; + } + } + ModuleLayout module = moduleAt(mouseX, mouseY); + if (module != null) { + drag = DragState.module(module.id(), mouseX, mouseY); + return true; + } + } + return super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) { + mouseX = toUiX(mouseX); + mouseY = toUiY(mouseY); + dragX /= uiScale; + dragY /= uiScale; + if (drag == null || button != GLFW.GLFW_MOUSE_BUTTON_LEFT) { + return super.mouseDragged(mouseX, mouseY, button, dragX, dragY); + } + drag.mouseX = mouseX; + drag.mouseY = mouseY; + if (!drag.moved && Math.hypot(mouseX - drag.startX, mouseY - drag.startY) >= DRAG_THRESHOLD) { + drag.moved = true; + } + if (!drag.moved) return true; + + if (drag.kind != DragKind.PAGE) { + int page = pageAt(mouseX, mouseY); + if (page >= 0 && page != currentPage()) { + DragState saved = drag; + selectPage(page, false, true); + drag = saved; + } + if (mouseY < contentTop + 12 && scrollOffset > 0) { + scrollOffset = Math.max(0, scrollOffset - 6); + updateGeometryAfterScroll(); + } else if (mouseY > contentBottom - 12 && scrollOffset < maxScroll) { + scrollOffset = Math.min(maxScroll, scrollOffset + 6); + updateGeometryAfterScroll(); + } + } + return true; + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + mouseX = toUiX(mouseX); + mouseY = toUiY(mouseY); + for (PressableAE2Button toolbarButton : toolbarButtons) toolbarButton.releaseVisualState(); + for (ModeButton modeButton : modeButtons) modeButton.button().releaseVisualState(); + if (drag != null && button == GLFW.GLFW_MOUSE_BUTTON_LEFT) { + drag.mouseX = mouseX; + drag.mouseY = mouseY; + DragState released = drag; + drag = null; + if (!released.moved) { + if (released.kind == DragKind.PAGE) { + selectPage(layout.pages().indexOf(released.page), true, true); + } + return true; + } + if (drop(released, mouseX, mouseY)) { + sendLayout(); + rebuildWidgets(); + } + return true; + } + return super.mouseReleased(mouseX, mouseY, button); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double deltaY) { + mouseX = toUiX(mouseX); + mouseY = toUiY(mouseY); + if (confirmImport || awaitingLayout) return true; + if (mouseY >= contentTop && mouseY <= contentBottom) { + int next = Mth.clamp(scrollOffset - (int) Math.round(deltaY * 18), 0, maxScroll); + if (next != scrollOffset) { + scrollOffset = next; + updateGeometryAfterScroll(); + } + return true; + } + if (mouseY >= panelTop + 22 && mouseY <= panelTop + 22 + PAGE_TAB_HEIGHT) { + pageTabOffset = Mth.clamp(pageTabOffset - (int) Math.signum(deltaY), + 0, Math.max(0, layout.pages().size() - 1)); + return true; + } + return super.mouseScrolled(mouseX, mouseY, deltaY); + } + + private double toUiX(double mouseX) { + return (mouseX - uiOffsetX) / uiScale; + } + + private double toUiY(double mouseY) { + return (mouseY - uiOffsetY) / uiScale; + } + + private void updateGeometryAfterScroll() { + calculateGeometry(); + positionModeButtons(); + updateEditorFieldPositions(); + } + + private boolean drop(DragState released, double mouseX, double mouseY) { + if (released.kind == DragKind.MODULE) return dropModule(released.moduleId, mouseX, mouseY); + if (released.kind == DragKind.GROUP) return dropGroup(released.group, mouseX, mouseY); + return dropPage(released.page, mouseX, mouseY); + } + + private boolean dropModule(String moduleId, double mouseX, double mouseY) { + CardLayout targetCard = groupAt(mouseX, mouseY); + if (targetCard != null) { + BeefToolLayout.Group targetGroup = groupAt(targetCard.pageIndex(), targetCard.groupIndex()); + if (targetGroup == null) return false; + int insertion = insertionIndex(targetCard, mouseX, mouseY); + removeModule(moduleId); + insertion = Mth.clamp(insertion, 0, targetGroup.modules().size()); + targetGroup.modules().add(insertion, moduleId); + return true; + } + if (unassignedCardAt(mouseX, mouseY) != null || pageAt(mouseX, mouseY) >= 0) { + removeModule(moduleId); + layout.unassignedModules().add(moduleId); + return true; + } + return false; + } + + private int insertionIndex(CardLayout targetCard, double mouseX, double mouseY) { + int insertion = targetCard.modules().size(); + for (ModuleLayout module : moduleLayouts) { + if (module.pageIndex() != targetCard.pageIndex() + || module.groupIndex() != targetCard.groupIndex() + || module.unassigned() != targetCard.unassigned()) continue; + if (module.rect().contains(mouseX, mouseY)) { + insertion = module.index() + (mouseY > module.rect().top() + module.rect().height() / 2 ? 1 : 0); + break; + } + } + return insertion; + } + + private void removeModule(String moduleId) { + layout.unassignedModules().removeIf(moduleId::equals); + for (BeefToolLayout.Page page : layout.pages()) { + for (BeefToolLayout.Group group : page.groups()) { + group.modules().removeIf(moduleId::equals); + } + } + } + + private boolean dropGroup(BeefToolLayout.Group group, double mouseX, double mouseY) { + if (group == null) return false; + CardLayout targetCard = groupAt(mouseX, mouseY); + BeefToolLayout.Page targetPage = layout.pages().get(currentPage()); + BeefToolLayout.Group targetGroup = targetCard == null + ? null : groupAt(targetCard.pageIndex(), targetCard.groupIndex()); + if (targetGroup == group) return false; + + if (targetCard == null && pageAt(mouseX, mouseY) < 0) return false; + int insertion = targetGroup == null ? targetPage.groups().size() + : targetPage.groups().indexOf(targetGroup); + BeefToolLayout.Page sourcePage = null; + for (BeefToolLayout.Page page : layout.pages()) { + if (page.groups().remove(group)) { + sourcePage = page; + break; + } + } + if (sourcePage == null) return false; + if (sourcePage == targetPage && targetGroup != null + && sourcePage.groups().indexOf(targetGroup) < insertion) insertion--; + insertion = Mth.clamp(insertion, 0, targetPage.groups().size()); + targetPage.groups().add(insertion, group); + layout.setSelectedPage(layout.pages().indexOf(targetPage)); + return true; + } + + private boolean dropPage(BeefToolLayout.Page page, double mouseX, double mouseY) { + int targetIndex = pageAt(mouseX, mouseY); + if (targetIndex < 0) return false; + BeefToolLayout.Page target = layout.pages().get(targetIndex); + if (target == page) return false; + int sourceIndex = layout.pages().indexOf(page); + layout.pages().remove(page); + int insertion = layout.pages().indexOf(target); + if (sourceIndex < insertion) insertion--; + layout.pages().add(insertion, page); + layout.setSelectedPage(layout.pages().indexOf(page)); + return true; + } + + private ModuleLayout moduleAt(double mouseX, double mouseY) { + for (ModuleLayout module : moduleLayouts) { + if (module.rect().contains(mouseX, mouseY) && intersectsContent(module.rect())) return module; + } + return null; + } + + /** 按模块 ID 找当前页的模块布局,找不到返回 null。 */ + private ModuleLayout findModule(String id) { + for (ModuleLayout module : moduleLayouts) { + if (module.id().equals(id)) { + return module; + } + } + return null; + } + + /** 连锁等价组齿轮的命中区域:贴在模块按钮右端内侧。 */ + private Rect chainGroupGearRect(ModuleLayout module) { + Rect rect = module.rect(); + return new Rect(rect.right() - GEAR_SIZE - 1, rect.top() + (rect.height() - GEAR_SIZE) / 2, + GEAR_SIZE, GEAR_SIZE); + } + + /** + * 在连锁挖掘模块按钮右端画一个齿轮,提示这里可以点开连锁等价组配置。 + * + *

用 {@code fill} 拼出轮廓,不新增贴图。

+ */ + private void drawChainGroupGear(GuiGraphics graphics, int mouseX, int mouseY) { + ModuleLayout module = findModule(BeefToolModuleRegistry.ENHANCED_CHAIN_MINING); + if (module == null || !intersectsContent(module.rect())) { + return; + } + Rect gear = chainGroupGearRect(module); + int color = gear.contains(mouseX, mouseY) + ? MachineScreenStyle.TEXT_COLOR + : MachineScreenStyle.SUBTLE_TEXT_COLOR; + + int cx = gear.left() + GEAR_SIZE / 2; + int cy = gear.top() + GEAR_SIZE / 2; + graphics.fill(cx - 2, cy - 2, cx + 2, cy + 2, color); + graphics.fill(cx - 1, gear.top(), cx + 1, cy - 2, color); + graphics.fill(cx - 1, cy + 2, cx + 1, gear.bottom(), color); + graphics.fill(gear.left(), cy - 1, cx - 2, cy + 1, color); + graphics.fill(cx + 2, cy - 1, gear.right(), cy + 1, color); + } + + /** 齿轮的悬停提示画在裁剪区之外,免得提示框被内容区裁掉。 */ + private void drawChainGroupGearTooltip(GuiGraphics graphics, int mouseX, int mouseY) { + if (awaitingLayout || editing) { + return; + } + ModuleLayout module = findModule(BeefToolModuleRegistry.ENHANCED_CHAIN_MINING); + if (module == null || !intersectsContent(module.rect()) + || !chainGroupGearRect(module).contains(mouseX, mouseY)) { + return; + } + graphics.renderTooltip(font, + Component.translatable("gui.useless_mod.chain_group.gear_tooltip"), mouseX, mouseY); + } + + /** 少数「光看名字不知道该怎么用」的模式按钮,悬停时给出说明。 */ + private void drawModeButtonTooltips(GuiGraphics graphics, int mouseX, int mouseY) { + if (awaitingLayout || editing) { + return; + } + for (ModeButton modeButton : modeButtons) { + PressableAE2Button button = modeButton.button(); + if (!button.visible || !button.isMouseOver(mouseX, mouseY)) continue; + if (!intersectsContent(new Rect(button.getX(), button.getY(), button.getWidth(), button.getHeight()))) { + return; + } + List tooltip = modeButtonTooltip(modeButton.id()); + if (tooltip != null && !tooltip.isEmpty()) { + graphics.renderComponentTooltip(font, tooltip, mouseX, mouseY); + } + return; + } + } + + /** + * 模式按钮的悬停说明,返回 {@code null} 表示不显示。 + * + *

只有需要额外说明「怎么操作」的模块才登记,其余一律不显示,免得把轮盘糊满。

+ * + *

必须「一行一个 Component」:{@code GuiGraphics#renderTooltip(Font, Component, int, int)} + * 走的是 {@code Component#getVisualOrderText()},它把整串文字当成一行做 bidi 重排, + * 翻译文本里的 {@code \n} 不会变成换行(只会渲染成一个缺字形)。 + * 只有传多行列表({@code renderComponentTooltip})才会逐行渲染。

+ */ + private List modeButtonTooltip(String id) { + return switch (id) { + case BeefToolModuleRegistry.BEEF_PROTECT_MODE -> List.of( + Component.translatable("gui.useless_mod.mode_config.beef_protect_mode.title"), + Component.translatable("gui.useless_mod.mode_config.beef_protect_mode.ctrl"), + Component.translatable("gui.useless_mod.mode_config.beef_protect_mode.shift"), + Component.translatable("gui.useless_mod.mode_config.beef_protect_mode.auto")); + case BeefToolModuleRegistry.BEEF_WIRELESS_LOGISTICS -> List.of( + Component.translatable("gui.useless_mod.mode_config.wireless_logistics.title"), + Component.translatable("gui.useless_mod.mode_config.wireless_logistics.key", + KeyBindings.OPEN_WIRELESS_LOGISTICS_KEY.get().getTranslatedKeyMessage())); + default -> null; + }; + } + + private CardLayout groupAt(double mouseX, double mouseY) { + for (CardLayout card : cardLayouts) { + if (!card.unassigned() && card.rect().contains(mouseX, mouseY) + && intersectsContent(card.rect())) return card; + } + return null; + } + + private CardLayout unassignedCardAt(double mouseX, double mouseY) { + for (CardLayout card : cardLayouts) { + if (card.unassigned() && card.rect().contains(mouseX, mouseY) + && intersectsContent(card.rect())) return card; + } + return null; + } + + private CardLayout cardDeleteAt(double mouseX, double mouseY) { + for (CardLayout card : cardLayouts) { + if (!card.unassigned() && card.rect().contains(mouseX, mouseY) + && mouseX >= card.right() - 20 && mouseY <= card.top() + CARD_HEADER_HEIGHT) return card; + } + return null; + } + + private CardLayout groupHandleAt(double mouseX, double mouseY) { + for (CardLayout card : cardLayouts) { + if (!card.unassigned() && card.rect().contains(mouseX, mouseY) + && mouseX <= card.left() + 10 && mouseY <= card.top() + CARD_HEADER_HEIGHT) return card; + } + return null; + } + + private int pageAt(double mouseX, double mouseY) { + if (mouseY < panelTop + 22 || mouseY > panelTop + 22 + PAGE_TAB_HEIGHT) return -1; + for (int index = 0; index < pageTabRects.size(); index++) { + if (pageTabRects.get(index).contains(mouseX, mouseY)) { + return pageTabOffset + index; + } + } + return -1; + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (keyCode == InputConstants.KEY_ESCAPE) { + if (confirmImport) { + confirmImport(false); + return true; + } + onClose(); + return true; + } + if (keyCode == GLFW.GLFW_KEY_ENTER && editing + && (pageNameField != null && pageNameField.isFocused() + || groupNameFields.stream().anyMatch(field -> field.field().isFocused()))) { + if (commitNameFields()) sendLayout(); + setFocused(null); + return true; + } + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public void onClose() { + if (editing && commitNameFields()) sendLayout(); + super.onClose(); + } + + @Override + public boolean shouldCloseOnEsc() { + return true; + } + + @Override + public boolean isPauseScreen() { + return false; + } + + private record RawCard(int pageIndex, int groupIndex, int left, int top, + int width, int height, boolean unassigned, List modules) { + } + + private record CardLayout(int pageIndex, int groupIndex, int left, int top, + int width, int height, boolean unassigned, List modules) { + Rect rect() { + return new Rect(left, top, width, height); + } + + int right() { + return left + width; + } + } + + private record ModuleLayout(int pageIndex, int groupIndex, boolean unassigned, + String id, int index, Rect rect) { + } + + private record ModeButton(String id, PressableAE2Button button) { + } + + private record GroupNameField(int pageIndex, int groupIndex, EditBox field) { + } + + private enum DragKind { + MODULE, + GROUP, + PAGE + } + + private static final class DragState { + private final DragKind kind; + private final String moduleId; + private final BeefToolLayout.Page page; + private final BeefToolLayout.Group group; + private final double startX; + private final double startY; + private boolean moved; + private double mouseX; + private double mouseY; + + private DragState(DragKind kind, String moduleId, BeefToolLayout.Page page, + BeefToolLayout.Group group, double startX, double startY) { + this.kind = kind; + this.moduleId = moduleId; + this.page = page; + this.group = group; + this.startX = startX; + this.startY = startY; + this.mouseX = startX; + this.mouseY = startY; + } + + static DragState module(String moduleId, double x, double y) { + return new DragState(DragKind.MODULE, moduleId, null, null, x, y); + } + + static DragState group(BeefToolLayout.Page page, BeefToolLayout.Group group, double x, double y) { + return new DragState(DragKind.GROUP, null, page, group, x, y); + } + + static DragState page(BeefToolLayout.Page page, double x, double y) { + return new DragState(DragKind.PAGE, null, page, null, x, y); + } + } + + private record Rect(int left, int top, int width, int height) { + int right() { + return left + width; + } + + int bottom() { + return top + height; + } + + boolean contains(double x, double y) { + return x >= left && x < right() && y >= top && y < bottom(); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/gui/MoldHubScreen.java b/src/main/java/com/sorrowmist/useless/client/gui/MoldHubScreen.java new file mode 100644 index 00000000..a84f4f41 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/gui/MoldHubScreen.java @@ -0,0 +1,26 @@ +package com.sorrowmist.useless.client.gui; + +import com.sorrowmist.useless.content.menus.OmniversalMoldHubMenu; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.Slot; + +/** + * 模具集散中心的容器界面。 + * + *

搜索按模具物品名与配方登记的可接受物品名匹配,使按任意可接受物品名都能定位到对应模具。 + * 内容全部为模具,不存在输入与输出的区分,因此不提供搜索范围选择;搜索范围由基类限定在 + * 活跃模具槽位,只读恢复页内容不进入结果。

+ */ +public final class MoldHubScreen extends PagedRecoverableScreen { + public MoldHubScreen(OmniversalMoldHubMenu menu, Inventory inventory, Component title) { + super(menu, inventory, title); + } + + @Override + protected boolean matchesSearch(int menuSlot) { + Slot slot = menu.getSlot(menuSlot); + return PatternSearchMatcher.matchesMold(slot.getItem(), normalizedQuery(), + minecraft == null ? null : minecraft.level); + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/gui/MultiblockAlloyFurnaceScreen.java b/src/main/java/com/sorrowmist/useless/client/gui/MultiblockAlloyFurnaceScreen.java new file mode 100644 index 00000000..4051ce62 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/gui/MultiblockAlloyFurnaceScreen.java @@ -0,0 +1,582 @@ +package com.sorrowmist.useless.client.gui; + +import com.sorrowmist.useless.api.enums.RedstoneControlMode; +import com.sorrowmist.useless.content.blocks.multiblock.MultiblockAlloyFurnaceCoreBlock; +import com.sorrowmist.useless.content.blocks.multiblock.OmniversalAlloyFurnaceStructure; +import com.sorrowmist.useless.content.blocks.multiblock.UselessCoilBlock; +import com.sorrowmist.useless.content.menus.MultiblockAlloyFurnaceMenu; +import com.sorrowmist.useless.init.ModBlocks; +import com.sorrowmist.useless.network.AECancelPacket; +import com.sorrowmist.useless.network.AETaskProgressRequestPacket; +import com.sorrowmist.useless.network.MultiblockAlloyFurnaceEnergyLimitPacket; +import com.sorrowmist.useless.network.RedstoneControlPacket; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.glfw.GLFW; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; + +public final class MultiblockAlloyFurnaceScreen extends AbstractContainerScreen { + private static final int PREVIEW_ORIGIN_X = 88; + private static final int PREVIEW_ORIGIN_Y = 32; + private static final int PREVIEW_CELL_SIZE = 6; + private static final int PREVIEW_LAYER_STRIDE = 21; + private static final int REDSTONE_BUTTON_X = 142; + private static final int CANCEL_BUTTON_X = 158; + private static final int CONTROL_BUTTON_Y = 3; + private static final int ENERGY_BAR_X = 11; + private static final int ENERGY_BAR_Y = 74; + private static final int ENERGY_BAR_WIDTH = 154; + private static final int ENERGY_BAR_HEIGHT = 6; + private static final int TASK_LIST_X = 11; + private static final int TASK_LIST_Y = 84; + private static final int TASK_LIST_WIDTH = 154; + private static final int TASK_LIST_MAX_LINES = 5; + private static final int TASK_LIST_LINE_HEIGHT = 9; + private static final float TASK_LIST_TEXT_SCALE = 0.75F; + private static final int TASK_PAGE_PREVIOUS_X = 124; + private static final int TASK_PAGE_NEXT_X = 154; + private static final int TASK_PAGE_BUTTON_Y = 118; + private static final int TASK_PAGE_BUTTON_WIDTH = 12; + private static final int TASK_PAGE_BUTTON_HEIGHT = 10; + private static final int ENERGY_LIMIT_FIELD_X = 88; + private static final int ENERGY_LIMIT_FIELD_Y = 60; + private static final int ENERGY_LIMIT_FIELD_WIDTH = 80; + + @Nullable + private OmniversalAlloyFurnaceStructure.ValidationResult liveValidation; + @Nullable + private AlloyFurnaceControlButton cancelButton; + @Nullable + private PressableAE2Button previousTaskPageButton; + @Nullable + private PressableAE2Button nextTaskPageButton; + @Nullable + private EditBox energyLimitField; + private long lastSyncedEnergyLimit = Long.MIN_VALUE; + private String syncedEnergyLimitText = ""; + private boolean updatingEnergyLimitField; + private boolean energyLimitDirty; + private boolean energyLimitFieldWasFocused; + private int taskPage; + private Map liveMismatches = Map.of(); + private Direction liveFacing = Direction.NORTH; + + public MultiblockAlloyFurnaceScreen(MultiblockAlloyFurnaceMenu menu, Inventory inventory, Component title) { + super(menu, inventory, title); + imageWidth = 176; + imageHeight = 222; + inventoryLabelY = 130; + } + + @Override + protected void init() { + super.init(); + addRenderableWidget(AlloyFurnaceControlButton.redstone( + leftPos + REDSTONE_BUTTON_X, topPos + CONTROL_BUTTON_Y, + () -> RedstoneControlMode.byIndex(menu.getRedstoneMode()), + button -> PacketDistributor.sendToServer( + new RedstoneControlPacket(menu.getBlockPos())))); + cancelButton = addRenderableWidget(AlloyFurnaceControlButton.cancel( + leftPos + CANCEL_BUTTON_X, topPos + CONTROL_BUTTON_Y, + button -> PacketDistributor.sendToServer( + new AECancelPacket(menu.getBlockPos())))); + energyLimitField = new EditBox(font, + leftPos + ENERGY_LIMIT_FIELD_X, topPos + ENERGY_LIMIT_FIELD_Y, + ENERGY_LIMIT_FIELD_WIDTH, 14, + Component.translatable("gui.useless_mod.multiblock_alloy_furnace.energy_limit")); + energyLimitField.setMaxLength(24); + energyLimitField.setFilter(ScaledEnergyAmount::isValidInput); + energyLimitField.setResponder(value -> { + if (!updatingEnergyLimitField) { + energyLimitDirty = !value.equals(syncedEnergyLimitText); + } + }); + addRenderableWidget(energyLimitField); + previousTaskPageButton = addRenderableWidget(new PressableAE2Button( + leftPos + TASK_PAGE_PREVIOUS_X, topPos + TASK_PAGE_BUTTON_Y, + TASK_PAGE_BUTTON_WIDTH, TASK_PAGE_BUTTON_HEIGHT, + Component.literal("<"), button -> changeTaskPage(-1))); + nextTaskPageButton = addRenderableWidget(new PressableAE2Button( + leftPos + TASK_PAGE_NEXT_X, topPos + TASK_PAGE_BUTTON_Y, + TASK_PAGE_BUTTON_WIDTH, TASK_PAGE_BUTTON_HEIGHT, + Component.literal(">"), button -> changeTaskPage(1))); + syncEnergyLimitField(true); + refreshStructurePreview(); + PacketDistributor.sendToServer(new AETaskProgressRequestPacket( + menu.containerId, menu.getBlockPos())); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (energyLimitField != null && energyLimitField.isFocused() + && !energyLimitField.isMouseOver(mouseX, mouseY)) { + commitEnergyLimit(); + setFocused(null); + energyLimitFieldWasFocused = false; + } + return super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + if (cancelButton != null) { + cancelButton.releaseVisualState(); + } + if (previousTaskPageButton != null) { + previousTaskPageButton.releaseVisualState(); + } + if (nextTaskPageButton != null) { + nextTaskPageButton.releaseVisualState(); + } + return super.mouseReleased(mouseX, mouseY, button); + } + + @Override + protected void containerTick() { + super.containerTick(); + boolean focused = energyLimitField != null && energyLimitField.isFocused(); + if (energyLimitFieldWasFocused && !focused) commitEnergyLimit(); + energyLimitFieldWasFocused = focused; + syncEnergyLimitField(false); + refreshStructurePreview(); + updateTaskPageControls(); + } + + private void syncEnergyLimitField(boolean force) { + if (energyLimitField == null) return; + long limit = menu.getAutomaticEnergyLimit(); + if ((force || (!energyLimitField.isFocused() && !energyLimitDirty)) + && limit != lastSyncedEnergyLimit) { + setEnergyLimitFieldValue(limit); + lastSyncedEnergyLimit = limit; + } + } + + private void commitEnergyLimit() { + if (energyLimitField == null || !energyLimitDirty) return; + long limit = ScaledEnergyAmount.parse( + energyLimitField.getValue(), menu.getCapacity()) + .orElse(menu.getAutomaticEnergyLimit()); + setEnergyLimitFieldValue(limit); + PacketDistributor.sendToServer(new MultiblockAlloyFurnaceEnergyLimitPacket( + menu.containerId, menu.getBlockPos(), limit)); + } + + private void setEnergyLimitFieldValue(long limit) { + if (energyLimitField == null) return; + syncedEnergyLimitText = ScaledEnergyAmount.format(limit); + updatingEnergyLimitField = true; + energyLimitField.setValue(syncedEnergyLimitText); + updatingEnergyLimitField = false; + energyLimitDirty = false; + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (energyLimitField != null && energyLimitField.isFocused() + && (keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER)) { + commitEnergyLimit(); + setFocused(null); + energyLimitFieldWasFocused = false; + return true; + } + if (keyCode != GLFW.GLFW_KEY_ESCAPE && energyLimitField != null + && consumesTextInputKey(energyLimitField, keyCode, scanCode, modifiers)) { + return true; + } + return super.keyPressed(keyCode, scanCode, modifiers); + } + + private static boolean consumesTextInputKey(EditBox field, int keyCode, int scanCode, int modifiers) { + return field.keyPressed(keyCode, scanCode, modifiers) || field.canConsumeInput(); + } + + @Override + public void onClose() { + commitEnergyLimit(); + super.onClose(); + } + + private void refreshStructurePreview() { + if (minecraft == null || minecraft.level == null) { + liveValidation = null; + liveMismatches = Map.of(); + return; + } + + BlockPos corePos = menu.getBlockPos(); + BlockState coreState = minecraft.level.getBlockState(corePos); + liveFacing = coreState.hasProperty(MultiblockAlloyFurnaceCoreBlock.FACING) + ? coreState.getValue(MultiblockAlloyFurnaceCoreBlock.FACING) + : Direction.NORTH; + for (var entry : OmniversalAlloyFurnaceStructure.entries()) { + if (!minecraft.level.isLoaded(entry.worldPos(corePos, liveFacing))) { + liveValidation = null; + liveMismatches = Map.of(); + return; + } + } + + liveValidation = OmniversalAlloyFurnaceStructure.validate( + minecraft.level, corePos, liveFacing); + Map mismatches = new HashMap<>(); + for (var mismatch : liveValidation.mismatches()) { + mismatches.put(mismatch.worldPos().asLong(), mismatch); + } + liveMismatches = Map.copyOf(mismatches); + } + + @Override + protected void renderBg(GuiGraphics graphics, float partialTick, int mouseX, int mouseY) { + updateTaskPageControls(); + MachineScreenStyle.drawPanel(graphics, leftPos, topPos, imageWidth, imageHeight); + MachineScreenStyle.drawInset(graphics, + leftPos + 4, topPos + 20, leftPos + imageWidth - 4, topPos + 128); + MachineScreenStyle.drawSlotGroup(graphics, leftPos, topPos, 8, 140, 9, 3); + MachineScreenStyle.drawSlotGroup(graphics, leftPos, topPos, 8, 198, 9, 1); + for (var slot : menu.slots) { + MachineScreenStyle.drawSlotBackground(graphics, leftPos, topPos, slot); + } + long capacity = menu.getCapacity(); + int width = capacity <= 0 ? 0 : (int) Math.round( + Math.min(1.0D, (double) menu.getEnergy() / (double) capacity) * ENERGY_BAR_WIDTH); + graphics.fill(leftPos + ENERGY_BAR_X, topPos + ENERGY_BAR_Y, + leftPos + ENERGY_BAR_X + ENERGY_BAR_WIDTH, + topPos + ENERGY_BAR_Y + ENERGY_BAR_HEIGHT, 0xFF272727); + graphics.fill(leftPos + ENERGY_BAR_X, topPos + ENERGY_BAR_Y, + leftPos + ENERGY_BAR_X + width, + topPos + ENERGY_BAR_Y + ENERGY_BAR_HEIGHT, 0xFF4CAF50); + } + + @Override + protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) { + String titleText = title.getString(); + if (font.width(titleText) > 116) titleText = font.plainSubstrByWidth(titleText, 113) + "..."; + graphics.drawString(font, titleText, 8, 6, MachineScreenStyle.TEXT_COLOR, false); + boolean structureValid = liveValidation == null ? menu.isFormed() : liveValidation.valid(); + int stateColor = structureValid ? 0xFF2E7D32 : MachineScreenStyle.ERROR_TEXT_COLOR; + Component stateText; + if (structureValid) { + stateText = Component.translatable("gui.useless_mod.multiblock_alloy_furnace.formed"); + } else if (liveValidation != null && !liveValidation.mismatches().isEmpty()) { + stateText = Component.translatable( + "gui.useless_mod.multiblock_alloy_furnace.invalid_count", + liveValidation.mismatches().size()); + } else { + stateText = Component.translatable("gui.useless_mod.multiblock_alloy_furnace.invalid"); + } + graphics.drawString(font, stateText, 8, 22, stateColor, false); + int detectedCoilTier = liveValidation == null ? menu.getCoilTier() : liveValidation.coilTier(); + graphics.drawString(font, Component.translatable( + "gui.useless_mod.multiblock_alloy_furnace.coil", detectedCoilTier), + 8, 34, MachineScreenStyle.MUTED_TEXT_COLOR, false); + RedstoneControlMode redstoneMode = RedstoneControlMode.byIndex(menu.getRedstoneMode()); + Component redstoneText = Component.translatable( + "gui.useless_mod.multiblock_alloy_furnace.redstone", + Component.translatable("gui.useless_mod.advanced_alloy_furnace.redstone_control." + + redstoneMode.name().toLowerCase(Locale.ROOT))); + graphics.drawString(font, truncate(redstoneText, 76), + 8, 52, MachineScreenStyle.TEXT_COLOR, false); + graphics.drawString(font, truncate(Component.translatable( + "gui.useless_mod.multiblock_alloy_furnace.energy_limit"), 80), + ENERGY_LIMIT_FIELD_X, 50, MachineScreenStyle.MUTED_TEXT_COLOR, false); + Component tasksText = Component.translatable( + "gui.useless_mod.multiblock_alloy_furnace.tasks", + menu.getActiveTasks(), menu.getMaxTasks()); + graphics.drawString(font, truncate(tasksText, 76), + 8, 63, MachineScreenStyle.MUTED_TEXT_COLOR, false); + renderStructurePreview(graphics); + renderTaskList(graphics); + graphics.drawString(font, playerInventoryTitle, 8, inventoryLabelY, + MachineScreenStyle.MUTED_TEXT_COLOR, false); + } + + private Component truncate(Component text, int maxWidth) { + String value = text.getString(); + return font.width(value) <= maxWidth + ? text + : Component.literal(font.plainSubstrByWidth( + value, maxWidth - font.width("...")) + "..."); + } + + private void renderStructurePreview(GuiGraphics graphics) { + for (int layer = 0; layer < 4; layer++) { + int layerX = PREVIEW_ORIGIN_X + layer * PREVIEW_LAYER_STRIDE; + for (var entry : OmniversalAlloyFurnaceStructure.entries()) { + if (entry.localPos().getY() != layer) continue; + int x = layerX + (entry.localPos().getX() + 1) * PREVIEW_CELL_SIZE; + int y = PREVIEW_ORIGIN_Y + entry.localPos().getZ() * PREVIEW_CELL_SIZE; + BlockPos worldPos = entry.worldPos(menu.getBlockPos(), liveFacing); + boolean mismatch = liveMismatches.containsKey(worldPos.asLong()); + if (liveValidation == null) { + graphics.fill(x, y, x + PREVIEW_CELL_SIZE - 1, + y + PREVIEW_CELL_SIZE - 1, MachineScreenStyle.MUTED_TEXT_COLOR); + } else if (mismatch) { + graphics.fill(x, y, x + PREVIEW_CELL_SIZE - 1, + y + PREVIEW_CELL_SIZE - 1, MachineScreenStyle.ERROR_TEXT_COLOR); + graphics.fill(x + 1, y + 1, x + PREVIEW_CELL_SIZE - 2, + y + PREVIEW_CELL_SIZE - 2, partColor(entry, worldPos)); + } else { + graphics.fill(x, y, x + PREVIEW_CELL_SIZE - 1, + y + PREVIEW_CELL_SIZE - 1, partColor(entry, worldPos)); + } + } + } + } + + private int partColor(OmniversalAlloyFurnaceStructure.Entry entry, BlockPos worldPos) { + if (entry.part() == OmniversalAlloyFurnaceStructure.Part.CASING + && minecraft != null && minecraft.level != null) { + OmniversalAlloyFurnaceStructure.FunctionalPart functionalPart = + OmniversalAlloyFurnaceStructure.functionalPart(minecraft.level.getBlockState(worldPos)); + if (functionalPart != null) { + return switch (functionalPart) { + case PATTERN_ASSEMBLY -> 0xFF00838F; + case MOLD_HUB -> 0xFFF9A825; + case PASSIVE_HATCH -> 0xFF517497; + }; + } + } + return switch (entry.part()) { + case CORE -> 0xFFD84315; + case CASING -> 0xFF757575; + case COIL -> 0xFF6A1B9A; + case AIR -> 0xFF212121; + }; + } + + private void renderTaskList(GuiGraphics graphics) { + List> pages = taskPages(); + taskPage = net.minecraft.util.Mth.clamp(taskPage, 0, pages.size() - 1); + List lines = new ArrayList<>(); + for (var task : pages.get(taskPage)) { + lines.addAll(taskLines(task)); + } + graphics.pose().pushPose(); + graphics.pose().translate(TASK_LIST_X, TASK_LIST_Y, 0); + graphics.pose().scale(TASK_LIST_TEXT_SCALE, TASK_LIST_TEXT_SCALE, 1.0F); + for (int line = 0; line < lines.size(); line++) { + graphics.drawString(font, lines.get(line), 0, line * TASK_LIST_LINE_HEIGHT, + MachineScreenStyle.MUTED_TEXT_COLOR, false); + } + graphics.pose().popPose(); + + if (pages.size() > 1) { + String pageText = (taskPage + 1) + "/" + pages.size(); + graphics.drawString(font, pageText, + TASK_PAGE_NEXT_X - 2 - font.width(pageText), TASK_PAGE_BUTTON_Y + 1, + MachineScreenStyle.SUBTLE_TEXT_COLOR, false); + } + } + + private String fitTaskLine(String text) { + int scaledWidth = (int) (TASK_LIST_WIDTH / TASK_LIST_TEXT_SCALE); + if (font.width(text) <= scaledWidth) return text; + return font.plainSubstrByWidth(text, scaledWidth - font.width("...")) + "..."; + } + + private List taskLines( + com.sorrowmist.useless.network.AETaskProgressPacket.TaskProgressData task) { + String status = task.progress > 0 && task.maxProgress > 0 + ? task.progress + "/" + task.maxProgress + : Component.translatable(task.statusKey).getString(); + String detail = task.statusDetail == null || task.statusDetail.isBlank() ? "" + : task.statusDetail.startsWith("gui.useless_mod.") + ? Component.translatable(task.statusDetail).getString() + : task.statusDetail; + String state = detail.isEmpty() ? status : status + ": " + detail; + String product = task.productName + " x" + task.totalOutputCount; + String combined = product + " " + state; + if (font.width(combined) <= (int) (TASK_LIST_WIDTH / TASK_LIST_TEXT_SCALE)) { + return List.of(combined); + } + if (detail.isEmpty() || font.width(state) <= (int) (TASK_LIST_WIDTH / TASK_LIST_TEXT_SCALE)) { + return List.of(fitTaskLine(product), fitTaskLine(state)); + } + return List.of(fitTaskLine(product), fitTaskLine(status), fitTaskLine(detail)); + } + + private List> taskPages() { + var tasks = new ArrayList<>(menu.getTaskProgress()); + tasks.sort(Comparator + .comparing((com.sorrowmist.useless.network.AETaskProgressPacket.TaskProgressData task) + -> task.productName) + .thenComparingInt(task -> task.progress)); + List> pages = new ArrayList<>(); + List page = new ArrayList<>(); + int usedLines = 0; + for (var task : tasks) { + int requiredLines = taskLines(task).size(); + if (!page.isEmpty() && usedLines + requiredLines > TASK_LIST_MAX_LINES) { + pages.add(List.copyOf(page)); + page.clear(); + usedLines = 0; + } + page.add(task); + usedLines += requiredLines; + } + if (!page.isEmpty() || pages.isEmpty()) { + pages.add(List.copyOf(page)); + } + return pages; + } + + private void updateTaskPageControls() { + int pageCount = taskPages().size(); + taskPage = net.minecraft.util.Mth.clamp(taskPage, 0, pageCount - 1); + if (previousTaskPageButton != null) { + previousTaskPageButton.visible = pageCount > 1; + previousTaskPageButton.active = taskPage > 0; + } + if (nextTaskPageButton != null) { + nextTaskPageButton.visible = pageCount > 1; + nextTaskPageButton.active = taskPage < pageCount - 1; + } + } + + private void changeTaskPage(int delta) { + int lastPage = taskPages().size() - 1; + taskPage = net.minecraft.util.Mth.clamp(taskPage + delta, 0, lastPage); + updateTaskPageControls(); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + super.render(graphics, mouseX, mouseY, partialTick); + renderTooltip(graphics, mouseX, mouseY); + if (isHovering(ENERGY_BAR_X, ENERGY_BAR_Y, + ENERGY_BAR_WIDTH, ENERGY_BAR_HEIGHT, mouseX, mouseY)) { + graphics.renderTooltip(font, Component.translatable( + "gui.useless_mod.advanced_alloy_furnace.energy", + ScaledEnergyAmount.format(menu.getEnergy()), + ScaledEnergyAmount.format(menu.getCapacity())), mouseX, mouseY); + } + if (energyLimitField != null && energyLimitField.isMouseOver(mouseX, mouseY)) { + graphics.renderTooltip(font, List.of( + Component.translatable( + "gui.useless_mod.multiblock_alloy_furnace.energy_limit"), + Component.translatable( + "gui.useless_mod.multiblock_alloy_furnace.energy_limit.desc"), + Component.translatable( + "gui.useless_mod.multiblock_alloy_furnace.energy_limit.units")), + Optional.empty(), mouseX, mouseY); + } + renderControlTooltips(graphics, mouseX, mouseY); + renderStructureTooltip(graphics, mouseX, mouseY); + } + + private void renderControlTooltips(GuiGraphics graphics, int mouseX, int mouseY) { + if (isHovering(REDSTONE_BUTTON_X, CONTROL_BUTTON_Y, + AlloyFurnaceControlIcons.WIDTH, AlloyFurnaceControlIcons.HEIGHT, + mouseX, mouseY)) { + RedstoneControlMode mode = RedstoneControlMode.byIndex(menu.getRedstoneMode()); + graphics.renderTooltip(font, List.of( + Component.translatable( + "gui.useless_mod.advanced_alloy_furnace.redstone_control"), + Component.translatable( + "gui.useless_mod.advanced_alloy_furnace.redstone_control." + + mode.name().toLowerCase(Locale.ROOT))), + Optional.empty(), mouseX, mouseY); + } else if (isHovering(CANCEL_BUTTON_X, CONTROL_BUTTON_Y, + AlloyFurnaceControlIcons.WIDTH, AlloyFurnaceControlIcons.HEIGHT, + mouseX, mouseY)) { + graphics.renderTooltip(font, List.of( + Component.translatable( + "gui.useless_mod.advanced_alloy_furnace.cancel_ae_tasks"), + Component.translatable( + "gui.useless_mod.advanced_alloy_furnace.cancel_ae_tasks.desc")), + Optional.empty(), mouseX, mouseY); + } + } + + private void renderStructureTooltip(GuiGraphics graphics, int mouseX, int mouseY) { + OmniversalAlloyFurnaceStructure.Entry entry = findHoveredPreviewEntry(mouseX, mouseY); + if (entry == null || minecraft == null || minecraft.level == null) return; + + BlockPos worldPos = entry.worldPos(menu.getBlockPos(), liveFacing); + BlockState actual = minecraft.level.getBlockState(worldPos); + boolean known = liveValidation != null; + boolean mismatch = known && liveMismatches.containsKey(worldPos.asLong()); + List lines = new ArrayList<>(4); + lines.add(Component.translatable(known + ? mismatch + ? "gui.useless_mod.multiblock_alloy_furnace.preview.incorrect" + : "gui.useless_mod.multiblock_alloy_furnace.preview.correct" + : "gui.useless_mod.multiblock_alloy_furnace.preview.unavailable") + .withStyle(!known ? ChatFormatting.GRAY + : mismatch ? ChatFormatting.RED : ChatFormatting.GREEN)); + Component expected = expectedPartName(entry, worldPos); + lines.add(Component.translatable( + "gui.useless_mod.multiblock_alloy_furnace.preview.expected", expected)); + lines.add(Component.translatable( + "gui.useless_mod.multiblock_alloy_furnace.preview.actual", + actual.getBlock().getName())); + lines.add(Component.translatable( + "gui.useless_mod.multiblock_alloy_furnace.preview.position", + worldPos.getX(), worldPos.getY(), worldPos.getZ())); + graphics.renderTooltip(font, lines, Optional.empty(), mouseX, mouseY); + } + + private Component expectedPartName( + OmniversalAlloyFurnaceStructure.Entry entry, BlockPos worldPos) { + return switch (entry.part()) { + case CORE -> ModBlocks.MULTIBLOCK_ALLOY_FURNACE_CORE.get().getName(); + case CASING -> expectedCasingName(worldPos); + case COIL -> expectedCoilName(); + case AIR -> Blocks.AIR.getName(); + }; + } + + private Component expectedCasingName(BlockPos worldPos) { + Component casingName = ModBlocks.OMNIVERSAL_FURNACE_CASING.get().getName(); + return Component.translatable( + "gui.useless_mod.multiblock_alloy_furnace.preview.casing_with_functional_parts", + casingName, ModBlocks.ME_PATTERN_ASSEMBLY.get().getName(), + ModBlocks.OMNIVERSAL_MOLD_HUB.get().getName(), + ModBlocks.PASSIVE_CRAFTING_HATCH.get().getName()); + } + + private Component expectedCoilName() { + int tier = liveValidation == null ? menu.getCoilTier() : liveValidation.coilTier(); + var coil = ModBlocks.USELESS_COILS.get(tier); + if (coil != null) return coil.get().getName(); + return Component.translatable( + "gui.useless_mod.multiblock_alloy_furnace.preview.coil_any", + ModBlocks.USELESS_COILS.get(UselessCoilBlock.MIN_TIER).get().getName(), + ModBlocks.USELESS_COILS.get(UselessCoilBlock.MAX_TIER).get().getName()); + } + + @Nullable + private OmniversalAlloyFurnaceStructure.Entry findHoveredPreviewEntry(int mouseX, int mouseY) { + int relativeX = mouseX - leftPos; + int relativeY = mouseY - topPos; + for (var entry : OmniversalAlloyFurnaceStructure.entries()) { + int layerX = PREVIEW_ORIGIN_X + + entry.localPos().getY() * PREVIEW_LAYER_STRIDE; + int x = layerX + (entry.localPos().getX() + 1) * PREVIEW_CELL_SIZE; + int y = PREVIEW_ORIGIN_Y + entry.localPos().getZ() * PREVIEW_CELL_SIZE; + if (relativeX >= x && relativeX < x + PREVIEW_CELL_SIZE - 1 + && relativeY >= y && relativeY < y + PREVIEW_CELL_SIZE - 1) { + return entry; + } + } + return null; + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/gui/OmniversalPatternMoldSearchTerms.java b/src/main/java/com/sorrowmist/useless/client/gui/OmniversalPatternMoldSearchTerms.java new file mode 100644 index 00000000..382c99a8 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/gui/OmniversalPatternMoldSearchTerms.java @@ -0,0 +1,57 @@ +package com.sorrowmist.useless.client.gui; + +import com.sorrowmist.useless.core.component.OmniversalPatternData; +import com.sorrowmist.useless.core.component.UComponents; + +import net.minecraft.world.item.ItemStack; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Extracts encoded internal molds as separate Trinity pattern-search candidates. + */ +public final class OmniversalPatternMoldSearchTerms { + + private OmniversalPatternMoldSearchTerms() {} + + /** + * Returns mold display names stored in one Omniversal Pattern, when that pattern needs molds. + * + * @param encodedPattern candidate encoded pattern stack + * @return independent mold-name candidates, or no candidates for every other pattern + */ + public static List searchTerms(ItemStack encodedPattern) { + OmniversalPatternData data = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(encodedPattern, UComponents.OMNIVERSAL_PATTERN_DATA.get()); + return data == null ? List.of() : searchTerms(data); + } + + /** + * Returns display candidates for metadata that has already been decoded from an Omniversal Pattern. + * + * @param data persisted Omniversal Pattern metadata + * @return independent mold-name candidates, or no candidates when no mold is required + */ + public static List searchTerms(OmniversalPatternData data) { + if (!data.requiresMold()) { + return List.of(); + } + List terms = new ArrayList<>(); + Set tagMoldSlots = new HashSet<>(); + data.moldTagInputSlots().forEach(slot -> tagMoldSlots.add(slot.moldSlot())); + if (!data.displayMolds().isEmpty()) { + for (int moldSlot = 0; moldSlot < data.displayMolds().size(); moldSlot++) { + if (tagMoldSlots.contains(moldSlot)) continue; + terms.add(data.displayMolds().get(moldSlot).getDisplayName().getString()); + } + } else if (!tagMoldSlots.contains(0)) { + data.displayMold().map(key -> key.getDisplayName().getString()).ifPresent(terms::add); + } + data.moldTagInputSlots().stream() + .map(slot -> "#" + slot.tag().location()) + .forEach(terms::add); + return terms.stream().distinct().toList(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/gui/OreGeneratorScreen.java b/src/main/java/com/sorrowmist/useless/client/gui/OreGeneratorScreen.java new file mode 100644 index 00000000..370a3dea --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/gui/OreGeneratorScreen.java @@ -0,0 +1,266 @@ +package com.sorrowmist.useless.client.gui; + +import com.sorrowmist.useless.content.menus.OreGeneratorMenu; +import com.sorrowmist.useless.network.OreGeneratorOutputTogglePacket; +import com.sorrowmist.useless.network.OreGeneratorSettingsPacket; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.Slot; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; +import org.lwjgl.glfw.GLFW; + +import java.util.List; +import java.util.Optional; + +public final class OreGeneratorScreen extends AbstractContainerScreen { + private static final int PANEL_WIDTH = 250; + private static final int PANEL_HEIGHT = 336; + + private EditBox rateField; + private PressableAE2Button rateDown; + private PressableAE2Button rateUp; + private PressableAE2Button applyButton; + private PressableAE2Button aeButton; + private PressableAE2Button previousPageButton; + private PressableAE2Button nextPageButton; + private long lastSyncedRate = Long.MIN_VALUE; + private String syncedRateText = ""; + private boolean updatingRateField; + private boolean rateDirty; + + public OreGeneratorScreen(OreGeneratorMenu menu, Inventory inventory, Component title) { + super(menu, inventory, title); + imageWidth = PANEL_WIDTH; + imageHeight = PANEL_HEIGHT; + inventoryLabelX = 44; + inventoryLabelY = 242; + titleLabelX = 8; + titleLabelY = 7; + } + + @Override + protected void init() { + super.init(); + previousPageButton = addRenderableWidget(new PressableAE2Button( + leftPos + 128, topPos + 4, 18, 12, + Component.literal("<"), button -> changePage(-1))); + nextPageButton = addRenderableWidget(new PressableAE2Button( + leftPos + 148, topPos + 4, 18, 12, + Component.literal(">"), button -> changePage(1))); + + rateField = new EditBox(font, leftPos + 177, topPos + 35, 62, 14, + Component.translatable("gui.useless_mod.ore_generator.rate")); + rateField.setMaxLength(24); + rateField.setFilter(ScaledEnergyAmount::isValidInput); + rateField.setResponder(value -> { + if (!updatingRateField) rateDirty = !value.equals(syncedRateText); + }); + addRenderableWidget(rateField); + + rateDown = addRenderableWidget(new PressableAE2Button( + leftPos + 177, topPos + 52, 28, 14, + Component.literal("-"), button -> adjustRate(-1))); + rateUp = addRenderableWidget(new PressableAE2Button( + leftPos + 211, topPos + 52, 28, 14, + Component.literal("+"), button -> adjustRate(1))); + applyButton = addRenderableWidget(new PressableAE2Button( + leftPos + 177, topPos + 73, 62, 18, + Component.translatable("gui.useless_mod.ore_generator.apply"), + button -> sendRate())); + aeButton = addRenderableWidget(new PressableAE2Button( + leftPos + 177, topPos + 112, 62, 18, + Component.translatable("gui.useless_mod.ore_generator.ae_toggle"), + button -> toggleAeOutput())); + syncRateField(true); + updateControls(); + } + + @Override + protected void containerTick() { + super.containerTick(); + syncRateField(false); + updateControls(); + } + + private void syncRateField(boolean force) { + if (rateField == null) return; + long rate = Math.max(1L, menu.getOutputRate()); + if ((force || (!rateField.isFocused() && !rateDirty)) && rate != lastSyncedRate) { + setRateFieldValue(rate); + lastSyncedRate = rate; + } + } + + private void setRateFieldValue(long rate) { + syncedRateText = ScaledEnergyAmount.format(rate); + lastSyncedRate = Math.max(0L, rate); + updatingRateField = true; + rateField.setValue(syncedRateText); + updatingRateField = false; + rateDirty = false; + } + + private void sendRate() { + long rate = readRateField(); + rate = Math.max(1L, rate); + setRateFieldValue(rate); + lastSyncedRate = rate; + PacketDistributor.sendToServer(new OreGeneratorSettingsPacket( + menu.containerId, menu.getBlockPos(), rate)); + } + + private void adjustRate(long delta) { + long current = readRateField(); + long adjusted; + if (delta > 0) { + adjusted = current == Long.MAX_VALUE ? Long.MAX_VALUE : current + 1L; + } else { + adjusted = Math.max(1L, current - 1L); + } + setRateFieldValue(Math.max(1L, adjusted)); + sendRate(); + } + + private long readRateField() { + if (!rateDirty && rateField.getValue().equals(syncedRateText) + && lastSyncedRate >= 1L) { + return lastSyncedRate; + } + return ScaledEnergyAmount.parse(rateField.getValue(), Long.MAX_VALUE) + .orElse(menu.getOutputRate()); + } + + private void toggleAeOutput() { + PacketDistributor.sendToServer(new OreGeneratorOutputTogglePacket( + menu.containerId, menu.getBlockPos())); + } + + private void changePage(int delta) { + if (minecraft == null || minecraft.gameMode == null) return; + int pageCount = Math.max(1, menu.getPageCount()); + int target = Math.floorMod(menu.getPage() + delta, pageCount); + if (target != menu.getPage()) { + minecraft.gameMode.handleInventoryButtonClick(menu.containerId, + delta > 0 ? OreGeneratorMenu.NEXT_PAGE : OreGeneratorMenu.PREVIOUS_PAGE); + } + } + + private void updateControls() { + if (rateField == null) return; + boolean editable = !menu.isRecoveryPage(); + rateField.setEditable(editable); + rateDown.active = editable; + rateUp.active = editable; + applyButton.active = editable; + aeButton.active = true; + previousPageButton.visible = menu.getPageCount() > 1; + nextPageButton.visible = menu.getPageCount() > 1; + previousPageButton.active = menu.getPage() > 0; + nextPageButton.active = menu.getPage() + 1 < menu.getPageCount(); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if ((keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) + && rateField != null && rateField.isFocused()) { + sendRate(); + setFocused(null); + return true; + } + if (keyCode != GLFW.GLFW_KEY_ESCAPE && rateField != null + && (rateField.keyPressed(keyCode, scanCode, modifiers) || rateField.canConsumeInput())) { + return true; + } + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public void onClose() { + if (rateDirty) sendRate(); + super.onClose(); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + previousPageButton.releaseVisualState(); + nextPageButton.releaseVisualState(); + rateDown.releaseVisualState(); + rateUp.releaseVisualState(); + applyButton.releaseVisualState(); + aeButton.releaseVisualState(); + return super.mouseReleased(mouseX, mouseY, button); + } + + @Override + protected void renderSlot(GuiGraphics graphics, Slot slot) { + super.renderSlot(graphics, slot); + int sampleSlot = menu.getSampleSlotIndex(slot); + if (sampleSlot < 0 || sampleSlot < menu.getActiveSlots()) return; + + graphics.fill(slot.x, slot.y, slot.x + 16, slot.y + 16, 0x66000000); + graphics.fill(slot.x + 11, slot.y + 2, slot.x + 15, slot.y + 7, 0xFFF2F2F2); + graphics.fill(slot.x + 12, slot.y + 1, slot.x + 14, slot.y + 3, 0xFFF2F2F2); + } + + @Override + protected void renderBg(GuiGraphics graphics, float partialTick, int mouseX, int mouseY) { + MachineScreenStyle.drawPanel(graphics, leftPos, topPos, imageWidth, imageHeight); + MachineScreenStyle.drawInset(graphics, + leftPos + 172, topPos + 18, leftPos + 246, topPos + 142); + MachineScreenStyle.drawSlotGroup(graphics, leftPos, topPos, 8, 22, 9, 10); + MachineScreenStyle.drawSlotGroup(graphics, leftPos, topPos, 44, 254, 9, 3); + MachineScreenStyle.drawSlotGroup(graphics, leftPos, topPos, 44, 312, 9, 1); + for (Slot slot : menu.slots) { + MachineScreenStyle.drawSlotBackground(graphics, leftPos, topPos, slot); + } + } + + @Override + protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) { + graphics.drawString(font, title, titleLabelX, titleLabelY, + MachineScreenStyle.TEXT_COLOR, false); + String page = (menu.getPage() + 1) + "/" + menu.getPageCount(); + graphics.drawString(font, page, 126 - font.width(page), 6, + menu.isRecoveryPage() ? MachineScreenStyle.ERROR_TEXT_COLOR + : MachineScreenStyle.MUTED_TEXT_COLOR, false); + graphics.drawString(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, + MachineScreenStyle.TEXT_COLOR, false); + + graphics.drawString(font, Component.translatable("gui.useless_mod.ore_generator.rate"), + 177, 22, MachineScreenStyle.TEXT_COLOR, false); + graphics.drawString(font, Component.translatable("gui.useless_mod.ore_generator.rate_unit"), + 177, 94, MachineScreenStyle.MUTED_TEXT_COLOR, false); + Component aeState = Component.translatable(menu.isAeOnline() + ? "gui.useless_mod.ore_generator.ae_online" + : "gui.useless_mod.ore_generator.ae_offline"); + graphics.drawString(font, aeState, 177, 101, + menu.isAeOnline() ? 0xFF2E7D32 : MachineScreenStyle.ERROR_TEXT_COLOR, false); + graphics.drawString(font, Component.translatable( + "gui.useless_mod.ore_generator.countdown", menu.getCountdownTicks()), + 8, 204, MachineScreenStyle.MUTED_TEXT_COLOR, false); + graphics.drawString(font, Component.translatable( + "gui.useless_mod.ore_generator.active_slots", + menu.getActiveSlots(), menu.getConfiguredSlots()), + 8, 215, MachineScreenStyle.MUTED_TEXT_COLOR, false); + graphics.drawString(font, Component.translatable( + menu.isOutputToAe() ? "gui.useless_mod.ore_generator.ae_enabled" + : "gui.useless_mod.ore_generator.ae_disabled"), + 177, 136, MachineScreenStyle.TEXT_COLOR, false); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + super.render(graphics, mouseX, mouseY, partialTick); + renderTooltip(graphics, mouseX, mouseY); + int sampleSlot = menu.getSampleSlotIndex(hoveredSlot); + if (hoveredSlot != null && sampleSlot >= menu.getActiveSlots() + && hoveredSlot.getItem().isEmpty()) { + graphics.renderTooltip(font, + List.of(Component.translatable("gui.useless_mod.ore_generator.locked_slot")), + Optional.empty(), mouseX, mouseY); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/gui/PagedRecoverableScreen.java b/src/main/java/com/sorrowmist/useless/client/gui/PagedRecoverableScreen.java new file mode 100644 index 00000000..9744563c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/gui/PagedRecoverableScreen.java @@ -0,0 +1,388 @@ +package com.sorrowmist.useless.client.gui; + +import com.sorrowmist.useless.content.menus.PagedRecoverableMenu; +import net.minecraft.Util; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.network.chat.Component; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.Slot; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.glfw.GLFW; + +/** + * 带分页、只读恢复页与搜索框的容器界面基类。 + * + *

搜索在客户端完成:屏幕遍历当前页同步过来的槽位内容,按子类给出的匹配规则筛出命中项, + * 并在这些槽位上叠加高亮标记。槽位位置与交互始终由原版按真实坐标处理,高亮只增加视觉标注, + * 不改变槽位排布,因此未命中项仍在原位可见,拾取与快速移动也无需额外换算。

+ */ +public class PagedRecoverableScreen extends AbstractContainerScreen { + /** 命中槽位的高亮边框颜色,带透明度以免遮挡槽位内容。 */ + protected static final int HIGHLIGHT_COLOR = 0xCC55FF55; + + /** + * 搜索框左缘,与下方样板槽位对齐。 + * + *

槽位由菜单以 {@code storageX = 8} 为起点排布,槽位实体左缘即 {@code leftPos + 8}。 + * 搜索框与该左缘取齐,使输入区与槽位内容收于同一条左侧竖线;槽位底板另有 1 像素内缩边框, + * 属底板自身装饰,不作为对齐基准。

+ */ + private static final int SEARCH_FIELD_X = 8; + /** 搜索框占据标题下方的独立一行,避免与标题和页码指示重叠。 */ + private static final int SEARCH_FIELD_Y = 17; + /** + * 搜索区右缘,与下一页按钮的右缘对齐。 + * + *

下一页按钮位于 {@code leftPos + 152}、宽 18,右缘为 170。搜索区整体以此为右边界, + * 使翻页按钮与搜索范围切换按钮在视觉上收束于同一条竖线。

+ */ + private static final int SEARCH_AREA_RIGHT = 152 + 18; + /** 搜索框与范围切换按钮之间的间隔。 */ + private static final int SEARCH_MODE_GAP = 2; + /** 搜索范围切换按钮宽度,与输入框高度一致以保持等宽比例。 */ + private static final int SEARCH_MODE_WIDTH = 26; + /** + * 搜索框宽度。 + * + *

由搜索区右缘反推:右缘减去按钮宽度与间隔,即为输入框的右边界;再减去左起点得到宽度。 + * 该推导保证按钮右缘始终与下一页按钮对齐,调整按钮宽度或右缘时无需另行手算。

+ */ + private static final int SEARCH_FIELD_WIDTH = + SEARCH_AREA_RIGHT - SEARCH_MODE_WIDTH - SEARCH_MODE_GAP - SEARCH_FIELD_X; + /** 搜索框高度,与范围切换按钮及 AE 文本框贴图的行高保持协调。 */ + private static final int SEARCH_FIELD_HEIGHT = 14; + /** 搜索范围切换按钮紧随搜索框右侧。 */ + private static final int SEARCH_MODE_X = SEARCH_FIELD_X + SEARCH_FIELD_WIDTH + SEARCH_MODE_GAP; + /** 槽位网格在搜索框之下的纵向起点,与菜单槽位坐标保持一致。 */ + private static final int SLOT_GRID_Y = 32; + + @Nullable + private PressableAE2Button previousPageButton; + @Nullable + private PressableAE2Button nextPageButton; + @Nullable + private AE2StyleTextField searchField; + @Nullable + private PressableAE2Button searchModeButton; + + private String searchQuery = ""; + private PatternSearchMode searchMode = PatternSearchMode.ALL; + /** 参与高亮的槽位上限:只读恢复页不进入搜索结果。 */ + private int filterableSlots = PagedRecoverableMenu.SLOTS_PER_PAGE; + + public PagedRecoverableScreen(T menu, Inventory inventory, Component title) { + super(menu, inventory, title); + imageWidth = 176; + imageHeight = 306; + inventoryLabelY = 213; + } + + @Override + protected void init() { + super.init(); + previousPageButton = addRenderableWidget(new PressableAE2Button( + leftPos + 132, topPos + 4, 18, 12, + Component.literal("<"), button -> page(PagedRecoverableMenu.PREVIOUS_PAGE))); + nextPageButton = addRenderableWidget(new PressableAE2Button( + leftPos + 152, topPos + 4, 18, 12, + Component.literal(">"), button -> page(PagedRecoverableMenu.NEXT_PAGE))); + + searchField = new AE2StyleTextField(font, + leftPos + SEARCH_FIELD_X, topPos + SEARCH_FIELD_Y, + SEARCH_FIELD_WIDTH, SEARCH_FIELD_HEIGHT, + Component.translatable("gui.useless_mod.paged.search")); + searchField.setMaxLength(64); + searchField.setHint(searchHint()); + searchField.setValue(searchQuery); + searchField.setResponder(value -> { + searchQuery = value; + applyFilter(); + }); + addRenderableWidget(searchField); + + if (supportsSearchMode()) { + searchModeButton = addRenderableWidget(new PressableAE2Button( + leftPos + SEARCH_MODE_X, topPos + SEARCH_FIELD_Y, SEARCH_MODE_WIDTH, SEARCH_FIELD_HEIGHT, + searchModeLabel(), button -> cycleSearchMode())); + } + + filterableSlots = computeFilterableSlots(); + applyFilter(); + } + + /** + * 当前界面是否需要搜索范围选择。 + * + *

只有样板类界面同时具有输入与输出两侧,需要区分范围;模具等单一语义界面返回 + * {@code false},避免出现无意义的切换按钮。

+ */ + protected boolean supportsSearchMode() { + return false; + } + + /** 当前生效的样板搜索范围。 */ + protected final PatternSearchMode searchMode() { + return searchMode; + } + + private void cycleSearchMode() { + searchMode = searchMode.next(); + if (searchModeButton != null) { + searchModeButton.setMessage(searchModeLabel()); + } + if (searchField != null) { + searchField.setHint(searchHint()); + } + applyFilter(); + } + + /** + * 搜索框的空值提示文本,随当前搜索范围变化。 + * + *

提示需要反映当前范围匹配的对象,否则玩家无从判断输入的名称会落在输入侧、输出侧 + * 还是模具上,范围切换也会缺少可见反馈。

+ */ + private Component searchHint() { + if (!supportsSearchMode()) { + return Component.translatable("gui.useless_mod.paged.search.hint"); + } + String key = switch (searchMode) { + case IN -> "gui.useless_mod.paged.search.hint.in"; + case OUT -> "gui.useless_mod.paged.search.hint.out"; + case MOLD -> "gui.useless_mod.paged.search.hint.mold"; + case ALL -> "gui.useless_mod.paged.search.hint.all"; + }; + return Component.translatable(key); + } + + /** 搜索范围按钮的短标签,用于在有限宽度内指示当前范围。 */ + private Component searchModeLabel() { + String key = switch (searchMode) { + case OUT -> "gui.useless_mod.paged.search_mode.out"; + case IN -> "gui.useless_mod.paged.search_mode.in"; + case MOLD -> "gui.useless_mod.paged.search_mode.mold"; + case ALL -> "gui.useless_mod.paged.search_mode.all"; + }; + return Component.translatable(key); + } + + /** + * 当前页内可搜索的槽位数量。 + * + *

默认把后备库存的活跃槽位总数换算成本页可搜索数量:溢出到只读恢复页的条目只能取出、 + * 无法回填,纳入搜索结果只会让玩家看到不可用项,因此恢复页整体落在范围之外。 + * {@code filterableSlots} 以当前页内的相对槽位号计数,换算后与其坐标系一致。

+ */ + protected int computeFilterableSlots() { + int remaining = menu.getActiveSlotCount() - menu.getPage() * PagedRecoverableMenu.SLOTS_PER_PAGE; + return Math.max(0, Math.min(PagedRecoverableMenu.SLOTS_PER_PAGE, remaining)); + } + + /** + * 判断指定菜单槽位是否命中当前查询。 + * + *

默认实现命中全部非空项。子类按样板或模具语义覆写后即可获得对应搜索能力。

+ */ + protected boolean matchesSearch(int menuSlot) { + return !menu.getSlot(menuSlot).getItem().isEmpty(); + } + + /** + * 按当前查询刷新搜索高亮状态。 + * + *

高亮不改变槽位排布,因此这里只需切换状态标记;命中项在渲染阶段按当前页内容即时判定, + * 页面切换或内容变化都会自然反映。查询为空时退出高亮状态。

+ */ + protected final void applyFilter() { + menu.setSearchActive(!PatternSearchMatcher.normalize(searchQuery).isEmpty()); + } + + /** 当前生效的规范化查询串。 */ + protected final String normalizedQuery() { + return PatternSearchMatcher.normalize(searchQuery); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + // 搜索框聚焦期间不把按键转交父类:AbstractContainerScreen 在子控件未消费按键时 + // 会比对容器快捷键(默认 E 打开物品栏),命中即调用 onClose 关闭本界面, + // 导致该字母无法输入。Esc 仍然放行,保持「Esc 关闭界面」的既有行为。 + if (keyCode != GLFW.GLFW_KEY_ESCAPE && searchField != null + && (searchField.keyPressed(keyCode, scanCode, modifiers) || searchField.canConsumeInput())) { + return true; + } + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + // 点击落在搜索框之外时清除其焦点。原版 AbstractContainerScreen 覆写的 mouseClicked + // 不处理组件焦点,缺少该判定时输入框会保持聚焦底纹,视觉上等同于一直处于按下状态。 + if (searchField != null && !searchField.isMouseOver(mouseX, mouseY)) { + setFocused(null); + } + return super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + // 逐个复位所有可按下按钮。若只复位部分按钮,未复位者会停留在按下态; + // 该复位无法由原版完成,因为 pressed 是本项目按钮自行维护的显示状态。 + releaseButtonVisuals(previousPageButton, nextPageButton, searchModeButton); + return super.mouseReleased(mouseX, mouseY, button); + } + + /** 复位给定按钮的按下显示状态,忽略空引用。 */ + private static void releaseButtonVisuals(PressableAE2Button... buttons) { + for (PressableAE2Button button : buttons) { + if (button != null) { + button.releaseVisualState(); + } + } + } + + private void page(int id) { + if (minecraft != null && minecraft.gameMode != null) { + minecraft.gameMode.handleInventoryButtonClick(menu.containerId, id); + } + } + + @Override + protected void renderBg(GuiGraphics graphics, float partialTick, int mouseX, int mouseY) { + MachineScreenStyle.drawPanel(graphics, leftPos, topPos, imageWidth, imageHeight); + MachineScreenStyle.drawSlotGroup(graphics, leftPos, topPos, 8, SLOT_GRID_Y, 9, 10); + MachineScreenStyle.drawSlotGroup(graphics, leftPos, topPos, 8, 225, 9, 3); + MachineScreenStyle.drawSlotGroup(graphics, leftPos, topPos, 8, 283, 9, 1); + // 槽位序号取自菜单槽位而非 Slot.index:玩家背包槽位的 index 同样落在存储区范围内, + // 按 index 判定会把背包槽位误画成恢复槽位。 + for (int index = 0; index < menu.slots.size(); index++) { + Slot slot = menu.getSlot(index); + if (menu.isRecoverySlot(index)) { + MachineScreenStyle.drawDisabledSlotBackground(graphics, leftPos, topPos, slot); + } else { + MachineScreenStyle.drawSlotBackground(graphics, leftPos, topPos, slot); + } + } + } + + /** + * 绘制鼠标悬停高亮。 + * + *

只读恢复槽位不绘制悬停高亮:该区域只允许取出,高亮会给出「可以放入」的错误暗示, + * 与未启用样式的底板相互矛盾。悬停本身仍被原版记录,因此恢复区中已有条目的物品提示与 + * 取出操作不受影响。

+ * + *

槽位序号经 {@code menu.slots} 反查而非取自 {@code Slot.index},后者是页视图内的相对 + * 序号,对玩家背包槽位同样落在存储区区间内。反查失败时返回 {@code -1},判定结果一律为 + * {@code false},不会误伤非存储区槽位。

+ */ + @Override + public int getSlotColor(int index) { + return menu.isRecoverySlot(index) ? 0 : super.getSlotColor(index); + } + + @Override + protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) { + String titleText = title.getString(); + if (font.width(titleText) > 88) { + titleText = font.plainSubstrByWidth(titleText, 85) + "..."; + } + graphics.drawString(font, titleText, 8, 6, MachineScreenStyle.TEXT_COLOR, false); + String page = (menu.getPage() + 1) + "/" + menu.getPageCount(); + graphics.drawString(font, page, 128 - font.width(page), 6, + menu.isRecoveryPage() + ? MachineScreenStyle.ERROR_TEXT_COLOR + : MachineScreenStyle.MUTED_TEXT_COLOR, + false); + graphics.drawString(font, playerInventoryTitle, 8, inventoryLabelY, + MachineScreenStyle.MUTED_TEXT_COLOR, false); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + super.render(graphics, mouseX, mouseY, partialTick); + // 物品与提示框均由原版在 super.render 中绘制,先提交其批次再绘制高亮, + // 否则同批次内的绘制顺序无法保证高亮位于物品之上。 + // 注意 flush 只解决「谁后画」,深度测试还得靠 SEARCH_HIGHLIGHT_Z 抬 z, + // 两件事缺一不可。 + graphics.flush(); + renderSearchHighlights(graphics, partialTick); + renderTooltip(graphics, mouseX, mouseY); + } + + /** + * 命中槽位高亮的绘制 z。 + * + *

GUI 的深度测试是开着的({@code RenderType.GUI} 走 {@code LEQUAL_DEPTH_TEST}), + * 而槽位物品画在 z=150 附近({@code GuiGraphics#renderItem} 的 +150,再叠模型自身 + * 经 {@code display.gui} 变换后的几个像素)。高亮边框留在 z=0 会比物品远, + * 压在物品贴图上的那几条边会被深度测试直接丢掉 —— 贴图铺满整格的方块类物品上 + * 高亮基本看不见,且光 {@code flush()} 修不好(它只管先后,不管远近)。

+ * + *

取 170:压过物品并留出余量,又刻意低于堆叠数字的 z=200 —— + * 数量是搜索时最要紧的信息,不该被半透明高亮染色;同时也低于原版 tooltip 的 400。

+ */ + private static final float SEARCH_HIGHLIGHT_Z = 170.0F; + + /** + * 高亮命中当前查询的槽位。 + * + *

槽位位置与交互均由原版按真实坐标处理,搜索结果只在其上叠加高亮边框, + * 因此未命中项仍在原位可见,玩家背包等非搜索区域不受影响,悬停与拾取也无需额外换算。

+ * + *

搜索范围限于当前页内的可回填槽位:只读恢复页的条目无法回填,不参与高亮。

+ */ + private void renderSearchHighlights(GuiGraphics graphics, float partialTick) { + if (!menu.isFiltered() || menu.isRecoveryPage()) return; + int limit = Math.min(filterableSlots, menu.slots.size()); + // 抬 z 放在这里而不是 highlightSlot 内部,是为了让子类覆写的标记样式一并生效。 + graphics.pose().pushPose(); + graphics.pose().translate(0.0F, 0.0F, SEARCH_HIGHLIGHT_Z); + int order = 0; + for (int menuSlot = 0; menuSlot < limit; menuSlot++) { + if (!matchesSearch(menuSlot)) continue; + Slot slot = menu.getSlot(menuSlot); + if (slot.getItem().isEmpty()) continue; + highlightSlot(graphics, slot, order++, partialTick); + } + graphics.pose().popPose(); + } + + /** + * 绘制单个命中槽位的高亮标记。 + * + *

默认绘制覆盖槽位区域的半透明边框,颜色沿命中项顺序与时间双重推进,形成彩虹流转效果; + * 子类可覆写以改变标记样式。

+ * + * @param order 命中项在当前页内的顺序,用于制造相邻槽位之间的色相梯度 + */ + protected void highlightSlot(GuiGraphics graphics, Slot slot, int order, float partialTick) { + int color = rainbowColor(order, partialTick); + int x1 = leftPos + slot.x; + int y1 = topPos + slot.y; + int x2 = x1 + 16; + int y2 = y1 + 16; + graphics.fill(x1, y1, x2, y1 + 1, color); + graphics.fill(x1, y2 - 1, x2, y2, color); + graphics.fill(x1, y1, x1 + 1, y2, color); + graphics.fill(x2 - 1, y1, x2, y2, color); + } + + /** + * 计算彩虹流转颜色。 + * + *

色相由命中项顺序与游戏运行时间共同决定:顺序项产生固定的相邻梯度,时间项使整体色相持续 + * 推移,两者叠加即得到沿搜索命中序列流动的彩虹。返回值按 ARGB 组装,透明度沿用 + * {@link #HIGHLIGHT_COLOR} 的高位,避免高亮遮挡槽位内容。

+ */ + private static int rainbowColor(int order, float partialTick) { + long millis = Util.getMillis(); + // 每 16 毫秒一帧的基准下,相位每约 3 秒循环一周。 + float phase = (millis % 3000L) / 3000.0F; + float hue = (phase + order * 0.07F) % 1.0F; + int rgb = Mth.hsvToRgb(hue, 0.85F, 1.0F); + return (HIGHLIGHT_COLOR & 0xFF000000) | (rgb & 0x00FFFFFF); + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/gui/PassiveCraftingHatchScreen.java b/src/main/java/com/sorrowmist/useless/client/gui/PassiveCraftingHatchScreen.java new file mode 100644 index 00000000..072410a6 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/gui/PassiveCraftingHatchScreen.java @@ -0,0 +1,771 @@ +package com.sorrowmist.useless.client.gui; + +import com.sorrowmist.useless.client.render.PatternSlotRenderer; +import com.sorrowmist.useless.content.blockentities.multiblock.PassiveCraftingHatchBlockEntity; +import com.sorrowmist.useless.content.menus.PassiveCraftingHatchMenu; +import com.sorrowmist.useless.content.menus.PagedRecoverableMenu; +import com.sorrowmist.useless.network.PassiveCraftingSettingsPacket; +import com.sorrowmist.useless.network.PassiveCraftingSlotMultiplierPacket; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.network.chat.Component; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.glfw.GLFW; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; + +/** + * Passive hatch screen: a paginated pattern inventory, the global interval/global multiplier + * controls, and per-slot batch sizes. + * + *

The right-hand multiplier field is context sensitive. With no slot selected it edits the + * global default exactly as before; after ctrl-clicking a pattern slot it edits that slot's own + * override instead. Ctrl-clicking the same slot again clears the selection. The scroll wheel + * adjusts whatever active slot is under the cursor, and a compact badge inside every slot shows + * the value it will actually run at (amber while pinned, grey while following the global value). + */ +public final class PassiveCraftingHatchScreen + extends AbstractContainerScreen { + /** Multiplier edits are optimistic on the client until the next server status snapshot. */ + private static final int PENDING_TICKS = 40; + /** + * Above {@code PatternSlotRenderer}'s 300 so a pattern's own display amount cannot cover it. + */ + private static final float BADGE_Z = 400.0F; + private static final int MULTIPLIER_CUSTOM_COLOR = 0xFFFFC65C; + private static final int MULTIPLIER_INHERITED_COLOR = 0xFFFFFFFF; + private static final int SELECTION_COLOR = 0xFFCC7A00; + /** Widest text the right-hand readout can hold without spilling out of its inset. */ + private static final int READOUT_WIDTH = 70; + private static final int READOUT_X = 174; + private static final int READOUT_TOP = 151; + private static final int READOUT_ROW_HEIGHT = 12; + + private EditBox intervalField; + private EditBox multiplierField; + private PressableAE2Button intervalDown; + private PressableAE2Button intervalUp; + private PressableAE2Button multiplierDown; + private PressableAE2Button multiplierUp; + private PressableAE2Button applyButton; + private PressableAE2Button applySlotGlobalButton; + private PressableAE2Button applyAllButton; + private PressableAE2Button previousPageButton; + private PressableAE2Button nextPageButton; + private int lastSyncedInterval = Integer.MIN_VALUE; + private long lastSyncedMultiplier = Long.MIN_VALUE; + private String syncedMultiplierText = ""; + private boolean updatingMultiplierField; + private boolean multiplierDirty; + private boolean multiplierFieldWasFocused; + + /** Slot currently under the cursor with an active pattern slot, or {@code null}. */ + @Nullable + private Slot multiplierHoverSlot; + private int multiplierHoverPatternSlot = -1; + /** Ctrl-clicked slot the multiplier field is bound to; {@code -1} means "the global default". */ + private int selectedPatternSlot = -1; + private int lastObservedPage = -1; + /** Pattern slot -> {pending value, remaining ticks}; a local echo of an in-flight edit. */ + private final Map pendingSlotMultipliers = new HashMap<>(); + private int pendingGlobalResetTicks; + + public PassiveCraftingHatchScreen( + PassiveCraftingHatchMenu menu, Inventory inventory, Component title) { + super(menu, inventory, title); + imageWidth = 250; + imageHeight = 336; + inventoryLabelX = 44; + inventoryLabelY = 242; + titleLabelX = 8; + titleLabelY = 7; + } + + @Override + protected void init() { + super.init(); + previousPageButton = addRenderableWidget(new PressableAE2Button( + leftPos + 128, topPos + 4, 18, 12, + Component.literal("<"), button -> page(PagedRecoverableMenu.PREVIOUS_PAGE))); + nextPageButton = addRenderableWidget(new PressableAE2Button( + leftPos + 148, topPos + 4, 18, 12, + Component.literal(">"), button -> page(PagedRecoverableMenu.NEXT_PAGE))); + + intervalField = numericField(177, 34, 62, + Component.translatable("gui.useless_mod.passive_crafting.interval")); + multiplierField = scaledAmountField(177, 78, 62, + Component.translatable("gui.useless_mod.passive_crafting.multiplier")); + addRenderableWidget(intervalField); + addRenderableWidget(multiplierField); + + intervalDown = addRenderableWidget(new PressableAE2Button( + leftPos + 177, topPos + 51, 28, 14, + Component.literal("-"), button -> adjustInterval(-20))); + intervalUp = addRenderableWidget(new PressableAE2Button( + leftPos + 211, topPos + 51, 28, 14, + Component.literal("+"), button -> adjustInterval(20))); + multiplierDown = addRenderableWidget(new PressableAE2Button( + leftPos + 177, topPos + 95, 28, 14, + Component.literal("-"), button -> adjustMultiplier(-1))); + multiplierUp = addRenderableWidget(new PressableAE2Button( + leftPos + 211, topPos + 95, 28, 14, + Component.literal("+"), button -> adjustMultiplier(1))); + applyButton = addRenderableWidget(new PressableAE2Button( + leftPos + 177, topPos + 116, 62, 18, + Component.translatable("gui.useless_mod.passive_crafting.apply"), + button -> sendSettings())); + applySlotGlobalButton = addRenderableWidget(new PressableAE2Button( + leftPos + 175, topPos + 186, 68, 16, + Component.translatable("gui.useless_mod.passive_crafting.apply_slot"), + button -> clearSlotMultiplier(selectedPatternSlot))); + applyAllButton = addRenderableWidget(new PressableAE2Button( + leftPos + 175, topPos + 204, 68, 16, + Component.translatable("gui.useless_mod.passive_crafting.apply_all"), + button -> clearSlotMultipliers())); + syncFields(true); + updatePageControls(); + } + + private EditBox numericField(int x, int y, int width, Component narration) { + EditBox field = new EditBox(font, leftPos + x, topPos + y, width, 14, narration); + field.setMaxLength(19); + field.setFilter(value -> value.isEmpty() || value.chars().allMatch(Character::isDigit)); + return field; + } + + private EditBox scaledAmountField(int x, int y, int width, Component narration) { + EditBox field = new EditBox(font, leftPos + x, topPos + y, width, 14, narration); + field.setMaxLength(24); + field.setFilter(ScaledEnergyAmount::isValidInput); + field.setResponder(value -> { + if (!updatingMultiplierField) { + multiplierDirty = !value.equals(syncedMultiplierText); + } + }); + return field; + } + + @Override + protected void containerTick() { + super.containerTick(); + if (!pendingSlotMultipliers.isEmpty()) { + pendingSlotMultipliers.entrySet().removeIf(entry -> --entry.getValue()[1] <= 0); + } + if (pendingGlobalResetTicks > 0) { + pendingGlobalResetTicks--; + } + // The selection is an absolute slot index, but its value only arrives for the visible page. + int page = menu.getPage(); + if (page != lastObservedPage) { + lastObservedPage = page; + clearSelection(); + } else if (!menu.isFormed() && selectedPatternSlot >= 0) { + clearSelection(); + } + boolean multiplierFocused = multiplierField.isFocused(); + if (multiplierFieldWasFocused && !multiplierFocused && multiplierDirty) { + sendSettings(); + } + multiplierFieldWasFocused = multiplierFocused; + syncFields(false); + boolean editable = menu.isFormed(); + intervalField.setEditable(editable); + multiplierField.setEditable(editable); + intervalDown.active = editable; + intervalUp.active = editable; + multiplierDown.active = editable; + multiplierUp.active = editable; + applyButton.active = editable; + // Only meaningful while a slot is bound to the multiplier field. + applySlotGlobalButton.visible = selectedPatternSlot >= 0; + applySlotGlobalButton.active = editable; + applyAllButton.active = editable; + updatePageControls(); + } + + private void clearSelection() { + if (selectedPatternSlot < 0) { + return; + } + selectedPatternSlot = -1; + syncFields(true); + } + + private void updatePageControls() { + int pageCount = menu.getPageCount(); + previousPageButton.visible = pageCount > 1; + previousPageButton.active = menu.getPage() > 0; + nextPageButton.visible = pageCount > 1; + nextPageButton.active = menu.getPage() < pageCount - 1; + } + + private void page(int id) { + if (minecraft != null && minecraft.gameMode != null) { + minecraft.gameMode.handleInventoryButtonClick(menu.containerId, id); + } + } + + /** Current value of the field's target: the selected slot's override, or the global default. */ + private long fieldTargetMultiplier() { + if (selectedPatternSlot >= 0) { + return Math.max(1L, displayedSlotMultiplier(selectedPatternSlot)); + } + return Math.max(1L, menu.getMultiplier()); + } + + private void syncFields(boolean force) { + int interval = Math.max(PassiveCraftingHatchBlockEntity.MIN_INTERVAL_TICKS, + menu.getIntervalTicks()); + long multiplier = fieldTargetMultiplier(); + if ((force || !intervalField.isFocused()) && interval != lastSyncedInterval) { + intervalField.setValue(Integer.toString(interval)); + lastSyncedInterval = interval; + } + if ((force || !multiplierField.isFocused()) && multiplier != lastSyncedMultiplier) { + setMultiplierFieldValue(multiplier); + lastSyncedMultiplier = multiplier; + } + } + + private void adjustInterval(int delta) { + long current = readNumber(intervalField, menu.getIntervalTicks()); + int value = (int) com.sorrowmist.useless.compat.forge1201.LongMath.clamp(current + delta, + PassiveCraftingHatchBlockEntity.MIN_INTERVAL_TICKS, + PassiveCraftingHatchBlockEntity.MAX_INTERVAL_TICKS); + intervalField.setValue(Integer.toString(value)); + sendSettings(); + } + + private void adjustMultiplier(int delta) { + long current = ScaledEnergyAmount.parse(multiplierField.getValue(), menu.getMaxMultiplier()) + .orElse(fieldTargetMultiplier()); + long adjusted = delta > 0 + ? current == Long.MAX_VALUE ? Long.MAX_VALUE : current + 1L + : current <= 1L ? 1L : current - 1L; + long value = Math.max(1L, Math.min(menu.getMaxMultiplier(), adjusted)); + setMultiplierFieldValue(value); + sendSettings(); + } + + /** + * Applies the right panel. The interval is always global; the multiplier goes to the selected + * slot when one is selected, and to the global default otherwise. + */ + private void sendSettings() { + if (!menu.isFormed()) { + syncFields(true); + return; + } + int interval = (int) com.sorrowmist.useless.compat.forge1201.LongMath.clamp(readNumber(intervalField, menu.getIntervalTicks()), + (long) PassiveCraftingHatchBlockEntity.MIN_INTERVAL_TICKS, + (long) PassiveCraftingHatchBlockEntity.MAX_INTERVAL_TICKS); + long maximum = menu.getMaxMultiplier(); + long multiplier = ScaledEnergyAmount.parse(multiplierField.getValue(), maximum) + .orElse(fieldTargetMultiplier()); + multiplier = com.sorrowmist.useless.compat.forge1201.LongMath.clamp(multiplier, 1L, maximum); + intervalField.setValue(Integer.toString(interval)); + setMultiplierFieldValue(multiplier); + lastSyncedInterval = interval; + lastSyncedMultiplier = multiplier; + PacketDistributor.sendToServer(new PassiveCraftingSettingsPacket( + menu.containerId, menu.getBlockPos(), interval, + selectedPatternSlot >= 0 ? Math.max(1L, menu.getMultiplier()) : multiplier)); + if (selectedPatternSlot >= 0) { + requestSlotMultiplier(selectedPatternSlot, multiplier); + } + } + + private void setMultiplierFieldValue(long multiplier) { + syncedMultiplierText = ScaledEnergyAmount.format(multiplier); + updatingMultiplierField = true; + multiplierField.setValue(syncedMultiplierText); + updatingMultiplierField = false; + multiplierDirty = false; + } + + private static long readNumber(EditBox field, long fallback) { + try { + return Long.parseLong(field.getValue()); + } catch (NumberFormatException exception) { + return Math.max(1, fallback); + } + } + + // ------------------------------------------------------------------ + // Per-slot multiplier editing + // ------------------------------------------------------------------ + + /** Ctrl-click toggles which slot the right-hand multiplier field edits. */ + private void toggleSlotSelection(int patternSlot) { + if (patternSlot < 0 || !menu.isFormed()) { + return; + } + selectedPatternSlot = selectedPatternSlot == patternSlot ? -1 : patternSlot; + syncFields(true); + } + + /** Sends one slot's new batch size and echoes it locally until the next status snapshot. */ + private void requestSlotMultiplier(int patternSlot, long value) { + if (!menu.isFormed() || patternSlot < 0) { + return; + } + long clamped = com.sorrowmist.useless.compat.forge1201.LongMath.clamp(value, 1L, menu.getMaxMultiplier()); + pendingSlotMultipliers.put(patternSlot, new long[]{clamped, PENDING_TICKS}); + PacketDistributor.sendToServer(new PassiveCraftingSlotMultiplierPacket( + menu.containerId, menu.getBlockPos(), patternSlot, clamped)); + } + + /** Scroll-wheel stepper for the slot under the cursor. */ + private void adjustHoveredSlotMultiplier(int direction, boolean doubleStep) { + int patternSlot = multiplierHoverPatternSlot; + if (patternSlot < 0) { + return; + } + long maximum = menu.getMaxMultiplier(); + long current = displayedSlotMultiplier(patternSlot); + long next; + if (doubleStep) { + next = direction > 0 + ? current >= maximum ? maximum : Math.min(maximum, current * 2L) + : Math.max(1L, current / 2L); + } else { + next = direction > 0 + ? current >= maximum ? maximum : current + 1L + : current <= 1L ? 1L : current - 1L; + } + long value = com.sorrowmist.useless.compat.forge1201.LongMath.clamp(next, 1L, maximum); + if (value != current) { + requestSlotMultiplier(patternSlot, value); + } + } + + private void clearSlotMultipliers() { + if (!menu.isFormed()) { + return; + } + pendingSlotMultipliers.clear(); + pendingGlobalResetTicks = PENDING_TICKS; + PacketDistributor.sendToServer(new PassiveCraftingSlotMultiplierPacket( + menu.containerId, menu.getBlockPos(), -1, 0L)); + } + + /** Drops one slot's override so it follows the global multiplier again. */ + private void clearSlotMultiplier(int patternSlot) { + if (!menu.isFormed() || patternSlot < 0) { + return; + } + // A pending value of 0 reads as "back to the global default" until the snapshot catches up. + pendingSlotMultipliers.put(patternSlot, new long[]{0L, PENDING_TICKS}); + PacketDistributor.sendToServer(new PassiveCraftingSlotMultiplierPacket( + menu.containerId, menu.getBlockPos(), patternSlot, 0L)); + } + + /** Value shown for a slot: the local echo of an in-flight edit wins over the snapshot. */ + private long displayedSlotMultiplier(int patternSlot) { + long[] pending = pendingSlotMultipliers.get(patternSlot); + if (pending != null && pending[1] > 0) { + return pending[0] > 0L ? pending[0] : Math.max(1L, menu.getMultiplier()); + } + if (pendingGlobalResetTicks > 0) { + return Math.max(1L, menu.getMultiplier()); + } + return menu.getSlotMultiplier(patternSlot); + } + + private boolean displayedSlotMultiplierIsCustom(int patternSlot) { + long[] pending = pendingSlotMultipliers.get(patternSlot); + if (pending != null && pending[1] > 0) { + return pending[0] > 0L; + } + return pendingGlobalResetTicks <= 0 && menu.hasOwnSlotMultiplier(patternSlot); + } + + /** Slot the right-hand readout describes: the selection wins over the hover preview. */ + private int readoutPatternSlot() { + return selectedPatternSlot >= 0 ? selectedPatternSlot : multiplierHoverPatternSlot; + } + + /** Recomputes which active pattern slot the cursor is over. */ + private void updateMultiplierHover(double mouseX, double mouseY) { + multiplierHoverSlot = null; + multiplierHoverPatternSlot = -1; + if (!menu.isFormed()) { + return; + } + Slot slot = slotAt(mouseX, mouseY); + if (slot == null) { + return; + } + int patternSlot = menu.getPatternSlotIndex(slot); + if (patternSlot < 0 || patternSlot >= menu.getActivePatternSlots()) { + return; + } + multiplierHoverSlot = slot; + multiplierHoverPatternSlot = patternSlot; + } + + @Nullable + private Slot slotAt(double mouseX, double mouseY) { + for (Slot slot : menu.slots) { + if (!slot.isActive()) { + continue; + } + int x = leftPos + slot.x; + int y = topPos + slot.y; + if (mouseX >= x && mouseX < x + 16 && mouseY >= y && mouseY < y + 16) { + return slot; + } + } + return null; + } + + private Component slotMultiplierTooltip(int patternSlot) { + long value = displayedSlotMultiplier(patternSlot); + if (displayedSlotMultiplierIsCustom(patternSlot)) { + return Component.translatable("gui.useless_mod.passive_crafting.slot_multiplier_custom", + ScaledEnergyAmount.format(value), ScaledEnergyAmount.format(menu.getMultiplier())); + } + return Component.translatable("gui.useless_mod.passive_crafting.slot_multiplier_global", + ScaledEnergyAmount.format(menu.getMultiplier())); + } + + // ------------------------------------------------------------------ + // Input + // ------------------------------------------------------------------ + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + updateMultiplierHover(mouseX, mouseY); + if (button == 0 && hasControlDown() && multiplierHoverPatternSlot >= 0) { + toggleSlotSelection(multiplierHoverPatternSlot); + return true; + } + return super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double scrollY) { + if (scrollY != 0.0D) { + updateMultiplierHover(mouseX, mouseY); + if (multiplierHoverPatternSlot >= 0) { + adjustHoveredSlotMultiplier(scrollY > 0.0D ? 1 : -1, hasShiftDown()); + return true; + } + } + return super.mouseScrolled(mouseX, mouseY, scrollY); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if ((keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) + && (intervalField.isFocused() || multiplierField.isFocused())) { + sendSettings(); + setFocused(null); + return true; + } + if (keyCode != GLFW.GLFW_KEY_ESCAPE + && (consumesTextInputKey(intervalField, keyCode, scanCode, modifiers) + || consumesTextInputKey(multiplierField, keyCode, scanCode, modifiers))) { + return true; + } + return super.keyPressed(keyCode, scanCode, modifiers); + } + + private static boolean consumesTextInputKey(EditBox field, int keyCode, int scanCode, int modifiers) { + return field.keyPressed(keyCode, scanCode, modifiers) || field.canConsumeInput(); + } + + @Override + public void onClose() { + if (multiplierDirty) { + sendSettings(); + } + super.onClose(); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + previousPageButton.releaseVisualState(); + nextPageButton.releaseVisualState(); + intervalDown.releaseVisualState(); + intervalUp.releaseVisualState(); + multiplierDown.releaseVisualState(); + multiplierUp.releaseVisualState(); + applyButton.releaseVisualState(); + applySlotGlobalButton.releaseVisualState(); + applyAllButton.releaseVisualState(); + return super.mouseReleased(mouseX, mouseY, button); + } + + // ------------------------------------------------------------------ + // Rendering + // ------------------------------------------------------------------ + + @Override + protected void renderBg(GuiGraphics graphics, float partialTick, int mouseX, int mouseY) { + MachineScreenStyle.drawPanel(graphics, leftPos, topPos, imageWidth, imageHeight); + MachineScreenStyle.drawInset(graphics, + leftPos + 172, topPos + 18, leftPos + 246, topPos + 142); + MachineScreenStyle.drawInset(graphics, + leftPos + 172, topPos + 146, leftPos + 246, topPos + 226); + MachineScreenStyle.drawSlotGroup(graphics, leftPos, topPos, 8, 22, 9, 10); + MachineScreenStyle.drawSlotGroup(graphics, leftPos, topPos, 44, 254, 9, 3); + MachineScreenStyle.drawSlotGroup(graphics, leftPos, topPos, 44, 312, 9, 1); + + for (Slot slot : menu.slots) { + MachineScreenStyle.drawSlotBackground(graphics, leftPos, topPos, slot); + } + } + + @Override + protected void renderSlot(GuiGraphics graphics, Slot slot) { + int patternSlot = menu.getPatternSlotIndex(slot); + if (patternSlot < 0 || !PatternSlotRenderer.renderPattern( + graphics, font, slot.getItem(), slot.x, slot.y, + slot.x + slot.y * imageWidth, minecraft == null ? null : minecraft.level)) { + super.renderSlot(graphics, slot); + } + if (patternSlot < 0) return; + + var status = menu.getSlotStatus(patternSlot); + boolean active = patternSlot < menu.getActivePatternSlots(); + if (active) { + renderSlotMultiplier(graphics, slot, patternSlot); + // Drawn after the badge so a running slot keeps its progress underline readable. + renderSlotProgress(graphics, slot, status); + if (patternSlot == selectedPatternSlot) { + renderSelectionFrame(graphics, slot); + } + } else { + renderSlotProgress(graphics, slot, status); + graphics.fill(slot.x, slot.y, slot.x + 16, slot.y + 16, 0x66000000); + graphics.fill(slot.x + 11, slot.y + 2, slot.x + 15, slot.y + 7, 0xFFF2F2F2); + graphics.fill(slot.x + 12, slot.y + 1, slot.x + 14, slot.y + 3, 0xFFF2F2F2); + } + } + + private void renderSlotProgress(GuiGraphics graphics, Slot slot, + PassiveCraftingHatchBlockEntity.SlotStatus status) { + if (status.state() == PassiveCraftingHatchBlockEntity.SlotState.EMPTY) { + return; + } + int width = status.maxProgress() <= 0 ? 16 + : Mth.clamp((int) ((long) status.progress() * 16L / status.maxProgress()), 0, 16); + if (width > 0) { + graphics.fill(slot.x, slot.y + 14, slot.x + width, slot.y + 16, statusColor(status.state())); + } + } + + /** + * Draws the batch-size badge of one slot. Slots that merely follow the global multiplier stay + * quiet when empty, so a configured slot is recognisable at a glance. + * + *

The badge has no backdrop and is pushed above the pattern render. A pattern slot shows its + * own display amount in the bottom-right corner (drawn at z 300 by {@code PatternSlotRenderer}), + * so the multiplier sits in the top-right corner at z 400: transparent, floating over the icon, + * and with a drop shadow for contrast against bright item sprites. + */ + private void renderSlotMultiplier(GuiGraphics graphics, Slot slot, int patternSlot) { + boolean configured = displayedSlotMultiplierIsCustom(patternSlot); + if (slot.getItem().isEmpty() && !configured) { + return; + } + String text = ScaledEnergyAmount.format(displayedSlotMultiplier(patternSlot)); + if (text.isEmpty()) { + return; + } + float scale = Math.min(0.75F, 14.0F / Math.max(1, font.width(text))); + int textWidth = Math.max(1, Math.round(font.width(text) * scale)); + var pose = graphics.pose(); + pose.pushPose(); + pose.translate(0.0F, 0.0F, BADGE_Z); + pose.translate(slot.x + 16 - textWidth - 1, slot.y + 1, 0.0F); + pose.scale(scale, scale, 1.0F); + graphics.drawString(font, text, 0, 0, + configured ? MULTIPLIER_CUSTOM_COLOR : MULTIPLIER_INHERITED_COLOR, true); + pose.popPose(); + } + + /** One-pixel frame around the ctrl-selected slot, drawn over the badge. */ + private void renderSelectionFrame(GuiGraphics graphics, Slot slot) { + int left = slot.x - 1; + int top = slot.y - 1; + int right = slot.x + 17; + int bottom = slot.y + 17; + graphics.fill(left, top, right, top + 1, SELECTION_COLOR); + graphics.fill(left, bottom - 1, right, bottom, SELECTION_COLOR); + graphics.fill(left, top, left + 1, bottom, SELECTION_COLOR); + graphics.fill(right - 1, top, right, bottom, SELECTION_COLOR); + } + + @Override + protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) { + graphics.drawString(font, title, titleLabelX, titleLabelY, + MachineScreenStyle.TEXT_COLOR, false); + String page = (menu.getPage() + 1) + "/" + menu.getPageCount(); + graphics.drawString(font, page, 126 - font.width(page), 6, + menu.isRecoveryPage() ? MachineScreenStyle.ERROR_TEXT_COLOR : MachineScreenStyle.MUTED_TEXT_COLOR, + false); + graphics.drawString(font, playerInventoryTitle, + inventoryLabelX, inventoryLabelY, MachineScreenStyle.TEXT_COLOR, false); + graphics.drawString(font, + Component.translatable("gui.useless_mod.passive_crafting.interval"), + 177, 22, MachineScreenStyle.TEXT_COLOR, false); + graphics.drawString(font, + Component.translatable("gui.useless_mod.passive_crafting.multiplier"), + 177, 66, MachineScreenStyle.TEXT_COLOR, false); + graphics.drawString(font, + Component.translatable("gui.useless_mod.passive_crafting.max_multiplier", + ScaledEnergyAmount.format(menu.getMaxMultiplier())), + 8, 204, MachineScreenStyle.MUTED_TEXT_COLOR, false); + graphics.drawString(font, + Component.translatable("gui.useless_mod.passive_crafting.countdown", + menu.getCountdownTicks()), + 8, 215, MachineScreenStyle.MUTED_TEXT_COLOR, false); + + renderSlotMultiplierReadout(graphics); + + int patternSlot = menu.getPatternSlotIndex(hoveredSlot); + if (patternSlot >= 0) { + var status = menu.getSlotStatus(patternSlot); + Component statusText = statusComponent(status); + graphics.drawString(font, font.split(statusText, 156).get(0), + 8, 227, statusColor(status.state()), false); + if (status.maxProgress() > 0) { + graphics.drawString(font, + Component.translatable("gui.useless_mod.passive_crafting.progress", + status.progress(), status.maxProgress()), + 8, 239, MachineScreenStyle.MUTED_TEXT_COLOR, false); + } + } else { + Component state = Component.translatable(menu.isFormed() + ? "gui.useless_mod.passive_crafting.connected" + : "gui.useless_mod.passive_crafting.unformed"); + graphics.drawString(font, state, 8, 227, + menu.isFormed() ? 0xFF2E7D32 : 0xFFA66A00, false); + graphics.drawString(font, + Component.translatable("gui.useless_mod.passive_crafting.active_slots", + menu.getActivePatternSlots(), menu.getConfiguredPatternSlots()), + 8, 239, MachineScreenStyle.MUTED_TEXT_COLOR, false); + } + } + + /** + * Right-hand readout telling the player what the multiplier field currently edits. Every row + * goes through {@link Font#split} so a long translation wraps instead of spilling out of the + * inset. + */ + private void renderSlotMultiplierReadout(GuiGraphics graphics) { + int target = readoutPatternSlot(); + if (target < 0) { + drawReadoutRow(graphics, 0, + Component.translatable("gui.useless_mod.passive_crafting.slot_global_target"), + MachineScreenStyle.TEXT_COLOR); + drawReadoutRow(graphics, 1, + Component.translatable("gui.useless_mod.passive_crafting.slot_select_hint"), + MachineScreenStyle.MUTED_TEXT_COLOR); + drawReadoutRow(graphics, 2, + Component.translatable("gui.useless_mod.passive_crafting.slot_wheel_hint"), + MachineScreenStyle.MUTED_TEXT_COLOR); + return; + } + boolean selected = selectedPatternSlot >= 0; + boolean custom = displayedSlotMultiplierIsCustom(target); + drawReadoutRow(graphics, 0, + Component.translatable(selected + ? "gui.useless_mod.passive_crafting.slot_label_selected" + : "gui.useless_mod.passive_crafting.slot_label", target), + selected ? SELECTION_COLOR : MachineScreenStyle.TEXT_COLOR); + drawReadoutRow(graphics, 1, + Component.translatable("gui.useless_mod.passive_crafting.slot_multiplier_value", + ScaledEnergyAmount.format(displayedSlotMultiplier(target))), + MachineScreenStyle.TEXT_COLOR); + drawReadoutRow(graphics, 2, + custom + ? Component.translatable("gui.useless_mod.passive_crafting.slot_multiplier_state_custom") + : Component.translatable("gui.useless_mod.passive_crafting.slot_multiplier_state_global", + ScaledEnergyAmount.format(menu.getMultiplier())), + custom ? MULTIPLIER_CUSTOM_COLOR : MachineScreenStyle.MUTED_TEXT_COLOR); + } + + private void drawReadoutRow(GuiGraphics graphics, int row, Component text, int color) { + var lines = font.split(text, READOUT_WIDTH); + if (lines.isEmpty()) { + return; + } + graphics.drawString(font, lines.get(0), READOUT_X, + READOUT_TOP + row * READOUT_ROW_HEIGHT, color, false); + } + + @Override + protected List getTooltipFromContainerItem(ItemStack stack) { + List tooltip = super.getTooltipFromContainerItem(stack); + if (multiplierHoverPatternSlot < 0) { + return tooltip; + } + List extended = new ArrayList<>(tooltip); + if (multiplierHoverPatternSlot == selectedPatternSlot) { + extended.add(Component.translatable( + "gui.useless_mod.passive_crafting.slot_selected_hint")); + } + extended.add(slotMultiplierTooltip(multiplierHoverPatternSlot)); + extended.add(Component.translatable("gui.useless_mod.passive_crafting.slot_multiplier_edit_hint")); + return extended; + } + + private static Component statusComponent(PassiveCraftingHatchBlockEntity.SlotStatus status) { + Component base = Component.translatable("gui.useless_mod.passive_crafting.status." + + status.state().name().toLowerCase(Locale.ROOT)); + return status.detail().isEmpty() ? base : Component.translatable( + "gui.useless_mod.passive_crafting.status_detail", base, status.detail()); + } + + private static int statusColor(PassiveCraftingHatchBlockEntity.SlotState state) { + return switch (state) { + case EMPTY -> MachineScreenStyle.MUTED_TEXT_COLOR; + case READY -> 0xFF517497; + case RUNNING -> 0xFF2E7D32; + case PAUSED -> 0xFFA66A00; + case WAITING_OUTPUT -> 0xFF7B4EA3; + case MISSING_INPUT, MISSING_MOLD, AE_OFFLINE, INVALID_PATTERN -> + MachineScreenStyle.ERROR_TEXT_COLOR; + }; + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + updateMultiplierHover(mouseX, mouseY); + super.render(graphics, mouseX, mouseY, partialTick); + renderTooltip(graphics, mouseX, mouseY); + int patternSlot = menu.getPatternSlotIndex(hoveredSlot); + if (hoveredSlot != null && patternSlot >= 0 && hoveredSlot.getItem().isEmpty()) { + var status = menu.getSlotStatus(patternSlot); + List lines = new ArrayList<>(); + lines.add(patternSlot >= menu.getActivePatternSlots() + ? Component.translatable("gui.useless_mod.passive_crafting.locked_slot") + : statusComponent(status)); + if (patternSlot < menu.getActivePatternSlots()) { + if (patternSlot == selectedPatternSlot) { + lines.add(Component.translatable( + "gui.useless_mod.passive_crafting.slot_selected_hint")); + } + lines.add(slotMultiplierTooltip(patternSlot)); + lines.add(Component.translatable( + "gui.useless_mod.passive_crafting.slot_multiplier_edit_hint")); + } + graphics.renderTooltip(font, lines, Optional.empty(), mouseX, mouseY); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/gui/PatternAssemblyScreen.java b/src/main/java/com/sorrowmist/useless/client/gui/PatternAssemblyScreen.java new file mode 100644 index 00000000..21bb4bfc --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/gui/PatternAssemblyScreen.java @@ -0,0 +1,42 @@ +package com.sorrowmist.useless.client.gui; + +import com.sorrowmist.useless.client.render.PatternSlotRenderer; +import com.sorrowmist.useless.content.menus.MePatternAssemblyMenu; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.Slot; + +/** + * ME 样板总成的容器界面。 + * + *

在分页界面基础上接入样板搜索:查询按所选范围匹配样板自身的输入、输出或模具显示名, + * 命中项在原槽位上高亮标记。只读恢复页内容不进入搜索结果,避免玩家在恢复页里看到无法回填的条目。

+ */ +public final class PatternAssemblyScreen extends PagedRecoverableScreen { + public PatternAssemblyScreen(MePatternAssemblyMenu menu, Inventory inventory, Component title) { + super(menu, inventory, title); + } + + @Override + protected boolean supportsSearchMode() { + return true; + } + + @Override + protected boolean matchesSearch(int menuSlot) { + Slot slot = menu.getSlot(menuSlot); + return PatternSearchMatcher.matchesPattern(slot.getItem(), normalizedQuery(), searchMode(), + minecraft == null ? null : minecraft.level); + } + + @Override + protected void renderSlot(GuiGraphics graphics, Slot slot) { + if (slot.index < MePatternAssemblyMenu.SLOTS_PER_PAGE + && PatternSlotRenderer.renderPattern(graphics, font, slot.getItem(), slot.x, slot.y, + slot.x + slot.y * imageWidth, minecraft == null ? null : minecraft.level)) { + return; + } + super.renderSlot(graphics, slot); + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/gui/PatternConverterScreen.java b/src/main/java/com/sorrowmist/useless/client/gui/PatternConverterScreen.java new file mode 100644 index 00000000..3a3fecf9 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/gui/PatternConverterScreen.java @@ -0,0 +1,87 @@ +package com.sorrowmist.useless.client.gui; + +import com.sorrowmist.useless.content.menus.ContainerPatternConverter; +import com.sorrowmist.useless.inventory.slot.PatternSlotItemHandler; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.Slot; + +/** Inventory screen for loading patterns and starting a batch conversion. */ +public final class PatternConverterScreen extends net.minecraft.client.gui.screens.inventory.AbstractContainerScreen { + private static final int CONVERT_BUTTON_WIDTH = 54; + private PressableAE2Button convertButton; + + public PatternConverterScreen(ContainerPatternConverter menu, Inventory inventory, Component title) { + super(menu, inventory, title); + imageWidth = 176; + imageHeight = 178; + inventoryLabelY = 85; + } + + @Override + protected void init() { + super.init(); + convertButton = addRenderableWidget(new PressableAE2Button( + leftPos + imageWidth - CONVERT_BUTTON_WIDTH - 6, + topPos + 4, + CONVERT_BUTTON_WIDTH, + 20, + Component.translatable("gui.useless_mod.pattern_converter.convert"), + button -> { + if (minecraft != null && minecraft.gameMode != null) { + minecraft.gameMode.handleInventoryButtonClick( + menu.containerId, ContainerPatternConverter.CONVERT_BUTTON); + } + })); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + convertButton.releaseVisualState(); + return super.mouseReleased(mouseX, mouseY, button); + } + + @Override + protected void renderBg(GuiGraphics graphics, float partialTick, int mouseX, int mouseY) { + MachineScreenStyle.drawPanel(graphics, leftPos, topPos, imageWidth, imageHeight); + MachineScreenStyle.drawSlotGroup(graphics, leftPos, topPos, 8, 30, 9, 3); + MachineScreenStyle.drawSlotGroup(graphics, leftPos, topPos, 8, 97, 9, 3); + MachineScreenStyle.drawSlotGroup(graphics, leftPos, topPos, 8, 155, 9, 1); + for (var slot : menu.slots) { + MachineScreenStyle.drawSlotBackground(graphics, leftPos, topPos, slot); + } + } + + @Override + protected void renderSlot(GuiGraphics graphics, Slot slot) { + if (slot instanceof PatternSlotItemHandler patternSlot) { + var icon = patternSlot.getIcon(); + if ((patternSlot.renderIconWithItem() || patternSlot.getItem().isEmpty()) + && patternSlot.isSlotEnabled() && icon != null) { + icon.getBlitter() + .dest(patternSlot.x, patternSlot.y) + .opacity(patternSlot.getOpacityOfIcon()) + .blit(graphics); + } + } + super.renderSlot(graphics, slot); + } + + @Override + protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) { + String titleText = title.getString(); + if (font.width(titleText) > 88) { + titleText = font.plainSubstrByWidth(titleText, 85) + "..."; + } + graphics.drawString(font, titleText, 8, 6, MachineScreenStyle.TEXT_COLOR, false); + graphics.drawString(font, playerInventoryTitle, 8, inventoryLabelY, + MachineScreenStyle.MUTED_TEXT_COLOR, false); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + super.render(graphics, mouseX, mouseY, partialTick); + renderTooltip(graphics, mouseX, mouseY); + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/gui/PatternSearchMatcher.java b/src/main/java/com/sorrowmist/useless/client/gui/PatternSearchMatcher.java new file mode 100644 index 00000000..488acf03 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/gui/PatternSearchMatcher.java @@ -0,0 +1,214 @@ +package com.sorrowmist.useless.client.gui; + +import appeng.api.crafting.PatternDetailsHelper; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import appeng.crafting.pattern.AEProcessingPattern; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeCatalog; +import net.minecraft.client.Minecraft; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.WeakHashMap; + +/** + * 样板与模具的搜索匹配工具。 + * + *

匹配在客户端执行:服务端只同步当前页的槽位内容,因此搜索必须自行遍历当前页并筛出 + * 命中项。样板的输入与输出分开索引,由 {@link PatternSearchMode} 决定本次查询作用于哪一侧; + * 模具没有编码内容,按其自身物品名与配方登记的可接受物品名匹配。

+ */ +final class PatternSearchMatcher { + /** 样板缓存键按输入、输出、模具三侧分列,避免不同搜索范围互相污染结果。 */ + private static final Map PATTERN_CACHE = new WeakHashMap<>(); + private static final Map MOLD_CACHE = new WeakHashMap<>(); + + private PatternSearchMatcher() { + } + + /** 将查询串规范化为小写,空白查询视为“未过滤”。 */ + static String normalize(String query) { + return query == null ? "" : query.trim().toLowerCase(Locale.ROOT); + } + + /** + * 判断单个样板在指定搜索范围下是否命中查询。 + * + *

空查询命中全部非空槽位。样板的输入与输出分别匹配,只有所选范围包含该侧且该侧文本 + * 含查询串时才算命中;解码不可用时退化为物品自身显示名,避免一次解码失败使样板无法被搜索。

+ */ + static boolean matchesPattern(ItemStack pattern, String normalizedQuery, PatternSearchMode mode, + @Nullable Level level) { + if (pattern.isEmpty()) return false; + if (normalizedQuery.isEmpty()) return true; + PatternSearchText text = patternSearchText(pattern, level); + return mode.matchesOutput() && text.outputs().contains(normalizedQuery) + || mode.matchesInput() && text.inputs().contains(normalizedQuery) + || mode.matchesMold() && text.molds().contains(normalizedQuery); + } + + /** + * 判断单个模具是否命中查询。 + * + *

模具没有编码内容,可搜索文本由物品显示名与配方登记的可接受物品名共同构成, + * 因此按任意可接受物品名搜索都能命中对应模具。

+ */ + static boolean matchesMold(ItemStack mold, String normalizedQuery, @Nullable Level level) { + if (mold.isEmpty()) return false; + if (normalizedQuery.isEmpty()) return true; + return moldSearchText(mold, level).contains(normalizedQuery); + } + + private static PatternSearchText patternSearchText(ItemStack pattern, @Nullable Level level) { + PatternSearchText cached = PATTERN_CACHE.get(pattern); + if (cached != null) return cached; + PatternSearchText text = buildPatternSearchText(pattern, level); + // 以副本作为弱引用键,避免外部修改原栈后污染缓存条目。 + PATTERN_CACHE.put(pattern.copy(), text); + return text; + } + + private static String moldSearchText(ItemStack mold, @Nullable Level level) { + String cached = MOLD_CACHE.get(mold); + if (cached != null) return cached; + String text = buildMoldSearchText(mold, level); + MOLD_CACHE.put(mold.copy(), text); + return text; + } + + private static PatternSearchText buildPatternSearchText(ItemStack pattern, @Nullable Level level) { + StringBuilder inputs = new StringBuilder(); + StringBuilder outputs = new StringBuilder(); + StringBuilder molds = new StringBuilder(); + // 样板自身的物品名归入三侧,使按样板名搜索在任意范围下都能命中。 + appendItemName(inputs, pattern); + appendItemName(outputs, pattern); + appendItemName(molds, pattern); + // 样板设置的模具写入组件,直接读取即可,无需解码配方。 + appendPatternMolds(molds, pattern); + + AEItemKey key = AEItemKey.of(pattern); + if (key != null) { + // 处理样板自带编好的输入输出,无需走配方解码,避免为搜索触发动态配方兼容检查。 + if (com.sorrowmist.useless.compat.forge1201.recipe.NativeProcessingPatterns.hasPayload(pattern)) { + appendEncodedProcessingPattern(inputs, outputs, key); + } else { + appendDecodedPattern(inputs, outputs, pattern, level); + } + } + // 单一出口:无论解码走到哪一步,三侧文本都按当前累积内容构造, + // 物品名始终保留,因此各失败分支无需各自返回。 + return new PatternSearchText(inputs.toString(), outputs.toString(), molds.toString()); + } + + /** 从样板自带编码的输入输出中提取可搜索文本。 */ + private static void appendEncodedProcessingPattern(StringBuilder inputs, StringBuilder outputs, AEItemKey key) { + try { + AEProcessingPattern decoded = new AEProcessingPattern(key); + for (var input : decoded.getInputs()) { + appendPossibleInputs(inputs, input.getPossibleInputs()); + } + appendStacks(outputs, java.util.Arrays.asList(decoded.getOutputs())); + } catch (RuntimeException ignored) { + // 非法样板仅保留物品名匹配。 + } + } + + /** + * 从配方解码结果中提取可搜索文本。 + * + *

解码仅在样板已编码且客户端层级可用时进行;条件不满足或解码失败时保留物品名匹配, + * 不清空已有内容,也不向上抛出。

+ */ + private static void appendDecodedPattern(StringBuilder inputs, StringBuilder outputs, ItemStack pattern, @Nullable Level level) { + Level effectiveLevel = level == null ? clientLevel() : level; + if (effectiveLevel == null || !PatternDetailsHelper.isEncodedPattern(pattern)) return; + try { + var details = PatternDetailsHelper.decodePattern(pattern, effectiveLevel); + if (details == null) return; + for (var input : details.getInputs()) { + appendPossibleInputs(inputs, input.getPossibleInputs()); + } + appendStacks(outputs, java.util.Arrays.asList(details.getOutputs())); + } catch (RuntimeException ignored) { + // 解码失败时保留物品名匹配,不清空已有内容。 + } + } + + /** + * 将样板设置的模具显示名写入模具侧可搜索文本。 + * + *

样板所需的模具以组件形式随样板持久化,读取组件即可覆盖已编码样板;仅当组件缺失时 + * 才回退到配方解码得到的模具信息,避免搜索路径触发不必要的配方兼容检查。

+ */ + private static void appendPatternMolds(StringBuilder molds, ItemStack pattern) { + // 只使用公开的 ItemStack 重载:data 版本是包私有的,跨包调用无法编译。 + for (String term : OmniversalPatternMoldSearchTerms.searchTerms(pattern)) { + molds.append(term.toLowerCase(Locale.ROOT)).append('\n'); + } + } + + private static String buildMoldSearchText(ItemStack mold, @Nullable Level level) { + StringBuilder text = new StringBuilder(); + appendItemName(text, mold); + + Level effectiveLevel = level == null ? clientLevel() : level; + if (effectiveLevel == null) return text.toString(); + + // 模具按配方登记的 Ingredient 匹配,把命中配方的全部可接受物品名一并纳入, + // 使按同一模具的任何可接受物品名都能搜到该模具。 + for (AlloyFurnaceRecipeCatalog.Entry entry : AlloyFurnaceRecipeCatalog.entries(effectiveLevel)) { + var recipe = entry.recipe(); + if (recipe == null) continue; + for (Ingredient registered : recipe.molds()) { + if (registered == null || registered.isEmpty() || !registered.test(mold)) continue; + for (ItemStack accepted : registered.getItems()) { + appendItemName(text, accepted); + } + } + } + return text.toString(); + } + + private static void appendPossibleInputs(StringBuilder text, GenericStack[] possible) { + for (GenericStack stack : possible) { + if (stack != null) appendKey(text, stack.what()); + } + } + + private static void appendStacks(StringBuilder text, List stacks) { + for (GenericStack stack : stacks) { + if (stack != null) appendKey(text, stack.what()); + } + } + + private static void appendKey(StringBuilder text, @Nullable AEKey key) { + if (key == null) return; + key.getDisplayName().visit(content -> { + text.append(content.toLowerCase(Locale.ROOT)).append('\n'); + return Optional.empty(); + }); + } + + private static void appendItemName(StringBuilder text, ItemStack stack) { + if (stack.isEmpty()) return; + text.append(stack.getHoverName().getString().toLowerCase(Locale.ROOT)).append('\n'); + } + + @Nullable + private static Level clientLevel() { + Minecraft minecraft = Minecraft.getInstance(); + return minecraft.level; + } + + /** 单个样板的输入侧、输出侧与模具侧可搜索文本,均已小写化。 */ + private record PatternSearchText(String inputs, String outputs, String molds) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/gui/PatternSearchMode.java b/src/main/java/com/sorrowmist/useless/client/gui/PatternSearchMode.java new file mode 100644 index 00000000..9dd273f7 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/gui/PatternSearchMode.java @@ -0,0 +1,38 @@ +package com.sorrowmist.useless.client.gui; + +/** + * 样板搜索范围。 + * + *

样板同时包含输入侧、输出侧与模具槽,三侧语义互不相同,因此搜索需要显式区分范围, + * 否则「按产物找样板」与「按原料找样板」会互相干扰。模具集散中心的内容全部为模具, + * 复用 {@link #MOLD} 即可获得一致的范围语义。

+ * + *

枚举声明顺序即界面按钮的循环顺序,须与「输入 → 输出 → 模具」的查找次序一致; + * 调整声明顺序会同时改变按钮的循环路径、初始范围与默认提示文本。

+ */ +enum PatternSearchMode { + /** 只匹配样板输入。 */ + IN, + /** 只匹配样板输出。 */ + OUT, + /** 只匹配样板设置所需的模具。 */ + MOLD, + /** 同时匹配输入、输出与模具。 */ + ALL; + + boolean matchesInput() { + return this == IN || this == ALL; + } + + boolean matchesOutput() { + return this == OUT || this == ALL; + } + + boolean matchesMold() { + return this == MOLD || this == ALL; + } + + PatternSearchMode next() { + return values()[(ordinal() + 1) % values().length]; + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/gui/PinyinSearch.java b/src/main/java/com/sorrowmist/useless/client/gui/PinyinSearch.java new file mode 100644 index 00000000..4b31d14c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/gui/PinyinSearch.java @@ -0,0 +1,70 @@ +package com.sorrowmist.useless.client.gui; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.sorrowmist.useless.UselessMod; +import net.minecraftforge.fml.ModList; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Method; +import java.util.Locale; + +/** + * 容器列表的搜索匹配。 + * + *

装了 Just Enough Characters(`jecharacters`,JEI 的拼音搜索就是它做的)时借它的 + * {@code me.towdium.jecharacters.utils.Match#contains} 做匹配——这样「xiangzi」也能搜到「箱子」, + * 与 JEI 搜索行为一致。没装(或反射失败)时退回大小写不敏感的子串匹配。

+ * + *

走反射而不是直接引用,是因为它是运行时才可能出现的可选模组,编译期并不存在。

+ */ +final class PinyinSearch { + private static final String MOD_ID = "jecharacters"; + private static final String MATCH_CLASS = "me.towdium.jecharacters.utils.Match"; + + private static boolean resolved; + @Nullable + private static Method contains; + + private PinyinSearch() { + } + + static boolean matches(String text, String query) { + if (query.isEmpty()) { + return true; + } + if (text.isEmpty()) { + return false; + } + resolve(); + if (contains != null) { + try { + if (Boolean.TRUE.equals(contains.invoke(null, text, query))) { + return true; + } + } catch (ReflectiveOperationException | RuntimeException ignored) { + // 借不到就按普通匹配来,不影响功能。 + } + } + // 拼音没命中(或借不到)时仍走普通子串匹配:中文直接输入、英文、坐标都还能搜。 + return text.toLowerCase(Locale.ROOT).contains(query.toLowerCase(Locale.ROOT)); + } + + private static synchronized void resolve() { + if (resolved) { + return; + } + resolved = true; + if (!ModList.get().isLoaded(MOD_ID)) { + return; + } + try { + Class match = Class.forName(MATCH_CLASS, true, PinyinSearch.class.getClassLoader()); + contains = match.getMethod("contains", CharSequence.class, CharSequence.class); + } catch (ReflectiveOperationException | LinkageError exception) { + RateLimitedLogger.warn(UselessMod.LOGGER, + "Just Enough Characters is installed but its Match API is unavailable; " + + "falling back to plain substring search", exception); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/gui/PlatformPreview.java b/src/main/java/com/sorrowmist/useless/client/gui/PlatformPreview.java new file mode 100644 index 00000000..5986b335 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/gui/PlatformPreview.java @@ -0,0 +1,684 @@ +package com.sorrowmist.useless.client.gui; + +import com.mojang.blaze3d.platform.NativeImage; +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.blocks.IColoredBlock; +import com.sorrowmist.useless.world.dimension.DimensionGenerationConfig; +import com.sorrowmist.useless.world.dimension.PlatformLayout; +import com.sorrowmist.useless.world.dimension.PlatformStyle; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.renderer.block.BlockModelShaper; +import net.minecraft.client.renderer.block.model.BakedQuad; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.client.renderer.texture.SpriteContents; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.client.renderer.texture.TextureManager; +import net.minecraft.client.resources.model.BakedModel; +import net.minecraft.core.Direction; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.MapColor; +import net.minecraftforge.client.model.data.ModelData; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * 维度传送方块配置界面的生成预览。 + * + *

俯视图用 {@link PlatformLayout}(与世界生成器同一份算法)逐格采样,因此预览 + * 与实际生成结果不会脱节;侧视图显示层数、起始 Y 与基岩位置。 + * + *

两种渲染模式:一格方块能分到的屏幕像素足够(≥ {@link #MIN_TEXTURE_CELL})时 + * 绘制真实材质,否则自动降级为平均色块——采样周期很大时贴图会退化为噪点,此时色块的辨识度更高。 + * + *

为什么烘焙成贴图:{@code GuiGraphics} 的每次 blit 都会单独提交一次绘制, + * 逐格画材质在密集网格下会到上千次 draw call;所以配置变化时把整个预览烘焙进一张 + * {@link DynamicTexture},每帧只画一次。色块模式同样走这条路,顺带省掉了逐行合并。 + */ +final class PlatformPreview implements AutoCloseable { + /** 低于这个像素数就放弃材质、改用平均色块。 */ + private static final int MIN_TEXTURE_CELL = 4; + /** 无法识别颜色时的中性灰。 */ + private static final int FALLBACK_COLOR = 0xFF7A7A7A; + private static final int GRID_COLOR = 0x33000000; + private static final int BEDROCK_COLOR = 0xFF3A3A3A; + private static final int ORIGIN_COLOR = 0xFFD03A2E; + private static final int MIN_BUILD_Y = -64; + private static final int MAX_BUILD_Y = 320; + + /** 无染色时的白色乘数。 */ + private static final int NO_TINT = 0xFFFFFFFF; + + private static final AtomicInteger NEXT_ID = new AtomicInteger(); + private static final Map COLOR_CACHE = new HashMap<>(); + private static final Map SPRITE_CACHE = new HashMap<>(); + private static final Map TINT_CACHE = new HashMap<>(); + + private final int viewW; + private final int viewH; + private final int sideW; + private final int sideH; + private final DynamicTexture topTexture; + private final DynamicTexture sideTexture; + private final ResourceLocation topTextureId; + private final ResourceLocation sideTextureId; + private boolean closed; + + private PlatformStyle style; + private DimensionGenerationConfig config; + + /* 俯视布局 */ + private int periodX = 16; + private int periodZ = 16; + private int cell = 1; + private int stepX = 1; + private int stepZ = 1; + private boolean textured; + private int cols; + private int rows; + private int offsetCols; + private int offsetRows; + + /* 侧视布局 */ + private int sideRows; + private int sideCellY = 1; + private int sideBottomY; + private int sideTopY; + private int sideBedrockY; + private boolean sideHasBedrock; + + PlatformPreview(int viewW, int viewH, int sideW, int sideH) { + this.viewW = Math.max(1, viewW); + this.viewH = Math.max(1, viewH); + this.sideW = Math.max(1, sideW); + this.sideH = Math.max(1, sideH); + this.topTexture = new DynamicTexture(this.viewW, this.viewH, false); + this.sideTexture = new DynamicTexture(this.sideW, this.sideH, false); + int suffix = NEXT_ID.incrementAndGet(); + this.topTextureId = new ResourceLocation( + UselessMod.MODID, "dynamic/dimension_preview_top_" + suffix); + this.sideTextureId = new ResourceLocation( + UselessMod.MODID, "dynamic/dimension_preview_side_" + suffix); + TextureManager manager = Minecraft.getInstance().getTextureManager(); + manager.register(topTextureId, topTexture); + manager.register(sideTextureId, sideTexture); + } + + @Override + public void close() { + if (closed) return; + closed = true; + TextureManager manager = Minecraft.getInstance().getTextureManager(); + manager.release(topTextureId); + manager.release(sideTextureId); + } + + /** 配置或样式变化时才重新采样;record 的值语义让比较很便宜。 */ + void refresh(PlatformStyle style, DimensionGenerationConfig config) { + if (style == null || config == null) { + this.style = null; + this.config = null; + return; + } + if (style == this.style && config.equals(this.config)) return; + this.style = style; + this.config = config; + computeLayout(); + bake(); + } + + boolean isValid() { + return style != null && config != null; + } + + /** 当前是否用真实材质渲染(否则是平均色块)。 */ + boolean isTextured() { + return isValid() && textured; + } + + /** 一个完整周期的尺寸(格),用于界面提示。 */ + int periodBlocksX() { + return periodX; + } + + int periodBlocksZ() { + return periodZ; + } + + /** 色块模式下 1 像素覆盖多少格;材质模式恒为 1。 */ + int blocksPerPixel() { + return textured || cell > 1 ? 1 : Math.max(stepX, stepZ); + } + + int topY() { + return isValid() ? PlatformLayout.topY(config) : 0; + } + + int bottomY() { + return isValid() ? PlatformLayout.bottomY(config) : 0; + } + + int bedrockY() { + return isValid() && sideHasBedrock ? sideBedrockY : Integer.MIN_VALUE; + } + + /* ================= 布局 ================= */ + + private void computeLayout() { + periodX = periodBlocks(config, true); + periodZ = periodBlocks(config, false); + double blockPx = Math.min(viewW / (double) periodX, viewH / (double) periodZ); + if (blockPx >= MIN_TEXTURE_CELL) { + textured = true; + cell = Math.max(1, (int) Math.floor(blockPx)); + stepX = 1; + stepZ = 1; + } else if (blockPx >= 1.0D) { + textured = false; + cell = Math.max(1, (int) Math.floor(blockPx)); + stepX = 1; + stepZ = 1; + } else { + // 一个方块分不到一个像素:让 1 像素覆盖多格,保证至少看到一个完整周期。 + textured = false; + cell = 1; + stepX = Math.max(1, (int) Math.ceil(periodX / (double) viewW)); + stepZ = Math.max(1, (int) Math.ceil(periodZ / (double) viewH)); + } + cols = (viewW + cell - 1) / cell; + rows = (viewH + cell - 1) / cell; + int periodCols = (periodX + stepX - 1) / stepX; + int periodRows = (periodZ + stepZ - 1) / stepZ; + offsetCols = Math.max(0, (cols - periodCols) / 2); + offsetRows = Math.max(0, (rows - periodRows) / 2); + } + + /** + * 一个完整重复单元的边长(格)。 + * + *

马路模式下,平台本身以区块(16 格)为周期,而道路/边界以 + * {@code boundaryInterval + roadWidth} 个区块为周期;中心标记只依赖 + * {@code boundaryInterval}。多联模式把边界间隔当作合并尺寸。 + */ + private static int periodBlocks(DimensionGenerationConfig config, boolean xAxis) { + if (config.mode() == DimensionGenerationConfig.Mode.MULTI) { + int size = xAxis ? config.boundaryIntervalX() : config.boundaryIntervalZ(); + return Math.max(16, Math.max(1, size) * 16); + } + int interval = xAxis ? config.boundaryIntervalX() : config.boundaryIntervalZ(); + int other = xAxis ? config.boundaryIntervalZ() : config.boundaryIntervalX(); + if (config.roadWidth() > 0 && interval > 0) { + return (interval + config.roadWidth()) * 16; + } + if (config.centerMarkerEnabled() && interval > 0 && other > 0) { + return interval * 16; + } + return 16; + } + + /* ================= 烘焙 ================= */ + + private void bake() { + bakeTop(); + bakeSide(); + } + + private void bakeTop() { + NativeImage image = topTexture.getPixels(); + if (image == null) return; + int background = argbToAbgr(MachineScreenStyle.PANEL_COLOR); + fill(image, viewW, viewH, background); + + for (int cy = 0; cy < rows; cy++) { + int py = cy * cell; + if (py >= viewH) break; + int baseZ = PlatformLayout.mod((cy - offsetRows) * stepZ, periodZ); + for (int cx = 0; cx < cols; cx++) { + int px = cx * cell; + if (px >= viewW) break; + int baseX = PlatformLayout.mod((cx - offsetCols) * stepX, periodX); + BlockState state = sampleState(baseX, baseZ); + // 边缘格子按整格缩放并裁掉越界像素,否则会被压缩变形。 + if (textured) { + TextureAtlasSprite sprite = spriteFor(state); + if (sprite != null) { + drawSprite(image, px, py, cell, viewW, viewH, sprite, tintFor(state)); + continue; + } + } + fill(image, viewW, viewH, px, py, cell, cell, argbToAbgr(colorFor(state))); + } + } + topTexture.upload(); + } + + /** + * 超采样时取子网格里最常见的方块,而不是平均值——平均值会把一格宽的道路或 + * 边界线混入周围填充色,导致其在预览图中不可见。 + */ + private BlockState sampleState(int baseX, int baseZ) { + if (stepX == 1 && stepZ == 1) return stateAt(baseX, baseZ); + + int nx = Math.min(stepX, 3); + int nz = Math.min(stepZ, 3); + BlockState[] seen = new BlockState[nx * nz]; + int[] counts = new int[nx * nz]; + int distinct = 0; + for (int j = 0; j < nz; j++) { + int bz = baseZ + j * stepZ / nz; + for (int i = 0; i < nx; i++) { + BlockState state = stateAt(baseX + i * stepX / nx, bz); + int found = -1; + for (int k = 0; k < distinct; k++) { + // BlockState 由注册表统一持有,引用比较即可。 + if (seen[k] == state) { + found = k; + break; + } + } + if (found < 0) { + seen[distinct] = state; + counts[distinct] = 1; + distinct++; + } else { + counts[found]++; + } + } + } + BlockState best = seen[0]; + int bestCount = counts[0]; + for (int k = 1; k < distinct; k++) { + if (counts[k] > bestCount) { + bestCount = counts[k]; + best = seen[k]; + } + } + return best; + } + + /** 俯视看到的是顶面:装饰方块优先,否则是该列的平台方块。 */ + private BlockState stateAt(int blockX, int blockZ) { + int chunkX = Math.floorDiv(blockX, 16); + int localX = Math.floorMod(blockX, 16); + int chunkZ = Math.floorDiv(blockZ, 16); + int localZ = Math.floorMod(blockZ, 16); + BlockState decoration = PlatformLayout.surfaceDecoration( + style, config, chunkX, chunkZ, localX, localZ); + if (decoration != null) return decoration; + return PlatformLayout.platformState(style, config, chunkX, chunkZ, localX, localZ); + } + + private void bakeSide() { + sideHasBedrock = config.generateBedrock(); + sideBedrockY = PlatformLayout.bedrockY(config, MIN_BUILD_Y, MAX_BUILD_Y); + int bottom = PlatformLayout.bottomY(config); + int top = PlatformLayout.topY(config); + sideBottomY = sideHasBedrock ? Math.min(bottom, sideBedrockY) : bottom; + sideTopY = Math.max(top, sideBottomY); + int range = sideTopY - sideBottomY + 1; + + if (range <= sideH) { + sideCellY = Math.max(1, sideH / range); + sideRows = range; + } else { + sideCellY = 1; + sideRows = Math.min(sideH, range); + } + int yStep = (int) Math.ceil(range / (double) sideRows); + + NativeImage image = sideTexture.getPixels(); + if (image == null) return; + int empty = argbToAbgr(MachineScreenStyle.PANEL_COLOR); + fill(image, sideW, sideH, empty); + + // 直接按平台带的起止行填充,而不是逐行采样:抽稀后一行代表多格 Y, + // 薄板(例如层数=1)可能整段落在两个采样行之间而完全消失。 + int platformLowRow = clampRow((bottom + 1 - sideBottomY) / yStep); + int platformHighRow = clampRow((top - sideBottomY) / yStep); + for (int col = 0; col < sideW; col++) { + // 与俯视图首行对齐:侧视的顶面颜色就是俯视最上面一行的颜色。 + int sourceCol = Math.min(cols - 1, col / cell); + BlockState surface = stateAt( + PlatformLayout.mod((sourceCol - offsetCols) * stepX, periodX), + PlatformLayout.mod(-offsetRows * stepZ, periodZ)); + int abgr = argbToAbgr(colorFor(surface)); + for (int row = platformLowRow; row <= platformHighRow; row++) { + // Y 越大越靠上:剖面要像真实侧视,不能把基岩画到平台上方。 + int py = (sideRows - 1 - row) * sideCellY; + for (int dy = 0; dy < sideCellY && py + dy < sideH; dy++) { + image.setPixelRGBA(col, py + dy, abgr); + } + } + } + // 基岩只有一格厚,同样会落不到采样行上,单独补一条线;后画所以压在平台带下方那一行。 + if (sideHasBedrock) { + int py = (sideRows - 1 - clampRow((sideBedrockY - sideBottomY) / yStep)) * sideCellY; + int abgr = argbToAbgr(BEDROCK_COLOR); + for (int dy = 0; dy < Math.max(1, sideCellY) && py + dy < sideH; dy++) { + for (int col = 0; col < sideW; col++) { + image.setPixelRGBA(col, py + dy, abgr); + } + } + } + sideTexture.upload(); + } + + private int clampRow(int row) { + return Math.max(0, Math.min(sideRows - 1, row)); + } + + /** + * 把精灵按盒式滤波缩放成一格 {@code cell × cell} 像素写入,比最近邻干净得多。 + * 越界的像素直接丢弃({@code clipWidth/clipHeight} 是图像边界)。 + */ + private static void drawSprite(NativeImage image, int px, int py, int cell, + int clipWidth, int clipHeight, + TextureAtlasSprite sprite, int tint) { + NativeImage source; + try { + SpriteContents contents = sprite.contents(); + source = contents.getOriginalImage(); + } catch (Throwable ignored) { + return; + } + int sourceWidth = source.getWidth(); + int sourceHeight = source.getHeight(); + if (sourceWidth <= 0 || sourceHeight <= 0) return; + int tintR = (tint >>> 16) & 0xFF; + int tintG = (tint >>> 8) & 0xFF; + int tintB = tint & 0xFF; + + for (int y = 0; y < cell; y++) { + int dy = py + y; + if (dy >= clipHeight) break; + int y0 = y * sourceHeight / cell; + int y1 = Math.min(sourceHeight, Math.max(y0 + 1, (y + 1) * sourceHeight / cell)); + for (int x = 0; x < cell; x++) { + int dx = px + x; + if (dx >= clipWidth) break; + int x0 = x * sourceWidth / cell; + int x1 = Math.min(sourceWidth, Math.max(x0 + 1, (x + 1) * sourceWidth / cell)); + image.setPixelRGBA(dx, dy, + multiplyTint(boxAverage(source, x0, x1, y0, y1), tintR, tintG, tintB)); + } + } + } + + /** + * 把染色乘到已平均好的颜色上(ABGR 进、ABGR 出)。 + * + *

本模组的塑料方块共用一张**灰度**贴图,颜色完全来自 {@code BlockColors} 的 + * tint(模型每个面都声明了 {@code tintindex: 0})。不乘这一步,材质模式就会画成灰的。 + */ + private static int multiplyTint(int abgr, int tintR, int tintG, int tintB) { + if (tintR == 255 && tintG == 255 && tintB == 255) return abgr; + int red = ((abgr & 0xFF) * tintR) / 255; + int green = (((abgr >>> 8) & 0xFF) * tintG) / 255; + int blue = (((abgr >>> 16) & 0xFF) * tintB) / 255; + return (0xFF << 24) | (blue << 16) | (green << 8) | red; + } + + /** 源区域内按 alpha 加权的平均色,返回 ABGR(与 {@code setPixelRGBA} 一致)。 */ + private static int boxAverage(NativeImage source, int x0, int x1, int y0, int y1) { + long sumR = 0; + long sumG = 0; + long sumB = 0; + long sumA = 0; + int samples = 0; + for (int y = y0; y < y1; y++) { + for (int x = x0; x < x1; x++) { + int pixel; + try { + pixel = source.getPixelRGBA(x, y); + } catch (Throwable ignored) { + continue; + } + int alpha = (pixel >>> 24) & 0xFF; + sumR += (long) (pixel & 0xFF) * alpha; + sumG += (long) ((pixel >>> 8) & 0xFF) * alpha; + sumB += (long) ((pixel >>> 16) & 0xFF) * alpha; + sumA += alpha; + samples++; + } + } + if (samples == 0 || sumA == 0) return argbToAbgr(FALLBACK_COLOR); + int red = (int) (sumR / sumA); + int green = (int) (sumG / sumA); + int blue = (int) (sumB / sumA); + int alpha = (int) (sumA / samples); + if (alpha < 255) { + // 半透明方块(玻璃等):按 alpha 合成到面板底色上,否则在浅色面板里几乎看不见。 + float keep = alpha / 255.0F; + red = composite(red, keep, 16); + green = composite(green, keep, 8); + blue = composite(blue, keep, 0); + } + return (0xFF << 24) | (blue << 16) | (green << 8) | red; + } + + /** 与 {@link #multiplyTint} 相同,只是输入输出都是 ARGB。 */ + private static int multiplyTintArgb(int argb, int tint) { + int tintR = (tint >>> 16) & 0xFF; + int tintG = (tint >>> 8) & 0xFF; + int tintB = tint & 0xFF; + if (tintR == 255 && tintG == 255 && tintB == 255) return argb; + int red = (((argb >>> 16) & 0xFF) * tintR) / 255; + int green = (((argb >>> 8) & 0xFF) * tintG) / 255; + int blue = ((argb & 0xFF) * tintB) / 255; + return 0xFF000000 | (red << 16) | (green << 8) | blue; + } + + private static int composite(int channel, float keep, int shift) { + int panel = (MachineScreenStyle.PANEL_COLOR >> shift) & 0xFF; + return (int) (channel * keep + panel * (1.0F - keep)); + } + + private static void fill(NativeImage image, int imageWidth, int imageHeight, int abgr) { + fill(image, imageWidth, imageHeight, 0, 0, imageWidth, imageHeight, abgr); + } + + private static void fill(NativeImage image, int imageWidth, int imageHeight, + int px, int py, int width, int height, int abgr) { + for (int y = py; y < py + height && y < imageHeight; y++) { + for (int x = px; x < px + width && x < imageWidth; x++) { + image.setPixelRGBA(x, y, abgr); + } + } + } + + /** ARGB({@code GuiGraphics.fill} 用)转 ABGR({@code NativeImage} 用)。 */ + private static int argbToAbgr(int argb) { + return (argb & 0xFF00FF00) + | ((argb & 0x00FF0000) >>> 16) + | ((argb & 0x000000FF) << 16); + } + + /* ================= 取色 ================= */ + + /** 顶面贴图;取不到时回退粒子图标,仍取不到返回 null。 */ + private static TextureAtlasSprite spriteFor(BlockState state) { + TextureAtlasSprite cached = SPRITE_CACHE.get(state); + if (cached != null) return cached; + TextureAtlasSprite sprite = resolveSprite(state); + if (sprite != null) SPRITE_CACHE.put(state, sprite); + return sprite; + } + + private static TextureAtlasSprite resolveSprite(BlockState state) { + try { + BlockModelShaper shaper = Minecraft.getInstance().getBlockRenderer().getBlockModelShaper(); + BakedModel model = shaper.getBlockModel(state); + // MinecraftForge 的 getQuads 重载;renderType 传 null 表示不限渲染层。 + List up = model.getQuads(state, Direction.UP, RandomSource.create(), + ModelData.EMPTY, null); + if (!up.isEmpty()) return up.get(0).getSprite(); + return model.getParticleIcon(ModelData.EMPTY); + } catch (Throwable ignored) { + return null; + } + } + + /** + * 代表色(ARGB)。优先本模组有色方块的精确 RGB,其次顶面纹理平均色,最后才用 + * MapColor 兜底——MapColor 只有几十个色值、且大量方块(玻璃、栏杆、门、 + * 多数模组方块)返回 {@code MapColor.NONE},只靠它会让预览出现黑块。 + */ + private static int colorFor(BlockState state) { + Integer cached = COLOR_CACHE.get(state); + if (cached != null) return cached; + int color = resolveColor(state); + COLOR_CACHE.put(state, color); + return color; + } + + /** + * 方块染色(ARGB)。本模组所有塑料方块共用同一张灰度贴图,颜色全靠 + * {@code ClientSetup.onBlockColor} 注册的 tint,因此取色与材质都必须乘上它。 + * 没有染色时返回白色乘数。 + */ + private static int tintFor(BlockState state) { + Integer cached = TINT_CACHE.get(state); + if (cached != null) return cached; + int tint = NO_TINT; + try { + int resolved = Minecraft.getInstance().getBlockColors().getColor(state, null, null, 0); + // -1 表示该方块没有染色(原版约定),此时按白色处理。 + if (resolved != -1) tint = 0xFF000000 | resolved; + } catch (Throwable ignored) { + // 少数方块的染色回调需要真实世界坐标,拿不到就当作无染色。 + } + TINT_CACHE.put(state, tint); + return tint; + } + + private static int resolveColor(BlockState state) { + if (state.getBlock() instanceof IColoredBlock colored) { + return colored.getColor().getRgb(); + } + TextureAtlasSprite sprite = spriteFor(state); + if (sprite != null) { + int averaged = averageColor(sprite); + if (averaged != 0) { + int tint = tintFor(state); + return multiplyTintArgb(averaged, tint); + } + } + try { + MapColor mapColor = state.getBlock().defaultMapColor(); + if (mapColor != null && mapColor != MapColor.NONE) { + return 0xFF000000 | mapColor.col; + } + } catch (Throwable ignored) { + // 少数模组方块会在这里抛异常,落到下面的兜底色。 + } + return FALLBACK_COLOR; + } + + /** 整张贴图的 alpha 加权平均色(ARGB);全透明或读取失败返回 0 表示不可用。 */ + private static int averageColor(TextureAtlasSprite sprite) { + try { + SpriteContents contents = sprite.contents(); + NativeImage image = contents.getOriginalImage(); + int width = image.getWidth(); + int height = image.getHeight(); + if (width <= 0 || height <= 0) return 0; + int abgr = boxAverage(image, 0, width, 0, height); + // 转回 ARGB:boxAverage 已经做过 alpha 合成,alpha 恒为 255。 + int red = abgr & 0xFF; + int green = (abgr >>> 8) & 0xFF; + int blue = (abgr >>> 16) & 0xFF; + return 0xFF000000 | (red << 16) | (green << 8) | blue; + } catch (Throwable ignored) { + return 0; + } + } + + /* ================= 绘制 ================= */ + + void renderTop(GuiGraphics graphics, int x0, int y0) { + if (!isValid()) return; + // 贴图是立即绘制,网格与原点标记是 fill(排队)——先后顺序天然正确: + // 网格会在帧末提交,落在贴图之上。 + graphics.blit(topTextureId, x0, y0, viewW, viewH, 0.0F, 0.0F, + viewW, viewH, viewW, viewH); + graphics.enableScissor(x0, y0, x0 + viewW, y0 + viewH); + drawChunkGrid(graphics, x0, y0); + drawOriginMarker(graphics, x0, y0); + // 排队的 fill 要在这里提交,否则裁剪在帧末已经解除,裁剪形同虚设。 + graphics.flush(); + graphics.disableScissor(); + } + + void renderSide(GuiGraphics graphics, int x0, int y0) { + if (!isValid()) return; + graphics.blit(sideTextureId, x0, y0, sideW, sideH, 0.0F, 0.0F, + sideW, sideH, sideW, sideH); + } + + /** 每 16 格一条区块分界线;超采样时线宽不足 1 像素,直接跳过。 */ + private void drawChunkGrid(GuiGraphics graphics, int x0, int y0) { + if (stepX != 1 || stepZ != 1) return; + for (int cx = 0; cx < cols; cx++) { + int baseX = PlatformLayout.mod((cx - offsetCols) * stepX, periodX); + if (PlatformLayout.mod(baseX, 16) != 0) continue; + int px = x0 + cx * cell; + if (px < x0 + viewW) { + graphics.fill(px, y0, px + 1, y0 + viewH, GRID_COLOR); + } + } + for (int cy = 0; cy < rows; cy++) { + int baseZ = PlatformLayout.mod((cy - offsetRows) * stepZ, periodZ); + if (PlatformLayout.mod(baseZ, 16) != 0) continue; + int py = y0 + cy * cell; + if (py < y0 + viewH) { + graphics.fill(x0, py, x0 + viewW, py + 1, GRID_COLOR); + } + } + } + + /** 周期原点(区块 0,0 的角落)标记,便于对位。 */ + private void drawOriginMarker(GuiGraphics graphics, int x0, int y0) { + int px = x0 + offsetCols * cell; + int py = y0 + offsetRows * cell; + if (px < x0 || px >= x0 + viewW || py < y0 || py >= y0 + viewH) return; + graphics.fill(px - 3, py, px + 4, py + 1, ORIGIN_COLOR); + graphics.fill(px, py - 3, px + 1, py + 4, ORIGIN_COLOR); + } + + /* ================= 鼠标查询 ================= */ + + /** 俯视区局部坐标对应的方块坐标,越界返回 null。 */ + int[] blockAt(int localX, int localY) { + if (!isValid() || localX < 0 || localY < 0) return null; + int cx = localX / cell; + int cy = localY / cell; + if (cx >= cols || cy >= rows) return null; + int blockX = PlatformLayout.mod((cx - offsetCols) * stepX, periodX); + int blockZ = PlatformLayout.mod((cy - offsetRows) * stepZ, periodZ); + return new int[]{blockX, blockZ}; + } + + /** 该坐标上的方块,用于悬浮提示。 */ + BlockState stateAtBlock(int blockX, int blockZ) { + return isValid() ? stateAt(blockX, blockZ) : null; + } + + /** 判断该方块当前承担的角色,返回 i18n 键后缀;无法归类时返回 null。 */ + String roleKey(BlockState state) { + if (!isValid() || state == null) return null; + if (state.is(config.roadBlockA())) return "road"; + if (state.is(config.roadBlockB())) return "road_edge"; + if (state.is(config.roadBlockC())) return "road_marking"; + if (state.is(config.centerMarkerBlock())) return "marker"; + if (state.is(config.boundaryBlockA()) || state.is(config.boundaryBlockB())) return "boundary"; + if (state.is(config.borderBlock())) return "border"; + if (state.is(config.centerBlock())) return "center"; + if (state.is(config.fillBlock())) return "fill"; + return null; + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/gui/PortConfigScreen.java b/src/main/java/com/sorrowmist/useless/client/gui/PortConfigScreen.java new file mode 100644 index 00000000..398e8d61 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/gui/PortConfigScreen.java @@ -0,0 +1,95 @@ +package com.sorrowmist.useless.client.gui; + +import com.electronwill.nightconfig.core.UnmodifiableConfig; +import com.google.gson.Gson; +import com.sorrowmist.useless.core.config.ConfigManager; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraftforge.common.ForgeConfigSpec; +import java.util.*; + +/** Forge 1.20.1 config editor: validates the whole visible page before applying anything. */ +public final class PortConfigScreen extends Screen { + private static final Gson JSON = new Gson(); + private final Screen parent; + private ForgeConfigSpec spec = ConfigManager.COMMON_SPEC; + private final List> values = new ArrayList<>(); + private final Map, EditBox> editors = new LinkedHashMap<>(); + private int page; + private boolean singleSpec; + private String message = ""; + public PortConfigScreen(Screen parent) { super(Component.literal("Useless Mod 配置")); this.parent = parent; } + public PortConfigScreen(Screen parent, ForgeConfigSpec selected, Component title) { + super(title); this.parent=parent; this.spec=selected; this.singleSpec=true; + } + private void collect(UnmodifiableConfig config) { + for (Object value : config.valueMap().values()) { + if (value instanceof ForgeConfigSpec.ConfigValue entry) values.add(entry); + else if (value instanceof UnmodifiableConfig nested) collect(nested); + } + } + private void select(ForgeConfigSpec selected) { spec=selected; page=0; message=""; rebuildWidgets(); } + private int rows() { return Math.max(1, (height-135)/40); } + @Override protected void init() { + values.clear(); editors.clear(); + if (spec.isLoaded()) collect(spec.getValues()); + values.sort(Comparator.comparing(value -> String.join(".",value.getPath()))); + if (!singleSpec) { + addRenderableWidget(Button.builder(Component.literal("通用"),b->select(ConfigManager.COMMON_SPEC)).bounds(width/2-155,25,100,20).build()); + addRenderableWidget(Button.builder(Component.literal("客户端"),b->select(ConfigManager.CLIENT_SPEC)).bounds(width/2-50,25,100,20).build()); + Button server=Button.builder(Component.literal("单人存档"),b->select(ConfigManager.SERVER_SPEC)).bounds(width/2+55,25,100,20).build(); + server.active=minecraft.hasSingleplayerServer() && ConfigManager.SERVER_SPEC.isLoaded(); addRenderableWidget(server); + } + page=Math.min(page,Math.max(0,(values.size()-1)/rows())); + int start=page*rows(); + for(int i=start;i ? JSON.toJson(current) : String.valueOf(current)); + ForgeConfigSpec.ValueSpec rule=spec.getSpec().get(value.getPath()); + if(rule.getComment()!=null) box.setTooltip(Tooltip.create(Component.literal(rule.getComment()))); + editors.put(value,box); addRenderableWidget(box); + } + addRenderableWidget(Button.builder(Component.literal("上一页"),b->{page--;rebuildWidgets();}).bounds(15,height-60,75,20).build()).active=page>0; + addRenderableWidget(Button.builder(Component.literal("下一页"),b->{page++;rebuildWidgets();}).bounds(95,height-60,75,20).build()).active=(page+1)*rows()save()).bounds(width-175,height-60,75,20).build()).active=spec.isLoaded(); + addRenderableWidget(Button.builder(Component.translatable("gui.done"),b->onClose()).bounds(width-95,height-60,80,20).build()); + if(!spec.isLoaded()) message="该配置尚未加载。"; + } + private Object parse(Object original,String text) { + if(original instanceof Boolean) { if(!text.equalsIgnoreCase("true")&&!text.equalsIgnoreCase("false")) throw new IllegalArgumentException("请输入 true 或 false"); return Boolean.parseBoolean(text); } + if(original instanceof Integer) return Integer.valueOf(text); + if(original instanceof Long) return Long.valueOf(text); + if(original instanceof Double) return Double.valueOf(text); + if(original instanceof Float) return Float.valueOf(text); + if(original instanceof Enum item) return Arrays.stream(item.getDeclaringClass().getEnumConstants()).filter(e->((Enum)e).name().equalsIgnoreCase(text)).findFirst().orElseThrow(); + if(original instanceof List) return JSON.fromJson(text,List.class); + return text; + } + @SuppressWarnings({"rawtypes","unchecked"}) private void save() { + Map,Object> pending=new LinkedHashMap<>(); + try { + for(var entry:editors.entrySet()) { + var key=entry.getKey(); Object parsed=parse(key.get(),entry.getValue().getValue()); + ForgeConfigSpec.ValueSpec rule=spec.getSpec().get(key.getPath()); + if(!rule.test(parsed)) throw new IllegalArgumentException(String.join(".",key.getPath())); + pending.put(key,parsed); + } + pending.forEach((key,value)->((ForgeConfigSpec.ConfigValue)key).set(value)); spec.save(); + message="已保存。标记为需要重启的配置将在重启后生效。"; + } catch(RuntimeException invalid) { message="未保存:"+Objects.toString(invalid.getMessage(),"配置值无效"); } + } + @Override public void onClose(){minecraft.setScreen(parent);} + @Override public void render(GuiGraphics graphics,int mouseX,int mouseY,float partialTick) { + renderBackground(graphics); super.render(graphics,mouseX,mouseY,partialTick); + graphics.drawCenteredString(font,title,width/2,8,0xFFFFFF); + int start=page*rows(); + for(int i=start;i按下态若只依赖屏幕的 {@code mouseReleased} 复位,在鼠标移出界面后松开等路径下不会触发, + * 按钮将停留在按下态。此处以点击回调作为复位兜底,保证任何一次成功点击后都会弹起。

+ */ + @Override + public void onPress() { + releaseVisualState(); + super.onPress(); + } + + void releaseVisualState() { + pressed = false; + } + + @Override + protected void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + renderSurface(graphics,pressed || isHoveredOrFocused()); + renderLabel(graphics,!active?0x888894:pressed?0xAED4ED:0xF2F2F2); + } + + @Override public boolean mouseReleased(double mouseX,double mouseY,int button) { + releaseVisualState(); + return super.mouseReleased(mouseX,mouseY,button); + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/gui/ScaledEnergyAmount.java b/src/main/java/com/sorrowmist/useless/client/gui/ScaledEnergyAmount.java new file mode 100644 index 00000000..4f172dc9 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/gui/ScaledEnergyAmount.java @@ -0,0 +1,71 @@ +package com.sorrowmist.useless.client.gui; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.OptionalLong; + +final class ScaledEnergyAmount { + private static final String[] SUFFIXES = {"", "K", "M", "G", "T", "P", "E"}; + + private ScaledEnergyAmount() { + } + + static String format(long amount) { + BigDecimal value = BigDecimal.valueOf(Math.max(0L, amount)); + int suffix = 0; + while (suffix < SUFFIXES.length - 1 + && value.compareTo(BigDecimal.valueOf(1_000L)) >= 0) { + value = value.movePointLeft(3); + suffix++; + } + return value.setScale(2, RoundingMode.DOWN) + .stripTrailingZeros() + .toPlainString() + SUFFIXES[suffix]; + } + + static OptionalLong parse(String text, long maximum) { + if (text == null) return OptionalLong.empty(); + String value = text.trim(); + if (value.isEmpty()) return OptionalLong.empty(); + + int suffix = suffixIndex(value.charAt(value.length() - 1)); + String number = suffix == 0 ? value : value.substring(0, value.length() - 1); + if (number.isEmpty() || number.equals(".")) return OptionalLong.empty(); + + try { + BigDecimal scaled = new BigDecimal(number).movePointRight(suffix * 3); + if (scaled.signum() < 0) return OptionalLong.empty(); + long clampedMaximum = Math.max(0L, maximum); + if (scaled.compareTo(BigDecimal.valueOf(clampedMaximum)) >= 0) { + return OptionalLong.of(clampedMaximum); + } + return OptionalLong.of(scaled.setScale(0, RoundingMode.DOWN).longValueExact()); + } catch (ArithmeticException | NumberFormatException exception) { + return OptionalLong.empty(); + } + } + + static boolean isValidInput(String text) { + if (text.isEmpty()) return true; + int end = suffixIndex(text.charAt(text.length() - 1)) == 0 + ? text.length() : text.length() - 1; + boolean decimalPoint = false; + for (int index = 0; index < end; index++) { + char character = text.charAt(index); + if (character == '.' && !decimalPoint) { + decimalPoint = true; + } else if (character < '0' || character > '9') { + return false; + } + } + return end == text.length() || end > 0; + } + + private static int suffixIndex(char suffix) { + char normalized = Character.toUpperCase(suffix); + for (int index = 1; index < SUFFIXES.length; index++) { + if (SUFFIXES[index].charAt(0) == normalized) return index; + } + return 0; + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/gui/StaffLinkScreen.java b/src/main/java/com/sorrowmist/useless/client/gui/StaffLinkScreen.java new file mode 100644 index 00000000..a037ee09 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/gui/StaffLinkScreen.java @@ -0,0 +1,3216 @@ +package com.sorrowmist.useless.client.gui; + +import com.sorrowmist.useless.client.network.ClientPacketHandlers; +import com.sorrowmist.useless.client.stafflink.StaffLinkStressClientState; +import com.sorrowmist.useless.content.menus.StaffLinkMenu; +import com.sorrowmist.useless.content.stafflink.LinkFilterCondition; +import com.sorrowmist.useless.content.stafflink.LinkFilterPattern; +import com.sorrowmist.useless.content.stafflink.LinkFilterSlot; +import com.sorrowmist.useless.content.stafflink.LinkFlow; +import com.sorrowmist.useless.content.stafflink.LinkMedium; +import com.sorrowmist.useless.content.stafflink.ResourceFamily; +import com.sorrowmist.useless.content.stafflink.StaffLinkFilters; +import com.sorrowmist.useless.content.stafflink.StaffLinkRoute; +import com.sorrowmist.useless.network.StaffLinkSyncPacket; +import com.sorrowmist.useless.network.StaffLinkStressStatusPacket; +import com.sorrowmist.useless.world.stafflink.StaffLinkNetwork; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.core.Direction; +import net.minecraft.core.GlobalPos; +import net.minecraft.network.chat.Component; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.InventoryMenu; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.client.extensions.common.IClientFluidTypeExtensions; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.glfw.GLFW; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalLong; + +/** + * 无线物流配置界面。 + * + *

沿用本模组机器界面的统一风格({@link MachineScreenStyle} + {@link PressableAE2Button})。 + * 布局分两块:上面是「网络 + 容器列表」(可搜索、可改名、Ctrl/Shift 多选后批量编辑), + * 下面是选中线路的搬运规则,底部是玩家背包。

+ * + *

容器列表本身不在这里高亮:手持杖且开着无线物流模式时,世界里的自动高亮由 + * {@code StaffLinkHighlightRenderer} 独立负责,界面关着也生效。

+ * + *

所有控件的右边缘统一落在 {@link #CONTENT_RIGHT},视觉上对齐成一列。

+ */ +public final class StaffLinkScreen extends AbstractContainerScreen { + private static final int PANEL_WIDTH = 250; + /** + * 面板高度。 + * + *

过滤槽不画在主界面上(18 格 + 每格两个限制框会撑得很高),主界面只留一个 + * 「过滤」按钮,点开在覆盖层里编辑。所以这里维持原来的高度。

+ */ + private static final int PANEL_HEIGHT = 336; + + /** 内容区左右边界;内衬比内容各外扩 2px,形成对称留白。 */ + private static final int CONTENT_LEFT = 8; + private static final int CONTENT_RIGHT = 240; + private static final int INSET_LEFT = 6; + private static final int INSET_RIGHT = 242; + private static final int CONTENT_WIDTH = CONTENT_RIGHT - CONTENT_LEFT; + + // ---- 锚点列表 + private static final int NETWORK_ROW_Y = 19; + private static final int SEARCH_ROW_Y = 33; + private static final int NAME_ROW_Y = 47; + private static final int SMALL_FIELD_HEIGHT = 12; + + /** 搜索框为批量按钮让出右侧空间;三个控件的右边缘仍落在 {@link #CONTENT_RIGHT}。 */ + private static final int SEARCH_WIDTH = 142; + private static final int SELECT_ALL_X = 154; + private static final int SELECT_ALL_WIDTH = 42; + private static final int CLEAR_SELECTION_X = 200; + private static final int CLEAR_SELECTION_WIDTH = 40; + + /** 网络切换按钮的宽度(`<` / `>`)。 */ + private static final int NETWORK_SWITCH_WIDTH = 14; + private static final int PREV_BUTTON_X = 8; + private static final int NETWORK_FIELD_X = 24; + private static final int NETWORK_FIELD_WIDTH = 110; + private static final int NEXT_BUTTON_X = 136; + private static final int NEW_BUTTON_X = 154; + private static final int NEW_BUTTON_WIDTH = 30; + + private static final int LIST_FIRST_ROW_Y = 61; + private static final int LIST_ROW_HEIGHT = 13; + private static final int LIST_VISIBLE_ROWS = 5; + private static final int LIST_TOP = 59; + private static final int LIST_BOTTOM = 124; + private static final int LIST_UNBIND_WIDTH = 12; + private static final int LIST_INSET_BOTTOM = 128; + /** 行首 ▲ / ▼ 两个按钮:宽度各 9px,从 CONTENT_LEFT 起。 */ + private static final int LIST_MOVE_WIDTH = 9; + private static final int LIST_MOVE_COUNT = 2; + /** 行首按钮占掉的横向空间;流向字形与名字整体右移这么多,名字预算也要相应扣掉。 */ + private static final int LIST_TEXT_SHIFT = LIST_MOVE_WIDTH * LIST_MOVE_COUNT + 2; + private static final int LIST_GLYPH_X = CONTENT_LEFT + 2 + LIST_TEXT_SHIFT; + private static final int LIST_NAME_X = CONTENT_LEFT + 10 + LIST_TEXT_SHIFT; + /** 应力线路上容器行尾那个数字占的宽度(紧凑写法最多 4~5 个字符)。 */ + private static final int STRESS_ROW_VALUE_WIDTH = 36; + + // ---- 线路配置 + private static final int CONFIG_INSET_TOP = 130; + private static final int ROUTE_ROW_Y = 134; + private static final int ROW_SECOND_Y = 152; + private static final int ROW_THIRD_Y = 170; + private static final int ROW_FOURTH_Y = 188; + private static final int ROW_HEIGHT = 14; + + private static final int FLOW_WIDTH = 74; + private static final int FLOW_MEDIUM_GAP = 5; + /** 资源类型按钮的左边界;下拉列表贴着它向下展开。 */ + private static final int MEDIUM_BUTTON_X = CONTENT_LEFT + FLOW_WIDTH + FLOW_MEDIUM_GAP; + private static final int MEDIUM_MENU_Y = ROW_SECOND_Y + ROW_HEIGHT + 1; + private static final int MEDIUM_MENU_WIDTH = FLOW_WIDTH; + private static final int MEDIUM_MENU_ROW_HEIGHT = ROW_HEIGHT; + + /** + * 下拉菜单的绘制 z。 + * + *

GUI 的深度测试是开着的({@code RenderType.GUI}、文字与 AE2 面板都走 LEQUAL), + * 而 {@link GuiGraphics} 给容器内容定的 z 依次是:槽位物品 150、堆叠数字 200、 + * 手持拖动物品 232(其数字再 +200)。下拉若留在 z=0,就比这些内容全都远, + * 片段会被深度测试直接丢掉 —— 光 {@code flush()} 只保证「后画」,救不回来, + * 表现就是下拉被背包物品和栏位数字盖住。

+ * + *

抬到 500 可稳定压过全部容器内容(原版 tooltip 用 400;下拉展开期间不弹 tooltip, + * 所以不必迁就它)。

+ */ + private static final float MEDIUM_MENU_Z = 500.0F; + private static final int SIDE_WIDTH = 113; + private static final int SIDE_TRIGGER_GAP = 6; + + /** 数值输入框一行的三格:起点与宽度,最后一格的右边缘正好落在 {@link #CONTENT_RIGHT}。 */ + private static final int[] NUMERIC_CELL_X = {8, 85, 162}; + private static final int[] NUMERIC_CELL_WIDTH = {77, 77, 78}; + + /** 输入框停手多久后自动提交(tick)。玩家填了值却没失焦时靠它兜底。 */ + private static final int AUTO_COMMIT_TICKS = 10; + + /** 复制/粘贴的一次性提示在屏幕上停留多久(tick)。 */ + private static final int NOTICE_TICKS = 50; + private static final int NOTICE_COLOR = 0xFF2E7D32; + + /** + * 批量编辑选中行:实色青底 + 左侧深青竖条 + 行尾 ✓,三重叠加保证一眼看出。 + * + *

底色刻意用不透明的青,和单选那层近白({@code HIGHLIGHT_COLOR})以及面板底色 + * ({@code PANEL_COLOR})都能明显区分。

+ */ + private static final int MULTI_SELECT_COLOR = 0xFFB6E4E4; + private static final int MULTI_SELECT_TEXT_COLOR = 0xFF149E9E; + /** 单选(配置区正在编辑的那台)行的左侧竖条。 */ + private static final int SELECTED_BAR_COLOR = 0xFF413F54; + + // ---- 过滤入口(主界面)+ 过滤面板(覆盖层) + private static final int FILTER_X = 8; + /** 主界面「过滤」按钮所在行;沿用原来过滤槽标题的位置。 */ + private static final int FILTER_Y = 204; + /** 主界面「过滤」按钮 / 「应用到全部同名容器」/「解散网络」并排。 */ + private static final int FILTER_BUTTON_WIDTH = 66; + private static final int APPLY_ALL_X = 82; + private static final int APPLY_ALL_WIDTH = 92; + private static final int DISSOLVE_X = 180; + private static final int DISSOLVE_WIDTH = CONTENT_RIGHT - DISSOLVE_X; + + /** + * 应力运行状态两行文字的位置。 + * + *

取在「过滤」按钮那一行({@link #FILTER_Y} + 16 = 220)与背包标题(242)之间的空白里: + * 上面已经排满了四行配置,下面紧跟着背包,只有这一段是空的。

+ */ + private static final int STRESS_STATUS_Y = 224; + /** 状态文字用色:出问题时换成醒目的琥珀色,正常时与其它次要文字一致。 */ + private static final int STRESS_WARN_COLOR = 0xFFB26A00; + + // ---- 过滤面板(覆盖层)几何 + /** + * 面板尺寸。 + * + *

v2 起每格只是一个 16×16 的槽位(模式框与两条条件都搬进了右键的详细面板), + * 所以面板整体收窄到「刚好放下标题 / 提示 + 6 列格子 + 两个按钮」。窄面板还有个好处: + * 右边必然留得下 JEI 的原料侧栏({@code 427 − 2 − 138 = 287 ≥ 48})。

+ * + *

高度 = `6(上边距) + 21(标题 + 提示两行) + 3×22 − 6(三行) + 6 + 16(按钮) + 6 = 115`。

+ */ + private static final int FILTER_PANEL_WIDTH = 138; + private static final int FILTER_PANEL_HEIGHT = 115; + private static final int FILTER_PANEL_PAD = 6; + /** 标题行 + 提示行占掉的高度,格子从这里往下排。 */ + private static final int FILTER_PANEL_HEADER = 21; + /** + * 一格 = 16×16 的槽位 + 6px 间隙。 + * + *

v1 的 78×30 是给「模式框 + 两个限制框」留的位置,那两样搬进详细面板之后就不需要了, + * 于是间距可以收到最小。

+ */ + private static final int FILTER_CELL_COL_STEP = 22; + private static final int FILTER_CELL_ROW_STEP = 22; + /** + * 6 列 × 3 行 = 18 格,跟 {@link StaffLinkRoute#FILTER_LIMIT} 一致。 + * + *

格子变成 16×16 的小方块之后,3 列会拼成一条细长的竖条、两侧留下大片空白; + * 改成 6 列后网格正好铺满面板宽度。

+ */ + private static final int FILTER_COLUMNS = 6; + /** + * 格内槽位尺寸。 + * + *

取 16 = 本模组自己的标准槽位({@link MachineScreenStyle#drawSlotBackground} 也是 + * 16×16 + 顶部 1px 暗边),跟一级菜单的容器槽视觉一致;v1 用的 18 会让过滤格看起来比 + * 界面里其它槽位大一整圈。

+ */ + private static final int FILTER_SLOT_SIZE = 16; + /** 格子网格在面板内的左上角;横向居中。 */ + private static final int FILTER_GRID_X = FILTER_PANEL_PAD + + (FILTER_PANEL_WIDTH - FILTER_PANEL_PAD * 2 + - ((FILTER_COLUMNS - 1) * FILTER_CELL_COL_STEP + FILTER_SLOT_SIZE)) / 2; + private static final int FILTER_GRID_Y = FILTER_PANEL_PAD + FILTER_PANEL_HEADER; + /** 面板在「垂直居中」基础上再上移这么多,给下方的玩家背包让位。 */ + private static final int FILTER_PANEL_RAISE = 16; + /** + * 条件数值框 / 模式框的字符上限。 + * + *

吃 {@code K / M / G / T / P / E} 后缀,一个缩写就能有 {@code 1.5K} 四个字符, + * 纯数字留到 {@code 9999999} 也够。

+ */ + private static final int FILTER_LIMIT_MAX_CHARS = 8; + /** 面板底部按钮行:高度、宽度与横向起点(两个按钮整体居中)。 */ + private static final int FILTER_BUTTON_HEIGHT = 16; + private static final int FILTER_PANEL_BUTTON_WIDTH = 48; + private static final int FILTER_PANEL_BUTTON_GAP = 6; + private static final int FILTER_PANEL_BUTTON_X = (FILTER_PANEL_WIDTH + - FILTER_PANEL_BUTTON_WIDTH * 2 - FILTER_PANEL_BUTTON_GAP) / 2; + /** 按钮行 y:格子网格底(87) + 6 间隙。 */ + private static final int FILTER_PANEL_BUTTON_Y = 93; + /** + * 过滤面板(覆盖层)绘制 z。 + * + *

面板是在 {@code super.render()} 之后画的(不能早于它,否则 JEI 侧栏不显示), + * 所以必须抬 z 才能压过普通深度的内容:槽位物品在 z=150、堆叠数字在 z=200。 + * 取 {@link #MEDIUM_MENU_Z} 同档的 500,可稳定压过全部容器内容。 + * 光 {@code flush()} 只解决「谁后画」,解决不了深度测试 —— 两件事都要做。

+ */ + private static final float FILTER_PANEL_Z = 500.0F; + /** + * 「光标上拿着的物品」的绘制 z。 + * + *

vanilla 只在 {@code super.render()} 里画它,那时两个覆盖层面板(500 / 700)都还没画, + * 所以从背包拿起物品来标记过滤格时,手上拿的东西会被面板整个盖住、看不见(用户实测反馈)。 + * 这里在覆盖层最后重画一遍。

+ * + *

取 750:物品自身还会再被推 150 ⇒ 实际 900,刚好压过详细面板里那些物品(700+150=850), + * 又不至于高到跟工具提示(700+400=1100)抢层。

+ */ + private static final float CARRIED_ITEM_Z = 750.0F; + + // ---- 详细编辑面板(每格右键打开)几何:独立宽度,跟过滤面板解耦 + private static final int DETAIL_PANEL_WIDTH = 250; + private static final int DETAIL_PANEL_HEIGHT = 122; + private static final int DETAIL_PANEL_PAD = 8; + /** + * 详细面板绘制 z:压过滤面板。 + * + *

⛔ 必须比 {@link #FILTER_PANEL_Z} 高 150 以上:{@code GuiGraphics.renderItem} + * 内部会把物品再往前推 150({@code pose.translate(x + 8, y + 8, 150 + …)}), + * 所以过滤面板里画在 z=500 的物品实际落在 650 —— 详细面板的背景若只到 600, + * 那些物品就会**穿透到详细面板之上**(用户实测:二级菜单的标记物品盖在三级菜单上)。 + * 取 +200 留出余量。

+ */ + private static final float DETAIL_PANEL_Z = FILTER_PANEL_Z + 200.0F; + /** 面板内各行 y(面板内相对坐标)。 */ + private static final int DETAIL_TITLE_Y = 8; + private static final int DETAIL_MARKER_ROW_Y = 26; + private static final int DETAIL_OUT_ROW_Y = 50; + private static final int DETAIL_IN_ROW_Y = 74; + private static final int DETAIL_BUTTON_Y = 98; + private static final int DETAIL_BUTTON_WIDTH = 70; + private static final int DETAIL_BUTTON_HEIGHT = 16; + /** 详细面板里槽位(A / B)的尺寸。 */ + private static final int DETAIL_SLOT_SIZE = FILTER_SLOT_SIZE; + /** 行内控件 x(面板内相对坐标)。 */ + private static final int DETAIL_MARKER_X = DETAIL_PANEL_PAD; // 8 + private static final int DETAIL_PATTERN_X = DETAIL_MARKER_X + DETAIL_SLOT_SIZE + 2; // 28 + private static final int DETAIL_PATTERN_WIDTH = 140; + private static final int DETAIL_EXCLUDE_X = 172; + private static final int DETAIL_EXCLUDE_WIDTH = 70; + private static final int DETAIL_LABEL_X = DETAIL_PANEL_PAD; // 8 + private static final int DETAIL_COND_SLOT_X = 44; + private static final int DETAIL_OP_X = DETAIL_COND_SLOT_X + DETAIL_SLOT_SIZE + 4; // 66 + private static final int DETAIL_OP_WIDTH = 32; + private static final int DETAIL_VALUE_X = DETAIL_OP_X + DETAIL_OP_WIDTH + 4; // 102 + private static final int DETAIL_VALUE_WIDTH = 140; + private static final int DETAIL_ROW_HEIGHT = 18; + + private static final Direction[] SIDE_ORDER = { + null, Direction.UP, Direction.DOWN, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST + }; + + private final PressableAE2Button[] routeButtons = new PressableAE2Button[StaffLinkNetwork.ROUTE_COUNT]; + private PressableAE2Button flowButton; + private PressableAE2Button mediumButton; + /** + * 资源类型下拉是否展开。 + * + *

做成下拉而不是「点一下换一个」:装了化学品 / 魔源 / 通量之后类型能到十种, + * 一路轮换过去太费手。

+ */ + private boolean mediumMenuOpen; + /** 下拉里的候选类型;环境不变,{@link #init()} 里算一次就够。 */ + private List mediumOptions = List.of(); + private PressableAE2Button enabledButton; + private PressableAE2Button sideButton; + private PressableAE2Button prevNetworkButton; + private PressableAE2Button nextNetworkButton; + private PressableAE2Button newNetworkButton; + private PressableAE2Button dissolveButton; + private PressableAE2Button selectAllButton; + private PressableAE2Button clearSelectionButton; + private PressableAE2Button applyAllButton; + /** 主界面上打开过滤面板的入口按钮。 */ + private PressableAE2Button filterButton; + /* 过滤面板(覆盖层)的按钮:只进 children、不进 renderables —— 放进 renderables 会被 + super.render() 画到底层 UI 之下。事件也要手动转发,见 mouseClicked/mouseReleased。 */ + private PressableAE2Button closeFilterButton; + private PressableAE2Button clearFilterButton; + + private EditBox networkNameField; + private EditBox searchField; + private EditBox nameField; + private EditBox weightField; + private EditBox amountField; + private EditBox intervalField; + /** 数值框 + 标签 + 取值范围;标签宽度决定框的起点,范围用来做悬停提示。 */ + private final List numericFields = new ArrayList<>(); + + /* ---- 过滤格的「详细编辑面板」(每格右键打开,嵌套在过滤面板之上) ---- + 面板放:标记 A、模式框、白名单/黑名单、输出端条件(B + ≥/≤ + 数值)、输入端条件、关闭。 + 这些控件只 addWidget(进 children、不进 renderables),手动 render + 手动转发事件—— + 理由同过滤面板的两个按钮:进 renderables 会被 super.render() 画到底层之下。 */ + private boolean detailPanelOpen; + /** 详细面板正在编辑的格号;没打开时 -1。 */ + private int detailIndex = -1; + private EditBox detailPatternField; + private EditBox detailOutValueField; + private EditBox detailInValueField; + private PressableAE2Button detailExcludeButton; + private PressableAE2Button detailOutOpButton; + private PressableAE2Button detailInOpButton; + private PressableAE2Button detailCloseButton; + + /** + * 过滤面板(覆盖层)是否打开。 + * + *

打开时独占点击与 ESC,并且跳过底层界面的渲染——否则底层槽位里的物品 + * 会在覆盖层之上显示(它们走 RenderBuffers 的固定缓冲,见 {@code DimensionConfigScreen} + * 里同一问题的处理)。

+ */ + private boolean filterPanelOpen; + + private record NumericSpec(EditBox field, Component label, long min, long max, Component hint, + boolean scaled) { + } + + /** + * 数值框在应力线路上的替代标签与提示。 + * + *

应力线路里同一行两个框的含义完全不同(目标转速、旋转方向),而标签是建框时就定死的, + * 因此这里按框覆盖一层。替代标签刻意取得比原名短,这样画在框左边不会越过格子左边界。

+ */ + private record NumericOverride(Component label, Component hint) { + } + + /** 需要替代标签的数值框;不在表里就按默认标签画。 */ + private final Map numericOverrides = new HashMap<>(); + /** 当前数值区是不是按应力的语义在画;用来避免每帧重复设置。 */ + private boolean stressFields; + /** 当前数值区是不是按气压的语义在画(数量格 = 目标气压 bar);同样避免每帧重复设置。 */ + private boolean pressureFields; + /** 应力线路的方向按钮:与周期输入框共用同一格,两者互斥显示。 */ + private PressableAE2Button stressDirectionButton; + + private int scrollOffset; + private String controlSignature = ""; + /** 上一 tick 有焦点的输入框,用来捕捉「焦点离开」这一刻。 */ + private EditBox focusedField; + /** 输入框停手了多少 tick,到 {@link #AUTO_COMMIT_TICKS} 就自动提交。 */ + private int editIdleTicks; + + /** 上一次点击的锚点;Shift+点击用它当范围选择的一端。 */ + private GlobalPos lastClickedAnchor; + + /** + * 会话内的配置剪贴板。 + * + *

Ctrl+C 存的是整条线路配置;Ctrl+V 只取它的字段,锚点与线路号一律用当前选中的 + * ——所以既能把 A 机器的配置贴到 B 机器,也能贴到同一台机器的另一条线路上。

+ * + *

刻意留在内存里、不走系统剪贴板:这里只想要「复制一份配置」这一件事, + * 读系统剪贴板就得额外处理一堆解析失败的脏数据,收益不成正比。

+ */ + @Nullable + private static StaffLinkRoute copiedConfig; + /** 复制/粘贴的一次性提示;{@link #noticeTicks} 归零后消失。 */ + @Nullable + private Component notice; + private int noticeTicks; + + public StaffLinkScreen(StaffLinkMenu menu, Inventory inventory, Component title) { + super(menu, inventory, title); + imageWidth = PANEL_WIDTH; + imageHeight = PANEL_HEIGHT; + titleLabelX = 8; + titleLabelY = 7; + inventoryLabelX = 44; + inventoryLabelY = 242; + } + + @Override + protected void init() { + super.init(); + + mediumOptions = LinkMedium.supported(); + mediumMenuOpen = false; + + for (int route = 0; route < routeButtons.length; route++) { + final int index = route; + routeButtons[route] = addRenderableWidget(new PressableAE2Button( + leftPos + CONTENT_LEFT + route * 26, topPos + ROUTE_ROW_Y, 24, ROW_HEIGHT, + Component.literal(String.valueOf(route)), button -> selectRoute(index))); + } + + flowButton = addRenderableWidget(new PressableAE2Button( + leftPos + CONTENT_LEFT, topPos + ROW_SECOND_Y, FLOW_WIDTH, ROW_HEIGHT, Component.empty(), + button -> edit(config -> withFlow(config, config.flow().next())))); + mediumButton = addRenderableWidget(new PressableAE2Button( + leftPos + MEDIUM_BUTTON_X, topPos + ROW_SECOND_Y, + FLOW_WIDTH, ROW_HEIGHT, Component.empty(), + button -> mediumMenuOpen = !mediumMenuOpen)); + enabledButton = addRenderableWidget(new PressableAE2Button( + leftPos + CONTENT_RIGHT - FLOW_WIDTH, topPos + ROW_SECOND_Y, FLOW_WIDTH, ROW_HEIGHT, + Component.empty(), button -> edit(config -> withEnabled(config, !config.enabled())))); + + sideButton = addRenderableWidget(new PressableAE2Button( + leftPos + CONTENT_LEFT, topPos + ROW_THIRD_Y, SIDE_WIDTH, ROW_HEIGHT, Component.empty(), + button -> edit(config -> withSide(config, nextSide(config.side()))))); + + networkNameField = addTextField(NETWORK_FIELD_X, NETWORK_ROW_Y, NETWORK_FIELD_WIDTH, + Component.translatable("gui.useless_mod.wireless_logistics.network_name_hint"), + StaffLinkNetwork.MAX_NAME); + searchField = addTextField(CONTENT_LEFT, SEARCH_ROW_Y, SEARCH_WIDTH, + Component.translatable("gui.useless_mod.wireless_logistics.search_hint"), + StaffLinkNetwork.MAX_ANCHOR_NAME); + nameField = addTextField(CONTENT_LEFT, NAME_ROW_Y, CONTENT_WIDTH, + Component.translatable("gui.useless_mod.wireless_logistics.rename"), + StaffLinkNetwork.MAX_ANCHOR_NAME); + + weightField = addNumericField(0, "weight_label", + StaffLinkRoute.MIN_WEIGHT, StaffLinkRoute.MAX_WEIGHT, true, false); + amountField = addNumericField(1, "amount_label", + StaffLinkRoute.MIN_AMOUNT, Long.MAX_VALUE, false, true); + intervalField = addNumericField(2, "interval_label", + StaffLinkRoute.MIN_INTERVAL, StaffLinkRoute.MAX_INTERVAL, false, false); + + // 应力线路上这一格放的是旋转方向,用按钮比让人输 1 / 2 直观。 + // 它与周期输入框共用同一格位置,靠 visible 互斥显示。 + stressDirectionButton = addRenderableWidget(new PressableAE2Button( + intervalField.getX(), intervalField.getY(), + intervalField.getWidth(), intervalField.getHeight(), + Component.empty(), + button -> edit(config -> withNumbers(config, config.weight(), config.amount(), + nextStressDirection(config.interval()))))); + stressDirectionButton.visible = false; + + newNetworkButton = addRenderableWidget(new PressableAE2Button( + leftPos + NEW_BUTTON_X, topPos + NETWORK_ROW_Y, NEW_BUTTON_WIDTH, SMALL_FIELD_HEIGHT, + Component.translatable("gui.useless_mod.wireless_logistics.network_new"), + button -> newNetwork())); + prevNetworkButton = addRenderableWidget(new PressableAE2Button( + leftPos + PREV_BUTTON_X, topPos + NETWORK_ROW_Y, NETWORK_SWITCH_WIDTH, SMALL_FIELD_HEIGHT, + Component.literal("<"), button -> cycleNetwork(-1))); + nextNetworkButton = addRenderableWidget(new PressableAE2Button( + leftPos + NEXT_BUTTON_X, topPos + NETWORK_ROW_Y, NETWORK_SWITCH_WIDTH, SMALL_FIELD_HEIGHT, + Component.literal(">"), button -> cycleNetwork(1))); + dissolveButton = addRenderableWidget(new PressableAE2Button( + leftPos + DISSOLVE_X, topPos + FILTER_Y, DISSOLVE_WIDTH, 16, + Component.translatable("gui.useless_mod.wireless_logistics.dissolve"), + button -> dissolveNetwork())); + + selectAllButton = addRenderableWidget(new PressableAE2Button( + leftPos + SELECT_ALL_X, topPos + SEARCH_ROW_Y, SELECT_ALL_WIDTH, SMALL_FIELD_HEIGHT, + Component.translatable("gui.useless_mod.wireless_logistics.select_all"), + button -> selectAllVisible())); + clearSelectionButton = addRenderableWidget(new PressableAE2Button( + leftPos + CLEAR_SELECTION_X, topPos + SEARCH_ROW_Y, CLEAR_SELECTION_WIDTH, SMALL_FIELD_HEIGHT, + Component.translatable("gui.useless_mod.wireless_logistics.clear_selection"), + button -> clearSelection())); + // 「应用到全部」明确只作用于同名容器:同名按界面显示的那个名字算。 + applyAllButton = addRenderableWidget(new PressableAE2Button( + leftPos + APPLY_ALL_X, topPos + FILTER_Y, APPLY_ALL_WIDTH, 16, + Component.translatable("gui.useless_mod.wireless_logistics.apply_all_same_name"), + button -> applyAllSameName())); + filterButton = addRenderableWidget(new PressableAE2Button( + leftPos + FILTER_X, topPos + FILTER_Y, FILTER_BUTTON_WIDTH, 16, + Component.empty(), button -> setFilterPanelOpen(!filterPanelOpen))); + + // 面板按钮只进 children:进 renderables 会被 super.render() 画到底层界面之下。 + closeFilterButton = addWidget(new PressableAE2Button( + filterPanelCloseX(), filterPanelCloseY(), FILTER_PANEL_BUTTON_WIDTH, FILTER_BUTTON_HEIGHT, + Component.translatable("gui.useless_mod.wireless_logistics.filter_close"), + button -> setFilterPanelOpen(false))); + clearFilterButton = addWidget(new PressableAE2Button( + filterPanelClearX(), filterPanelClearY(), FILTER_PANEL_BUTTON_WIDTH, FILTER_BUTTON_HEIGHT, + Component.translatable("gui.useless_mod.wireless_logistics.filter_clear"), + button -> clearAllFilterSlots())); + initDetailWidgets(); + syncFilterPanelButtons(); + + // 开界面与下发快照是两个包;万一快照先到,这里把它捞回来,界面就不会空着。 + StaffLinkSyncPacket pending = ClientPacketHandlers.consumePendingStaffLinkSync(menu.getNetworkId()); + if (pending != null) { + menu.receiveSync(pending.network(), pending.index(), pending.count()); + } + + // init() 会重建全部控件(缩放窗口也会走这里),而新的周期输入框默认是可见的。 + // 这里把「已按应力渲染过」的状态清掉,让下面的 updateControls 重新套用一次, + // 否则缩放之后周期输入框会重新冒出来、压在方向按钮上。 + stressFields = false; + pressureFields = false; + numericOverrides.clear(); + updateControls(); + } + + /** 面板按钮的 visible 同时决定是否响应点击,每次开关面板都要同步。 */ + private void syncFilterPanelButtons() { + closeFilterButton.visible = filterPanelOpen; + clearFilterButton.visible = filterPanelOpen; + } + + private void setFilterPanelOpen(boolean open) { + filterPanelOpen = open; + if (!open) { + // 关过滤面板时把详细面板也一起收掉(会提交它),免得留下一个看不见却有焦点的框。 + // 强关:连过滤面板都不要了,模式非法也直接丢弃。 + closeDetailPanel(true); + jeiDragActive = false; + } else { + // 过滤格必须挂在「某个已选中的锚点 × 线路」的配置上。开面板前先保证这份配置存在, + // 否则 `getSelectedConfig()` 会是 null —— 界面渲染成一片灰格,JEI 那边 + // `getTargetsTyped` 也直接返回空列表,表现就是「能看见格子但拖不进去」。 + ensureRouteConfig(); + } + syncFilterPanelButtons(); + } + + /** 「清空」:把 18 格全部置空。 */ + private void clearAllFilterSlots() { + closeDetailPanel(true); + for (int index = 0; index < StaffLinkRoute.FILTER_LIMIT; index++) { + if (!currentFilterSlot(index).isEmpty()) { + menu.setFilterSlot(index, LinkFilterSlot.EMPTY); + } + } + } + + private EditBox addTextField(int x, int y, int width, Component hint, int maxLength) { + EditBox field = new EditBox(font, leftPos + x, topPos + y, width, SMALL_FIELD_HEIGHT, hint); + field.setMaxLength(maxLength); + field.setHint(hint); + addRenderableWidget(field); + trackEdits(field); + return field; + } + + /** + * 记录「玩家正在打字」。 + * + *

只在输入框有焦点时重置空闲计数——服务端同步也会触发 responder,若不加这个判断, + * 自动提交会被同步一直推迟。

+ */ + private void trackEdits(EditBox field) { + field.setResponder(value -> { + if (field.isFocused()) { + editIdleTicks = 0; + } + }); + } + + /** + * 一行放三个数值输入框:每个占一格,标签画在框左边,框右对齐到本格右边缘。 + * + *

框的起点按当前语言下标签的实际宽度算,所以中英文都不会把标签压在框上; + * 英文标签用的是短名,完整含义与取值范围看悬停提示。

+ */ + private EditBox addNumericField(int cell, String labelKey, long min, long max, + boolean allowNegative, boolean scaled) { + Component label = Component.translatable("gui.useless_mod.wireless_logistics." + labelKey); + int cellWidth = NUMERIC_CELL_WIDTH[cell]; + int fieldWidth = Math.max(34, cellWidth - font.width(label) - 4); + int fieldX = NUMERIC_CELL_X[cell] + cellWidth - fieldWidth; + + EditBox field = new EditBox(font, leftPos + fieldX, topPos + ROW_FOURTH_Y, fieldWidth, + ROW_HEIGHT, label); + if (scaled) { + // 「数量」没有上限,允许 K / M / G / T / P / E 输入——和矿石生成器的能量输入同一套。 + field.setMaxLength(24); + field.setFilter(ScaledEnergyAmount::isValidInput); + } else { + field.setMaxLength(6); + field.setFilter(value -> value.isEmpty() + || (allowNegative ? value.matches("-?\\d{0,3}") : value.matches("\\d{0,5}"))); + } + addRenderableWidget(field); + trackEdits(field); + numericFields.add(new NumericSpec(field, label, min, max, + Component.translatable("gui.useless_mod.wireless_logistics." + labelKey + "_hint"), + scaled)); + return field; + } + + // ---- 过滤面板:18 格 + 每格两个限制框 + 右键弹出的模式框 + + /** + * 建详细编辑面板的控件。 + * + *

位置取屏幕坐标而不是 {@code leftPos/topPos}:它属于覆盖层面板,按窗口居中, + * 跟主面板无关。一律先收起({@code visible=false})——{@link EditBox#isMouseOver} 不看 + * {@code visible},收起时在别处要自己判断,否则收起的框照样会抢走点击。

+ */ + private void initDetailWidgets() { + // 不设 hint:「#标签 或通配符,例如 #c:ingots、*iron*」有三十来字,EditBox 的 hint 不裁切地 + // 铺出去会盖到右边的「白名单/黑名单」按钮上。用法改由悬停提示承担(renderDetailTooltip)。 + detailPatternField = new EditBox(font, 0, 0, DETAIL_PATTERN_WIDTH, DETAIL_ROW_HEIGHT, + Component.empty()); + detailPatternField.setMaxLength(LinkFilterPattern.MAX_LENGTH); + detailPatternField.setVisible(false); + addWidget(detailPatternField); + trackEdits(detailPatternField); + + detailOutValueField = addDetailValueField(); + detailInValueField = addDetailValueField(); + + detailExcludeButton = addWidget(new PressableAE2Button(0, 0, DETAIL_EXCLUDE_WIDTH, DETAIL_ROW_HEIGHT, + Component.empty(), button -> toggleDetailExclude())); + detailOutOpButton = addWidget(new PressableAE2Button(0, 0, DETAIL_OP_WIDTH, DETAIL_ROW_HEIGHT, + Component.empty(), button -> cycleDetailOp(false))); + detailInOpButton = addWidget(new PressableAE2Button(0, 0, DETAIL_OP_WIDTH, DETAIL_ROW_HEIGHT, + Component.empty(), button -> cycleDetailOp(true))); + detailCloseButton = addWidget(new PressableAE2Button(0, 0, DETAIL_BUTTON_WIDTH, DETAIL_BUTTON_HEIGHT, + Component.translatable("gui.useless_mod.wireless_logistics.filter_detail_done"), + button -> closeDetailPanel())); + setDetailWidgetsVisible(false); + } + + /** + * 一个只吃数字的条件数值框。 + * + *

吃 {@code K / M / G / T / P / E} 后缀,跟一级菜单的「数量」同一套 + * ({@link ScaledEnergyAmount}):搬上万的东西时不用数零。

+ */ + private EditBox addDetailValueField() { + EditBox field = new EditBox(font, 0, 0, DETAIL_VALUE_WIDTH, DETAIL_ROW_HEIGHT, Component.empty()); + field.setMaxLength(FILTER_LIMIT_MAX_CHARS); + field.setFilter(ScaledEnergyAmount::isValidInput); + field.setVisible(false); + addWidget(field); + trackEdits(field); + return field; + } + + /** 收起 / 显示详细面板的全部控件。 */ + private void setDetailWidgetsVisible(boolean visible) { + detailPatternField.setVisible(visible); + detailOutValueField.setVisible(visible); + detailInValueField.setVisible(visible); + detailExcludeButton.visible = visible; + detailOutOpButton.visible = visible; + detailInOpButton.visible = visible; + detailCloseButton.visible = visible; + } + + /** + * 摆位、刷新文案与显示值;每 tick 调一次(跟主界面按钮一样跟着 {@code resize()} 走)。 + * + *

值用 {@link #setIfUnfocused} 同步:正在编辑的框保持不动,免得打字被打断。

+ */ + private void updateDetailControls() { + if (!detailPanelOpen || detailIndex < 0) { + setDetailWidgetsVisible(false); + return; + } + int px = detailPanelX(); + int py = detailPanelY(); + detailPatternField.setX(px + DETAIL_PATTERN_X); + detailPatternField.setY(py + DETAIL_MARKER_ROW_Y); + detailExcludeButton.setX(px + DETAIL_EXCLUDE_X); + detailExcludeButton.setY(py + DETAIL_MARKER_ROW_Y); + detailOutOpButton.setX(px + DETAIL_OP_X); + detailOutOpButton.setY(py + DETAIL_OUT_ROW_Y); + detailOutValueField.setX(px + DETAIL_VALUE_X); + detailOutValueField.setY(py + DETAIL_OUT_ROW_Y); + detailInOpButton.setX(px + DETAIL_OP_X); + detailInOpButton.setY(py + DETAIL_IN_ROW_Y); + detailInValueField.setX(px + DETAIL_VALUE_X); + detailInValueField.setY(py + DETAIL_IN_ROW_Y); + detailCloseButton.setX(px + (DETAIL_PANEL_WIDTH - DETAIL_BUTTON_WIDTH) / 2); + detailCloseButton.setY(py + DETAIL_BUTTON_Y); + + LinkFilterSlot slot = currentFilterSlot(detailIndex); + boolean hasMarker = !slot.isEmpty(); + LinkFilterCondition out = slot.outCond(); + LinkFilterCondition in = slot.inCond(); + + detailPatternField.setVisible(true); + detailCloseButton.visible = true; + detailExcludeButton.visible = hasMarker; + detailOutOpButton.visible = hasMarker; + detailInOpButton.visible = hasMarker; + detailOutValueField.setVisible(hasMarker && !out.isOff()); + detailInValueField.setVisible(hasMarker && !in.isOff()); + + detailExcludeButton.active = hasMarker; + detailOutOpButton.active = hasMarker; + detailInOpButton.active = hasMarker; + detailOutValueField.setEditable(hasMarker && !out.isOff()); + detailInValueField.setEditable(hasMarker && !in.isOff()); + + detailExcludeButton.setMessage(Component.translatable(slot.isExcluded() + ? "gui.useless_mod.wireless_logistics.filter_exclude_on" + : "gui.useless_mod.wireless_logistics.filter_exclude_off")); + detailOutOpButton.setMessage(opLabel(out.op())); + detailInOpButton.setMessage(opLabel(in.op())); + + setIfUnfocused(detailPatternField, slot.pattern() == null ? "" : slot.pattern()); + setIfUnfocused(detailOutValueField, conditionText(out)); + setIfUnfocused(detailInValueField, conditionText(in)); + } + + private static Component opLabel(LinkFilterCondition.Op op) { + return Component.translatable(switch (op) { + case OFF -> "gui.useless_mod.wireless_logistics.filter_cond_op_off"; + case AT_LEAST -> "gui.useless_mod.wireless_logistics.filter_cond_op_at_least"; + case AT_MOST -> "gui.useless_mod.wireless_logistics.filter_cond_op_at_most"; + }); + } + + /** 条件数值的显示文本:不启用时留空。 */ + private static String conditionText(LinkFilterCondition cond) { + return cond.isOff() ? "" : limitText(cond.value()); + } + + /** + * {@code 0} 在语义上是「不限制」,界面上就该是留空。 + * + *

非零值复用一级菜单的 {@link #formatAmount}:只在缩写能原样解析回来时才缩写 + * (1000 → {@code 1K}),像 1024 这种缩写会丢精度的({@code 1.02K} 只能解析回 1020) + * 就照原样显示数字。否则玩家点一下别的框触发提交,限制值就被悄悄改小了。

+ */ + private static String limitText(long value) { + return value > 0L ? formatAmount(value) : ""; + } + + private static void setIfUnfocused(EditBox field, String value) { + if (!field.isFocused() && !value.equals(field.getValue())) { + field.setValue(value); + } + } + + // ---- 详细编辑面板 + + /** 打开某一格的详细面板(会先把上一格未提交的编辑落下去)。 */ + private void openDetailPanel(int index) { + if (index < 0 || index >= StaffLinkRoute.FILTER_LIMIT || !menu.isFilterActive()) { + return; + } + if (detailPanelOpen && detailIndex == index) { + return; + } + if (detailPanelOpen) { + commitDetail(); + } + detailPanelOpen = true; + detailIndex = index; + setFocused(null); + updateDetailControls(); + } + + /** + * 关闭详细面板(会提交它)。 + * + *

「完成」按钮走这里:模式非法时**不关**,把面板留在原地让玩家改(提示已经弹了)。 + * 强关路径(ESC / 关过滤面板 / 清空)走 {@link #closeDetailPanel(boolean)} 传 {@code true}。

+ */ + private void closeDetailPanel() { + closeDetailPanel(false); + } + + private void closeDetailPanel(boolean force) { + if (!detailPanelOpen) { + return; + } + if (!force && !commitDetail()) { + return; + } + detailPanelOpen = false; + detailIndex = -1; + setFocused(null); + setDetailWidgetsVisible(false); + } + + /** + * 按详细面板当前控件的内容,把「镜像里的那一格」重读一遍。 + * + *

读的是控件而不是镜像:这样点 op / 黑名单按钮时,输入框里还没失焦提交的数值与模式 + * 不会丢。

+ * + *

⚠️ 空格子也要读模式框:详细面板是右键打开的,玩家完全可能在一个还没放标记的 + * 格子里直接打 {@code #tag} —— 早先这里一看到「镜像里是空槽」就立刻返回 {@code EMPTY}, + * 那段模式就永远存不进去(用户实测:「输入后点完成都无法保存」)。

+ * + * @return 组装好的格子;模式非法时返回 {@code null}(已弹提示) + */ + @Nullable + private LinkFilterSlot readDetailSlot() { + LinkFilterSlot slot = currentFilterSlot(detailIndex); + String text = detailPatternField.getValue().trim(); + if (!text.equals(slot.pattern() == null ? "" : slot.pattern())) { + if (text.isEmpty()) { + // 模式框被清空:这一格退回「没有标记」(若原本是物品/流体标记也会一起清掉—— + // 那正是玩家把模式删空的意图)。 + return LinkFilterSlot.EMPTY; + } + LinkFilterPattern parsed = LinkFilterPattern.parse(text); + if (parsed == null) { + showNotice(Component.translatable( + "gui.useless_mod.wireless_logistics.filter_pattern_invalid", text)); + return null; + } + // 空格子 ⇒ 这一打模式就是它的标记;非空 ⇒ 换掉原标记(保留 exclude 与条件)。 + slot = (slot.isEmpty() ? LinkFilterSlot.EMPTY : slot) + .withMarker(LinkFilterSlot.ofPattern(parsed)); + } + if (slot.isEmpty()) { + return LinkFilterSlot.EMPTY; + } + slot = applyDetailValue(slot, false); + return applyDetailValue(slot, true); + } + + /** 把某一条条件的数值框内容读进来(只在该条件启用时)。 */ + private LinkFilterSlot applyDetailValue(LinkFilterSlot slot, boolean input) { + LinkFilterCondition cond = input ? slot.inCond() : slot.outCond(); + if (cond.isOff()) { + return slot; + } + EditBox field = input ? detailInValueField : detailOutValueField; + long value = Math.max(0L, parseLimit(field.getValue(), cond.value())); + if (value == cond.value()) { + return slot; + } + LinkFilterCondition next = cond.withValue(value); + return input ? slot.withConditions(slot.outCond(), next) + : slot.withConditions(next, slot.inCond()); + } + + /** + * 把详细面板里未提交的编辑落成一次 {@code setFilterSlot}(只在真有变化时才发)。 + * + * @return 是否提交成功;模式非法时返回 {@code false}(已弹提示),调用方可以据此不关面板 + */ + private boolean commitDetail() { + if (!detailPanelOpen || detailIndex < 0) { + return true; + } + LinkFilterSlot slot = readDetailSlot(); + if (slot == null) { + return false; + } + if (slot.isEmpty()) { + if (!currentFilterSlot(detailIndex).isEmpty()) { + menu.setFilterSlot(detailIndex, LinkFilterSlot.EMPTY); + } + return true; + } + if (!slot.equals(currentFilterSlot(detailIndex))) { + menu.setFilterSlot(detailIndex, slot); + } + return true; + } + + /** 在详细面板上叠加一次改动(op / 黑名单),并把输入框里未提交的值一起带上。 */ + private void editDetail(java.util.function.UnaryOperator change) { + if (!detailPanelOpen || detailIndex < 0) { + return; + } + LinkFilterSlot slot = readDetailSlot(); + if (slot == null || slot.isEmpty()) { + return; + } + LinkFilterSlot updated = change.apply(slot); + if (!updated.equals(currentFilterSlot(detailIndex))) { + menu.setFilterSlot(detailIndex, updated); + } + } + + /** 切换这一格的「白名单 / 黑名单」。 */ + private void toggleDetailExclude() { + editDetail(slot -> slot.withExclude(!slot.isExcluded())); + } + + /** + * 循环这一格某条条件的方向:关 → ≥ → ≤ → 关。 + * + *

从「关」启用时给一个符合直觉的默认:输出端先给 {@code ≥}(保留),输入端先给 {@code ≤}(封顶)。

+ */ + private void cycleDetailOp(boolean input) { + editDetail(slot -> { + LinkFilterCondition cond = input ? slot.inCond() : slot.outCond(); + LinkFilterCondition.Op next = switch (cond.op()) { + case OFF -> input ? LinkFilterCondition.Op.AT_MOST : LinkFilterCondition.Op.AT_LEAST; + case AT_LEAST -> LinkFilterCondition.Op.AT_MOST; + case AT_MOST -> LinkFilterCondition.Op.OFF; + }; + LinkFilterCondition updated = cond.withOp(next); + return input ? slot.withConditions(slot.outCond(), updated) + : slot.withConditions(updated, slot.inCond()); + }); + } + + // ---- 详细面板里的槽位点击 + + /** 详细面板里的 A 标记槽:手持物放下,空手清空这一格。 */ + private void clickDetailMarkerSlot() { + // 直接用镜像那一格当底子:放标记会顶掉模式,所以模式框里就算有非法文本也无所谓 + // (换完标记 updateDetailControls 会把模式框刷成空)。 + LinkFilterSlot slot = currentFilterSlot(detailIndex); + ItemStack carried = menu.getCarried(); + if (carried.isEmpty()) { + menu.setFilterSlot(detailIndex, LinkFilterSlot.EMPTY); + closeDetailPanel(true); + return; + } + LinkFilterSlot marker = StaffLinkFilters.fromItem(menu.getSelectedMedium(), carried); + if (marker != null) { + // 换标记时保留这一格原有的白名单/黑名单与条件。 + menu.setFilterSlot(detailIndex, slot.withMarker(marker)); + updateDetailControls(); + } + } + + /** + * 详细面板里的 B 控制材料槽:手持物放下,空手清回「测 A 自身」。 + * + *

条件没启用(OFF)时槽位是灰的,点了不生效——先点 op 按钮把它打开。

+ */ + private void clickDetailControlSlot(boolean input) { + LinkFilterCondition cond = input ? currentFilterSlot(detailIndex).inCond() + : currentFilterSlot(detailIndex).outCond(); + if (cond.isOff()) { + return; + } + ItemStack carried = menu.getCarried(); + if (carried.isEmpty()) { + menu.setFilterSlotConditionControl(detailIndex, input, LinkFilterSlot.EMPTY); + return; + } + LinkFilterSlot marker = StaffLinkFilters.fromItem(menu.getSelectedMedium(), carried); + if (marker != null) { + menu.setFilterSlotConditionControl(detailIndex, input, marker); + } + } + + /** 详细面板里的点击:按钮 → 输入框 → A/B 槽 → 玩家背包 → 面板外吞掉。 */ + private boolean handleDetailPanelClick(double mouseX, double mouseY, int button) { + if (detailCloseButton.mouseClicked(mouseX, mouseY, button) + || detailExcludeButton.mouseClicked(mouseX, mouseY, button) + || detailOutOpButton.mouseClicked(mouseX, mouseY, button) + || detailInOpButton.mouseClicked(mouseX, mouseY, button)) { + return true; + } + if (button == GLFW.GLFW_MOUSE_BUTTON_LEFT) { + if (detailPatternField.visible && detailPatternField.isMouseOver(mouseX, mouseY)) { + focusDetailField(detailPatternField, mouseX, mouseY); + return true; + } + if (detailOutValueField.visible && detailOutValueField.isMouseOver(mouseX, mouseY)) { + focusDetailField(detailOutValueField, mouseX, mouseY); + return true; + } + if (detailInValueField.visible && detailInValueField.isMouseOver(mouseX, mouseY)) { + focusDetailField(detailInValueField, mouseX, mouseY); + return true; + } + if (inDetailSlot(mouseX, mouseY, 0)) { + clickDetailMarkerSlot(); + updateDetailControls(); + return true; + } + if (inDetailSlot(mouseX, mouseY, 1)) { + clickDetailControlSlot(false); + updateDetailControls(); + return true; + } + if (inDetailSlot(mouseX, mouseY, 2)) { + clickDetailControlSlot(true); + updateDetailControls(); + return true; + } + } + // 玩家背包槽:面板开着时也允许正常「拿起 / 放下」——不然只能靠 JEI 拖。 + if (inventorySlotAt(mouseX, mouseY) != null) { + return super.mouseClicked(mouseX, mouseY, button); + } + // 点在面板空白处:让输入框失焦,但不关面板。 + setFocused(null); + return true; + } + + private boolean inDetailSlot(double mouseX, double mouseY, int which) { + int x = detailSlotScreenX(which); + int y = detailSlotScreenY(which); + return mouseX >= x && mouseX < x + DETAIL_SLOT_SIZE + && mouseY >= y && mouseY < y + DETAIL_SLOT_SIZE; + } + + /** + * 让详细面板里的输入框获得焦点。 + * + *

跟过滤面板一样:覆盖层这条路径到不了框架的 {@code mouseClicked},得自己补 + * {@code setFocused} + {@code field.mouseClicked}(摆光标)两步。

+ */ + private void focusDetailField(EditBox field, double mouseX, double mouseY) { + if (getFocused() != field) { + commitDetail(); + } + setFocused(field); + field.mouseClicked(mouseX, mouseY, GLFW.GLFW_MOUSE_BUTTON_LEFT); + } + + /** 当前在镜子里的第 index 格;越界或没有选中线路时回落到 {@link LinkFilterSlot#EMPTY}。 */ + private LinkFilterSlot currentFilterSlot(int index) { + List mirror = menu.getFilterMirror(); + return index >= 0 && index < mirror.size() ? mirror.get(index) : LinkFilterSlot.EMPTY; + } + + /** + * 空串 = 0(不限制);写歪了回落到原值,不悄悄改配置。 + * + *

和「数量」一样吃 {@code K / M / G / T / P / E} 后缀,走同一套 + * {@link ScaledEnergyAmount#parse}。

+ */ + private static long parseLimit(String text, long fallback) { + if (text == null || text.isBlank()) { + return 0L; + } + OptionalLong parsed = ScaledEnergyAmount.parse(text, Long.MAX_VALUE); + return parsed.isPresent() ? parsed.getAsLong() : fallback; + } + + @Override + protected void containerTick() { + super.containerTick(); + // 焦点离开输入框时把这次编辑提交掉,否则下一 tick 的同步会把它覆盖回去。 + EditBox focused = focusedField(); + if (focusedField != null && focused != focusedField) { + applyEdits(); + } + focusedField = focused; + + // 兜底:玩家填完值没失焦(比如直接看效果)时,停手一会儿就自动提交, + // 否则设置根本没送到服务端,表现就是「明明填了 2 却还是按旧值搬」。 + if (focused == null) { + editIdleTicks = 0; + } else if (++editIdleTicks >= AUTO_COMMIT_TICKS) { + applyEdits(); + editIdleTicks = 0; + } + + if (noticeTicks > 0 && --noticeTicks == 0) { + notice = null; + } + + updateControls(); + // 详细面板:摆位 + 显示值对齐(有焦点的框保持不动),每 tick 一次。 + updateDetailControls(); + // resize() 会重建控件,按钮的屏幕坐标要跟着走;顺便保证可见性跟着面板状态。 + if (filterPanelOpen) { + closeFilterButton.setX(filterPanelCloseX()); + closeFilterButton.setY(filterPanelCloseY()); + clearFilterButton.setX(filterPanelClearX()); + clearFilterButton.setY(filterPanelClearY()); + } + syncFilterPanelButtons(); + } + + /** 界面收到服务端快照。 */ + public void receiveSync(StaffLinkSyncPacket packet) { + menu.receiveSync(packet.network(), packet.index(), packet.count()); + clampScroll(); + updateControls(); + } + + @Override + public void onClose() { + applyEdits(); + // 应力状态是服务端按「谁开着界面」推下来的,关掉就该忘掉, + // 免得下次打开界面先闪一屏上一次的旧数字。 + StaffLinkStressClientState.clear(); + super.onClose(); + } + + // ------------------------------------------------------------------ 状态刷新 + + private void selectRoute(int route) { + menu.setSelection(menu.getSelectedAnchor(), route); + ensureRouteConfig(); + updateControls(); + } + + /** 选中的「锚点 × 线路」还没有配置时补一条默认的,让配置区永远有东西可调。 */ + private void ensureRouteConfig() { + GlobalPos anchor = menu.getSelectedAnchor(); + if (anchor != null && menu.isAnchorBound(anchor) && menu.getSelectedConfig() == null) { + // 只给这一台补默认值,不能走批量:多选期间点一下别的机器不该把默认值糊到全体上。 + menu.applyRouteSingle(anchor, menu.defaultRouteFor(anchor, menu.getSelectedRoute())); + } + } + + private void newNetwork() { + // 先把当前网络名提交掉,再让输入框失焦——否则新网络的空名字会被旧名字覆盖回去。 + applyEdits(); + setFocused(null); + menu.createNetwork(); + scrollOffset = 0; + } + + /** 切到相邻的一张网络;先把当前编辑提交掉,免得刚改的值丢了。 */ + private void cycleNetwork(int delta) { + applyEdits(); + setFocused(null); + menu.cycleNetwork(delta); + scrollOffset = 0; + } + + private void dissolveNetwork() { + applyEdits(); + setFocused(null); + menu.dissolveNetwork(); + scrollOffset = 0; + } + + private interface ConfigEdit { + StaffLinkRoute apply(StaffLinkRoute config); + } + + private void edit(ConfigEdit editor) { + StaffLinkRoute config = menu.getSelectedConfig(); + if (config == null) { + return; + } + menu.applyRoute(editor.apply(config)); + updateControls(); + } + + /** 把改名框、网络名框与三个数值输入框的内容提交上去。 */ + private void applyEdits() { + String requestedNetwork = networkNameField.getValue().trim(); + if (!requestedNetwork.equals(menu.getNetworkName())) { + menu.renameNetwork(requestedNetwork); + } + + GlobalPos anchor = menu.getSelectedAnchor(); + if (anchor != null && menu.isAnchorBound(anchor)) { + String requested = nameField.getValue().trim(); + String current = menu.getAnchorName(anchor); + if (!requested.equals(current == null ? "" : current)) { + menu.renameAnchor(anchor, requested); + } + } + + StaffLinkRoute config = menu.getSelectedConfig(); + if (config == null) { + return; + } + long weight = parsePlain(weightField.getValue(), config.weight(), + StaffLinkRoute.MIN_WEIGHT, StaffLinkRoute.MAX_WEIGHT); + long amount = config.medium().family() == ResourceFamily.PRESSURE + ? parsePressureMbar(amountField.getValue(), config.amount()) + : parseScaled(amountField.getValue(), config.amount(), StaffLinkRoute.MIN_AMOUNT); + long interval = parsePlain(intervalField.getValue(), config.interval(), + StaffLinkRoute.MIN_INTERVAL, StaffLinkRoute.MAX_INTERVAL); + if (weight != config.weight() || amount != config.amount() || interval != config.interval()) { + menu.applyRoute(withNumbers(config, (int) weight, amount, (int) interval)); + } + // 详细面板里被改过、还没失焦提交的条件 / 模式。 + commitDetail(); + } + + private static long parsePlain(String text, long fallback, long min, long max) { + try { + return com.sorrowmist.useless.compat.forge1201.LongMath.clamp(Long.parseLong(text.trim()), min, max); + } catch (NumberFormatException notANumber) { + return fallback; + } + } + + /** 「数量」允许 {@code K / M / G / T / P / E} 后缀;解析不了就回落到原值。 */ + private static long parseScaled(String text, long fallback, long min) { + OptionalLong parsed = ScaledEnergyAmount.parse(text, Long.MAX_VALUE); + return parsed.isPresent() ? Math.max(min, parsed.getAsLong()) : fallback; + } + + /** 输入框里当前的「数量」(解析不出来按 0 算),用来做实时提示。 */ + private long currentAmount() { + return parseScaled(amountField.getValue(), 0L, 0L); + } + + /** + * 「数量」的显示形式。 + * + *

只在缩写的能原样解析回来时才用缩写(1000 → {@code 1K},1e12 → {@code 1T}); + * 像 1024 这种缩写会丢精度的({@code 1.02K} 只能解析回 1020)就老老实实显示原数字—— + * 否则玩家点一下别的按钮触发提交,数量就被悄悄改小了。

+ */ + private static String formatAmount(long amount) { + String scaled = ScaledEnergyAmount.format(amount); + OptionalLong roundTrip = ScaledEnergyAmount.parse(scaled, Long.MAX_VALUE); + return roundTrip.isPresent() && roundTrip.getAsLong() == amount + ? scaled : String.valueOf(amount); + } + + /** + * 「目标气压」输入框:界面按 bar 输入,内部按毫巴({@code amount})。 + * + *

解析不了(空串 / 只输了个负号)就回落到原值,与 {@link #parseScaled} 一致; + * 解析成功则夹到 {@code [-1.0, 20.0]} bar——气动的物理下限是 -1 bar 的绝对真空。

+ */ + private static long parsePressureMbar(String text, long fallback) { + try { + double bar = Double.parseDouble(text.trim()); + return Math.round(Mth.clamp(bar, -1.0D, 20.0D) * 1000.0D); + } catch (NumberFormatException notANumber) { + return fallback; + } + } + + /** 毫巴 → bar 的显示形式;整 bar 不带小数点。 */ + private static String formatPressureMbar(long mbar) { + return mbar % 1000L == 0L + ? String.valueOf(mbar / 1000L) + : String.valueOf(mbar / 1000.0D); + } + + private void updateControls() { + StaffLinkRoute config = menu.getSelectedConfig(); + boolean hasConfig = config != null; + GlobalPos anchor = menu.getSelectedAnchor(); + boolean hasAnchor = anchor != null && menu.isAnchorBound(anchor); + + flowButton.active = hasConfig; + mediumButton.active = hasConfig; + enabledButton.active = hasConfig; + sideButton.active = hasConfig; + if (!hasConfig) { + // 没选中线路时下拉没有意义,顺手收起来。 + mediumMenuOpen = false; + } + int[] position = staffNetworkPosition(); + boolean multipleNetworks = position[1] > 1; + prevNetworkButton.active = multipleNetworks; + nextNetworkButton.active = multipleNetworks; + newNetworkButton.active = true; + dissolveButton.active = true; + // 这三个按钮的状态依赖「可见锚点」与「多选集合」,两者每 tick 都可能变, + // 所以必须放在下面的 signature 提前 return 之前。 + List visible = visibleAnchors(); + selectAllButton.active = !visible.isEmpty(); + clearSelectionButton.active = !menu.getMultiSelection().isEmpty(); + applyAllButton.active = hasConfig && !visible.isEmpty(); + // 「过滤」按钮始终可点(面板里能看到 18 空格的情况,也方便玩家确认当前资源类型吃不吃过滤器)。 + filterButton.active = true; + filterButton.setMessage(Component.translatable( + hasConfig ? "gui.useless_mod.wireless_logistics.filter_button" + : "gui.useless_mod.wireless_logistics.filter_button_none", + filterFilledCount())); + networkNameField.setEditable(true); + nameField.setEditable(hasAnchor); + // 一般介质是「释放端发起搬运」,所以「数量 / 周期」只在释放端可编辑,吸收端的禁掉以免误解。 + // 应力正好相反:它的目标转速与旋转方向是每个输出端各自的事(同一条线路上 + // 不同机器可以转不同的速度),所以这两个框配在吸收端。 + // 气压同理:这一格是「接收端要调到多少气压」,属于接收端,所以也配在吸收端。 + // (「权重」对两端都有意义:输入端之间排序、输出端之间排序都看它。) + boolean initiates = hasConfig && config.flow() == LinkFlow.RELEASE; + boolean stress = hasConfig && config.medium().family() == ResourceFamily.STRESS; + boolean pressure = hasConfig && config.medium().family() == ResourceFamily.PRESSURE; + boolean numbersEditable = (stress || pressure) ? !initiates : initiates; + weightField.active = hasConfig; + amountField.active = numbersEditable; + // 应力时这一格被方向按钮顶掉,输入框整个隐起来(它的值仍由同步逻辑维持,按钮改的就是它)。 + intervalField.active = numbersEditable && !stress; + weightField.setEditable(hasConfig); + amountField.setEditable(numbersEditable); + intervalField.setEditable(numbersEditable && !stress); + applyNumericSemantics(stress, pressure); + if (stressDirectionButton != null) { + stressDirectionButton.visible = stress && hasConfig; + stressDirectionButton.active = stress && hasConfig && numbersEditable; + if (stress) { + stressDirectionButton.setMessage(Component.translatable( + config.interval() == StaffLinkRoute.STRESS_COUNTER_CLOCKWISE + ? "gui.useless_mod.wireless_logistics.stress.counter_clockwise" + : "gui.useless_mod.wireless_logistics.stress.clockwise")); + } + } + + // 正在输入的框不要被同步覆盖,否则打字会被打断。 + if (!networkNameField.isFocused()) { + setFieldValue(networkNameField, menu.getNetworkName()); + } + if (!nameField.isFocused()) { + String name = hasAnchor ? menu.getAnchorName(anchor) : null; + setFieldValue(nameField, name == null ? "" : name); + } + if (config == null) { + if (!weightField.isFocused()) setFieldValue(weightField, ""); + if (!amountField.isFocused()) setFieldValue(amountField, ""); + if (!intervalField.isFocused()) setFieldValue(intervalField, ""); + } else { + if (!weightField.isFocused()) setFieldValue(weightField, String.valueOf(config.weight())); + if (!amountField.isFocused()) setFieldValue(amountField, + config.medium().family() == ResourceFamily.PRESSURE + ? formatPressureMbar(config.amount()) + : formatAmount(config.amount())); + if (!intervalField.isFocused()) setFieldValue(intervalField, String.valueOf(config.interval())); + } + + String signature = hasConfig + ? config.flow() + "|" + config.medium() + "|" + config.enabled() + "|" + config.side() + + "|" + menu.getSelectedRoute() + : "none|" + menu.getSelectedRoute(); + if (signature.equals(controlSignature)) { + return; + } + controlSignature = signature; + + if (!hasConfig) { + Component empty = Component.literal("-"); + flowButton.setMessage(empty); + mediumButton.setMessage(empty); + enabledButton.setMessage(empty); + sideButton.setMessage(empty); + return; + } + + flowButton.setMessage(label("flow_label", config.flow().displayName())); + mediumButton.setMessage(label("medium_label", config.medium().displayName())); + enabledButton.setMessage(label("enabled_label", + Component.translatable(config.enabled() + ? "gui.useless_mod.wireless_logistics.state_on" + : "gui.useless_mod.wireless_logistics.state_off"))); + sideButton.setMessage(label("side_label", config.side() == null + ? Component.translatable("gui.useless_mod.wireless_logistics.side.null") + : Component.translatable("gui.useless_mod.wireless_logistics.side." + + config.side().getName()))); + } + + private static void setFieldValue(EditBox field, String value) { + if (!value.equals(field.getValue())) { + field.setValue(value); + } + } + + private static Component label(String key, Component value) { + return Component.translatable("gui.useless_mod.wireless_logistics." + key, value); + } + + // ------------------------------------------------------------------ 搜索过滤 + + /** 经过搜索过滤后的锚点列表。 */ + private List visibleAnchors() { + List anchors = menu.getAnchors(); + String query = searchField == null ? "" : searchField.getValue().trim(); + if (query.isEmpty()) { + return anchors; + } + List filtered = new ArrayList<>(); + for (GlobalPos anchor : anchors) { + if (matchesSearch(anchor, query)) { + filtered.add(anchor); + } + } + return filtered; + } + + /** 自定义名、方块本名、坐标都参与匹配;方块名走 {@link PinyinSearch} 所以支持拼音。 */ + private boolean matchesSearch(GlobalPos anchor, String query) { + String custom = menu.getAnchorName(anchor); + if (custom != null && PinyinSearch.matches(custom, query)) { + return true; + } + String blockName = blockNameOf(anchor); + if (blockName != null && PinyinSearch.matches(blockName, query)) { + return true; + } + return anchor.pos().toShortString().contains(query); + } + + // ------------------------------------------------------------------ 绘制 + + @Override + protected void renderBg(GuiGraphics graphics, float partialTick, int mouseX, int mouseY) { + MachineScreenStyle.drawPanel(graphics, leftPos, topPos, imageWidth, imageHeight); + MachineScreenStyle.drawInset(graphics, leftPos + INSET_LEFT, topPos + 18, + leftPos + INSET_RIGHT, topPos + LIST_INSET_BOTTOM); + MachineScreenStyle.drawInset(graphics, leftPos + INSET_LEFT, topPos + CONFIG_INSET_TOP, + leftPos + INSET_RIGHT, topPos + 238); + MachineScreenStyle.drawSlotGroup(graphics, leftPos, topPos, 44, 254, 9, 3); + MachineScreenStyle.drawSlotGroup(graphics, leftPos, topPos, 44, 312, 9, 1); + for (Slot slot : menu.slots) { + MachineScreenStyle.drawSlotBackground(graphics, leftPos, topPos, slot); + } + } + + /** + * 画过滤面板(覆盖层)。 + * + *

只负责「画」,不负责事件:面板上的按钮是 {@code addWidget} 进来的(不在 renderables 里), + * 由 {@link #render} 在这里手动调 {@code render},点击则由 {@code mouseClicked/mouseReleased} + * 手动转发。

+ * + *

面板上的一切都按屏幕坐标画——它跟主面板位置无关,直接居中于窗口。

+ */ + private void renderFilterPanel(GuiGraphics graphics, int mouseX, int mouseY) { + int panelX = filterPanelX(); + int panelY = filterPanelY(); + boolean active = menu.isFilterActive(); + + MachineScreenStyle.drawPanel(graphics, panelX, panelY, FILTER_PANEL_WIDTH, FILTER_PANEL_HEIGHT); + + Component heading = Component.translatable("gui.useless_mod.wireless_logistics.filter_panel_title", + filterFilledCount()); + graphics.drawString(font, heading, panelX + FILTER_PANEL_PAD, panelY + 6, + MachineScreenStyle.TEXT_COLOR, false); + // 面板只有 140 宽,标题 + 提示放不下一行,所以提示换到第二行(面板高度里已经留了位置)。 + Component hint = Component.translatable("gui.useless_mod.wireless_logistics.filter_panel_hint"); + graphics.drawString(font, hint, panelX + FILTER_PANEL_PAD, panelY + 16, + MachineScreenStyle.MUTED_TEXT_COLOR, false); + + // 面板切出来时底层 UI 已经被盖掉,所以要先把面板底子落地, + // 免得下面 fill 出来的格子背景和先前那批混在一个缓冲里被重新排序。 + graphics.flush(); + + // 详细面板开着时它整片盖住了 18 格,网格不必再画(也省得物品的 z 偏移穿透上来)。 + if (!detailPanelOpen) { + renderFilterSlotBackgrounds(graphics, panelX, panelY, active); + // JEI 拖拽悬停高亮要压在槽位底色之上,所以放这里。 + renderFilterDragHover(graphics, mouseX, mouseY); + renderFilterItems(graphics); + // 角标(黑名单 / 有条件)画在物品之上,先入批再落地。 + renderFilterBadges(graphics); + graphics.flush(); + } + + closeFilterButton.render(graphics, mouseX, mouseY, 0.0F); + clearFilterButton.render(graphics, mouseX, mouseY, 0.0F); + + // 面板自己的一套悬停提示;不走 super.render(),所以得单独调。 + // 详细面板开着时它整个压住了这两个按钮,按钮提示也别画(否则会浮在详细面板上)。 + renderFilterTooltip(graphics, mouseX, mouseY); + if (!detailPanelOpen) { + renderFilterButtonTooltip(graphics, mouseX, mouseY); + } + } + + /** 18 格的底板 + 有标记那一格的顶面高光 + 详细面板正在编辑的那一格描边。 */ + private void renderFilterSlotBackgrounds(GuiGraphics graphics, int panelX, int panelY, boolean active) { + int fill = active ? MachineScreenStyle.SLOT_COLOR : 0xFF777B8D; + for (int index = 0; index < StaffLinkRoute.FILTER_LIMIT; index++) { + int x = filterSlotScreenX(index); + int y = filterSlotScreenY(index); + graphics.fill(x, y, x + FILTER_SLOT_SIZE, y + FILTER_SLOT_SIZE, fill); + if (active) { + graphics.fill(x, y, x + FILTER_SLOT_SIZE, y + 1, MachineScreenStyle.SLOT_SHADOW_COLOR); + } + } + // 详细面板正在编辑的那一格描边——面板里空格子之间留白不少,不圈一下容易看错行。 + if (detailPanelOpen && detailIndex >= 0 && detailIndex < StaffLinkRoute.FILTER_LIMIT) { + int x = filterSlotScreenX(detailIndex); + int y = filterSlotScreenY(detailIndex); + outline(graphics, x - 1, y - 1, FILTER_SLOT_SIZE + 2, FILTER_SLOT_SIZE + 2, + MULTI_SELECT_TEXT_COLOR); + } + } + + /** + * 每格的状态角标:右上角红 {@code x} = 这一格是「黑名单」;右下角青点 = 这一格带了控制条件。 + * + *

角标是必要的:格子本身只画标记,光看图标分不出「白名单」还是「黑名单」,也看不出有没有条件。

+ */ + private void renderFilterBadges(GuiGraphics graphics) { + if (!menu.isFilterActive()) { + return; + } + List mirror = menu.getFilterMirror(); + for (int index = 0; index < StaffLinkRoute.FILTER_LIMIT; index++) { + LinkFilterSlot slot = index < mirror.size() ? mirror.get(index) : LinkFilterSlot.EMPTY; + if (slot.isEmpty()) { + continue; + } + int x = filterSlotScreenX(index); + int y = filterSlotScreenY(index); + if (slot.isExcluded()) { + graphics.drawString(font, "x", x + FILTER_SLOT_SIZE - 6, y - 1, + MachineScreenStyle.ERROR_TEXT_COLOR, false); + } + if (slot.hasConditions()) { + graphics.fill(x + FILTER_SLOT_SIZE - 5, y + FILTER_SLOT_SIZE - 5, + x + FILTER_SLOT_SIZE - 1, y + FILTER_SLOT_SIZE - 1, + MULTI_SELECT_TEXT_COLOR); + } + } + } + + /** + * JEI 拖拽悬停时,把鼠标底下那一格圈出来。 + * + *

为什么不用 JEI 自己的高亮:JEI 的目标高亮画在 + * {@code ContainerScreenEvent.Render.Foreground} 里,那是 {@code super.render()} 阶段、 + * z=0 的一批;而过滤面板是在 {@code super.render()}之后、抬到 + * {@link #FILTER_PANEL_Z} 才画的 —— JEI 那圈高亮会被面板整个盖住, + * 玩家看不到任何「这一格能放」的反馈(功能其实是好的,只是看不见)。 + * 所以这里在面板同一层再画一遍。

+ */ + private void renderFilterDragHover(GuiGraphics graphics, int mouseX, int mouseY) { + if (!jeiDragActive) { + return; + } + int index = filterSlotAtScreen(mouseX, mouseY); + if (index < 0) { + return; + } + int x = filterSlotScreenX(index); + int y = filterSlotScreenY(index); + graphics.fill(x, y, x + FILTER_SLOT_SIZE, y + FILTER_SLOT_SIZE, 0x80FFFFFF); + outline(graphics, x - 1, y - 1, FILTER_SLOT_SIZE + 2, FILTER_SLOT_SIZE + 2, + MachineScreenStyle.HIGHLIGHT_COLOR); + } + + /** 屏幕坐标命中的过滤格下标;没命中返回 -1。 */ + private int filterSlotAtScreen(double mouseX, double mouseY) { + for (int index = 0; index < StaffLinkRoute.FILTER_LIMIT; index++) { + int x = filterSlotScreenX(index); + int y = filterSlotScreenY(index); + if (mouseX >= x && mouseX < x + FILTER_SLOT_SIZE + && mouseY >= y && mouseY < y + FILTER_SLOT_SIZE) { + return index; + } + } + return -1; + } + + /** 有标记的格数,标题里用。 */ + private int filterFilledCount() { + int count = 0; + for (LinkFilterSlot slot : menu.getFilterMirror()) { + if (!slot.isEmpty()) { + count++; + } + } + return count; + } + + /** 悬停面板底部两个按钮时的一句话说明。 */ + private void renderFilterButtonTooltip(GuiGraphics graphics, int mouseX, int mouseY) { + String key; + if (closeFilterButton.visible && closeFilterButton.isMouseOver(mouseX, mouseY)) { + key = "filter_close_hint"; + } else if (clearFilterButton.visible && clearFilterButton.isMouseOver(mouseX, mouseY)) { + key = "filter_clear_hint"; + } else { + return; + } + graphics.renderTooltip(font, + List.of(Component.translatable("gui.useless_mod.wireless_logistics." + key)), + Optional.empty(), mouseX, mouseY); + } + + @Override + protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) { + graphics.drawString(font, title, titleLabelX, titleLabelY, MachineScreenStyle.TEXT_COLOR, false); + graphics.drawString(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, + MachineScreenStyle.TEXT_COLOR, false); + renderNetworkLabel(graphics); + renderAnchorList(graphics); + renderNumericLabels(graphics); + renderStressStatus(graphics); + renderAeWhitelistWarning(graphics); + renderSelectionCount(graphics); + renderNotice(graphics); + } + + /** + * Req3 的界面警告:源端是 AE、目标端又没有白名单时,这条线路一个物品都不会搬。 + * + *

这条规则不看就完全看不出原因(线路是启用的、类型也对得上),所以必须在按钮那一行上方 + * 明说,否则玩家只会觉得「这机器坏了」。

+ */ + private void renderAeWhitelistWarning(GuiGraphics graphics) { + StaffLinkRoute config = menu.getSelectedConfig(); + if (config == null) { + return; + } + // blocksAeSourceWithoutWhitelist 内部已经处理了「本资源类型根本不吃过滤器」这件事 + // (能量 / 魔源恒 false —— 它们从网络出来只有一种,不需要白名单,也标记不上), + // 这里只补「源端是不是 AE」。 + if (config.medium().isAe() && config.blocksAeSourceWithoutWhitelist()) { + Component text = Component.translatable( + "gui.useless_mod.wireless_logistics.ae_whitelist_warning"); + graphics.drawString(font, text, CONTENT_LEFT, FILTER_Y - 11, + MachineScreenStyle.ERROR_TEXT_COLOR, false); + } + } + + /** 标题行右侧的一次性提示(复制/粘贴等);{@link #NOTICE_TICKS} tick 后自己消失。 */ + private void renderNotice(GuiGraphics graphics) { + if (notice == null) { + return; + } + graphics.drawString(font, notice, CONTENT_RIGHT - font.width(notice), titleLabelY, + NOTICE_COLOR, false); + } + + /** 批量编辑计数:多选非空时在按钮行上方右对齐提示选了几台。 */ + private void renderSelectionCount(GuiGraphics graphics) { + int count = menu.getMultiSelection().size(); + if (count <= 0) { + return; + } + Component text = Component.translatable( + "gui.useless_mod.wireless_logistics.multi_select_count", count); + // 和 Req3 警告同一行:一个左对齐、一个右对齐,互不打架。 + graphics.drawString(font, text, CONTENT_RIGHT - font.width(text), FILTER_Y - 11, + MULTI_SELECT_TEXT_COLOR, false); + } + + /** 第一行右侧:「网络 2/3」,右对齐到面板右边。 */ + private void renderNetworkLabel(GuiGraphics graphics) { + int[] position = staffNetworkPosition(); + Component text = Component.translatable("gui.useless_mod.wireless_logistics.network_position", + position[0] + 1, position[1]); + graphics.drawString(font, text, CONTENT_RIGHT - font.width(text), NETWORK_ROW_Y + 3, + MachineScreenStyle.MUTED_TEXT_COLOR, false); + } + + private void renderAnchorList(GuiGraphics graphics) { + List anchors = visibleAnchors(); + if (anchors.isEmpty()) { + Component hint = menu.getAnchors().isEmpty() + ? Component.translatable("gui.useless_mod.wireless_logistics.empty") + : Component.translatable("gui.useless_mod.wireless_logistics.search_no_match"); + graphics.drawString(font, hint, CONTENT_LEFT + 2, LIST_FIRST_ROW_Y, + MachineScreenStyle.MUTED_TEXT_COLOR, false); + return; + } + + for (int row = 0; row < LIST_VISIBLE_ROWS; row++) { + int index = scrollOffset + row; + if (index >= anchors.size()) { + break; + } + GlobalPos anchor = anchors.get(index); + int y = LIST_FIRST_ROW_Y + row * LIST_ROW_HEIGHT; + + // 选中态用「底色 + 左侧竖条 + 行尾 ✓」三重标记:只靠一层底色太淡, + // 一堆同名容器时根本看不出哪些进了批量编辑。 + boolean isSelected = anchor.equals(menu.getSelectedAnchor()); + boolean isMulti = menu.isMultiSelected(anchor); + if (isMulti) { + graphics.fill(CONTENT_LEFT - 2, y - 2, CONTENT_RIGHT, y + 9, MULTI_SELECT_COLOR); + } else if (isSelected) { + graphics.fill(CONTENT_LEFT - 2, y - 2, CONTENT_RIGHT, y + 9, + MachineScreenStyle.HIGHLIGHT_COLOR); + } + if (isMulti) { + graphics.fill(CONTENT_LEFT - 2, y - 2, CONTENT_LEFT, y + 9, MULTI_SELECT_TEXT_COLOR); + } else if (isSelected) { + graphics.fill(CONTENT_LEFT - 2, y - 2, CONTENT_LEFT, y + 9, SELECTED_BAR_COLOR); + } + if (isSelected && isMulti) { + // 既是批量目标、又是配置区正在编辑的那台:加一圈描边区分出来。 + outline(graphics, CONTENT_LEFT - 2, y - 2, CONTENT_WIDTH + 2, 11, SELECTED_BAR_COLOR); + } + // 行首 ▲ / ▼:调整这个容器在列表里的次序(权重相同时,靠前的先拿)。 + // 能不能点看的是「可见列表里有没有上一行 / 下一行」——搜索过滤开着时,玩家期望的 + // 就是「跟上面那一行换位」,而不是跟列表里某个看不见的邻居换。 + drawMoveButton(graphics, CONTENT_LEFT + 1, y, true, index > 0); + drawMoveButton(graphics, CONTENT_LEFT + 1 + LIST_MOVE_WIDTH, y, false, + index < anchors.size() - 1); + + // 名字前面标出「这条线路上它是发还是收」——一堆同名容器时,光看名字分不出谁在发。 + // 箭头方向按「相对这个名字所在行」看:输出端 < 指向外侧(发出去)、输入端 > 指向名字(收进来)。 + StaffLinkRoute routeConfig = menu.getConfig(anchor, menu.getSelectedRoute()); + String glyph = routeConfig == null ? "-" : routeConfig.flow() == LinkFlow.RELEASE ? "<" : ">"; + int glyphColor; + if (routeConfig == null || !routeConfig.enabled()) { + glyphColor = MachineScreenStyle.MUTED_TEXT_COLOR; + } else if (routeConfig.flow() == LinkFlow.RELEASE) { + glyphColor = 0xFF2E7D32; + } else { + glyphColor = 0xFF3B6EA5; + } + graphics.drawString(font, glyph, LIST_GLYPH_X, y, glyphColor, false); + + // 应力线路上行尾还要放一个「这一台自己」的应力量,名字预算里先给它留出位置。 + // 用紧凑写法(8.2k)—— 13px 高的行放不下长数字。 + StaffLinkStressStatusPacket.Entry stress = null; + int valueWidth = 0; + if (routeConfig != null && routeConfig.medium().family() == ResourceFamily.STRESS) { + stress = StaffLinkStressClientState.find( + menu.getNetworkId(), anchor, menu.getSelectedRoute()); + valueWidth = STRESS_ROW_VALUE_WIDTH; + } + + // 名字后面缀上坐标:多个同名容器(都叫「箱子」)光看名字根本分不出来。 + // 额外预留 8px 给行尾的批量 ✓,无条件预留,免得勾选/取消时名字左右跳。 + String coord = anchor.pos().toShortString(); + int nameBudget = Math.max(24, + CONTENT_WIDTH - LIST_TEXT_SHIFT - LIST_UNBIND_WIDTH - 16 - 8 - valueWidth + - font.width(coord)); + String name = font.plainSubstrByWidth(anchorDisplayName(anchor).getString(), nameBudget); + graphics.drawString(font, name, LIST_NAME_X, y, MachineScreenStyle.TEXT_COLOR, false); + graphics.drawString(font, coord, LIST_NAME_X + font.width(name) + 4, y, + MachineScreenStyle.MUTED_TEXT_COLOR, false); + if (stress != null) { + // 一行一台、各看各的:源看「这一台提供了多少」,目标看「这一台需要多少」。 + // 整条线路的合计在配置区的状态行里,两处分工不同。 + String value = formatStressCompact(stress.local().supplied()); + graphics.drawString(font, value, + CONTENT_RIGHT - LIST_UNBIND_WIDTH - 8 - 4 - font.width(value), y, + MachineScreenStyle.MUTED_TEXT_COLOR, false); + } + if (isMulti) { + graphics.drawString(font, "✓", CONTENT_RIGHT - LIST_UNBIND_WIDTH - 8, y, + MULTI_SELECT_TEXT_COLOR, false); + } + graphics.drawString(font, "x", CONTENT_RIGHT - LIST_UNBIND_WIDTH + 3, y, + MachineScreenStyle.ERROR_TEXT_COLOR, false); + } + } + + /** + * 画一个行首的「上移 / 下移」小按钮。 + * + *

用 ▲ / ▼ 文字字形而不是自绘三角:默认字体带了 unicode 回退,行尾的批量 ✓ 就是这么画的。

+ */ + private void drawMoveButton(GuiGraphics graphics, int x, int y, boolean up, boolean enabled) { + graphics.drawString(font, up ? "▲" : "▼", x, y, + enabled ? MachineScreenStyle.TEXT_COLOR : MachineScreenStyle.MUTED_TEXT_COLOR, false); + } + + /** + * 判断点击落在行首的哪个移动按钮上。 + * + * @return -1 上移按钮、+1 下移按钮、0 没点到按钮 + */ + private int moveButtonAt(double localX) { + int left = CONTENT_LEFT + 1; + if (localX >= left && localX < left + LIST_MOVE_WIDTH) { + return -1; + } + if (localX >= left + LIST_MOVE_WIDTH && localX < left + LIST_MOVE_WIDTH * LIST_MOVE_COUNT) { + return 1; + } + return 0; + } + + /** + * 「往上 / 往下越过一个可见行」对应的目标下标(在完整锚点列表里)。 + * + *

搜索过滤开着时,可见的上一行未必是列表里的上一个,所以要用可见邻居去换算目标下标, + * 服务端只管挪到那个位置。

+ * + * @return 目标下标;已经到可见列表的头 / 尾时返回 -1 + */ + private int moveTargetIndex(List visible, GlobalPos anchor, int direction) { + int visibleIndex = visible.indexOf(anchor); + if (visibleIndex < 0) { + return -1; + } + int neighbour = visibleIndex + direction; + if (neighbour < 0 || neighbour >= visible.size()) { + return -1; + } + return menu.getAnchors().indexOf(visible.get(neighbour)); + } + + /** 锚点显示名:玩家改过的名字优先,否则用方块本名,区块没加载时退回坐标。 */ + private Component anchorDisplayName(GlobalPos anchor) { + String custom = menu.getAnchorName(anchor); + if (custom != null) { + return Component.literal(custom); + } + String blockName = blockNameOf(anchor); + return blockName != null ? Component.literal(blockName) : Component.literal(anchor.pos().toShortString()); + } + + /** 该坐标上方块的本名;维度不匹配或区块未加载时返回 {@code null}。 */ + @Nullable + private String blockNameOf(GlobalPos anchor) { + Level level = minecraft == null ? null : minecraft.level; + if (level == null || !anchor.dimension().equals(level.dimension()) || !level.isLoaded(anchor.pos())) { + return null; + } + return level.getBlockState(anchor.pos()).getBlock().getName().getString(); + } + + /** + * 按资源类型切换数值区的语义。 + * + *

应力线路上「数量 / 周期」两个格子改叫「转速 / 方向」:转速是纯整数(不再接受 + * K / M / G 这类缩写),方向只认 1 / 2,而且方向那一格整个换成按钮。

+ * + *

气压线路上「数量」改叫「目标气压」:界面按 bar 显示与输入(-1.0 ~ 20.0), + * 内部仍存毫巴({@code amount}),因此过滤器要接受小数与负号。

+ * + *

只在语义真的变了的时候动手:这些设置会清掉输入焦点与光标位置,每帧重设等于让人没法打字。

+ */ + private void applyNumericSemantics(boolean stress, boolean pressure) { + if (stressFields == stress && pressureFields == pressure) { + return; + } + stressFields = stress; + pressureFields = pressure; + if (stress) { + numericOverrides.put(amountField, new NumericOverride( + Component.translatable("gui.useless_mod.wireless_logistics.stress.rpm_label"), + Component.translatable("gui.useless_mod.wireless_logistics.stress.rpm_hint"))); + amountField.setMaxLength(6); + amountField.setFilter(value -> value.isEmpty() || value.matches("\\d{0,6}")); + intervalField.setMaxLength(1); + intervalField.setFilter(value -> value.isEmpty() || value.matches("[12]?")); + intervalField.visible = false; + } else if (pressure) { + numericOverrides.put(amountField, new NumericOverride( + Component.translatable("gui.useless_mod.wireless_logistics.pressure.target_label"), + Component.translatable("gui.useless_mod.wireless_logistics.pressure.target_hint"))); + amountField.setMaxLength(6); + amountField.setFilter(value -> value.isEmpty() + || value.matches("-?\\d{0,2}(\\.\\d{0,2})?")); + intervalField.setMaxLength(6); + intervalField.setFilter(value -> value.isEmpty() || value.matches("\\d{0,5}")); + intervalField.visible = true; + } else { + numericOverrides.remove(amountField); + amountField.setMaxLength(24); + amountField.setFilter(ScaledEnergyAmount::isValidInput); + intervalField.setMaxLength(6); + intervalField.setFilter(value -> value.isEmpty() || value.matches("\\d{0,5}")); + intervalField.visible = true; + } + } + + private Component numericLabel(NumericSpec spec) { + NumericOverride override = numericOverrides.get(spec.field()); + return override == null ? spec.label() : override.label(); + } + + private Component numericHint(NumericSpec spec) { + NumericOverride override = numericOverrides.get(spec.field()); + return override == null ? spec.hint() : override.hint(); + } + + private void renderNumericLabels(GuiGraphics graphics) { + for (NumericSpec spec : numericFields) { + if (!spec.field().visible) { + continue; + } + Component label = numericLabel(spec); + int labelX = spec.field().getX() - leftPos - font.width(label) - 4; + graphics.drawString(font, label, labelX, ROW_FOURTH_Y + 4, + MachineScreenStyle.MUTED_TEXT_COLOR, false); + if (!spec.field().active) { + // 不生效的输入框压一层暗色,一眼能看出来它不可编辑。 + graphics.fill(spec.field().getX(), spec.field().getY(), + spec.field().getX() + spec.field().getWidth(), + spec.field().getY() + spec.field().getHeight(), 0x66000000); + } + } + // 应力时周期框被方向按钮顶掉,它的标签在这里单独补一次:按钮上写的是当前方向, + // 而「方向」这两个字才是告诉玩家这一格是什么的那个标签。 + if (stressFields && stressDirectionButton != null && stressDirectionButton.visible) { + Component direction = Component.translatable( + "gui.useless_mod.wireless_logistics.stress.direction_label"); + int labelX = stressDirectionButton.getX() - leftPos - font.width(direction) - 4; + graphics.drawString(font, direction, labelX, ROW_FOURTH_Y + 4, + MachineScreenStyle.MUTED_TEXT_COLOR, false); + } + } + + /** + * 应力线路的运行状态:源网络能提供多少、目标网络需要多少、这一端现在怎么样。 + * + *

这些是服务端每 20 tick 推下来的运行时数字,不属于线路配置,因此整网快照里没有它们。 + * 还没收到过就什么都不画——显示一排 0 只会让人以为线路坏了。

+ * + *

第二行只在出问题时才换成醒目的琥珀色:玩家最需要一眼看到的就是「为什么没转」。

+ */ + private void renderStressStatus(GuiGraphics graphics) { + StaffLinkRoute config = menu.getSelectedConfig(); + GlobalPos anchor = menu.getSelectedAnchor(); + if (config == null || anchor == null || config.medium().family() != ResourceFamily.STRESS) { + return; + } + StaffLinkStressStatusPacket.Entry entry = + StaffLinkStressClientState.find(menu.getNetworkId(), anchor, config.route()); + if (entry == null) { + // 线路关着的时候服务端根本不会为它记状态(桥只跑已启用的线路),快照里自然没有它。 + // 这时补一句「已关闭」——否则玩家看到一片空白,很容易以为功能坏了。 + if (!config.enabled()) { + graphics.drawString(font, Component.translatable( + "gui.useless_mod.wireless_logistics.stress.status", + Component.translatable( + "gui.useless_mod.wireless_logistics.stress.state.disabled")), + CONTENT_LEFT, STRESS_STATUS_Y, MachineScreenStyle.MUTED_TEXT_COLOR, false); + } + return; + } + // 第一行三个数,两种角色口径一致: + // 自身 —— 这一台自己提供 / 消耗多少(源看它的容量 × 生成转速,目标看它的耗力 × 转速) + // 网络总计 —— 这条线路上所有源网络能提供的容量合计 + // 剩余 —— 这条链路还能借出去多少(源网络自身负载扣掉之后的富余) + // 「自身」与「网络总计」分开显示,是因为同一张网络上并了好几个源 / 好几台机器时, + // 这两个数不是一回事。 + Component numbers = Component.translatable( + "gui.useless_mod.wireless_logistics.stress.status_breakdown", + formatStress(entry.local().supplied()), + formatStress(entry.line().supplied()), + formatStress(entry.line().extra())); + graphics.drawString(font, numbers, CONTENT_LEFT, STRESS_STATUS_Y, + MachineScreenStyle.MUTED_TEXT_COLOR, false); + + Component state = Component.translatable("gui.useless_mod.wireless_logistics.stress.status", + stressStateName(entry.state())); + graphics.drawString(font, state, CONTENT_LEFT, STRESS_STATUS_Y + 9, + isStressWarning(entry.state()) ? STRESS_WARN_COLOR : MachineScreenStyle.MUTED_TEXT_COLOR, false); + } + + /** + * 这个状态码是「出问题了」还是「只是说明情况」。 + * + *

服务端把状态码当普通字符串发过来,所以这里只能按字面认。 + * {@code fed_elsewhere}(应力由其它无线输出补足)与 {@code self_fed}(网络自身有动力) + * 都说明机器转得好好的,用醒目的琥珀色会让人以为坏了。

+ */ + private static boolean isStressWarning(@Nullable String state) { + if (state == null || state.isEmpty()) { + return false; + } + return !"fed_elsewhere".equals(state) && !"self_fed".equals(state); + } + + /** 应力量的显示:太大就写无穷,整数就不带小数点,否则保留一位小数。 */ + private static String formatStress(float value) { + if (value >= 1.0E9F) { + return "\u221e"; + } + // 650.0 写成「650」而不是「650.0」——「注入 0.0」这种显示看起来像是坏了。 + if (value >= 100.0F || Math.abs(value - Math.round(value)) < 0.05F) { + return String.valueOf(Math.round(value)); + } + return String.format("%.1f", value); + } + + /** 容器列表行尾的应力量:紧凑写法(8.2k / 1.5M),一行只有 13px 高,放不下长数字。 */ + private static String formatStressCompact(float value) { + if (value >= 1.0E9F) { + return "\u221e"; + } + if (value >= 1_000_000.0F) { + return String.format("%.1fM", value / 1_000_000.0F); + } + if (value >= 1_000.0F) { + return String.format("%.1fk", value / 1_000.0F); + } + return String.valueOf(Math.round(value)); + } + + /** 状态码 → 可读文字;空串表示一切正常。 */ + private static Component stressStateName(@Nullable String state) { + return state == null || state.isEmpty() + ? Component.translatable("gui.useless_mod.wireless_logistics.stress.state.ok") + : Component.translatable("gui.useless_mod.wireless_logistics.stress.state." + state); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + if (filterPanelOpen) { + // 先骨架渲染:super.render() 里除了底层界面,还负责触发 JEI 的 + // ContainerScreenEvent.Render.Background / Foreground —— JEI 的原料侧栏就在那两处画。 + // 之前这里直接 return,等于把 JEI 整个跳过,侧栏自然不显示(也没法往过滤格里拖)。 + super.render(graphics, mouseX, mouseY, partialTick); + renderFilterOverlay(graphics, mouseX, mouseY); + return; + } + super.render(graphics, mouseX, mouseY, partialTick); + // 过滤格住在覆盖层面板里,`filterSlotScreenX/Y` 给的是**屏幕坐标**,面板关着时那些 + // 坐标指向的是主界面别的控件。所以这里**不能**再画过滤格物品——否则关掉面板后, + // 标记会孤零零地停在一级菜单的容器列表上(用户实测反馈过)。 + PressableAE2Button selected = routeButtons[menu.getSelectedRoute()]; + if (menu.getSelectedAnchor() != null) { + outline(graphics, selected.getX() - 1, selected.getY() - 1, + selected.getWidth() + 2, selected.getHeight() + 2, MachineScreenStyle.TEXT_COLOR); + } + if (mediumMenuOpen) { + // 下拉盖住了下面的控件:这时候再弹它们的悬停提示只会互相打架。 + // 连容器槽的 tooltip 也一起跳过——鼠标其实停在下拉上,不该弹出底下那个槽的说明。 + renderMediumMenu(graphics, mouseX, mouseY); + return; + } + renderAnchorTooltip(graphics, mouseX, mouseY); + renderFilterTooltip(graphics, mouseX, mouseY); + renderNumericTooltip(graphics, mouseX, mouseY); + renderNetworkTooltip(graphics, mouseX, mouseY); + renderRouteTooltip(graphics, mouseX, mouseY); + renderTooltip(graphics, mouseX, mouseY); + } + + /** + * 过滤面板打开时的收尾渲染(在 {@code super.render()} 之后调用)。 + * + *

不再跳过 {@code super.render()}。原因是 JEI 的原料侧栏是在 + * {@code ContainerScreenEvent.Render.Background / Foreground} 里画的,而这两个事件由 + * {@code super.render()} 触发——一旦跳过,JEI 侧栏就整个不显示,玩家也就没法从 JEI 往 + * 过滤格里拖物品(而这正是过滤面板存在的意义)。一级菜单的预览弹窗同样是 + * {@code AbstractContainerScreen},它盖满全屏所以看不出侧栏消失。

+ * + *

改为 {@code renderMediumMenu} 那套已经验证过的做法两件套:

+ *
    + *
  1. {@code flush()} —— 把底层界面已入批的内容(槽位物品、堆叠数字、输入框)先提交定型, + * 否则后画的填充会跟它们排进同一批、按图层重排,遮不住;
  2. + *
  3. 整段抬 z 到 {@link #FILTER_PANEL_Z} —— GUI 的深度测试是开着的,槽位物品在 z=150、 + * 堆叠数字在 z=200,留在 z=0 的填充比它们都远,片段会被深度测试直接丢掉。
  4. + *
+ * + *

没有压暗层(v2 修):早先会在面板四周铺一圈半透明黑,但面板只占屏幕一小块, + * 四块矩形与背包 / 侧栏的边界会露出一道道硬边,看着很乱(用户反馈「压暗层乱七八糟的」)。 + * 面板本身是 {@code drawPanel} 的不透明底,不压暗也照样能看清。

+ */ + private void renderFilterOverlay(GuiGraphics graphics, int mouseX, int mouseY) { + // 底层已入批的内容先落地,之后画的面板才能稳定压在上面。 + graphics.flush(); + graphics.pose().pushPose(); + graphics.pose().translate(0.0F, 0.0F, FILTER_PANEL_Z); + renderFilterPanel(graphics, mouseX, mouseY); + graphics.pose().popPose(); + // 详细面板再抬一层 z,压过滤面板。 + if (detailPanelOpen) { + graphics.pose().pushPose(); + graphics.pose().translate(0.0F, 0.0F, DETAIL_PANEL_Z); + renderDetailPanel(graphics, mouseX, mouseY); + graphics.pose().popPose(); + } + // 收尾再 flush 一次,保证面板立刻定型,不会被本帧后面的内容盖掉。 + graphics.flush(); + // 背包槽的悬停提示:它本来由 super.render() 画,但那时还没画面板,会被盖住。 + // 面板开着时从背包取物品是常规操作,提示必须在最上面重画一遍。 + if (inventorySlotAt(mouseX, mouseY) != null) { + super.renderTooltip(graphics, mouseX, mouseY); + } + // 一次性提示(比如「模式非法」)本来画在主界面标题行,被面板整个盖住了 —— 面板开着时 + // 在这里再画一遍,否则玩家完全看不到反馈,只会觉得「输入了没反应 / 存不上」。 + if (notice != null) { + graphics.drawString(font, notice, (width - font.width(notice)) / 2, 6, NOTICE_COLOR, true); + } + // 光标上拿着的物品同理:vanilla 只在 super.render() 里画它(z 也低于面板), + // 从背包拿起物品来标记时会被面板整个盖住、看不见手上是什么。这里抬到最上面重画。 + renderCarriedItem(graphics, mouseX, mouseY); + } + + /** 把「光标上拿着的物品」画在覆盖层最上面(含堆叠数字)。 */ + private void renderCarriedItem(GuiGraphics graphics, int mouseX, int mouseY) { + ItemStack carried = menu.getCarried(); + if (carried.isEmpty()) { + return; + } + int x = mouseX - 8; + int y = mouseY - 8; + graphics.pose().pushPose(); + graphics.pose().translate(0.0F, 0.0F, CARRIED_ITEM_Z); + graphics.renderItem(carried, x, y); + graphics.renderItemDecorations(font, carried, x, y); + graphics.pose().popPose(); + } + + /** + * 画详细编辑面板。 + * + *

跟过滤面板同一套约定:底板不透明、控件手动 render(它们只进了 children, + * 进 renderables 会被 {@code super.render()} 画到下面去)。

+ */ + private void renderDetailPanel(GuiGraphics graphics, int mouseX, int mouseY) { + int panelX = detailPanelX(); + int panelY = detailPanelY(); + MachineScreenStyle.drawPanel(graphics, panelX, panelY, DETAIL_PANEL_WIDTH, DETAIL_PANEL_HEIGHT); + + LinkFilterSlot slot = currentFilterSlot(detailIndex); + graphics.drawString(font, Component.translatable( + "gui.useless_mod.wireless_logistics.filter_slot_title", detailIndex + 1), + panelX + DETAIL_PANEL_PAD, panelY + DETAIL_TITLE_Y, + MachineScreenStyle.TEXT_COLOR, false); + + // 输出端 / 输入端 标签 + graphics.drawString(font, Component.translatable( + "gui.useless_mod.wireless_logistics.filter_cond_out_label"), + panelX + DETAIL_LABEL_X, panelY + DETAIL_OUT_ROW_Y + 5, + MachineScreenStyle.MUTED_TEXT_COLOR, false); + graphics.drawString(font, Component.translatable( + "gui.useless_mod.wireless_logistics.filter_cond_in_label"), + panelX + DETAIL_LABEL_X, panelY + DETAIL_IN_ROW_Y + 5, + MachineScreenStyle.MUTED_TEXT_COLOR, false); + + // 三个槽位(A 标记、输出端 B、输入端 B)的底板 + for (int which = 0; which < 3; which++) { + int x = detailSlotScreenX(which); + int y = detailSlotScreenY(which); + graphics.fill(x, y, x + DETAIL_SLOT_SIZE, y + DETAIL_SLOT_SIZE, + MachineScreenStyle.SLOT_COLOR); + graphics.fill(x, y, x + DETAIL_SLOT_SIZE, y + 1, MachineScreenStyle.SLOT_SHADOW_COLOR); + } + graphics.flush(); + + // A 槽内容 + 两个 B 槽内容 + renderDetailMarker(graphics, detailSlotScreenX(0), detailSlotScreenY(0), slot); + renderDetailControl(graphics, detailSlotScreenX(1), detailSlotScreenY(1), slot.outCond()); + renderDetailControl(graphics, detailSlotScreenX(2), detailSlotScreenY(2), slot.inCond()); + graphics.flush(); + + // 控件手动 render(覆盖层阶段不在 super.render() 的渲染序列里) + detailPatternField.render(graphics, mouseX, mouseY, 0.0F); + detailOutValueField.render(graphics, mouseX, mouseY, 0.0F); + detailInValueField.render(graphics, mouseX, mouseY, 0.0F); + detailExcludeButton.render(graphics, mouseX, mouseY, 0.0F); + detailOutOpButton.render(graphics, mouseX, mouseY, 0.0F); + detailInOpButton.render(graphics, mouseX, mouseY, 0.0F); + detailCloseButton.render(graphics, mouseX, mouseY, 0.0F); + + // 面板自己的一套悬停提示 + renderDetailTooltip(graphics, mouseX, mouseY); + } + + /** A 标记槽的内容:模式字形 / 流体贴图 / 物品图标。 */ + private void renderDetailMarker(GuiGraphics graphics, int x, int y, LinkFilterSlot slot) { + if (slot.isEmpty()) { + return; + } + if (slot.isPattern()) { + renderPatternMarker(graphics, x, y, slot); + } else if (slot.isFluid()) { + renderFluidMarker(graphics, x, y, slot.fluid()); + } else { + graphics.renderItem(slot.item(), x, y); + } + } + + /** B 控制材料槽的内容:留空画「自身」,否则画物品 / 流体。 */ + private void renderDetailControl(GuiGraphics graphics, int x, int y, LinkFilterCondition cond) { + if (cond.isOff()) { + return; + } + if (cond.isSelf()) { + graphics.drawString(font, Component.translatable( + "gui.useless_mod.wireless_logistics.filter_cond_self"), + x, y + 5, MachineScreenStyle.MUTED_TEXT_COLOR, false); + } else if (cond.isFluidControl()) { + renderFluidMarker(graphics, x, y, cond.fluid()); + } else if (cond.isItemControl()) { + graphics.renderItem(cond.item(), x, y); + } + } + + /** 详细面板的悬停提示:三个槽位 + 模式框 + 两个按钮。 */ + private void renderDetailTooltip(GuiGraphics graphics, int mouseX, int mouseY) { + List lines = null; + if (inDetailSlot(mouseX, mouseY, 0)) { + lines = List.of(Component.translatable( + "gui.useless_mod.wireless_logistics.filter_hint")); + } else if (inDetailSlot(mouseX, mouseY, 1) || inDetailSlot(mouseX, mouseY, 2)) { + boolean input = inDetailSlot(mouseX, mouseY, 2); + lines = List.of(Component.translatable( + "gui.useless_mod.wireless_logistics." + + (input ? "filter_cond_in_hint" : "filter_cond_out_hint"))); + } else if (detailPatternField.visible + && detailPatternField.isMouseOver(mouseX, mouseY)) { + lines = List.of(Component.translatable( + "gui.useless_mod.wireless_logistics.filter_pattern_hint")); + } else if (detailExcludeButton.visible && detailExcludeButton.isMouseOver(mouseX, mouseY)) { + lines = List.of(Component.translatable( + "gui.useless_mod.wireless_logistics.filter_exclude_hint")); + } + if (lines != null) { + graphics.renderTooltip(font, lines, Optional.empty(), mouseX, mouseY); + } + } + + /** 屏幕坐标下的玩家背包槽;没命中返回 {@code null}。 */ + @Nullable + private Slot inventorySlotAt(double mouseX, double mouseY) { + for (Slot slot : menu.slots) { + int x = leftPos + slot.x; + int y = topPos + slot.y; + if (mouseX >= x && mouseX < x + 16 && mouseY >= y && mouseY < y + 16) { + return slot; + } + } + return null; + } + + /** + * 画资源类型下拉。 + * + *

铺在按钮正下方,选项来自 {@link LinkMedium#supported()}:装了化学品 / 魔源 / 通量之后 + * 类型能到十种,一路轮换过去太费手,直接列出来点。

+ */ + private void renderMediumMenu(GuiGraphics graphics, int mouseX, int mouseY) { + if (mediumOptions.isEmpty()) { + return; + } + int x = leftPos + MEDIUM_BUTTON_X; + int y = topPos + MEDIUM_MENU_Y; + + // 先把此前攒下的批处理内容落地。 + // + // GuiGraphics 是「按图层批处理、最后统一提交」的,不是按调用顺序即时绘制: + // 背包槽位、标签、输入框都在 super.render() 里进了同一批,即便我们在它之后才画, + // 提交时仍会按图层顺序排到弹出层前面(表现就是物品和输入框盖在下拉菜单上)。 + // 先 flush 一次把它们定型,弹出层随后单独成一批。 + graphics.flush(); + + // 再整体抬 z。flush() 只解决「谁后画」,解决不了深度测试: + // 槽位物品画在 z=150、堆叠数字画在 z=200(见 GuiGraphics#renderItem 与 + // #renderItemDecorations),下拉留在 z=0 会比它们都远,片段照样被丢掉。 + // 两者都要做:先 flush 定序,再抬 z 让深度测试放行。 + graphics.pose().pushPose(); + graphics.pose().translate(0.0F, 0.0F, MEDIUM_MENU_Z); + MachineScreenStyle.drawPanel(graphics, x, y, MEDIUM_MENU_WIDTH, + mediumOptions.size() * MEDIUM_MENU_ROW_HEIGHT); + + LinkMedium current = menu.getSelectedMedium(); + int hovered = mediumMenuIndexAt(mouseX - leftPos, mouseY - topPos); + for (int index = 0; index < mediumOptions.size(); index++) { + LinkMedium option = mediumOptions.get(index); + int rowY = y + index * MEDIUM_MENU_ROW_HEIGHT; + if (option == current) { + // 当前这一项给个底色,玩家一眼看到自己在哪一档。 + graphics.fill(x + 2, rowY, x + MEDIUM_MENU_WIDTH - 2, + rowY + MEDIUM_MENU_ROW_HEIGHT, MachineScreenStyle.HIGHLIGHT_COLOR); + } else if (index == hovered) { + graphics.fill(x + 2, rowY, x + MEDIUM_MENU_WIDTH - 2, + rowY + MEDIUM_MENU_ROW_HEIGHT, MachineScreenStyle.SLOT_COLOR); + } + Component text = option.displayName(); + graphics.drawString(font, text, x + (MEDIUM_MENU_WIDTH - font.width(text)) / 2, + rowY + 3, + option == current ? MachineScreenStyle.TEXT_COLOR : MachineScreenStyle.SUBTLE_TEXT_COLOR, + false); + } + graphics.pose().popPose(); + // 再落一次地,让弹出层立刻定型——否则本帧后面还有内容入批时会重新排到它前面。 + graphics.flush(); + } + + /** 命中的下拉项下标;没命中返回 -1。 */ + private int mediumMenuIndexAt(double localX, double localY) { + if (localX < MEDIUM_BUTTON_X || localX >= MEDIUM_BUTTON_X + MEDIUM_MENU_WIDTH + || localY < MEDIUM_MENU_Y) { + return -1; + } + int index = (int) ((localY - MEDIUM_MENU_Y) / MEDIUM_MENU_ROW_HEIGHT); + return index >= 0 && index < mediumOptions.size() ? index : -1; + } + + /** 悬停线路按钮行时说明 Ctrl+C / Ctrl+V 能整条复制粘贴配置——否则这个快捷键没人发现得了。 */ + private void renderRouteTooltip(GuiGraphics graphics, int mouseX, int mouseY) { + double localX = mouseX - leftPos; + double localY = mouseY - topPos; + if (localY < ROUTE_ROW_Y || localY > ROUTE_ROW_Y + ROW_HEIGHT + || localX < CONTENT_LEFT || localX > CONTENT_RIGHT) { + return; + } + graphics.renderTooltip(font, + List.of(Component.translatable( + "gui.useless_mod.wireless_logistics.config_copy_hint", + menu.getSelectedRoute())), + Optional.empty(), mouseX, mouseY); + } + + private void renderFilterItems(GuiGraphics graphics) { + if (!menu.isFilterActive()) { + return; + } + List mirror = menu.getFilterMirror(); + for (int index = 0; index < mirror.size() && index < StaffLinkRoute.FILTER_LIMIT; index++) { + LinkFilterSlot slot = mirror.get(index); + if (slot.isEmpty()) { + continue; + } + int x = filterSlotScreenX(index); + int y = filterSlotScreenY(index); + if (slot.isPattern()) { + renderPatternMarker(graphics, x, y, slot); + } else if (slot.isFluid()) { + renderFluidMarker(graphics, x, y, slot.fluid()); + } else { + // 槽位 18×18 就是原版槽位大小,物品正好填满、不用内缩。 + graphics.renderItem(slot.item(), x, y); + } + } + } + + /** + * 把 {@code #tag} / 通配符模式画进过滤槽。 + * + *

画符号而不是画「第一个匹配到的物品」:模式的含义是一整类,挑一个代表物反而会让人 + * 以为这一格只过滤那一个。{@code #} 开头的画井号、通配符画星号,玩家一眼分得清两种模式。

+ * + *

标签没被任何物品使用时用错误色画:这是最常见的写法错误(少打一个 {@code s}、 + * 忘了 {@code c:} 前缀),不管的话过滤器会静默失效——什么都不搬,而玩家看不出为什么。

+ */ + private void renderPatternMarker(GuiGraphics graphics, int x, int y, LinkFilterSlot slot) { + String text = slot.pattern(); + if (text == null) { + return; + } + LinkFilterPattern parsed = LinkFilterPattern.parse(text); + int color = parsed != null && parsed.isTag() && !parsed.isResolvable() + ? MachineScreenStyle.ERROR_TEXT_COLOR + : MachineScreenStyle.TEXT_COLOR; + String glyph = parsed != null && parsed.isTag() ? "#" : "*"; + // 槽位只有 16px:字形贴左上、尾巴压在下面一行(字号不缩,16px 高度塞得下两行 9px 的字体)。 + graphics.drawString(font, glyph, x + 1, y + 1, color, false); + // 井号右侧再点一下模式里的关键字,不然一堆格子全是「#」根本分不出哪个是哪个。 + String tail = patternTail(text); + if (!tail.isEmpty()) { + graphics.drawString(font, tail, x, y + 8, color, false); + } + } + + /** + * 从模式里取出「认得出是哪个」的那一段短字。 + * + *

只取末段并截到 6 个字符:格子只有 16px 宽,画不下一整个 {@code #c:ingots/copper}, + * 而这个尾巴({@code copper})比 {@code ingots} 更能区分相邻两格。

+ */ + private static String patternTail(String text) { + int cut = Math.max(text.lastIndexOf('/'), text.lastIndexOf(':')); + String tail = cut >= 0 && cut + 1 < text.length() ? text.substring(cut + 1) : text; + // 通配符模式去掉前导的 *,不然尾巴全是星号。 + while (!tail.isEmpty() && (tail.charAt(0) == '*' || tail.charAt(0) == '#')) { + tail = tail.substring(1); + } + return tail.length() > 6 ? tail.substring(0, 6) : tail; + } + + /** + * 把流体本身画进过滤槽:取它的静止贴图、按流体的染色画满一格。 + * + *

刻意不画桶。槽里标记的就是这种流体,画个桶会让玩家以为过滤的是「水桶这个物品」—— + * 那正是这次要修掉的混淆。

+ */ + private void renderFluidMarker(GuiGraphics graphics, int x, int y, FluidStack fluid) { + if (fluid.isEmpty() || fluid.getFluid() == Fluids.EMPTY || minecraft == null) { + return; + } + IClientFluidTypeExtensions extensions = IClientFluidTypeExtensions.of(fluid.getFluid()); + TextureAtlasSprite sprite = minecraft.getTextureAtlas(InventoryMenu.BLOCK_ATLAS) + .apply(extensions.getStillTexture(fluid)); + + int tint = extensions.getTintColor(fluid); + float alpha = ((tint >> 24) & 0xFF) / 255.0F; + graphics.setColor( + ((tint >> 16) & 0xFF) / 255.0F, + ((tint >> 8) & 0xFF) / 255.0F, + (tint & 0xFF) / 255.0F, + alpha == 0.0F ? 1.0F : alpha); + // 流体贴图是 16×16,槽位也是 16×16,直接铺满。 + graphics.blit(x, y, 0, 16, 16, sprite); + graphics.setColor(1.0F, 1.0F, 1.0F, 1.0F); + } + + /** 悬停锚点行时给出「维度 + 坐标」的完整信息,方便确认改名的到底是哪一个。 */ + private void renderAnchorTooltip(GuiGraphics graphics, int mouseX, int mouseY) { + int row = anchorRowAt(mouseX - leftPos, mouseY - topPos); + if (row < 0) { + return; + } + List anchors = visibleAnchors(); + int index = scrollOffset + row; + if (index >= anchors.size()) { + return; + } + GlobalPos anchor = anchors.get(index); + List lines = new ArrayList<>(4); + lines.add(anchorDisplayName(anchor)); + lines.add(Component.literal( + anchor.dimension().location() + " " + anchor.pos().toShortString())); + lines.add(Component.translatable("gui.useless_mod.wireless_logistics.highlight_hint")); + lines.add(Component.translatable("gui.useless_mod.wireless_logistics.multi_select_hint")); + lines.add(Component.translatable("gui.useless_mod.wireless_logistics.anchor_order_hint")); + if (menu.isMultiSelected(anchor)) { + lines.add(Component.translatable( + "gui.useless_mod.wireless_logistics.multi_select_active")); + } + graphics.renderTooltip(font, lines, Optional.empty(), mouseX, mouseY); + } + + /** 过滤器槽的说明:标记的是资源本身(流体就是流体、物品就是物品),得讲清楚。 */ + private void renderFilterTooltip(GuiGraphics graphics, int mouseX, int mouseY) { + // 详细面板开着时它压在最上面,格子提示会被它盖掉一半(玩家实测反馈)。 + // 这时候由详细面板自己出提示,格子提示整个不画。 + if (!filterPanelOpen || detailPanelOpen || !menu.isFilterActive()) { + return; + } + int index = filterSlotAt(mouseX, mouseY); + if (index < 0) { + return; + } + LinkFilterSlot marker = menu.getFilterMirror().get(index); + List lines = new ArrayList<>(6); + LinkFilterPattern pattern = marker.isPattern() ? LinkFilterPattern.parse(marker.pattern()) : null; + if (pattern != null) { + lines.add(Component.literal(pattern.displayName())); + lines.add(Component.translatable(pattern.isTag() + ? "gui.useless_mod.wireless_logistics.filter_pattern_tag" + : "gui.useless_mod.wireless_logistics.filter_pattern_glob")); + if (pattern.isTag() && !pattern.isResolvable()) { + // 标签没被任何资源用到:静默失效是最坏的结果,必须标红。 + lines.add(Component.translatable( + "gui.useless_mod.wireless_logistics.filter_pattern_unresolved") + .withStyle(ChatFormatting.RED)); + } + if (pattern.isTag() && chemicalRoute()) { + // 化学品没有跨模组的通用标签体系,#tag 对它永远匹配不上——这条得说出来, + // 否则玩家会以为是标签写错了。 + lines.add(Component.translatable( + "gui.useless_mod.wireless_logistics.filter_pattern_unsupported") + .withStyle(ChatFormatting.RED)); + } + } else if (marker.isFluid()) { + lines.add(marker.fluid().getDisplayName()); + } else if (marker.isItem()) { + lines.add(marker.item().getHoverName()); + } + // 方向:白名单 / 黑名单。 + lines.add(Component.translatable(marker.isExcluded() + ? "gui.useless_mod.wireless_logistics.filter_exclude_on" + : "gui.useless_mod.wireless_logistics.filter_exclude_off")); + appendConditionLines(lines, marker); + lines.add(Component.translatable("gui.useless_mod.wireless_logistics.filter_hint")); + lines.add(Component.translatable("gui.useless_mod.wireless_logistics.filter_detail_hint")); + graphics.renderTooltip(font, lines, Optional.empty(), mouseX, mouseY); + } + + /** + * 把两条控制条件写成 tooltip 行;没启用的不加。 + * + *

数值走 {@link #limitText},跟框里显示的写法一致——不然会出现 + * 「框里写 1K、提示里蹦出 1000」这种对不上的情况,玩家会以为两边是两个数。

+ */ + private static void appendConditionLines(List lines, LinkFilterSlot marker) { + appendConditionLine(lines, marker.outCond(), "filter_cond_out_label"); + appendConditionLine(lines, marker.inCond(), "filter_cond_in_label"); + } + + private static void appendConditionLine(List lines, LinkFilterCondition cond, String labelKey) { + if (cond.isOff()) { + return; + } + Component who = cond.isSelf() + ? Component.translatable("gui.useless_mod.wireless_logistics.filter_cond_self") + : (cond.isFluidControl() ? cond.fluid().getDisplayName() : cond.item().getHoverName()); + lines.add(Component.translatable("gui.useless_mod.wireless_logistics." + labelKey) + .append(Component.literal(": ")) + .append(who) + .append(Component.literal(" ")) + .append(opLabel(cond.op())) + .append(Component.literal(" " + limitText(cond.value())))); + } + + /** 当前选中的线路搬的是不是化学品;判不了(没选中)时返回 false。 */ + private boolean chemicalRoute() { + StaffLinkRoute config = menu.getSelectedConfig(); + return config != null && config.medium().family() == ResourceFamily.CHEMICAL; + } + + /** + * 悬停网络名 / 切换按钮时说明这一行是干什么的。 + * + *

顺带讲清归属:网络挂在玩家(组队后是队伍)名下,和手上这把杖无关。玩家最容易 + * 误解的就是「换把杖、把杖放进箱子,网络还在不在」。

+ */ + private void renderNetworkTooltip(GuiGraphics graphics, int mouseX, int mouseY) { + double localX = mouseX - leftPos; + double localY = mouseY - topPos; + if (localY < NETWORK_ROW_Y || localY > NETWORK_ROW_Y + SMALL_FIELD_HEIGHT) { + return; + } + boolean overField = localX >= NETWORK_FIELD_X && localX < NETWORK_FIELD_X + NETWORK_FIELD_WIDTH; + boolean overPrev = localX >= PREV_BUTTON_X && localX < PREV_BUTTON_X + NETWORK_SWITCH_WIDTH; + boolean overNext = localX >= NEXT_BUTTON_X && localX < NEXT_BUTTON_X + NETWORK_SWITCH_WIDTH; + if (!overField && !overPrev && !overNext) { + return; + } + graphics.renderTooltip(font, List.of( + Component.translatable(overField + ? "gui.useless_mod.wireless_logistics.network_name_hint" + : "gui.useless_mod.wireless_logistics.network_switch_hint"), + Component.translatable("gui.useless_mod.wireless_logistics.network_owned_hint")), + Optional.empty(), mouseX, mouseY); + } + + /** 悬停数值框时给出完整含义与可填范围(英文标签是短名,靠这里补全)。 */ + private void renderNumericTooltip(GuiGraphics graphics, int mouseX, int mouseY) { + for (NumericSpec spec : numericFields) { + if (!spec.field().visible || !spec.field().isMouseOver(mouseX, mouseY)) { + continue; + } + boolean overridden = numericOverrides.containsKey(spec.field()); + List lines = new ArrayList<>(4); + lines.add(numericLabel(spec)); + lines.add(numericHint(spec)); + if (!spec.field().active) { + lines.add(Component.translatable(stressFields + ? "gui.useless_mod.wireless_logistics.stress.input_side_hint" + : "gui.useless_mod.wireless_logistics.release_only_hint")); + } + if (pressureFields && spec.field() == amountField) { + // 气压的「数量」是目标气压,界面单位是 bar,范围就是气动的物理区间。 + lines.add(Component.translatable("gui.useless_mod.wireless_logistics.pressure.range")); + } else { + // 「数量」与应力转速都没有静态上限,只提示下限。 + lines.add(overridden || spec.scaled() + ? Component.translatable("gui.useless_mod.wireless_logistics.range_min", spec.min()) + : Component.translatable("gui.useless_mod.wireless_logistics.range", + spec.min(), spec.max())); + } + graphics.renderTooltip(font, lines, Optional.empty(), mouseX, mouseY); + return; + } + } + + private static void outline(GuiGraphics graphics, int x, int y, int width, int height, int color) { + graphics.fill(x, y, x + width, y + 1, color); + graphics.fill(x, y + height - 1, x + width, y + height, color); + graphics.fill(x, y, x + 1, y + height, color); + graphics.fill(x + width - 1, y, x + width, y + height, color); + } + + // ------------------------------------------------------------------ 交互 + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (keyCode == GLFW.GLFW_KEY_ESCAPE && detailPanelOpen) { + // ESC 逐层退:先关详细面板,再关过滤面板,最后才是整个界面。 + // 强关(ESC 是「退出」语义,模式非法也走人,提示已经弹过了)。 + closeDetailPanel(true); + return true; + } + if (keyCode == GLFW.GLFW_KEY_ESCAPE && filterPanelOpen) { + // 面板是覆盖层,ESC 先关它、别顺手把整个界面关了。 + setFilterPanelOpen(false); + return true; + } + if (keyCode == GLFW.GLFW_KEY_ESCAPE && mediumMenuOpen) { + // 先收下拉,别顺手把整个界面关了。 + mediumMenuOpen = false; + return true; + } + if ((keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) && anyFieldFocused()) { + // 回车 = 「打完了」:先提交全部输入框,再收面板(applyEdits 里已经提交过详细面板, + // 所以这里强关,免得模式非法时弹两次提示又关不掉)。 + applyEdits(); + closeDetailPanel(true); + setFocused(null); + return true; + } + if (keyCode != GLFW.GLFW_KEY_ESCAPE) { + for (EditBox field : fields()) { + if (field.isFocused() + && (field.keyPressed(keyCode, scanCode, modifiers) || field.canConsumeInput())) { + // 有输入框聚焦时 Ctrl+C/V 是「复制/粘贴文本」,轮不到配置剪贴板。 + return true; + } + } + } + // 面板开着时,配置剪贴板与其他主界面快捷键都不该生效(玩家在编辑过滤,看不到底层)。 + if (filterPanelOpen) { + return true; + } + // 到这儿说明没有输入框在抢按键,Ctrl+C / Ctrl+V 才归配置剪贴板。 + if (Screen.hasControlDown()) { + if (keyCode == GLFW.GLFW_KEY_C) { + copyConfig(); + return true; + } + if (keyCode == GLFW.GLFW_KEY_V) { + pasteConfig(); + return true; + } + } + return super.keyPressed(keyCode, scanCode, modifiers); + } + + /** + * Ctrl+C:把当前线路的配置存进会话剪贴板。 + * + *

连过滤器一起复制——只复制数值、把过滤器落下,玩家还得再拖一遍标记,那就不叫复制了。

+ */ + private void copyConfig() { + StaffLinkRoute config = menu.getSelectedConfig(); + if (config == null) { + return; + } + copiedConfig = config; + showNotice(Component.translatable("gui.useless_mod.wireless_logistics.config_copied")); + } + + /** + * Ctrl+V:把剪贴板里的配置贴到当前选中线路。 + * + *

走 {@link StaffLinkMenu#applyRoute},所以多选非空时会一次贴给所有被选锚点—— + * 「配好一台 → 圈选其余 → Ctrl+V」就是一条批量套用路径。

+ */ + private void pasteConfig() { + StaffLinkRoute source = copiedConfig; + GlobalPos anchor = menu.getSelectedAnchor(); + if (source == null || anchor == null || !menu.isAnchorBound(anchor)) { + return; + } + // 先把输入框里没提交的值落下去,否则它会在随后回来的快照里覆盖掉刚贴上的配置。 + applyEdits(); + menu.applyRoute(new StaffLinkRoute( + anchor, menu.getSelectedRoute(), + source.enabled(), source.flow(), source.medium(), source.amount(), source.interval(), + source.side(), source.weight(), source.filter())); + updateControls(); + showNotice(Component.translatable("gui.useless_mod.wireless_logistics.config_pasted")); + } + + /** 显示一条一次性提示,{@link #NOTICE_TICKS} tick 后自己消失。 */ + private void showNotice(Component message) { + notice = message; + noticeTicks = NOTICE_TICKS; + } + + private List fields() { + List all = new ArrayList<>(numericFields.size() + 6); + all.add(networkNameField); + all.add(searchField); + all.add(nameField); + for (NumericSpec spec : numericFields) { + all.add(spec.field()); + } + all.add(detailPatternField); + all.add(detailOutValueField); + all.add(detailInValueField); + return all; + } + + /** 当前有焦点的输入框;都没有时返回 {@code null}。 */ + @Nullable + private EditBox focusedField() { + for (EditBox field : fields()) { + if (field.isFocused()) { + return field; + } + } + return null; + } + + private boolean anyFieldFocused() { + return focusedField() != null; + } + + private boolean isOverAnyField(double mouseX, double mouseY) { + for (EditBox field : fields()) { + if (field.isMouseOver(mouseX, mouseY)) { + return true; + } + } + return false; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + // 面板占满整个窗口,打开时它最优先:底层界面一个控件也不该收到这一下。 + if (filterPanelOpen) { + return handleFilterPanelClick(mouseX, mouseY, button); + } + + double localX = mouseX - leftPos; + double localY = mouseY - topPos; + + // 下拉展开时先吃掉这一次点击:点在选项上就选中,点在别处就收起。 + // 这也是下拉的常规行为——第一次点只负责关掉它,不会顺手点到下面的控件。 + if (mediumMenuOpen) { + int picked = mediumMenuIndexAt(localX, localY); + mediumMenuOpen = false; + if (picked >= 0 && picked < mediumOptions.size()) { + LinkMedium medium = mediumOptions.get(picked); + edit(config -> withMedium(config, medium)); + } + return true; + } + + int row = anchorRowAt(localX, localY); + if (row >= 0) { + List anchors = visibleAnchors(); + int index = scrollOffset + row; + if (index < anchors.size()) { + GlobalPos anchor = anchors.get(index); + // 行首 ▲ / ▼:把这一台挪到「上一个 / 下一个可见行」的位置。 + int moveDirection = moveButtonAt(localX); + if (moveDirection != 0) { + int target = moveTargetIndex(anchors, anchor, moveDirection); + if (target >= 0) { + menu.moveAnchorTo(anchor, target); + } + return true; + } + // 优先级:解绑(x) > Shift 范围 > Ctrl 加减 > 普通单选。 + if (localX >= CONTENT_RIGHT - LIST_UNBIND_WIDTH) { + menu.detach(anchor); + clampScroll(); + } else if (Screen.hasShiftDown() && lastClickedAnchor != null) { + selectRange(anchor); + } else if (Screen.hasControlDown()) { + // Ctrl + 点击:把这一台加进 / 移出批量编辑的集合。 + menu.toggleMultiSelection(anchor); + menu.setSelection(anchor, menu.getSelectedRoute()); + ensureRouteConfig(); + } else { + // 普通点击是「重新单选」:顺手清掉上一次的批量选择,语义才不粘。 + menu.clearMultiSelection(); + menu.setSelection(anchor, menu.getSelectedRoute()); + ensureRouteConfig(); + } + lastClickedAnchor = anchor; + updateControls(); + } + return true; + } + + // 点到按钮等控件之前,先把输入框里已填但未提交的值提交掉。 + if (!isOverAnyField(mouseX, mouseY) && anyFieldFocused()) { + applyEdits(); + } + return super.mouseClicked(mouseX, mouseY, button); + } + + /** + * 过滤面板里的点击。 + * + *

顺序有讲究:先给详细面板(它压在最上面),再给过滤面板的两个按钮(它们只进了 children, + * 得手动转发),然后是玩家背包槽(要能从这里取物品来标记),最后才是 18 格。

+ */ + private boolean handleFilterPanelClick(double mouseX, double mouseY, int button) { + if (detailPanelOpen) { + return handleDetailPanelClick(mouseX, mouseY, button); + } + if (closeFilterButton.mouseClicked(mouseX, mouseY, button) + || clearFilterButton.mouseClicked(mouseX, mouseY, button)) { + return true; + } + + // 玩家背包槽:面板开着时也允许正常「拿起 / 放下」——不然只能靠 JEI 拖,或者先把物品 + // 拿在手上再开面板。转发给框架走标准的槽位点击(拾取到手、放下、右键分半等)。 + if (inventorySlotAt(mouseX, mouseY) != null) { + return super.mouseClicked(mouseX, mouseY, button); + } + + int filterIndex = filterSlotAt(mouseX, mouseY); + if (filterIndex < 0) { + // 点在面板空白处:让输入框失焦,但不关面板。 + setFocused(null); + return true; + } + if (!menu.isFilterActive()) { + return true; + } + if (button == GLFW.GLFW_MOUSE_BUTTON_RIGHT) { + // 右键:打开这一格的详细编辑面板(模式、白名单/黑名单、两条控制条件都在里面)。 + openDetailPanel(filterIndex); + return true; + } + if (button != GLFW.GLFW_MOUSE_BUTTON_LEFT) { + return true; + } + // 左键:按线路资源类型解释手上的东西——流体线路从容器里取出流体本身, + // 解释不了就不动这一格(绝不把不匹配的东西塞进去)。空手 = 清掉这一格。 + ItemStack carried = menu.getCarried(); + if (carried.isEmpty()) { + menu.setFilterSlot(filterIndex, LinkFilterSlot.EMPTY); + } else { + LinkFilterSlot marker = StaffLinkFilters.fromItem(menu.getSelectedMedium(), carried); + if (marker != null) { + // 换标记时保留这一格原有的白名单/黑名单与条件。 + menu.setFilterSlot(filterIndex, currentFilterSlot(filterIndex).withMarker(marker)); + } + } + updateControls(); + return true; + } + + /** + * Shift+点击:从上次点击的锚点到当前锚点,按可见列表整段选中。 + * + *

用锚点而不是行下标当端点:搜索过滤与滚动都会改变可见集合,记死下标会错位; + * 上次那台已经被过滤掉或解绑时({@code indexOf} 返回 -1)退化成只选当前这一台。

+ */ + private void selectRange(GlobalPos anchor) { + List anchors = visibleAnchors(); + int to = anchors.indexOf(anchor); + if (to < 0) { + return; + } + int from = lastClickedAnchor == null ? -1 : anchors.indexOf(lastClickedAnchor); + if (from < 0) { + menu.setMultiSelection(List.of(anchor)); + } else { + menu.setMultiSelection(anchors.subList(Math.min(from, to), Math.max(from, to) + 1)); + } + menu.setSelection(anchor, menu.getSelectedRoute()); + ensureRouteConfig(); + } + + /** 「全选」:把当前列表(受搜索过滤)里的锚点整批加入批量编辑。 */ + private void selectAllVisible() { + applyEdits(); + menu.setMultiSelection(visibleAnchors()); + ensureRouteConfig(); + updateControls(); + } + + /** 「清空」:退出批量编辑,只留单选。 */ + private void clearSelection() { + menu.clearMultiSelection(); + updateControls(); + } + + /** + * 「应用到全部同名容器」:把当前线路配置刷给跟当前这台显示名一样的全部锚点。 + * + *

只作用于同名而不是整个列表:一台机器上挂几条线路很常见,整片刷过去会把别的线路配置 + * 一起冲掉。「同名」按界面显示的那个名字算——玩家改过就用改过的, + * 没改过就是方块本名,和列表里看到的一模一样({@link #anchorDisplayName})。

+ * + *

走 {@link StaffLinkMenu#applyRouteTo},不改变玩家当前的多选状态。

+ */ + private void applyAllSameName() { + applyEdits(); + StaffLinkRoute config = menu.getSelectedConfig(); + GlobalPos selected = menu.getSelectedAnchor(); + if (config == null || selected == null) { + return; + } + Component wanted = anchorDisplayName(selected); + List targets = new ArrayList<>(); + for (GlobalPos anchor : menu.getAnchors()) { + if (anchorDisplayName(anchor).equals(wanted)) { + targets.add(anchor); + } + } + if (targets.isEmpty()) { + return; + } + menu.applyRouteTo(targets, config); + updateControls(); + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) { + // 面板开着时框选文本全靠这里:跳过 super,就得自己转发,否则框里的选区永远拖不出来。 + if (filterPanelOpen) { + EditBox focused = focusedField(); + if (focused != null && focused.canConsumeInput()) { + return focused.mouseDragged(mouseX, mouseY, button, dragX, dragY); + } + return true; + } + return super.mouseDragged(mouseX, mouseY, button, dragX, dragY); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + // 面板按钮只进了 children,事件得手动转发,不然按下去会一直停在「按下」的视觉状态。 + if (closeFilterButton != null) { + closeFilterButton.releaseVisualState(); + clearFilterButton.releaseVisualState(); + detailExcludeButton.releaseVisualState(); + detailOutOpButton.releaseVisualState(); + detailInOpButton.releaseVisualState(); + detailCloseButton.releaseVisualState(); + // EditBox 的 releaseVisualState 由它自己管,但松手这一刻要通知到, + // 否则拖了一半的文本选区不会收尾。 + EditBox focused = focusedField(); + if (filterPanelOpen && focused != null) { + focused.mouseReleased(mouseX, mouseY, button); + } + } + flowButton.releaseVisualState(); + mediumButton.releaseVisualState(); + enabledButton.releaseVisualState(); + sideButton.releaseVisualState(); + prevNetworkButton.releaseVisualState(); + nextNetworkButton.releaseVisualState(); + newNetworkButton.releaseVisualState(); + dissolveButton.releaseVisualState(); + selectAllButton.releaseVisualState(); + clearSelectionButton.releaseVisualState(); + applyAllButton.releaseVisualState(); + for (PressableAE2Button routeButton : routeButtons) { + routeButton.releaseVisualState(); + } + return super.mouseReleased(mouseX, mouseY, button); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double scrollY) { + if (filterPanelOpen) { + // 面板是覆盖层,滚动轮不该偷偷滚下面的锚点列表。 + return true; + } + double localX = mouseX - leftPos; + double localY = mouseY - topPos; + if (localY >= LIST_TOP && localY <= LIST_BOTTOM && localX >= CONTENT_LEFT && localX <= CONTENT_RIGHT) { + int maxScroll = Math.max(0, visibleAnchors().size() - LIST_VISIBLE_ROWS); + scrollOffset = Mth.clamp(scrollOffset + (scrollY > 0 ? -1 : 1), 0, maxScroll); + return true; + } + return super.mouseScrolled(mouseX, mouseY, scrollY); + } + + private int anchorRowAt(double localX, double localY) { + if (localX < CONTENT_LEFT || localX > CONTENT_RIGHT) { + return -1; + } + if (localY < LIST_TOP || localY > LIST_BOTTOM) { + return -1; + } + int row = (int) ((localY - LIST_TOP) / LIST_ROW_HEIGHT); + return row >= 0 && row < LIST_VISIBLE_ROWS ? row : -1; + } + + /** 命中的过滤格(参数是屏幕坐标,面板是居中的覆盖层);没命中返回 -1。 */ + private int filterSlotAt(double screenX, double screenY) { + for (int index = 0; index < StaffLinkRoute.FILTER_LIMIT; index++) { + int x = filterSlotScreenX(index); + int y = filterSlotScreenY(index); + if (screenX >= x && screenX < x + FILTER_SLOT_SIZE + && screenY >= y && screenY < y + FILTER_SLOT_SIZE) { + return index; + } + } + return -1; + } + + private void clampScroll() { + int maxScroll = Math.max(0, visibleAnchors().size() - LIST_VISIBLE_ROWS); + scrollOffset = Mth.clamp(scrollOffset, 0, maxScroll); + } + + // ---- 过滤面板几何 + + /** + * 面板左上角。 + * + *

横向居中即可({@link #FILTER_PANEL_WIDTH} 只有 140,右边必然留得下 JEI 侧栏)。 + * 纵向在居中的基础上上移一点:面板下方紧挨着玩家背包,抬上去能给背包留出更多 + * 可见空间(用户要求「位置也上移」)。

+ */ + private int filterPanelX() { + return Math.max(2, (width - FILTER_PANEL_WIDTH) / 2); + } + + private int filterPanelY() { + return Math.max(4, (height - FILTER_PANEL_HEIGHT) / 2 - FILTER_PANEL_RAISE); + } + + /** 第 index 格槽位相对面板的偏移(网格整体在面板内横向居中)。 */ + private static int filterSlotPanelX(int index) { + return FILTER_GRID_X + (index % FILTER_COLUMNS) * FILTER_CELL_COL_STEP; + } + + private static int filterSlotPanelY(int index) { + return FILTER_GRID_Y + (index / FILTER_COLUMNS) * FILTER_CELL_ROW_STEP; + } + + /** + * 过滤格槽位的屏幕坐标(绘制、命中检测、JEI 拖拽都用它)。 + * + *

注意是屏幕坐标而不是 {@code leftPos/topPos} 的偏移:过滤格住在覆盖层面板里, + * 跟主面板的位置无关。

+ */ + public int filterSlotScreenX(int index) { + return filterPanelX() + filterSlotPanelX(index); + } + + public int filterSlotScreenY(int index) { + return filterPanelY() + filterSlotPanelY(index); + } + + // ---- 详细编辑面板几何 + + private int detailPanelX() { + return Math.max(2, (width - DETAIL_PANEL_WIDTH) / 2); + } + + private int detailPanelY() { + return Math.max(4, (height - DETAIL_PANEL_HEIGHT) / 2); + } + + /** + * 详细面板里三个可放置槽的屏幕坐标。 + * + * @param which {@code 0} = A 标记槽、{@code 1} = 输出端 B 槽、{@code 2} = 输入端 B 槽 + */ + public int detailSlotScreenX(int which) { + int local = (which == 1 || which == 2) ? DETAIL_COND_SLOT_X : DETAIL_MARKER_X; + return detailPanelX() + local; + } + + public int detailSlotScreenY(int which) { + int local = switch (which) { + case 1 -> DETAIL_OUT_ROW_Y; + case 2 -> DETAIL_IN_ROW_Y; + default -> DETAIL_MARKER_ROW_Y; + }; + return detailPanelY() + local; + } + + public static int detailSlotSize() { + return DETAIL_SLOT_SIZE; + } + + /** 详细面板是否开着(给 JEI 拖拽切换靶点用)。 */ + public boolean isDetailPanelOpen() { + return detailPanelOpen; + } + + /** 详细面板正在编辑的格号;没打开时 -1。 */ + public int getDetailIndex() { + return detailIndex; + } + + /** 面板底部「关闭」按钮的屏幕坐标(两个按钮整体在面板内居中)。 */ + private int filterPanelCloseX() { + return filterPanelX() + FILTER_PANEL_BUTTON_X; + } + + private int filterPanelCloseY() { + return filterPanelY() + FILTER_PANEL_BUTTON_Y; + } + + /** 面板底部「清空」按钮的屏幕坐标。 */ + private int filterPanelClearX() { + return filterPanelX() + FILTER_PANEL_BUTTON_X + + FILTER_PANEL_BUTTON_WIDTH + FILTER_PANEL_BUTTON_GAP; + } + + private int filterPanelClearY() { + return filterPanelY() + FILTER_PANEL_BUTTON_Y; + } + + public static int filterSlotSize() { + return FILTER_SLOT_SIZE; + } + + public static int filterSlotCount() { + return StaffLinkRoute.FILTER_LIMIT; + } + + /** + * 过滤面板(覆盖层)是否开着。 + * + *

给 JEI 拖拽用:{@link #filterSlotScreenX}/{@link #filterSlotScreenY} 给的是覆盖层的 + * 屏幕坐标,面板关着的时候那些坐标是没有意义的,拖放目标必须整个取消。

+ */ + public boolean isFilterPanelOpen() { + return filterPanelOpen; + } + + /** + * JEI 拖拽是否正在进行(由 {@code StaffLinkGhostHandler} 在 {@code doStart=true} 时点亮, + * 面板关闭或拖拽结束时熄灭)。 + * + *

只有它为真时才画拖拽悬停高亮 —— 否则平时鼠标划过格子也变色,玩家会以为「这里能放」。

+ */ + private boolean jeiDragActive; + + public void setJeiDragActive(boolean active) { + this.jeiDragActive = active; + } + + /** + * 当前是「第几张 / 共几张」网络。 + * + *

网络列表挂在归属者(玩家或队伍)名下、存在服务端存档里,客户端手里没有这份 + * 列表,所以位置信息由同步包一起带下来(见 {@code StaffLinkSyncPacket})。

+ */ + private int[] staffNetworkPosition() { + return new int[]{menu.getNetworkIndex(), menu.getNetworkCount()}; + } + + // ------------------------------------------------------------------ 小工具 + + private static Direction nextSide(Direction current) { + for (int i = 0; i < SIDE_ORDER.length; i++) { + if (SIDE_ORDER[i] == current) { + return SIDE_ORDER[(i + 1) % SIDE_ORDER.length]; + } + } + return SIDE_ORDER[0]; + } + + private static StaffLinkRoute withFlow(StaffLinkRoute config, LinkFlow flow) { + return new StaffLinkRoute(config.anchor(), config.route(), config.enabled(), flow, + config.medium(), config.amount(), config.interval(), config.side(), + config.weight(), config.filter()); + } + + private static StaffLinkRoute withMedium(StaffLinkRoute config, LinkMedium medium) { + boolean stress = medium.family() == ResourceFamily.STRESS; + // 换到应力时顺手给一对有意义的初值:原来的「16 / 5」在应力语义下读作「16 RPM、方向 5」, + // 而方向只认 1 / 2,5 会被服务端收敛掉——不如直接给对,免得玩家看到配置被悄悄改了。 + boolean pressure = medium.family() == ResourceFamily.PRESSURE; + // 气压同理:原来的「16」在气压语义下读作 0.016 bar(几乎没压力),直接给 2 bar 更合理。 + return new StaffLinkRoute(config.anchor(), config.route(), config.enabled(), config.flow(), + medium, + stress ? StaffLinkRoute.STRESS_DEFAULT_RPM + : pressure ? StaffLinkRoute.PRESSURE_DEFAULT_MBAR : config.amount(), + stress ? StaffLinkRoute.STRESS_CLOCKWISE : config.interval(), + config.side(), config.weight(), pruneFilter(config.filter(), medium)); + } + + /** + * 换资源类型时丢掉不适用的过滤标记。 + * + *

不丢的话,物品线路上留下的物品标记会跟着走到流体线路上——它们一个流体也匹配不上, + * 过滤器就变成「什么都不搬」,而玩家完全看不出原因。丢掉之后过滤器为空 = 不限制, + * 行为安全;真要限制再重新拖一个就行。

+ * + *

丢的时候保留格位(换成空格而不是把后面的往前挤),否则换个资源类型整排标记 + * 就跟着挪位置,玩家会以为配置被改乱了。

+ */ + private static List pruneFilter(List filter, LinkMedium medium) { + boolean fluidRoute = medium.family() == ResourceFamily.FLUID; + List kept = new ArrayList<>(filter.size()); + for (LinkFilterSlot slot : filter) { + boolean keep = slot.isEmpty() || (fluidRoute ? slot.isFluid() : slot.isItem()); + if (!keep) { + kept.add(LinkFilterSlot.EMPTY); + continue; + } + // 标记留得下,但条件里的控制材料可能换了族:清掉(回到「测 A 自身」), + // 否则会留下一个永远量不到、永远不成立的条件。 + kept.add(slot.withConditions(pruneCondition(slot.outCond(), fluidRoute), + pruneCondition(slot.inCond(), fluidRoute))); + } + return kept; + } + + /** 换资源类型时清掉族不匹配的控制材料(保留方向与数值)。 */ + private static LinkFilterCondition pruneCondition(LinkFilterCondition cond, boolean fluidRoute) { + if (cond.isOff() || cond.isSelf()) { + return cond; + } + boolean compatible = fluidRoute ? cond.isFluidControl() : cond.isItemControl(); + return compatible ? cond : cond.withoutControl(); + } + + private static StaffLinkRoute withEnabled(StaffLinkRoute config, boolean enabled) { + return new StaffLinkRoute(config.anchor(), config.route(), enabled, config.flow(), + config.medium(), config.amount(), config.interval(), config.side(), + config.weight(), config.filter()); + } + + private static StaffLinkRoute withSide(StaffLinkRoute config, Direction side) { + return new StaffLinkRoute(config.anchor(), config.route(), config.enabled(), config.flow(), + config.medium(), config.amount(), config.interval(), side, + config.weight(), config.filter()); + } + + private static StaffLinkRoute withNumbers(StaffLinkRoute config, int weight, long amount, int interval) { + return new StaffLinkRoute(config.anchor(), config.route(), config.enabled(), config.flow(), + config.medium(), amount, interval, config.side(), + weight, config.filter()); + } + + /** 顺时针 ↔ 逆时针。 */ + private static int nextStressDirection(int current) { + return current == StaffLinkRoute.STRESS_COUNTER_CLOCKWISE + ? StaffLinkRoute.STRESS_CLOCKWISE + : StaffLinkRoute.STRESS_COUNTER_CLOCKWISE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/gui/ThermostatScreen.java b/src/main/java/com/sorrowmist/useless/client/gui/ThermostatScreen.java new file mode 100644 index 00000000..aa3fb457 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/gui/ThermostatScreen.java @@ -0,0 +1,285 @@ +package com.sorrowmist.useless.client.gui; + +import com.sorrowmist.useless.content.blockentities.ThermostatBlockEntity; +import com.sorrowmist.useless.network.ThermostatSetPacket; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.glfw.GLFW; + +import java.util.ArrayList; +import java.util.List; +import java.util.OptionalLong; + +/** + * 「温度调节器」方块的配置界面。 + * + *

由权杖 Alt + 右键 打开(见 {@code ClientEventBusSubscriber#tryThermostatGesture})。 + * 刻意不做成容器界面:没有物品槽、也不需要 JEI 侧栏,普通 {@link Screen} 就够。

+ * + *

外观

+ * + *

沿用模组统一的 AE 风格:{@link MachineScreenStyle#drawPanel} 画底板、 + * {@link PressableAE2Button} 画按钮、{@link AE2StyleTextField} 画输入框,与 + * {@code ModeWheelScreen} 同一套写法。

+ * + *

底板在 {@link #renderBackground} 里画,不要在 {@code render} 里再调一次。 + * 由本界面的 {@code render()} 在控件之前调用 {@code renderBackground()} + * (已反编译确认),多调一次等于把遮罩叠两层、整个界面发黑。

+ * + *

值的权威

+ * + *

服务端是唯一权威:界面读客户端方块实体的同步值,改动通过 + * {@link ThermostatSetPacket} 提交,服务端校验后再同步回来。

+ */ +public final class ThermostatScreen extends Screen { + + private static final int PANEL_WIDTH = 260; + private static final int PANEL_HEIGHT = 136; + + /** 快速步进档位:覆盖「微调」与「大幅改」两种用法。 */ + private static final int[] QUICK_STEPS = {-1000, -100, -10, 10, 100, 1000}; + + private final BlockPos pos; + private final List buttons = new ArrayList<>(); + + private int leftPos; + private int topPos; + private AE2StyleTextField temperatureField; + private PressableAE2Button toggleButton; + + /** 最近一次由服务端同步下来的温度,用来判断输入框是否需要刷新。 */ + private long lastSyncedTemperature = Long.MIN_VALUE; + /** 最近一次写进输入框的文本;与它不同即视为「玩家改过」。 */ + private String syncedTemperatureText = ""; + /** 正在由代码改写输入框时抑制 responder,避免把同步当成玩家输入。 */ + private boolean updatingField; + /** 输入框里的内容与同步值不一致。 */ + private boolean fieldDirty; + + public ThermostatScreen(BlockPos pos) { + super(Component.translatable("gui.useless_mod.thermostat.title")); + this.pos = pos; + } + + @Nullable + private ThermostatBlockEntity thermostat() { + if (minecraft == null || minecraft.level == null) { + return null; + } + return minecraft.level.getBlockEntity(pos) instanceof ThermostatBlockEntity thermostat + ? thermostat : null; + } + + @Override + protected void init() { + super.init(); + buttons.clear(); + leftPos = (width - PANEL_WIDTH) / 2; + topPos = (height - PANEL_HEIGHT) / 2; + + temperatureField = new AE2StyleTextField(font, + leftPos + PANEL_WIDTH - 8 - 80, topPos + 22, 80, 14, + Component.translatable("gui.useless_mod.thermostat.temperature")); + temperatureField.setMaxLength(24); + // 与「数量 / 产出速率」同一套:吃 K / M / G / T / P / E 后缀,1M = 1,000,000 K。 + temperatureField.setFilter(ScaledEnergyAmount::isValidInput); + temperatureField.setHint(Component.translatable("gui.useless_mod.thermostat.temperature")); + temperatureField.setResponder(value -> { + if (!updatingField) { + fieldDirty = !value.equals(syncedTemperatureText); + } + }); + addRenderableWidget(temperatureField); + + int buttonWidth = 36; + int gap = 3; + int totalWidth = QUICK_STEPS.length * buttonWidth + (QUICK_STEPS.length - 1) * gap; + int startX = leftPos + (PANEL_WIDTH - totalWidth) / 2; + for (int index = 0; index < QUICK_STEPS.length; index++) { + int step = QUICK_STEPS[index]; + addButton(startX + index * (buttonWidth + gap), topPos + 42, buttonWidth, 14, + Component.literal((step > 0 ? "+" : "") + step), + () -> adjust(step)); + } + + toggleButton = addButton(leftPos + 8, topPos + 60, PANEL_WIDTH - 16, 16, + toggleLabel(), this::toggleEnabled); + + addButton(leftPos + (PANEL_WIDTH - 70) / 2, topPos + PANEL_HEIGHT - 22, 70, 16, + Component.translatable("gui.useless_mod.thermostat.done"), this::onClose); + + syncField(true); + } + + private PressableAE2Button addButton(int x, int y, int width, int height, + Component message, Runnable action) { + PressableAE2Button button = addRenderableWidget(new PressableAE2Button( + x, y, width, height, message, ignored -> action.run())); + buttons.add(button); + return button; + } + + @Override + public void tick() { + super.tick(); + syncField(false); + if (toggleButton != null) { + toggleButton.setMessage(toggleLabel()); + } + } + + /** + * 把服务端的温度同步进输入框。 + * + *

正在输入(聚焦)或玩家已改过({@code fieldDirty})时不动它,否则打字会被打断。

+ */ + private void syncField(boolean force) { + if (temperatureField == null) { + return; + } + long temperature = currentTemperature(); + if ((force || (!temperatureField.isFocused() && !fieldDirty)) + && temperature != lastSyncedTemperature) { + setFieldValue(temperature); + } + } + + private void setFieldValue(long temperature) { + syncedTemperatureText = String.valueOf(temperature); + lastSyncedTemperature = Math.max(0L, temperature); + updatingField = true; + temperatureField.setValue(syncedTemperatureText); + updatingField = false; + fieldDirty = false; + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + // Forge 1.20.1 Screen.render draws widgets only; paint the panel first. + renderBackground(graphics); + super.render(graphics, mouseX, mouseY, partialTick); + + graphics.drawString(font, title, leftPos + 8, topPos + 8, + MachineScreenStyle.TEXT_COLOR, false); + graphics.drawString(font, Component.translatable("gui.useless_mod.thermostat.kelvin"), + leftPos + 8, topPos + 26, MachineScreenStyle.TEXT_COLOR, false); + + long kelvin = parseTemperature(); + graphics.drawString(font, Component.translatable("gui.useless_mod.thermostat.celsius", + String.format("%.2f", kelvin - 273.15D)), + leftPos + 8, topPos + 84, MachineScreenStyle.TEXT_COLOR, false); + graphics.drawString(font, Component.translatable("gui.useless_mod.thermostat.hint"), + leftPos + 8, topPos + 96, MachineScreenStyle.MUTED_TEXT_COLOR, false); + } + + /** + * 遮罩 + 底板。 + * + *

只在这里画,不要同时在 {@code render} 里再画一遍——本界面的 + * {@code render()} 显式调用本方法,重复绘制会让整个界面叠两层暗色。

+ */ + @Override + public void renderBackground(GuiGraphics graphics) { + graphics.fill(0, 0, width, height, 0x66000000); + MachineScreenStyle.drawPanel(graphics, leftPos, topPos, PANEL_WIDTH, PANEL_HEIGHT); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if ((keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) + && temperatureField != null && temperatureField.isFocused()) { + commit(parseTemperature(), isEnabled()); + setFocused(null); + return true; + } + if (keyCode != GLFW.GLFW_KEY_ESCAPE && temperatureField != null + && (temperatureField.keyPressed(keyCode, scanCode, modifiers) + || temperatureField.canConsumeInput())) { + return true; + } + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + for (PressableAE2Button widget : buttons) { + widget.releaseVisualState(); + } + return super.mouseReleased(mouseX, mouseY, button); + } + + @Override + public void onClose() { + // 关界面前把输入框里没提交的值也落下去,免得玩家以为改了却没生效。 + if (fieldDirty) { + commit(parseTemperature(), isEnabled()); + } + super.onClose(); + } + + @Override + public boolean isPauseScreen() { + // 不暂停:让玩家当场看到相邻机器的温度变化。 + return false; + } + + private void adjust(int step) { + long current = parseTemperature(); + // 防溢出:接近 long 上限时 +1000 会绕回负数。 + long next = step > 0 + ? (current > Long.MAX_VALUE - step ? Long.MAX_VALUE : current + step) + : Math.max(0L, current + step); + setFieldValue(next); + commit(next, isEnabled()); + } + + private void toggleEnabled() { + commit(parseTemperature(), !isEnabled()); + } + + private boolean isEnabled() { + ThermostatBlockEntity thermostat = thermostat(); + return thermostat != null && thermostat.isEnabled(); + } + + private void commit(long temperature, boolean enabled) { + ThermostatBlockEntity thermostat = thermostat(); + if (thermostat != null + && thermostat.getTemperature() == temperature + && thermostat.isEnabled() == enabled) { + return; + } + PacketDistributor.sendToServer(new ThermostatSetPacket(pos, temperature, enabled)); + } + + /** + * 读输入框里的温度(开尔文)。 + * + *

吃 {@code K / M / G / T / P / E} 后缀(与「数量 / 产出速率」同一套 + * {@link ScaledEnergyAmount}),上限就是 {@code long} 的上限; + * 解析不了就回落到当前生效值。

+ */ + private long parseTemperature() { + if (temperatureField == null) { + return currentTemperature(); + } + OptionalLong parsed = ScaledEnergyAmount.parse(temperatureField.getValue(), Long.MAX_VALUE); + return parsed.isPresent() ? parsed.getAsLong() : currentTemperature(); + } + + private long currentTemperature() { + ThermostatBlockEntity thermostat = thermostat(); + return thermostat == null + ? ThermostatBlockEntity.DEFAULT_TEMPERATURE : thermostat.getTemperature(); + } + + private Component toggleLabel() { + return Component.translatable(isEnabled() + ? "gui.useless_mod.thermostat.enabled" + : "gui.useless_mod.thermostat.disabled"); + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/network/ClientPacketHandlers.java b/src/main/java/com/sorrowmist/useless/client/network/ClientPacketHandlers.java new file mode 100644 index 00000000..e7a42b34 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/network/ClientPacketHandlers.java @@ -0,0 +1,182 @@ +package com.sorrowmist.useless.client.network; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.client.gui.ChainGroupScreen; +import com.sorrowmist.useless.client.gui.ModeWheelScreen; +import com.sorrowmist.useless.client.gui.StaffLinkScreen; +import com.sorrowmist.useless.client.render.AlloyFurnaceErrorHighlightRenderer; +import com.sorrowmist.useless.client.render.StaffLinkHighlightRenderer; +import com.sorrowmist.useless.client.stafflink.StaffLinkStressClientState; +import com.sorrowmist.useless.content.blockentities.AdvancedAlloyFurnaceBlockEntity; +import com.sorrowmist.useless.content.blockentities.multiblock.MultiblockAlloyFurnaceCoreBlockEntity; +import com.sorrowmist.useless.content.menus.MultiblockAlloyFurnaceMenu; +import com.sorrowmist.useless.core.config.BeefToolProtectionManager; +import com.sorrowmist.useless.core.config.ChainGroupManager; +import com.sorrowmist.useless.data.BeefToolLayout; +import com.sorrowmist.useless.network.AETaskProgressPacket; +import com.sorrowmist.useless.network.BeefInvulnerabilitySyncPacket; +import com.sorrowmist.useless.network.BeefToolLayoutResultPacket; +import com.sorrowmist.useless.network.BeefToolLayoutSyncPacket; +import com.sorrowmist.useless.network.AlloyFurnaceErrorHighlightPacket; +import com.sorrowmist.useless.network.StaffLinkHighlightPacket; +import com.sorrowmist.useless.network.StaffLinkSyncPacket; +import com.sorrowmist.useless.network.StaffLinkStressStatusPacket; +import net.minecraft.client.Minecraft; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.Pose; +import net.minecraft.world.entity.player.Player; +import org.jetbrains.annotations.Nullable; + +/** + * 客户端专用的载荷(Packet)处理逻辑。 + * + *

所有会直接引用 {@code net.minecraft.client.*} 的代码都必须放在这里,而不能留在 + * {@code com.sorrowmist.useless.network} 的公共载荷类中:在专用服务器(DEDICATED_SERVER)上, + * 那些类会在 {@code RegisterPayloadHandlersEvent} 阶段被加载,JVM 校验器随后会尝试解析 + * 客户端类型(例如 {@code Minecraft.player} 的字段类型 {@code LocalPlayer}), + * 触发 RuntimeDistCleaner 抛出 + * “Attempted to load class net/minecraft/client/player/LocalPlayer for invalid dist DEDICATED_SERVER” + * 并导致启动崩溃。载荷类只保留“分发检查 + 委托”,客户端类只会在客户端真正执行时被加载。 + * + *

注意:这里的静态方法签名中不要出现客户端类型(参数/返回值),否则校验器在服务端 + * 仍需加载它们。参数一律使用公共的载荷类型。 + */ +public final class ClientPacketHandlers { + private ClientPacketHandlers() {} + + /** 同步被保护玩家的血量与状态(抑制死亡与无敌帧,维持存活表现)。 */ + public static void handleBeefInvulnerabilitySync(BeefInvulnerabilitySyncPacket msg) { + Minecraft minecraft = Minecraft.getInstance(); + if (minecraft.level == null) { + return; + } + if (!Float.isFinite(msg.health()) || msg.health() <= 0.0F) { + RateLimitedLogger.warn(UselessMod.LOGGER, "Ignoring invalid protected-player health sync: entityId={}, health={}", + msg.entityId(), msg.health()); + return; + } + + Player player = null; + if (minecraft.player != null && minecraft.player.getId() == msg.entityId()) { + player = minecraft.player; + } else { + Entity entity = minecraft.level.getEntity(msg.entityId()); + if (entity instanceof Player target) { + player = target; + } + } + if (player == null) { + return; + } + + player.dead = false; + player.deathTime = 0; + player.hurtTime = 0; + player.hurtDuration = 0; + player.setHealth(msg.health()); + player.setPose(Pose.STANDING); + player.clearFire(); + player.fallDistance = 0.0F; + } + + /** 把 AE 任务进度刷新到当前打开的高级合金炉界面。 */ + public static void handleAETaskProgress(AETaskProgressPacket msg) { + Minecraft mc = Minecraft.getInstance(); + if (mc.player != null + && mc.player.containerMenu instanceof MultiblockAlloyFurnaceMenu menu + && menu.getBlockPos().equals(msg.pos())) { + menu.updateTaskProgress(msg.tasks()); + } + if (mc.level != null) { + var blockEntity = mc.level.getBlockEntity(msg.pos()); + if (blockEntity instanceof AdvancedAlloyFurnaceBlockEntity furnace) { + furnace.updateClientTaskProgress(msg.tasks()); + } else if (blockEntity instanceof MultiblockAlloyFurnaceCoreBlockEntity furnace) { + furnace.updateClientTaskProgress(msg.tasks()); + } + } + } + + /** 把服务端校验后的工具配置同步到模式轮盘界面。 */ + public static void handleBeefToolLayoutSync(BeefToolLayoutSyncPacket packet) { + try { + BeefToolLayout layout = BeefToolLayout.fromJson(packet.json()); + // 等价组必须无条件刷新:右键连锁走 Item#useOn,客户端也会本地预测, + // 界面开着还是关着都需要这份数据,否则客户端预测的范围会小于服务端实际破坏的范围。 + ChainGroupManager.setClientMirror(layout.chainGroups()); + // 生物保护名单同理:tooltip 的「已保护 N 种 / N 只」直接读这份镜像。 + BeefToolProtectionManager.setClientMirror(layout.protectedTypes(), layout.protectedEntities()); + if (Minecraft.getInstance().screen instanceof ModeWheelScreen screen) { + screen.receiveLayout(layout); + } else if (Minecraft.getInstance().screen instanceof ChainGroupScreen screen) { + screen.receiveSync(layout); + } + } catch (BeefToolLayout.LayoutException ignored) { + if (Minecraft.getInstance().screen instanceof ModeWheelScreen screen) { + screen.receiveLayoutError(BeefToolLayout.Error.INVALID_TEXT); + } + } + } + + /** 把服务端返回的错误码同步到模式轮盘界面。 */ + public static void handleBeefToolLayoutResult(BeefToolLayoutResultPacket packet) { + if (Minecraft.getInstance().screen instanceof ModeWheelScreen screen) { + screen.receiveLayoutError(packet.error()); + } else if (Minecraft.getInstance().screen instanceof ChainGroupScreen screen) { + screen.receiveError(packet.error()); + } + } + + /** 无线物流:把服务端下发的整网快照交给已打开的配置界面。 */ + public static void handleStaffLinkSync(StaffLinkSyncPacket packet) { + lastStaffLinkSync = packet; + if (Minecraft.getInstance().screen instanceof StaffLinkScreen screen) { + screen.receiveSync(packet); + } + } + + /** + * 无线物流:应力线路各端点的运行状态。 + * + *

纯展示数据,不参与任何判定——「能不能搬」永远是服务端说了算。界面每帧自己去读, + * 所以这里只把缓存换掉,不需要回调。

+ */ + public static void handleStaffLinkStressStatus(StaffLinkStressStatusPacket packet) { + StaffLinkStressClientState.replace(packet.entries()); + } + + /** + * 无线物流:服务端回发的「当前网络内容器按流向分类」结果,交给世界高亮渲染器。 + * + *

与界面无关:只要手持杖、开着无线物流模式,界面关着也要能看见框。

+ */ + public static void handleStaffLinkHighlight(StaffLinkHighlightPacket packet) { + StaffLinkHighlightRenderer.setHighlights(packet.release(), packet.absorb(), packet.disabled()); + } + + /** 万象合金炉:服务端下发的「未成型核心当前该高亮的最低错误方块」,转交世界渲染器。 */ + public static void handleAlloyFurnaceErrorHighlight(AlloyFurnaceErrorHighlightPacket packet) { + AlloyFurnaceErrorHighlightRenderer.setError(packet.corePos(), packet.errorPos()); + } + + /** + * 取走「界面还没建好时先到的」那份快照。 + * + *

开界面与下发快照是两个包,正常情况下顺序到达;这里兜底的是极端情况下快照先到、 + * 界面尚未创建的那一瞬——否则界面会一直空着直到下一次同步。

+ */ + @Nullable + public static StaffLinkSyncPacket consumePendingStaffLinkSync(java.util.UUID networkId) { + StaffLinkSyncPacket pending = lastStaffLinkSync; + if (pending == null || !pending.network().id().equals(networkId)) { + return null; + } + lastStaffLinkSync = null; + return pending; + } + + @Nullable + private static StaffLinkSyncPacket lastStaffLinkSync; +} diff --git a/src/main/java/com/sorrowmist/useless/client/render/AECraftingPatternProviderRenderer.java b/src/main/java/com/sorrowmist/useless/client/render/AECraftingPatternProviderRenderer.java deleted file mode 100644 index 1435edab..00000000 --- a/src/main/java/com/sorrowmist/useless/client/render/AECraftingPatternProviderRenderer.java +++ /dev/null @@ -1,217 +0,0 @@ -package com.sorrowmist.useless.client.render; - -import com.sorrowmist.useless.items.EndlessBeafItem; -import com.mojang.blaze3d.vertex.PoseStack; -import com.mojang.blaze3d.vertex.VertexConsumer; -import com.mojang.math.Axis; -import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.MultiBufferSource; -import net.minecraft.client.renderer.RenderType; -import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; -import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; -import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.level.Level; -import net.minecraftforge.api.distmarker.Dist; -import net.minecraftforge.api.distmarker.OnlyIn; -import net.minecraftforge.client.event.EntityRenderersEvent; -import net.minecraftforge.eventbus.api.SubscribeEvent; -import net.minecraftforge.fml.common.Mod; - -@OnlyIn(Dist.CLIENT) -@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD, value = Dist.CLIENT, modid = "useless_mod") -public class AECraftingPatternProviderRenderer implements BlockEntityRenderer { - - // 纹理资源位置 - private static final ResourceLocation MASTER_TEXTURE = new ResourceLocation("useless_mod:textures/icon/master.png"); - private static final ResourceLocation SLAVE_TEXTURE = new ResourceLocation("useless_mod:textures/icon/slave.png"); - - float thickness = 0.05f; - - public AECraftingPatternProviderRenderer(BlockEntityRendererProvider.Context ctx) {} - - @Override - public int getViewDistance() { - return 512; - } - - @Override - public boolean shouldRenderOffScreen(appeng.blockentity.crafting.PatternProviderBlockEntity be) { - // 始终允许渲染,因为我们需要检查玩家是否手持工具 - return true; - } - - @Override - public void render(appeng.blockentity.crafting.PatternProviderBlockEntity be, float partialTicks, PoseStack poseStack, - MultiBufferSource buffers, int packedLight, int packedOverlay) { - if (be == null) return; - - Level level = be.getLevel(); - if (level == null) return; - - // 检查玩家是否手持牛排工具(只检查主手和副手) - Player player = Minecraft.getInstance().player; - if (player == null) return; - - boolean holdingItem = false; - ItemStack mainHand = player.getMainHandItem(); - ItemStack offHand = player.getOffhandItem(); - if ((mainHand.getItem() instanceof EndlessBeafItem) || (offHand.getItem() instanceof EndlessBeafItem)) { - holdingItem = true; - } - - // 如果玩家没有手持牛排工具,不渲染 - if (!holdingItem) return; - - BlockPos currentPos = be.getBlockPos(); - - // 检查当前方块是否是主供应器或从供应器 - boolean isMaster = EndlessBeafItem.isMasterPatternProvider(currentPos); - boolean isSlave = EndlessBeafItem.isSlavePatternProvider(currentPos); - - // 只渲染主端或当前选定主端的从端 - boolean shouldRender = false; - ResourceLocation texture = null; - - if (isMaster) { - // 主端始终渲染 - shouldRender = true; - texture = MASTER_TEXTURE; - } else if (isSlave) { - // 从端只有当它属于当前选定的主端时才渲染 - if (EndlessBeafItem.isSlaveOfCurrentMaster(currentPos, level)) { - shouldRender = true; - texture = SLAVE_TEXTURE; - } - } - - if (!shouldRender) return; - - // 保存状态 - poseStack.pushPose(); - - // 在当前方块的六个面上渲染图片 - for (Direction direction : Direction.values()) { - renderStaticLinkPoint(poseStack, buffers, direction, packedLight, texture); - } - - // 恢复状态 - poseStack.popPose(); - } - - /** - * 在指定面上渲染静态图片 - */ - private void renderStaticLinkPoint(PoseStack poseStack, MultiBufferSource buffers, Direction face, int packedLight, ResourceLocation texture) { - poseStack.pushPose(); - - // 图片大小 - float size = 0.2f; - // 增加偏移量,确保图片渲染在机器表面 - float offset = 0.01f; - - // 根据面调整位置和旋转 - switch (face) { - case DOWN: - poseStack.translate(0.5f, -offset, 0.5f); - poseStack.mulPose(Axis.XP.rotationDegrees(90.0f)); - poseStack.mulPose(Axis.YP.rotationDegrees(180.0f)); - break; - case UP: - poseStack.translate(0.5f, 1.0f + offset, 0.5f); - poseStack.mulPose(Axis.XP.rotationDegrees(-90.0f)); - poseStack.mulPose(Axis.YP.rotationDegrees(180.0f)); - break; - case NORTH: - poseStack.translate(0.5f, 0.5f, -offset); - // 北面不需要额外旋转,直接显示 - break; - case SOUTH: - poseStack.translate(0.5f, 0.5f, 1.0f + offset); - poseStack.mulPose(Axis.YP.rotationDegrees(180.0f)); - break; - case WEST: - poseStack.translate(-offset, 0.5f, 0.5f); - poseStack.mulPose(Axis.YP.rotationDegrees(90.0f)); - break; - case EAST: - poseStack.translate(1.0f + offset, 0.5f, 0.5f); - poseStack.mulPose(Axis.YP.rotationDegrees(-90.0f)); - break; - } - - // 使用适合纹理渲染的渲染类型 - RenderType renderType = RenderType.text(texture); - VertexConsumer vertexConsumer = buffers.getBuffer(renderType); - - // 渲染图片 - renderTextureQuad(poseStack, vertexConsumer, size, packedLight); - - poseStack.popPose(); - } - - /** - * 渲染完整的纹理图像,确保图像完整显示且方向正确 - */ - private void renderTextureQuad(PoseStack poseStack, VertexConsumer consumer, float size, int packedLight) { - PoseStack.Pose pose = poseStack.last(); - - // 颜色(白色,不改变纹理颜色) - float r = 1.0f; - float g = 1.0f; - float b = 1.0f; - float a = 1.0f; - - // 基础顶点坐标 - float x1 = -size; - float y1 = -size; - float x2 = size; - float y2 = size; - - // 使用标准UV坐标 - float u1 = 0.0f; - float v1 = 0.0f; - float u2 = 1.0f; - float v2 = 1.0f; - - // 渲染四边形,使用正确的顶点顺序 - // 顶点顺序:左上 -> 右上 -> 右下 -> 左下 - // 左上 - consumer.vertex(pose.pose(), x1, y2, 0.0f) - .color(r, g, b, a) - .uv(u1, v1) - .uv2(packedLight) - .endVertex(); - - // 右上 - consumer.vertex(pose.pose(), x2, y2, 0.0f) - .color(r, g, b, a) - .uv(u2, v1) - .uv2(packedLight) - .endVertex(); - - // 右下 - consumer.vertex(pose.pose(), x2, y1, 0.0f) - .color(r, g, b, a) - .uv(u2, v2) - .uv2(packedLight) - .endVertex(); - - // 左下 - consumer.vertex(pose.pose(), x1, y1, 0.0f) - .color(r, g, b, a) - .uv(u1, v2) - .uv2(packedLight) - .endVertex(); - } - - // 注册渲染器 - @SubscribeEvent - public static void registerRenderers(EntityRenderersEvent.RegisterRenderers event) { - // 注册AE2普通样板供应器渲染器 - event.registerBlockEntityRenderer(appeng.core.definitions.AEBlockEntities.PATTERN_PROVIDER, AECraftingPatternProviderRenderer::new); - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/client/render/AdvancedAEAdvPatternProviderRenderer.java b/src/main/java/com/sorrowmist/useless/client/render/AdvancedAEAdvPatternProviderRenderer.java deleted file mode 100644 index 5b2a3095..00000000 --- a/src/main/java/com/sorrowmist/useless/client/render/AdvancedAEAdvPatternProviderRenderer.java +++ /dev/null @@ -1,290 +0,0 @@ -package com.sorrowmist.useless.client.render; - -import com.sorrowmist.useless.items.EndlessBeafItem; -import com.mojang.blaze3d.vertex.PoseStack; -import com.mojang.blaze3d.vertex.VertexConsumer; -import com.mojang.math.Axis; -import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.MultiBufferSource; -import net.minecraft.client.renderer.RenderType; -import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; -import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; -import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.level.Level; -import net.minecraft.world.level.block.entity.BlockEntity; -import net.minecraftforge.api.distmarker.Dist; -import net.minecraftforge.api.distmarker.OnlyIn; -import net.minecraftforge.client.event.EntityRenderersEvent; -import net.minecraftforge.eventbus.api.SubscribeEvent; -import net.minecraftforge.fml.common.Mod; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; - -@OnlyIn(Dist.CLIENT) -@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD, value = Dist.CLIENT, modid = "useless_mod") -public class AdvancedAEAdvPatternProviderRenderer implements BlockEntityRenderer { - - // 纹理资源位置 - private static final ResourceLocation MASTER_TEXTURE = new ResourceLocation("useless_mod:textures/icon/master.png"); - private static final ResourceLocation SLAVE_TEXTURE = new ResourceLocation("useless_mod:textures/icon/slave.png"); - - public AdvancedAEAdvPatternProviderRenderer(BlockEntityRendererProvider.Context ctx) {} - - @Override - public int getViewDistance() { - return 512; - } - - @Override - public boolean shouldRenderOffScreen(BlockEntity be) { - // 始终允许渲染,因为我们需要检查玩家是否手持工具 - return true; - } - - @Override - public void render(BlockEntity be, float partialTicks, PoseStack poseStack, - MultiBufferSource buffers, int packedLight, int packedOverlay) { - if (be == null) return; - - // 检查是否是AdvancedAE的高级样板供应器 - String className = be.getClass().getName(); - if (!(className.equals("net.pedroksl.advanced_ae.common.entities.AdvPatternProviderEntity") || - className.equals("net.pedroksl.advanced_ae.common.entities.SmallAdvPatternProviderEntity"))) { - return; - } - - // 获取level和blockPos,直接使用BlockEntity的getLevel和getBlockPos方法 - Level level = be.getLevel(); - BlockPos currentPos = be.getBlockPos(); - - if (level == null || currentPos == null) return; - - // 检查玩家是否手持牛排工具(只检查主手和副手) - Player player = Minecraft.getInstance().player; - if (player == null) return; - - boolean holdingItem = false; - ItemStack mainHand = player.getMainHandItem(); - ItemStack offHand = player.getOffhandItem(); - if ((mainHand.getItem() instanceof EndlessBeafItem) || (offHand.getItem() instanceof EndlessBeafItem)) { - holdingItem = true; - } - - // 如果玩家没有手持牛排工具,不渲染 - if (!holdingItem) return; - - // 检查当前方块是否是主供应器或从供应器 - boolean isMaster = EndlessBeafItem.isMasterPatternProvider(currentPos); - boolean isSlave = EndlessBeafItem.isSlavePatternProvider(currentPos); - - // 只渲染主端或当前选定主端的从端 - boolean shouldRender = false; - ResourceLocation texture = null; - - if (isMaster) { - // 主端始终渲染 - shouldRender = true; - texture = MASTER_TEXTURE; - } else if (isSlave) { - // 从端只有当它属于当前选定的主端时才渲染 - if (EndlessBeafItem.isSlaveOfCurrentMaster(currentPos, level)) { - shouldRender = true; - texture = SLAVE_TEXTURE; - } - } - - if (!shouldRender || texture == null) return; - - // 保存状态 - poseStack.pushPose(); - - // 在当前方块的六个面上渲染图片 - for (Direction direction : Direction.values()) { - renderStaticLinkPoint(poseStack, buffers, direction, packedLight, texture); - } - - // 恢复状态 - poseStack.popPose(); - } - - /** - * 在指定面上渲染静态图片 - */ - private void renderStaticLinkPoint(PoseStack poseStack, MultiBufferSource buffers, Direction face, int packedLight, ResourceLocation texture) { - poseStack.pushPose(); - - // 图片大小 - float size = 0.2f; - // 增加偏移量,确保图片渲染在机器表面 - float offset = 0.01f; - - // 根据面调整位置和旋转 - switch (face) { - case DOWN: - poseStack.translate(0.5f, -offset, 0.5f); - poseStack.mulPose(Axis.XP.rotationDegrees(90.0f)); - poseStack.mulPose(Axis.YP.rotationDegrees(180.0f)); - break; - case UP: - poseStack.translate(0.5f, 1.0f + offset, 0.5f); - poseStack.mulPose(Axis.XP.rotationDegrees(-90.0f)); - poseStack.mulPose(Axis.YP.rotationDegrees(180.0f)); - break; - case NORTH: - poseStack.translate(0.5f, 0.5f, -offset); - // 北面不需要额外旋转,直接显示 - break; - case SOUTH: - poseStack.translate(0.5f, 0.5f, 1.0f + offset); - poseStack.mulPose(Axis.YP.rotationDegrees(180.0f)); - break; - case WEST: - poseStack.translate(-offset, 0.5f, 0.5f); - poseStack.mulPose(Axis.YP.rotationDegrees(90.0f)); - break; - case EAST: - poseStack.translate(1.0f + offset, 0.5f, 0.5f); - poseStack.mulPose(Axis.YP.rotationDegrees(-90.0f)); - break; - } - - // 使用适合纹理渲染的渲染类型 - RenderType renderType = RenderType.text(texture); - VertexConsumer vertexConsumer = buffers.getBuffer(renderType); - - // 渲染图片 - renderTextureQuad(poseStack, vertexConsumer, size, packedLight); - - poseStack.popPose(); - } - - /** - * 渲染完整的纹理图像,确保图像完整显示且方向正确 - */ - private void renderTextureQuad(PoseStack poseStack, VertexConsumer consumer, float size, int packedLight) { - PoseStack.Pose pose = poseStack.last(); - - // 颜色(白色,不改变纹理颜色) - float r = 1.0f; - float g = 1.0f; - float b = 1.0f; - float a = 1.0f; - - // 基础顶点坐标 - float x1 = -size; - float y1 = -size; - float x2 = size; - float y2 = size; - - // 使用标准UV坐标 - float u1 = 0.0f; - float v1 = 0.0f; - float u2 = 1.0f; - float v2 = 1.0f; - - // 渲染四边形,使用正确的顶点顺序 - // 顶点顺序:左上 -> 右上 -> 右下 -> 左下 - // 左上 - consumer.vertex(pose.pose(), x1, y2, 0.0f) - .color(r, g, b, a) - .uv(u1, v1) - .uv2(packedLight) - .endVertex(); - - // 右上 - consumer.vertex(pose.pose(), x2, y2, 0.0f) - .color(r, g, b, a) - .uv(u2, v1) - .uv2(packedLight) - .endVertex(); - - // 右下 - consumer.vertex(pose.pose(), x2, y1, 0.0f) - .color(r, g, b, a) - .uv(u2, v2) - .uv2(packedLight) - .endVertex(); - - // 左下 - consumer.vertex(pose.pose(), x1, y1, 0.0f) - .color(r, g, b, a) - .uv(u1, v2) - .uv2(packedLight) - .endVertex(); - } - - // 注册渲染器 - @SubscribeEvent - public static void registerRenderers(EntityRenderersEvent.RegisterRenderers event) { - // 只有在AdvancedAE模组安装时才注册渲染器 - try { - // 检查AdvancedAE类是否存在 - Class.forName("net.pedroksl.advanced_ae.common.definitions.AAEBlockEntities"); - - // 使用反射获取BlockEntityType实例 - Class aaeBlockEntitiesClass = Class.forName("net.pedroksl.advanced_ae.common.definitions.AAEBlockEntities"); - - // 尝试获取ADV_PATTERN_PROVIDER字段 - Object advPatternProviderType = null; - Object smallAdvPatternProviderType = null; - - try { - // 尝试获取静态字段 - Field advPatternProviderField = aaeBlockEntitiesClass.getDeclaredField("ADV_PATTERN_PROVIDER"); - advPatternProviderField.setAccessible(true); - Object advPatternProviderHolder = advPatternProviderField.get(null); - - // 获取实际的BlockEntityType - Method getMethod = advPatternProviderHolder.getClass().getMethod("get"); - advPatternProviderType = getMethod.invoke(advPatternProviderHolder); - - // 尝试获取小型高级样板供应器字段 - Field smallAdvPatternProviderField = aaeBlockEntitiesClass.getDeclaredField("SMALL_ADV_PATTERN_PROVIDER"); - smallAdvPatternProviderField.setAccessible(true); - Object smallAdvPatternProviderHolder = smallAdvPatternProviderField.get(null); - smallAdvPatternProviderType = getMethod.invoke(smallAdvPatternProviderHolder); - } catch (NoSuchFieldException e) { - // 如果字段不存在,尝试获取方法 - Method advPatternProviderMethod = aaeBlockEntitiesClass.getDeclaredMethod("ADV_PATTERN_PROVIDER"); - advPatternProviderMethod.setAccessible(true); - Object advPatternProviderHolder = advPatternProviderMethod.invoke(null); - - // 获取实际的BlockEntityType - Method getMethod = advPatternProviderHolder.getClass().getMethod("get"); - advPatternProviderType = getMethod.invoke(advPatternProviderHolder); - - // 尝试获取小型高级样板供应器方法 - Method smallAdvPatternProviderMethod = aaeBlockEntitiesClass.getDeclaredMethod("SMALL_ADV_PATTERN_PROVIDER"); - smallAdvPatternProviderMethod.setAccessible(true); - Object smallAdvPatternProviderHolder = smallAdvPatternProviderMethod.invoke(null); - smallAdvPatternProviderType = getMethod.invoke(smallAdvPatternProviderHolder); - } - - // 注册渲染器 - if (advPatternProviderType != null) { - @SuppressWarnings("unchecked") - net.minecraft.world.level.block.entity.BlockEntityType advType = - (net.minecraft.world.level.block.entity.BlockEntityType) advPatternProviderType; - event.registerBlockEntityRenderer(advType, AdvancedAEAdvPatternProviderRenderer::new); - } - - // 注册小型高级样板供应器渲染器 - if (smallAdvPatternProviderType != null) { - @SuppressWarnings("unchecked") - net.minecraft.world.level.block.entity.BlockEntityType smallType = - (net.minecraft.world.level.block.entity.BlockEntityType) smallAdvPatternProviderType; - event.registerBlockEntityRenderer(smallType, AdvancedAEAdvPatternProviderRenderer::new); - } - } catch (ClassNotFoundException e) { - // AdvancedAE模组未安装,跳过注册 - } catch (Exception e) { - // 注册失败,打印错误日志以便调试 - e.printStackTrace(); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/client/render/AeLinkHighlightRenderer.java b/src/main/java/com/sorrowmist/useless/client/render/AeLinkHighlightRenderer.java new file mode 100644 index 00000000..5eda65e8 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/render/AeLinkHighlightRenderer.java @@ -0,0 +1,132 @@ +package com.sorrowmist.useless.client.render; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.items.BeefToolVariants; +import com.sorrowmist.useless.core.component.UComponents; +import com.sorrowmist.useless.network.AeLinkPreviewRequestPacket; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.LevelRenderer; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod.EventBusSubscriber; +import net.minecraftforge.event.TickEvent.ClientTickEvent; +import net.minecraftforge.client.event.RenderLevelStageEvent; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; + +import java.util.List; + +/** + * 手持造化杖且开了 AE 连接模式时,为绑定的访问点和已连接机器画边框。 + * + *

Forge 1.20.1 的 AFTER_LEVEL 传入的是投影栈,不是世界视图栈。 + * 使用 AFTER_PARTICLES 的世界视图栈:保留本帧相机旋转(含第三方 roll), + * 顶点仅减去相机位置,不再补相机旋转,也不在顶点端重复应用投影。

+ */ +@EventBusSubscriber(modid = UselessMod.MODID, value = Dist.CLIENT) +public final class AeLinkHighlightRenderer { + /** 提示数据变化很慢,没必要每 tick 问服务端。 */ + private static final int REFRESH_INTERVAL = 10; + private static final double MAX_RENDER_DISTANCE = 128.0; + + private static ResourceLocation dimension; + private static BlockPos accessPoint; + private static List machines = List.of(); + private static int tickCounter = REFRESH_INTERVAL; + + private AeLinkHighlightRenderer() {} + + public static void setLinks(ResourceLocation machineDimension, BlockPos accessPoint, + List machines) { + AeLinkHighlightRenderer.dimension = machineDimension; + AeLinkHighlightRenderer.accessPoint = accessPoint.immutable(); + AeLinkHighlightRenderer.machines = List.copyOf(machines); + } + + @SubscribeEvent + public static void onClientTick(ClientTickEvent event) { + if (event.phase != net.minecraftforge.event.TickEvent.Phase.END) return; + Minecraft minecraft = Minecraft.getInstance(); + if (minecraft.level == null || minecraft.player == null || !shouldRequest(minecraft)) { + clear(); + return; + } + if (++tickCounter < REFRESH_INTERVAL) { + return; + } + tickCounter = 0; + PacketDistributor.sendToServer(new AeLinkPreviewRequestPacket()); + } + + @SubscribeEvent + public static void onRenderStage(RenderLevelStageEvent event) { + if (event.getStage() != RenderLevelStageEvent.Stage.AFTER_PARTICLES) { + return; + } + Minecraft minecraft = Minecraft.getInstance(); + if (minecraft.level == null || minecraft.player == null || !shouldRequest(minecraft) + || dimension == null + || !dimension.equals(minecraft.level.dimension().location()) + || (accessPoint == null && machines.isEmpty())) { + return; + } + + Camera camera = event.getCamera(); + Vec3 cameraPos = camera.getPosition(); + PoseStack pose = event.getPoseStack(); + MultiBufferSource.BufferSource buffer = minecraft.renderBuffers().bufferSource(); + + pose.pushPose(); + + VertexConsumer lines = buffer.getBuffer(RenderType.lines()); + + if (accessPoint != null) { + // 绑定的访问点用暖色,和「已连接的机器」区分开。 + drawBox(pose, lines, accessPoint, cameraPos, 1.0F, 0.82F, 0.30F, 0.95F); + } + for (BlockPos machine : machines) { + if (Vec3.atCenterOf(machine).distanceToSqr(cameraPos) > MAX_RENDER_DISTANCE * MAX_RENDER_DISTANCE) { + continue; + } + drawBox(pose, lines, machine, cameraPos, 0.35F, 0.78F, 1.0F, 0.9F); + } + + pose.popPose(); + buffer.endBatch(RenderType.lines()); + } + + private static void drawBox(PoseStack pose, VertexConsumer lines, BlockPos pos, Vec3 cameraPos, + float red, float green, float blue, float alpha) { + AABB box = new AABB(pos).inflate(0.0025).move(-cameraPos.x, -cameraPos.y, -cameraPos.z); + LevelRenderer.renderLineBox(pose, lines, box, red, green, blue, alpha); + } + + /** 只在手上真的拿着「开着 AE 连接模式、且已经绑定访问点」的造化杖时才要提示数据。 */ + private static boolean shouldRequest(Minecraft minecraft) { + for (InteractionHand hand : InteractionHand.values()) { + ItemStack stack = minecraft.player.getItemInHand(hand); + if (!BeefToolVariants.isBeafTool(stack)) continue; + if (!com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.AeNetworkConnectComponent.get(), false)) continue; + if (com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.has(stack, UComponents.WIRELESS_LINK_TARGET.get())) return true; + } + return false; + } + + /** 清空缓存并将刷新计数器置为上限,使下一次手持当 tick 立即请求一次最新数据。 */ + private static void clear() { + dimension = null; + accessPoint = null; + machines = List.of(); + tickCounter = REFRESH_INTERVAL; + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/render/AlloyFurnaceErrorHighlightRenderer.java b/src/main/java/com/sorrowmist/useless/client/render/AlloyFurnaceErrorHighlightRenderer.java new file mode 100644 index 00000000..295180b7 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/render/AlloyFurnaceErrorHighlightRenderer.java @@ -0,0 +1,123 @@ +package com.sorrowmist.useless.client.render; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.blockentities.multiblock.MultiblockAlloyFurnaceCoreBlockEntity; +import com.sorrowmist.useless.content.blocks.multiblock.MultiblockAlloyFurnaceCoreBlock; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.LevelRenderer; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod.EventBusSubscriber; +import net.minecraftforge.client.event.RenderLevelStageEvent; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * 万象合金炉未成型时,用红框标出服务端指定的最低错误方块。 + * 核心消失、成型或维度变化时清除对应缓存。 + * + *

在 AFTER_PARTICLES 使用本帧世界视图栈;AFTER_LEVEL 在 Forge 1.20.1 + * 提供的是投影栈,不能用于世界顶点变换。坐标仅减一次相机位置。

+ */ +@EventBusSubscriber(modid = UselessMod.MODID, value = Dist.CLIENT) +public final class AlloyFurnaceErrorHighlightRenderer { + /** 未成型提示只在玩家附近才有意义,太远裁掉。 */ + private static final double MAX_RENDER_DISTANCE = 128.0; + + /** 错误提示用醒目红色。 */ + private static final float ERROR_RED = 1.00F; + private static final float ERROR_GREEN = 0.15F; + private static final float ERROR_BLUE = 0.15F; + private static final float LINE_ALPHA = 0.95F; + + /** 核心坐标 → 当前该高亮的错误方块。 */ + private static final Map errorsByCore = new HashMap<>(); + @Nullable + private static ResourceKey dimension; + + private AlloyFurnaceErrorHighlightRenderer() { + } + + /** 客户端收到服务端的「最低错误」更新;{@code errorPos} 为 null 表示该核心已无可高亮错误。 */ + public static void setError(BlockPos corePos, @Nullable BlockPos errorPos) { + BlockPos core = corePos.immutable(); + if (errorPos == null) { + errorsByCore.remove(core); + } else { + errorsByCore.put(core, errorPos.immutable()); + } + } + + @SubscribeEvent + public static void onRenderStage(RenderLevelStageEvent event) { + if (event.getStage() != RenderLevelStageEvent.Stage.AFTER_PARTICLES) { + return; + } + Minecraft minecraft = Minecraft.getInstance(); + if (minecraft.level == null || errorsByCore.isEmpty()) { + return; + } + + // 缓存以核心坐标为键,只在同一维度内有效;换维度就整体作废。 + ResourceKey current = minecraft.level.dimension(); + if (dimension == null) { + dimension = current; + } else if (!dimension.equals(current)) { + errorsByCore.clear(); + dimension = current; + return; + } + + Camera camera = event.getCamera(); + Vec3 cameraPos = camera.getPosition(); + PoseStack pose = event.getPoseStack(); + MultiBufferSource.BufferSource buffer = minecraft.renderBuffers().bufferSource(); + + pose.pushPose(); + + VertexConsumer lines = buffer.getBuffer(RenderType.lines()); + + Iterator> iterator = errorsByCore.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + BlockPos core = entry.getKey(); + // 核心方块实体没了(被拆)就剔除;服务端不会专门发清除包。 + if (!(minecraft.level.getBlockEntity(core) instanceof MultiblockAlloyFurnaceCoreBlockEntity)) { + iterator.remove(); + continue; + } + // 已成型:靠 FORMED 方块状态同步剔除,不必等服务端再发一次。 + BlockState state = minecraft.level.getBlockState(core); + if (!state.hasProperty(MultiblockAlloyFurnaceCoreBlock.FORMED) + || state.getValue(MultiblockAlloyFurnaceCoreBlock.FORMED)) { + iterator.remove(); + continue; + } + BlockPos error = entry.getValue(); + if (Vec3.atCenterOf(error).distanceToSqr(cameraPos) + > MAX_RENDER_DISTANCE * MAX_RENDER_DISTANCE) { + continue; + } + AABB box = new AABB(error).inflate(0.0025) + .move(-cameraPos.x, -cameraPos.y, -cameraPos.z); + LevelRenderer.renderLineBox(pose, lines, box, ERROR_RED, ERROR_GREEN, ERROR_BLUE, LINE_ALPHA); + } + + pose.popPose(); + buffer.endBatch(RenderType.lines()); + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/render/BeefTimeAccelerationClient.java b/src/main/java/com/sorrowmist/useless/client/render/BeefTimeAccelerationClient.java new file mode 100644 index 00000000..032f1697 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/render/BeefTimeAccelerationClient.java @@ -0,0 +1,19 @@ +package com.sorrowmist.useless.client.render; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.init.ModEntities; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod.EventBusSubscriber; +import net.minecraftforge.client.event.EntityRenderersEvent; + +@EventBusSubscriber(modid = UselessMod.MODID, value = Dist.CLIENT, bus = EventBusSubscriber.Bus.MOD) +public final class BeefTimeAccelerationClient { + private BeefTimeAccelerationClient() { + } + + @SubscribeEvent + public static void registerRenderers(EntityRenderersEvent.RegisterRenderers event) { + event.registerEntityRenderer(ModEntities.BEEF_TIME_ACCELERATION.get(), BeefTimeAccelerationRenderer::new); + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/render/BeefTimeAccelerationRenderer.java b/src/main/java/com/sorrowmist/useless/client/render/BeefTimeAccelerationRenderer.java new file mode 100644 index 00000000..4212ccad --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/render/BeefTimeAccelerationRenderer.java @@ -0,0 +1,139 @@ +package com.sorrowmist.useless.client.render; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import com.sorrowmist.useless.content.entities.BeefTimeAccelerationEntity; +import net.minecraft.client.gui.Font; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.core.Direction; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import java.util.Locale; + +@OnlyIn(Dist.CLIENT) +public final class BeefTimeAccelerationRenderer extends EntityRenderer { + private static final float TEXT_SCALE = 0.02F; + private final Font font; + + public BeefTimeAccelerationRenderer(EntityRendererProvider.Context context) { + super(context); + this.font = context.getFont(); + this.shadowRadius = 0.0F; + } + + @Override + public void render(BeefTimeAccelerationEntity entity, + float entityYaw, + float partialTick, + PoseStack poseStack, + MultiBufferSource buffer, + int packedLight) { + String speedText = String.format(Locale.ROOT, "x%.1f", (float) (1 << entity.getTickSpeed())); + String timeText = String.format(Locale.ROOT, "%.2fs", entity.getRemainingTime() / 20.0F); + + // 生物目标:载体悬在目标头顶,画始终朝向摄像机的悬浮文字(billboard) + if (entity.isEntityTarget()) { + poseStack.pushPose(); + poseStack.mulPose(this.entityRenderDispatcher.cameraOrientation()); + poseStack.scale(TEXT_SCALE, -TEXT_SCALE, TEXT_SCALE); + drawBillboardLine(poseStack, buffer, speedText, -0.28F, 0xFFFFFF, packedLight); + drawBillboardLine(poseStack, buffer, timeText, 0.12F, 0xD0D0D0, packedLight); + poseStack.popPose(); + return; + } + + // 方块目标:目标方块被挖掉/替换后不再显示 + if (entity.level().getBlockState(entity.getTargetPos()).isAir()) { + return; + } + + for (Direction face : Direction.values()) { + renderText(poseStack, buffer, speedText, face, -0.08F, 0xFFFFFF, packedLight); + renderText(poseStack, buffer, timeText, face, 0.12F, 0xD0D0D0, packedLight); + } + } + + /** 画一行朝向摄像机的文字(调用方已应用 cameraOrientation 与缩放)。 */ + private void drawBillboardLine(PoseStack poseStack, + MultiBufferSource buffer, + String text, + float y, + int color, + int packedLight) { + float x = -this.font.width(text) / 2.0F; + this.font.drawInBatch( + text, + x, + y / TEXT_SCALE, + color, + false, + poseStack.last().pose(), + buffer, + Font.DisplayMode.NORMAL, + 0, + packedLight + ); + } + + private void renderText(PoseStack poseStack, + MultiBufferSource buffer, + String text, + Direction face, + float y, + int color, + int packedLight) { + poseStack.pushPose(); + moveToFace(poseStack, face); + poseStack.scale(TEXT_SCALE, -TEXT_SCALE, TEXT_SCALE); + + float x = -this.font.width(text) / 2.0F; + this.font.drawInBatch( + text, + x, + y / TEXT_SCALE, + color, + false, + poseStack.last().pose(), + buffer, + Font.DisplayMode.NORMAL, + 0, + packedLight + ); + poseStack.popPose(); + } + + private static void moveToFace(PoseStack poseStack, Direction face) { + switch (face) { + case UP -> { + poseStack.translate(0.0D, 0.506D, 0.0D); + poseStack.mulPose(Axis.XP.rotationDegrees(-90.0F)); + } + case DOWN -> { + poseStack.translate(0.0D, -0.506D, 0.0D); + poseStack.mulPose(Axis.XP.rotationDegrees(90.0F)); + } + case NORTH -> { + poseStack.translate(0.0D, 0.0D, -0.506D); + poseStack.mulPose(Axis.YP.rotationDegrees(180.0F)); + } + case SOUTH -> poseStack.translate(0.0D, 0.0D, 0.506D); + case WEST -> { + poseStack.translate(-0.506D, 0.0D, 0.0D); + poseStack.mulPose(Axis.YP.rotationDegrees(-90.0F)); + } + case EAST -> { + poseStack.translate(0.506D, 0.0D, 0.0D); + poseStack.mulPose(Axis.YP.rotationDegrees(90.0F)); + } + } + } + + @Override + public ResourceLocation getTextureLocation(BeefTimeAccelerationEntity entity) { + return null; + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/render/ChainMiningHighlightRenderer.java b/src/main/java/com/sorrowmist/useless/client/render/ChainMiningHighlightRenderer.java new file mode 100644 index 00000000..be06c9d6 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/render/ChainMiningHighlightRenderer.java @@ -0,0 +1,376 @@ +package com.sorrowmist.useless.client.render; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.items.BeefToolVariants; +import com.sorrowmist.useless.core.common.KeyBindings; +import com.sorrowmist.useless.data.PlayerMiningData; +import com.sorrowmist.useless.utils.mining.MiningDispatcher; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.client.renderer.LevelRenderer; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.culling.Frustum; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod.EventBusSubscriber; +import net.minecraftforge.client.event.RenderLevelStageEvent; +import org.joml.Matrix4f; +import org.joml.Vector3f; + +import java.util.ArrayList; +import java.util.List; + +/** + * 按住连锁键期间,把本次连锁将波及的全部方块描上边框。 + * + *

数据直接取自客户端缓存的 {@link PlayerMiningData#getCachedBlocks()}:该列表由服务端在准星 + * 命中新方块时用与破坏阶段完全相同的扫描流程算出并下发,因此预览范围与实际破坏范围天然一致, + * 不需要额外的请求包或二次预测。 + * + *

Forge 1.20.1 使用 AFTER_PARTICLES 的世界视图栈,保留当前相机旋转, + * 顶点坐标只减去相机位置。AFTER_LEVEL 提供投影栈,不能当作世界视图栈使用。 + * + *

相邻两个方块的边框在公共面上完全重合。若双方都提交该面,同一批顶点内该处的深度值相同, + * 显示结果由浮点精度而非绘制顺序决定,原点边框会被相邻方块的冷色边框覆盖或闪烁。 + * 因此非原点方块一律跳过与其它待破坏方块共享的面,每个面至多被提交一次; + * 原点保留完整边框,使玩家可明确辨认扫描起点。 + * + *

连锁上限可达数十万格,因此候选方块在提交顶点前依次经过三重剔除:距离、视锥与面朝向。 + * 距离与视锥以方块为单位,面朝向以暴露面为单位。三者叠加后,顶点数只与玩家当前可见的轮廓 + * 规模相关,与连锁方块总数无关。 + * + *

该渲染器每帧执行一次,热路径避免逐方块与逐顶点的临时对象分配:距离判定使用标量运算, + * 顶点提交复用单个 {@link Vector3f},坐标集合跨帧复用同一实例。 + */ +@EventBusSubscriber(modid = UselessMod.MODID, value = Dist.CLIENT) +public final class ChainMiningHighlightRenderer { + /** 连锁原点用暖色,与其余待破坏方块区分,便于判断扫描起点。 */ + private static final float ORIGIN_RED = 1.0F; + private static final float ORIGIN_GREEN = 0.78F; + private static final float ORIGIN_BLUE = 0.25F; + + /** 其余待破坏方块用冷色。 */ + private static final float EDGE_RED = 0.35F; + private static final float EDGE_GREEN = 0.85F; + private static final float EDGE_BLUE = 1.0F; + + /** 边框相对方块表面的外扩量,用于避免线框与方块表面共面产生深度冲突。 */ + private static final double EDGE_INFLATE = 0.0025; + + /** + * 超出该距离的方块不绘制。 + * + *

视锥只约束视野角度,不约束距离:远处方块仍可能落在视锥内。缺少该判定时, + * 连锁上限较高会让视锥内堆积大量不可辨认的密集线框,既无信息量又增加顶点开销。 + */ + private static final double MAX_RENDER_DISTANCE = 128.0; + + /** 视距平方,避免逐方块重复开方。 */ + private static final double MAX_RENDER_DISTANCE_SQR = MAX_RENDER_DISTANCE * MAX_RENDER_DISTANCE; + + /** 单帧绘制上限,用于兜底保护;默认配置上限为 1000,正常情形不会触及。 */ + private static final int MAX_RENDERED_BLOCKS = 4096; + + /** + * 本帧参与绘制的方块坐标集合,跨帧复用。 + * + *

渲染发生在客户端渲染线程,单帧内不存在并发访问;复用同一实例可避免每帧按方块 + * 数量重新分配哈希表。 + */ + private static final LongOpenHashSet RENDERED_POSITIONS = new LongOpenHashSet(1024); + + /** 顶点提交的坐标暂存,跨顶点复用,避免每顶点分配。 */ + private static final Vector3f VERTEX_SCRATCH = new Vector3f(); + + private ChainMiningHighlightRenderer() { + } + + @SubscribeEvent + public static void onRenderStage(RenderLevelStageEvent event) { + if (event.getStage() != RenderLevelStageEvent.Stage.AFTER_PARTICLES) { + return; + } + + Minecraft minecraft = Minecraft.getInstance(); + LocalPlayer player = minecraft.player; + if (minecraft.level == null || player == null) { + return; + } + + // 仅按住连锁键时显示:缓存会在松开按键后保留,若不加该判定,高亮会常驻在屏幕上。 + if (!KeyBindings.isChainMiningKeyDown()) { + return; + } + if (!holdsBeafTool(player)) { + return; + } + + PlayerMiningData data = MiningDispatcher.getPlayerData(player); + if (data == null) { + return; + } + List blocks = data.getCachedBlocks(); + if (blocks.isEmpty()) { + return; + } + + Camera camera = event.getCamera(); + Vec3 cameraPos = camera.getPosition(); + PoseStack pose = event.getPoseStack(); + MultiBufferSource.BufferSource buffer = minecraft.renderBuffers().bufferSource(); + + // 视锥取自本帧渲染流程,已按当前相机完成计算;为空时退化为不做视锥剔除。 + Frustum frustum = minecraft.levelRenderer.getFrustum(); + + pose.pushPose(); + + VertexConsumer lines = buffer.getBuffer(RenderType.lines()); + + BlockPos origin = data.getCachedPos(); + + // 服务端下发的缓存按到原点的距离升序排列,超距方块因此集中在列表尾部。以 + // 「相机到原点距离 + 渲染距离」为界即可安全终止遍历:列表有序时,更远的方块到相机 + // 的距离必然超出渲染距离。形状布局未经排序,故在遍历中同步检测有序性, + // 一旦发现逆序立即放弃提前终止,退化为完整遍历。 + double breakDistanceSqr = 0.0; + if (origin != null) { + double reach = Math.sqrt(distanceToSqr(origin, cameraPos)) + MAX_RENDER_DISTANCE; + breakDistanceSqr = reach * reach; + } + + // 先筛出本帧实际绘制的方块。共享面判定必须基于同一集合:若某个邻块因距离、视锥 + // 或数量上限未被绘制,而本方块仍跳过朝向它的面,外轮廓上会留下缺口。 + List rendered = new ArrayList<>(Math.min(blocks.size(), MAX_RENDERED_BLOCKS)); + boolean ordered = origin != null; + double previousOriginDistanceSqr = -1.0; + for (BlockPos pos : blocks) { + if (rendered.size() >= MAX_RENDERED_BLOCKS) { + break; + } + if (ordered) { + double originDistanceSqr = distanceToSqr(pos, origin); + if (originDistanceSqr < previousOriginDistanceSqr) { + ordered = false; + } else if (originDistanceSqr > breakDistanceSqr) { + break; + } + previousOriginDistanceSqr = originDistanceSqr; + } + if (pos.equals(origin)) { + continue; + } + if (distanceToSqr(pos, cameraPos) > MAX_RENDER_DISTANCE_SQR) { + continue; + } + if (frustum != null && !frustum.isVisible(new AABB(pos))) { + continue; + } + rendered.add(pos); + } + + boolean originVisible = origin != null + && distanceToSqr(origin, cameraPos) <= MAX_RENDER_DISTANCE_SQR; + + LongOpenHashSet positions = RENDERED_POSITIONS; + positions.clear(); + for (BlockPos pos : rendered) { + positions.add(pos.asLong()); + } + if (originVisible) { + positions.add(origin.asLong()); + } + + Matrix4f matrix = pose.last().pose(); + for (BlockPos pos : rendered) { + drawBoxSkippingSharedFaces(matrix, lines, pos, cameraPos, positions); + } + + // 原点绘制完整边框。它与相邻方块之间的公共面已由相邻方块跳过,此处不会再有重叠。 + if (originVisible) { + AABB box = new AABB(origin).inflate(EDGE_INFLATE).move(-cameraPos.x, -cameraPos.y, -cameraPos.z); + LevelRenderer.renderLineBox(pose, lines, box, + ORIGIN_RED, ORIGIN_GREEN, ORIGIN_BLUE, 0.95F); + } + + pose.popPose(); + buffer.endBatch(RenderType.lines()); + } + + /** + * 计算方块中心到相机的距离平方。 + * + *

使用标量运算而非 {@link Vec3#atCenterOf},避免逐方块的临时对象分配: + * 该循环每帧遍历整个缓存列表,连锁上限较高时分配量会随方块数线性增长。 + */ + private static double distanceToSqr(BlockPos pos, Vec3 cameraPos) { + double dx = pos.getX() + 0.5 - cameraPos.x; + double dy = pos.getY() + 0.5 - cameraPos.y; + double dz = pos.getZ() + 0.5 - cameraPos.z; + return dx * dx + dy * dy + dz * dz; + } + + /** + * 计算两个方块中心之间的距离平方。 + * + *

用于判断缓存列表是否仍按到原点的距离升序排列:该判定只需比较大小关系, + * 因此省去开方。 + */ + private static double distanceToSqr(BlockPos pos, BlockPos other) { + double dx = pos.getX() - other.getX(); + double dy = pos.getY() - other.getY(); + double dz = pos.getZ() - other.getZ(); + return dx * dx + dy * dy + dz * dz; + } + + /** + * 绘制方框,但跳过与相邻待破坏方块共享的面,以及背对相机的面。 + * + *

共享面在相邻两个方框上完全重合,该面的四条棱若由双方各提交一次,深度值相同, + * 最终显示结果由浮点精度而非绘制顺序决定,表现为棱线闪烁或颜色相互覆盖。跳过公共面后, + * 每个面至多被提交一次,被完全包围的内部方块也不再产生任何顶点。 + * + *

面朝向剔除针对暴露面:外法线背对相机的面位于方块背面,被方块本体遮挡, + * 提交其棱线不会增加可见信息,只会增加顶点开销。 + * + * @param positions 本帧参与绘制的全部方块坐标,用于判断某个面是否与邻块共享 + */ + private static void drawBoxSkippingSharedFaces(Matrix4f matrix, VertexConsumer lines, BlockPos pos, + Vec3 cameraPos, LongOpenHashSet positions) { + for (Direction face : Direction.values()) { + long neighbour = BlockPos.asLong( + pos.getX() + face.getStepX(), + pos.getY() + face.getStepY(), + pos.getZ() + face.getStepZ()); + if (positions.contains(neighbour)) { + continue; + } + if (!isFaceVisible(pos, face, cameraPos)) { + continue; + } + drawFaceOutline(matrix, lines, pos, cameraPos, face); + } + } + + /** + * 判断某个面是否朝向相机。 + * + *

取面中心指向相机的方向与该面外法线做点积,结果为正表示相机位于该面正面。 + * 面中心为方块中心沿法线偏移半格,因此该判定等价于相机是否位于该面所在平面之外。 + * + * @param pos 方块坐标 + * @param face 待判定的面 + * @param cameraPos 相机位置 + * @return 该面朝向相机返回 true + */ + private static boolean isFaceVisible(BlockPos pos, Direction face, Vec3 cameraPos) { + double centerX = pos.getX() + 0.5 + face.getStepX() * 0.5; + double centerY = pos.getY() + 0.5 + face.getStepY() * 0.5; + double centerZ = pos.getZ() + 0.5 + face.getStepZ() * 0.5; + double dx = cameraPos.x - centerX; + double dy = cameraPos.y - centerY; + double dz = cameraPos.z - centerZ; + return dx * face.getStepX() + dy * face.getStepY() + dz * face.getStepZ() > 0.0; + } + + /** + * 提交单个面的四条棱。 + * + *

坐标按 {@link #EDGE_INFLATE} 外扩,与原点框使用同一几何,使两类边框在公共棱上完全对齐。 + * 顶点逐个内联提交,不构造中间数组:每帧面数可达数万,临时数组会显著增加垃圾回收压力。 + */ + private static void drawFaceOutline(Matrix4f matrix, VertexConsumer lines, BlockPos pos, Vec3 cameraPos, + Direction face) { + double x1 = pos.getX() - EDGE_INFLATE; + double y1 = pos.getY() - EDGE_INFLATE; + double z1 = pos.getZ() - EDGE_INFLATE; + double x2 = pos.getX() + 1.0 + EDGE_INFLATE; + double y2 = pos.getY() + 1.0 + EDGE_INFLATE; + double z2 = pos.getZ() + 1.0 + EDGE_INFLATE; + + switch (face) { + case DOWN -> { + edge(matrix, lines, cameraPos, face, x1, y1, z1, x2, y1, z1); + edge(matrix, lines, cameraPos, face, x2, y1, z1, x2, y1, z2); + edge(matrix, lines, cameraPos, face, x2, y1, z2, x1, y1, z2); + edge(matrix, lines, cameraPos, face, x1, y1, z2, x1, y1, z1); + } + case UP -> { + edge(matrix, lines, cameraPos, face, x1, y2, z1, x1, y2, z2); + edge(matrix, lines, cameraPos, face, x1, y2, z2, x2, y2, z2); + edge(matrix, lines, cameraPos, face, x2, y2, z2, x2, y2, z1); + edge(matrix, lines, cameraPos, face, x2, y2, z1, x1, y2, z1); + } + case NORTH -> { + edge(matrix, lines, cameraPos, face, x1, y1, z1, x1, y2, z1); + edge(matrix, lines, cameraPos, face, x1, y2, z1, x2, y2, z1); + edge(matrix, lines, cameraPos, face, x2, y2, z1, x2, y1, z1); + edge(matrix, lines, cameraPos, face, x2, y1, z1, x1, y1, z1); + } + case SOUTH -> { + edge(matrix, lines, cameraPos, face, x1, y1, z2, x2, y1, z2); + edge(matrix, lines, cameraPos, face, x2, y1, z2, x2, y2, z2); + edge(matrix, lines, cameraPos, face, x2, y2, z2, x1, y2, z2); + edge(matrix, lines, cameraPos, face, x1, y2, z2, x1, y1, z2); + } + case WEST -> { + edge(matrix, lines, cameraPos, face, x1, y1, z1, x1, y1, z2); + edge(matrix, lines, cameraPos, face, x1, y1, z2, x1, y2, z2); + edge(matrix, lines, cameraPos, face, x1, y2, z2, x1, y2, z1); + edge(matrix, lines, cameraPos, face, x1, y2, z1, x1, y1, z1); + } + case EAST -> { + edge(matrix, lines, cameraPos, face, x2, y1, z1, x2, y2, z1); + edge(matrix, lines, cameraPos, face, x2, y2, z1, x2, y2, z2); + edge(matrix, lines, cameraPos, face, x2, y2, z2, x2, y1, z2); + edge(matrix, lines, cameraPos, face, x2, y1, z2, x2, y1, z1); + } + } + } + + /** 提交一条棱的两个端点,法线取所属面的朝向。 */ + private static void edge(Matrix4f matrix, VertexConsumer lines, Vec3 cameraPos, Direction face, + double ax, double ay, double az, double bx, double by, double bz) { + addVertex(matrix, lines, cameraPos, face, ax, ay, az); + addVertex(matrix, lines, cameraPos, face, bx, by, bz); + } + + /** + * 提交单个顶点。 + * + *

顶点坐标复用 {@link #VERTEX_SCRATCH},避免每个顶点分配一个 {@link Vector3f}: + * 最坏情形下单帧顶点数可达数万,逐顶点分配会造成明显的垃圾回收开销。 + */ + private static void addVertex(Matrix4f matrix, VertexConsumer lines, Vec3 cameraPos, Direction face, + double worldX, double worldY, double worldZ) { + Vector3f vertex = VERTEX_SCRATCH; + vertex.set((float) (worldX - cameraPos.x), + (float) (worldY - cameraPos.y), + (float) (worldZ - cameraPos.z)); + matrix.transformPosition(vertex); + lines.vertex(vertex.x, vertex.y, vertex.z) + .color(EDGE_RED, EDGE_GREEN, EDGE_BLUE, 0.85F) + .normal(face.getStepX(), face.getStepY(), face.getStepZ()).endVertex(); + } + + /** 主手或副手持有造化杖时才绘制:其余物品不触发连锁,其缓存预览没有意义。 */ + private static boolean holdsBeafTool(LocalPlayer player) { + for (InteractionHand hand : InteractionHand.values()) { + ItemStack stack = player.getItemInHand(hand); + if (BeefToolVariants.isBeafTool(stack)) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/render/ConstructionWandPreviewRenderer.java b/src/main/java/com/sorrowmist/useless/client/render/ConstructionWandPreviewRenderer.java new file mode 100644 index 00000000..6a08bb88 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/render/ConstructionWandPreviewRenderer.java @@ -0,0 +1,208 @@ +package com.sorrowmist.useless.client.render; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.api.enums.tool.ConstructionWandCoreMode; +import com.sorrowmist.useless.compat.constructionwand.ConstructionWandLogic; +import com.sorrowmist.useless.core.component.UComponents; +import com.sorrowmist.useless.network.ConstructionWandPreviewRequestPacket; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.LevelRenderer; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod.EventBusSubscriber; +import net.minecraftforge.client.event.RenderHighlightEvent; +import net.minecraftforge.client.event.RenderLevelStageEvent; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; + +import java.util.List; + +@EventBusSubscriber(modid = UselessMod.MODID, value = Dist.CLIENT) +public final class ConstructionWandPreviewRenderer { + private static List preview = List.of(); + private static BlockPos requestedPos; + private static Direction requestedFace; + private static Direction requestedStep; + private static InteractionHand requestedHand; + private static ConstructionWandCoreMode requestedCore; + private static boolean requestedAir; + private static ItemStack requestedTool = ItemStack.EMPTY; + private static ItemStack requestedOffhand = ItemStack.EMPTY; + private static long lastRequestTick = Long.MIN_VALUE; + private static int nextRequestId; + private static int latestRequestId = -1; + + private ConstructionWandPreviewRenderer() {} + + public static void setPreview(int requestId, List positions) { + if (requestId != latestRequestId) return; + preview = List.copyOf(positions); + } + + @SubscribeEvent + public static void render(RenderHighlightEvent.Block event) { + Minecraft minecraft = Minecraft.getInstance(); + if (minecraft.level == null || minecraft.player == null) return; + + InteractionHand hand = activeHand(minecraft); + if (hand == null) { + clear(); + return; + } + + BlockHitResult hit = event.getTarget(); + ConstructionWandCoreMode core = activeCore(minecraft, hand); + ItemStack tool = minecraft.player.getItemInHand(hand); + ItemStack offhand = minecraft.player.getOffhandItem(); + long tick = minecraft.level.getGameTime(); + if (requestedAir + || requestedPos == null + || !requestedPos.equals(hit.getBlockPos()) + || requestedFace != hit.getDirection() + || requestedHand != hand + || requestedCore != core + || !ItemStack.matches(requestedTool, tool) + || !ItemStack.matches(requestedOffhand, offhand) + || tick - lastRequestTick >= 5) { + int requestId = beginRequest(hit.getBlockPos(), hit.getDirection(), null, + hand, core, false, tick, tool, offhand); + preview = List.of(); + PacketDistributor.sendToServer( + ConstructionWandPreviewRequestPacket.block(requestId, hit, hand)); + } + + if (!renderPreview(event.getPoseStack(), event.getMultiBufferSource(), event.getCamera())) return; + event.setCanceled(true); + } + + @SubscribeEvent + public static void renderAirPreview(RenderLevelStageEvent event) { + if (event.getStage() != RenderLevelStageEvent.Stage.AFTER_PARTICLES) return; + + Minecraft minecraft = Minecraft.getInstance(); + if (minecraft.level == null || minecraft.player == null) { + clear(); + return; + } + + InteractionHand hand = activeHand(minecraft); + HitResult hit = minecraft.hitResult; + if (hand == null || hit == null) { + clear(); + return; + } + if (hit.getType() != HitResult.Type.MISS) { + if (hit.getType() != HitResult.Type.BLOCK) clear(); + return; + } + + ConstructionWandCoreMode core = activeCore(minecraft, hand); + ItemStack offhand = minecraft.player.getOffhandItem(); + if (core != ConstructionWandCoreMode.ANGEL + || !(offhand.getItem() instanceof BlockItem)) { + clear(); + return; + } + + BlockPos start = ConstructionWandLogic.angelAirStart(minecraft.player); + Direction step = ConstructionWandLogic.angelAirStep(minecraft.player); + ItemStack tool = minecraft.player.getItemInHand(hand); + long tick = minecraft.level.getGameTime(); + if (!requestedAir + || requestedPos == null + || !requestedPos.equals(start) + || requestedStep != step + || requestedHand != hand + || requestedCore != core + || !ItemStack.matches(requestedTool, tool) + || !ItemStack.matches(requestedOffhand, offhand) + || tick - lastRequestTick >= 5) { + int requestId = beginRequest(start, Direction.UP, step, hand, core, true, + tick, tool, offhand); + preview = List.of(); + PacketDistributor.sendToServer(ConstructionWandPreviewRequestPacket.air(requestId, hand)); + } + + MultiBufferSource.BufferSource buffer = minecraft.renderBuffers().bufferSource(); + PoseStack pose = event.getPoseStack(); + pose.pushPose(); + + if (renderPreview(pose, buffer, event.getCamera())) { + buffer.endBatch(RenderType.lines()); + } + pose.popPose(); + } + + private static boolean renderPreview(PoseStack pose, MultiBufferSource buffers, Camera camera) { + if (preview.isEmpty()) return false; + + VertexConsumer lines = buffers.getBuffer(RenderType.lines()); + Vec3 cameraPos = camera.getPosition(); + pose.pushPose(); + for (BlockPos pos : preview) { + AABB box = new AABB(pos).move(-cameraPos.x, -cameraPos.y, -cameraPos.z); + LevelRenderer.renderLineBox(pose, lines, box, 0.25F, 1.0F, 0.35F, 0.9F); + } + pose.popPose(); + return true; + } + + private static int beginRequest(BlockPos pos, Direction face, Direction step, + InteractionHand hand, ConstructionWandCoreMode core, + boolean air, long tick, ItemStack tool, ItemStack offhand) { + int requestId = nextRequestId++; + latestRequestId = requestId; + requestedPos = pos.immutable(); + requestedFace = face; + requestedStep = step; + requestedHand = hand; + requestedCore = core; + requestedAir = air; + requestedTool = tool.copy(); + requestedOffhand = offhand.copy(); + lastRequestTick = tick; + return requestId; + } + + private static InteractionHand activeHand(Minecraft minecraft) { + if (ConstructionWandLogic.isEnabled(minecraft.player.getMainHandItem())) { + return InteractionHand.MAIN_HAND; + } + if (ConstructionWandLogic.isEnabled(minecraft.player.getOffhandItem())) { + return InteractionHand.OFF_HAND; + } + return null; + } + + private static ConstructionWandCoreMode activeCore(Minecraft minecraft, InteractionHand hand) { + return com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(minecraft.player.getItemInHand(hand), + UComponents.ConstructionWandCoreComponent.get(), ConstructionWandCoreMode.DEFAULT); + } + + private static void clear() { + preview = List.of(); + requestedPos = null; + requestedFace = null; + requestedStep = null; + requestedHand = null; + requestedCore = null; + requestedAir = false; + requestedTool = ItemStack.EMPTY; + requestedOffhand = ItemStack.EMPTY; + lastRequestTick = Long.MIN_VALUE; + latestRequestId = -1; + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/render/ExPatternProviderRenderer.java b/src/main/java/com/sorrowmist/useless/client/render/ExPatternProviderRenderer.java deleted file mode 100644 index 54a58ae5..00000000 --- a/src/main/java/com/sorrowmist/useless/client/render/ExPatternProviderRenderer.java +++ /dev/null @@ -1,234 +0,0 @@ -package com.sorrowmist.useless.client.render; - -import com.glodblock.github.extendedae.common.tileentities.TileExPatternProvider; -import com.sorrowmist.useless.items.EndlessBeafItem; -import com.mojang.blaze3d.vertex.PoseStack; -import com.mojang.blaze3d.vertex.VertexConsumer; -import com.mojang.math.Axis; -import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.MultiBufferSource; -import net.minecraft.client.renderer.RenderType; -import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; -import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; -import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.level.Level; -import net.minecraft.world.level.block.entity.BlockEntity; -import net.minecraftforge.api.distmarker.Dist; -import net.minecraftforge.api.distmarker.OnlyIn; -import net.minecraftforge.client.event.EntityRenderersEvent; -import net.minecraftforge.eventbus.api.SubscribeEvent; -import net.minecraftforge.fml.common.Mod; - -import java.util.Map; -import java.util.Set; -import java.util.UUID; - -@OnlyIn(Dist.CLIENT) -@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD, value = Dist.CLIENT, modid = "useless_mod") -public class ExPatternProviderRenderer implements BlockEntityRenderer { - - // 纹理资源位置 - private static final ResourceLocation MASTER_TEXTURE = new ResourceLocation("useless_mod:textures/icon/master.png"); - private static final ResourceLocation SLAVE_TEXTURE = new ResourceLocation("useless_mod:textures/icon/slave.png"); - - float thickness = 0.05f; - - public ExPatternProviderRenderer(BlockEntityRendererProvider.Context ctx) {} - - @Override - public int getViewDistance() { - return 512; - } - - @Override - public boolean shouldRenderOffScreen(TileExPatternProvider be) { - // 始终允许渲染,因为我们需要检查玩家是否手持工具 - return true; - } - - @Override - public void render(TileExPatternProvider be, float partialTicks, PoseStack poseStack, - MultiBufferSource buffers, int packedLight, int packedOverlay) { - if (be == null) return; - - Level level = be.getLevel(); - if (level == null) return; - - // 检查玩家是否手持牛排工具(只检查主手和副手) - Player player = Minecraft.getInstance().player; - if (player == null) return; - - boolean holdingItem = false; - ItemStack mainHand = player.getMainHandItem(); - ItemStack offHand = player.getOffhandItem(); - if ((mainHand.getItem() instanceof EndlessBeafItem) || (offHand.getItem() instanceof EndlessBeafItem)) { - holdingItem = true; - } - - // 如果玩家没有手持牛排工具,不渲染 - if (!holdingItem) return; - - BlockPos currentPos = be.getBlockPos(); - - // 检查当前方块是否是主供应器或从供应器 - boolean isMaster = EndlessBeafItem.isMasterPatternProvider(currentPos); - boolean isSlave = EndlessBeafItem.isSlavePatternProvider(currentPos); - - // 只渲染主端或当前选定主端的从端 - boolean shouldRender = false; - ResourceLocation texture = null; - - if (isMaster) { - // 主端始终渲染 - shouldRender = true; - texture = MASTER_TEXTURE; - } else if (isSlave) { - // 从端只有当它属于当前选定的主端时才渲染 - if (EndlessBeafItem.isSlaveOfCurrentMaster(currentPos, level)) { - shouldRender = true; - texture = SLAVE_TEXTURE; - } - } - - if (!shouldRender) return; - - // 保存状态 - poseStack.pushPose(); - - // 在当前方块的六个面上渲染图片 - for (Direction direction : Direction.values()) { - renderStaticLinkPoint(poseStack, buffers, direction, packedLight, texture); - } - - // 恢复状态 - poseStack.popPose(); - } - - /** - * 在指定面上渲染静态图片 - */ - private void renderStaticLinkPoint(PoseStack poseStack, MultiBufferSource buffers, Direction face, int packedLight, ResourceLocation texture) { - poseStack.pushPose(); - - // 图片大小 - float size = 0.2f; - // 增加偏移量,确保图片渲染在机器表面 - float offset = 0.01f; - - // 根据面调整位置和旋转 - switch (face) { - case DOWN: - poseStack.translate(0.5f, -offset, 0.5f); - poseStack.mulPose(Axis.XP.rotationDegrees(90.0f)); - poseStack.mulPose(Axis.YP.rotationDegrees(180.0f)); - break; - case UP: - poseStack.translate(0.5f, 1.0f + offset, 0.5f); - poseStack.mulPose(Axis.XP.rotationDegrees(-90.0f)); - poseStack.mulPose(Axis.YP.rotationDegrees(180.0f)); - break; - case NORTH: - poseStack.translate(0.5f, 0.5f, -offset); - // 北面不需要额外旋转,直接显示 - break; - case SOUTH: - poseStack.translate(0.5f, 0.5f, 1.0f + offset); - poseStack.mulPose(Axis.YP.rotationDegrees(180.0f)); - break; - case WEST: - poseStack.translate(-offset, 0.5f, 0.5f); - poseStack.mulPose(Axis.YP.rotationDegrees(90.0f)); - break; - case EAST: - poseStack.translate(1.0f + offset, 0.5f, 0.5f); - poseStack.mulPose(Axis.YP.rotationDegrees(-90.0f)); - break; - } - - // 使用适合纹理渲染的渲染类型 - RenderType renderType = RenderType.text(texture); - VertexConsumer vertexConsumer = buffers.getBuffer(renderType); - - // 渲染图片 - renderTextureQuad(poseStack, vertexConsumer, size, packedLight); - - poseStack.popPose(); - } - - /** - * 渲染完整的纹理图像,确保图像完整显示且方向正确 - */ - private void renderTextureQuad(PoseStack poseStack, VertexConsumer consumer, float size, int packedLight) { - PoseStack.Pose pose = poseStack.last(); - - // 颜色(白色,不改变纹理颜色) - float r = 1.0f; - float g = 1.0f; - float b = 1.0f; - float a = 1.0f; - - // 基础顶点坐标 - float x1 = -size; - float y1 = -size; - float x2 = size; - float y2 = size; - - // 使用标准UV坐标 - float u1 = 0.0f; - float v1 = 0.0f; - float u2 = 1.0f; - float v2 = 1.0f; - - // 渲染四边形,使用正确的顶点顺序 - // 顶点顺序:左上 -> 右上 -> 右下 -> 左下 - // 左上 - consumer.vertex(pose.pose(), x1, y2, 0.0f) - .color(r, g, b, a) - .uv(u1, v1) - .uv2(packedLight) - .endVertex(); - - // 右上 - consumer.vertex(pose.pose(), x2, y2, 0.0f) - .color(r, g, b, a) - .uv(u2, v1) - .uv2(packedLight) - .endVertex(); - - // 右下 - consumer.vertex(pose.pose(), x2, y1, 0.0f) - .color(r, g, b, a) - .uv(u2, v2) - .uv2(packedLight) - .endVertex(); - - // 左下 - consumer.vertex(pose.pose(), x1, y1, 0.0f) - .color(r, g, b, a) - .uv(u1, v2) - .uv2(packedLight) - .endVertex(); - } - - // 注册渲染器 - @SubscribeEvent - public static void registerRenderers(EntityRenderersEvent.RegisterRenderers event) { - // 只有在ExtendedAE模组安装时才注册渲染器 - try { - // 检查ExtendedAE类是否存在 - Class.forName("com.glodblock.github.extendedae.common.tileentities.TileExPatternProvider"); - // 使用GlodUtil.getTileType获取BlockEntityType,确保与ExtendedAE兼容 - event.registerBlockEntityRenderer(com.glodblock.github.glodium.util.GlodUtil.getTileType(TileExPatternProvider.class), ExPatternProviderRenderer::new); - } catch (ClassNotFoundException e) { - // ExtendedAE模组未安装,跳过注册 - } - } - - // 需要将EndlessBeafItem中的静态字段设为public或提供访问方法 - // 这里我们假设EndlessBeafItem中的masterToSlaves和slaveToMaster字段是public的 - // 如果不是,需要修改EndlessBeafItem类,添加getter方法 -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/client/render/PatternOutputCache.java b/src/main/java/com/sorrowmist/useless/client/render/PatternOutputCache.java new file mode 100644 index 00000000..5e659dd2 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/render/PatternOutputCache.java @@ -0,0 +1,84 @@ +package com.sorrowmist.useless.client.render; + +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.GenericStack; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.Supplier; + +/** Caches the output used to paint encoded patterns in inventory screens. */ +final class PatternOutputCache { + private static final int MAX_ENTRIES_PER_LEVEL = 512; + private static final Object LOCK = new Object(); + private static final Map LEVEL_CACHES = new java.util.WeakHashMap<>(); + private static final Map> LEVEL_INDEPENDENT_CACHE = + new LinkedHashMap<>(64, 0.75F, true) { + @Override + protected boolean removeEldestEntry(Map.Entry> eldest) { + return size() > MAX_ENTRIES_PER_LEVEL; + } + }; + + private PatternOutputCache() { + } + + static Optional get( + AEItemKey key, @Nullable Level level, long generation, Supplier resolver) { + if (level == null) { + synchronized (LOCK) { + Optional cached = LEVEL_INDEPENDENT_CACHE.get(key); + if (cached != null) { + return cached; + } + } + Optional resolved = resolve(resolver); + synchronized (LOCK) { + LEVEL_INDEPENDENT_CACHE.put(key, resolved); + } + return resolved; + } + + LevelCache cache; + synchronized (LOCK) { + cache = LEVEL_CACHES.computeIfAbsent(level, ignored -> new LevelCache()); + if (cache.generation != generation) { + cache.generation = generation; + cache.entries.clear(); + } + Optional cached = cache.entries.get(key); + if (cached != null) { + return cached; + } + } + + Optional resolved = resolve(resolver); + synchronized (LOCK) { + cache.entries.put(key, resolved); + } + return resolved; + } + + private static Optional resolve(Supplier resolver) { + try { + return Optional.ofNullable(resolver.get()); + } catch (RuntimeException ignored) { + return Optional.empty(); + } + } + + private static final class LevelCache { + private long generation = Long.MIN_VALUE; + private final LinkedHashMap> entries = + new LinkedHashMap<>(64, 0.75F, true) { + @Override + protected boolean removeEldestEntry( + Map.Entry> eldest) { + return size() > MAX_ENTRIES_PER_LEVEL; + } + }; + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/render/PatternSlotRenderer.java b/src/main/java/com/sorrowmist/useless/client/render/PatternSlotRenderer.java new file mode 100644 index 00000000..e4154528 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/render/PatternSlotRenderer.java @@ -0,0 +1,116 @@ +package com.sorrowmist.useless.client.render; + +import appeng.api.crafting.PatternDetailsHelper; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.GenericStack; +import appeng.crafting.pattern.AEProcessingPattern; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeCatalog; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +public final class PatternSlotRenderer { + private static final float AMOUNT_SCALE = 0.6F; + private static final String[] AMOUNT_PREFIXES = {"k", "M", "G", "T", "P", "E", "Z", "Y"}; + + private PatternSlotRenderer() { + } + + public static boolean renderPattern(GuiGraphics graphics, Font font, ItemStack pattern, + int x, int y, int seed, @Nullable Level level) { + return renderPattern(graphics, font, getPrimaryOutput(pattern, level), x, y, seed); + } + + /** Renders a display output which was already decoded by the caller. */ + public static boolean renderPattern(GuiGraphics graphics, Font font, @Nullable GenericStack output, + int x, int y, int seed) { + if (output == null) return false; + ItemStack display = output.what().wrapForDisplayOrFilter(); + graphics.renderItem(display, x, y, seed); + renderAmount(graphics, font, x, y, output); + return true; + } + + @Nullable + public static GenericStack getPrimaryOutput(ItemStack pattern, @Nullable Level level) { + if (pattern.isEmpty()) return null; + AEItemKey key = AEItemKey.of(pattern); + if (key == null) return null; + + return PatternOutputCache.get(key, level, AlloyFurnaceRecipeCatalog.generation(), () -> { + // Processing patterns already carry their display inputs and outputs. Decoding them + // via PatternDetailsHelper can invoke dynamic recipe compatibility solely to render + // an icon. + if (com.sorrowmist.useless.compat.forge1201.recipe.NativeProcessingPatterns.hasPayload(pattern)) { + return getEncodedProcessingPrimaryOutput(pattern); + } + if (level == null || !PatternDetailsHelper.isEncodedPattern(pattern)) return null; + var details = PatternDetailsHelper.decodePattern(pattern, level); + return details == null || (details.getOutputs().length == 0) + ? null : details.getOutputs()[0]; + }).orElse(null); + } + + @Nullable + private static GenericStack getEncodedProcessingPrimaryOutput(ItemStack pattern) { + AEItemKey definition = AEItemKey.of(pattern); + if (definition == null) return null; + try { + var source = new AEProcessingPattern(definition); + return (source.getOutputs().length == 0) ? null : source.getOutputs()[0]; + } catch (RuntimeException ignored) { + // Invalid patterns are rendered normally and retain AE2's usual invalid tooltip. + return null; + } + } + + private static void renderAmount(GuiGraphics graphics, Font font, int x, int y, GenericStack output) { + String label = formatAmount(output); + if (label.isEmpty()) return; + + int scaledWidth = (int) (font.width(label) * AMOUNT_SCALE); + int textX = x + 16 - scaledWidth; + int textY = y + 11; + var pose = graphics.pose(); + pose.pushPose(); + pose.translate(0.0F, 0.0F, 300.0F); + pose.scale(AMOUNT_SCALE, AMOUNT_SCALE, 1.0F); + graphics.drawString(font, label, + (int) (textX / AMOUNT_SCALE), (int) (textY / AMOUNT_SCALE), 0xFFFFFFFF, true); + pose.popPose(); + } + + static String formatAmount(GenericStack output) { + long amount = output.amount(); + int amountPerUnit = output.what().getAmountPerUnit(); + if (amount <= 0 || amountPerUnit <= 0) return ""; + + double units = (double) amount / amountPerUnit; + String unitSymbol = output.what().getUnitSymbol(); + return formatNumber(units) + (unitSymbol == null ? "" : unitSymbol); + } + + private static String formatNumber(double value) { + String prefix = ""; + for (int i = 0; value >= 1000.0D && i < AMOUNT_PREFIXES.length; i++) { + value /= 1000.0D; + prefix = AMOUNT_PREFIXES[i]; + } + + String number; + if (value == Math.rint(value)) { + number = Long.toString((long) value); + } else { + number = BigDecimal.valueOf(value) + .setScale(2, RoundingMode.HALF_UP) + .stripTrailingZeros() + .toPlainString(); + } + return number + prefix; + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/render/StaffLinkHighlightRenderer.java b/src/main/java/com/sorrowmist/useless/client/render/StaffLinkHighlightRenderer.java new file mode 100644 index 00000000..a75f0e97 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/render/StaffLinkHighlightRenderer.java @@ -0,0 +1,165 @@ +package com.sorrowmist.useless.client.render; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.items.BeefToolVariants; +import com.sorrowmist.useless.content.items.EndlessBeafItem; +import com.sorrowmist.useless.network.StaffLinkHighlightRequestPacket; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.LevelRenderer; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.core.GlobalPos; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod.EventBusSubscriber; +import net.minecraftforge.event.TickEvent.ClientTickEvent; +import net.minecraftforge.client.event.RenderLevelStageEvent; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; + +import java.util.List; + +/** + * 手持启用无线物流的造化杖时,按流向绘制服务端下发的容器边框。 + * 每 {@link #REFRESH_INTERVAL} tick 请求最新数据,界面关闭时仍能显示。 + * + *

与 {@link AeLinkHighlightRenderer} 共用 1.20.1 的世界视图阶段 + * AFTER_PARTICLES,只减一次相机位置,不能使用 AFTER_LEVEL 的投影栈。

+ */ +@EventBusSubscriber(modid = UselessMod.MODID, value = Dist.CLIENT) +public final class StaffLinkHighlightRenderer { + /** 提示数据变化很慢,没必要每 tick 问服务端。 */ + private static final int REFRESH_INTERVAL = 10; + /** 网络可以铺得很远,太远的框既看不见又白费顶点,裁掉。 */ + private static final double MAX_RENDER_DISTANCE = 128.0; + + /** 发送端:至少有一条启用的「释放」线路。 */ + private static final float RELEASE_RED = 1.00F; + private static final float RELEASE_GREEN = 0.55F; + private static final float RELEASE_BLUE = 0.15F; + /** 接收端:只有启用的「吸收」线路。 */ + private static final float ABSORB_RED = 0.35F; + private static final float ABSORB_GREEN = 0.72F; + private static final float ABSORB_BLUE = 1.00F; + /** 已绑定但所有线路都关着。 */ + private static final float DISABLED_RED = 0.45F; + private static final float DISABLED_GREEN = 0.45F; + private static final float DISABLED_BLUE = 0.48F; + private static final float LINE_ALPHA = 0.95F; + + private static List releaseAnchors = List.of(); + private static List absorbAnchors = List.of(); + private static List disabledAnchors = List.of(); + private static int tickCounter = REFRESH_INTERVAL; + + private StaffLinkHighlightRenderer() { + } + + /** 客户端收到服务端的分类结果。 */ + public static void setHighlights(List release, List absorb, + List disabled) { + releaseAnchors = List.copyOf(release); + absorbAnchors = List.copyOf(absorb); + disabledAnchors = List.copyOf(disabled); + } + + @SubscribeEvent + public static void onClientTick(ClientTickEvent event) { + if (event.phase != net.minecraftforge.event.TickEvent.Phase.END) return; + Minecraft minecraft = Minecraft.getInstance(); + if (minecraft.level == null || minecraft.player == null || !shouldRequest(minecraft)) { + clear(); + return; + } + if (++tickCounter < REFRESH_INTERVAL) { + return; + } + tickCounter = 0; + PacketDistributor.sendToServer(new StaffLinkHighlightRequestPacket()); + } + + @SubscribeEvent + public static void onRenderStage(RenderLevelStageEvent event) { + if (event.getStage() != RenderLevelStageEvent.Stage.AFTER_PARTICLES) { + return; + } + Minecraft minecraft = Minecraft.getInstance(); + if (minecraft.level == null || minecraft.player == null || !shouldRequest(minecraft) + || (releaseAnchors.isEmpty() && absorbAnchors.isEmpty() && disabledAnchors.isEmpty())) { + return; + } + + Camera camera = event.getCamera(); + Vec3 cameraPos = camera.getPosition(); + PoseStack pose = event.getPoseStack(); + MultiBufferSource.BufferSource buffer = minecraft.renderBuffers().bufferSource(); + + pose.pushPose(); + + VertexConsumer lines = buffer.getBuffer(RenderType.lines()); + + ResourceKey dimension = minecraft.level.dimension(); + drawAll(pose, lines, cameraPos, dimension, releaseAnchors, + RELEASE_RED, RELEASE_GREEN, RELEASE_BLUE); + drawAll(pose, lines, cameraPos, dimension, absorbAnchors, + ABSORB_RED, ABSORB_GREEN, ABSORB_BLUE); + drawAll(pose, lines, cameraPos, dimension, disabledAnchors, + DISABLED_RED, DISABLED_GREEN, DISABLED_BLUE); + + pose.popPose(); + buffer.endBatch(RenderType.lines()); + } + + private static void drawAll(PoseStack pose, VertexConsumer lines, Vec3 cameraPos, + ResourceKey dimension, List anchors, + float red, float green, float blue) { + for (GlobalPos anchor : anchors) { + // 一张网络的锚点可以跨维度,这里只画当前维度那批。 + if (!anchor.dimension().equals(dimension)) { + continue; + } + if (Vec3.atCenterOf(anchor.pos()).distanceToSqr(cameraPos) + > MAX_RENDER_DISTANCE * MAX_RENDER_DISTANCE) { + continue; + } + AABB box = new AABB(anchor.pos()).inflate(0.0025) + .move(-cameraPos.x, -cameraPos.y, -cameraPos.z); + LevelRenderer.renderLineBox(pose, lines, box, red, green, blue, LINE_ALPHA); + } + } + + /** + * 只在手上真的拿着「开着无线物流模式」的造化杖时才要数据。 + * + *

刻意不在这里判断「有没有网络」:网络列表现在挂在归属者名下、存在服务端, + * 客户端手里没有这份数据。一张都没有时服务端会回空包,自然什么都不画。

+ */ + private static boolean shouldRequest(Minecraft minecraft) { + for (InteractionHand hand : InteractionHand.values()) { + ItemStack stack = minecraft.player.getItemInHand(hand); + if (!BeefToolVariants.isBeafTool(stack)) { + continue; + } + if (EndlessBeafItem.isStaffLinkEnabled(stack)) { + return true; + } + } + return false; + } + + /** 清空缓存并把刷新计数器置为上限,使下次手持当 tick 立即请求一次最新数据。 */ + private static void clear() { + releaseAnchors = List.of(); + absorbAnchors = List.of(); + disabledAnchors = List.of(); + tickCounter = REFRESH_INTERVAL; + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/render/ctm/CtmBakedModel.java b/src/main/java/com/sorrowmist/useless/client/render/ctm/CtmBakedModel.java new file mode 100644 index 00000000..eea17242 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/render/ctm/CtmBakedModel.java @@ -0,0 +1,286 @@ +package com.sorrowmist.useless.client.render.ctm; + +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.block.model.BakedQuad; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.client.resources.model.BakedModel; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.BlockAndTintGetter; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraftforge.client.model.BakedModelWrapper; +import net.minecraftforge.client.model.IQuadTransformer; +import net.minecraftforge.client.model.data.ModelData; +import net.minecraftforge.client.model.data.ModelProperty; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +final class CtmBakedModel extends BakedModelWrapper { + private static final ModelProperty CONNECTIONS = new ModelProperty<>(); + + private final List textures; + private volatile List spritePairs = List.of(); + private final ConcurrentMap> quadCache = + new ConcurrentHashMap<>(); + + CtmBakedModel(BakedModel originalModel, ResourceLocation base, ResourceLocation ctm, + @Nullable ResourceLocation overlayBase, @Nullable ResourceLocation overlayCtm) { + super(originalModel); + var pairs = new ArrayList(2); + pairs.add(new TexturePair(base, ctm)); + if (overlayBase != null && overlayCtm != null) pairs.add(new TexturePair(overlayBase, overlayCtm)); + textures = List.copyOf(pairs); + } + + // Called on the render thread once the current atlas has been uploaded. Each reload creates + // fresh wrappers, so worker threads never retain sprites from the previous atlas generation. + void bindSprites(java.util.function.Function getter) { + spritePairs = textures.stream().map(pair -> new SpritePair( + java.util.Objects.requireNonNull(getter.apply(pair.base())), + java.util.Objects.requireNonNull(getter.apply(pair.ctm())))).toList(); + quadCache.clear(); + } + + @Override + public ModelData getModelData( + BlockAndTintGetter level, BlockPos pos, BlockState state, + ModelData modelData) { + ModelData originalData = originalModel.getModelData(level, pos, state, modelData); + return originalData.derive() + .with(CONNECTIONS, CtmConnectionMask.collect(level, pos, state)) + .build(); + } + + @Override + public List getQuads( + @Nullable BlockState state, @Nullable Direction side, RandomSource random, + ModelData modelData, @Nullable RenderType renderType) { + Long packed = modelData.get(CONNECTIONS); + if (state == null || packed == null || spritePairs.isEmpty()) { + return originalModel.getQuads(state, side, random, modelData, renderType); + } + QuadCacheKey key = new QuadCacheKey(packed, side == null ? -1 : side.ordinal()); + return quadCache.computeIfAbsent(key, ignored -> transform( + originalModel.getQuads(state, side, random, modelData, renderType), packed)); + } + + private List transform(List source, long packed) { + if (source.isEmpty()) { + return List.of(); + } + List result = new ArrayList<>(source.size() * 4); + for (BakedQuad quad : source) { + SpritePair sprites = spritePairFor(quad); + if (sprites == null) { + result.add(quad); + continue; + } + result.addAll(slice(quad, CtmConnectionMask.forFace(packed, quad.getDirection()), + sprites.base(), sprites.ctm())); + } + return List.copyOf(result); + } + + private List slice( + BakedQuad source, int mask, TextureAtlasSprite baseSprite, + TextureAtlasSprite ctmSprite) { + Direction face = source.getDirection(); + int[] sourceVertices = canonicalizeWinding(source.getVertices(), face); + CtmFaceAxes axes = CtmFaceAxes.forFace(face); + float[][][] positions = new float[2][2][3]; + float[][] sourceU = new float[2][2]; + float[][] sourceV = new float[2][2]; + boolean[][] found = new boolean[2][2]; + for (int vertex = 0; vertex < 4; vertex++) { + int offset = vertex * IQuadTransformer.STRIDE; + float x = Float.intBitsToFloat( + sourceVertices[offset + IQuadTransformer.POSITION]); + float y = Float.intBitsToFloat( + sourceVertices[offset + IQuadTransformer.POSITION + 1]); + float z = Float.intBitsToFloat( + sourceVertices[offset + IQuadTransformer.POSITION + 2]); + float faceU = coordinate(axes.right(), x, y, z); + float faceV = coordinate(axes.down(), x, y, z); + int cornerX = snap(faceU); + int cornerY = snap(faceV); + found[cornerX][cornerY] = true; + for (int axis = 0; axis < 3; axis++) { + positions[cornerX][cornerY][axis] = Float.intBitsToFloat( + sourceVertices[offset + IQuadTransformer.POSITION + axis]); + } + sourceU[cornerX][cornerY] = baseSprite.getUOffset(Float.intBitsToFloat( + sourceVertices[offset + IQuadTransformer.UV0])); + sourceV[cornerX][cornerY] = baseSprite.getVOffset(Float.intBitsToFloat( + sourceVertices[offset + IQuadTransformer.UV0 + 1])); + } + if (!found[0][0] || !found[1][0] || !found[0][1] || !found[1][1]) { + return List.of(source); + } + + List result = new ArrayList<>(4); + for (int cornerY = 0; cornerY < 2; cornerY++) { + for (int cornerX = 0; cornerX < 2; cornerX++) { + CtmQuadrantSelector.Tile tile = + CtmQuadrantSelector.select(mask, cornerX, cornerY); + TextureAtlasSprite sprite = tile.baseTexture() ? baseSprite : ctmSprite; + int[] vertices = Arrays.copyOf(sourceVertices, sourceVertices.length); + for (int vertex = 0; vertex < 4; vertex++) { + int offset = vertex * IQuadTransformer.STRIDE; + float x = Float.intBitsToFloat( + sourceVertices[offset + IQuadTransformer.POSITION]); + float y = Float.intBitsToFloat( + sourceVertices[offset + IQuadTransformer.POSITION + 1]); + float z = Float.intBitsToFloat( + sourceVertices[offset + IQuadTransformer.POSITION + 2]); + float vertexU = coordinate(axes.right(), x, y, z); + float vertexV = coordinate(axes.down(), x, y, z); + float geometryU = (cornerX + snap(vertexU)) * 0.5F; + float geometryV = (cornerY + snap(vertexV)) * 0.5F; + for (int axis = 0; axis < 3; axis++) { + vertices[offset + IQuadTransformer.POSITION + axis] = + Float.floatToRawIntBits(bilinear( + positions[0][0][axis], positions[1][0][axis], + positions[0][1][axis], positions[1][1][axis], + geometryU, geometryV)); + } + float interpolatedU = bilinear( + sourceU[0][0], sourceU[1][0], sourceU[0][1], sourceU[1][1], + geometryU, geometryV); + float interpolatedV = bilinear( + sourceV[0][0], sourceV[1][0], sourceV[0][1], sourceV[1][1], + geometryU, geometryV); + float textureU = tile.baseTexture() + ? interpolatedU + : (tile.x() + normalizeWithin( + interpolatedU, + bilinear(sourceU[0][0], sourceU[1][0], + sourceU[0][1], sourceU[1][1], + cornerX * 0.5F, geometryV), + bilinear(sourceU[0][0], sourceU[1][0], + sourceU[0][1], sourceU[1][1], + (cornerX + 1) * 0.5F, geometryV))) * 4.0F; + float textureV = tile.baseTexture() + ? interpolatedV + : (tile.y() + normalizeWithin( + interpolatedV, + bilinear(sourceV[0][0], sourceV[1][0], + sourceV[0][1], sourceV[1][1], + geometryU, cornerY * 0.5F), + bilinear(sourceV[0][0], sourceV[1][0], + sourceV[0][1], sourceV[1][1], + geometryU, (cornerY + 1) * 0.5F))) * 4.0F; + vertices[offset + IQuadTransformer.UV0] = + Float.floatToRawIntBits(sprite.getU(textureU)); + vertices[offset + IQuadTransformer.UV0 + 1] = + Float.floatToRawIntBits(sprite.getV(textureV)); + } + result.add(new BakedQuad(vertices, source.getTintIndex(), source.getDirection(), + sprite, source.isShade(), source.hasAmbientOcclusion())); + } + } + return result; + } + + /** + * Vanilla models do not promise a common starting vertex for every face. + * GregTech rotates the winding to a face-specific anchor before subdividing; + * doing the same keeps vertex lighting and quad winding stable on all sides. + */ + private static int[] canonicalizeWinding(int[] source, Direction face) { + int anchor = -1; + for (int vertex = 0; vertex < 4; vertex++) { + int offset = vertex * IQuadTransformer.STRIDE; + float x = Float.intBitsToFloat( + source[offset + IQuadTransformer.POSITION]); + float y = Float.intBitsToFloat( + source[offset + IQuadTransformer.POSITION + 1]); + float z = Float.intBitsToFloat( + source[offset + IQuadTransformer.POSITION + 2]); + boolean matches = switch (face) { + case UP -> near(x, 0.0F) && near(z, 0.0F); + case DOWN -> near(x, 0.0F) && near(z, 1.0F); + case NORTH -> near(x, 1.0F) && near(y, 1.0F); + case SOUTH -> near(x, 0.0F) && near(y, 1.0F); + case EAST -> near(y, 1.0F) && near(z, 1.0F); + case WEST -> near(y, 1.0F) && near(z, 0.0F); + }; + if (matches) { + anchor = vertex; + break; + } + } + if (anchor <= 0) { + return source; + } + int[] canonical = new int[source.length]; + for (int vertex = 0; vertex < 4; vertex++) { + int sourceVertex = (vertex + anchor) & 3; + System.arraycopy(source, + sourceVertex * IQuadTransformer.STRIDE, + canonical, + vertex * IQuadTransformer.STRIDE, + IQuadTransformer.STRIDE); + } + return canonical; + } + + private static boolean near(float first, float second) { + return Math.abs(first - second) < 0.01F; + } + + @Nullable + private SpritePair spritePairFor(BakedQuad quad) { + for (SpritePair sprites : spritePairs) { + if (quad.getSprite() == sprites.base() + || quad.getSprite().contents().name() + .equals(sprites.base().contents().name())) { + return sprites; + } + } + return null; + } + + private static float coordinate(Direction direction, float x, float y, float z) { + float value = switch (direction.getAxis()) { + case X -> x; + case Y -> y; + case Z -> z; + }; + return direction.getAxisDirection() == Direction.AxisDirection.POSITIVE + ? value : 1.0F - value; + } + + private static float normalizeWithin(float value, float min, float max) { + if (Math.abs(max - min) < 1.0e-6F) { + return 0.5F; + } + return Math.max(0.0F, Math.min(1.0F, (value - min) / (max - min))); + } + + private static int snap(float coordinate) { + return coordinate < 0.5F ? 0 : 1; + } + + private static float bilinear( + float topLeft, float topRight, float bottomLeft, float bottomRight, + float u, float v) { + float top = topLeft + (topRight - topLeft) * u; + float bottom = bottomLeft + (bottomRight - bottomLeft) * u; + return top + (bottom - top) * v; + } + + private record QuadCacheKey(long connections, int side) { + } + + private record TexturePair(ResourceLocation base, ResourceLocation ctm) {} + + private record SpritePair(TextureAtlasSprite base, TextureAtlasSprite ctm) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/render/ctm/CtmConnectionMask.java b/src/main/java/com/sorrowmist/useless/client/render/ctm/CtmConnectionMask.java new file mode 100644 index 00000000..d7292c15 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/render/ctm/CtmConnectionMask.java @@ -0,0 +1,68 @@ +package com.sorrowmist.useless.client.render.ctm; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.BlockAndTintGetter; +import net.minecraft.world.level.block.state.BlockState; + +public final class CtmConnectionMask { + private CtmConnectionMask() { + } + + public static long collect( + BlockAndTintGetter level, BlockPos pos, BlockState state) { + int family = CtmConnectionRules.family(state); + if (family == CtmConnectionRules.NONE) { + return 0L; + } + long packed = 0L; + for (Direction face : Direction.values()) { + CtmFaceAxes axes = CtmFaceAxes.forFace(face); + int mask = 0; + if (connects(level, pos, face, family, axes.left(), null)) { + mask |= CtmQuadrantSelector.LEFT; + } + if (connects(level, pos, face, family, axes.right(), null)) { + mask |= CtmQuadrantSelector.RIGHT; + } + if (connects(level, pos, face, family, axes.up(), null)) { + mask |= CtmQuadrantSelector.UP; + } + if (connects(level, pos, face, family, axes.down(), null)) { + mask |= CtmQuadrantSelector.DOWN; + } + if (connects(level, pos, face, family, axes.up(), axes.left())) { + mask |= CtmQuadrantSelector.UP_LEFT; + } + if (connects(level, pos, face, family, axes.up(), axes.right())) { + mask |= CtmQuadrantSelector.UP_RIGHT; + } + if (connects(level, pos, face, family, axes.down(), axes.left())) { + mask |= CtmQuadrantSelector.DOWN_LEFT; + } + if (connects(level, pos, face, family, axes.down(), axes.right())) { + mask |= CtmQuadrantSelector.DOWN_RIGHT; + } + packed |= (long) mask << (face.ordinal() * Byte.SIZE); + } + return packed; + } + + public static int forFace(long packed, Direction face) { + return (int) (packed >>> (face.ordinal() * Byte.SIZE)) & 0xFF; + } + + private static boolean connects( + BlockAndTintGetter level, BlockPos origin, Direction face, int family, + Direction firstOffset, Direction secondOffset) { + BlockPos neighborPos = origin.relative(firstOffset); + if (secondOffset != null) { + neighborPos = neighborPos.relative(secondOffset); + } + if (CtmConnectionRules.family(level.getBlockState(neighborPos)) != family) { + return false; + } + BlockPos occluderPos = neighborPos.relative(face); + return CtmConnectionRules.family(level.getBlockState(occluderPos)) != family; + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/render/ctm/CtmConnectionRules.java b/src/main/java/com/sorrowmist/useless/client/render/ctm/CtmConnectionRules.java new file mode 100644 index 00000000..368f457e --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/render/ctm/CtmConnectionRules.java @@ -0,0 +1,44 @@ +package com.sorrowmist.useless.client.render.ctm; + +import com.sorrowmist.useless.content.blocks.multiblock.DirectionalMultiblockPartBlock; +import com.sorrowmist.useless.content.blocks.multiblock.MultiblockAlloyFurnaceCoreBlock; +import com.sorrowmist.useless.content.blocks.multiblock.MultiblockPartBlock; +import com.sorrowmist.useless.content.blocks.multiblock.UselessCoilBlock; +import com.sorrowmist.useless.content.blocks.GlowPlasticBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; + +public final class CtmConnectionRules { + public static final int NONE = 0; + public static final int FURNACE = 1; + private static final int COIL_OFFSET = 100; + private static final int PLASTIC_OFFSET = 1000; + + private CtmConnectionRules() { + } + + public static int family(BlockState state) { + return family(state.getBlock()); + } + + public static int family(Block block) { + if (block instanceof GlowPlasticBlock plastic && plastic.hasConnectedTexture()) { + return PLASTIC_OFFSET + plastic.getColor().ordinal() * 2 + + (plastic.isGlowing() ? 1 : 0); + } + if (block instanceof UselessCoilBlock coil) { + return COIL_OFFSET + coil.tier(); + } + if (block instanceof MultiblockAlloyFurnaceCoreBlock + || block instanceof DirectionalMultiblockPartBlock + || block.getClass() == MultiblockPartBlock.class) { + return FURNACE; + } + return NONE; + } + + public static boolean connects(BlockState first, BlockState second) { + int family = family(first); + return family != NONE && family == family(second); + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/render/ctm/CtmFaceAxes.java b/src/main/java/com/sorrowmist/useless/client/render/ctm/CtmFaceAxes.java new file mode 100644 index 00000000..8c3a9e9f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/render/ctm/CtmFaceAxes.java @@ -0,0 +1,25 @@ +package com.sorrowmist.useless.client.render.ctm; + +import net.minecraft.core.Direction; + +public record CtmFaceAxes(Direction right, Direction down) { + public Direction left() { + return right.getOpposite(); + } + + public Direction up() { + return down.getOpposite(); + } + + public static CtmFaceAxes forFace(Direction face) { + return switch (face) { + case EAST -> new CtmFaceAxes(Direction.NORTH, Direction.DOWN); + case WEST -> new CtmFaceAxes(Direction.SOUTH, Direction.DOWN); + case NORTH -> new CtmFaceAxes(Direction.WEST, Direction.DOWN); + case SOUTH -> new CtmFaceAxes(Direction.EAST, Direction.DOWN); + // Keep the horizontal-face V axis aligned with vanilla cube UVs. + case DOWN -> new CtmFaceAxes(Direction.EAST, Direction.NORTH); + case UP -> new CtmFaceAxes(Direction.EAST, Direction.SOUTH); + }; + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/render/ctm/CtmModelRegistrar.java b/src/main/java/com/sorrowmist/useless/client/render/ctm/CtmModelRegistrar.java new file mode 100644 index 00000000..43bad6b1 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/render/ctm/CtmModelRegistrar.java @@ -0,0 +1,105 @@ +package com.sorrowmist.useless.client.render.ctm; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.blocks.GlowPlasticBlock; +import com.sorrowmist.useless.content.blocks.multiblock.UselessCoilBlock; +import com.sorrowmist.useless.init.ModBlocks; +import net.minecraft.client.renderer.block.BlockModelShaper; +import net.minecraft.client.resources.model.BakedModel; +import net.minecraft.client.resources.model.ModelResourceLocation; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.inventory.InventoryMenu; +import net.minecraft.world.level.block.Block; +import net.minecraftforge.client.event.ModelEvent; + +import java.util.Map; +import java.util.function.Function; + +public final class CtmModelRegistrar { + private static final String FURNACE_TEXTURE_ROOT = + "block/multiblock_alloy_furnace/"; + private static final String COIL_TEXTURE_ROOT = FURNACE_TEXTURE_ROOT + "coils/"; + private static final String PLASTIC_TEXTURE = "block/plastic_glow_block"; + private static final String PLASTIC_CTM_TEXTURE = "block/plastic_glow_block_ctm"; + + private CtmModelRegistrar() { + } + + public static void modifyBakingResult(ModelEvent.ModifyBakingResult event) { + // ModifyBakingResult runs before atlas upload. Keep texture IDs until BakingCompleted. + Function textureGetter = Function.identity(); + ResourceLocation furnaceBase = sprite(textureGetter, + FURNACE_TEXTURE_ROOT + "furnace_casing"); + ResourceLocation furnaceCtm = sprite(textureGetter, + FURNACE_TEXTURE_ROOT + "furnace_casing_ctm"); + ResourceLocation uselessCoilActive = sprite(textureGetter, + COIL_TEXTURE_ROOT + "useless_coil_active"); + ResourceLocation uselessCoilActiveCtm = sprite(textureGetter, + COIL_TEXTURE_ROOT + "useless_coil_active_ctm"); + ResourceLocation usefulCoilActive = sprite(textureGetter, + COIL_TEXTURE_ROOT + "useful_coil_active"); + ResourceLocation usefulCoilActiveCtm = sprite(textureGetter, + COIL_TEXTURE_ROOT + "useful_coil_active_ctm"); + + wrapBlock(event.getModels(), ModBlocks.MULTIBLOCK_ALLOY_FURNACE_CORE.get(), + furnaceBase, furnaceCtm); + wrapBlock(event.getModels(), ModBlocks.OMNIVERSAL_FURNACE_CASING.get(), + furnaceBase, furnaceCtm); + wrapBlock(event.getModels(), ModBlocks.ME_PATTERN_ASSEMBLY.get(), + furnaceBase, furnaceCtm); + wrapBlock(event.getModels(), ModBlocks.OMNIVERSAL_MOLD_HUB.get(), + furnaceBase, furnaceCtm); + wrapBlock(event.getModels(), ModBlocks.PASSIVE_CRAFTING_HATCH.get(), + furnaceBase, furnaceCtm); + + ModBlocks.USELESS_COILS.forEach((tier, holder) -> { + String name = UselessCoilBlock.registryName(tier); + boolean useful = tier == UselessCoilBlock.USEFUL_TIER; + wrapBlock(event.getModels(), holder.get(), + sprite(textureGetter, COIL_TEXTURE_ROOT + name), + sprite(textureGetter, COIL_TEXTURE_ROOT + name + "_ctm"), + useful ? usefulCoilActive : uselessCoilActive, + useful ? usefulCoilActiveCtm : uselessCoilActiveCtm); + }); + + ResourceLocation plasticBase = sprite(textureGetter, PLASTIC_TEXTURE); + ResourceLocation plasticCtm = sprite(textureGetter, PLASTIC_CTM_TEXTURE); + GlowPlasticBlock.PLASTIC_CTM_BLOCKS.values().forEach(block -> + wrapBlock(event.getModels(), block.get(), plasticBase, plasticCtm)); + GlowPlasticBlock.GLOW_PLASTIC_CTM_BLOCKS.values().forEach(block -> + wrapBlock(event.getModels(), block.get(), plasticBase, plasticCtm)); + } + + public static void bakingCompleted(ModelEvent.BakingCompleted event) { + var atlas = event.getModelManager().getAtlas(InventoryMenu.BLOCK_ATLAS); + event.getModels().values().stream().filter(CtmBakedModel.class::isInstance) + .map(CtmBakedModel.class::cast).distinct().forEach(model -> model.bindSprites(atlas::getSprite)); + } + + private static ResourceLocation sprite( + Function textureGetter, String path) { + ResourceLocation texture = new ResourceLocation( + UselessMod.MODID, path); + return textureGetter.apply(texture); + } + + private static void wrapBlock( + Map models, Block block, + ResourceLocation baseSprite, ResourceLocation ctmSprite) { + wrapBlock(models, block, baseSprite, ctmSprite, null, null); + } + + private static void wrapBlock( + Map models, Block block, + ResourceLocation baseSprite, ResourceLocation ctmSprite, + ResourceLocation overlayBaseSprite, ResourceLocation overlayCtmSprite) { + for (var state : block.getStateDefinition().getPossibleStates()) { + ModelResourceLocation location = BlockModelShaper.stateToModelLocation(state); + models.computeIfPresent(location, (ignored, model) -> + model instanceof CtmBakedModel + ? model + : new CtmBakedModel(model, baseSprite, ctmSprite, + overlayBaseSprite, overlayCtmSprite)); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/render/ctm/CtmQuadrantSelector.java b/src/main/java/com/sorrowmist/useless/client/render/ctm/CtmQuadrantSelector.java new file mode 100644 index 00000000..fc97f5c8 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/render/ctm/CtmQuadrantSelector.java @@ -0,0 +1,51 @@ +package com.sorrowmist.useless.client.render.ctm; + +public final class CtmQuadrantSelector { + public static final int LEFT = 1; + public static final int RIGHT = 1 << 1; + public static final int UP = 1 << 2; + public static final int DOWN = 1 << 3; + public static final int UP_LEFT = 1 << 4; + public static final int UP_RIGHT = 1 << 5; + public static final int DOWN_LEFT = 1 << 6; + public static final int DOWN_RIGHT = 1 << 7; + + private CtmQuadrantSelector() { + } + + public static Tile select(int mask, int cornerX, int cornerY) { + if ((cornerX & ~1) != 0 || (cornerY & ~1) != 0) { + throw new IllegalArgumentException("CTM corner coordinates must be 0 or 1"); + } + int horizontalBit = cornerX == 0 ? LEFT : RIGHT; + int verticalBit = cornerY == 0 ? UP : DOWN; + int diagonalBit = switch (cornerY * 2 + cornerX) { + case 0 -> UP_LEFT; + case 1 -> UP_RIGHT; + case 2 -> DOWN_LEFT; + default -> DOWN_RIGHT; + }; + boolean horizontal = (mask & horizontalBit) != 0; + boolean vertical = (mask & verticalBit) != 0; + if (!horizontal && !vertical) { + return Tile.base(cornerX, cornerY); + } + if (horizontal && vertical && (mask & diagonalBit) != 0) { + return Tile.ctm(cornerX, cornerY); + } + // GregTech's X4 atlas is indexed row-first: horizontal connections + // select the lower rows, while vertical connections select the right columns. + return Tile.ctm(cornerX + (vertical ? 2 : 0), + cornerY + (horizontal ? 2 : 0)); + } + + public record Tile(boolean baseTexture, int x, int y) { + private static Tile base(int x, int y) { + return new Tile(true, x, y); + } + + private static Tile ctm(int x, int y) { + return new Tile(false, x, y); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/render/supervisor/SupervisorModelLoader.java b/src/main/java/com/sorrowmist/useless/client/render/supervisor/SupervisorModelLoader.java new file mode 100644 index 00000000..233053be --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/render/supervisor/SupervisorModelLoader.java @@ -0,0 +1,251 @@ +package com.sorrowmist.useless.client.render.supervisor; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.mojang.math.Transformation; +import com.sorrowmist.useless.UselessMod; +import net.minecraft.client.renderer.block.model.BakedQuad; +import net.minecraft.client.renderer.block.model.BlockElement; +import net.minecraft.client.renderer.block.model.BlockElementFace; +import net.minecraft.client.renderer.block.model.BlockFaceUV; +import net.minecraft.client.renderer.block.model.FaceBakery; +import net.minecraft.client.renderer.texture.TextureAtlas; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.client.resources.model.Material; +import net.minecraft.client.resources.model.ModelBaker; +import net.minecraft.client.resources.model.ModelState; +import net.minecraft.core.Direction; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.GsonHelper; +import net.minecraftforge.client.model.IModelBuilder; +import net.minecraftforge.client.model.QuadTransformers; +import net.minecraftforge.client.model.SimpleModelState; +import net.minecraftforge.client.model.geometry.IGeometryBakingContext; +import net.minecraftforge.client.model.geometry.IGeometryLoader; +import net.minecraftforge.client.model.geometry.SimpleUnbakedGeometry; +import net.minecraftforge.client.model.geometry.UnbakedGeometryHelper; +import org.joml.Matrix4f; +import org.joml.Vector3f; + +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +public final class SupervisorModelLoader implements IGeometryLoader { + public static final SupervisorModelLoader INSTANCE = new SupervisorModelLoader(); + + private SupervisorModelLoader() { + } + + @Override + public SupervisorGeometry read(JsonObject jsonObject, com.google.gson.JsonDeserializationContext context) { + return SupervisorGeometry.read(GsonHelper.getAsJsonObject(jsonObject, "supervisor_model")); + } +} + +final class SupervisorGeometry extends SimpleUnbakedGeometry { + private static final float PIXEL = 1.0F / 16.0F; + private static final ModelState IDENTITY_MODEL_STATE = new SimpleModelState(Transformation.identity()); + + private final List elements; + private final Map textures; + + private SupervisorGeometry(List elements, Map textures) { + this.elements = List.copyOf(elements); + this.textures = Map.copyOf(textures); + } + + static SupervisorGeometry read(JsonObject source) { + Map textures = new HashMap<>(); + JsonObject textureObject = GsonHelper.getAsJsonObject(source, "textures"); + for (Map.Entry entry : textureObject.entrySet()) { + if (!entry.getValue().isJsonPrimitive() || !entry.getValue().getAsJsonPrimitive().isString()) { + throw new JsonParseException("Supervisor texture must be a string: " + entry.getKey()); + } + textures.put(entry.getKey(), entry.getValue().getAsString()); + } + + List elements = new ArrayList<>(); + JsonArray elementArray = GsonHelper.getAsJsonArray(source, "elements"); + for (int index = 0; index < elementArray.size(); index++) { + JsonObject element = elementArray.get(index).getAsJsonObject(); + elements.add(new ElementData( + readVector(element, "from"), + readVector(element, "to"), + readFaces(GsonHelper.getAsJsonObject(element, "faces")), + parseRotation(element.has("rotation") + ? GsonHelper.getAsJsonObject(element, "rotation") : null), + GsonHelper.getAsBoolean(element, "shade", true) + )); + } + + return new SupervisorGeometry(elements, textures); + } + + private static Map readFaces(JsonObject facesObject) { + EnumMap faces = new EnumMap<>(Direction.class); + for (Map.Entry entry : facesObject.entrySet()) { + Direction direction = Direction.byName(entry.getKey()); + if (direction == null) { + throw new JsonParseException("Unknown Supervisor face: " + entry.getKey()); + } + + JsonObject face = entry.getValue().getAsJsonObject(); + float[] uv = null; + if (face.has("uv")) { + JsonArray uvArray = GsonHelper.getAsJsonArray(face, "uv"); + if (uvArray.size() != 4) { + throw new JsonParseException("Supervisor face UV must contain four values"); + } + uv = new float[4]; + for (int index = 0; index < uv.length; index++) { + uv[index] = GsonHelper.convertToFloat(uvArray.get(index), "uv[" + index + "]"); + } + } + + int uvRotation = GsonHelper.getAsInt(face, "rotation", 0); + if (uvRotation < 0 || uvRotation % 90 != 0 || uvRotation > 270) { + throw new JsonParseException("Invalid Supervisor UV rotation: " + uvRotation); + } + + faces.put(direction, new FaceData( + GsonHelper.getAsString(face, "texture"), + uv, + uvRotation, + GsonHelper.getAsInt(face, "tintindex", -1) + )); + } + if (faces.isEmpty()) { + throw new JsonParseException("Supervisor element must contain at least one face"); + } + return Map.copyOf(faces); + } + + private static Vector3f readVector(JsonObject object, String key) { + JsonArray values = GsonHelper.getAsJsonArray(object, key); + if (values.size() != 3) { + throw new JsonParseException("Supervisor " + key + " must contain three values"); + } + return new Vector3f( + GsonHelper.convertToFloat(values.get(0), key + "[0]"), + GsonHelper.convertToFloat(values.get(1), key + "[1]"), + GsonHelper.convertToFloat(values.get(2), key + "[2]") + ); + } + + static Matrix4f parseRotation(JsonObject rotation) { + if (rotation == null) { + return new Matrix4f(); + } + + Vector3f origin = readVector(rotation, "origin").mul(PIXEL); + Matrix4f transform = new Matrix4f() + .translate(origin.x(), origin.y(), origin.z()); + if (rotation.has("axis")) { + Direction.Axis axis = Direction.Axis.byName(GsonHelper.getAsString(rotation, "axis")); + if (axis == null) { + throw new JsonParseException("Invalid Supervisor rotation axis"); + } + float angle = (float) Math.toRadians(GsonHelper.getAsFloat(rotation, "angle")); + switch (axis) { + case X -> transform.rotateX(angle); + case Y -> transform.rotateY(angle); + case Z -> transform.rotateZ(angle); + } + } else { + float x = (float) Math.toRadians(getOptionalFloat(rotation, "x")); + float y = (float) Math.toRadians(getOptionalFloat(rotation, "y")); + float z = (float) Math.toRadians(getOptionalFloat(rotation, "z")); + transform.rotateZYX(z, y, x); + } + return transform.translate(-origin.x(), -origin.y(), -origin.z()); + } + + private static float getOptionalFloat(JsonObject object, String key) { + return object.has(key) ? GsonHelper.getAsFloat(object, key) : 0.0F; + } + + static Matrix4f composeTransforms(ModelState modelTransform, Matrix4f elementTransform) { + Matrix4f modelMatrix = new Matrix4f() + .translate(0.5F, 0.5F, 0.5F) + .mul(modelTransform.getRotation().getMatrix()) + .translate(-0.5F, -0.5F, -0.5F); + return modelMatrix.mul(elementTransform); + } + + private Material materialFor(String textureReference) { + String key = textureReference.startsWith("#") + ? textureReference.substring(1) : textureReference; + String texture = textures.get(key); + if (texture == null) { + throw new JsonParseException("Missing Supervisor texture reference: " + textureReference); + } + if (texture.startsWith("#")) { + return materialFor(texture); + } + + ResourceLocation location = texture.contains(":") + ? new ResourceLocation(texture) + : UselessMod.id("block/" + texture); + return new Material(TextureAtlas.LOCATION_BLOCKS, location); + } + + @Override + protected void addQuads(IGeometryBakingContext owner, IModelBuilder modelBuilder, + ModelBaker baker, Function spriteGetter, + ModelState modelTransform, ResourceLocation modelLocation) { + Map spriteCache = new HashMap<>(); + for (ElementData data : elements) { + EnumMap faces = new EnumMap<>(Direction.class); + for (Map.Entry entry : data.faces.entrySet()) { + FaceData face = entry.getValue(); + faces.put(entry.getKey(), new BlockElementFace( + null, + face.tintIndex, + face.texture, + new BlockFaceUV(face.uv == null ? null : face.uv.clone(), face.uvRotation) + )); + } + + BlockElement element = new BlockElement( + new Vector3f(data.from), + new Vector3f(data.to), + faces, + null, + data.shade + ); + Matrix4f transform = composeTransforms(modelTransform, data.elementTransform); + var transformer = QuadTransformers.applying(new Transformation(transform)); + + for (Map.Entry entry : faces.entrySet()) { + Direction faceDirection = entry.getKey(); + BlockElementFace face = entry.getValue(); + Material material = materialFor(face.texture); + TextureAtlasSprite sprite = spriteCache.computeIfAbsent(material, spriteGetter); + BakedQuad baked = UnbakedGeometryHelper.bakeElementFace( + element, face, sprite, faceDirection, IDENTITY_MODEL_STATE, modelLocation); + BakedQuad transformed = transformer.process(baked); + modelBuilder.addUnculledFace(new BakedQuad( + transformed.getVertices(), + transformed.getTintIndex(), + FaceBakery.calculateFacing(transformed.getVertices()), + transformed.getSprite(), + transformed.isShade(), + transformed.hasAmbientOcclusion() + )); + } + } + } + + private record FaceData(String texture, float[] uv, int uvRotation, int tintIndex) { + } + + private record ElementData(Vector3f from, Vector3f to, Map faces, + Matrix4f elementTransform, boolean shade) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/client/stafflink/StaffLinkStressClientState.java b/src/main/java/com/sorrowmist/useless/client/stafflink/StaffLinkStressClientState.java new file mode 100644 index 00000000..282a132e --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/client/stafflink/StaffLinkStressClientState.java @@ -0,0 +1,49 @@ +package com.sorrowmist.useless.client.stafflink; + +import com.sorrowmist.useless.network.StaffLinkStressStatusPacket; +import net.minecraft.core.GlobalPos; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * 客户端手上的应力运行状态。 + * + *

服务端每 20 tick 覆盖一次;界面只读它,不参与任何判定——「能不能搬」永远是服务端说了算, + * 这里纯粹是给玩家看的数字与原因。

+ */ +public final class StaffLinkStressClientState { + + /** 界面每帧都要按「当前网络 × 锚点 × 线路」查一次,因此建成表而不是每次线性扫。 */ + private static volatile Map entries = Map.of(); + + private StaffLinkStressClientState() { + } + + public static void replace(List incoming) { + Map next = new HashMap<>(); + for (StaffLinkStressStatusPacket.Entry entry : incoming) { + next.put(new Key(entry.networkId(), entry.anchor(), entry.route()), entry); + } + entries = Map.copyOf(next); + } + + public static void clear() { + entries = Map.of(); + } + + /** 该「网络 × 锚点 × 线路」的状态;没收到过就是 {@code null}。 */ + @Nullable + public static StaffLinkStressStatusPacket.Entry find(UUID networkId, GlobalPos anchor, int route) { + if (networkId == null || anchor == null) { + return null; + } + return entries.get(new Key(networkId, anchor, route)); + } + + private record Key(UUID networkId, GlobalPos anchor, int route) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/command/OmniversalPatternRepairService.java b/src/main/java/com/sorrowmist/useless/command/OmniversalPatternRepairService.java new file mode 100644 index 00000000..a036d8ab --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/command/OmniversalPatternRepairService.java @@ -0,0 +1,345 @@ +package com.sorrowmist.useless.command; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.crafting.PatternDetailsHelper; +import appeng.api.inventories.InternalInventory; +import appeng.api.networking.IGrid; +import appeng.api.stacks.AEItemKey; +import appeng.crafting.pattern.AEProcessingPattern; +import appeng.helpers.patternprovider.PatternContainer; +import appeng.items.tools.powered.WirelessTerminalItem; +import com.sorrowmist.useless.compat.AE2Compat; +import com.sorrowmist.useless.content.blockentities.AdvancedAlloyFurnaceBlockEntity; +import com.sorrowmist.useless.content.blockentities.multiblock.MePatternAssemblyBlockEntity; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.OmniversalPatternDetails; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.OmniversalPatternEncoding; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeCatalog; +import com.sorrowmist.useless.core.component.OmniversalPatternData; +import com.sorrowmist.useless.core.component.UComponents; +import com.sorrowmist.useless.init.ModItems; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraftforge.fml.ModList; +import org.jetbrains.annotations.Nullable; +import top.theillusivec4.curios.api.CuriosApi; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +/** + * 扫描并修复 AE 网格上失效的万象样板。 + * + *

失效的权威判定与样板 tooltip 红字同源:{@link PatternDetailsHelper#decodePattern} 的 + * 结果不是 {@link OmniversalPatternDetails}(含返回 {@code null} 或抛异常)。判定为失效的 + * 样板会按当前配方目录重新匹配配方并重编码,然后就地写回原槽位。

+ * + *

只覆盖 AE 网络上已通电的机器:单方块高级合金炉与多方块 ME 样板总成。玩家背包与终端 + * 内尚未上传的样板不在扫描范围内。

+ */ +public final class OmniversalPatternRepairService { + private static final int MAX_DETAILS = 3; + + private OmniversalPatternRepairService() { + } + + /** 一次修复的统计。{@code repaired} 在预览模式下语义为「可修复数」。 */ + public record RepairReport(int scanned, int invalid, int repaired, int unresolved, int skipped) { + } + + /** + * 修复结果。成功时携带 {@link RepairReport},失败时携带一句主消息与至多若干条细节。 + */ + public record RepairResult(@Nullable RepairReport report, @Nullable Component failure, + List details) { + public static RepairResult success(RepairReport report, List details) { + return new RepairResult(report, null, List.copyOf(details)); + } + + public static RepairResult failure(Component failure, List details) { + return new RepairResult(null, failure, List.copyOf(details)); + } + + public boolean isFailure() { + return failure != null; + } + } + + /** + * 扫描并(可选)修复玩家所在网格上的失效万象样板。 + * + * @param preview 为 {@code true} 时只扫描报告,不写回任何槽位 + */ + public static RepairResult repair(ServerPlayer player, boolean preview) { + GridResolution resolution = resolvePlayerGrid(player); + if (resolution.grid() == null) { + return resolution.foundTerminal() + ? RepairResult.failure( + Component.translatable("commands.useless_mod.patterns.repair.grid_unavailable"), + resolution.errors()) + : RepairResult.failure( + Component.translatable("commands.useless_mod.patterns.repair.unbound"), + resolution.errors()); + } + + Level level = player.level(); + List machines = collectMachines(resolution.grid()); + if (machines.isEmpty()) { + return RepairResult.failure( + Component.translatable("commands.useless_mod.patterns.repair.no_machines"), List.of()); + } + + int scanned = 0; + int invalid = 0; + int repaired = 0; + int unresolved = 0; + int skipped = 0; + List details = new ArrayList<>(); + + for (PatternContainer machine : machines) { + InternalInventory inventory = machine.getTerminalPatternInventory(); + if (inventory == null) continue; + + boolean dirty = false; + for (int slot = 0; slot < inventory.size(); slot++) { + ItemStack stack = inventory.getStackInSlot(slot); + if (stack.isEmpty() || !stack.is(ModItems.OMNIVERSAL_PATTERN.get())) continue; + + OmniversalPatternData data = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(stack, UComponents.OMNIVERSAL_PATTERN_DATA.get()); + if (data == null) { + skipped++; + continue; + } + + scanned++; + if (isValid(stack, level)) continue; + invalid++; + + ItemStack replacement = reencode(stack, data, level); + if (replacement.isEmpty()) { + unresolved++; + if (details.size() < MAX_DETAILS) { + details.add(Component.translatable( + "commands.useless_mod.patterns.repair.detail", + describe(machine, slot), data.recipeId().toString())); + } + continue; + } + + // 幂等:重编码结果与旧栈完全一致时不写回,也就不计入 repaired。 + if (ItemStack.isSameItemSameTags(stack, replacement)) continue; + + if (!preview) { + inventory.setItemDirect(slot, replacement); + dirty = true; + } + repaired++; + } + + if (dirty && machine instanceof AdvancedAlloyFurnaceBlockEntity furnace) { + // 单方块炉的 setItemDirect 不会触发刷新,需显式重建。 + // 多方块总成由 RecoverableItemStackHandler 的 inventoryChanged 回调自动刷新。 + furnace.onPatternsRebuilt(); + } + } + + return RepairResult.success( + new RepairReport(scanned, invalid, repaired, unresolved, skipped), details); + } + + /** 判定一张万象样板当前是否可正常解码。 */ + private static boolean isValid(ItemStack stack, Level level) { + try { + IPatternDetails decoded = PatternDetailsHelper.decodePattern(stack, level); + return decoded instanceof OmniversalPatternDetails; + } catch (RuntimeException exception) { + return false; + } + } + + /** + * 按当前配方目录重解析并重编码;任一步失败返回 {@link ItemStack#EMPTY}。 + * + *

绑定配方若仍能由 {@code recipeId} 唯一定位,就以该配方**当前的输入输出**重建 + * payload——配方被改动(输入变化)而失效的样板只能这样修复。只有 recipeId 无法唯一 + * 定位时才退回用旧 payload 做内容匹配,复用旧 payload 重编码。

+ */ + private static ItemStack reencode(ItemStack stack, OmniversalPatternData data, Level level) { + Optional byRecipeId = resolveEntryByRecipeId(data, level); + if (byRecipeId.isPresent()) { + ItemStack rebuilt = OmniversalPatternEncoding.reencodeFromRecipe(byRecipeId.get(), level); + if (!rebuilt.isEmpty()) return rebuilt; + } + + AEItemKey definition = AEItemKey.of(stack); + if (definition == null) return ItemStack.EMPTY; + + AEProcessingPattern source; + try { + source = new AEProcessingPattern(definition); + } catch (RuntimeException exception) { + return ItemStack.EMPTY; + } + + Optional entry = resolveEntry(data, source, level); + return entry.map(value -> OmniversalPatternEncoding.reencodeExisting(stack, value, level)) + .orElse(ItemStack.EMPTY); + } + + /** + * 仅按 {@code recipeId} 定位当前配方:忽略指纹与旧 payload,返回唯一匹配项。 + * + *

优先在样板记录的 {@code sourceId} 范围内查找,找不到再退到全目录;命中数不为 1 + * (无匹配或存在歧义)时返回空,交由内容匹配回退处理。

+ */ + private static Optional resolveEntryByRecipeId( + OmniversalPatternData data, Level level) { + var recipeId = data.recipeId(); + if (recipeId == null) return Optional.empty(); + + List matched = new ArrayList<>(); + for (AlloyFurnaceRecipeCatalog.Entry entry : AlloyFurnaceRecipeCatalog.entries(level, data.sourceId())) { + if (recipeId.equals(entry.identity().recipeId())) matched.add(entry); + } + if (matched.isEmpty()) { + for (AlloyFurnaceRecipeCatalog.Entry entry : AlloyFurnaceRecipeCatalog.entries(level)) { + if (recipeId.equals(entry.identity().recipeId())) matched.add(entry); + } + } + return matched.size() == 1 ? Optional.of(matched.get(0)) : Optional.empty(); + } + + /** + * 按稳健性递减的顺序回查当前配方目录。 + * + *

首选按 {@code sourceId + identity} 精确匹配(旧版样板必须走 legacy 指纹算法); + * 其次退到不带画像的身份匹配;最后按样板内容模糊匹配候选,并以确定性评分取首个。

+ */ + private static Optional resolveEntry( + OmniversalPatternData data, AEProcessingPattern source, Level level) { + Optional entry = + data.version() < OmniversalPatternData.TAG_INPUT_VERSION + ? AlloyFurnaceRecipeCatalog.resolveLegacyPattern( + level, data.sourceId(), data.identity(), source, data.version()) + : AlloyFurnaceRecipeCatalog.resolvePattern( + level, data.sourceId(), data.identity(), source); + if (entry.isPresent()) return entry; + + entry = AlloyFurnaceRecipeCatalog.resolve(level, data.sourceId(), data.identity()); + if (entry.isPresent()) return entry; + + entry = AlloyFurnaceRecipeCatalog.resolve(level, data.identity()); + if (entry.isPresent()) return entry; + + // allowMissingOutputs=true 是该目录为 omniversal converter 预留的内容匹配模式。 + return selectBest(AlloyFurnaceRecipeCatalog.findPatternCandidates(level, source, true)); + } + + /** 模具数多者优先,其次按 recipeId 字符串序,保证多次运行结果稳定。 */ + private static Optional selectBest( + List candidates) { + AlloyFurnaceRecipeCatalog.Entry best = null; + for (AlloyFurnaceRecipeCatalog.Entry candidate : candidates) { + if (candidate == null || candidate.recipe() == null) continue; + if (best == null || compareEntries(candidate, best) < 0) best = candidate; + } + return Optional.ofNullable(best); + } + + private static int compareEntries(AlloyFurnaceRecipeCatalog.Entry left, + AlloyFurnaceRecipeCatalog.Entry right) { + int moldComparison = Integer.compare( + right.recipe().molds().size(), left.recipe().molds().size()); + if (moldComparison != 0) return moldComparison; + return left.identity().recipeId().toString() + .compareTo(right.identity().recipeId().toString()); + } + + /** 收集网格上已通电的两类目标机器,并按终端排序值升序排列以保证报告可复现。 */ + private static List collectMachines(IGrid grid) { + Set machines = new LinkedHashSet<>(); + for (Class machineClass : grid.getMachineClasses()) { + if (AdvancedAlloyFurnaceBlockEntity.class.isAssignableFrom(machineClass)) { + machines.addAll(grid.getActiveMachines(AdvancedAlloyFurnaceBlockEntity.class)); + } + } + for (Class machineClass : grid.getMachineClasses()) { + if (MePatternAssemblyBlockEntity.class.isAssignableFrom(machineClass)) { + machines.addAll(grid.getActiveMachines(MePatternAssemblyBlockEntity.class)); + } + } + List result = new ArrayList<>(machines); + result.sort(Comparator.comparingLong(PatternContainer::getTerminalSortOrder)); + return result; + } + + private static Component describe(PatternContainer machine, int slot) { + if (machine instanceof BlockEntity blockEntity && blockEntity.getLevel() != null) { + BlockPos pos = blockEntity.getBlockPos(); + return Component.literal(blockEntity.getLevel().dimension().location() + + " [" + pos.getX() + "," + pos.getY() + "," + pos.getZ() + "] #" + slot); + } + return Component.literal("#" + slot); + } + + /** + * 从玩家背包、副手、护甲与 curios 饰品槽中找出第一个可解析的无线终端并返回其网格。 + * + *

不做多网格合并:遇到第一个能给出非空 {@link IGrid} 的终端即返回。

+ */ + private static GridResolution resolvePlayerGrid(ServerPlayer player) { + List errors = new ArrayList<>(); + boolean foundTerminal = false; + Level level = player.level(); + + for (ItemStack stack : candidateStacks(player)) { + if (stack.isEmpty()) continue; + + if (com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.has(stack, UComponents.WIRELESS_LINK_TARGET.get())) { + AE2Compat.LinkedGridAccess access = AE2Compat.resolveLinkedGrid(stack, player); + if (access != null && access.grid() != null) { + return new GridResolution(access.grid(), true, errors); + } + } + + // ae2wtlib 的 ItemWT 继承 WirelessTerminalItem,instanceof 天然覆盖, + // 无需编译期依赖或反射。 + if (stack.getItem() instanceof WirelessTerminalItem terminal) { + foundTerminal = true; + IGrid grid = terminal.getLinkedGrid(stack, level, player); + if (grid != null) { + return new GridResolution(grid, true, errors); + } + } + } + + return new GridResolution(null, foundTerminal, errors); + } + + private static List candidateStacks(ServerPlayer player) { + List stacks = new ArrayList<>(); + player.getInventory().items.forEach(stacks::add); + player.getInventory().offhand.forEach(stacks::add); + player.getInventory().armor.forEach(stacks::add); + + if (ModList.get().isLoaded("curios")) { + CuriosApi.getCuriosInventory(player).ifPresent(handler -> { + var equipped = handler.getEquippedCurios(); + for (int slot = 0; slot < equipped.getSlots(); slot++) { + stacks.add(equipped.getStackInSlot(slot)); + } + }); + } + return stacks; + } + + private record GridResolution(@Nullable IGrid grid, boolean foundTerminal, List errors) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/command/UselessCommands.java b/src/main/java/com/sorrowmist/useless/command/UselessCommands.java new file mode 100644 index 00000000..78005c7d --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/command/UselessCommands.java @@ -0,0 +1,63 @@ +package com.sorrowmist.useless.command; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.sorrowmist.useless.command.OmniversalPatternRepairService.RepairReport; +import com.sorrowmist.useless.command.OmniversalPatternRepairService.RepairResult; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; + +/** 本模组的 Brigadier 命令树。 */ +public final class UselessCommands { + /** 批量改写机器内容是破坏性操作,需权限等级 4。 */ + private static final int REPAIR_PERMISSION_LEVEL = 4; + + private UselessCommands() { + } + + public static void register(CommandDispatcher dispatcher) { + dispatcher.register(Commands.literal("uselessmod") + .then(Commands.literal("patterns") + .then(Commands.literal("repair") + .requires(source -> source.hasPermission(REPAIR_PERMISSION_LEVEL)) + .executes(context -> run(context.getSource(), false)) + .then(Commands.literal("preview") + .executes(context -> run(context.getSource(), true)))))); + } + + private static int run(CommandSourceStack source, boolean preview) { + ServerPlayer player; + try { + player = source.getPlayerOrException(); + } catch (CommandSyntaxException exception) { + // 修复依赖玩家身上的无线终端,控制台与命令方块无从解析网格。 + source.sendFailure(Component.translatable("commands.useless_mod.patterns.repair.player_only")); + return 0; + } + + RepairResult result = OmniversalPatternRepairService.repair(player, preview); + if (result.isFailure()) { + source.sendFailure(result.failure()); + for (Component detail : result.details()) { + source.sendFailure(detail); + } + return 0; + } + + RepairReport report = result.report(); + if (report == null) return 0; + + source.sendSuccess(() -> Component.translatable( + preview + ? "commands.useless_mod.patterns.repair.summary.preview" + : "commands.useless_mod.patterns.repair.summary", + report.scanned(), report.invalid(), report.repaired(), report.unresolved()), false); + + for (Component detail : result.details()) { + source.sendSuccess(() -> detail, false); + } + return Math.max(report.repaired(), 1); + } +} diff --git a/src/main/java/com/sorrowmist/useless/common/inventory/CustomItemStorage.java b/src/main/java/com/sorrowmist/useless/common/inventory/CustomItemStorage.java deleted file mode 100644 index fcd0e9c9..00000000 --- a/src/main/java/com/sorrowmist/useless/common/inventory/CustomItemStorage.java +++ /dev/null @@ -1,268 +0,0 @@ -package com.sorrowmist.useless.common.inventory; - -import net.minecraft.nbt.CompoundTag; -import net.minecraft.world.item.ItemStack; -import net.minecraftforge.items.IItemHandler; - -import javax.annotation.Nonnull; -import java.util.function.Predicate; - -/** - * 自定义物品存储类,支持高堆叠上限 - * 参考热力膨胀的设计思路,但不依赖其类 - */ -public class CustomItemStorage implements IItemHandler { - protected static final Predicate DEFAULT_VALIDATOR = (stack) -> true; - - protected final long baseCapacity; - protected Predicate validator; - protected ItemStack item; - protected long count; - protected long capacity; - - public CustomItemStorage() { - this(DEFAULT_VALIDATOR); - } - - public CustomItemStorage(long capacity) { - this(capacity, DEFAULT_VALIDATOR); - } - - public CustomItemStorage(Predicate validator) { - this(0L, validator); - } - - public CustomItemStorage(long capacity, Predicate validator) { - this.item = ItemStack.EMPTY; - this.count = 0; - this.baseCapacity = capacity; - this.capacity = capacity; - this.validator = validator; - } - - public CustomItemStorage setCapacity(long capacity) { - this.capacity = capacity; - return this; - } - - public CustomItemStorage setValidator(Predicate validator) { - if (validator != null) { - this.validator = validator; - } - return this; - } - - public boolean isItemValid(@Nonnull ItemStack stack) { - return this.validator.test(stack); - } - - public void setItemStack(ItemStack item) { - this.item = item.isEmpty() ? ItemStack.EMPTY : item; - this.count = item.isEmpty() ? 0 : (long) item.getCount(); - } - - public CustomItemStorage read(CompoundTag nbt) { - this.item = loadItemStack(nbt); - // 从NBT中读取实际的long计数 - if (nbt.contains("LongCount")) { - this.count = nbt.getLong("LongCount"); - } else if (nbt.contains("IntCount")) { - this.count = nbt.getInt("IntCount"); - } else { - this.count = this.item.isEmpty() ? 0 : (long) this.item.getCount(); - } - return this; - } - - public CompoundTag write(CompoundTag nbt) { - // 保存ItemStack,但忽略其count字段 - ItemStack stack = this.item.copy(); - stack.setCount(1); // 设置为1,因为实际count存储在LongCount中 - stack.save(nbt); - // 保存实际的long计数 - nbt.putLong("LongCount", this.count); - return nbt; - } - - public static ItemStack loadItemStack(CompoundTag nbt) { - ItemStack retStack = ItemStack.of(nbt); - if (nbt.contains("LongCount")) { - long storedCount = nbt.getLong("LongCount"); - // 由于ItemStack的count是int类型,我们只能设置为Math.min(storedCount, Integer.MAX_VALUE) - // 实际的long计数会在CustomItemStorage中管理 - retStack.setCount((int) Math.min(storedCount, Integer.MAX_VALUE)); - } else if (nbt.contains("IntCount")) { - // 兼容旧的IntCount标签 - int storedCount = nbt.getInt("IntCount"); - retStack.setCount(storedCount); - } - return retStack; - } - - protected static CompoundTag saveItemStack(ItemStack stack, CompoundTag nbt) { - // 保存ItemStack的基本信息 - stack.save(nbt); - // 总是保存为LongCount,支持更大的数值 - nbt.putLong("LongCount", stack.getCount()); - return nbt; - } - - public ItemStack getItemStack() { - return this.item; - } - - public long getCount() { - return this.count; - } - - public boolean isEmpty() { - return this.count <= 0 || this.item.isEmpty(); - } - - public boolean isFull() { - return this.count >= this.getCapacity(); - } - - @Override - public int getSlots() { - return 1; - } - - @Nonnull - @Override - public ItemStack getStackInSlot(int slot) { - if (this.isEmpty()) { - return ItemStack.EMPTY; - } - // 创建一个副本并设置正确的count(转换为int) - ItemStack stack = this.item.copy(); - stack.setCount((int) Math.min(this.count, Integer.MAX_VALUE)); - return stack; - } - - @Nonnull - @Override - public ItemStack insertItem(int slot, @Nonnull ItemStack stack, boolean simulate) { - if (stack.isEmpty()) { - return ItemStack.EMPTY; - } - - if (!this.isItemValid(stack)) { - return stack; - } - - if (this.isEmpty()) { - // 计算可插入的数量 - long maxInsert = Math.min((long) stack.getCount(), this.capacity); - if (!simulate) { - this.item = stack.copy(); - this.item.setCount(1); // 实际数量存储在count字段中 - this.count = maxInsert; - } - - // 计算剩余物品 - if (maxInsert >= (long) stack.getCount()) { - return ItemStack.EMPTY; - } else { - return copyStackWithSize(stack, stack.getCount() - (int) maxInsert); - } - } - else if (ItemStack.isSameItemSameTags(this.item, stack)) { - // 计算剩余空间 - long availableSpace = this.capacity - this.count; - if (availableSpace <= 0) { - // 没有空间,返回所有物品 - return stack; - } - - // 计算可添加的数量 - long addAmount = Math.min((long) stack.getCount(), availableSpace); - if (addAmount <= 0) { - return stack; - } - - // 更新物品数量 - if (!simulate) { - this.count += addAmount; - } - - // 计算剩余物品 - if (addAmount >= (long) stack.getCount()) { - return ItemStack.EMPTY; - } else { - return copyStackWithSize(stack, stack.getCount() - (int) addAmount); - } - } - else { - return stack; - } - } - - @Nonnull - @Override - public ItemStack extractItem(int slot, int amount, boolean simulate) { - if (amount <= 0 || this.isEmpty()) { - return ItemStack.EMPTY; - } - - // 计算可提取的数量 - long retCount = Math.min(this.count, (long) amount); - ItemStack ret = copyStackWithSize(this.item, (int) retCount); - - if (!simulate) { - this.count -= retCount; - if (this.count <= 0) { - this.item = ItemStack.EMPTY; - this.count = 0; - } - } - - return ret; - } - - @Override - public int getSlotLimit(int slot) { - return Integer.MAX_VALUE; - } - - @Override - public boolean isItemValid(int slot, @Nonnull ItemStack stack) { - return this.isItemValid(stack); - } - - public boolean clear() { - if (this.isEmpty()) { - return false; - } - this.item = ItemStack.EMPTY; - return true; - } - - public void modify(long quantity) { - this.count = Math.min(this.count + quantity, this.capacity); - if (this.count <= 0) { - this.item = ItemStack.EMPTY; - this.count = 0; - } - } - - public long getCapacity() { - return this.capacity; - } - - public long getStored() { - return this.count; - } - - public long getSpace() { - return this.capacity - this.count; - } - - // 辅助方法 - private static ItemStack copyStackWithSize(@Nonnull ItemStack stack, int size) { - if (size <= 0) return ItemStack.EMPTY; - ItemStack copy = stack.copy(); - copy.setCount(size); - return copy; - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/common/inventory/MultiSlotItemHandler.java b/src/main/java/com/sorrowmist/useless/common/inventory/MultiSlotItemHandler.java deleted file mode 100644 index 635c7795..00000000 --- a/src/main/java/com/sorrowmist/useless/common/inventory/MultiSlotItemHandler.java +++ /dev/null @@ -1,163 +0,0 @@ -package com.sorrowmist.useless.common.inventory; - -import net.minecraft.nbt.CompoundTag; -import net.minecraft.nbt.ListTag; -import net.minecraft.world.item.ItemStack; -import net.minecraftforge.items.IItemHandlerModifiable; - -import javax.annotation.Nonnull; -import java.util.function.Predicate; - -/** - * 多槽位物品处理器,支持高堆叠,实现 IItemHandlerModifiable 接口 - */ -public class MultiSlotItemHandler implements IItemHandlerModifiable { - private final CustomItemStorage[] slots; - private final int slotCount; - private final Predicate globalValidator; - - public MultiSlotItemHandler(int slotCount) { - this(slotCount, CustomItemStorage.DEFAULT_VALIDATOR); - } - - public MultiSlotItemHandler(int slotCount, Predicate globalValidator) { - this.slotCount = slotCount; - this.globalValidator = globalValidator; - this.slots = new CustomItemStorage[slotCount]; - - for (int i = 0; i < slotCount; i++) { - this.slots[i] = new CustomItemStorage(0, globalValidator); - } - } - - public MultiSlotItemHandler setSlotCapacity(int slot, long capacity) { - if (slot >= 0 && slot < slotCount) { - this.slots[slot].setCapacity(capacity); - } - return this; - } - - public MultiSlotItemHandler setAllSlotCapacity(long capacity) { - for (CustomItemStorage slot : slots) { - slot.setCapacity(capacity); - } - return this; - } - - public MultiSlotItemHandler setSlotValidator(int slot, Predicate validator) { - if (slot >= 0 && slot < slotCount) { - this.slots[slot].setValidator(validator); - } - return this; - } - - @Override - public int getSlots() { - return slotCount; - } - - @Nonnull - @Override - public ItemStack getStackInSlot(int slot) { - if (slot < 0 || slot >= slotCount) { - return ItemStack.EMPTY; - } - return slots[slot].getStackInSlot(0); - } - - @Nonnull - @Override - public ItemStack insertItem(int slot, @Nonnull ItemStack stack, boolean simulate) { - if (slot < 0 || slot >= slotCount || stack.isEmpty()) { - return stack; - } - return slots[slot].insertItem(0, stack, simulate); - } - - @Nonnull - @Override - public ItemStack extractItem(int slot, int amount, boolean simulate) { - if (slot < 0 || slot >= slotCount || amount <= 0) { - return ItemStack.EMPTY; - } - return slots[slot].extractItem(0, amount, simulate); - } - - @Override - public int getSlotLimit(int slot) { - if (slot < 0 || slot >= slotCount) { - return 0; - } - return slots[slot].getSlotLimit(0); - } - - @Override - public boolean isItemValid(int slot, @Nonnull ItemStack stack) { - if (slot < 0 || slot >= slotCount) { - return false; - } - return slots[slot].isItemValid(0, stack); - } - - // 实现 IItemHandlerModifiable 接口的方法 - @Override - public void setStackInSlot(int slot, @Nonnull ItemStack stack) { - if (slot >= 0 && slot < slotCount) { - slots[slot].setItemStack(stack); - } - } - - // NBT 序列化 - public CompoundTag serializeNBT() { - CompoundTag nbt = new CompoundTag(); - ListTag slotsTag = new ListTag(); - - for (int i = 0; i < slotCount; i++) { - CompoundTag slotTag = new CompoundTag(); - slots[i].write(slotTag); - slotsTag.add(slotTag); - } - - nbt.put("Slots", slotsTag); - nbt.putInt("Size", slotCount); - return nbt; - } - - public void deserializeNBT(CompoundTag nbt) { - if (nbt.contains("Slots")) { - ListTag slotsTag = nbt.getList("Slots", 10); // 10 = CompoundTag - for (int i = 0; i < Math.min(slotsTag.size(), slotCount); i++) { - CompoundTag slotTag = slotsTag.getCompound(i); - slots[i].read(slotTag); - } - } - } - - - - - // 便捷方法 - public ItemStack getItemInSlot(int slot) { - return getStackInSlot(slot); - } - - public void setItemInSlot(int slot, ItemStack stack) { - setStackInSlot(slot, stack); - } - - public boolean isSlotEmpty(int slot) { - return getStackInSlot(slot).isEmpty(); - } - - public long getSlotStored(int slot) { - return slots[slot].getStored(); - } - - public long getSlotCapacity(int slot) { - return slots[slot].getCapacity(); - } - - public long getSlotSpace(int slot) { - return slots[slot].getSpace(); - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/compat/AE2Compat.java b/src/main/java/com/sorrowmist/useless/compat/AE2Compat.java new file mode 100644 index 00000000..50f1ee8d --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/AE2Compat.java @@ -0,0 +1,159 @@ +package com.sorrowmist.useless.compat; + +import appeng.api.config.Actionable; +import appeng.api.networking.IGrid; +import appeng.api.networking.security.IActionSource; +import appeng.api.networking.storage.IStorageService; +import appeng.api.stacks.AEItemKey; +import appeng.blockentity.networking.WirelessAccessPointBlockEntity; +import com.sorrowmist.useless.core.component.UComponents; +import net.minecraft.core.BlockPos; +import net.minecraft.core.GlobalPos; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.BlockEntity; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Predicate; + +public class AE2Compat { + + public static long tryExtractFromLinkedGrid( + ItemStack tool, Player player, ItemStack requested, Actionable mode) { + LinkedGridAccess access = resolveLinkedGrid(tool, player); + AEItemKey key = AEItemKey.of(requested); + if (access == null || key == null || requested.isEmpty()) return 0L; + return access.grid.getStorageService().getInventory().extract( + key, requested.getCount(), mode, access.source); + } + + public static long tryInsertIntoLinkedGrid( + ItemStack tool, Player player, ItemStack stack, Actionable mode) { + LinkedGridAccess access = resolveLinkedGrid(tool, player); + AEItemKey key = AEItemKey.of(stack); + if (access == null || key == null || stack.isEmpty()) return 0L; + return access.grid.getStorageService().getInventory().insert( + key, stack.getCount(), mode, access.source); + } + + /** + * 尝试将物品存入工具绑定的 AE2 网络 + * + * @return 实际存入的数量 + */ + public static int tryInsertToLinkedGrid(ItemStack tool, Player player, ItemStack drop) { + // 1. 获取绑定坐标组件 + GlobalPos linkedPos = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(tool, UComponents.WIRELESS_LINK_TARGET.get()); + if (linkedPos == null) { + player.displayClientMessage(Component.translatable("gui.useless_mod.ae2.not_bound"), true); + return 0; + } + BlockPos pos = linkedPos.pos(); + + // 2. 服务器安全检查 + MinecraftServer server = player.getServer(); + if (server == null) return 0; + + // 3. 获取目标维度世界并检查区块是否加载 + ServerLevel targetLevel = server.getLevel(linkedPos.dimension()); + if (targetLevel == null || !targetLevel.isLoaded(pos)) { + player.displayClientMessage(Component.translatable("gui.useless_mod.ae2.chunk_not_loaded"), true); + return 0; + } + + // 4. 获取 BlockEntity 并验证类型 + BlockEntity be = targetLevel.getBlockEntity(pos); + if (!(be instanceof WirelessAccessPointBlockEntity wap)) { + player.displayClientMessage(Component.translatable("gui.useless_mod.ae2.invalid_target"), true); + return 0; + } + + // 5. 检查无线访问点是否在线 + if (!wap.getMainNode().isOnline()) { + player.displayClientMessage(Component.translatable("gui.useless_mod.ae2.offline"), true); + return 0; + } + + // 6. 获取网格及存储服务 + IGrid grid = wap.getGrid(); + if (grid == null) return 0; + + IStorageService storage = grid.getStorageService(); + IActionSource source = IActionSource.ofPlayer(player); + AEItemKey key = AEItemKey.of(drop); + + if (key == null) return 0; + + // 7. 执行存入操作 + long inserted = storage.getInventory().insert( + key, + drop.getCount(), + Actionable.MODULATE, + source + ); + + return (int) inserted; + } + + /** + * 从绑定网络提取一件满足条件的物品。 + * + *

提取必须按网络中该件物品自身的 key 进行:带耐久的物品(例如 occultism 的粉笔) + * 其 key 包含耐久等组件,使用全新的同类物品无法匹配。返回的物品栈保留其原有耐久, + * 以便使用后原样放回。

+ * + * @param matcher 判断某件物品是否可用;参数为带原始耐久的物品栈 + * @return 提取到的物品;网络不可用或无匹配项时返回 null + */ + @Nullable + public static ItemStack extractMatchingFromLinkedGrid(ItemStack tool, Player player, + Predicate matcher) { + LinkedGridAccess access = resolveLinkedGrid(tool, player); + if (access == null) return null; + + var inventory = access.grid.getStorageService().getInventory(); + + // 先定位目标 key,避免在遍历库存期间修改库存 + AEItemKey matched = null; + for (var entry : inventory.getAvailableStacks()) { + if (entry.getLongValue() <= 0L) continue; + if (!(entry.getKey() instanceof AEItemKey itemKey)) continue; + + ItemStack candidate = itemKey.toStack(1); + if (candidate.isEmpty() || !matcher.test(candidate)) continue; + + matched = itemKey; + break; + } + if (matched == null) return null; + + if (inventory.extract(matched, 1L, Actionable.MODULATE, access.source) <= 0L) return null; + return matched.toStack(1); + } + + /** + * 解析物品绑定的 AE2 网络。 + * + *

供命令等外部调用方复用:返回的 {@link LinkedGridAccess} 同时携带网格与玩家动作源。 + * 物品未绑定、目标区块未加载或无线访问点离线时返回 {@code null}。

+ */ + @Nullable + public static LinkedGridAccess resolveLinkedGrid(ItemStack tool, Player player) { + GlobalPos linkedPos = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(tool, UComponents.WIRELESS_LINK_TARGET.get()); + MinecraftServer server = player.getServer(); + if (linkedPos == null || server == null) return null; + ServerLevel targetLevel = server.getLevel(linkedPos.dimension()); + if (targetLevel == null || !targetLevel.isLoaded(linkedPos.pos())) return null; + if (!(targetLevel.getBlockEntity(linkedPos.pos()) instanceof WirelessAccessPointBlockEntity accessPoint) + || !accessPoint.getMainNode().isOnline()) return null; + IGrid grid = accessPoint.getGrid(); + return grid == null ? null : new LinkedGridAccess(grid, IActionSource.ofPlayer(player)); + } + + /** 网格与玩家动作源的组合,供跨包调用方(如命令层)复用。 */ + public record LinkedGridAccess(IGrid grid, IActionSource source) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/AppFluxCompat.java b/src/main/java/com/sorrowmist/useless/compat/AppFluxCompat.java new file mode 100644 index 00000000..b034d5b8 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/AppFluxCompat.java @@ -0,0 +1,58 @@ +package com.sorrowmist.useless.compat; + +import appeng.api.config.Actionable; +import appeng.api.networking.IGrid; +import appeng.api.networking.security.IActionSource; +import appeng.api.storage.MEStorage; +import com.glodblock.github.appflux.common.me.key.FluxKey; +import com.glodblock.github.appflux.common.me.key.type.EnergyType; +import net.minecraftforge.fml.ModList; +import org.jetbrains.annotations.Nullable; + +/** + * AppliedFlux (AppFlux) 兼容层。 + *

+ * AppFlux 将 FE 能量以 {@link FluxKey} 的形式存入 AE 网络存储(通量元件), + * FluxKey 的数量即 FE 数量,直接通过 {@link MEStorage#extract} 抽取。 + */ +public final class AppFluxCompat { + + private static final String MOD_ID = "appflux"; + + @Nullable + private static Boolean isLoaded; + + private AppFluxCompat() {} + + public static boolean isLoaded() { + if (isLoaded == null) { + isLoaded = ModList.get().isLoaded(MOD_ID); + } + return isLoaded; + } + + /** + * 从 AE 网络存储中抽取 AppFlux 存储的 FE 能量。 + * AppFlux 未安装时静默返回 0。 + * + * @param amount 期望抽取的 FE 数量 + * @return 实际抽取的 FE 数量 + */ + public static long extractFe(IGrid grid, long amount, IActionSource source) { + if (amount <= 0 || !isLoaded()) { + return 0; + } + return Impl.extractFe(grid, amount, source); + } + + /** + * AppFlux 类引用隔离在内部类中,未安装时不会触发类加载。 + */ + private static final class Impl { + + static long extractFe(IGrid grid, long amount, IActionSource source) { + MEStorage storage = grid.getStorageService().getInventory(); + return storage.extract(FluxKey.of(EnergyType.FE), amount, Actionable.MODULATE, source); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/DraconicEvolutionCompat.java b/src/main/java/com/sorrowmist/useless/compat/DraconicEvolutionCompat.java new file mode 100644 index 00000000..5331e284 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/DraconicEvolutionCompat.java @@ -0,0 +1,115 @@ +package com.sorrowmist.useless.compat; + +import com.brandon3055.draconicevolution.DEConfig; +import com.brandon3055.draconicevolution.blocks.ChaosCrystal; +import com.brandon3055.draconicevolution.blocks.tileentity.TileChaosCrystal; +import com.brandon3055.draconicevolution.init.DEContent; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraftforge.fml.ModList; + +/** + * Draconic Evolution 模组兼容性处理 + * 主要用于处理混沌水晶的特殊破坏逻辑 + */ +public class DraconicEvolutionCompat { + + private static final String MOD_ID = "draconicevolution"; + private static Boolean isDELoaded = null; + + /** + * 检查 Draconic Evolution 是否已加载 + */ + public static boolean isLoaded() { + if (isDELoaded == null) { + isDELoaded = ModList.get().isLoaded(MOD_ID); + } + return isDELoaded; + } + + /** + * 检查方块是否是混沌水晶 + */ + public static boolean isChaosCrystal(BlockState state) { + if (!isLoaded()) return false; + return state.getBlock() instanceof ChaosCrystal; + } + + /** + * 处理混沌水晶的强制破坏 + * 绕过正常的破坏流程,直接破坏并生成掉落物,避免触发 detonate() 的重生逻辑 + * + * @param level 世界 + * @param pos 位置 + * @param state 方块状态 + * @param player 玩家 + * @return 如果处理了返回 true + */ + public static boolean handleChaosCrystalBreak(ServerLevel level, BlockPos pos, BlockState state, Player player) { + if (!isLoaded()) return false; + + // 检查是否是混沌水晶 + if (!(state.getBlock() instanceof ChaosCrystal)) { + return false; + } + + BlockEntity tileEntity = level.getBlockEntity(pos); + if (!(tileEntity instanceof TileChaosCrystal chaosCrystal)) { + return false; + } + + // 1. 设置守卫已击败状态 + chaosCrystal.setDefeated(); + + // 2. 获取混沌碎片掉落数量并生成掉落物 + int dropCount = DEConfig.chaosDropCount; + ItemStack chaosShardStack = new ItemStack(DEContent.CHAOS_SHARD.get(), dropCount); + Block.popResource(level, pos, chaosShardStack); + + // 3. 清除周围的水晶部分(上下各2格) + clearCrystalParts(level, pos); + + // 4. 直接设置方块为空气,绕过 onRemove 方法的 detonate 调用 + // 先移除 BlockEntity 防止触发 detonate + level.removeBlockEntity(pos); + level.setBlock(pos, Blocks.AIR.defaultBlockState(), 3); + + return true; + } + + /** + * 清除混沌水晶周围的部分(上下各2格) + */ + private static void clearCrystalParts(ServerLevel level, BlockPos pos) { + Block partBlock = DEContent.CHAOS_CRYSTAL_PART.get(); + + // 清除上下各2格 + BlockPos[] positions = { + pos.above(), pos.above(2), + pos.below(), pos.below(2) + }; + + for (BlockPos partPos : positions) { + BlockState partState = level.getBlockState(partPos); + // 如果是混沌水晶部分,先移除 BlockEntity 再设置空气 + if (partState.is(partBlock)) { + level.removeBlockEntity(partPos); + level.setBlock(partPos, Blocks.AIR.defaultBlockState(), 3); + } + } + } + + /** + * 检查并处理连锁挖掘中的混沌水晶 + * 返回是否应该跳过此方块(由本方法处理) + */ + public static boolean handleChainMiningChaosCrystal(ServerLevel level, BlockPos pos, BlockState state, Player player) { + return handleChaosCrystalBreak(level, pos, state, player); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/ae/AeChemicalBridge.java b/src/main/java/com/sorrowmist/useless/compat/ae/AeChemicalBridge.java new file mode 100644 index 00000000..909f73b4 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ae/AeChemicalBridge.java @@ -0,0 +1,28 @@ +package com.sorrowmist.useless.compat.ae; + +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalHandlerView; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +/** + * 无线物流接「AE 网络里的化学品」的桥接口。 + * + *

独立于 {@link AeLogisticsBridge} 的原因:化学品需要 Applied Mekanistics(modid + * {@code appmek})才能存在于 ME 网络里,而物品/流体只要 AE2。若把两件事塞进同一个实现类, + * 只装 AE2 没装 appmek 的整合包会在加载该实现类时直接 {@code NoClassDefFoundError}。 + * 拆开之后两个实现各自只在对应前置齐备时才被反射载入。

+ * + *

本接口同样不出现任何 AE2 或 Mekanism 类型:{@code ChemicalHandlerView} 本就是 + * 本模组的 long 级抽象。

+ */ +public interface AeChemicalBridge { + + /** + * 把该端点所属 ME 网络里的化学品当作一个容器端点。 + * + * @return 不是可用的 AE 端点时返回 {@code null} + */ + @Nullable + ChemicalHandlerView chemicalEndpoint(Level level, BlockPos pos); +} diff --git a/src/main/java/com/sorrowmist/useless/compat/ae/AeChemicalCompat.java b/src/main/java/com/sorrowmist/useless/compat/ae/AeChemicalCompat.java new file mode 100644 index 00000000..38bb14e9 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ae/AeChemicalCompat.java @@ -0,0 +1,187 @@ +package com.sorrowmist.useless.compat.ae; + +import appeng.api.config.Actionable; +import appeng.api.networking.security.IActionSource; +import appeng.api.stacks.AEKey; +import appeng.api.storage.MEStorage; +import com.sorrowmist.useless.compat.mekanism.MekanismChemicalStackView; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalHandlerView; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalStackView; +import me.ramidzkh.mekae2.ae2.MekanismKey; +import mekanism.api.chemical.ChemicalStack; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link AeChemicalBridge} 的 Applied Mekanistics 实现。 + * + *

只有 AE2 与 appmek 同时在场时,{@link AeChemicalCompatLoader} 才会加载本类。

+ * + *

为什么是「一个端点」而不是「一次插入 / 一次抽出」

+ * + *

搬运侧走的是「先模拟后提交、余量退回源」,这套时序要求模拟与执行面对同一个对象。 + * 把整个 ME 网络里的化学品包成一个 {@link ChemicalHandlerView},搬运逻辑就与方块容器 + * 完全同源,不存在两套代码走样的问题。

+ * + *

槽位快照

+ * + *

ME 网络没有「第几号储罐」,储罐号是搬运侧遍历内容时的临时编号。因此在第一次被问到 + * 内容时抓一份快照(网络里现有的化学品 key 与存量),本次搬运全程沿用,避免边抽边变导致 + * 编号指向另一种化学品。

+ * + *

数量

+ * + *

全程 long:{@code ChemicalStack} 的 amount 与 AE2 的 {@code insert}/{@code extract} + * 都是 long 签名,中间不需要任何 int 中转。

+ */ +public final class AeChemicalCompat implements AeChemicalBridge { + + private static volatile IActionSource cachedSource; + + private static IActionSource source() { + IActionSource local = cachedSource; + if (local == null) { + local = IActionSource.empty(); + cachedSource = local; + } + return local; + } + + @Override + public ChemicalHandlerView chemicalEndpoint(Level level, BlockPos pos) { + return AeNetworks.isEndpoint(level, pos) ? new Endpoint(level, pos) : null; + } + + /** 一次搬运期间固定的「网络里有哪些化学品」快照条目。 */ + private static final class Entry { + private final AEKey key; + private long amount; + + Entry(AEKey key, long amount) { + this.key = key; + this.amount = amount; + } + } + + private static final class Endpoint implements ChemicalHandlerView { + private final Level level; + private final BlockPos pos; + private List snapshot; + + Endpoint(Level level, BlockPos pos) { + this.level = level; + this.pos = pos; + } + + private List snapshot() { + List local = snapshot; + if (local == null) { + local = new ArrayList<>(); + MEStorage storage = AeNetworks.storage(level, pos); + if (storage != null) { + for (var entry : storage.getAvailableStacks()) { + AEKey key = entry.getKey(); + long amount = entry.getLongValue(); + if (amount > 0L && key instanceof MekanismKey) { + local.add(new Entry(key, amount)); + } + } + } + snapshot = local; + } + return local; + } + + @Override + public ChemicalStackView insertChemical(ChemicalStackView stack, boolean simulate) { + if (stack == null || stack.isEmpty() || stack.amount() <= 0L) { + return ChemicalStackView.EMPTY; + } + if (!(stack instanceof MekanismChemicalStackView view)) { + // 不是 Mekanism 支撑的视图,说明这个化学品根本不是本环境能表达的,原样退回。 + return stack; + } + ChemicalStack chemical = view.stack(); + MekanismKey key = MekanismKey.of(chemical); + if (key == null) { + return stack; + } + MEStorage storage = AeNetworks.storage(level, pos); + if (storage == null) { + return stack; + } + long inserted = storage.insert(key, stack.amount(), + simulate ? Actionable.SIMULATE : Actionable.MODULATE, source()); + long remaining = stack.amount() - Math.max(0L, inserted); + return remaining <= 0L ? ChemicalStackView.EMPTY : stack.copyWithAmount(remaining); + } + + @Override + public ChemicalStackView extractChemical(long amount, boolean simulate) { + if (amount <= 0L) { + return ChemicalStackView.EMPTY; + } + List entries = snapshot(); + for (Entry entry : entries) { + if (entry.amount <= 0L) { + continue; + } + long taken = extract(entry, amount, simulate); + if (taken > 0L) { + return asView(entry, taken); + } + } + return ChemicalStackView.EMPTY; + } + + @Override + public ChemicalStackView extractChemical(ChemicalStackView stack, long amount, boolean simulate) { + if (stack == null || stack.isEmpty() || amount <= 0L) { + return ChemicalStackView.EMPTY; + } + List entries = snapshot(); + for (Entry entry : entries) { + if (entry.amount <= 0L) { + continue; + } + ChemicalStackView candidate = asView(entry, entry.amount); + if (!stack.isSameType(candidate)) { + continue; + } + long taken = extract(entry, amount, simulate); + return taken > 0L ? asView(entry, taken) : ChemicalStackView.EMPTY; + } + return ChemicalStackView.EMPTY; + } + + private long extract(Entry entry, long amount, boolean simulate) { + MEStorage storage = AeNetworks.storage(level, pos); + if (storage == null) { + return 0L; + } + long request = Math.min(amount, entry.amount); + long taken = storage.extract(entry.key, request, + simulate ? Actionable.SIMULATE : Actionable.MODULATE, source()); + if (taken > 0L && !simulate) { + // 同步扣减快照:同一次搬运里若又轮到这一条,不会按旧数字重复抽。 + entry.amount -= taken; + } + return Math.max(0L, taken); + } + + /** 把快照条目还原成本模组的化学品视图。 */ + private static ChemicalStackView asView(Entry entry, long amount) { + if (entry.key instanceof MekanismKey key) { + ChemicalStack chemical = key.getStack(); + if (chemical == null || chemical.isEmpty()) { + return ChemicalStackView.EMPTY; + } + return new MekanismChemicalStackView(mekanism.common.util.ChemicalUtil.copyWithAmount(chemical, amount)); + } + return ChemicalStackView.EMPTY; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/ae/AeChemicalCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/ae/AeChemicalCompatLoader.java new file mode 100644 index 00000000..2b4caabc --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ae/AeChemicalCompatLoader.java @@ -0,0 +1,54 @@ +package com.sorrowmist.useless.compat.ae; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalHandlerView; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraftforge.fml.ModList; +import org.jetbrains.annotations.Nullable; + +/** + * 按需加载 {@link AeChemicalCompat}。 + * + *

需要 AE2 与 Applied Mekanistics 同时在场:前者提供 ME 存储,后者提供 + * {@code MekanismKey} 这个「化学品在 ME 网络里的身份」。任一缺席时本加载器恒返回 + * {@code null},无线物流只是解析不出化学品端点,其余功能不受影响。

+ */ +public final class AeChemicalCompatLoader { + private static final String AE2 = "ae2"; + private static final String APPLIED_MEKANISTICS = "appmek"; + + private static final String IMPLEMENTATION = "com.sorrowmist.useless.compat.ae.AeChemicalCompat"; + + private static volatile boolean initialized; + private static volatile AeChemicalBridge bridge; + + private AeChemicalCompatLoader() { + } + + @Nullable + public static AeChemicalBridge bridge() { + if (!initialized) { + initialized = true; + if (ModList.get().isLoaded(AE2) && ModList.get().isLoaded(APPLIED_MEKANISTICS)) { + try { + Class implementation = Class.forName( + IMPLEMENTATION, true, AeChemicalCompatLoader.class.getClassLoader()); + bridge = (AeChemicalBridge) implementation.getDeclaredConstructor().newInstance(); + } catch (ReflectiveOperationException | LinkageError exception) { + RateLimitedLogger.error(UselessMod.LOGGER, "Failed to initialise the AE chemical bridge", exception); + } + } + } + return bridge; + } + + /** 该坐标的 AE 网络端点(化学品);不可用时返回 {@code null}。 */ + @Nullable + public static ChemicalHandlerView chemicalEndpoint(Level level, BlockPos pos) { + AeChemicalBridge resolved = bridge(); + return resolved == null ? null : resolved.chemicalEndpoint(level, pos); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/ae/AeDeviceLinker.java b/src/main/java/com/sorrowmist/useless/compat/ae/AeDeviceLinker.java new file mode 100644 index 00000000..585b1bf3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ae/AeDeviceLinker.java @@ -0,0 +1,415 @@ +package com.sorrowmist.useless.compat.ae; + +import appeng.capabilities.Capabilities; +import appeng.api.networking.GridHelper; +import appeng.api.networking.IGrid; +import appeng.api.networking.IGridConnection; +import appeng.api.networking.IGridNode; +import appeng.api.networking.IInWorldGridNodeHost; +import appeng.api.parts.IPart; +import appeng.api.parts.IPartHost; +import appeng.blockentity.networking.ControllerBlockEntity; +import appeng.blockentity.networking.WirelessAccessPointBlockEntity; +import appeng.util.Platform; +import com.sorrowmist.useless.core.component.UComponents; +import com.sorrowmist.useless.world.ae.AeConnectLinkSavedData; +import net.minecraft.ChatFormatting; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.GlobalPos; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraftforge.fml.ModList; +import org.jetbrains.annotations.Nullable; +import java.util.List; + +/** + * 把「拥有 AE 网格节点」的机器并进工具绑定的那张网。 + * + *

走的是 AE2 自己在量子网络桥 / P2P 上用的那条路: + * {@code GridHelper.createConnection(a, b)} 传 {@code null} 方向 ⇒ 这条连接不是「世界内连接」 + * ({@code isInWorld() == false}),而 {@code InWorldGridNode#cleanupConnections()} 明确跳过非空间连接, + * 所以它不会被 AE2 的邻居重扫清掉;与此同时 {@code GridConnection#mergeGrids} 会把两张网格合成一张。

+ * + *

几个必须记住的副作用:

+ *
    + *
  • 连接按「一根线」算通道({@code getMaxChannels() = 32 × cableCapacityFactor});
  • + *
  • AE2 不把连接写进存档,所以登记表 + {@link #ensureLinks} 是唯一的续命手段;
  • + *
  • 同一对节点只能有一条连接,重复建会抛 {@link IllegalStateException},判重必须遍历 + * {@code getConnections()} 而不是自己维护标志位;
  • + *
  • AE2 的网格节点销毁后不可重建,所以这里只创建/销毁连接,绝不碰别人的节点。
  • + *
  • 断开只认「登记表里那条 + 非空间 + 另一端正好是这台机器」的连接: + * 世界内的线缆连接({@code isInWorld() == true})和其它模组建的连接一律不动。 + * 已登记的连接必然由本模组建立:机器若在登记时已带有连接,则不会进入登记表。
  • + *
+ */ +public final class AeDeviceLinker { + private AeDeviceLinker() { + } + + /** 方块是不是「能连入 AE 网络」的机器:注册了 AE2 的网格节点宿主能力即可(本模组机器 + AE2 设备 + 其它模组 ME 设备)。 */ + public static boolean isLinkTarget(Level level, BlockPos pos) { + if (!ModList.get().isLoaded("ae2")) { + return false; + } + return com.sorrowmist.useless.compat.forge1201.capability.CapabilityAccess.get(level, appeng.capabilities.Capabilities.IN_WORLD_GRID_NODE_HOST, pos, null) != null; + } + + /** + * 右键入口:已登记过的断开,没登记的接入。 + * + *

目标必须是「当前不属于任何网络」的机器——节点已经带着连接的话说明它已经在别人网里了, + * 这时直接拒绝,避免把两张网误并成一张。

+ */ + public static void toggle(ServerLevel level, ServerPlayer player, ItemStack tool, BlockPos pos) { + BlockPos machinePos = pos.immutable(); + GlobalPos bound = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(tool, UComponents.WIRELESS_LINK_TARGET.get()); + if (bound == null) { + notify(player, "gui.useless_mod.ae_connect.not_bound", ChatFormatting.YELLOW); + return; + } + if (bound.dimension().equals(level.dimension()) && bound.pos().equals(machinePos)) { + notify(player, "gui.useless_mod.ae_connect.access_point", ChatFormatting.YELLOW); + return; + } + + AeConnectLinkSavedData data = AeConnectLinkSavedData.get(level.getServer()); + AeConnectLinkSavedData.Link link = new AeConnectLinkSavedData.Link( + level.dimension(), bound.dimension(), bound.pos(), machinePos); + + // 已登记过 ⇒ 断开。断开不要求访问点在线(它卸载时 AE2 已经把连接连带销毁了), + // 而且只会拆「我们自己建的那条非空间连接」:线缆连接和其它模组的无线连接都原样保留。 + if (data.contains(link)) { + IGridNode accessNode = resolveAccessNode(level, bound); + IInWorldGridNodeHost host = com.sorrowmist.useless.compat.forge1201.capability.CapabilityAccess.get(level, + appeng.capabilities.Capabilities.IN_WORLD_GRID_NODE_HOST, machinePos, null); + IGridNode machineNode = host == null ? null : resolveNode(host); + if (accessNode != null && machineNode != null) { + destroyLink(accessNode, machineNode); + } + data.remove(link); + display(player, level, machinePos, "gui.useless_mod.ae_connect.unlinked", ChatFormatting.GREEN); + chime(level, machinePos, false); + return; + } + + IGridNode accessNode = requireAccessNode(player, level, bound); + if (accessNode == null) { + return; + } + + IInWorldGridNodeHost host = com.sorrowmist.useless.compat.forge1201.capability.CapabilityAccess.get(level, + appeng.capabilities.Capabilities.IN_WORLD_GRID_NODE_HOST, machinePos, null); + IGridNode machineNode = host == null ? null : resolveNode(host); + if (machineNode == null) { + notify(player, "gui.useless_mod.ae_connect.not_ready", ChatFormatting.YELLOW); + return; + } + + NetworkState state = networkState(machineNode); + if (state == NetworkState.UNKNOWN) { + notify(player, "gui.useless_mod.ae_connect.not_ready", ChatFormatting.YELLOW); + return; + } + if (state == NetworkState.ALREADY_NETWORKED) { + display(player, level, machinePos, "gui.useless_mod.ae_connect.already_networked", ChatFormatting.RED); + return; + } + + if (!createLink(accessNode, machineNode)) { + notify(player, "gui.useless_mod.ae_connect.not_ready", ChatFormatting.YELLOW); + return; + } + data.add(link); + display(player, level, machinePos, "gui.useless_mod.ae_connect.linked", ChatFormatting.GREEN); + chime(level, machinePos, true); + } + + /** 某个维度下、某台访问点已经连了哪些机器(客户端渲染连接提示用)。 */ + public static List linkedMachines(ServerLevel level, + ResourceKey accessPointDimension, + BlockPos accessPoint) { + return AeConnectLinkSavedData.get(level.getServer()) + .machinesOf(level.dimension(), accessPointDimension, accessPoint); + } + + /** + * 目标现在算不算「已经进了别的网」。 + * + *

判据只看「这张网是不是一张真正在用的网」,不看网格里有几个节点 —— + * 线缆与终端本身就是独立节点(线缆的 owner 是 {@code CablePart}、终端是 + * {@code AbstractTerminalPart},都是部件而非方块实体),所以按「有别的节点就拒绝」会把 + * 「目标身上挂了根线」也误判成「已接入别的网络」,且与供电状态无关。

+ * + *

现行判据只有两条:① 网格中存在控制器(离线亦计入,并入后会产生「多控制器冲突」导致两张网同时失效); + * ② 整网已通电({@code isPowered()} 即判断网格是否供电)。其余情况一律允许并入, + * 目标身上的线缆 / 终端会一并接入 —— 这正是「连进来」的本意。

+ * + *

历史问题:cluster 型多方块(AE2 / 高级AE 的合成 CPU、量子计算机)的部件之间天然存在连接, + * 曾因此误报「已接入其它网络」;现行判据不再依据节点数量,该误报不再出现。

+ */ + private static NetworkState networkState(IGridNode machineNode) { + IGrid grid; + try { + grid = machineNode.getGrid(); + } catch (Throwable notReady) { + // 节点尚未完成初始化(getGrid 抛出 IllegalStateException),本轮无法判定。 + return NetworkState.UNKNOWN; + } + + // ① 目标网络存在控制器 ⇒ 一律拒绝(**离线控制器亦计入**): + // AE2 单个网络只允许一个控制器,并入后会产生「多控制器冲突」,导致两张网同时失效。 + if (grid.getMachineNodes(ControllerBlockEntity.class).iterator().hasNext()) { + return NetworkState.ALREADY_NETWORKED; + } + + // ② 整网已通电并处于运行状态 ⇒ 视为正在使用的网络,不并入。 + if (machineNode.isPowered()) { + return NetworkState.ALREADY_NETWORKED; + } + + // 其余情况(仅挂有线缆 / 终端 / 部件且未通电)⇒ 允许并入。 + return NetworkState.STANDALONE; + } + + /** 静默解析访问点节点:没加载 / 方块不对 / 离线都只返回 null。 */ + @Nullable + private static IGridNode resolveAccessNode(ServerLevel level, GlobalPos bound) { + ServerLevel accessLevel = level.getServer().getLevel(bound.dimension()); + if (accessLevel == null || !accessLevel.isLoaded(bound.pos()) + || !(accessLevel.getBlockEntity(bound.pos()) instanceof WirelessAccessPointBlockEntity ap)) { + return null; + } + IGridNode node = ap.getMainNode().getNode(); + if (node == null || !ap.getMainNode().isOnline() || ap.getGrid() == null) { + return null; + } + return node; + } + + /** 连接前必须有一个在线的访问点,失败时把原因说清楚。 */ + @Nullable + private static IGridNode requireAccessNode(ServerPlayer player, ServerLevel level, GlobalPos bound) { + ServerLevel accessLevel = level.getServer().getLevel(bound.dimension()); + if (accessLevel == null || !accessLevel.isLoaded(bound.pos())) { + notify(player, "gui.useless_mod.ae_connect.access_point_unloaded", ChatFormatting.RED); + return null; + } + if (!(accessLevel.getBlockEntity(bound.pos()) instanceof WirelessAccessPointBlockEntity accessPoint)) { + notify(player, "gui.useless_mod.ae_connect.invalid_access_point", ChatFormatting.RED); + return null; + } + IGridNode node = accessPoint.getMainNode().getNode(); + if (node == null || !accessPoint.getMainNode().isOnline() || accessPoint.getGrid() == null) { + notify(player, "gui.useless_mod.ae_connect.access_point_offline", ChatFormatting.RED); + return null; + } + return node; + } + + /** + * 低频自愈:AE2 不保存连接,区块或存档重载后要把登记过的连接重建回来。 + * 机器/访问点已不存在时,一并清除失效登记。 + */ + public static void ensureLinks(MinecraftServer server) { + AeConnectLinkSavedData data = AeConnectLinkSavedData.get(server); + if (!data.isEmpty()) { + for (AeConnectLinkSavedData.Link link : data.snapshot()) { + // 访问点与机器可能在不同维度,两边都要按各自的维度解析。 + ServerLevel accessLevel = server.getLevel(link.accessPointDimension()); + ServerLevel machineLevel = server.getLevel(link.machineDimension()); + if (accessLevel == null || machineLevel == null) { + continue; + } + if (!accessLevel.isLoaded(link.accessPoint()) || !machineLevel.isLoaded(link.machine())) { + continue; + } + + BlockEntity accessEntity = accessLevel.getBlockEntity(link.accessPoint()); + if (!(accessEntity instanceof WirelessAccessPointBlockEntity accessPoint)) { + data.removeByAccessPoint(link.accessPointDimension(), link.accessPoint()); + continue; + } + + IInWorldGridNodeHost host = com.sorrowmist.useless.compat.forge1201.capability.CapabilityAccess.get(machineLevel, + appeng.capabilities.Capabilities.IN_WORLD_GRID_NODE_HOST, link.machine(), null); + if (host == null) { + data.removeByMachine(link.machineDimension(), link.machine()); + continue; + } + + IGridNode machineNode = resolveNode(host); + IGridNode accessNode = accessPoint.getMainNode().getNode(); + if (machineNode == null || accessNode == null || !accessPoint.getMainNode().isOnline()) { + // 节点尚未就绪(AE2 将节点创建推迟到首个 tick),留待下一轮重试,避免误删登记。 + continue; + } + + createLink(accessNode, machineNode); + } + } + + // 豁免索引按节点身份查询:AE2 更换节点后旧条目无法再匹配,但会持续持有引用, + // 且每次通道重算都会遍历一次。因此在该低频周期中一并清理失效条目。 + AeLinkChannelBypass.pruneStaleLinks(); + } + + /** + * 取宿主的网格节点。 + * + *

三条路径按优先级依次尝试,前一条拿到就返回:

+ * + *
    + *
  1. 契约路径:{@code getGridNode(null)} —— 方块实体自身的主节点, + * 以及挂在 CableBus 中心位的线缆({@code CableBusContainer.getGridNode(null)} + * 会回退到 {@code storage.getCenter()})。
  2. + *
  3. 对外朝向节点:逐个方向问 {@code getGridNode(side)}。只有重写了 + * {@code IPart.getExternalFacingNode()} 的部件才在这条路上有值 —— + * 目前是 P2P 隧道、石英纤维、开关总线三类。
  4. + *
  5. 侧挂部件自身节点(新增):终端、各类总线、接口这些普通部件 + * 没有重写 {@code getExternalFacingNode()}(接口默认返回 null), + * 它们的真实节点只在 {@code IPart.getGridNode()} 上。这类部件通常挂在 CableBus + * 侧面,若该方块又没有中心线缆,前两条路径会一路 fallthrough 返回 null, + * 表现为「孤立终端 / 孤立总线连不上」。这里直接遍历部件补回。
  6. + *
+ */ + @Nullable + private static IGridNode resolveNode(IInWorldGridNodeHost host) { + // ① 契约路径:方块实体主节点 / 中心线缆。 + try { + IGridNode node = host.getGridNode(null); + if (node != null) { + return node; + } + } catch (Throwable ignored) { + } + + // ② 对外朝向节点:P2P 隧道 / 石英纤维 / 开关总线。 + for (Direction direction : Direction.values()) { + try { + IGridNode node = host.getGridNode(direction); + if (node != null) { + return node; + } + } catch (Throwable ignored) { + } + } + + // ③ 侧挂部件自身节点:终端 / 各类总线 / 接口。 + // 这些部件没重写 getExternalFacingNode(),CableBus 的 getGridNode(side) + // 会在 ① 里返回 null,只能从部件本身取。 + if (host instanceof IPartHost partHost) { + IGridNode firstAny = null; + for (Direction side : Platform.DIRECTIONS_WITH_NULL) { + try { + IPart part = partHost.getPart(side); + if (part == null) { + continue; + } + IGridNode node = part.getGridNode(); + if (node == null) { + continue; + } + // 优先返回已就绪(已挂上网格)的节点;未就绪的暂存, + // 若整个方块尚未完成初始化,则由 ensureLinks() 在下一轮重试。 + if (isNodeReady(node)) { + return node; + } + if (firstAny == null) { + firstAny = node; + } + } catch (Throwable ignored) { + } + } + return firstAny; + } + + return null; + } + + /** 节点是否已经挂上网格:未 create 的节点 {@code getGrid()} 会抛异常。 */ + private static boolean isNodeReady(IGridNode node) { + try { + return node.getGrid() != null; + } catch (Throwable notReady) { + return false; + } + } + + static boolean createLink(IGridNode accessNode, IGridNode machineNode) { + if (hasConnection(accessNode, machineNode)) { + // 连接已存在(自愈重跑或重复触发):仍需补登记通道豁免索引,避免遗漏。 + AeLinkChannelBypass.register(accessNode, machineNode); + return true; + } + try { + GridHelper.createConnection(accessNode, machineNode); + } catch (IllegalStateException alreadyConnected) { + boolean connected = hasConnection(accessNode, machineNode); + if (connected) { + AeLinkChannelBypass.register(accessNode, machineNode); + } + return connected; + } + AeLinkChannelBypass.register(accessNode, machineNode); + return true; + } + + /** 只拆我们自己建的非空间连接,绝不动世界内的线缆连接。 */ + static boolean destroyLink(IGridNode accessNode, IGridNode machineNode) { + // 先注销通道豁免,再拆连接:避免重算通道时还按「这条连接在」来豁免。 + AeLinkChannelBypass.unregister(accessNode, machineNode); + boolean destroyed = false; + for (IGridConnection connection : List.copyOf(accessNode.getConnections())) { + if (connection.isInWorld() || connection.getOtherSide(accessNode) != machineNode) { + continue; + } + connection.destroy(); + destroyed = true; + } + return destroyed; + } + + private static boolean hasConnection(IGridNode a, IGridNode b) { + for (IGridConnection connection : a.getConnections()) { + if (connection.getOtherSide(a) == b) { + return true; + } + } + return false; + } + + /** 目标机器的网络归属判定结果。 */ + private enum NetworkState { + /** 没有控制器、也未通电(至多连接线缆 / 终端):可以并入工具绑定的网络。 */ + STANDALONE, + /** 有控制器、或整网已经在跑:拒绝,避免把两张网误并成一张。 */ + ALREADY_NETWORKED, + /** 节点尚未就绪,这一轮判断不了。 */ + UNKNOWN + } + + private static void display(ServerPlayer player, ServerLevel level, BlockPos pos, + String key, ChatFormatting style) { + player.displayClientMessage( + Component.translatable(key, pos.toShortString()).withStyle(style), true); + } + + private static void notify(ServerPlayer player, String key, ChatFormatting style) { + player.displayClientMessage(Component.translatable(key).withStyle(style), true); + } + + private static void chime(ServerLevel level, BlockPos pos, boolean linked) { + level.playSound(null, pos, + linked ? SoundEvents.AMETHYST_BLOCK_CHIME : SoundEvents.AMETHYST_BLOCK_BREAK, + SoundSource.BLOCKS, 0.7F, linked ? 1.4F : 0.8F); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/ae/AeEnergyBridge.java b/src/main/java/com/sorrowmist/useless/compat/ae/AeEnergyBridge.java new file mode 100644 index 00000000..a6befab8 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ae/AeEnergyBridge.java @@ -0,0 +1,25 @@ +package com.sorrowmist.useless.compat.ae; + +import com.sorrowmist.useless.api.logistics.LongEnergyHandler; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +/** + * AE 能量端点的桥接口。 + * + *

与 {@link AeLogisticsBridge} 同一套手法:本接口不含任何 AE2 / AppliedFlux 类型, + * 实现类由 {@link AeEnergyCompatLoader} 反射加载,因此没装这些模组时 + * {@code StaffLinkTargets} 照样能加载、只是永远解析不出能量端点。

+ */ +public interface AeEnergyBridge { + + /** 该坐标是不是 AE 网络端点。 */ + boolean isEndpoint(Level level, BlockPos pos); + + /** + * 该坐标所属 ME 网络的 FE 视角;不是端点、或网络取不到通量存储时返回 {@code null}。 + */ + @Nullable + LongEnergyHandler energyEndpoint(Level level, BlockPos pos); +} diff --git a/src/main/java/com/sorrowmist/useless/compat/ae/AeEnergyCompat.java b/src/main/java/com/sorrowmist/useless/compat/ae/AeEnergyCompat.java new file mode 100644 index 00000000..c28bbc43 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ae/AeEnergyCompat.java @@ -0,0 +1,142 @@ +package com.sorrowmist.useless.compat.ae; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import appeng.api.config.Actionable; +import appeng.api.networking.security.IActionSource; +import appeng.api.stacks.AEKey; +import appeng.api.storage.MEStorage; +import com.glodblock.github.appflux.common.me.key.FluxKey; +import com.glodblock.github.appflux.common.me.key.type.EnergyType; +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.api.logistics.LongEnergyHandler; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * {@link AeEnergyBridge} 的 AppliedFlux 实现。 + * + *

只有本类(以及它引用的 AE2 / AppliedFlux 类)会在两个模组都装了时被加载: + * 常驻代码碰到的永远是 {@link AeEnergyBridge} 那个不含模组类型的接口。

+ * + *

为什么「AE 能量」要靠 AppliedFlux

+ * + *

AE2 自身没有「把 FE 当资源存进网络」这回事,通量元件是 AppliedFlux 加的。它把 FE 存成 + * {@link FluxKey}({@link EnergyType#FE}),键的数量就是 FE 数量,走的就是 ME 存储那套 + * long 契约的 {@code extract} / {@code insert}。所以这里把整个 ME 网络包成一个标准的 + * {@link LongEnergyHandler},搬运逻辑(先模拟后提交、余量退回源)与方块能量完全同源。

+ * + *

判定「是不是端点」复用 {@link AeNetworks#isEndpoint}:只要方块注册了 AE2 的网格节点宿主 + * 能力即可,无线访问点、ME 接口、终端、总线都能绑。

+ */ +public final class AeEnergyCompat implements AeEnergyBridge { + + /** AE2 的动作源:无线物流没有玩家或机器作为发起者,用空源即可。 */ + private static volatile IActionSource cachedSource; + + private static IActionSource source() { + IActionSource local = cachedSource; + if (local == null) { + local = IActionSource.empty(); + cachedSource = local; + } + return local; + } + + @Override + public boolean isEndpoint(Level level, BlockPos pos) { + return AeNetworks.isEndpoint(level, pos); + } + + @Override + @Nullable + public LongEnergyHandler energyEndpoint(Level level, BlockPos pos) { + return isEndpoint(level, pos) ? new FluxEndpoint(level, pos) : null; + } + + /** + * 写入被拒时的限流警告:每 {@link #WARN_INTERVAL_MS} 毫秒最多一条。 + * + *

「接受不了」最常见的原因是网络里压根没有通量存储——FE 不是 AE2 自带的资源, + * 得靠 AppliedFlux 的通量元件。这跟「网络取不到」是两回事,所以要写进日志。 + * 搬运每秒都在重试,不限流会把日志刷爆。

+ */ + private static final long WARN_INTERVAL_MS = 5000L; + private static final AtomicLong LAST_WARN_AT = new AtomicLong(); + + private static void warnRejected(BlockPos pos, String reason) { + long now = System.currentTimeMillis(); + long last = LAST_WARN_AT.get(); + if (now - last < WARN_INTERVAL_MS || !LAST_WARN_AT.compareAndSet(last, now)) { + return; + } + RateLimitedLogger.warn(UselessMod.LOGGER, "无线物流:AE 能量端点 {} 无法接收 FE——{}", pos.toShortString(), reason); + } + + /** ME 网络的 FE(AppliedFlux 通量)视角。 */ + private static final class FluxEndpoint implements LongEnergyHandler { + private final Level level; + private final BlockPos pos; + + FluxEndpoint(Level level, BlockPos pos) { + this.level = level; + this.pos = pos; + } + + /** 通量键:AppliedFlux 把 FE 存成 {@code FluxKey(FE)},数量即 FE 数。 */ + private static AEKey fluxKey() { + return FluxKey.of(EnergyType.FE); + } + + @Override + public long extract(long amount, boolean simulate) { + if (amount <= 0L) { + return 0L; + } + MEStorage storage = AeNetworks.storage(level, pos); + if (storage == null) { + // 取不到网络:表现为「空容器」,抽不出即可。不必报警——掉电/掉线是暂时的。 + return 0L; + } + return Math.max(0L, storage.extract(fluxKey(), amount, + simulate ? Actionable.SIMULATE : Actionable.MODULATE, source())); + } + + @Override + public long receive(long amount, boolean simulate) { + if (amount <= 0L) { + return 0L; + } + MEStorage storage = AeNetworks.storage(level, pos); + if (storage == null) { + warnRejected(pos, "该方块当前取不到 ME 网络(没有挂上网格,或节点尚未就绪)"); + return 0L; + } + long inserted = storage.insert(fluxKey(), amount, + simulate ? Actionable.SIMULATE : Actionable.MODULATE, source()); + if (inserted <= 0L) { + warnRejected(pos, "ME 网络没有收下这些 FE:网里没有通量存储" + + "(需要 AppliedFlux 的通量元件放进 ME 驱动器),或者已经存满了"); + } + return Math.max(0L, inserted); + } + + @Override + public long stored() { + MEStorage storage = AeNetworks.storage(level, pos); + if (storage == null) { + return 0L; + } + return Math.max(0L, storage.getAvailableStacks().get(fluxKey())); + } + + @Override + public long capacity() { + // ME 网络对每一种资源都没有「容量」这个概念,取 long 上限表示不设限。 + return Long.MAX_VALUE; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/ae/AeEnergyCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/ae/AeEnergyCompatLoader.java new file mode 100644 index 00000000..67031c76 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ae/AeEnergyCompatLoader.java @@ -0,0 +1,62 @@ +package com.sorrowmist.useless.compat.ae; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.api.logistics.LongEnergyHandler; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraftforge.fml.ModList; +import org.jetbrains.annotations.Nullable; + +/** + * 按需加载 {@link AeEnergyCompat}。 + * + *

与 {@link AeLogisticsCompatLoader} 同一套手法:常驻类只引用 {@link AeEnergyBridge} 这个 + * 不含 AE2 / AppliedFlux 类型的接口,实现类用 {@code Class.forName} 反射加载,从而在没装 + * AppliedFlux 的整合包里也不会触发类解析错误。

+ * + *

「AE 能量」比其它 AE 资源多一道门槛:FE 要进 ME 网络得靠 AppliedFlux 的通量元件, + * 只有 AE2 是不够的,所以这里同时确认两个模组都在。

+ */ +public final class AeEnergyCompatLoader { + public static final String MOD_ID = "appflux"; + private static final String REQUIRED_MOD_ID = "ae2"; + + private static final String IMPLEMENTATION = "com.sorrowmist.useless.compat.ae.AeEnergyCompat"; + + private static volatile boolean initialized; + private static volatile AeEnergyBridge bridge; + + private AeEnergyCompatLoader() { + } + + /** AE 能量桥;未加载 AppliedFlux 或初始化失败时为 {@code null}。 */ + @Nullable + public static AeEnergyBridge bridge() { + if (!initialized) { + initialized = true; + if (ModList.get().isLoaded(MOD_ID) && ModList.get().isLoaded(REQUIRED_MOD_ID)) { + try { + Class implementation = Class.forName( + IMPLEMENTATION, true, AeEnergyCompatLoader.class.getClassLoader()); + bridge = (AeEnergyBridge) implementation.getDeclaredConstructor().newInstance(); + } catch (ReflectiveOperationException | LinkageError exception) { + RateLimitedLogger.error(UselessMod.LOGGER, "Failed to initialise the AppliedFlux energy bridge", exception); + } + } + } + return bridge; + } + + public static boolean isAvailable() { + return bridge() != null; + } + + /** 该坐标的 AE 能量端点;未加载模组或不是端点时返回 {@code null}。 */ + @Nullable + public static LongEnergyHandler energyEndpoint(Level level, BlockPos pos) { + AeEnergyBridge resolved = bridge(); + return resolved == null ? null : resolved.energyEndpoint(level, pos); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/ae/AeGenericInvBridge.java b/src/main/java/com/sorrowmist/useless/compat/ae/AeGenericInvBridge.java new file mode 100644 index 00000000..d08af915 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ae/AeGenericInvBridge.java @@ -0,0 +1,63 @@ +package com.sorrowmist.useless.compat.ae; + +import com.sorrowmist.useless.api.logistics.LongFluidHandler; +import com.sorrowmist.useless.api.logistics.LongItemHandler; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +/** + * 无线物流接 AE2「局部通用库存」的桥接口。 + * + *

本接口刻意不出现任何 AE2 类型:常驻的 {@code StaffLinkTargets} 只认这个契约, + * 真正的实现({@link AeGenericInvCompat})由 {@link AeGenericInvCompatLoader} 反射加载。 + * 这样没装 AE2 的整合包里,无线物流的全部代码都不会触发 AE2 的类解析错误。

+ * + *

这跟 {@link AeLogisticsBridge} 不是一回事

+ * + *

{@link AeLogisticsBridge} 交付的是「整张 ME 网络」的 long 视角({@code ME_STORAGE} + * 能力),对应 {@code LinkMedium#AE_ITEM} / {@code AE_FLUID}——资源在网里,槽位是即时编号的。

+ * + *

本桥交付的是「方块自己那一小格本地库存」({@code GENERIC_INTERNAL_INV} 能力)。 + * 对 ME 接口而言就是它那 9 格 {@code storage}({@code InterfaceLogic#getStorage()}), + * 与它挂在哪张网、网上有什么完全无关。AE2 已经为这类方块顺带注册了 + * {@code ForgeCapabilities.ITEM_HANDLER}(见 {@code InitCapabilityProviders#registerGenericAdapters}, + * 投影成 {@code GenericStackItemStorage}),所以它本来就能被无线物流当作普通容器搬—— + * 只是那条路要来回 {@code AEKey ↔ ItemStack} 物化。

+ * + *

为什么要专门开一条路

+ * + *

{@code GenericInternalInventory} 的 {@code getAmount} / {@code insert} / {@code extract} + * 本来就是 long 签名,而且不碰 {@code ItemStack}。走它就能把「抽取」这一步的物化整个 + * 去掉——实测那一段占无线物流总耗时的相当一块(见内部无线物流性能分析报告第十三节)。

+ * + *

优先级

+ * + *

命中了 generic 就用 generic,不再问 {@code ItemHandler.BLOCK}。两者语义等价 + * (后者本就是前者的投影),没有方块会同时是「真容器」和「generic 库存」, + * 因此「谁赢」在结果上不可区分。

+ */ +public interface AeGenericInvBridge { + + /** + * 该坐标是不是一个「有局部通用库存」的 AE2 方块。 + * + *

刻意不看在线状态与内容:掉电、空库存都是暂时的。

+ */ + boolean isGenericInv(Level level, BlockPos pos); + + /** + * 把该方块的局部通用库存当作「物品容器」。 + * + * @param side 只在调用方指定时透传;generic 能力的注册 lambda 忽略 context, + * 因此任何面(含 {@code null})都返回同一个库存对象 + * @return 不是这种方块时返回 {@code null} + */ + @Nullable + LongItemHandler itemEndpoint(Level level, BlockPos pos, @Nullable Direction side); + + /** 把该方块的局部通用库存当作「流体容器」;语义同 {@link #itemEndpoint}。 */ + @Nullable + LongFluidHandler fluidEndpoint(Level level, BlockPos pos, @Nullable Direction side); +} diff --git a/src/main/java/com/sorrowmist/useless/compat/ae/AeGenericInvCompat.java b/src/main/java/com/sorrowmist/useless/compat/ae/AeGenericInvCompat.java new file mode 100644 index 00000000..cdfc1d92 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ae/AeGenericInvCompat.java @@ -0,0 +1,590 @@ +package com.sorrowmist.useless.compat.ae; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import appeng.capabilities.Capabilities; +import appeng.api.behaviors.GenericInternalInventory; +import appeng.api.config.Actionable; +import appeng.api.stacks.AEFluidKey; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.AEKeyType; +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.api.logistics.LongFluidHandler; +import com.sorrowmist.useless.api.logistics.LongItemHandler; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * {@link AeGenericInvBridge} 的 AE2 实现。 + * + *

只有本类(以及它引用的 AE2 类)会在装了 AE2 时被加载:常驻代码碰到的永远是 + * {@link AeGenericInvBridge} 那个不含 AE2 类型的接口。

+ * + *

这个端点是「方块本地那一小格库存」

+ * + *

拿的是 {@code appeng.capabilities.Capabilities.GENERIC_INTERNAL_INV}。对 ME 接口,它是 + * {@code InterfaceLogic#getStorage()} —— 那 9 格缓冲,不是整张网络 + * (整张网络挂在 {@code ME_STORAGE} 上,归 {@link AeLogisticsCompat} 管)。因此它天然是个 + * 固定 9 槽、可以「一格一个 key」的普通容器,槽位编号稳定,不需要快照。

+ * + *

收益在哪

+ * + *

{@code GenericInternalInventory} 的 {@code getAmount} / {@code insert} / {@code extract} + * 全是 long 签名,而且完全不碰 {@code ItemStack}。原先这条路走的是 AE2 顺手注册的 + * {@code GenericStackItemStorage}({@code ForgeCapabilities.ITEM_HANDLER} 投影), + * 每读一个槽就要 {@code AEKey → ItemStack} 物化一次(连带复制整份 DataComponent 映射), + * 每抽一次同样要物化。走 native 接口把这一整类开销删掉。

+ * + *

两条硬约束

+ * + *
    + *
  1. {@code beginBatch} / {@code endBatch} 只在真正写入时开,且必须 {@code try/finally}。 + * {@code GenericStackInv} 用 {@code Preconditions.checkState} 守两侧: + * 嵌套会崩;而一旦漏掉 {@code endBatch},{@code suppressOnChange} 就永久为真, + * 这个接口从此再也不会刷新——比崩溃更糟,因为它静默地把机器弄坏。
  2. + *
  3. {@code insert} 的多槽循环不能提前 {@code break}。 + * "收得比给得少就说明满了" 这条推理在这里不成立:某一槽可能只是 key 不符或被 + * 过滤器挡下,后面的槽还有空间。代价是最多 9 次调用,可忽略。
  4. + *
+ */ +public final class AeGenericInvCompat implements AeGenericInvBridge { + + /** + * 写入被拒时的限流警告:每 {@link #WARN_INTERVAL_MS} 毫秒最多一条。 + * + *

独立于 {@link AeLogisticsCompat} 里的那份(那边是「网络不收」,这边是「本地槽不收」, + * 原因不同,混在一起会互相盖掉)。

+ */ + private static final long WARN_INTERVAL_MS = 5000L; + private static final AtomicLong LAST_WARN_AT = new AtomicLong(); + + private static void warnRejected(BlockPos pos, String resource, String reason) { + long now = System.currentTimeMillis(); + long last = LAST_WARN_AT.get(); + if (now - last < WARN_INTERVAL_MS || !LAST_WARN_AT.compareAndSet(last, now)) { + return; + } + RateLimitedLogger.warn(UselessMod.LOGGER, + "无线物流:AE 本地库存 {} 无法接收{}——{}", pos.toShortString(), resource, reason); + } + + /** + * 取该坐标的局部通用库存;不是这种方块、或正处于替换的中间态时返回 {@code null}。 + * + *

{@code side} 直接透传。AE2 的注册 lambda 忽略 context,所以任何面拿到的都是同一个 + * 库存;透传只是为了万一以后有方块按面区分。

+ * + *

{@code catch (Throwable)} 是刻意的:能力查询本身不该抛,但方块实体可能正被替换/卸载 + * (与 {@code AeNetworks#host} 同款防御)。但只在这里吞——常驻代码里不许有。

+ */ + @Nullable + private static GenericInternalInventory inventory(Level level, BlockPos pos, @Nullable Direction side) { + if (level == null || pos == null || !level.isLoaded(pos)) { + return null; + } + try { + return com.sorrowmist.useless.compat.forge1201.capability.CapabilityAccess.get(level, appeng.capabilities.Capabilities.GENERIC_INTERNAL_INV, pos, side); + } catch (Throwable notReady) { + return null; + } + } + + @Override + public boolean isGenericInv(Level level, BlockPos pos) { + return inventory(level, pos, null) != null; + } + + @Override + @Nullable + public LongItemHandler itemEndpoint(Level level, BlockPos pos, @Nullable Direction side) { + GenericInternalInventory inv = inventory(level, pos, side); + // 只接「已挂上网格」的方块:节点未 create 时 getStorage() 会是 null, + // 这里自然就退回了。附带的过滤是 isSupportedType,见端点里的说明。 + return inv == null ? null : new GenericInvItemEndpoint(inv, pos); + } + + @Override + @Nullable + public LongFluidHandler fluidEndpoint(Level level, BlockPos pos, @Nullable Direction side) { + GenericInternalInventory inv = inventory(level, pos, side); + return inv == null ? null : new GenericInvFluidEndpoint(inv, pos); + } + + // ------------------------------------------------------------------ 物品端点 + + /** + * 局部通用库存的物品视角。 + * + *

没有快照。 与整网端点不同,这里的槽位是真实且稳定的(ME 接口恒为 9 格), + * 直接按 {@code inv.getKey(slot)} 读即可。这也是它比走 {@code ItemHandler.BLOCK} 快的原因之一。

+ */ + private static final class GenericInvItemEndpoint implements LongItemHandler { + private final GenericInternalInventory inv; + private final BlockPos pos; + /** 本次端点寿命内「这个库存支持物品」的判定结果;一次调用里不会变。 */ + private final boolean supported; + + GenericInvItemEndpoint(GenericInternalInventory inv, BlockPos pos) { + this.inv = inv; + this.pos = pos; + this.supported = safeSupportedType(AEKeyType.items()); + } + + private boolean safeSupportedType(AEKeyType type) { + try { + return inv.getCapacity(type) > 0; + } catch (Throwable notReady) { + return false; + } + } + + @Override + public int getSlots() { + return inv.size(); + } + + /** + * 这一格的 key;不是物品(或空)时返回 {@code null}。 + * + *

判 {@code AEItemKey} 而不是靠 {@code supported}:接口的 `storage` 是「通用」库存, + * 同一个接口的不同槽可能装着不同类型的 key(升级卡装流体时尤其明显), + * 逐槽判类型才是对的。

+ */ + @Nullable + private AEItemKey keyAt(int slot) { + if (slot < 0 || slot >= inv.size()) { + return null; + } + AEKey key = inv.getKey(slot); + return key instanceof AEItemKey itemKey && inv.getAmount(slot) > 0L ? itemKey : null; + } + + @Override + public ItemStack getStackInSlot(int slot) { + AEItemKey key = keyAt(slot); + // toStack 每次新建对象,无需再 copy。 + return key == null ? ItemStack.EMPTY : key.toStack(1); + } + + @Override + public ItemStack peekStack(int slot) { + // 同 getStackInSlot:底层本来就是新对象,没有「引用被就地清空」的问题。 + return getStackInSlot(slot); + } + + /** + * 槽内实际数量,纯 long,零物化——这是本端点最主要的收益来源。 + * + *

对比:走 {@code GenericStackItemStorage.amountIn} 要先 {@code toStack(1)} 造一个 + * {@code ItemStack},只为读它的 {@code getCount()}。

+ */ + @Override + public long amountIn(int slot) { + if (slot < 0 || slot >= inv.size()) { + return 0L; + } + AEKey key = inv.getKey(slot); + if (!(key instanceof AEItemKey)) { + return 0L; + } + return Math.max(0L, inv.getAmount(slot)); + } + + @Override + public long amountIn(int slot, ItemStack stackFromSlot) { + // 模板的 count 恒为 1(见 getStackInSlot),不代表真实数量,所以忽略它。 + return amountIn(slot); + } + + /** + * 跨槽累加同一 key 的存量。 + * + *

ME 接口的 9 格可以同时装同一种物品的多个槽(配置成同一种、或模糊卡的 + * {@code IGNORE_ALL} 模式下不同组件落进不同槽)。过滤器的「源端保留 8 个」 + * 要按总存量算,所以这里遍历求和——本来就只有 9 次 {@code getKey},纯 long。

+ */ + @Override + public long amountOf(ItemStack template) { + if (template == null || template.isEmpty()) { + return 0L; + } + AEItemKey wanted = AEItemKey.of(template); + if (wanted == null) { + return 0L; + } + long total = 0L; + for (int slot = 0; slot < inv.size(); slot++) { + if (wanted.equals(inv.getKey(slot))) { + total += Math.max(0L, inv.getAmount(slot)); + } + } + return total; + } + + /** + * 按 key 定位,不物化任何 {@code ItemStack}。 + * + *

默认实现要逐槽 {@code getStackInSlot} 再 {@code ItemStack.isSameItemSameComponents} + * ——9 次物化 + 9 次组件映射比较。

+ */ + @Override + public int findSlot(ItemStack template) { + if (template == null || template.isEmpty()) { + return -1; + } + AEItemKey wanted = AEItemKey.of(template); + if (wanted == null) { + return -1; + } + for (int slot = 0; slot < inv.size(); slot++) { + if (inv.getAmount(slot) > 0L && wanted.equals(inv.getKey(slot))) { + return slot; + } + } + return -1; + } + + @Override + public long extractMatching(ItemStack template, long amount, boolean simulate) { + if (template == null || template.isEmpty() || amount <= 0L) { + return 0L; + } + AEItemKey key = AEItemKey.of(template); + if (key == null) { + return 0L; + } + long remaining = amount; + long moved = 0L; + for (int slot = 0; slot < inv.size() && remaining > 0L; slot++) { + if (!key.equals(inv.getKey(slot))) { + continue; + } + long taken = extractFromSlot(slot, key, remaining, simulate); + moved += taken; + remaining -= taken; + } + return moved; + } + + /** + * 抽走某槽里指定 key 的 {@code amount} 个。 + * + *

零物化:{@code inv.extract} 收 {@code AEKey} + long,返回值也是 long, + * 全程不需要 {@code ItemStack}。这是相对 {@code GenericStackItemStorage.extractItem} + * 最大的差异(后者要 {@code toStack(extracted)} 才能报出抽了多少)。

+ * + *

{@code canExtract()} 必须问:样板供应器的 {@code PatternProviderReturnInventory} + * 是「只进不出」的({@code canExtract() = false}),当源端时不能搬空它。

+ */ + private long extractFromSlot(int slot, AEKey key, long amount, boolean simulate) { + if (slot < 0 || slot >= inv.size() || amount <= 0L) { + return 0L; + } + if (simulate) { + try { + return Math.max(0L, inv.extract(slot, key, amount, Actionable.SIMULATE)); + } catch (Throwable notReady) { + return 0L; + } + } + boolean batch = false; + try { + inv.beginBatch(); + batch = true; + return Math.max(0L, inv.extract(slot, key, amount, Actionable.MODULATE)); + } catch (Throwable notReady) { + return 0L; + } finally { + // 必须 endBatch,否则 suppressOnChange 永久为真、接口再不刷新。见类注释。 + if (batch) { + inv.endBatch(); + } + } + } + + @Override + public long extract(int slot, long amount, boolean simulate) { + if (amount <= 0L) { + return 0L; + } + AEItemKey key = keyAt(slot); + if (key == null) { + return 0L; + } + // 不能抽超过槽内存量:AE2 侧会自己夹,但显式夹一次能让返回值与 + // 「快照式实现」的语义完全一致,调用方不必区分。 + long request = Math.min(amount, Math.max(0L, inv.getAmount(slot))); + return request <= 0L ? 0L : extractFromSlot(slot, key, request, simulate); + } + + /** + * 向库存里塞东西。 + * + *

逐槽试 {@code isAllowedIn}(那是配置/过滤卡的判定),但不因某一槽少收就退出 + * ——某个槽可能只是 key 不符,后面的槽还有空间。见类注释第二条硬约束。

+ */ + @Override + public long insert(ItemStack template, long amount, boolean simulate) { + if (template == null || template.isEmpty() || amount <= 0L) { + return 0L; + } + AEItemKey key = AEItemKey.of(template); + if (key == null) { + return 0L; + } + if (!supported) { + warnRejected(pos, "物品", "这个接口的本地库存不接受物品类型(配置里可能只允许流体等)"); + return 0L; + } + long remaining = amount; + long inserted = 0L; + // 只在提交时才开 batch:模拟不改状态、不该牵动通知;而且 endBatch 一旦漏掉 + // 后果严重,能不碰就不碰。 + boolean batch = false; + try { + if (!simulate) { + inv.beginBatch(); + batch = true; + } + for (int slot = 0; slot < inv.size() && remaining > 0L; slot++) { + if (!inv.isAllowed(key)) { + continue; + } + long accepted = inv.insert(slot, key, remaining, + simulate ? Actionable.SIMULATE : Actionable.MODULATE); + if (accepted <= 0L) { + continue; + } + inserted += accepted; + remaining -= accepted; + } + } catch (Throwable notReady) { + // 恰好在这里出错时 insert 可能已经部分生效,返回已计到的量即可(不谎报)。 + return inserted; + } finally { + if (batch) { + inv.endBatch(); + } + } + if (inserted <= 0L && !simulate) { + warnRejected(pos, "物品", "本地库存没有收下它:可能是这 9 个槽都满了、" + + "或配置/过滤卡不允许这种物品进任何槽"); + } + return inserted; + } + } + + // ------------------------------------------------------------------ 流体端点 + + /** 局部通用库存的流体视角;与物品端点严格对称,说明见那边。 */ + private static final class GenericInvFluidEndpoint implements LongFluidHandler { + private final GenericInternalInventory inv; + private final BlockPos pos; + + GenericInvFluidEndpoint(GenericInternalInventory inv, BlockPos pos) { + this.inv = inv; + this.pos = pos; + } + + @Nullable + private AEFluidKey keyAt(int tank) { + if (tank < 0 || tank >= inv.size()) { + return null; + } + AEKey key = inv.getKey(tank); + return key instanceof AEFluidKey fluidKey && inv.getAmount(tank) > 0L ? fluidKey : null; + } + + @Override + public int getTanks() { + return inv.size(); + } + + @Override + public FluidStack getFluidInTank(int tank) { + AEFluidKey key = keyAt(tank); + return key == null ? FluidStack.EMPTY : key.toStack(1); + } + + @Override + public long amountIn(int tank) { + if (tank < 0 || tank >= inv.size()) { + return 0L; + } + AEKey key = inv.getKey(tank); + return key instanceof AEFluidKey ? Math.max(0L, inv.getAmount(tank)) : 0L; + } + + /** + * 这一格的容量。 + * + *

空槽时没有 key 可问 {@code getMaxAmount},回落到「这种 key 类型的通用容量」 + * ({@code getCapacity})。不返回 {@code Long.MAX_VALUE}——那会让调用方以为 + * 可以无限注入;AE2 自己的 {@code GenericStackFluidStorage.getTankCapacity} 也是这个口径。

+ * + *

注意 {@code getCapacity(AEKeyType)} 的文档:它可能只是 estimate。 + * 当前无线物流没有调用方,按契约给个合理值即可。

+ */ + @Override + public long capacityOf(int tank) { + if (tank < 0 || tank >= inv.size()) { + return 0L; + } + AEKey key = inv.getKey(tank); + if (key != null) { + return Math.max(0L, inv.getMaxAmount(key)); + } + try { + return Math.max(0L, inv.getCapacity(AEKeyType.fluids())); + } catch (Throwable notReady) { + return 0L; + } + } + + @Override + public long amountOf(FluidStack type) { + if (type == null || type.isEmpty()) { + return 0L; + } + AEFluidKey wanted = AEFluidKey.of(type); + if (wanted == null) { + return 0L; + } + long total = 0L; + for (int tank = 0; tank < inv.size(); tank++) { + if (wanted.equals(inv.getKey(tank))) { + total += Math.max(0L, inv.getAmount(tank)); + } + } + return total; + } + + @Override + public int findTank(FluidStack type) { + if (type == null || type.isEmpty()) { + return -1; + } + AEFluidKey wanted = AEFluidKey.of(type); + if (wanted == null) { + return -1; + } + for (int tank = 0; tank < inv.size(); tank++) { + if (inv.getAmount(tank) > 0L && wanted.equals(inv.getKey(tank))) { + return tank; + } + } + return -1; + } + + @Override + public long drainMatching(FluidStack type, long amount, boolean simulate) { + if (type == null || type.isEmpty() || amount <= 0L) { + return 0L; + } + AEFluidKey key = AEFluidKey.of(type); + if (key == null) { + return 0L; + } + long remaining = amount; + long moved = 0L; + for (int tank = 0; tank < inv.size() && remaining > 0L; tank++) { + if (!key.equals(inv.getKey(tank))) { + continue; + } + long drained = drainFromTank(tank, key, remaining, simulate); + moved += drained; + remaining -= drained; + } + return moved; + } + + private long drainFromTank(int tank, AEKey key, long amount, boolean simulate) { + if (tank < 0 || tank >= inv.size() || amount <= 0L) { + return 0L; + } + if (simulate) { + try { + return Math.max(0L, inv.extract(tank, key, amount, Actionable.SIMULATE)); + } catch (Throwable notReady) { + return 0L; + } + } + boolean batch = false; + try { + inv.beginBatch(); + batch = true; + return Math.max(0L, inv.extract(tank, key, amount, Actionable.MODULATE)); + } catch (Throwable notReady) { + return 0L; + } finally { + if (batch) { + inv.endBatch(); + } + } + } + + @Override + public long drain(int tank, long amount, boolean simulate) { + if (amount <= 0L) { + return 0L; + } + AEFluidKey key = keyAt(tank); + if (key == null) { + return 0L; + } + long request = Math.min(amount, Math.max(0L, inv.getAmount(tank))); + return request <= 0L ? 0L : drainFromTank(tank, key, request, simulate); + } + + @Override + public long fill(FluidStack type, long amount, boolean simulate) { + if (type == null || type.isEmpty() || amount <= 0L) { + return 0L; + } + AEFluidKey key = AEFluidKey.of(type); + if (key == null) { + warnRejected(pos, "流体", "这种流体无法转成 AE 的资源键(多半是未正确注册的流体)"); + return 0L; + } + long remaining = amount; + long filled = 0L; + boolean batch = false; + try { + if (!simulate) { + inv.beginBatch(); + batch = true; + } + for (int tank = 0; tank < inv.size() && remaining > 0L; tank++) { + if (!inv.isAllowed(key)) { + continue; + } + long accepted = inv.insert(tank, key, remaining, + simulate ? Actionable.SIMULATE : Actionable.MODULATE); + if (accepted <= 0L) { + continue; + } + filled += accepted; + remaining -= accepted; + } + } catch (Throwable notReady) { + return filled; + } finally { + if (batch) { + inv.endBatch(); + } + } + if (filled <= 0L && !simulate) { + warnRejected(pos, "流体", "本地库存没有收下它:可能是这 9 个槽都满了、" + + "或配置/过滤卡不允许这种流体进任何槽"); + } + return filled; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/ae/AeGenericInvCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/ae/AeGenericInvCompatLoader.java new file mode 100644 index 00000000..ef80c8d7 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ae/AeGenericInvCompatLoader.java @@ -0,0 +1,84 @@ +package com.sorrowmist.useless.compat.ae; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.api.logistics.LongFluidHandler; +import com.sorrowmist.useless.api.logistics.LongItemHandler; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; +import net.minecraftforge.fml.ModList; +import org.jetbrains.annotations.Nullable; + +/** + * 按需加载 {@link AeGenericInvCompat}。 + * + *

与 {@link AeLogisticsCompatLoader} 同一套手法:常驻类只引用 {@link AeGenericInvBridge} + * 这个不含 AE2 类型的接口,实现类用 {@code Class.forName} 反射加载, + * 从而在没装 AE2 的整合包里也不会触发类解析错误。

+ * + *

下面的静态方法就是常驻代码要用的全部入口:它们只出现 long 契约与 Minecraft 类型, + * 因此 {@code StaffLinkTargets} 不必自己判空、判模组,也永远不会碰到 AE2 的类。

+ */ +public final class AeGenericInvCompatLoader { + private static final String IMPLEMENTATION = + "com.sorrowmist.useless.compat.ae.AeGenericInvCompat"; + + private static volatile boolean initialized; + private static volatile AeGenericInvBridge bridge; + + private AeGenericInvCompatLoader() { + } + + /** + * 通用库存桥;未加载 AE2 或初始化失败时为 {@code null}。 + * + *

{@code Class.forName(..., true, ...)} 里的 {@code initialize = true} 是刻意的: + * 实现类的静态块形状自检若发现 AE2 的 API 签名漂移({@code GenericInternalInventory} + * 标着 {@code @ApiStatus.Experimental},确实可能变),会在这里抛 + * {@code ExceptionInInitializerError}(属 {@link LinkageError}),被下面捕获后桥就是 + * {@code null},无线物流干净地退回旧路径。若写成 {@code false},那个失败会推迟到第一次 + * 调用,那时已经没人接着了。

+ */ + @Nullable + public static AeGenericInvBridge bridge() { + if (!initialized) { + initialized = true; + if (ModList.get().isLoaded(AeLogisticsCompatLoader.MOD_ID)) { + try { + Class implementation = Class.forName( + IMPLEMENTATION, true, AeGenericInvCompatLoader.class.getClassLoader()); + bridge = (AeGenericInvBridge) implementation.getDeclaredConstructor().newInstance(); + } catch (ReflectiveOperationException | LinkageError exception) { + RateLimitedLogger.error(UselessMod.LOGGER, "Failed to initialise the AE2 generic-inventory bridge", exception); + } + } + } + return bridge; + } + + public static boolean isAvailable() { + return bridge() != null; + } + + /** 该坐标是不是有局部通用库存的 AE2 方块;未加载 AE2 时恒为 {@code false}。 */ + public static boolean isGenericInv(Level level, BlockPos pos) { + AeGenericInvBridge resolved = bridge(); + return resolved != null && resolved.isGenericInv(level, pos); + } + + /** 该坐标的局部通用库存(物品视角);不是这种方块时返回 {@code null}。 */ + @Nullable + public static LongItemHandler itemEndpoint(Level level, BlockPos pos, @Nullable Direction side) { + AeGenericInvBridge resolved = bridge(); + return resolved == null ? null : resolved.itemEndpoint(level, pos, side); + } + + /** 该坐标的局部通用库存(流体视角);不是这种方块时返回 {@code null}。 */ + @Nullable + public static LongFluidHandler fluidEndpoint(Level level, BlockPos pos, @Nullable Direction side) { + AeGenericInvBridge resolved = bridge(); + return resolved == null ? null : resolved.fluidEndpoint(level, pos, side); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/ae/AeLinkChannelBypass.java b/src/main/java/com/sorrowmist/useless/compat/ae/AeLinkChannelBypass.java new file mode 100644 index 00000000..319eb90f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ae/AeLinkChannelBypass.java @@ -0,0 +1,282 @@ +package com.sorrowmist.useless.compat.ae; + +import appeng.api.networking.IGridConnection; +import appeng.api.networking.IGridNode; +import appeng.me.GridNode; +import appeng.me.pathfinding.IPathItem; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayDeque; +import java.util.Collections; +import java.util.Deque; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +/** + * 造化杖「AE 连接模式」那条连接的**通道豁免**索引。 + * + *

背景(读 AE2 19.2.17 源码得出,别再被那个 32 误导):通道上限与 {@code GridConnection} 无关 —— + * {@code GridConnection#getMaxChannels()}({@code 32 × cableCapacityFactor})在通道计算里 + * 没有任何调用点;真正的闸门是沿途节点的 {@code GridNode#getMaxChannels()} + * ({@code 8 × factor},带 {@code DENSE_CAPACITY} 才 {@code 32 × factor},只有 {@code INFINITE} + * 模式返回 {@code MAX_VALUE}),外加「无控制器网格」那条 + * {@code ChannelMode#getAdHocNetworkChannels()}(DEFAULT = 8)的总量限制。 + * 所以想让连接无视频道,只能改这两处计算 —— 也就是这里的两个 mixin。

+ * + *

本类只回答两个问题:

+ *
    + *
  • {@link #crossesLink} —— 某个需要通道的节点,它的通道去路是否跨过了我们建的连接 + * (跨过 ⇒ 直接放行、且完全不进瓶颈记账 ⇒ 那条连接等价于「不占通道也不封顶的线缆」)
  • + *
  • {@link #carriesLinkTraffic} —— 某个节点是否处在「链路」上(用于精准静音 AE2 的超容自检日志)
  • + *
+ * + *

全部按身份比较:网格重建时节点/连接都会被换成新对象,{@code equals} 在这套体系里不可靠。

+ */ +public final class AeLinkChannelBypass { + /** 路由遍历的最大步数,仅用于防御控制器节点自身也持有 route 指针所形成的环。 */ + private static final int MAX_ROUTE_STEPS = 512; + + /** 访问点侧节点 → 它连着的机器侧节点们(一个访问点通常连多台机器)。 */ + private static final Map> MACHINES_BY_ACCESS = new IdentityHashMap<>(); + + /** 一次通道重算期间缓存的「链路节点」集合。 */ + private static Set laneCache; + private static boolean laneValid; + + private AeLinkChannelBypass() { + } + + // ------------------------------------------------------------------ 登记 + + public static void register(IGridNode accessNode, IGridNode machineNode) { + MACHINES_BY_ACCESS + .computeIfAbsent(accessNode, ignored -> Collections.newSetFromMap(new IdentityHashMap<>())) + .add(machineNode); + invalidate(); + } + + public static void unregister(IGridNode accessNode, IGridNode machineNode) { + Set machines = MACHINES_BY_ACCESS.get(accessNode); + if (machines != null && machines.remove(machineNode) && machines.isEmpty()) { + MACHINES_BY_ACCESS.remove(accessNode); + } + invalidate(); + } + + public static boolean hasLinks() { + return !MACHINES_BY_ACCESS.isEmpty(); + } + + /** + * 清掉已经失效的登记。 + * + *

AE2 的节点与连接在区块卸载、方块实体重建时会被整体换掉(连接更是连存档都不写), + * 而我们这张表是按身份查的:旧条目再也匹配不上任何新连接(所以不会误判), + * 但会一直把旧节点强引用住,并且 {@link #farSideNodes()} 每次通道重算都要遍历一遍 —— + * 连接反复重建时表会一直长。低频清一次即可。

+ * + *

判据只看「那条连接还在不在」:连接被拆掉就说明这条链路已经不成立, + * 对应登记本来也再也匹配不上任何东西。重建连接时 {@code AeDeviceLinker#createLink} 会重新登记, + * 所以清掉是幂等且自愈的。

+ */ + public static void pruneStaleLinks() { + if (MACHINES_BY_ACCESS.isEmpty()) { + return; + } + boolean changed = false; + Iterator>> entries = MACHINES_BY_ACCESS.entrySet().iterator(); + while (entries.hasNext()) { + Map.Entry> entry = entries.next(); + if (entry.getValue().removeIf(machine -> !isStillLinked(entry.getKey(), machine))) { + changed = true; + } + if (entry.getValue().isEmpty()) { + entries.remove(); + changed = true; + } + } + if (changed) { + invalidate(); + } + } + + /** 访问点与机器之间是否还有一条「我们建的」非空间连接(节点已销毁按失效处理)。 */ + private static boolean isStillLinked(IGridNode accessNode, IGridNode machineNode) { + try { + for (IGridConnection connection : accessNode.getConnections()) { + if (!connection.isInWorld() && connection.getOtherSide(accessNode) == machineNode) { + return true; + } + } + } catch (Throwable broken) { + // 节点可能在这次清理前被销毁,按失效处理。 + } + return false; + } + + /** 每次通道重算开始时把「链路节点」缓存作废。 */ + public static void invalidate() { + laneCache = null; + laneValid = false; + } + + /** 服务器停止时清空,别把上一局的节点引用留到下一局。 */ + public static void clear() { + MACHINES_BY_ACCESS.clear(); + invalidate(); + } + + /** 这条连接是不是我们建的。 */ + public static boolean isOurLink(IGridConnection connection) { + if (MACHINES_BY_ACCESS.isEmpty()) { + return false; + } + IGridNode a = connection.a(); + IGridNode b = connection.b(); + Set machines = MACHINES_BY_ACCESS.get(a); + if (machines != null && machines.contains(b)) { + return true; + } + machines = MACHINES_BY_ACCESS.get(b); + return machines != null && machines.contains(a); + } + + // ------------------------------------------------------------------ 通道判定 + + /** + * 从 {@code start} 沿「去控制器」的路走,路上是否跨过我们建的连接。 + * + *

{@code GridNode#getControllerRoute()} 给出去控制器的下一条连接, + * {@code GridConnection#getControllerRoute()} 给出更靠控制器那一侧的节点, + * 于是「节点 → 连接 → 节点 → …」交替前进即可;到控制器后指针会打转,用 seen 集合收住。

+ */ + public static boolean crossesLink(@Nullable GridNode start) { + if (start == null || MACHINES_BY_ACCESS.isEmpty()) { + return false; + } + Set seen = Collections.newSetFromMap(new IdentityHashMap<>()); + IPathItem item = start; + seen.add(item); + for (int step = 0; step < MAX_ROUTE_STEPS; step++) { + if (item instanceof IGridConnection connection && isOurLink(connection)) { + return true; + } + IPathItem next; + try { + next = item.getControllerRoute(); + } catch (Throwable noRoute) { + return false; + } + if (next == null || !seen.add(next)) { + return false; + } + item = next; + } + return false; + } + + /** + * 该节点是否在「链路」上:机器侧整片子网,以及从机器节点一路到控制器的祖先链。 + * 这些节点因造化杖连接而超容,静音其自检日志可避免掩盖 AE2 的真实问题。 + */ + public static boolean carriesLinkTraffic(GridNode node) { + if (MACHINES_BY_ACCESS.isEmpty()) { + return false; + } + Set lane = lane(); + return lane != null && lane.contains(node); + } + + /** + * 机器侧那片子网的全部节点(跨过我们的连接就到不了的那些)。 + * 无控制器(ad-hoc)网格的通道配额计数里要把它排除掉。 + */ + public static Set farSideNodes() { + Set result = Collections.newSetFromMap(new IdentityHashMap<>()); + if (MACHINES_BY_ACCESS.isEmpty()) { + return result; + } + for (Set machines : MACHINES_BY_ACCESS.values()) { + for (IGridNode machine : machines) { + collectFarSide(machine, result); + } + } + return result; + } + + @Nullable + private static synchronized Set lane() { + if (laneValid) { + return laneCache; + } + laneValid = true; + Set nodes = Collections.newSetFromMap(new IdentityHashMap<>()); + for (Set machines : MACHINES_BY_ACCESS.values()) { + for (IGridNode machine : machines) { + collectFarSide(machine, nodes); + collectAncestors(machine, nodes); + } + } + Set result = Collections.newSetFromMap(new IdentityHashMap<>()); + for (IGridNode node : nodes) { + if (node instanceof GridNode gridNode) { + result.add(gridNode); + } + } + laneCache = result; + return laneCache; + } + + /** 从机器侧节点做一次「不跨过我们的连接」的遍历,收集它那一侧的整片子网。 */ + private static void collectFarSide(IGridNode start, Set out) { + Deque queue = new ArrayDeque<>(); + Set seen = Collections.newSetFromMap(new IdentityHashMap<>()); + queue.add(start); + seen.add(start); + while (!queue.isEmpty()) { + IGridNode current = queue.poll(); + out.add(current); + try { + for (IGridConnection connection : current.getConnections()) { + if (isOurLink(connection)) { + continue; + } + IGridNode other = connection.getOtherSide(current); + if (other != null && seen.add(other)) { + queue.add(other); + } + } + } catch (Throwable broken) { + // 节点可能在这次重算里被销毁,跳过即可。 + } + } + } + + /** 从机器侧节点沿路由一路收到控制器(含访问点及其上游)。 */ + private static void collectAncestors(IGridNode machine, Set out) { + // IPathItem 是 appeng.me 的实现接口(GridNode / GridConnection 各自实现),IGridNode 本身不是。 + if (!(machine instanceof GridNode start)) { + return; + } + Set seen = Collections.newSetFromMap(new IdentityHashMap<>()); + IPathItem item = start; + seen.add(item); + for (int step = 0; step < MAX_ROUTE_STEPS; step++) { + IPathItem next; + try { + next = item.getControllerRoute(); + } catch (Throwable noRoute) { + return; + } + if (next == null || !seen.add(next)) { + return; + } + if (next instanceof GridNode node) { + out.add(node); + } + item = next; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/ae/AeLogisticsBridge.java b/src/main/java/com/sorrowmist/useless/compat/ae/AeLogisticsBridge.java new file mode 100644 index 00000000..7ce3df3d --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ae/AeLogisticsBridge.java @@ -0,0 +1,51 @@ +package com.sorrowmist.useless.compat.ae; + +import com.sorrowmist.useless.api.logistics.LongFluidHandler; +import com.sorrowmist.useless.api.logistics.LongItemHandler; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +/** + * 无线物流接 AE 网络的桥接口。 + * + *

本接口刻意不出现任何 AE2 类型:常驻的 {@code StaffLinkTargets} 只认这个契约, + * 真正的实现({@link AeLogisticsCompat})由 {@link AeLogisticsCompatLoader} 反射加载。 + * 这样没装 AE2 的整合包里,无线物流的全部代码都不会触发 AE2 的类解析错误。

+ * + *

桥交付的不是「一次插入 / 一次抽出」这样的动作,而是端点本身:AE 网络被包装成 + * 标准的 {@link LongItemHandler} / {@link LongFluidHandler},于是搬运那一侧完全不必知道自己 + * 在跟 AE 打交道 —— {@code moveItems} / {@code moveFluid} 里那套「先模拟后提交、余量退回源」 + * 的逻辑原样复用,连诊断路径都是同一份代码。

+ * + *

数量全程 {@code long}:AE2 的 {@code MEStorage#insert} / {@code #extract} 本身就是 long + * 签名,接进来不需要任何 int 中转,也不存在「超 int 要分多次」的限制。

+ * + *

坐标指的是挂在 AE 网格上的方块那一格(无线访问点、ME 接口、终端、总线都算): + * 桥自己解析它的网格与 ME 存储,调用方不必关心网格是怎么拿到的。

+ */ +public interface AeLogisticsBridge { + + /** + * 该坐标是不是一个 AE 网络端点:方块注册了 AE2 的网格节点宿主能力即可。 + * + *

刻意不看在线状态:掉电、掉线、节点尚未 create 都是暂时的,据此判「方块没了」 + * 会让无线物流的自愈逻辑把玩家配好的线整条删掉。

+ */ + boolean isEndpoint(Level level, BlockPos pos); + + /** + * 把该端点所属的 ME 网络当作「物品容器」。 + * + *

返回的处理器既可抽出(网络是源)也可插入(网络是目标);网络没有固定槽位, + * 槽位是按当前内容即时编号的,详见实现里的说明。

+ * + * @return 不是可用的 AE 端点时返回 {@code null} + */ + @Nullable + LongItemHandler itemEndpoint(Level level, BlockPos pos); + + /** 把该端点所属的 ME 网络当作「流体容器」;语义同 {@link #itemEndpoint}。 */ + @Nullable + LongFluidHandler fluidEndpoint(Level level, BlockPos pos); +} diff --git a/src/main/java/com/sorrowmist/useless/compat/ae/AeLogisticsCompat.java b/src/main/java/com/sorrowmist/useless/compat/ae/AeLogisticsCompat.java new file mode 100644 index 00000000..d05611d1 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ae/AeLogisticsCompat.java @@ -0,0 +1,546 @@ +package com.sorrowmist.useless.compat.ae; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import appeng.api.config.Actionable; +import appeng.api.networking.security.IActionSource; +import appeng.api.stacks.AEFluidKey; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.AEKey; +import appeng.api.storage.MEStorage; +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.api.logistics.LongFluidHandler; +import com.sorrowmist.useless.api.logistics.LongItemHandler; +import net.minecraft.core.BlockPos; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +/** + * {@link AeLogisticsBridge} 的 AE2 实现。 + * + *

只有本类(以及它引用的 AE2 类)会在装了 AE2 时被加载:常驻代码碰到的永远是 + * {@link AeLogisticsBridge} 那个不含 AE2 类型的接口。

+ * + *

为什么交付的是「端点」而不是「一次插入」

+ * + *

无线物流的搬运讲究「先模拟后提交」:先问目标能接多少,再按那个量抽源、塞目标, + * 余量退回源。若桥只提供「插一次、抽一次」的动作,调用方就得在 AE 这条路上重写一遍 + * 这套时序,两套代码迟早走样。

+ * + *

所以这里把整个 ME 网络包成一个标准的 {@link LongItemHandler} / {@link LongFluidHandler}: + * {@code moveItems} / {@code moveFluid} 完全不知道自己面对的是 AE,模拟与执行天然同源, + * 也不存在「超 int 要分多次」——AE2 的 {@code insert} / {@code extract} 本身就是 long 签名。

+ * + *

哪些方块算端点

+ * + *

判定交给 {@link AeNetworks#isEndpoint}:只要方块注册了 AE2 的网格节点宿主能力即可, + * 因此无线访问点、ME 接口、终端、总线都能直接绑进无线物流。早先只认无线访问点, + * 玩家绑别的东西再选 {@code AE_*} 就会「过几秒被自动删掉」,见 {@link AeNetworks} 的说明。

+ * + *

槽位快照

+ * + *

AE 网络没有「第几号槽」这种概念,槽位是常驻代码遍历内容时用的临时编号。端点因此在 + * 第一次被问到内容时抓一份快照(网络里现有的 key 与存量),本次搬运全程沿用这一份: + * 否则边抽边变,{@code slot} 会在遍历途中指向另一个 key。

+ * + *

快照是每次 {@code resolve} 新建端点时重新抓的,所以跨 tick 不会读到陈旧数据; + * 端点只在一次搬运内被使用,不存在长期持有的问题。

+ * + *

但过滤器驱动的搬运根本不走快照。那时「要搬什么」已经完全由过滤器确定, + * 不需要知道网络里有什么,因此调用方走的是 + * {@link LongItemHandler#extractMatching(ItemStack, long, boolean)} / + * {@link LongFluidHandler#drainMatching(FluidStack, long, boolean)} + * —— 本类把它们覆写成「拿模板直接构造资源键、问一次 {@code MEStorage.extract}」。 + * 这一步是必须的:默认实现要先 {@code findSlot},而它必须先抓整网快照, + * 于是每次搬运都要把 ME 网络里的每一种资源遍历一遍(实测占无线物流总耗时的 32%)。 + * 快照只在没有过滤器的扫描路径上才会被用到。

+ * + *

离线端点

+ * + *

判定「是不是端点」只看方块类型,不看是否在线:掉电、掉线都是暂时的, + * 若据此认定线路失效,自愈逻辑会把玩家配好的线整条删掉。网络取不到时端点表现为「空容器」 + * (抽不出、塞不进),搬运自然失败,等网络回来又自动恢复。

+ */ +public final class AeLogisticsCompat implements AeLogisticsBridge { + + /** AE2 的动作源:无线物流没有玩家或机器作为发起者,用空源即可。 */ + private static volatile IActionSource cachedSource; + + private static IActionSource source() { + IActionSource local = cachedSource; + if (local == null) { + local = IActionSource.empty(); + cachedSource = local; + } + return local; + } + + @Override + public boolean isEndpoint(Level level, BlockPos pos) { + return AeNetworks.isEndpoint(level, pos); + } + + @Override + @Nullable + public LongItemHandler itemEndpoint(Level level, BlockPos pos) { + return isEndpoint(level, pos) ? new ItemEndpoint(level, pos) : null; + } + + @Override + @Nullable + public LongFluidHandler fluidEndpoint(Level level, BlockPos pos) { + return isEndpoint(level, pos) ? new FluidEndpoint(level, pos) : null; + } + + /** + * 写入被拒时的限流警告:每 {@link #WARN_INTERVAL_MS} 毫秒最多一条。 + * + *

AE 端点「接受不了」有两类完全不同的原因,而界面上的 {@code TARGET_REJECTED} + * 分不出来:一是网络根本取不到(端点没挂上网 / 节点尚未就绪),二是 + * 网络取到了但不收——最常见的是 ME 网络里压根没有能存这种资源的存储 + * (AE2 的物品与流体各有各的存储元件,只放物品元件的话流体无处可去)。 + * 这两类的处理方式完全不同,所以这里把具体原因写进日志。

+ * + *

限流是必须的:搬运每秒都在重试,不限流会把日志刷爆。

+ */ + private static final long WARN_INTERVAL_MS = 5000L; + private static final AtomicLong LAST_WARN_AT = new AtomicLong(); + + private static void warnRejected(BlockPos pos, String resource, String reason) { + long now = System.currentTimeMillis(); + long last = LAST_WARN_AT.get(); + if (now - last < WARN_INTERVAL_MS || !LAST_WARN_AT.compareAndSet(last, now)) { + return; + } + RateLimitedLogger.warn(UselessMod.LOGGER, + "无线物流:AE 端点 {} 无法接收{}——{}", pos.toShortString(), resource, reason); + } + + /** 一次搬运期间固定的「网络里有什么」快照条目。 */ + private static final class Entry { + private final AEKey key; + private long amount; + + Entry(AEKey key, long amount) { + this.key = key; + this.amount = amount; + } + } + + // ------------------------------------------------------------------ 物品端点 + + /** ME 网络的物品视角。 */ + private static final class ItemEndpoint implements LongItemHandler { + private final Level level; + private final BlockPos pos; + @Nullable + private List snapshot; + /** 本次使用的网络句柄;见 {@link #storage()}。 */ + @Nullable + private MEStorage storage; + private boolean storageResolved; + + ItemEndpoint(Level level, BlockPos pos) { + this.level = level; + this.pos = pos; + } + + /** + * 取 ME 网络句柄,一个端点实例只解析一次。 + * + *

{@code AeNetworks.storage} 每次都要走「取方块实体 → 取能力」。早先这里每次用 + * 都现调一遍,于是一次搬运里要付三四次;而端点在同一轮分配里还会被多个接收端共用 + * (见 {@code StaffLinkEngine.distribute}),次数会再乘上接收端个数。

+ * + *

句柄在网格存续期内是稳定的,缓存不会读到陈旧对象;{@code storageResolved} 单独 + * 记录「查过了但没查到」,避免网络离线时反复重查。端点寿命只有一轮分配, + * 网络恢复后下一个周期就会拿到新的句柄。

+ */ + @Nullable + private MEStorage storage() { + if (!storageResolved) { + storage = AeNetworks.storage(level, pos); + storageResolved = true; + } + return storage; + } + + private List snapshot() { + List local = snapshot; + if (local == null) { + local = new ArrayList<>(); + MEStorage storage = storage(); + if (storage != null) { + for (var entry : storage.getAvailableStacks()) { + AEKey key = entry.getKey(); + long amount = entry.getLongValue(); + if (amount > 0L && key instanceof AEItemKey) { + local.add(new Entry(key, amount)); + } + } + } + snapshot = local; + } + return local; + } + + @Override + public int getSlots() { + return snapshot().size(); + } + + @Override + public ItemStack getStackInSlot(int slot) { + List entries = snapshot(); + if (slot < 0 || slot >= entries.size()) { + return ItemStack.EMPTY; + } + return entries.get(slot).key instanceof AEItemKey itemKey + ? itemKey.toStack(1) + : ItemStack.EMPTY; + } + + @Override + public long amountIn(int slot) { + List entries = snapshot(); + return slot < 0 || slot >= entries.size() ? 0L : entries.get(slot).amount; + } + + /** + * 按资源键定位,不经过 {@link ItemStack}。 + * + *

扫描上千条时这一步不能有额外分配——默认实现每条都要 {@code toStack(1)} 造一个栈。

+ * + *

但它仍然要先抓整网快照,所以过滤器驱动的搬运不再走这里(走 + * {@link #extractMatching(ItemStack, long, boolean)})。保留它是为了没有过滤器的 + * 扫描路径:那里本来就必须知道网络里有什么,绕不开快照;而删掉它会让默认实现退化成 + * 「逐条 {@code toStack(1)} 再比较」,比现在更慢。

+ */ + @Override + public int findSlot(ItemStack template) { + if (template == null || template.isEmpty()) { + return -1; + } + AEItemKey wanted = AEItemKey.of(template); + if (wanted == null) { + return -1; + } + List entries = snapshot(); + for (int slot = 0; slot < entries.size(); slot++) { + Entry entry = entries.get(slot); + if (entry.amount > 0L && entry.key.equals(wanted)) { + return slot; + } + } + return -1; + } + + /** + * 按资源键直接抽取,完全不碰 {@link #snapshot()}。 + * + *

这是过滤器驱动路径的入口(见类注释「槽位快照」一节)。默认实现要先 + * {@code findSlot},而它对 ME 网络意味着先抓一份整网快照 —— 每次搬运遍历一遍网络里 + * 每一种资源。AE2 的 {@code extract} 本来就是 long 签名、按 key 的哈希查找, + * 直接问它即可,「先模拟后提交」的时序与 {@link #extract(int, long, boolean)} 完全一致。

+ * + *

提交后把已抓的快照作废:万一同一轮分配里另一个接收端走的是无过滤器的扫描路径, + * 它必须重新抓一份反映当前存量的快照,不能沿用抽取之前的旧数字。

+ */ + @Override + public long extractMatching(ItemStack template, long amount, boolean simulate) { + if (template == null || template.isEmpty() || amount <= 0L) { + return 0L; + } + AEItemKey key = AEItemKey.of(template); + if (key == null) { + return 0L; + } + MEStorage storage = storage(); + if (storage == null) { + return 0L; + } + long extracted = storage.extract(key, amount, + simulate ? Actionable.SIMULATE : Actionable.MODULATE, source()); + if (extracted > 0L && !simulate) { + snapshot = null; + } + return Math.max(0L, extracted); + } + + /** + * 网络里这一种物品有多少。 + * + *

要抓一份整网快照({@code getAvailableStacks()} 遍历网络里每一种资源), + * 所以只有玩家的过滤器真的勾了「源端保留」或「接收端上限」时才会被调用; + * 没勾就一次都不进来(见 {@code StaffLinkTargets})。

+ * + *

默认实现({@code findSlot} + {@code amountIn})在这里等价但要抓两次快照—— + * 一次 {@code findSlot}、一次 {@code amountIn}。直接查 {@code snapshot()} 的缓存 + * 只抓一次。ME 网络的 {@code getAvailableStacks} 对同一种 key 只报一个条目 + * ({@code KeyCounter} 按 key 聚拢),所以「第一个命中的条目」就是全部存量。

+ */ + @Override + public long amountOf(ItemStack template) { + if (template == null || template.isEmpty()) { + return 0L; + } + AEItemKey wanted = AEItemKey.of(template); + if (wanted == null) { + return 0L; + } + for (Entry entry : snapshot()) { + if (entry.key.equals(wanted)) { + return Math.max(0L, entry.amount); + } + } + return 0L; + } + + @Override + public long extract(int slot, long amount, boolean simulate) { + List entries = snapshot(); + if (slot < 0 || slot >= entries.size() || amount <= 0L) { + return 0L; + } + Entry entry = entries.get(slot); + if (entry.amount <= 0L) { + return 0L; + } + MEStorage storage = storage(); + if (storage == null) { + return 0L; + } + long request = Math.min(amount, entry.amount); + long extracted = storage.extract(entry.key, request, + simulate ? Actionable.SIMULATE : Actionable.MODULATE, source()); + if (extracted > 0L && !simulate) { + // 同步扣减快照:同一次搬运里若又轮到这一条,不会按旧数字重复抽。 + entry.amount -= extracted; + } + return Math.max(0L, extracted); + } + + @Override + public long insert(ItemStack template, long amount, boolean simulate) { + if (template == null || template.isEmpty() || amount <= 0L) { + return 0L; + } + AEItemKey key = AEItemKey.of(template); + if (key == null) { + warnRejected(pos, "物品", "这个物品无法转成 AE 的资源键"); + return 0L; + } + MEStorage storage = storage(); + if (storage == null) { + warnRejected(pos, "物品", "该方块当前取不到 ME 网络(没有挂上网格,或节点尚未就绪)"); + return 0L; + } + long inserted = storage.insert(key, amount, + simulate ? Actionable.SIMULATE : Actionable.MODULATE, source()); + if (inserted <= 0L) { + warnRejected(pos, "物品", "ME 网络没有收下它:要么网里没有物品存储" + + "(物品存储元件,或用存储总线接入的容器),要么这些存储已经满了"); + } + // 刻意不在这里作废快照:无过滤器的扫描路径正靠「一次搬运沿用同一份快照」 + // 保证 slot 在遍历途中始终指向同一个 key(见类注释)。作废它会让扫描循环每插一次 + // 就重抓一遍整网。多出来的存量下次搬运自然可见。 + return Math.max(0L, inserted); + } + } + + // ------------------------------------------------------------------ 流体端点 + + /** ME 网络的流体视角。 */ + private static final class FluidEndpoint implements LongFluidHandler { + private final Level level; + private final BlockPos pos; + @Nullable + private List snapshot; + /** 本次使用的网络句柄;见 {@link #storage()}。 */ + @Nullable + private MEStorage storage; + private boolean storageResolved; + + FluidEndpoint(Level level, BlockPos pos) { + this.level = level; + this.pos = pos; + } + + /** 理由同 {@link ItemEndpoint#storage()}。 */ + @Nullable + private MEStorage storage() { + if (!storageResolved) { + storage = AeNetworks.storage(level, pos); + storageResolved = true; + } + return storage; + } + + private List snapshot() { + List local = snapshot; + if (local == null) { + local = new ArrayList<>(); + MEStorage storage = storage(); + if (storage != null) { + for (var entry : storage.getAvailableStacks()) { + AEKey key = entry.getKey(); + long amount = entry.getLongValue(); + if (amount > 0L && key instanceof AEFluidKey) { + local.add(new Entry(key, amount)); + } + } + } + snapshot = local; + } + return local; + } + + @Override + public int getTanks() { + return snapshot().size(); + } + + @Override + public FluidStack getFluidInTank(int tank) { + List entries = snapshot(); + if (tank < 0 || tank >= entries.size()) { + return FluidStack.EMPTY; + } + return entries.get(tank).key instanceof AEFluidKey fluidKey + ? fluidKey.toStack(1) + : FluidStack.EMPTY; + } + + @Override + public long amountIn(int tank) { + List entries = snapshot(); + return tank < 0 || tank >= entries.size() ? 0L : entries.get(tank).amount; + } + + /** 按资源键定位,不经过 {@link FluidStack};理由同物品端点的 {@code findSlot}。 */ + @Override + public int findTank(FluidStack type) { + if (type == null || type.isEmpty()) { + return -1; + } + AEFluidKey wanted = AEFluidKey.of(type); + if (wanted == null) { + return -1; + } + List entries = snapshot(); + for (int tank = 0; tank < entries.size(); tank++) { + Entry entry = entries.get(tank); + if (entry.amount > 0L && entry.key.equals(wanted)) { + return tank; + } + } + return -1; + } + + @Override + public long capacityOf(int tank) { + // AE 网络对每一种资源都没有「储罐容量」这个概念,取 long 上限表示不设限。 + return Long.MAX_VALUE; + } + + /** + * 按资源键直接抽取,完全不碰 {@link #snapshot()};理由同 + * {@link ItemEndpoint#extractMatching(ItemStack, long, boolean)}。 + */ + @Override + public long drainMatching(FluidStack type, long amount, boolean simulate) { + if (type == null || type.isEmpty() || amount <= 0L) { + return 0L; + } + AEFluidKey key = AEFluidKey.of(type); + if (key == null) { + return 0L; + } + MEStorage storage = storage(); + if (storage == null) { + return 0L; + } + long drained = storage.extract(key, amount, + simulate ? Actionable.SIMULATE : Actionable.MODULATE, source()); + if (drained > 0L && !simulate) { + snapshot = null; + } + return Math.max(0L, drained); + } + + /** + * 网络里这一种流体有多少;理由与代价同 {@link ItemEndpoint#amountOf(ItemStack)}。 + */ + @Override + public long amountOf(FluidStack type) { + if (type == null || type.isEmpty()) { + return 0L; + } + AEFluidKey wanted = AEFluidKey.of(type); + if (wanted == null) { + return 0L; + } + for (Entry entry : snapshot()) { + if (entry.key.equals(wanted)) { + return Math.max(0L, entry.amount); + } + } + return 0L; + } + + @Override + public long drain(int tank, long amount, boolean simulate) { + List entries = snapshot(); + if (tank < 0 || tank >= entries.size() || amount <= 0L) { + return 0L; + } + Entry entry = entries.get(tank); + if (entry.amount <= 0L) { + return 0L; + } + MEStorage storage = storage(); + if (storage == null) { + return 0L; + } + long request = Math.min(amount, entry.amount); + long drained = storage.extract(entry.key, request, + simulate ? Actionable.SIMULATE : Actionable.MODULATE, source()); + if (drained > 0L && !simulate) { + entry.amount -= drained; + } + return Math.max(0L, drained); + } + + @Override + public long fill(FluidStack type, long amount, boolean simulate) { + if (type == null || type.isEmpty() || amount <= 0L) { + return 0L; + } + AEFluidKey key = AEFluidKey.of(type); + if (key == null) { + warnRejected(pos, "流体", "这种流体无法转成 AE 的资源键(多半是未正确注册的流体)"); + return 0L; + } + MEStorage storage = storage(); + if (storage == null) { + warnRejected(pos, "流体", "该方块当前取不到 ME 网络(没有挂上网格,或节点尚未就绪)"); + return 0L; + } + long inserted = storage.insert(key, amount, + simulate ? Actionable.SIMULATE : Actionable.MODULATE, source()); + if (inserted <= 0L) { + warnRejected(pos, "流体", "ME 网络没有收下它:要么网里没有流体存储(AE2 的物品与" + + "流体存储是分开的,需要流体存储元件放进 ME 驱动器,或用存储总线接入一个" + + "流体容器),要么这些存储已经满了"); + } + // 理由同 ItemEndpoint.insert:刻意不作废快照。 + return Math.max(0L, inserted); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/ae/AeLogisticsCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/ae/AeLogisticsCompatLoader.java new file mode 100644 index 00000000..85c2dfb1 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ae/AeLogisticsCompatLoader.java @@ -0,0 +1,75 @@ +package com.sorrowmist.useless.compat.ae; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.api.logistics.LongFluidHandler; +import com.sorrowmist.useless.api.logistics.LongItemHandler; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraftforge.fml.ModList; +import org.jetbrains.annotations.Nullable; + +/** + * 按需加载 {@link AeLogisticsCompat}。 + * + *

与 {@code ArsSourceCompatLoader} / {@code MekanismCompatLoader} 同一套手法:常驻类只引用 + * {@link AeLogisticsBridge} 这个不含 AE2 类型的接口,实现类用 {@code Class.forName} 反射加载, + * 从而在没装 AE2 的整合包里也不会触发类解析错误。

+ * + *

下面的静态方法就是常驻代码要用的全部入口:它们只出现 long 契约与 Minecraft 类型, + * 因此 {@code StaffLinkTargets} 不必自己判空、判模组,也永远不会碰到 AE2 的类。

+ */ +public final class AeLogisticsCompatLoader { + public static final String MOD_ID = "ae2"; + + private static final String IMPLEMENTATION = "com.sorrowmist.useless.compat.ae.AeLogisticsCompat"; + + private static volatile boolean initialized; + private static volatile AeLogisticsBridge bridge; + + private AeLogisticsCompatLoader() { + } + + /** AE 物流桥;未加载 AE2 或初始化失败时为 {@code null}。 */ + @Nullable + public static AeLogisticsBridge bridge() { + if (!initialized) { + initialized = true; + if (ModList.get().isLoaded(MOD_ID)) { + try { + Class implementation = Class.forName( + IMPLEMENTATION, true, AeLogisticsCompatLoader.class.getClassLoader()); + bridge = (AeLogisticsBridge) implementation.getDeclaredConstructor().newInstance(); + } catch (ReflectiveOperationException | LinkageError exception) { + RateLimitedLogger.error(UselessMod.LOGGER, "Failed to initialise the AE2 logistics bridge", exception); + } + } + } + return bridge; + } + + public static boolean isAvailable() { + return bridge() != null; + } + + /** 该坐标是不是 AE 网络端点;未加载 AE2 时恒为 {@code false}。 */ + public static boolean isEndpoint(Level level, BlockPos pos) { + AeLogisticsBridge resolved = bridge(); + return resolved != null && resolved.isEndpoint(level, pos); + } + + /** 该坐标的 AE 网络端点(物品);不是可用端点时返回 {@code null}。 */ + @Nullable + public static LongItemHandler itemEndpoint(Level level, BlockPos pos) { + AeLogisticsBridge resolved = bridge(); + return resolved == null ? null : resolved.itemEndpoint(level, pos); + } + + /** 该坐标的 AE 网络端点(流体);不是可用端点时返回 {@code null}。 */ + @Nullable + public static LongFluidHandler fluidEndpoint(Level level, BlockPos pos) { + AeLogisticsBridge resolved = bridge(); + return resolved == null ? null : resolved.fluidEndpoint(level, pos); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/ae/AeNetworks.java b/src/main/java/com/sorrowmist/useless/compat/ae/AeNetworks.java new file mode 100644 index 00000000..8ceba5ac --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ae/AeNetworks.java @@ -0,0 +1,174 @@ +package com.sorrowmist.useless.compat.ae; + +import appeng.capabilities.Capabilities; +import appeng.api.networking.IGrid; +import appeng.api.networking.IGridNode; +import appeng.api.networking.IInWorldGridNodeHost; +import appeng.api.parts.IPart; +import appeng.api.parts.IPartHost; +import appeng.api.storage.MEStorage; +import appeng.util.Platform; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +/** + * AE 网络解析的共用部分。 + * + *

物品 / 流体 / 化学品 / 魔源四个端点都要「从方块拿到它所属的 ME 存储」这一件事, + * 逻辑完全一样,所以抽到这里。本类只被已经确定加载了 AE2 的类引用。

+ * + *

端点是「任何挂网方块」,不是「无线访问点」

+ * + *

早先这里只认 {@code WirelessAccessPointBlockEntity},于是玩家把 ME 接口、终端、 + * 总线这类方块绑进无线物流、并把资源类型选成 {@code AE_*} 时,端点永远解析成 {@code null}: + * 方块本身明明就在、也明明挂在网上,却像不存在一样。现在判定改为 + * 注册了 AE2 网格节点宿主能力({@code appeng.capabilities.Capabilities.IN_WORLD_GRID_NODE_HOST})即可, + * 与 {@link AeDeviceLinker#isLinkTarget} 同一把尺子,无线访问点只是其中一种。

+ * + *

为什么判定不看「在线」

+ * + *

{@link #isEndpoint} 只看方块是不是网格成员,不看它此刻有没有电、有没有挂上网。 + * 掉电、掉线、节点尚未 create 都是暂时的:若据此认定方块没了,无线物流的自愈逻辑会把玩家 + * 配好的线整条删掉。取不到网络时端点表现为「空容器」(抽不出、塞不进),搬运自然失败, + * 等网络回来又自动恢复。

+ */ +final class AeNetworks { + + private AeNetworks() { + } + + /** + * 该坐标是不是一个 AE 网络端点。 + * + *

只看方块是否注册了网格节点宿主能力,不看在线状态,理由见类注释。

+ */ + static boolean isEndpoint(Level level, BlockPos pos) { + return host(level, pos) != null; + } + + /** 该坐标的网格节点宿主能力;方块不是 AE 设备时为 {@code null}。 */ + @Nullable + static IInWorldGridNodeHost host(Level level, BlockPos pos) { + if (level == null || pos == null || !level.isLoaded(pos)) { + return null; + } + try { + return com.sorrowmist.useless.compat.forge1201.capability.CapabilityAccess.get(level, appeng.capabilities.Capabilities.IN_WORLD_GRID_NODE_HOST, pos, null); + } catch (Throwable notReady) { + // 能力查询本身不该抛,但方块实体可能正处于替换/卸载的中间态。当作「暂时没有」。 + return null; + } + } + + /** 该坐标的网格节点;方块不是 AE 设备、或节点尚未 create 时为 {@code null}。 */ + @Nullable + static IGridNode node(Level level, BlockPos pos) { + IInWorldGridNodeHost host = host(level, pos); + return host == null ? null : resolveNode(host); + } + + /** + * 该坐标所属网络的 ME 存储。 + * + *

每次都现取而不是缓存:网络、存储服务、库存都会在运行期被 AE2 替换,缓存下来的 + * 引用迟早指向一个已经脱离网络的旧对象。

+ */ + @Nullable + static MEStorage storage(Level level, BlockPos pos) { + IGridNode node = node(level, pos); + if (node == null) { + return null; + } + try { + IGrid grid = node.getGrid(); + return grid == null ? null : grid.getStorageService().getInventory(); + } catch (Throwable notReady) { + // 节点尚未 create 时 getGrid() 会抛异常。这是「暂时用不了」,不是错误。 + return null; + } + } + + /** + * 取宿主的网格节点。 + * + *

三条路径按优先级依次尝试,前一条拿到就返回:

+ * + *
    + *
  1. 契约路径:{@code getGridNode(null)} —— 方块实体自身的主节点, + * 以及挂在 CableBus 中心位的线缆({@code CableBusContainer.getGridNode(null)} + * 会回退到 {@code storage.getCenter()})。
  2. + *
  3. 对外朝向节点:逐个方向问 {@code getGridNode(side)}。只有重写了 + * {@code IPart.getExternalFacingNode()} 的部件才在这条路上有值 —— + * 目前是 P2P 隧道、石英纤维、开关总线三类。
  4. + *
  5. 侧挂部件自身节点:终端、各类总线、接口这些普通部件 + * 没有重写 {@code getExternalFacingNode()}(接口默认返回 null), + * 它们的真实节点只在 {@code IPart.getGridNode()} 上。这类部件通常挂在 CableBus + * 侧面,若该方块又没有中心线缆,前两条路径会一路 fallthrough 返回 null, + * 表现为「孤立终端 / 孤立总线连不上」。这里直接遍历部件补回。
  6. + *
+ */ + @Nullable + static IGridNode resolveNode(IInWorldGridNodeHost host) { + // ① 契约路径:方块实体主节点 / 中心线缆。 + try { + IGridNode node = host.getGridNode(null); + if (node != null) { + return node; + } + } catch (Throwable ignored) { + } + + // ② 对外朝向节点:P2P 隧道 / 石英纤维 / 开关总线。 + for (Direction direction : Direction.values()) { + try { + IGridNode node = host.getGridNode(direction); + if (node != null) { + return node; + } + } catch (Throwable ignored) { + } + } + + // ③ 侧挂部件自身节点:终端 / 各类总线 / 接口。 + // 这些部件没重写 getExternalFacingNode(),CableBus 的 getGridNode(side) + // 会在 ① 里返回 null,只能从部件本身取。 + if (host instanceof IPartHost partHost) { + IGridNode firstAny = null; + for (Direction side : Platform.DIRECTIONS_WITH_NULL) { + try { + IPart part = partHost.getPart(side); + if (part == null) { + continue; + } + IGridNode node = part.getGridNode(); + if (node == null) { + continue; + } + // 优先返回已就绪(已挂上网格)的节点;未就绪的暂存, + // 若整个方块尚未完成初始化,则由调用方在下一轮重试。 + if (isNodeReady(node)) { + return node; + } + if (firstAny == null) { + firstAny = node; + } + } catch (Throwable ignored) { + } + } + return firstAny; + } + + return null; + } + + /** 节点是否已经挂上网格:未 create 的节点 {@code getGrid()} 会抛异常。 */ + static boolean isNodeReady(IGridNode node) { + try { + return node.getGrid() != null; + } catch (Throwable notReady) { + return false; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/ae/AePressureBridge.java b/src/main/java/com/sorrowmist/useless/compat/ae/AePressureBridge.java new file mode 100644 index 00000000..1e6ac059 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ae/AePressureBridge.java @@ -0,0 +1,27 @@ +package com.sorrowmist.useless.compat.ae; + +import com.sorrowmist.useless.api.logistics.LongPressureHandler; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +/** + * AE 气压端点的桥接口。 + * + *

与 {@link AeEnergyBridge} 同一套手法:本接口不含任何 AE2 / Applied Pneumatics 类型, + * 实现类由 {@link AePressureCompatLoader} 反射加载,因此没装这些模组时 + * {@code StaffLinkTargets} 照样能加载、只是永远解析不出气压端点。

+ * + *

「AE 气压」来自 Applied Pneumatics:它把气动工艺的空气建模成一个 AE 资源键 + * ({@code AirKey}),于是空气能像物品一样存进 ME 网络。网络里只有「一堆空气(mL)」, + * 没有体积,因此这条端点 {@link LongPressureHandler#volume()} 恒为 0——目标气压对它无意义。

+ */ +public interface AePressureBridge { + + /** 该坐标是不是 AE 网络端点。 */ + boolean isEndpoint(Level level, BlockPos pos); + + /** 该坐标所属 ME 网络的气压视角;不是端点、或网络取不到时返回 {@code null}。 */ + @Nullable + LongPressureHandler pressureEndpoint(Level level, BlockPos pos); +} diff --git a/src/main/java/com/sorrowmist/useless/compat/ae/AePressureCompat.java b/src/main/java/com/sorrowmist/useless/compat/ae/AePressureCompat.java new file mode 100644 index 00000000..2efd45e8 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ae/AePressureCompat.java @@ -0,0 +1,140 @@ +package com.sorrowmist.useless.compat.ae; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import appeng.api.config.Actionable; +import appeng.api.networking.security.IActionSource; +import appeng.api.stacks.AEKey; +import appeng.api.storage.MEStorage; +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.api.logistics.LongPressureHandler; +import com.wintercogs.appliedpneumatics.common.me.keys.AirKey; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * {@link AePressureBridge} 的 Applied Pneumatics 实现。 + * + *

只有本类(以及它引用的 AE2 / Applied Pneumatics 类)会在两个模组都装了时被加载: + * 常驻代码碰到的永远是 {@link AePressureBridge} 那个不含模组类型的接口。

+ * + *

为什么「AE 气压」要靠 Applied Pneumatics

+ * + *

AE2 自身没有「把空气当资源存进网络」这回事,Applied Pneumatics 把气动工艺的空气建模成 + * {@link AirKey}({@code appliedpneumatics:air_key}),键的数量就是空气量(mL), + * 走的就是 ME 存储那套 long 契约的 {@code extract} / {@code insert}。结构与 + * {@link AeEnergyCompat}(AppliedFlux 的 {@code FluxKey})完全同源。

+ * + *

判定「是不是端点」复用 {@link AeNetworks#isEndpoint}:只要方块注册了 AE2 的网格节点宿主 + * 能力即可,无线访问点、ME 接口、Applied Pneumatics 的 ME 气压接口都能绑。

+ */ +public final class AePressureCompat implements AePressureBridge { + + /** AE2 的动作源:无线物流没有玩家或机器作为发起者,用空源即可。 */ + private static volatile IActionSource cachedSource; + + private static IActionSource source() { + IActionSource local = cachedSource; + if (local == null) { + local = IActionSource.empty(); + cachedSource = local; + } + return local; + } + + @Override + public boolean isEndpoint(Level level, BlockPos pos) { + return AeNetworks.isEndpoint(level, pos); + } + + @Override + @Nullable + public LongPressureHandler pressureEndpoint(Level level, BlockPos pos) { + return isEndpoint(level, pos) ? new AirEndpoint(level, pos) : null; + } + + /** 写入被拒时的限流警告:每 {@link #WARN_INTERVAL_MS} 毫秒最多一条。 */ + private static final long WARN_INTERVAL_MS = 5000L; + private static final AtomicLong LAST_WARN_AT = new AtomicLong(); + + private static void warnRejected(BlockPos pos, String reason) { + long now = System.currentTimeMillis(); + long last = LAST_WARN_AT.get(); + if (now - last < WARN_INTERVAL_MS || !LAST_WARN_AT.compareAndSet(last, now)) { + return; + } + RateLimitedLogger.warn(UselessMod.LOGGER, "无线物流:AE 气压端点 {} 无法接收空气——{}", pos.toShortString(), reason); + } + + /** ME 网络里空气(Applied Pneumatics 的 AirKey)的视角。 */ + private static final class AirEndpoint implements LongPressureHandler { + private final Level level; + private final BlockPos pos; + + AirEndpoint(Level level, BlockPos pos) { + this.level = level; + this.pos = pos; + } + + private static AEKey airKey() { + return AirKey.INSTANCE; + } + + @Override + public long extract(long amount, boolean simulate) { + if (amount <= 0L) { + return 0L; + } + MEStorage storage = AeNetworks.storage(level, pos); + if (storage == null) { + // 取不到网络:表现为「空容器」,抽不出即可。不必报警——掉电/掉线是暂时的。 + return 0L; + } + return Math.max(0L, storage.extract(airKey(), amount, + simulate ? Actionable.SIMULATE : Actionable.MODULATE, source())); + } + + @Override + public long receive(long amount, boolean simulate) { + if (amount <= 0L) { + return 0L; + } + MEStorage storage = AeNetworks.storage(level, pos); + if (storage == null) { + warnRejected(pos, "该方块当前取不到 ME 网络(没有挂上网格,或节点尚未就绪)"); + return 0L; + } + long inserted = storage.insert(airKey(), amount, + simulate ? Actionable.SIMULATE : Actionable.MODULATE, source()); + if (inserted <= 0L) { + warnRejected(pos, "ME 网络没有收下这些空气:网里没有空气存储" + + "(需要 Applied Pneumatics 的气体存储元件),或者已经存满了"); + } + return Math.max(0L, inserted); + } + + @Override + public long stored() { + MEStorage storage = AeNetworks.storage(level, pos); + if (storage == null) { + return 0L; + } + return Math.max(0L, storage.getAvailableStacks().get(airKey())); + } + + @Override + public long capacity() { + // ME 网络对每一种资源都没有「容量」这个概念,取 long 上限表示不设限。 + return Long.MAX_VALUE; + } + + @Override + public long volume() { + // 网络里只有一堆空气,没有容器体积 ⇒ 没有压力概念,目标气压对它无意义。 + return 0L; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/ae/AePressureCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/ae/AePressureCompatLoader.java new file mode 100644 index 00000000..92432fc3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ae/AePressureCompatLoader.java @@ -0,0 +1,62 @@ +package com.sorrowmist.useless.compat.ae; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.api.logistics.LongPressureHandler; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraftforge.fml.ModList; +import org.jetbrains.annotations.Nullable; + +/** + * 按需加载 {@link AePressureCompat}。 + * + *

与 {@link AeEnergyCompatLoader} 同一套手法:常驻类只引用 {@link AePressureBridge} 这个 + * 不含 AE2 / Applied Pneumatics 类型的接口,实现类用 {@code Class.forName} 反射加载, + * 从而在没装这些模组的整合包里也不会触发类解析错误。

+ * + *

空气要进 ME 网络得靠 Applied Pneumatics(它把空气做成一个 AE 资源键), + * 只有 AE2 是不够的,所以这里同时确认两个模组都在。

+ */ +public final class AePressureCompatLoader { + public static final String MOD_ID = "appliedpneumatics"; + private static final String REQUIRED_MOD_ID = "ae2"; + + private static final String IMPLEMENTATION = "com.sorrowmist.useless.compat.ae.AePressureCompat"; + + private static volatile boolean initialized; + private static volatile AePressureBridge bridge; + + private AePressureCompatLoader() { + } + + /** AE 气压桥;未加载 Applied Pneumatics 或初始化失败时为 {@code null}。 */ + @Nullable + public static AePressureBridge bridge() { + if (!initialized) { + initialized = true; + if (ModList.get().isLoaded(MOD_ID) && ModList.get().isLoaded(REQUIRED_MOD_ID)) { + try { + Class implementation = Class.forName( + IMPLEMENTATION, true, AePressureCompatLoader.class.getClassLoader()); + bridge = (AePressureBridge) implementation.getDeclaredConstructor().newInstance(); + } catch (ReflectiveOperationException | LinkageError exception) { + RateLimitedLogger.error(UselessMod.LOGGER, "Failed to initialise the Applied Pneumatics bridge", exception); + } + } + } + return bridge; + } + + public static boolean isAvailable() { + return bridge() != null; + } + + /** 该坐标的 AE 气压端点;未加载模组或不是端点时返回 {@code null}。 */ + @Nullable + public static LongPressureHandler pressureEndpoint(Level level, BlockPos pos) { + AePressureBridge resolved = bridge(); + return resolved == null ? null : resolved.pressureEndpoint(level, pos); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/ae/AeSourceBridge.java b/src/main/java/com/sorrowmist/useless/compat/ae/AeSourceBridge.java new file mode 100644 index 00000000..e5fb004f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ae/AeSourceBridge.java @@ -0,0 +1,24 @@ +package com.sorrowmist.useless.compat.ae; + +import com.sorrowmist.useless.content.stafflink.SourceHandlerView; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +/** + * 无线物流接「AE 网络里的魔源」的桥接口。 + * + *

魔源能进 ME 网络靠的是 Ars Énergistique(modid {@code arseng})注册的 + * {@code SourceKey}。同 {@link AeChemicalBridge},这里刻意不含任何 AE2 / Ars 类型, + * 由独立实现类在对应前置齐备时才被反射载入。

+ */ +public interface AeSourceBridge { + + /** + * 把该端点所属 ME 网络里的魔源当作一个容器端点。 + * + * @return 不是可用的 AE 端点时返回 {@code null} + */ + @Nullable + SourceHandlerView sourceEndpoint(Level level, BlockPos pos); +} diff --git a/src/main/java/com/sorrowmist/useless/compat/ae/AeSourceCompat.java b/src/main/java/com/sorrowmist/useless/compat/ae/AeSourceCompat.java new file mode 100644 index 00000000..3e7460c1 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ae/AeSourceCompat.java @@ -0,0 +1,116 @@ +package com.sorrowmist.useless.compat.ae; + +import appeng.api.config.Actionable; +import appeng.api.networking.security.IActionSource; +import appeng.api.stacks.AEKey; +import appeng.api.storage.MEStorage; +import com.sorrowmist.useless.content.stafflink.SourceHandlerView; +import gripe._90.arseng.me.key.SourceKey; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; + +/** + * {@link AeSourceBridge} 的 Ars Énergistique 实现。 + * + *

只有 AE2 与 arseng 同时在场时,{@link AeSourceCompatLoader} 才会加载本类。

+ * + *

为什么需要一个条目而不是直接读写

+ * + *

ME 网络里的魔源就是一条 {@link SourceKey#KEY} 记录,量走 key 的值。 + * 因此端点不需要快照列表:每次操作直接对这条记录做 long 的 {@code insert}/{@code extract}, + * 再夹到 int 返回给搬运侧——魔源的 int 上限是 Ars Nouveau 自身的物理事实,不是我们的截断。

+ */ +public final class AeSourceCompat implements AeSourceBridge { + + private static volatile IActionSource cachedSource; + + private static IActionSource source() { + IActionSource local = cachedSource; + if (local == null) { + local = IActionSource.empty(); + cachedSource = local; + } + return local; + } + + @Override + public SourceHandlerView sourceEndpoint(Level level, BlockPos pos) { + return AeNetworks.isEndpoint(level, pos) ? new Endpoint(level, pos) : null; + } + + private static final class Endpoint implements SourceHandlerView { + private final Level level; + private final BlockPos pos; + + Endpoint(Level level, BlockPos pos) { + this.level = level; + this.pos = pos; + } + + /** + * 网络里的魔源存量。 + * + *

每次现查而不是缓存:ME 网络内容随时在变,缓存出来的值会立刻过时。

+ */ + private long stored() { + MEStorage storage = AeNetworks.storage(level, pos); + if (storage == null) { + return 0L; + } + long total = 0L; + for (var entry : storage.getAvailableStacks()) { + AEKey key = entry.getKey(); + if (key == SourceKey.KEY) { + total += Math.max(0L, entry.getLongValue()); + } + } + return total; + } + + @Override + public int amount() { + return clamp(stored()); + } + + @Override + public int capacity() { + // ME 网络对魔源不设「罐容量」,用 SourceKey 自己的上限表示。 + return SourceKey.MAX_SOURCE; + } + + @Override + public int extract(int amount, boolean simulate) { + if (amount <= 0) { + return 0; + } + MEStorage storage = AeNetworks.storage(level, pos); + if (storage == null) { + return 0; + } + long taken = storage.extract(SourceKey.KEY, amount, + simulate ? Actionable.SIMULATE : Actionable.MODULATE, source()); + return clamp(taken); + } + + @Override + public int receive(int amount, boolean simulate) { + if (amount <= 0) { + return 0; + } + MEStorage storage = AeNetworks.storage(level, pos); + if (storage == null) { + return 0; + } + long inserted = storage.insert(SourceKey.KEY, amount, + simulate ? Actionable.SIMULATE : Actionable.MODULATE, source()); + return clamp(inserted); + } + + private static int clamp(long value) { + if (value <= 0L) { + return 0; + } + return (int) Math.min(value, Integer.MAX_VALUE); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/ae/AeSourceCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/ae/AeSourceCompatLoader.java new file mode 100644 index 00000000..47aa030b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ae/AeSourceCompatLoader.java @@ -0,0 +1,53 @@ +package com.sorrowmist.useless.compat.ae; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.stafflink.SourceHandlerView; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraftforge.fml.ModList; +import org.jetbrains.annotations.Nullable; + +/** + * 按需加载 {@link AeSourceCompat}。 + * + *

需要 AE2 与 Ars Énergistique 同时在场:前者提供 ME 存储,后者提供 + * {@code SourceKey} 这个「魔源在 ME 网络里的身份」。

+ */ +public final class AeSourceCompatLoader { + private static final String AE2 = "ae2"; + private static final String ARS_ENERGISTIQUE = "arseng"; + + private static final String IMPLEMENTATION = "com.sorrowmist.useless.compat.ae.AeSourceCompat"; + + private static volatile boolean initialized; + private static volatile AeSourceBridge bridge; + + private AeSourceCompatLoader() { + } + + @Nullable + public static AeSourceBridge bridge() { + if (!initialized) { + initialized = true; + if (ModList.get().isLoaded(AE2) && ModList.get().isLoaded(ARS_ENERGISTIQUE)) { + try { + Class implementation = Class.forName( + IMPLEMENTATION, true, AeSourceCompatLoader.class.getClassLoader()); + bridge = (AeSourceBridge) implementation.getDeclaredConstructor().newInstance(); + } catch (ReflectiveOperationException | LinkageError exception) { + RateLimitedLogger.error(UselessMod.LOGGER, "Failed to initialise the AE source bridge", exception); + } + } + } + return bridge; + } + + /** 该坐标的 AE 网络端点(魔源);不可用时返回 {@code null}。 */ + @Nullable + public static SourceHandlerView sourceEndpoint(Level level, BlockPos pos) { + AeSourceBridge resolved = bridge(); + return resolved == null ? null : resolved.sourceEndpoint(level, pos); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/ae/DynamicReflectionSupport.java b/src/main/java/com/sorrowmist/useless/compat/ae/DynamicReflectionSupport.java new file mode 100644 index 00000000..937d32b9 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ae/DynamicReflectionSupport.java @@ -0,0 +1,115 @@ +package com.sorrowmist.useless.compat.ae; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.mojang.logging.LogUtils; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** Null-safe reflection helpers for optional CPU implementations. */ +public final class DynamicReflectionSupport { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final Set LOGGED_FAILURES = + Collections.synchronizedSet(new HashSet<>()); + + private DynamicReflectionSupport() { + } + + @Nullable + public static Class findClassSafe(String name) { + try { + return Class.forName(name); + } catch (ReflectiveOperationException | LinkageError ignored) { + return null; + } + } + + @Nullable + public static Field findFieldSafe(@Nullable Class owner, String name) { + if (owner == null) { + return null; + } + try { + Field field = owner.getDeclaredField(name); + field.setAccessible(true); + return field; + } catch (Exception ignored) { + return null; + } + } + + @Nullable + public static Method findMethodSafe(@Nullable Class owner, String name, Class... parameters) { + if (owner == null) { + return null; + } + try { + Method method = owner.getDeclaredMethod(name, parameters); + method.setAccessible(true); + return method; + } catch (Exception ignored) { + return null; + } + } + + @Nullable + public static Object get(@Nullable Field field, Object target) { + if (field == null) { + return null; + } + try { + return field.get(target); + } catch (ReflectiveOperationException exception) { + logOnce("read field", exception); + return null; + } + } + + public static long getLong(@Nullable Field field, Object target, long fallback) { + if (field == null) { + return fallback; + } + try { + return field.getLong(target); + } catch (ReflectiveOperationException exception) { + logOnce("read long field", exception); + return fallback; + } + } + + public static void setLong(@Nullable Field field, Object target, long value, String action) { + if (field == null) { + return; + } + try { + field.setLong(target, value); + } catch (ReflectiveOperationException exception) { + logOnce(action, exception); + } + } + + @Nullable + public static Object invoke(@Nullable Method method, Object target, String action, Object... arguments) { + if (method == null) { + return null; + } + try { + return method.invoke(target, arguments); + } catch (ReflectiveOperationException exception) { + logOnce(action, exception); + return null; + } + } + + private static void logOnce(String action, Throwable exception) { + if (LOGGED_FAILURES.add(action)) { + RateLimitedLogger.warn(LOGGER, "Useless Mod optional CPU reflection failed while trying to {}.", action, exception); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/apotheosis/ApotheosisRecipeCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/apotheosis/ApotheosisRecipeCompatLoader.java new file mode 100644 index 00000000..1d37b9d6 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/apotheosis/ApotheosisRecipeCompatLoader.java @@ -0,0 +1,16 @@ +package com.sorrowmist.useless.compat.apotheosis; + +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.apotheosis.ApotheosisAnvilSmashingRecipeAdapter; + +/** 在神化加载完成后注册其铁砧宝石粉碎转换。 */ +public final class ApotheosisRecipeCompatLoader { + private ApotheosisRecipeCompatLoader() { + } + + public static void register() { + AlloyFurnaceRecipeManager.getInstance().registerAdapter( + new ApotheosisAnvilSmashingRecipeAdapter(), RecipeSourceIds.APOTHEOSIS); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/apothicflux/ApothicFluxRecipeCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/apothicflux/ApothicFluxRecipeCompatLoader.java new file mode 100644 index 00000000..5474eed9 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/apothicflux/ApothicFluxRecipeCompatLoader.java @@ -0,0 +1,19 @@ +package com.sorrowmist.useless.compat.apothicflux; + +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.apothicflux.InfusionRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.apothicflux.RitualCraftingRecipeAdapter; + +/** Registers Apothic Flux recipe integrations after the optional mod has loaded. */ +public final class ApothicFluxRecipeCompatLoader { + private ApothicFluxRecipeCompatLoader() { + } + + public static void register() { + AlloyFurnaceRecipeManager.getInstance().registerAdapter( + new RitualCraftingRecipeAdapter(), RecipeSourceIds.APOTHIC_FLUX); + AlloyFurnaceRecipeManager.getInstance().registerAdapter( + new InfusionRecipeAdapter(), RecipeSourceIds.APOTHIC_FLUX); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/appmek/AppMekChemicalCompat.java b/src/main/java/com/sorrowmist/useless/compat/appmek/AppMekChemicalCompat.java new file mode 100644 index 00000000..2c2217c9 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/appmek/AppMekChemicalCompat.java @@ -0,0 +1,69 @@ +package com.sorrowmist.useless.compat.appmek; + +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import com.sorrowmist.useless.compat.mekanism.MekanismChemicalCompat; +import com.sorrowmist.useless.compat.mekanism.MekanismChemicalStackView; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalKeyProvider; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalKeyProviders; +import me.ramidzkh.mekae2.ae2.MekanismKey; +import mekanism.api.chemical.ChemicalStack; +import com.sorrowmist.useless.compat.forge1201.capability.RegisterCapabilitiesEvent; +import org.jetbrains.annotations.Nullable; + +/** AppMek-only native capability and AE key bridge. */ +public final class AppMekChemicalCompat { + public static final String MOD_ID = "appmek"; + + private AppMekChemicalCompat() { + } + + public static void registerKeyProvider() { + ChemicalKeyProviders.register(new Provider()); + } + + public static void register(RegisterCapabilitiesEvent event) { + registerKeyProvider(); + // Forge capabilities are keyed by their handler interface. AppMek's four + // CapabilityTokens resolve to the same native GAS/INFUSION/PIGMENT/SLURRY + // capabilities already registered by MekanismChemicalCompat. + } + + /** Converts a concrete Mekanism stack to the AppMek AE key without exposing AppMek to callers. */ + public static @Nullable GenericStack toGenericStack(ChemicalStack stack) { + if (stack == null || stack.isEmpty() || stack.getAmount() <= 0L) return null; + MekanismKey key = MekanismKey.of(mekanism.common.util.ChemicalUtil.copyWithAmount(stack,1L)); + return key == null ? null : new GenericStack(key, stack.getAmount()); + } + + /** Creates a concrete Mekanism stack from a registered AppMek key. */ + public static @Nullable ChemicalStack toChemicalStack(GenericStack stack) { + if (stack == null || stack.amount() <= 0L || !(stack.what() instanceof MekanismKey key)) return null; + ChemicalStack result = key.withAmount(stack.amount()); + return result.isEmpty() ? null : result; + } + + private static final class Provider implements ChemicalKeyProvider { + @Override + public @Nullable GenericStack toGenericStack(com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalStackView stack) { + if (!(stack instanceof MekanismChemicalStackView mek) || mek.isEmpty() || mek.amount() <= 0L) { + return null; + } + return AppMekChemicalCompat.toGenericStack(mek.stack()); + } + + @Override + public @Nullable com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalStackView fromGenericStack(GenericStack stack) { + if (stack == null || stack.amount() <= 0L || !(stack.what() instanceof MekanismKey key)) { + return null; + } + ChemicalStack chemical = AppMekChemicalCompat.toChemicalStack(stack); + return chemical == null ? null : new MekanismChemicalStackView(chemical); + } + + @Override + public boolean isChemicalKey(@Nullable AEKey key) { + return key instanceof MekanismKey; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/appmek/AppMekJeiChemicalCompat.java b/src/main/java/com/sorrowmist/useless/compat/appmek/AppMekJeiChemicalCompat.java new file mode 100644 index 00000000..a0ac2a6c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/appmek/AppMekJeiChemicalCompat.java @@ -0,0 +1,38 @@ +package com.sorrowmist.useless.compat.appmek; + +import com.sorrowmist.useless.compat.jei.GenericStackJeiIngredientProvider; +import mekanism.api.IMekanismAccess; +import mekanism.api.chemical.ChemicalStack; +import mekanism.api.integration.jei.IMekanismJEIHelper; +import mekanism.client.jei.ChemicalStackRenderer; +import mezz.jei.api.ingredients.IIngredientType; +import me.ramidzkh.mekae2.ae2.MekanismKey; + +/** Mekanism 10.4 has four distinct chemical JEI types, not 1.21's unified ChemicalStack. */ +public final class AppMekJeiChemicalCompat { + private AppMekJeiChemicalCompat() {} + public static GenericStackJeiIngredientProvider createProvider() { + return createProvider(IMekanismAccess.INSTANCE.jeiHelper()); + } + static GenericStackJeiIngredientProvider createProvider(IMekanismJEIHelper helpers) { + var gas = helpers.getGasStackHelper().getIngredientType(); + var infusion = helpers.getInfusionStackHelper().getIngredientType(); + var pigment = helpers.getPigmentStackHelper().getIngredientType(); + var slurry = helpers.getSlurryStackHelper().getIngredientType(); + return stack -> { + if (!(stack.what() instanceof MekanismKey key) || stack.amount() <= 0) return null; + ChemicalStack chemical = key.withAmount(stack.amount()); + if (chemical.isEmpty()) return null; + IIngredientType type = switch (key.getForm()) { + case MekanismKey.GAS -> gas; + case MekanismKey.INFUSION -> infusion; + case MekanismKey.PIGMENT -> pigment; + case MekanismKey.SLURRY -> slurry; + default -> null; + }; + if (type == null || !type.getIngredientClass().isInstance(chemical)) return null; + return new GenericStackJeiIngredientProvider.Ingredient(type, chemical, + new ChemicalStackRenderer<>(chemical.getAmount(), 16, 16)); + }; + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/appmek/AppMekRecipeCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/appmek/AppMekRecipeCompatLoader.java new file mode 100644 index 00000000..222c38c1 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/appmek/AppMekRecipeCompatLoader.java @@ -0,0 +1,54 @@ +package com.sorrowmist.useless.compat.appmek; + +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.BoilerRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.ChemicalChemicalRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.ChemicalCrystallizerRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.ChemicalDissolutionRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.ChemicalInjectionChamberRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.ChemicalToChemicalRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.ElectrolysisRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.FluidChemicalRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.ItemToChemicalRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.MetallurgicInfuserRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.NucleosynthesizingRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.PaintingRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.PressurizedReactionRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.PurificationChamberRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.RotaryRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.OsmiumCompressorRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.SpsRecipeAdapter; + +/** Registers adapters whose output or input is represented by AppMek's MekanismKey. */ +public final class AppMekRecipeCompatLoader { + private AppMekRecipeCompatLoader() { + } + + public static void register() { + AppMekChemicalCompat.registerKeyProvider(); + AlloyFurnaceRecipeManager manager = AlloyFurnaceRecipeManager.getInstance(); + + manager.registerAdapter(new MetallurgicInfuserRecipeAdapter(), RecipeSourceIds.APP_MEK); + manager.registerAdapter(new OsmiumCompressorRecipeAdapter(), RecipeSourceIds.APP_MEK); + manager.registerAdapter(new PurificationChamberRecipeAdapter(), RecipeSourceIds.APP_MEK); + manager.registerAdapter(new ChemicalInjectionChamberRecipeAdapter(), RecipeSourceIds.APP_MEK); + manager.registerAdapter(new PaintingRecipeAdapter(), RecipeSourceIds.APP_MEK); + manager.registerAdapter(ChemicalChemicalRecipeAdapter.chemicalInfuser(), RecipeSourceIds.APP_MEK); + manager.registerAdapter(ChemicalChemicalRecipeAdapter.pigmentMixer(), RecipeSourceIds.APP_MEK); + manager.registerAdapter(ChemicalToChemicalRecipeAdapter.isotopicCentrifuge(), RecipeSourceIds.APP_MEK); + manager.registerAdapter(ChemicalToChemicalRecipeAdapter.solarNeutronActivator(), RecipeSourceIds.APP_MEK); + manager.registerAdapter(new ElectrolysisRecipeAdapter(), RecipeSourceIds.APP_MEK); + manager.registerAdapter(new ChemicalDissolutionRecipeAdapter(), RecipeSourceIds.APP_MEK); + manager.registerAdapter(new ChemicalCrystallizerRecipeAdapter(), RecipeSourceIds.APP_MEK); + manager.registerAdapter(new FluidChemicalRecipeAdapter(), RecipeSourceIds.APP_MEK); + manager.registerAdapter(new PressurizedReactionRecipeAdapter(), RecipeSourceIds.APP_MEK); + manager.registerAdapter(new RotaryRecipeAdapter(), RecipeSourceIds.APP_MEK); + manager.registerAdapter(ItemToChemicalRecipeAdapter.chemicalConversion(), RecipeSourceIds.APP_MEK); + manager.registerAdapter(ItemToChemicalRecipeAdapter.oxidizing(), RecipeSourceIds.APP_MEK); + manager.registerAdapter(ItemToChemicalRecipeAdapter.pigmentExtracting(), RecipeSourceIds.APP_MEK); + manager.registerAdapter(new NucleosynthesizingRecipeAdapter(), RecipeSourceIds.APP_MEK); + manager.registerAdapter(new SpsRecipeAdapter(), RecipeSourceIds.APP_MEK); + manager.registerAdapter(new BoilerRecipeAdapter(), RecipeSourceIds.APP_MEK); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/ars/ArsCreatureTimeBridge.java b/src/main/java/com/sorrowmist/useless/compat/ars/ArsCreatureTimeBridge.java new file mode 100644 index 00000000..3f91b922 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ars/ArsCreatureTimeBridge.java @@ -0,0 +1,25 @@ +package com.sorrowmist.useless.compat.ars; + +import net.minecraft.world.entity.LivingEntity; + +/** + * 「生物加速」对第三方工作生物的桥。 + * + *

刻意不出现任何 Ars Nouveau 类型:{@code BeefTimeAcceleration} 只依赖这个接口, + * 真正引用 {@code EntityDrygmy} / {@code DrygmyTile} / {@code Alakarkinos} / {@code Whirlisprig} 的 + * 实现类由 {@link ArsCreatureTimeCompatLoader} 在检测到模组后才反射加载,因此没装 + * 新生魔艺的整合包不会触发类解析错误。

+ */ +public interface ArsCreatureTimeBridge { + + /** + * 推进目标的工作进度 / 冷却。 + * + *

与 {@code BeefTimeAcceleration#accelerateEntityTimers} 的原版分支同一语义: + * 只写数值字段,不跑 AI、不驱动状态机、不做寻路与移动。目标不是 Ars 工作生物时什么都不做。

+ * + * @param target 被加速的生物 + * @param extra 本 tick 要推进的「计时器」量(= {@code 1 << tickSpeed}) + */ + void accelerate(LivingEntity target, int extra); +} diff --git a/src/main/java/com/sorrowmist/useless/compat/ars/ArsCreatureTimeCompat.java b/src/main/java/com/sorrowmist/useless/compat/ars/ArsCreatureTimeCompat.java new file mode 100644 index 00000000..be20fb13 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ars/ArsCreatureTimeCompat.java @@ -0,0 +1,110 @@ +package com.sorrowmist.useless.compat.ars; + +import com.hollingsworth.arsnouveau.common.block.tile.DrygmyTile; +import com.hollingsworth.arsnouveau.common.block.tile.WhirlisprigTile; +import com.hollingsworth.arsnouveau.common.entity.Alakarkinos; +import com.hollingsworth.arsnouveau.common.entity.EntityDrygmy; +import com.hollingsworth.arsnouveau.common.entity.Whirlisprig; +import com.hollingsworth.arsnouveau.setup.config.Config; +import net.minecraft.world.entity.LivingEntity; + +/** + * {@link ArsCreatureTimeBridge} 的 Ars Nouveau 实现。 + * + *

本类是全模组唯一引用新生魔艺类型的地方({@code compat/ars} 里已有的 + * {@code ArsSourceCompat} 是另一处、面向魔源搬运)。只有 Ars Nouveau 加载时, + * {@link ArsCreatureTimeCompatLoader} 才会通过 {@code Class.forName} 把本类载入 JVM。

+ * + *

三只工作生物为什么各自推不同的东西

+ *
    + *
  • 德格米:工作进度不在实体上,而在「家」方块实体 {@link DrygmyTile} 的 + * {@code progress} 字段上。原版节奏是「引导 100 tick + 冷却 100 tick = 200 tick 才 +1 点, + * 默认 20 点约 4000 tick 产一批」,所以直接把 {@code progress} 推满即可。
  • + *
  • 探宝蟹:由状态机驱动,主节流是实体字段 {@code findBlockCooldown} + * (一轮成功转换后设 1200 tick,搜索失败设 100~200 tick),只压这个冷却。 + * 状态机内部的 {@code waitTicks} 与寻路刻意不动——驱动它们等于额外跑 AI。
  • + *
  • 旋风精灵:与德格米同构,进度在 {@link WhirlisprigTile} 的 {@code progress} 上, + * 但原版每次 {@code addProgress()} 加的是 {@code moodScore / 30}(林地心情), + * 所以按同一比例放大,而不是按 1 加。
  • + *
+ * + *

所有被读写的字段与 {@code updateBlock()} 都是 public,不需要任何 mixin / accessor。

+ */ +public final class ArsCreatureTimeCompat implements ArsCreatureTimeBridge { + + /** 旋风精灵原版 {@code addProgress()} 的换算基准(见 {@code WhirlisprigTile#addProgress})。 */ + private static final int WHIRLISPRIG_MOOD_DIVISOR = 30; + + @Override + public void accelerate(LivingEntity target, int extra) { + if (extra <= 0) { + return; + } + if (target instanceof EntityDrygmy drygmy) { + accelerateDrygmy(drygmy, extra); + } else if (target instanceof Alakarkinos alakarkinos) { + accelerateAlakarkinos(alakarkinos, extra); + } else if (target instanceof Whirlisprig whirlisprig) { + accelerateWhirlisprig(whirlisprig, extra); + } + } + + /** + * 德格米:把「家」方块实体上的进度推满。 + * + *

{@code getHome()} 自带「坐标处的方块实体是不是 DrygmyTile」校验,区块未加载或罐子被拆 + * 都会返回 {@code null},这里直接跳过。产出本身仍受原版 {@code gameTime % 100 == 0} 的窗口 + * 与每批 1000 魔源的限制,本方法只负责让进度不再是瓶颈。

+ */ + private void accelerateDrygmy(EntityDrygmy drygmy, int extra) { + DrygmyTile home = drygmy.getHome(); + if (home == null || home.isOff) { + return; + } + int max = home.getMaxProgress(); + if (home.progress >= max) { + return; + } + // 等价于循环调用 giveProgress(),但只发一次方块更新包(giveProgress 每次都 updateBlock)。 + home.progress = Math.min(max, home.progress + extra); + home.updateBlock(); + } + + /** + * 探宝蟹:只缩短「找方块」冷却。 + * + *

未绑定容器({@code getHome() == null})时状态机在 {@code DecideCrabActionState} 里直接空转, + * 压冷却不会有任何可观察效果,故跳过。冷却已经是 0 时也没什么可推的。

+ */ + private void accelerateAlakarkinos(Alakarkinos alakarkinos, int extra) { + if (alakarkinos.getHome() == null || alakarkinos.findBlockCooldown <= 0) { + return; + } + alakarkinos.findBlockCooldown = Math.max(0, alakarkinos.findBlockCooldown - extra); + } + + /** + * 旋风精灵:按原版 {@code moodScore / 30} 的同比例放大推进 {@link WhirlisprigTile} 的进度。 + * + *

{@code perEvent <= 0}(林地心情不足 30)时什么都不做 —— 与原版 {@code addProgress()} + * 每次加 0 的行为一致,这种林地本来就不产出,加速它没有意义。

+ */ + private void accelerateWhirlisprig(Whirlisprig whirlisprig, int extra) { + WhirlisprigTile tile = whirlisprig.getTile(); + if (tile == null) { + return; + } + int perEvent = tile.moodScore / WHIRLISPRIG_MOOD_DIVISOR; + if (perEvent <= 0) { + return; + } + int max = Config.WHIRLISPRIG_MAX_PROGRESS.get(); + if (tile.progress >= max) { + return; + } + // perEvent 与 extra 都可能很大(extra 上限 32768),用 long 累加后再夹取,避免 int 溢出。 + long advanced = (long) tile.progress + (long) perEvent * extra; + tile.progress = (int) Math.min(max, advanced); + tile.updateBlock(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/ars/ArsCreatureTimeCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/ars/ArsCreatureTimeCompatLoader.java new file mode 100644 index 00000000..a892cfbe --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ars/ArsCreatureTimeCompatLoader.java @@ -0,0 +1,67 @@ +package com.sorrowmist.useless.compat.ars; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.sorrowmist.useless.UselessMod; +import net.minecraft.world.entity.LivingEntity; +import net.minecraftforge.fml.ModList; + +/** + * 按需加载 {@link ArsCreatureTimeCompat}。 + * + *

与 {@link ArsSourceCompatLoader} 同一套手法:常驻类只引用 {@link ArsCreatureTimeBridge} + * 这个不含 Ars 类型的接口,实现类用 {@code Class.forName} 反射加载,从而在没装 + * Ars Nouveau 的整合包里也不会触发类解析错误。

+ */ +public final class ArsCreatureTimeCompatLoader { + public static final String MOD_ID = "ars_nouveau"; + + private static final String IMPLEMENTATION = + "com.sorrowmist.useless.compat.ars.ArsCreatureTimeCompat"; + + private static volatile boolean initialized; + private static volatile ArsCreatureTimeBridge bridge; + + private ArsCreatureTimeCompatLoader() { + } + + /** 生物加速桥;未加载 Ars Nouveau 或初始化失败时为 {@code null}。 */ + private static ArsCreatureTimeBridge bridge() { + if (!initialized) { + initialized = true; + if (ModList.get().isLoaded(MOD_ID)) { + try { + Class implementation = Class.forName( + IMPLEMENTATION, true, ArsCreatureTimeCompatLoader.class.getClassLoader()); + bridge = (ArsCreatureTimeBridge) implementation.getDeclaredConstructor().newInstance(); + } catch (ReflectiveOperationException | LinkageError exception) { + RateLimitedLogger.error(UselessMod.LOGGER, "Failed to initialise the Ars Nouveau creature time bridge", exception); + } + } + } + return bridge; + } + + /** + * 让 Ars Nouveau 的工作生物也吃到生物加速;未加载该模组时是空操作。 + * + *

本方法每 tick 每只被加速的生物都会调用一次,所以除了 {@code bridge()} 里的 + * {@code volatile} 读之外不做任何额外工作。

+ */ + public static void accelerate(LivingEntity target, int extra) { + ArsCreatureTimeBridge resolved = bridge(); + if (resolved == null) { + return; + } + try { + resolved.accelerate(target, extra); + } catch (LinkageError error) { + // 实现类里的字段引用是在首次执行时链接的,Class.forName 的 try/catch 覆盖不到。 + // 一旦 Ars 换了字段名(版本错配),这里会抛 NoSuchFieldError —— 每 tick 每只生物抛一次 + // 会刷爆日志,所以记一次就永久停用桥。 + bridge = null; + RateLimitedLogger.error(UselessMod.LOGGER, + "Disabling the Ars Nouveau creature time bridge after a linkage failure", error); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/ars/ArsSourceCompat.java b/src/main/java/com/sorrowmist/useless/compat/ars/ArsSourceCompat.java new file mode 100644 index 00000000..7ff07426 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ars/ArsSourceCompat.java @@ -0,0 +1,92 @@ +package com.sorrowmist.useless.compat.ars; + +import com.hollingsworth.arsnouveau.api.source.ISourceTile; +import com.sorrowmist.useless.content.stafflink.SourceHandlerView; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import org.jetbrains.annotations.Nullable; + +/** + * {@link SourceBridge} 的 Ars Nouveau 实现。 + * + *

本类是全模组唯一引用 {@code ISourceTile} 的地方。只有 Ars Nouveau 加载时, + * {@link ArsSourceCompatLoader} 才会通过 {@code Class.forName} 把本类载入 JVM; + * 否则这里的方法签名永远不会被解析,缺少该模组的整合包不会因此启动失败。

+ * + *

数量沿用 int:{@code ISourceTile} 本身就是 int 契约,魔源的容量与传输速率离 + * {@link Integer#MAX_VALUE} 极远,包一层 long 只会多一次无意义的转换。

+ */ +public final class ArsSourceCompat implements SourceBridge { + + @Override + @Nullable + public SourceHandlerView sourceHandler(Level level, BlockPos pos) { + if (level == null || pos == null || !level.isLoaded(pos)) { + return null; + } + BlockEntity entity = level.getBlockEntity(pos); + return entity instanceof ISourceTile tile ? new TileEndpoint(tile) : null; + } + + private record TileEndpoint(ISourceTile tile) implements SourceHandlerView { + + @Override + public int amount() { + return Math.max(0, this.tile.getSource()); + } + + @Override + public int capacity() { + return Math.max(0, this.tile.getMaxSource()); + } + + @Override + public int extract(int amount, boolean simulate) { + if (amount <= 0 || !this.tile.canProvideSource()) { + return 0; + } + int available = amount(); + if (available <= 0) { + return 0; + } + int move = Math.min(Math.min(amount, available), rate()); + if (move <= 0) { + return 0; + } + if (!simulate) { + this.tile.removeSource(move); + } + return move; + } + + @Override + public int receive(int amount, boolean simulate) { + if (amount <= 0 || !this.tile.canAcceptSource()) { + return 0; + } + int space = capacity() - amount(); + if (space <= 0) { + return 0; + } + int move = Math.min(Math.min(amount, space), rate()); + if (move <= 0) { + return 0; + } + if (!simulate) { + this.tile.addSource(move); + } + return move; + } + + @Override + public int transferRate() { + return rate(); + } + + private int rate() { + int rate = this.tile.getTransferRate(); + return rate <= 0 ? Integer.MAX_VALUE : rate; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/ars/ArsSourceCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/ars/ArsSourceCompatLoader.java new file mode 100644 index 00000000..05784d27 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ars/ArsSourceCompatLoader.java @@ -0,0 +1,58 @@ +package com.sorrowmist.useless.compat.ars; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.stafflink.SourceHandlerView; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraftforge.fml.ModList; +import org.jetbrains.annotations.Nullable; + +/** + * 按需加载 {@link ArsSourceCompat}。 + * + *

与 {@code MekanismCompatLoader} 同一套手法:常驻类只引用 {@link SourceBridge} + * 这个不含 Ars 类型的接口,实现类用 {@code Class.forName} 反射加载,从而在没装 + * Ars Nouveau 的整合包里也不会触发类解析错误。

+ */ +public final class ArsSourceCompatLoader { + public static final String MOD_ID = "ars_nouveau"; + + private static final String IMPLEMENTATION = "com.sorrowmist.useless.compat.ars.ArsSourceCompat"; + + private static volatile boolean initialized; + private static volatile SourceBridge bridge; + + private ArsSourceCompatLoader() { + } + + /** 魔源搬运桥;未加载 Ars Nouveau 或初始化失败时为 {@code null}。 */ + @Nullable + public static SourceBridge bridge() { + if (!initialized) { + initialized = true; + if (ModList.get().isLoaded(MOD_ID)) { + try { + Class implementation = Class.forName( + IMPLEMENTATION, true, ArsSourceCompatLoader.class.getClassLoader()); + bridge = (SourceBridge) implementation.getDeclaredConstructor().newInstance(); + } catch (ReflectiveOperationException | LinkageError exception) { + RateLimitedLogger.error(UselessMod.LOGGER, "Failed to initialise the Ars Nouveau source bridge", exception); + } + } + } + return bridge; + } + + public static boolean isAvailable() { + return bridge() != null; + } + + /** 该坐标的方块魔源端点;不是魔源容器或未加载 Ars Nouveau 时返回 {@code null}。 */ + @Nullable + public static SourceHandlerView sourceHandler(Level level, BlockPos pos) { + SourceBridge resolved = bridge(); + return resolved == null ? null : resolved.sourceHandler(level, pos); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/ars/SourceBridge.java b/src/main/java/com/sorrowmist/useless/compat/ars/SourceBridge.java new file mode 100644 index 00000000..b0144a11 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ars/SourceBridge.java @@ -0,0 +1,28 @@ +package com.sorrowmist.useless.compat.ars; + +import com.sorrowmist.useless.content.stafflink.SourceHandlerView; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +/** + * 魔源(Ars Nouveau)搬运桥。 + * + *

刻意不出现任何 Ars Nouveau 类型:{@link com.sorrowmist.useless.content.stafflink.StaffLinkTargets} + * 只依赖这个接口,真正引用 {@code ISourceTile} 的实现类由 + * {@link ArsSourceCompatLoader} 在检测到模组后才反射加载。

+ * + *

交付的是端点而不是「从 A 搬到 B」这个动作:魔源既要能在方块容器之间搬, + * 也要能跟 ME 网络里的魔源(Ars Énergistique)互搬,而后者没有方块坐标。 + * 抽象成端点后,搬运侧对两种来源一视同仁。

+ */ +public interface SourceBridge { + + /** + * 把该坐标的方块魔源容器包成端点。 + * + * @return 该坐标不是魔源容器时返回 {@code null} + */ + @Nullable + SourceHandlerView sourceHandler(Level level, BlockPos pos); +} diff --git a/src/main/java/com/sorrowmist/useless/compat/barbequesdelight/BarbequesDelightRecipeCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/barbequesdelight/BarbequesDelightRecipeCompatLoader.java new file mode 100644 index 00000000..a51f7c5e --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/barbequesdelight/BarbequesDelightRecipeCompatLoader.java @@ -0,0 +1,19 @@ +package com.sorrowmist.useless.compat.barbequesdelight; + +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.barbequesdelight.GrillingRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.delight.barbequesdelight.SkeweringRecipeAdapter; + +/** Registers Barbeque's Delight recipe adapters after the optional mod is detected. */ +public final class BarbequesDelightRecipeCompatLoader { + private BarbequesDelightRecipeCompatLoader() { + } + + public static void register() { + AlloyFurnaceRecipeManager.getInstance().registerAdapter( + new GrillingRecipeAdapter(), RecipeSourceIds.BARBEQUES_DELIGHT); + AlloyFurnaceRecipeManager.getInstance().registerAdapter( + new SkeweringRecipeAdapter(), RecipeSourceIds.BARBEQUES_DELIGHT); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/bloodmagic/BloodMagicRecipeConverter.java b/src/main/java/com/sorrowmist/useless/compat/bloodmagic/BloodMagicRecipeConverter.java deleted file mode 100644 index aad266ad..00000000 --- a/src/main/java/com/sorrowmist/useless/compat/bloodmagic/BloodMagicRecipeConverter.java +++ /dev/null @@ -1,391 +0,0 @@ -package com.sorrowmist.useless.compat.bloodmagic; - -import com.sorrowmist.useless.recipes.advancedalloyfurnace.AdvancedAlloyFurnaceRecipe; -import com.sorrowmist.useless.registry.ModIngots; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.crafting.Ingredient; -import net.minecraft.world.item.crafting.RecipeManager; -import net.minecraftforge.fluids.FluidStack; -import net.minecraftforge.registries.ForgeRegistries; - -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -/** - * 血魔法配方转换器,用于将血魔法系列模组的配方转换为高级合金炉配方 - */ -public class BloodMagicRecipeConverter { - - /** - * 将血魔法血之祭坛(BloodAltar)配方转换为高级合金炉配方 - * - * @param recipeManager 游戏配方管理器 - * @return 转换后的高级合金炉配方列表 - */ - public static List convertBloodAltarRecipes(RecipeManager recipeManager) { - List convertedRecipes = new ArrayList<>(); - - try { - Class bloodMagicRecipeTypeClass = Class.forName("wayoftime.bloodmagic.common.recipe.BloodMagicRecipeType"); - Object altarRecipeType = bloodMagicRecipeTypeClass.getField("ALTAR").get(null); - - Class wrappedRegistryObjectClass = Class.forName("wayoftime.bloodmagic.common.registration.WrappedRegistryObject"); - java.lang.reflect.Method getMethod = wrappedRegistryObjectClass.getMethod("get"); - Object recipeType = getMethod.invoke(altarRecipeType); - - java.lang.reflect.Method getAllRecipesForMethod = RecipeManager.class.getMethod("getAllRecipesFor", net.minecraft.world.item.crafting.RecipeType.class); - Collection> allRecipes = (Collection>) getAllRecipesForMethod.invoke(recipeManager, recipeType); - - for (net.minecraft.world.item.crafting.Recipe recipe : allRecipes) { - Object bloodAltarRecipe = recipe; - - ResourceLocation originalId = recipe.getId(); - - // 使用getter方法获取配方数据 - java.lang.reflect.Method getInputMethod = recipe.getClass().getMethod("getInput"); - Ingredient input = (Ingredient) getInputMethod.invoke(bloodAltarRecipe); - - java.lang.reflect.Method getOutputMethod = recipe.getClass().getMethod("getOutput"); - ItemStack output = (ItemStack) getOutputMethod.invoke(bloodAltarRecipe); - - java.lang.reflect.Method getMinimumTierMethod = recipe.getClass().getMethod("getMinimumTier"); - int minimumTier = (int) getMinimumTierMethod.invoke(bloodAltarRecipe); - - java.lang.reflect.Method getSyphonMethod = recipe.getClass().getMethod("getSyphon"); - int syphon = (int) getSyphonMethod.invoke(bloodAltarRecipe); - - if (output == null || output.isEmpty()) { - continue; - } - - ResourceLocation newId = new ResourceLocation( - "useless_mod", - "bm_altar_" + originalId.getNamespace() + "_" + originalId.getPath() - ); - - List inputIngredients = new ArrayList<>(); - List inputItemCounts = new ArrayList<>(); - - // 合并相同的输入物品 - java.util.Map itemCountMap = new java.util.HashMap<>(); - - ItemStack[] inputItems = input.getItems(); - if (inputItems != null && inputItems.length > 0) { - ItemStack itemStack = inputItems[0]; - net.minecraft.world.item.Item item = itemStack.getItem(); - long count = itemStack.getCount(); - - itemCountMap.put(item, itemCountMap.getOrDefault(item, 0L) + count); - } - - // 构建合并后的输入列表 - for (java.util.Map.Entry entry : itemCountMap.entrySet()) { - net.minecraft.world.item.Item item = entry.getKey(); - long count = entry.getValue(); - inputIngredients.add(Ingredient.of(item)); - inputItemCounts.add(count * 64); - } - - List outputItems = new ArrayList<>(); - ItemStack output16 = output.copy(); - output16.setCount(output.getCount() * 64); - outputItems.add(output16); - - List inputFluids = new ArrayList<>(); - List outputFluids = new ArrayList<>(); - - int energy = syphon * 2; - int processTime = 40 ; - - Ingredient catalyst = Ingredient.of(ModIngots.USELESS_INGOT_TIER_1.get(), - ModIngots.USELESS_INGOT_TIER_2.get(), - ModIngots.USELESS_INGOT_TIER_3.get(), - ModIngots.USELESS_INGOT_TIER_4.get(), - ModIngots.USELESS_INGOT_TIER_5.get(), - ModIngots.USELESS_INGOT_TIER_6.get(), - ModIngots.USELESS_INGOT_TIER_7.get(), - ModIngots.USELESS_INGOT_TIER_8.get(), - ModIngots.USELESS_INGOT_TIER_9.get(), - ModIngots.USEFUL_INGOT.get()); - int catalystCount = 1; - - ResourceLocation bloodAltarId = ResourceLocation.fromNamespaceAndPath("bloodmagic", "altar"); - net.minecraft.world.item.Item bloodAltar = ForgeRegistries.ITEMS.getValue(bloodAltarId); - if (bloodAltar == null) { - throw new RuntimeException("Could not find item: " + bloodAltarId); - } - Ingredient mold = Ingredient.of(bloodAltar); - - AdvancedAlloyFurnaceRecipe convertedRecipe = new AdvancedAlloyFurnaceRecipe( - newId, - inputIngredients, - inputItemCounts, - inputFluids, - outputItems, - outputFluids, - energy, - processTime, - catalyst, - catalystCount, - mold, - false, - false - ); - - convertedRecipes.add(convertedRecipe); - } - } catch (Exception e) { - e.printStackTrace(); - System.err.println("Failed to convert blood altar recipes: " + e.getMessage()); - } - - return convertedRecipes; - } - - /** - * 将血魔法狱火熔炉(TartaricForge)配方转换为高级合金炉配方 - * - * @param recipeManager 游戏配方管理器 - * @return 转换后的高级合金炉配方列表 - */ - public static List convertTartaricForgeRecipes(RecipeManager recipeManager) { - List convertedRecipes = new ArrayList<>(); - - try { - Class bloodMagicRecipeTypeClass = Class.forName("wayoftime.bloodmagic.common.recipe.BloodMagicRecipeType"); - Object soulForgeRecipeType = bloodMagicRecipeTypeClass.getField("TARTARICFORGE").get(null); - - Class wrappedRegistryObjectClass = Class.forName("wayoftime.bloodmagic.common.registration.WrappedRegistryObject"); - java.lang.reflect.Method getMethod = wrappedRegistryObjectClass.getMethod("get"); - Object recipeType = getMethod.invoke(soulForgeRecipeType); - - java.lang.reflect.Method getAllRecipesForMethod = RecipeManager.class.getMethod("getAllRecipesFor", net.minecraft.world.item.crafting.RecipeType.class); - Collection> allRecipes = (Collection>) getAllRecipesForMethod.invoke(recipeManager, recipeType); - - for (net.minecraft.world.item.crafting.Recipe recipe : allRecipes) { - Object soulForgeRecipe = recipe; - - ResourceLocation originalId = recipe.getId(); - - // 使用getter方法获取配方数据 - java.lang.reflect.Method getInputMethod = recipe.getClass().getMethod("getInput"); - List inputIngredients = (List) getInputMethod.invoke(soulForgeRecipe); - - java.lang.reflect.Method getOutputMethod = recipe.getClass().getMethod("getOutput"); - ItemStack output = (ItemStack) getOutputMethod.invoke(soulForgeRecipe); - - if (output == null || output.isEmpty()) { - continue; - } - - ResourceLocation newId = new ResourceLocation( - "useless_mod", - "bm_soulforge_" + originalId.getNamespace() + "_" + originalId.getPath() - ); - - List convertedInputIngredients = new ArrayList<>(); - List inputItemCounts = new ArrayList<>(); - - // 合并相同的输入物品 - java.util.Map itemCountMap = new java.util.HashMap<>(); - - for (Ingredient ingredient : inputIngredients) { - ItemStack[] items = ingredient.getItems(); - if (items != null && items.length > 0) { - ItemStack itemStack = items[0]; - net.minecraft.world.item.Item item = itemStack.getItem(); - long count = itemStack.getCount(); - - itemCountMap.put(item, itemCountMap.getOrDefault(item, 0L) + count); - } - } - - // 构建合并后的输入列表 - for (java.util.Map.Entry entry : itemCountMap.entrySet()) { - net.minecraft.world.item.Item item = entry.getKey(); - long count = entry.getValue(); - convertedInputIngredients.add(Ingredient.of(item)); - inputItemCounts.add(count * 64); - } - - List outputItems = new ArrayList<>(); - ItemStack output16 = output.copy(); - output16.setCount(output.getCount() * 64); - outputItems.add(output16); - - List inputFluids = new ArrayList<>(); - List outputFluids = new ArrayList<>(); - - int energy = 2000; - int processTime = 40; - - Ingredient catalyst = Ingredient.of(ModIngots.USELESS_INGOT_TIER_1.get(), - ModIngots.USELESS_INGOT_TIER_2.get(), - ModIngots.USELESS_INGOT_TIER_3.get(), - ModIngots.USELESS_INGOT_TIER_4.get(), - ModIngots.USELESS_INGOT_TIER_5.get(), - ModIngots.USELESS_INGOT_TIER_6.get(), - ModIngots.USELESS_INGOT_TIER_7.get(), - ModIngots.USELESS_INGOT_TIER_8.get(), - ModIngots.USELESS_INGOT_TIER_9.get(), - ModIngots.USEFUL_INGOT.get()); - int catalystCount = 1; - - ResourceLocation soulForgeId = ResourceLocation.fromNamespaceAndPath("bloodmagic", "soulforge"); - net.minecraft.world.item.Item soulForge = ForgeRegistries.ITEMS.getValue(soulForgeId); - if (soulForge == null) { - throw new RuntimeException("Could not find item: " + soulForgeId); - } - Ingredient mold = Ingredient.of(soulForge); - - AdvancedAlloyFurnaceRecipe convertedRecipe = new AdvancedAlloyFurnaceRecipe( - newId, - convertedInputIngredients, - inputItemCounts, - inputFluids, - outputItems, - outputFluids, - energy, - processTime, - catalyst, - catalystCount, - mold, - false, - false - ); - - convertedRecipes.add(convertedRecipe); - } - } catch (Exception e) { - e.printStackTrace(); - System.err.println("Failed to convert soul forge recipes: " + e.getMessage()); - } - - return convertedRecipes; - } - - /** - * 将血魔法炼金术桌(AlchemyTable)配方转换为高级合金炉配方 - * - * @param recipeManager 游戏配方管理器 - * @return 转换后的高级合金炉配方列表 - */ - public static List convertAlchemyTableRecipes(RecipeManager recipeManager) { - List convertedRecipes = new ArrayList<>(); - - try { - Class bloodMagicRecipeTypeClass = Class.forName("wayoftime.bloodmagic.common.recipe.BloodMagicRecipeType"); - Object alchemyTableRecipeType = bloodMagicRecipeTypeClass.getField("ALCHEMYTABLE").get(null); - - Class wrappedRegistryObjectClass = Class.forName("wayoftime.bloodmagic.common.registration.WrappedRegistryObject"); - java.lang.reflect.Method getMethod = wrappedRegistryObjectClass.getMethod("get"); - Object recipeType = getMethod.invoke(alchemyTableRecipeType); - - java.lang.reflect.Method getAllRecipesForMethod = RecipeManager.class.getMethod("getAllRecipesFor", net.minecraft.world.item.crafting.RecipeType.class); - Collection> allRecipes = (Collection>) getAllRecipesForMethod.invoke(recipeManager, recipeType); - - for (net.minecraft.world.item.crafting.Recipe recipe : allRecipes) { - Object alchemyTableRecipe = recipe; - - ResourceLocation originalId = recipe.getId(); - - // 使用getter方法获取配方数据 - java.lang.reflect.Method getInputMethod = recipe.getClass().getMethod("getInput"); - List inputIngredients = (List) getInputMethod.invoke(alchemyTableRecipe); - - java.lang.reflect.Method getOutputMethod = recipe.getClass().getMethod("getOutput"); - ItemStack output = (ItemStack) getOutputMethod.invoke(alchemyTableRecipe); - - if (output == null || output.isEmpty()) { - continue; - } - - ResourceLocation newId = new ResourceLocation( - "useless_mod", - "bm_alchemytable_" + originalId.getNamespace() + "_" + originalId.getPath() - ); - - List convertedInputIngredients = new ArrayList<>(); - List inputItemCounts = new ArrayList<>(); - - // 合并相同的输入物品 - java.util.Map itemCountMap = new java.util.HashMap<>(); - - for (Ingredient ingredient : inputIngredients) { - ItemStack[] items = ingredient.getItems(); - if (items != null && items.length > 0) { - ItemStack itemStack = items[0]; - net.minecraft.world.item.Item item = itemStack.getItem(); - long count = itemStack.getCount(); - - itemCountMap.put(item, itemCountMap.getOrDefault(item, 0L) + count); - } - } - - // 构建合并后的输入列表 - for (java.util.Map.Entry entry : itemCountMap.entrySet()) { - net.minecraft.world.item.Item item = entry.getKey(); - long count = entry.getValue(); - convertedInputIngredients.add(Ingredient.of(item)); - inputItemCounts.add(count * 64); - } - - List outputItems = new ArrayList<>(); - ItemStack output16 = output.copy(); - output16.setCount(output.getCount() * 64); - outputItems.add(output16); - - List inputFluids = new ArrayList<>(); - List outputFluids = new ArrayList<>(); - - int energy = 2000; - int processTime = 40; - - Ingredient catalyst = Ingredient.of(ModIngots.USELESS_INGOT_TIER_1.get(), - ModIngots.USELESS_INGOT_TIER_2.get(), - ModIngots.USELESS_INGOT_TIER_3.get(), - ModIngots.USELESS_INGOT_TIER_4.get(), - ModIngots.USELESS_INGOT_TIER_5.get(), - ModIngots.USELESS_INGOT_TIER_6.get(), - ModIngots.USELESS_INGOT_TIER_7.get(), - ModIngots.USELESS_INGOT_TIER_8.get(), - ModIngots.USELESS_INGOT_TIER_9.get(), - ModIngots.USEFUL_INGOT.get()); - int catalystCount = 1; - - ResourceLocation alchemyTableId = ResourceLocation.fromNamespaceAndPath("bloodmagic", "alchemytable"); - net.minecraft.world.item.Item alchemyTable = ForgeRegistries.ITEMS.getValue(alchemyTableId); - if (alchemyTable == null) { - throw new RuntimeException("Could not find item: " + alchemyTableId); - } - Ingredient mold = Ingredient.of(alchemyTable); - - AdvancedAlloyFurnaceRecipe convertedRecipe = new AdvancedAlloyFurnaceRecipe( - newId, - convertedInputIngredients, - inputItemCounts, - inputFluids, - outputItems, - outputFluids, - energy, - processTime, - catalyst, - catalystCount, - mold, - false, - false - ); - - convertedRecipes.add(convertedRecipe); - } - } catch (Exception e) { - e.printStackTrace(); - System.err.println("Failed to convert alchemy table recipes: " + e.getMessage()); - } - - return convertedRecipes; - } -} diff --git a/src/main/java/com/sorrowmist/useless/compat/brewinandchewin/BrewinAndChewinRecipeCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/brewinandchewin/BrewinAndChewinRecipeCompatLoader.java new file mode 100644 index 00000000..85348096 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/brewinandchewin/BrewinAndChewinRecipeCompatLoader.java @@ -0,0 +1,19 @@ +package com.sorrowmist.useless.compat.brewinandchewin; + +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.brewinandchewin.KegFermentingRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.delight.brewinandchewin.KegPouringRecipeAdapter; + +/** Registers Brewin And Chewin's keg fermentation adapter when the optional mod is present. */ +public final class BrewinAndChewinRecipeCompatLoader { + private BrewinAndChewinRecipeCompatLoader() { + } + + public static void register() { + AlloyFurnaceRecipeManager.getInstance().registerAdapter( + new KegFermentingRecipeAdapter(), RecipeSourceIds.BREWIN_AND_CHEWIN); + AlloyFurnaceRecipeManager.getInstance().registerAdapter( + new KegPouringRecipeAdapter(), RecipeSourceIds.BREWIN_AND_CHEWIN); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/casualnessdelight/CasualnessDelightRecipeCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/casualnessdelight/CasualnessDelightRecipeCompatLoader.java new file mode 100644 index 00000000..f1322b7a --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/casualnessdelight/CasualnessDelightRecipeCompatLoader.java @@ -0,0 +1,16 @@ +package com.sorrowmist.useless.compat.casualnessdelight; + +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.casualnessdelight.DeepFryingRecipeAdapter; + +/** Registers Casualness Delight's deep-frying recipes when the optional mod is present. */ +public final class CasualnessDelightRecipeCompatLoader { + private CasualnessDelightRecipeCompatLoader() { + } + + public static void register() { + AlloyFurnaceRecipeManager.getInstance().registerAdapter( + new DeepFryingRecipeAdapter(), RecipeSourceIds.CASUALNESS_DELIGHT); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/constructionwand/ConstructionWandLogic.java b/src/main/java/com/sorrowmist/useless/compat/constructionwand/ConstructionWandLogic.java new file mode 100644 index 00000000..d3ce683f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/constructionwand/ConstructionWandLogic.java @@ -0,0 +1,661 @@ +package com.sorrowmist.useless.compat.constructionwand; + +import com.sorrowmist.useless.api.enums.tool.ConstructionWandCoreMode; +import com.sorrowmist.useless.compat.AE2Compat; +import com.sorrowmist.useless.compat.ftbchunks.BlockEditGuard; +import com.sorrowmist.useless.compat.ftbchunks.FtbChunksProtectionLoader; +import com.sorrowmist.useless.core.component.UComponents; +import com.sorrowmist.useless.core.config.ConfigManager; +import com.sorrowmist.useless.content.items.EndlessBeafItem; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; +import net.minecraftforge.event.entity.player.PlayerInteractEvent; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import appeng.api.config.Actionable; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** Independent construction-wand behavior for the beef tool. */ +public final class ConstructionWandLogic { + private static final int MAX_UNDO_OPERATIONS = 3; + public static final int MAX_PREVIEW_BLOCKS = 4096; + private static final Map> HISTORY = new ConcurrentHashMap<>(); + + private ConstructionWandLogic() {} + + /** + * 判定该位置是否受方块编辑保护(FTB Chunks)。 + * + *

未安装对应模组、保护判定不可用或玩家持有 bypass 权限时返回 {@code false}。

+ * + *

判定必须逐格进行:一次建造或破坏可能横跨多个区块,各区块的归属队伍与 + * 权限设置并不相同,只对起点判定一次会漏掉其余区块的保护。

+ */ + private static boolean isProtected(ServerPlayer player, BlockPos pos) { + BlockEditGuard guard = FtbChunksProtectionLoader.guard(); + return guard != null && !guard.isEditAllowed(player, pos); + } + + public static boolean isEnabled(ItemStack stack) { + return stack.getItem() instanceof EndlessBeafItem + && com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.ConstructionWandEnabledComponent.get(), false); + } + + public static List preview(ServerPlayer player, InteractionHand hand, BlockHitResult hit) { + return preview(player, hand, hit, false); + } + + public static List preview(ServerPlayer player, InteractionHand hand, + BlockHitResult hit, boolean air) { + ItemStack tool = player.getItemInHand(hand); + if (!isEnabled(tool)) return List.of(); + + ConstructionWandCoreMode core = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(tool, + UComponents.ConstructionWandCoreComponent.get(), ConstructionWandCoreMode.DEFAULT); + return switch (core) { + case DEFAULT -> air ? List.of() : previewBuild(player.serverLevel(), player, hit, tool); + case ANGEL -> air + ? previewAngelAir(player.serverLevel(), player, tool) + : previewAngel(player.serverLevel(), player, hit, tool); + case DESTRUCTION -> air ? List.of() : previewDestroy(player.serverLevel(), player, hit); + }; + } + + public static boolean handleRightClickBlock(PlayerInteractEvent.RightClickBlock event) { + if (!isEnabled(event.getItemStack())) return false; + + event.setCanceled(true); + event.setCancellationResult(InteractionResult.sidedSuccess(event.getLevel().isClientSide())); + if (!(event.getLevel() instanceof ServerLevel level) + || !(event.getEntity() instanceof ServerPlayer player)) { + return true; + } + + BlockHitResult hit = event.getHitVec(); + if (player.isShiftKeyDown()) { + undo(player, level, hit.getBlockPos()); + return true; + } + + ItemStack tool = event.getItemStack(); + ConstructionWandCoreMode core = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(tool, + UComponents.ConstructionWandCoreComponent.get(), ConstructionWandCoreMode.DEFAULT); + Operation operation = switch (core) { + case DEFAULT -> build(level, player, hit, tool); + case ANGEL -> angel(level, player, hit, false, tool); + case DESTRUCTION -> destroy(level, player, hit, tool); + }; + remember(player, operation); + return true; + } + + public static boolean handleRightClickItem(PlayerInteractEvent.RightClickItem event) { + if (!isEnabled(event.getItemStack())) return false; + + event.setCanceled(true); + if (!(event.getLevel() instanceof ServerLevel level) + || !(event.getEntity() instanceof ServerPlayer player)) { + return true; + } + + ItemStack tool = event.getItemStack(); + ConstructionWandCoreMode core = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(tool, + UComponents.ConstructionWandCoreComponent.get(), ConstructionWandCoreMode.DEFAULT); + if (core == ConstructionWandCoreMode.ANGEL) { + remember(player, angel(level, player, null, true, tool)); + } + return true; + } + + public static void clearPlayer(UUID playerId) { + HISTORY.remove(playerId); + } + + private static Operation build(ServerLevel level, ServerPlayer player, BlockHitResult hit, ItemStack tool) { + BlockState targetState = level.getBlockState(hit.getBlockPos()); + if (!(targetState.getBlock().asItem() instanceof BlockItem clickedItem)) return null; + BlockItem targetItem = placementTarget(player, clickedItem); + + int limit = ConfigManager.getBeefConstructionWandBuildLimit(); + Direction face = hit.getDirection(); + BlockPos[] offsets = planeOffsets(face); + Deque pending = new ArrayDeque<>(); + Set visited = new HashSet<>(); + List changed = new ArrayList<>(); + pending.add(hit.getBlockPos().relative(face)); + int attempts = 0; + int maxAttempts = Math.max(64, Math.min(200_000, limit * 16)); + + while (!pending.isEmpty() && changed.size() < limit && attempts++ < maxAttempts) { + if (!player.isCreative() && findSupply(player, tool, targetItem).stack.isEmpty()) break; + BlockPos pos = pending.removeFirst(); + if (!visited.add(pos)) continue; + + BlockPos supportPos = pos.relative(face.getOpposite()); + if (level.getBlockState(supportPos).getBlock() != targetState.getBlock()) continue; + // 受保护的格子不放置,也不作为扩散起点:建造链条在此中止。 + if (isProtected(player, pos)) continue; + if (!place(level, player, tool, pos, face, targetItem, changed)) continue; + if (changed.size() >= limit) break; + + for (BlockPos offset : offsets) { + pending.addLast(pos.offset(offset)); + } + } + return changed.isEmpty() ? null : new Operation(changed, tool.copy()); + } + + private static List previewBuild(ServerLevel level, ServerPlayer player, + BlockHitResult hit, ItemStack tool) { + BlockState targetState = level.getBlockState(hit.getBlockPos()); + if (!(targetState.getBlock().asItem() instanceof BlockItem clickedItem)) return List.of(); + BlockItem targetItem = placementTarget(player, clickedItem); + + int limit = previewLimit(level, player, tool, targetItem, ConfigManager.getBeefConstructionWandBuildLimit()); + if (limit <= 0) return List.of(); + Supply previewSupply = findSupply(player, tool, targetItem); + InteractionHand placementHand = previewSupply.hand; + ItemStack previewStack = previewSupply.stack.isEmpty() + ? new ItemStack(targetItem) + : previewSupply.stack.copyWithCount(1); + + Direction face = hit.getDirection(); + BlockPos[] offsets = planeOffsets(face); + Deque pending = new ArrayDeque<>(); + Set visited = new HashSet<>(); + List preview = new ArrayList<>(Math.min(limit, MAX_PREVIEW_BLOCKS)); + pending.add(hit.getBlockPos().relative(face)); + + int attempts = 0; + int maxAttempts = Math.max(64, Math.min(200_000, limit * 16)); + while (!pending.isEmpty() && preview.size() < limit && attempts++ < maxAttempts) { + BlockPos pos = pending.removeFirst(); + if (!visited.add(pos)) continue; + if (level.getBlockState(pos.relative(face.getOpposite())).getBlock() != targetState.getBlock()) { + continue; + } + // 预览必须与落地结果一致:受保护的格子不作为可放置位置显示。 + if (isProtected(player, pos)) continue; + if (getPlacementState(level, player, pos, face, targetItem, previewStack, placementHand) == null) { + continue; + } + + preview.add(pos.immutable()); + for (BlockPos offset : offsets) { + pending.addLast(pos.offset(offset)); + } + } + return List.copyOf(preview); + } + + private static Operation angel(ServerLevel level, ServerPlayer player, + BlockHitResult hit, boolean fromAir, ItemStack tool) { + BlockItem targetItem; + Direction face; + BlockPos start; + + if (fromAir) { + ItemStack offhand = player.getOffhandItem(); + if (!(offhand.getItem() instanceof BlockItem blockItem)) return null; + targetItem = blockItem; + face = Direction.UP; + start = angelAirStart(player); + } else { + BlockState supportingState = level.getBlockState(hit.getBlockPos()); + if (!(supportingState.getBlock().asItem() instanceof BlockItem blockItem)) return null; + targetItem = placementTarget(player, blockItem); + face = hit.getDirection(); + start = hit.getBlockPos().relative(face.getOpposite()); + } + + int limit = ConfigManager.getBeefConstructionWandAngelLimit(); + List changed = new ArrayList<>(); + Direction step = fromAir ? angelAirStep(player) : face.getOpposite(); + BlockPos pos = start; + for (int i = 0; i < limit; i++) { + // 天使模式沿直线推进:受保护的格子只跳过放置,不中断推进。 + if (!isProtected(player, pos) + && place(level, player, tool, pos, face, targetItem, changed)) { + if (changed.size() >= limit) break; + } + pos = pos.relative(step); + } + return changed.isEmpty() ? null : new Operation(changed, tool.copy()); + } + + private static List previewAngel(ServerLevel level, ServerPlayer player, + BlockHitResult hit, ItemStack tool) { + if (hit == null) return previewAngelAir(level, player, tool); + + BlockState supportingState = level.getBlockState(hit.getBlockPos()); + if (!(supportingState.getBlock().asItem() instanceof BlockItem clickedItem)) return List.of(); + BlockItem targetItem = placementTarget(player, clickedItem); + + int limit = previewLimit(level, player, tool, targetItem, ConfigManager.getBeefConstructionWandAngelLimit()); + if (limit <= 0) return List.of(); + Supply previewSupply = findSupply(player, tool, targetItem); + InteractionHand placementHand = previewSupply.hand; + ItemStack previewStack = previewSupply.stack.isEmpty() + ? new ItemStack(targetItem) + : previewSupply.stack.copyWithCount(1); + Direction face = hit.getDirection(); + Direction step = face.getOpposite(); + BlockPos pos = hit.getBlockPos().relative(step); + List preview = new ArrayList<>(Math.min(limit, MAX_PREVIEW_BLOCKS)); + for (int i = 0; i < limit && preview.size() < MAX_PREVIEW_BLOCKS; i++) { + if (!isProtected(player, pos) + && getPlacementState(level, player, pos, face, targetItem, + previewStack, placementHand) != null) { + preview.add(pos.immutable()); + } + pos = pos.relative(step); + } + return List.copyOf(preview); + } + + private static List previewAngelAir(ServerLevel level, ServerPlayer player, ItemStack tool) { + ItemStack offhand = player.getOffhandItem(); + if (!(offhand.getItem() instanceof BlockItem targetItem)) return List.of(); + + int limit = previewLimit(level, player, tool, targetItem, + ConfigManager.getBeefConstructionWandAngelLimit()); + if (limit <= 0) return List.of(); + Supply previewSupply = findSupply(player, tool, targetItem); + InteractionHand placementHand = previewSupply.hand; + ItemStack previewStack = previewSupply.stack.isEmpty() + ? offhand.copyWithCount(1) + : previewSupply.stack.copyWithCount(1); + + Direction face = Direction.UP; + Direction step = angelAirStep(player); + BlockPos pos = angelAirStart(player); + List preview = new ArrayList<>(Math.min(limit, MAX_PREVIEW_BLOCKS)); + for (int i = 0; i < limit && preview.size() < MAX_PREVIEW_BLOCKS; i++) { + if (!isProtected(player, pos) + && getPlacementState(level, player, pos, face, targetItem, + previewStack, placementHand) != null) { + preview.add(pos.immutable()); + } + pos = pos.relative(step); + } + return List.copyOf(preview); + } + + private static Operation destroy(ServerLevel level, ServerPlayer player, + BlockHitResult hit, ItemStack tool) { + BlockState target = level.getBlockState(hit.getBlockPos()); + if (target.isAir()) return null; + + int limit = ConfigManager.getBeefConstructionWandDestructionLimit(); + BlockPos[] offsets = planeOffsets(hit.getDirection()); + Deque pending = new ArrayDeque<>(); + Set visited = new HashSet<>(); + List changed = new ArrayList<>(); + pending.add(hit.getBlockPos()); + int attempts = 0; + // 8 邻域会访问更多非目标方块,给 BFS 一个与上限成比例的尝试预算,避免在开阔区域无界扩散。 + int maxAttempts = Math.max(64, Math.min(200_000, limit * 16)); + + while (!pending.isEmpty() && changed.size() < limit && attempts++ < maxAttempts) { + BlockPos pos = pending.removeFirst(); + if (!visited.add(pos)) continue; + + BlockState state = level.getBlockState(pos); + boolean destroyed = false; + // 受保护的格子不破坏,也不作为扩散起点:破坏链条在此中止。 + if (!state.isAir() && state.getBlock() == target.getBlock() && !isProtected(player, pos)) { + SavedBlock saved = SavedBlock.capture(level, pos, state, ItemStack.EMPTY, false); + if (level.destroyBlock(pos, false, player)) { + saved.afterState = level.getBlockState(pos); + changed.add(saved); + destroyed = true; + } + } + if (!destroyed) continue; + for (BlockPos offset : offsets) { + pending.addLast(pos.offset(offset)); + } + } + return changed.isEmpty() ? null : new Operation(changed, tool.copy()); + } + + private static List previewDestroy(ServerLevel level, ServerPlayer player, + BlockHitResult hit) { + BlockState target = level.getBlockState(hit.getBlockPos()); + if (target.isAir()) return List.of(); + + int limit = Math.min(ConfigManager.getBeefConstructionWandDestructionLimit(), MAX_PREVIEW_BLOCKS); + BlockPos[] offsets = planeOffsets(hit.getDirection()); + Deque pending = new ArrayDeque<>(); + Set visited = new HashSet<>(); + List preview = new ArrayList<>(limit); + pending.add(hit.getBlockPos()); + int attempts = 0; + int maxAttempts = Math.max(64, Math.min(200_000, limit * 16)); + + while (!pending.isEmpty() && preview.size() < limit && attempts++ < maxAttempts) { + BlockPos pos = pending.removeFirst(); + if (!visited.add(pos)) continue; + BlockState state = level.getBlockState(pos); + if (state.isAir() || state.getBlock() != target.getBlock()) continue; + if (isProtected(player, pos)) continue; + + preview.add(pos.immutable()); + for (BlockPos offset : offsets) { + pending.addLast(pos.offset(offset)); + } + } + return List.copyOf(preview); + } + + private static boolean place(ServerLevel level, ServerPlayer player, ItemStack tool, + BlockPos pos, + Direction face, BlockItem targetItem, List changed) { + BlockState existing = level.getBlockState(pos); + Supply supply = findSupply(player, tool, targetItem); + if (supply.stack.isEmpty() && !player.isCreative()) return false; + if (!existing.canBeReplaced()) return false; + + ItemStack placementStack = supply.stack.isEmpty() ? new ItemStack(targetItem) : supply.stack; + BlockState placed = getPlacementState(level, player, pos, face, targetItem, + placementStack, supply.hand); + if (placed == null) return false; + + // 先在消耗前留一份快照:消耗后源栈可能变成空栈,空栈上再 copyWithCount + // 会直接得到 ItemStack.EMPTY,物品自带的组件(NBT)就跟着丢了。 + ItemStack consumed = placementStack.copyWithCount(1); + + SavedBlock saved = SavedBlock.capture(level, pos, existing, + player.isCreative() ? ItemStack.EMPTY : consumed.copy(), supply.fromAe); + if (!level.setBlock(pos, placed, Block.UPDATE_ALL)) return false; + if (!player.isCreative() && supply.fromAe) { + long extracted = AE2Compat.tryExtractFromLinkedGrid( + tool, player, consumed, Actionable.MODULATE); + if (extracted != 1) { + saved.restore(level); + return false; + } + } else if (!player.isCreative()) { + placementStack.shrink(1); + } + applyPlacedItemData(level, player, pos, placed, consumed); + saved.afterState = level.getBlockState(pos); + changed.add(saved); + return true; + } + + /** + * 复刻 {@link BlockItem#place} 落地之后的收尾步骤,把物品自带的组件写回放下的方块: + * 方块状态组件、BLOCK_ENTITY_DATA(方块实体 NBT)、方块实体暴露的隐式组件,以及 setPlacedBy 回调。 + * 手杖只做 setBlock,少了这几步,任何靠 NBT/组件承载数据的方块 + * (箱子、潜影盒、已配置的机器、带外置存储引用的方块等)放下后都会变成默认的空方块。 + */ + private static void applyPlacedItemData(ServerLevel level, ServerPlayer player, BlockPos pos, + BlockState state, ItemStack stack) { + if (stack.isEmpty()) return; + + BlockState effective = state; + CompoundTag stateTag = stack.getTagElement("BlockStateTag"); + if (stateTag != null) { + for (String name : stateTag.getAllKeys()) { + var property = effective.getBlock().getStateDefinition().getProperty(name); + if (property != null) effective = applyStateProperty(effective, property, stateTag.getString(name)); + } + if (effective != state) level.setBlock(pos, effective, Block.UPDATE_CLIENTS); + } + BlockItem.updateCustomBlockEntityTag(level, player, pos, stack); + BlockEntity blockEntity = level.getBlockEntity(pos); + if (blockEntity != null) blockEntity.setChanged(); + effective.getBlock().setPlacedBy(level, pos, effective, player, stack); + } + + private static > BlockState applyStateProperty(BlockState state, + net.minecraft.world.level.block.state.properties.Property property,String value) { + return property.getValue(value).map(parsed -> state.setValue(property,parsed)).orElse(state); + } + + private static Supply findSupply(ServerPlayer player, ItemStack tool, BlockItem targetItem) { + // AE storage is consulted only while the "AE storage priority" toggle is enabled. + if (isAeStoragePriorityEnabled(tool)) { + Supply aeSupply = findAeSupply(player, tool, targetItem); + if (!aeSupply.stack.isEmpty()) { + return aeSupply; + } + } + + // Prefer non-offhand items: consume the main inventory before falling back to the offhand. + for (ItemStack stack : player.getInventory().items) { + if (stack.getItem() == targetItem && !stack.isEmpty()) { + return new Supply(stack, false, InteractionHand.MAIN_HAND); + } + } + + ItemStack offhand = player.getOffhandItem(); + if (offhand.getItem() == targetItem && !offhand.isEmpty()) { + return new Supply(offhand, false, InteractionHand.OFF_HAND); + } + + return emptySupply(); + } + + private static BlockState getPlacementState(ServerLevel level, ServerPlayer player, BlockPos pos, + Direction face, BlockItem targetItem, ItemStack stack, + InteractionHand hand) { + if (!level.getBlockState(pos).canBeReplaced()) return null; + BlockHitResult placeHit = new BlockHitResult( + Vec3.atCenterOf(pos), face, pos.relative(face.getOpposite()), false); + BlockPlaceContext context = new BlockPlaceContext( + level, player, hand, stack, placeHit); + BlockState placed = targetItem.getBlock().getStateForPlacement(context); + if (placed == null || !placed.canSurvive(level, pos) + || !level.isUnobstructed(placed, pos, + net.minecraft.world.phys.shapes.CollisionContext.of(player))) { + return null; + } + return placed; + } + + private static int previewLimit(ServerLevel level, ServerPlayer player, ItemStack tool, + BlockItem targetItem, int configuredLimit) { + int limit = Math.min(configuredLimit, MAX_PREVIEW_BLOCKS); + if (player.isCreative()) return limit; + + long available = countLocalSupply(player, targetItem); + if (isAeStoragePriorityEnabled(tool)) { + try { + available += AE2Compat.tryExtractFromLinkedGrid( + tool, player, new ItemStack(targetItem, limit), Actionable.SIMULATE); + } catch (Throwable ignored) { + } + } + return (int) Math.min(limit, available); + } + + private static long countLocalSupply(ServerPlayer player, BlockItem targetItem) { + long count = 0; + ItemStack offhand = player.getOffhandItem(); + if (offhand.getItem() == targetItem) count += offhand.getCount(); + for (ItemStack stack : player.getInventory().items) { + if (stack.getItem() == targetItem) count += stack.getCount(); + } + return count; + } + + private static Supply findAeSupply(ServerPlayer player, ItemStack tool, BlockItem targetItem) { + if (!isAeStoragePriorityEnabled(tool)) { + return emptySupply(); + } + + ItemStack requested = new ItemStack(targetItem); + try { + if (AE2Compat.tryExtractFromLinkedGrid(tool, player, requested, Actionable.SIMULATE) > 0) { + return new Supply(requested, true, InteractionHand.MAIN_HAND); + } + } catch (Throwable ignored) { + } + return emptySupply(); + } + + private static Supply emptySupply() { + return new Supply(ItemStack.EMPTY, false, InteractionHand.MAIN_HAND); + } + + private static BlockItem placementTarget(ServerPlayer player, BlockItem fallback) { + ItemStack offhand = player.getOffhandItem(); + return offhand.getItem() instanceof BlockItem blockItem ? blockItem : fallback; + } + + public static BlockPos angelAirStart(Player player) { + return BlockPos.containing(player.getEyePosition().add(player.getLookAngle().scale(2.0))); + } + + public static Direction angelAirStep(Player player) { + return player.getDirection(); + } + + private static boolean isAeStoragePriorityEnabled(ItemStack tool) { + return com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(tool, UComponents.AEStoragePriorityComponent.get(), false) + && com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.has(tool, UComponents.WIRELESS_LINK_TARGET.get()); + } + + /** + * 与 {@code face} 垂直的平面内、相对目标方块的 8 邻域偏移(4 个轴向 + 4 个对角)。 + * 只沿 4 个轴向扩展会漏掉仅通过角点相连的同类方块(斜向/L 形的平面), + * 所以这里把对角也算作相邻,建造、破坏及其预览共用同一套邻域。 + */ + private static BlockPos[] planeOffsets(Direction face) { + Direction[] axes = switch (face.getAxis()) { + case X -> new Direction[]{Direction.UP, Direction.NORTH}; + case Y -> new Direction[]{Direction.NORTH, Direction.EAST}; + case Z -> new Direction[]{Direction.UP, Direction.EAST}; + }; + Direction first = axes[0]; + Direction second = axes[1]; + List offsets = new ArrayList<>(8); + for (Direction axis : new Direction[]{first, first.getOpposite(), second, second.getOpposite()}) { + offsets.add(new BlockPos(axis.getStepX(), axis.getStepY(), axis.getStepZ())); + } + for (Direction a : new Direction[]{first, first.getOpposite()}) { + for (Direction b : new Direction[]{second, second.getOpposite()}) { + offsets.add(new BlockPos(a.getStepX() + b.getStepX(), + a.getStepY() + b.getStepY(), + a.getStepZ() + b.getStepZ())); + } + } + return offsets.toArray(BlockPos[]::new); + } + + private static void remember(ServerPlayer player, Operation operation) { + if (operation == null || operation.blocks.isEmpty()) return; + Deque operations = HISTORY.computeIfAbsent(player.getUUID(), ignored -> new ArrayDeque<>()); + operations.addLast(operation); + while (operations.size() > MAX_UNDO_OPERATIONS) operations.removeFirst(); + } + + private static void undo(ServerPlayer player, ServerLevel level, BlockPos clickedPos) { + Deque operations = HISTORY.get(player.getUUID()); + if (operations == null || operations.isEmpty()) return; + + Operation operation = operations.peekLast(); + if (operation.dimension != level.dimension() || !operation.contains(clickedPos)) return; + + boolean restored = false; + for (SavedBlock block : operation.blocks) { + if (!level.getBlockState(block.pos).equals(block.afterState)) continue; + block.restore(level); + if (!block.refund.isEmpty()) { + ItemStack refund = block.refund.copy(); + if (block.refundToAe) { + try { + long inserted = AE2Compat.tryInsertIntoLinkedGrid( + operation.tool, player, refund, Actionable.MODULATE); + refund.shrink((int) Math.min(Integer.MAX_VALUE, inserted)); + } catch (Throwable ignored) { + } + } + if (!refund.isEmpty()) { + player.getInventory().placeItemBackInInventory(refund); + } + } + restored = true; + } + if (restored) operations.removeLast(); + } + + private record Operation(List blocks, + ItemStack tool, + net.minecraft.resources.ResourceKey dimension) { + private Operation(List blocks, ItemStack tool) { + this(blocks, tool, blocks.get(0).dimension); + } + + private boolean contains(BlockPos pos) { + return blocks.stream().anyMatch(block -> block.pos.equals(pos)); + } + } + + private static final class SavedBlock { + private final BlockPos pos; + private final BlockState beforeState; + private final CompoundTag blockEntityData; + private final ItemStack refund; + private final boolean refundToAe; + private BlockState afterState; + private final net.minecraft.resources.ResourceKey dimension; + + private SavedBlock(BlockPos pos, BlockState beforeState, CompoundTag blockEntityData, + ItemStack refund, boolean refundToAe, + net.minecraft.resources.ResourceKey dimension) { + this.pos = pos.immutable(); + this.beforeState = beforeState; + this.blockEntityData = blockEntityData; + this.refund = refund; + this.refundToAe = refundToAe; + this.dimension = dimension; + } + + private static SavedBlock capture(ServerLevel level, BlockPos pos, BlockState state, + ItemStack refund, boolean refundToAe) { + BlockEntity blockEntity = level.getBlockEntity(pos); + CompoundTag data = blockEntity == null + ? null + : blockEntity.saveWithFullMetadata(); + return new SavedBlock(pos, state, data, refund, refundToAe, level.dimension()); + } + + private void restore(ServerLevel level) { + level.removeBlockEntity(pos); + level.setBlock(pos, beforeState, Block.UPDATE_ALL); + if (blockEntityData != null) { + BlockEntity restored = BlockEntity.loadStatic( + pos, beforeState, blockEntityData); + if (restored != null) level.setBlockEntity(restored); + } + } + } + + private record Supply(ItemStack stack, boolean fromAe, InteractionHand hand) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/crabbersdelight/CrabbersDelightRecipeCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/crabbersdelight/CrabbersDelightRecipeCompatLoader.java new file mode 100644 index 00000000..45b4729f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/crabbersdelight/CrabbersDelightRecipeCompatLoader.java @@ -0,0 +1,16 @@ +package com.sorrowmist.useless.compat.crabbersdelight; + +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.crabbersdelight.CrabTrapRecipeAdapter; + +/** Registers Crabbers Delight's data-driven crab-trap integration when the mod is present. */ +public final class CrabbersDelightRecipeCompatLoader { + private CrabbersDelightRecipeCompatLoader() { + } + + public static void register() { + AlloyFurnaceRecipeManager.getInstance().registerAdapter( + new CrabTrapRecipeAdapter(), RecipeSourceIds.CRABBERS_DELIGHT); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/create/CreateRecipeCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/create/CreateRecipeCompatLoader.java new file mode 100644 index 00000000..719cdfa8 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/create/CreateRecipeCompatLoader.java @@ -0,0 +1,24 @@ +package com.sorrowmist.useless.compat.create; + +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.content.recipe.adapters.create.CreateBlastingRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.create.CreateItemApplicationRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.create.CreateMechanicalCraftingRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.create.CreateProcessingRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.create.CreateSequencedAssemblyRecipeAdapter; + +/** Optional Create entrypoint. This class is only loaded when Create is present. */ +public final class CreateRecipeCompatLoader { + private CreateRecipeCompatLoader() { + } + + public static void register() { + AlloyFurnaceRecipeManager manager = AlloyFurnaceRecipeManager.getInstance(); + manager.registerAdapter(new CreateProcessingRecipeAdapter(), RecipeSourceIds.CREATE); + manager.registerAdapter(new CreateBlastingRecipeAdapter(), RecipeSourceIds.CREATE); + manager.registerAdapter(new CreateMechanicalCraftingRecipeAdapter(), RecipeSourceIds.CREATE); + manager.registerAdapter(new CreateSequencedAssemblyRecipeAdapter(), RecipeSourceIds.CREATE); + manager.registerAdapter(new CreateItemApplicationRecipeAdapter(), RecipeSourceIds.CREATE); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/create/CreateStressBridge.java b/src/main/java/com/sorrowmist/useless/compat/create/CreateStressBridge.java new file mode 100644 index 00000000..243279e1 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/create/CreateStressBridge.java @@ -0,0 +1,72 @@ +package com.sorrowmist.useless.compat.create; + +import com.sorrowmist.useless.content.menus.StaffLinkMenu; +import com.sorrowmist.useless.content.stafflink.StaffLinkRoute; +import com.sorrowmist.useless.content.stafflink.StaffLinkStressBridge; +import com.sorrowmist.useless.network.StaffLinkStressStatusPacket; +import com.sorrowmist.useless.world.stafflink.StaffLinkManager; +import net.minecraft.core.BlockPos; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.Level; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * {@link StaffLinkStressBridge} 的实现。 + * + *

本类引用了动力学模组的类型,因此只能由 + * {@link CreateStressCompatLoader} 在确认模组存在之后反射加载。它自己不含任何逻辑, + * 只是把调用转给 {@link StaffLinkStressState},并把运行状态下发给正在看界面的玩家。

+ */ +public final class CreateStressBridge implements StaffLinkStressBridge { + + @Override + @Nullable + public Object resolveEndpoint(Level level, BlockPos pos) { + return StaffLinkStressState.INSTANCE.resolveEndpoint(level, pos); + } + + @Override + public void applyRoute(MinecraftServer server, UUID networkId, int routeIndex, + List releases, List absorbs) { + StaffLinkStressState.INSTANCE.applyRoute(server, networkId, routeIndex, releases, absorbs); + } + + @Override + public void sweep(MinecraftServer server) { + StaffLinkStressState.INSTANCE.sweep(server); + } + + /** + * 把状态发给「正开着无线物流界面」的玩家,且只发他自己有权编辑的网络。 + * + *

不做广播:这些数字只对正在配置的人有意义,别人既看不懂也不该看到别人的网络规模。 + * 即使一条都没有也要发一个空包——客户端靠它清掉已经失效的旧状态。

+ */ + @Override + public void syncStatus(MinecraftServer server) { + List all = StaffLinkStressState.INSTANCE.entries(); + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + if (!(player.containerMenu instanceof StaffLinkMenu)) { + continue; + } + List visible = new ArrayList<>(all.size()); + for (StaffLinkStressStatusPacket.Entry entry : all) { + if (StaffLinkManager.canAccess(server, player, entry.networkId())) { + visible.add(entry); + } + } + PacketDistributor.sendToPlayer(player, new StaffLinkStressStatusPacket(List.copyOf(visible))); + } + } + + @Override + public void clearRuntimeState() { + StaffLinkStressState.INSTANCE.clearRuntimeState(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/create/CreateStressCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/create/CreateStressCompatLoader.java new file mode 100644 index 00000000..50c7bdb2 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/create/CreateStressCompatLoader.java @@ -0,0 +1,100 @@ +package com.sorrowmist.useless.compat.create; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.stafflink.StaffLinkRoute; +import com.sorrowmist.useless.content.stafflink.StaffLinkStressBridge; +import net.minecraft.core.BlockPos; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.Level; +import net.minecraftforge.fml.ModList; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.UUID; + +/** + * 按需加载 {@link CreateStressBridge}。 + * + *

与仓库里其它可选集成同一套手法:常驻类只引用 {@link StaffLinkStressBridge} + * 这个不含外部模组类型的接口,实现类用 {@code Class.forName} 反射加载, + * 从而在没装对应模组的整合包里也不会触发类解析错误。

+ * + *

模组缺失时这里的所有方法都退化成安全空操作(返回 {@code null} / 什么都不做), + * 调用方不需要到处写判空。

+ */ +public final class CreateStressCompatLoader { + public static final String MOD_ID = "create"; + + private static final String IMPLEMENTATION = + "com.sorrowmist.useless.compat.create.CreateStressBridge"; + + private static volatile boolean initialized; + private static volatile StaffLinkStressBridge bridge; + + private CreateStressCompatLoader() { + } + + /** 应力搬运桥;未加载对应模组或初始化失败时为 {@code null}。 */ + @Nullable + public static StaffLinkStressBridge bridge() { + if (!initialized) { + initialized = true; + if (ModList.get().isLoaded(MOD_ID)) { + try { + Class implementation = Class.forName( + IMPLEMENTATION, true, CreateStressCompatLoader.class.getClassLoader()); + bridge = (StaffLinkStressBridge) implementation.getDeclaredConstructor().newInstance(); + } catch (ReflectiveOperationException | LinkageError exception) { + RateLimitedLogger.error(UselessMod.LOGGER, "Failed to initialise the kinetic stress bridge", exception); + } + } + } + return bridge; + } + + public static boolean isAvailable() { + return bridge() != null; + } + + /** + * 该坐标的应力端点;不是动力学方块或未加载对应模组时返回 {@code null}。 + * + *

返回值对调用方不透明,只用于判空。

+ */ + @Nullable + public static Object resolveEndpoint(Level level, BlockPos pos) { + StaffLinkStressBridge resolved = bridge(); + return resolved == null ? null : resolved.resolveEndpoint(level, pos); + } + + public static void applyRoute(MinecraftServer server, UUID networkId, int routeIndex, + List releases, List absorbs) { + StaffLinkStressBridge resolved = bridge(); + if (resolved != null) { + resolved.applyRoute(server, networkId, routeIndex, releases, absorbs); + } + } + + public static void sweep(MinecraftServer server) { + StaffLinkStressBridge resolved = bridge(); + if (resolved != null) { + resolved.sweep(server); + } + } + + public static void syncStatus(MinecraftServer server) { + StaffLinkStressBridge resolved = bridge(); + if (resolved != null) { + resolved.syncStatus(server); + } + } + + public static void clearRuntimeState() { + StaffLinkStressBridge resolved = bridge(); + if (resolved != null) { + resolved.clearRuntimeState(); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/create/StaffLinkStressState.java b/src/main/java/com/sorrowmist/useless/compat/create/StaffLinkStressState.java new file mode 100644 index 00000000..7c0b914a --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/create/StaffLinkStressState.java @@ -0,0 +1,970 @@ +package com.sorrowmist.useless.compat.create; + +import com.sorrowmist.useless.content.stafflink.StaffLinkRoute; +import com.sorrowmist.useless.network.StaffLinkStressStatusPacket; +import com.sorrowmist.useless.world.stafflink.StaffLinkManager; +import com.simibubi.create.content.kinetics.KineticNetwork; +import com.simibubi.create.content.kinetics.base.KineticBlockEntity; +import com.simibubi.create.infrastructure.config.AllConfigs; +import net.minecraft.core.BlockPos; +import net.minecraft.core.GlobalPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +/** + * 应力线路的运行时状态。 + * + *

全部是运行时状态,不存档。转速与方向这两样配置存在 {@link StaffLinkRoute} 里, + * 已经随无线物流的存档一起持久化了;这里保存的只是「本 tick 谁被驱动成了什么转速」 + * 「本 tick 给哪个网络补了多少容量」「源网络要承担多少负载」这类可以随时重算的东西。 + * 服务器重启后第一个 tick 就会重建,因此没必要多占一个存档键。

+ * + *

两个方向的数据流。

+ *
    + *
  • 向外:给输出端所在网络补上虚拟容量。动力学那边每次重算网络容量时 + * 都会来问 {@link #additionalCapacity(long)}。
  • + *
  • 向内:把目标网络的实际需求回馈成源网络的负载,见 + * {@link #additionalStress(long)}。这是「不凭空产生动力」的关键—— + * 源网络该扛多少就扛多少,扛不住就和目标一起停。
  • + *
+ * + *

所有跨 tick 的集合都在 {@link #beginTick} 里按 tick 号整体换新, + * 因此「某一轮的结果」不会残留到下一轮。

+ */ +public final class StaffLinkStressState { + + public static final StaffLinkStressState INSTANCE = new StaffLinkStressState(); + + /** 桥停手多久之后,输出端方块身上的虚拟转速就该被摘掉。 */ + private static final int CLAIM_EXPIRY_TICKS = 40; + /** 同一张网络的同类冲突提示最多多久提醒一次。 */ + private static final int CONFLICT_NOTICE_INTERVAL = 40; + /** + * 「源网络里有无限动力源」时使用的代用容量。 + * + *

不需要精确:只要大到任何真实负载都压不过它,效果就是「无限」。

+ */ + private static final float INFINITE_POWER = 4.2535293E37F; + /** 判定两个转速是否相等的容差。 */ + private static final float SPEED_EPSILON = 0.001F; + /** 视为「无限动力源」的方块。 */ + private static final ResourceLocation INFINITE_SOURCE_BLOCK = + new ResourceLocation("create", "creative_motor"); + + // ------------------------------------------------------------------ 状态码 + // 空串表示一切正常;其余都是给界面取翻译键用的固定标识。 + + private static final String STATE_OK = ""; + /** 输入端所在的网络根本没在转。 */ + private static final String STATE_IDLE = "idle"; + /** 输入端方块不在任何动力学网络里。 */ + private static final String STATE_NO_NETWORK = "no_network"; + /** 锚点上不是动力学方块(被换掉了 / 绑错了)。 */ + private static final String STATE_NOT_KINETIC = "not_kinetic"; + /** 这条线路上缺输入或缺输出。 */ + private static final String STATE_MISSING_IO = "missing_io"; + /** 源网络一点容量都给不出来。 */ + private static final String STATE_NO_POWER = "no_power"; + /** 源网络有富余,但按优先级轮到它时已经被别的输出分完了。 */ + private static final String STATE_NO_SHARE = "no_share"; + /** 目标网络已经有容量了(同一条线路上别的输出补进去的),这一个输出无需再注入。 */ + private static final String STATE_FED_ELSEWHERE = "fed_elsewhere"; + /** 目标网络自己就带着动力源,压根不需要外部补应力。 */ + private static final String STATE_SELF_FED = "self_fed"; + /** 目标网络已经有真实动力源。 */ + private static final String STATE_REAL_SOURCE = "real_source"; + /** 同一张目标网络已被另一条输出用别的转速驱动。 */ + private static final String STATE_SPEED_CONFLICT = "speed_conflict"; + /** 这个方块(或它所在网络)已经被另一条线路占用。 */ + private static final String STATE_SHARED = "shared"; + + // ------------------------------------------------------------------ 字段 + + /** 被桥驱动过的方块。 */ + private final Map claims = new HashMap<>(); + /** 本 tick 生效的虚拟容量:目标网络 → 贡献列表。 */ + private final Map> grants = new HashMap<>(); + /** 本 tick 回馈到源网络的负载:源网络 → 额外应力。 */ + private final Map inputStress = new HashMap<>(); + /** 本 tick 各目标网络被驱动的转速;用来发现「同一网络两个转速」的冲突。 */ + private final Map outputSpeed = new HashMap<>(); + /** 本 tick 碰过的网络对象(已经用本 tick 的贡献重算过)。 */ + private Map touched = new HashMap<>(); + /** + * 上一 tick 碰过的网络。 + * + *

这一 tick 若不再有虚拟贡献,就要在引擎跑完之后把它们重算一次,把多出来的容量退掉。

+ * + *

退容量不能在 tick 开头做。那时贡献是空的,网络会被判成过载(容量掉到真实值), + * 而动力学把「过载状态翻转」也算作一次转速变化:{@code getSpeed()} 从 0 变回来 → + * {@code onSpeedChanged} 里 {@code fromOrToZero} 成立 → {@code flickerTally += 5}。 + * 紧接着本 tick 又把贡献写回去、再翻转一次,于是每 tick +10、衰减只 -1, + * 十几 tick 后 flickerScore 越过阈值,传播逻辑就会 {@code destroyBlock} 把方块打掉。 + * (实测症状:成排的动力合成器变成掉落物 —— 它们的应力占用是 2.0/RPM,网络一定会过载。)

+ */ + private Map pendingRelease = new HashMap<>(); + /** 界面用:暂停原因(键 = 网络 + 线路 + 锚点)。 */ + private final Map pauseReasons = new HashMap<>(); + /** 界面用:源网络里有无限动力源的线路。 */ + private final Set infiniteSupply = new HashSet<>(); + /** 界面用:每个端点的状态。 */ + private final Map status = new HashMap<>(); + /** 冲突提示节流。 */ + private final Map conflictNotices = new HashMap<>(); + private long lastTick = Long.MIN_VALUE; + + private StaffLinkStressState() { + } + + // ------------------------------------------------------------------ 端点解析 + + /** + * 该坐标能不能当应力端点。 + * + *

判据只有一条:那一格是动力学方块。转速与应力容量都是它的固有属性, + * 不需要额外的能力注册。

+ * + * @return 不是动力学方块(或区块未加载)时返回 {@code null} + */ + @Nullable + public Object resolveEndpoint(Level level, BlockPos pos) { + if (level == null || pos == null || !level.isLoaded(pos)) { + return null; + } + return level.getBlockEntity(pos) instanceof KineticBlockEntity entity ? entity : null; + } + + // ------------------------------------------------------------------ 每 tick 的主流程 + + /** + * 跑一整条线路。 + * + *

顺序是刻意的:先判冲突、再驱动、最后才算容量与回馈。因为「需要多少应力」 + * 取决于目标网络里都有哪些机器,而那些机器只有在网络被驱动起来之后才会挂上去; + * 反过来先算容量就没得可算。

+ */ + public void applyRoute(MinecraftServer server, UUID networkId, int routeIndex, + List releases, List absorbs) { + long tick = server.getTickCount(); + beginTick(tick); + + String prefix = networkId + ":" + routeIndex + ":"; + pauseReasons.keySet().removeIf(key -> key.startsWith(prefix)); + infiniteSupply.removeIf(key -> key.startsWith(prefix)); + status.keySet().removeIf(key -> key.networkId().equals(networkId) && key.route() == routeIndex); + + Map inputStates = new HashMap<>(); + Map outputStates = new HashMap<>(); + + List inputs = new ArrayList<>(releases.size()); + for (StaffLinkRoute release : releases) { + KineticBlockEntity entity = kinetic(server, release.anchor()); + if (entity == null) { + inputStates.put(release.anchor(), STATE_NOT_KINETIC); + } else if (!entity.hasNetwork()) { + inputStates.put(release.anchor(), STATE_NO_NETWORK); + } else if (!hasAnySource(entity.getOrCreateNetwork())) { + // 判据是「网络里有没有动力源」,不是「现在转不转」。 + // + // 源网络自己过载时转速是 0,但它仍然是个合法的输入端:算出来的富余就是 0, + // 目标端自然拿不到容量而停 —— 这正是「源不够时两边一起停」。 + // 若按「转不转」判,源一过载输入端就被判成闲置 → 回馈被撤 → 源恢复 → + // 再被推过载,两边以 tick 为周期来回抖;而每次过载翻转都会给网络成员记一次 + // flicker,抖久了就会触发动力学那边的「方块变更太频繁」保护,把方块打掉。 + inputStates.put(release.anchor(), STATE_IDLE); + } else { + inputs.add(new InputEndpoint(entity, release)); + } + } + + List outputs = new ArrayList<>(absorbs.size()); + for (StaffLinkRoute absorb : absorbs) { + KineticBlockEntity entity = kinetic(server, absorb.anchor()); + if (entity == null) { + outputStates.put(absorb.anchor(), STATE_NOT_KINETIC); + continue; + } + float rpm = clampRpm(absorb.amount()); + boolean reverse = absorb.interval() == StaffLinkRoute.STRESS_COUNTER_CLOCKWISE; + outputs.add(new OutputEndpoint(entity, absorb, reverse ? -rpm : rpm)); + } + + // ---- 源网络各自的「提供 / 消耗 / 可用」 ---- + // + // 放在「有没有输出」的判定之前:这一步与输出无关,而只绑了源、还没绑目标时, + // 界面也应该看得到源网络的真实状况 —— 那正是玩家判断「怎么只有这么点」的依据。 + // + // 三个细节不能省: + // 1. 容量要减掉自己上一轮加进去的虚拟容量(那部分不是源网络自己产的); + // 2. 负载要减掉自己上一轮回馈的那一份(否则「可用」会被自己压小,看起来凭空少一块); + // 3. 可用量用「容量 − 负载」而不是「容量」:源网络自己带的机器也要吃应力, + // 只看容量等于把已经被占用的部分又借了一遍。 + Set infiniteNetworks = new HashSet<>(); + Map numbersByNetwork = new HashMap<>(); + Map inputLocal = new HashMap<>(); + float capacitySum = 0.0F; + float consumedSum = 0.0F; + float spare = 0.0F; + for (InputEndpoint input : inputs) { + KineticNetwork network = input.entity.getOrCreateNetwork(); + long id = networkIdOf(network, input.entity); + if (hasInfiniteSource(network)) { + infiniteNetworks.add(id); + infiniteSupply.add(prefix + input.route.anchor()); + } + float[] numbers = numbersByNetwork.get(id); + if (numbers == null) { + if (infiniteNetworks.contains(id)) { + numbers = new float[]{INFINITE_POWER, 0.0F, INFINITE_POWER}; + } else { + float capacity = Math.max(0.0F, network.calculateCapacity() - additionalCapacity(id)); + float reflected = inputStress.getOrDefault(id, 0.0F); + float consumed = Math.max(0.0F, network.calculateStress() - reflected); + numbers = new float[]{capacity, consumed, Math.max(0.0F, capacity - consumed)}; + } + numbersByNetwork.put(id, numbers); + // 同一张网络只累加一次:四个锚点接在同一根轴上的话,它是「一张 32768 的网络」, + // 不是「四张 8192 的网络」。 + capacitySum += numbers[0]; + consumedSum += numbers[1]; + spare += numbers[2]; + } + // 「自身」= 这一台所在那张动力网络的量,不是单块自己的。 + // + // 动力合成器这类机器是「一片共用一个网络」的:单块 2.0 × 256 = 512 没有意义, + // 玩家要的是「我这片装置一共吃多少」。源端同理 —— 看的是它所在网络能提供多少。 + // (同一张网络上绑了多个锚点时,它们显示的就是同一个数,这是刻意的。) + inputLocal.put(input.route.anchor(), new float[]{numbers[0], 0.0F, 0.0F}); + } + if (!infiniteNetworks.isEmpty()) { + spare = INFINITE_POWER; + capacitySum = INFINITE_POWER; + } + + // 诊断:把每个输入网络的真实构成打出来(每 20 tick 一行)。 + // + // Create 6.0.x 的应力口径和直觉不一样,出问题时光看汇总数根本定位不到: + // · `calculateStress()` 遍历 `members` 时**不跳过动力源**,而 `add()` 会把所有方块 + // (含源)都放进 `members`,取值是**加入时缓存的** `calculateStressApplied()`; + // · `calculateCapacity()` 用 `sources` 里缓存的容量 × **`getGeneratedSpeed()`**; + // · 两者都还要加上 `unloadedStress` / `unloadedCapacity`(区块未加载的成员残值)。 + // 所以「谁贡献了多少」必须逐块打出来才能对上账。 + + // 配置区状态行用整条线路汇总的三个数。 + // + // 玩家想知道的是「这条无线链路总共能借出多少」。四台风车轴承各自成网时,只看单个锚点 + // 会显示 8192,让人以为总量只有一台的量;汇总后才是 32768,与「每台 8192、共四台」对得上。 + // 汇总出来的「可用」也正是下面分配时用的那个数,于是界面和实际行为不会再互相矛盾。 + float[] routeNumbers = {capacitySum, consumedSum, spare}; + + Map outputNumbers = new HashMap<>(); + + if (inputs.isEmpty() || outputs.isEmpty()) { + releaseRoute(server, networkId, routeIndex); + for (OutputEndpoint output : outputs) { + outputStates.put(output.route.anchor(), STATE_MISSING_IO); + } + writeStatus(networkId, routeIndex, releases, absorbs, inputStates, outputStates, + inputLocal, routeNumbers, outputNumbers); + return; + } + + // ---- 逐个输出:先查冲突,没冲突才驱动 ---- + List drives = new ArrayList<>(outputs.size()); + List demands = new ArrayList<>(outputs.size()); + for (OutputEndpoint output : outputs) { + String reason = conflictAt(output.entity, networkId, routeIndex, + output.route.anchor(), output.signedSpeed); + if (!reason.isEmpty()) { + // 有冲突就不驱动,并把这个方块从认领表里摘掉。 + // 硬驱动会让动力学去「抢源」,而抢不过的一方会被直接破坏掉。 + releaseClaim(server, networkId, routeIndex, output.route.anchor()); + pauseReasons.put(prefix + output.route.anchor(), reason); + notifyConflict(server, networkId, reason, tick); + outputStates.put(output.route.anchor(), reason); + continue; + } + drive(output.entity, networkId, routeIndex, output.route.anchor(), output.signedSpeed, tick); + KineticNetwork network = output.entity.getOrCreateNetwork(); + long networkId2 = networkIdOf(network, output.entity); + outputSpeed.putIfAbsent(networkId2, output.signedSpeed); + + float impact = 0.0F; + for (Float value : network.members.values()) { + impact += Math.max(0.0F, value); + } + String key = output.route.anchor().toString(); + // 目标网络自己已有的容量(含本 tick 里先前已经喂给它的那一部分)。 + // 扣掉它才是「还需要我们补多少」—— 目标网络本来就有动力的话,我们不该再注入一份, + // 那等于白占源网络的额度(回馈会把它算成源网络的负载)。 + float ownCapacity = network.calculateCapacity(); + float totalPower = StressAllocationMath.power(impact, output.signedSpeed); + drives.add(new Drive(key, network, networkId2, output.route, output.signedSpeed)); + demands.add(new StressAllocationMath.OutputDemand( + key, + Math.max(0.0F, totalPower - ownCapacity), + totalPower, + output.route.weight(), + nearestInputDistance(inputs, output.route))); + } + + if (drives.isEmpty()) { + writeStatus(networkId, routeIndex, releases, absorbs, inputStates, outputStates, + inputLocal, routeNumbers, outputNumbers); + return; + } + + // ---- 按权重分配,并登记虚拟容量 ---- + Map allocated = new HashMap<>(); + for (StressAllocationMath.Allocation allocation : StressAllocationMath.allocate(demands, spare)) { + allocated.put(allocation.key(), allocation.power()); + } + Map demandByKey = new HashMap<>(); + for (StressAllocationMath.OutputDemand demand : demands) { + demandByKey.put(demand.key(), demand); + } + float granted = 0.0F; + for (Drive drive : drives) { + float power = allocated.getOrDefault(drive.key, 0.0F); + granted += power; + grants.computeIfAbsent(drive.networkId, id -> new ArrayList<>()) + .add(new StressGrant(power, drive.signedSpeed)); + touched.put(drive.networkId, drive.network); + StressAllocationMath.OutputDemand demand = demandByKey.get(drive.key); + float networkCapacity = drive.network.calculateCapacity(); + outputStates.put(drive.route.anchor(), power > 0.0F ? STATE_OK : idleOutputState( + spare, + demand == null ? 0.0F : demand.requestedPower(), + networkCapacity, + additionalCapacity(drive.networkId) > 0.0F)); + // 「自身」= 这一台所在那张动力网络总共需要多少应力(整片装置的耗力合计), + // 不是单块自己的 —— 见上面 inputLocal 处的说明。 + // 「网络总计 / 剩余」是整条无线线路的量,由 routeNumbers 统一给出,两处分工不同。 + outputNumbers.put(drive.route.anchor(), new float[]{ + demand == null ? 0.0F : demand.totalPower(), 0.0F, 0.0F}); + } + + // 「剩余」= 这条链路可借总额 − 已经借出去的。 + // 要等上面登记完才知道借出去多少,所以在这里回填(「网络总计」那一格不动)。 + routeNumbers[2] = Math.max(0.0F, spare - granted); + + // ---- 回馈:源网络承担它实际给出的那一份 ---- + // + // 刻意不是「目标需要多少就回馈多少」。回馈需求的话,目标的需求一旦超过源网络的富余, + // 源网络就会被推过载、转速归零,于是下一轮输入端被判成「没在转」、回馈又撤掉、 + // 源网络恢复、再被推过载……两边会以 tick 为周期来回抖。 + // 只回馈实际给出的量,源网络最多被推到「刚好满载」,状态是稳定的: + // 源不够时表现为目标端过载停转,而源网络自己的机器照常运行。 + float shared = granted / Math.max(1, inputs.size()); + for (InputEndpoint input : inputs) { + KineticNetwork network = input.entity.getOrCreateNetwork(); + long id = networkIdOf(network, input.entity); + inputStress.merge(id, shared, Float::sum); + touched.put(id, network); + // 源网络自己就没有富余(容量被它自己带的机器吃满)时如实报出来 —— + // 否则玩家只看到「已连接」和一个 0,不知道卡在哪一步。 + inputStates.put(input.route.anchor(), spare > 0.0F ? STATE_OK : STATE_NO_POWER); + } + + // ---- 让动力学按新的贡献重算容量与应力 ---- + for (Drive drive : drives) { + drive.network.updateNetwork(); + } + for (InputEndpoint input : inputs) { + input.entity.getOrCreateNetwork().updateNetwork(); + } + + writeStatus(networkId, routeIndex, releases, absorbs, inputStates, outputStates, + inputLocal, routeNumbers, outputNumbers); + } + + /** + * 摘掉已经过期的驱动认领。 + * + *

必须每 tick 调用。线路被关掉、被解绑、或者整张网络没了之后, + * {@link #applyRoute} 就不会再来续期,只能靠这里把方块身上的虚拟转速清掉。

+ */ + public void sweep(MinecraftServer server) { + releaseStaleContributions(server); + if (claims.isEmpty()) { + return; + } + long now = server.getTickCount(); + List expired = new ArrayList<>(); + Iterator> iterator = claims.entrySet().iterator(); + while (iterator.hasNext()) { + Claim claim = iterator.next().getValue(); + if (claim.stale(now)) { + iterator.remove(); + expired.add(claim); + } + } + for (Claim claim : expired) { + stopDriving(server, claim); + } + } + + /** + * 把「上一轮借出去、这一轮不再借」的虚拟容量退掉。 + * + *

放在 sweep(引擎跑完之后)而不是 tick 开头,是为了让本 tick 的新贡献先写好: + * 仍然被借用的网络不会被动到,只有真正不再有贡献的网络才翻转一次过载状态。

+ */ + private void releaseStaleContributions(MinecraftServer server) { + long now = server.getTickCount(); + // 本 tick 引擎压根没跑(应力线路被删光 / 网络不再活跃):上一轮的贡献还挂着, + // 一并退掉,否则那些网络会一直留着借来的容量。 + if (lastTick != now && !grants.isEmpty()) { + grants.clear(); + inputStress.clear(); + for (KineticNetwork network : touched.values()) { + if (network != null) { + network.updateNetwork(); + } + } + touched.clear(); + } + if (pendingRelease.isEmpty()) { + return; + } + for (Map.Entry entry : pendingRelease.entrySet()) { + if (grants.containsKey(entry.getKey())) { + // 这一轮还在借用:留给下一轮再判。 + continue; + } + KineticNetwork network = entry.getValue(); + if (network != null) { + network.updateNetwork(); + } + } + pendingRelease.clear(); + } + + // ------------------------------------------------------------------ 动力学侧的两个钩子 + + /** + * 补一个「临时认领」:方块上次退出前是无线驱动的,而认领表是运行时状态、重启后就没了。 + * + *

为什么必须补。认领丢了之后,这些方块在这一 tick 里会出现一种矛盾状态: + * 身上还留着上次同步过去的转速(方块实体自己的 {@code speed} 存在存档里), + * 但 {@code getGeneratedSpeed()} 因为查不到认领而返回 0 —— 于是动力学既不当它是动力源, + * 又看到它在转。随后 {@code validateKinetics()} 会把它的转速清零、依赖它的邻居会去 + * {@code propagateMissingSource},而传播逻辑把「同一张网络里转速不一致」判成环路, + * 直接 {@code destroyBlock} 把方块打掉。实测症状:**重进游戏后成排的动力合成器两侧变掉落物**。

+ * + *

补上之后它从第一 tick 起就仍然表现为动力源,网络状态与存档前一致;引擎随后会把它 + * 换成正式的认领。若这条线路已经不在了,临时认领会在 40 tick 后自然过期并被停掉。

+ */ + public void reseedClaim(KineticBlockEntity entity) { + Level level = entity.getLevel(); + if (level == null) { + return; + } + ClaimKey key = new ClaimKey(level.dimension().location(), entity.getBlockPos().asLong()); + if (claims.containsKey(key)) { + return; + } + MinecraftServer server = level.getServer(); + long now = server == null ? 0L : server.getTickCount(); + claims.put(key, new Claim(null, -1, null, level.dimension().location(), + entity.getBlockPos(), entity.getTheoreticalSpeed(), now, true)); + } + + /** 目标网络在本 tick 额外获得的容量。 */ + public float additionalCapacity(long networkId) { + List list = grants.get(networkId); + if (list == null) { + return 0.0F; + } + float total = 0.0F; + for (StressGrant grant : list) { + total += grant.suppliedCapacity(); + } + return total; + } + + /** 源网络在本 tick 额外承担的负载。 */ + public float additionalStress(long networkId) { + return inputStress.getOrDefault(networkId, 0.0F); + } + + /** + * 这个方块是不是被桥驱动着的(也就是「它的转速是无线来的」)。 + * + *

动力学每次询问方块的自身转速时都会走这里;返回非空就说明该方块应该以这个转速 + * 作为动力源参与网络,而不是按它方块类里写死的那个转速。

+ */ + public Optional wirelessSpeed(@Nullable KineticBlockEntity entity) { + if (entity == null) { + return Optional.empty(); + } + Level level = entity.getLevel(); + if (level == null) { + return Optional.empty(); + } + Claim claim = claims.get(new ClaimKey(level.dimension().location(), entity.getBlockPos().asLong())); + if (claim == null || claim.released) { + return Optional.empty(); + } + return Optional.of(claim.speed); + } + + // ------------------------------------------------------------------ 界面状态 + + /** 下发给客户端的运行状态快照。 */ + public List entries() { + return List.copyOf(status.values()); + } + + /** 服务器停止时清掉全部运行时状态。 */ + public void clearRuntimeState() { + claims.clear(); + grants.clear(); + inputStress.clear(); + outputSpeed.clear(); + touched = new HashMap<>(); + pendingRelease = new HashMap<>(); + pauseReasons.clear(); + infiniteSupply.clear(); + status.clear(); + conflictNotices.clear(); + lastTick = Long.MIN_VALUE; + } + + // ------------------------------------------------------------------ 内部:tick 边界 + + private void beginTick(long tick) { + if (lastTick == tick) { + return; + } + lastTick = tick; + // 只把集合换新,不在这里重算任何网络。原因见 pendingRelease 的注释: + // 此刻贡献是空的,重算会把网络判成过载,翻转出来的 flicker 会累积到把方块打掉。 + // 真正该退的容量交给 sweep():那时本 tick 的新贡献已经写好,仍被借用的网络不会被动到。 + pendingRelease = touched; + touched = new HashMap<>(); + grants.clear(); + inputStress.clear(); + outputSpeed.clear(); + } + + // ------------------------------------------------------------------ 内部:驱动与释放 + + private void drive(KineticBlockEntity entity, UUID networkId, int routeIndex, + GlobalPos anchor, float signedSpeed, long tick) { + Level level = entity.getLevel(); + if (level == null) { + return; + } + ClaimKey key = new ClaimKey(level.dimension().location(), entity.getBlockPos().asLong()); + Claim claim = claims.get(key); + if (claim == null || claim.restored) { + // 临时认领(重启后补的)没有真正的归属,第一次被驱动时直接换成正式的。 + claims.put(key, new Claim(networkId, routeIndex, anchor, level.dimension().location(), + entity.getBlockPos(), signedSpeed, tick)); + } else { + claim.speed = signedSpeed; + claim.lastSeen = tick; + claim.released = false; + } + + if (level.isClientSide) { + return; + } + float previous = entity.getTheoreticalSpeed(); + if (!entity.hasNetwork() || Math.abs(previous - signedSpeed) > SPEED_EPSILON) { + // 转速变了(或原本不在网络里):先脱离旧的传播关系,再按新转速重新接入。 + if (Math.abs(previous) > 1.0E-4F) { + entity.detachKinetics(); + } + entity.setSpeed(signedSpeed); + if (!entity.hasNetwork()) { + entity.setNetwork(entity.getBlockPos().asLong()); + } + entity.attachKinetics(); + entity.onSpeedChanged(previous); + entity.sendData(); + } + } + + private void releaseClaim(MinecraftServer server, UUID networkId, int routeIndex, GlobalPos anchor) { + Claim claim = null; + Iterator> iterator = claims.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + if (entry.getValue().owns(networkId, routeIndex, anchor)) { + claim = entry.getValue(); + iterator.remove(); + break; + } + } + if (claim != null) { + stopDriving(server, claim); + } + } + + private void releaseRoute(MinecraftServer server, UUID networkId, int routeIndex) { + List released = new ArrayList<>(); + Iterator> iterator = claims.entrySet().iterator(); + while (iterator.hasNext()) { + Claim claim = iterator.next().getValue(); + if (claim.ownsRoute(networkId, routeIndex)) { + iterator.remove(); + released.add(claim); + } + } + for (Claim claim : released) { + stopDriving(server, claim); + } + } + + private void stopDriving(MinecraftServer server, Claim claim) { + ServerLevel level = server.getLevel(ResourceKey.create(Registries.DIMENSION, claim.dimension)); + if (level == null || !level.isLoaded(claim.pos)) { + return; + } + if (level.getBlockEntity(claim.pos) instanceof KineticBlockEntity entity) { + stopDriving(level, entity); + } + } + + private void stopDriving(Level level, KineticBlockEntity entity) { + if (level.isClientSide) { + return; + } + if (entity.hasNetwork()) { + // 这张网络里如果还有别的动力源(真实源,或别的线路驱动着的方块), + // 就不该由我们把转速清零——那会把别人的动力一起掐掉。 + for (KineticBlockEntity source : entity.getOrCreateNetwork().sources.keySet()) { + if (source != entity && isPresent(source) + && !claims.containsKey(new ClaimKey( + source.getLevel().dimension().location(), source.getBlockPos().asLong()))) { + return; + } + } + } + float previous = entity.getTheoreticalSpeed(); + if (previous != 0.0F || entity.hasNetwork()) { + if (previous != 0.0F) { + entity.detachKinetics(); + } + entity.setSpeed(0.0F); + entity.setNetwork(null); + entity.onSpeedChanged(previous); + entity.sendData(); + } + } + + // ------------------------------------------------------------------ 内部:冲突判定 + + /** + * 这个输出端能不能驱动。 + * + *

三种情况必须让位,否则动力学那边会去「抢源」,抢不过的一方会被直接破坏掉:

+ *
    + *
  • 目标网络里已经有真实动力源(既不是无限动力源,也不是被桥驱动的方块);
  • + *
  • 同一张目标网络已经被另一条输出用不同转速驱动;
  • + *
  • 这个方块或它所在网络已经被别的线路占用。
  • + *
+ * + * @return 空串表示可以驱动,否则是状态码 + */ + private String conflictAt(KineticBlockEntity entity, UUID networkId, int routeIndex, + GlobalPos anchor, float signedSpeed) { + Level level = entity.getLevel(); + if (level == null) { + return STATE_NOT_KINETIC; + } + Claim own = claims.get(new ClaimKey(level.dimension().location(), entity.getBlockPos().asLong())); + if (own != null && !own.owns(networkId, routeIndex, anchor)) { + return STATE_SHARED; + } + if (!entity.hasNetwork()) { + return STATE_OK; + } + KineticNetwork network = entity.getOrCreateNetwork(); + for (KineticBlockEntity source : network.sources.keySet()) { + if (!isPresent(source)) { + continue; + } + Level sourceLevel = source.getLevel(); + if (sourceLevel == null) { + continue; + } + Claim claim = claims.get(new ClaimKey( + sourceLevel.dimension().location(), source.getBlockPos().asLong())); + if (claim == null) { + if (!isInfiniteSource(source)) { + return STATE_REAL_SOURCE; + } + } else if (!claim.owns(networkId, routeIndex, anchor)) { + return STATE_SHARED; + } + } + + Float claimed = outputSpeed.get(networkIdOf(network, entity)); + if (claimed == null) { + return STATE_OK; + } + return Math.abs(claimed - signedSpeed) > SPEED_EPSILON ? STATE_SPEED_CONFLICT : STATE_SHARED; + } + + private void notifyConflict(MinecraftServer server, UUID networkId, String reason, long tick) { + String key = networkId + ":" + reason; + Long previous = conflictNotices.get(key); + if (previous != null && tick - previous < CONFLICT_NOTICE_INTERVAL) { + return; + } + conflictNotices.put(key, tick); + String messageKey = switch (reason) { + case STATE_REAL_SOURCE -> "gui.useless_mod.wireless_logistics.stress.chat.real_source"; + case STATE_SPEED_CONFLICT -> "gui.useless_mod.wireless_logistics.stress.chat.speed_conflict"; + case STATE_SHARED -> "gui.useless_mod.wireless_logistics.stress.chat.shared"; + default -> "gui.useless_mod.wireless_logistics.stress.chat.conflict"; + }; + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + if (StaffLinkManager.canAccess(server, player, networkId)) { + player.displayClientMessage(Component.translatable(messageKey), true); + } + } + } + + // ------------------------------------------------------------------ 内部:工具 + + /** + * 写界面用的状态条目。 + * + * @param inputLocal 按锚点索引的「本锚点所在网络」三个数:提供 / 消耗 / 可用 + * @param routeNumbers 整条线路源侧汇总的三个数,同上 + * @param outputNumbers 按锚点索引的「注入 / 需求 / 容量」 + */ + private void writeStatus(UUID networkId, int routeIndex, + List releases, List absorbs, + Map inputStates, Map outputStates, + Map inputLocal, float[] routeNumbers, + Map outputNumbers) { + StaffLinkStressStatusPacket.Numbers route = numbers(routeNumbers); + for (StaffLinkRoute release : releases) { + String state = inputStates.get(release.anchor()); + status.put(new StatusKey(networkId, release.anchor(), routeIndex), + new StaffLinkStressStatusPacket.Entry(networkId, release.anchor(), routeIndex, true, + numbers(inputLocal.get(release.anchor())), route, + state == null ? STATE_OK : state)); + } + for (StaffLinkRoute absorb : absorbs) { + String state = outputStates.get(absorb.anchor()); + // 「网络总计 / 剩余」是整条线路的量,吸收端同样要看 —— 它回答的是 + // 「这条链路能给我多少、还剩多少」,与本机自己需要多少(local)是两回事。 + status.put(new StatusKey(networkId, absorb.anchor(), routeIndex), + new StaffLinkStressStatusPacket.Entry(networkId, absorb.anchor(), routeIndex, false, + numbers(outputNumbers.get(absorb.anchor())), route, + state == null ? STATE_OK : state)); + } + } + + private static StaffLinkStressStatusPacket.Numbers numbers(@Nullable float[] values) { + return values == null + ? StaffLinkStressStatusPacket.Numbers.ZERO + : new StaffLinkStressStatusPacket.Numbers(values[0], values[1], values[2]); + } + + /** + * 某个输出端这一份「没注入」时,给出真正的原因。 + * + *

判据是目标网络够不够用,而不是「我们这一份有没有给出去」。同一个网络上绑了多个 + * 输出时,只有第一个会拿到额度、后面的都是 0,但机器照样在转 —— 那时候报「故障」是错的。 + * 同理,额度也可能是同一条线路上别的输出补进去的,说成「别人供电」也不对。

+ * + * @param spare 源侧汇总出来的可用量 + * @param shortfall 这个输出端还需要补多少(扣掉目标网络自有容量之后的缺口) + * @param capacity 目标网络加总后的容量(含已经喂给它的那一部分) + * @param wirelessFed 目标网络当前的容量里有没有我们(任意一条线路)补进去的那一份 + */ + private static String idleOutputState(float spare, float shortfall, float capacity, boolean wirelessFed) { + if (shortfall <= 0.0F) { + // 目标网络已经够了,这一份压根不需要注入。 + if (capacity <= 0.0F) { + return STATE_OK; + } + return wirelessFed ? STATE_FED_ELSEWHERE : STATE_SELF_FED; + } + if (spare <= 0.0F) { + // 源网络自己就满了:容量被它自己带的机器吃干净,没有富余可以借出去。 + return STATE_NO_POWER; + } + return STATE_NO_SHARE; + } + + private static double nearestInputDistance(List inputs, StaffLinkRoute output) { + double best = Double.MAX_VALUE; + for (InputEndpoint input : inputs) { + if (!input.route.anchor().dimension().equals(output.anchor().dimension())) { + continue; + } + best = Math.min(best, input.route.anchor().pos().distSqr(output.anchor().pos())); + } + return best == Double.MAX_VALUE ? 0.0 : best; + } + + @Nullable + private static KineticBlockEntity kinetic(MinecraftServer server, GlobalPos anchor) { + ServerLevel level = server.getLevel(anchor.dimension()); + if (level == null || !level.isLoaded(anchor.pos())) { + return null; + } + return level.getBlockEntity(anchor.pos()) instanceof KineticBlockEntity entity ? entity : null; + } + + private static long networkIdOf(KineticNetwork network, KineticBlockEntity fallback) { + return network.id == null ? fallback.getBlockPos().asLong() : network.id; + } + + private static boolean isPresent(KineticBlockEntity entity) { + Level level = entity.getLevel(); + return level != null && level.getBlockEntity(entity.getBlockPos()) == entity; + } + + private static boolean isInfiniteSource(KineticBlockEntity entity) { + return INFINITE_SOURCE_BLOCK.equals(BuiltInRegistries.BLOCK.getKey(entity.getBlockState().getBlock())); + } + + private static boolean hasInfiniteSource(KineticNetwork network) { + for (KineticBlockEntity source : network.sources.keySet()) { + if (isPresent(source) && isInfiniteSource(source)) { + return true; + } + } + return false; + } + + /** + * 这张网络里有没有(还在世界里的)动力源。没有源就根本不会转,也就没东西可借。 + */ + private static boolean hasAnySource(KineticNetwork network) { + for (KineticBlockEntity source : network.sources.keySet()) { + if (isPresent(source)) { + return true; + } + } + return false; + } + + + /** 目标转速的夹取:至少 1 RPM,至多取动力学配置里的最高转速。 */ + private static float clampRpm(long amount) { + float max = maxRotationSpeed(); + return Math.max(1.0F, Math.min(max, (float) Math.abs(amount))); + } + + private static float maxRotationSpeed() { + try { + return Math.max(1.0F, AllConfigs.server().kinetics.maxRotationSpeed.get()); + } catch (RuntimeException | LinkageError ignored) { + // 配置还没读起来(或读不到)时给一个保守值,宁可夹紧也不能放出去一个离谱转速。 + return 256.0F; + } + } + + // ------------------------------------------------------------------ 内部:数据结构 + + /** 认领的键:维度 + 坐标。光用坐标会让不同维度的同一格互相顶掉。 */ + private record ClaimKey(ResourceLocation dimension, long pos) { + } + + private record StatusKey(UUID networkId, GlobalPos anchor, int route) { + } + + /** + * 给某个目标网络补的一份虚拟容量。 + * + * @param suppliedCapacity 这一份贡献多少应力容量 + * @param targetSpeed 该网络被驱动的转速(记录用,方便排查) + */ + private record StressGrant(float suppliedCapacity, float targetSpeed) { + } + + private record InputEndpoint(KineticBlockEntity entity, StaffLinkRoute route) { + } + + private record OutputEndpoint(KineticBlockEntity entity, StaffLinkRoute route, float signedSpeed) { + } + + private record Drive(String key, KineticNetwork network, long networkId, + StaffLinkRoute route, float signedSpeed) { + } + + /** 一处被桥驱动着的方块。 */ + private static final class Claim { + private final UUID networkId; + private final int routeIndex; + private final GlobalPos anchor; + private final ResourceLocation dimension; + private final BlockPos pos; + private float speed; + private long lastSeen; + private boolean released; + /** + * 「临时认领」:重启后按存档里的转速补出来的,还没有真正的线路归属。 + * + *

它不跟任何线路冲突({@link #owns} 恒真),也不会被解绑/释放逻辑摘掉 + * ({@link #ownsRoute} 恒假);引擎第一次驱动它时会被替换成正式认领, + * 若一直没人认领就会像普通认领一样过期、被停掉。

+ */ + private boolean restored; + + private Claim(UUID networkId, int routeIndex, GlobalPos anchor, ResourceLocation dimension, + BlockPos pos, float speed, long lastSeen) { + this(networkId, routeIndex, anchor, dimension, pos, speed, lastSeen, false); + } + + private Claim(UUID networkId, int routeIndex, GlobalPos anchor, ResourceLocation dimension, + BlockPos pos, float speed, long lastSeen, boolean restored) { + this.networkId = networkId; + this.routeIndex = routeIndex; + this.anchor = anchor; + this.dimension = dimension; + this.pos = pos; + this.speed = speed; + this.lastSeen = lastSeen; + this.restored = restored; + } + + private boolean ownsRoute(UUID networkId, int routeIndex) { + // 临时认领不属于任何线路:解绑/释放逻辑摘不到它,只能等它自己过期。 + if (restored) { + return false; + } + return this.networkId.equals(networkId) && this.routeIndex == routeIndex; + } + + private boolean owns(UUID networkId, int routeIndex, GlobalPos anchor) { + // 临时认领也不跟任何人冲突 —— 它只代表「这个方块上次是被无线驱动的」这个事实。 + if (restored) { + return true; + } + return ownsRoute(networkId, routeIndex) && this.anchor.equals(anchor); + } + + private boolean stale(long now) { + return released || now - lastSeen >= CLAIM_EXPIRY_TICKS; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/create/StressAllocationMath.java b/src/main/java/com/sorrowmist/useless/compat/create/StressAllocationMath.java new file mode 100644 index 00000000..ca6d4cb4 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/create/StressAllocationMath.java @@ -0,0 +1,91 @@ +package com.sorrowmist.useless.compat.create; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * 应力在多个输出端之间的分配。 + * + *

应力是「源网络能提供多少」与「目标网络需要多少」的比较,而目标可能不止一个。 + * 这里只做纯计算,不碰任何游戏对象,方便单独推敲。

+ */ +public final class StressAllocationMath { + + private StressAllocationMath() { + } + + /** + * 一个耗力部件在给定转速下需要多少应力。 + * + *

动力学里的耗力与转速成正比:方块给出的是「每 RPM 的耗力」,乘上转速才是实际占用。

+ */ + public static float power(float stressPerRpm, float rpm) { + return Math.max(0.0F, stressPerRpm) * Math.abs(rpm); + } + + /** + * 把可用应力分给各输出端。 + * + *

次序即优先级:先按权重降序,权重相同再按「离最近的输入端更近者优先」。 + * 按次序逐个满足——前一个拿够了才轮到下一个,因此高优先级的输出不会被低优先级的 + * 稀释。这与无线物流里「数量是每个输出各搬多少、不互相瓜分」的语义不同,是刻意的: + * 应力是有限的共享资源,而机器要的是「要么够、要么过载」,均分只会让所有机器一起降速。

+ * + *

可用量为 0(源网络停转或没有容量)时全部给 0:此时输出端应停在过载状态, + * 而不是拿一个凑合的量继续转。

+ * + * @param demands 各输出端的需求,键用于回填结果 + * @param available 源网络能提供的应力总量 + */ + public static List allocate(List demands, float available) { + if (demands.isEmpty()) { + return List.of(); + } + List ordered = new ArrayList<>(demands); + ordered.sort(Comparator.comparingInt(OutputDemand::priority) + .reversed() + .thenComparingDouble(OutputDemand::distanceSquared)); + + List result = new ArrayList<>(ordered.size()); + if (available <= 0.0F) { + for (OutputDemand demand : ordered) { + result.add(new Allocation(demand.key(), 0.0F)); + } + return result; + } + + float remaining = available; + for (OutputDemand demand : ordered) { + float need = Math.max(0.0F, demand.requestedPower()); + // 全给或一点不给。 + // + // Create 里一张网络的容量只要小于它的应力,整张网络就过载停转 —— 给一半并不能让它 + // 半速运行,那一半纯属白占:回馈会把「已经给出去的量」算成源网络的负载, + // 于是源网络少了一份额度、目标端却一点没转起来。所以宁可把这份留给后面能吃饱的输出。 + if (need <= remaining) { + result.add(new Allocation(demand.key(), need)); + remaining -= need; + } else { + result.add(new Allocation(demand.key(), 0.0F)); + } + } + return result; + } + + /** 一个输出端分到的应力。 */ + public record Allocation(String key, float power) { + } + + /** + * 一个输出端的需求。 + * + * @param requestedPower 它还需要我们补多少(已经扣掉目标网络自己已有的容量) + * @param totalPower 它在目标转速下总共需要多少,只用于界面显示 + * @param priority 权重,越大越优先 + * @param distanceSquared 到最近输入端的距离平方,仅作同权重时的次序依据 + */ + public record OutputDemand(String key, float requestedPower, float totalPower, + int priority, double distanceSquared) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/draconicevolution/DraconicOpStorageCompat.java b/src/main/java/com/sorrowmist/useless/compat/draconicevolution/DraconicOpStorageCompat.java new file mode 100644 index 00000000..f2f1adae --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/draconicevolution/DraconicOpStorageCompat.java @@ -0,0 +1,72 @@ +package com.sorrowmist.useless.compat.draconicevolution; + +import com.brandon3055.brandonscore.api.power.IOPStorage; +import com.brandon3055.brandonscore.capability.CapabilityOP; +import com.sorrowmist.useless.energy.IEnergyManager; +import com.sorrowmist.useless.init.ModBlockEntities; +import com.sorrowmist.useless.compat.forge1201.capability.RegisterCapabilitiesEvent; + +public final class DraconicOpStorageCompat { + private DraconicOpStorageCompat() { + } + + public static void registerCapabilities(RegisterCapabilitiesEvent event) { + event.registerBlockEntity( + CapabilityOP.OP, + ModBlockEntities.ADVANCED_ALLOY_FURNACE.get(), + (blockEntity, side) -> new FurnaceOpStorage(blockEntity.getEnergyManager()) + ); + event.registerBlockEntity( + CapabilityOP.OP, + ModBlockEntities.MULTIBLOCK_ALLOY_FURNACE_CORE.get(), + (blockEntity, side) -> new FurnaceOpStorage(blockEntity.getEnergyManager()) + ); + } + + private record FurnaceOpStorage(IEnergyManager energy) implements IOPStorage { + @Override + public long receiveOP(long maxReceive, boolean simulate) { + return this.energy.receiveEnergy(maxReceive, simulate); + } + + @Override + public long extractOP(long maxExtract, boolean simulate) { + return this.energy.extractEnergy(maxExtract, simulate); + } + + @Override + public long getOPStored() { + return this.energy.getEnergyStoredLong(); + } + + @Override + public long getMaxOPStored() { + return this.energy.getMaxEnergyStoredLong(); + } + + @Override + public long maxExtract() { + return this.energy.getMaxExtractLong(); + } + + @Override + public long maxReceive() { + return this.energy.getMaxReceiveLong(); + } + + @Override + public boolean canExtract() { + return this.energy.canExtract(); + } + + @Override + public boolean canReceive() { + return this.energy.canReceive(); + } + + @Override + public long modifyEnergyStored(long amount) { + return this.energy.modifyEnergyStored(amount); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/embers/EmbersRecipeConverter.java b/src/main/java/com/sorrowmist/useless/compat/embers/EmbersRecipeConverter.java deleted file mode 100644 index 542f7c5b..00000000 --- a/src/main/java/com/sorrowmist/useless/compat/embers/EmbersRecipeConverter.java +++ /dev/null @@ -1,733 +0,0 @@ -package com.sorrowmist.useless.compat.embers; - -import com.sorrowmist.useless.recipes.advancedalloyfurnace.AdvancedAlloyFurnaceRecipe; -import com.sorrowmist.useless.registry.ModIngots; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.crafting.Ingredient; -import net.minecraft.world.item.crafting.RecipeManager; -import net.minecraftforge.fluids.FluidStack; -import net.minecraftforge.registries.ForgeRegistries; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; - -/** - * 余烬mod配方转换器,用于将余烬系列模组的配方转换为高级合金炉配方 - */ -public class EmbersRecipeConverter { - - /** - * 将余烬炼金台(Alchemy Tablet)配方转换为高级合金炉配方 - * - * @param recipeManager 游戏配方管理器 - * @return 转换后的高级合金炉配方列表 - */ - public static List convertAlchemyTabletRecipes(RecipeManager recipeManager) { - List convertedRecipes = new ArrayList<>(); - - try { - Class iAlchemyRecipeClass = Class.forName("com.rekindled.embers.recipe.IAlchemyRecipe"); - Class registryManagerClass = Class.forName("com.rekindled.embers.RegistryManager"); - Field alchemyRecipeTypeField = registryManagerClass.getField("ALCHEMY"); - Object alchemyRecipeTypeObject = alchemyRecipeTypeField.get(null); - - // 获取配方类型的get方法 - Class registryObjectClass = Class.forName("net.minecraftforge.registries.RegistryObject"); - Method getMethod = registryObjectClass.getMethod("get"); - net.minecraft.world.item.crafting.RecipeType recipeType = (net.minecraft.world.item.crafting.RecipeType) getMethod.invoke(alchemyRecipeTypeObject); - - // 使用反射调用RecipeManager.getAllRecipesFor方法 - java.lang.reflect.Method getAllRecipesForMethod = RecipeManager.class.getMethod("getAllRecipesFor", net.minecraft.world.item.crafting.RecipeType.class); - Collection> allRecipes = (Collection>) getAllRecipesForMethod.invoke(recipeManager, recipeType); - - for (net.minecraft.world.item.crafting.Recipe recipe : allRecipes) { - if (iAlchemyRecipeClass.isInstance(recipe)) { - Object alchemyRecipe = recipe; - - ResourceLocation originalId = recipe.getId(); - - // 使用getter方法获取配方数据 - java.lang.reflect.Method getCenterInputMethod = alchemyRecipe.getClass().getMethod("getCenterInput"); - Ingredient tablet = (Ingredient) getCenterInputMethod.invoke(alchemyRecipe); - - java.lang.reflect.Method getInputsMethod = alchemyRecipe.getClass().getMethod("getInputs"); - List inputs = (List) getInputsMethod.invoke(alchemyRecipe); - - java.lang.reflect.Method getResultItemMethod = alchemyRecipe.getClass().getMethod("getResultItem"); - ItemStack output = (ItemStack) getResultItemMethod.invoke(alchemyRecipe); - - if (output == null || output.isEmpty()) { - continue; - } - - ResourceLocation newId = new ResourceLocation( - "useless_mod", - "embers_alchemy_" + originalId.getNamespace() + "_" + originalId.getPath() - ); - - List convertedInputIngredients = new ArrayList<>(); - List inputItemCounts = new ArrayList<>(); - - // 合并相同的输入物品 - java.util.Map itemCountMap = new java.util.HashMap<>(); - - // 添加中心物品 - ItemStack[] tabletItems = tablet.getItems(); - if (tabletItems != null && tabletItems.length > 0) { - ItemStack itemStack = tabletItems[0]; - net.minecraft.world.item.Item item = itemStack.getItem(); - long count = itemStack.getCount(); - itemCountMap.put(item, itemCountMap.getOrDefault(item, 0L) + count); - } - - // 添加其他输入物品 - for (Ingredient ingredient : inputs) { - ItemStack[] items = ingredient.getItems(); - if (items != null && items.length > 0) { - ItemStack itemStack = items[0]; - net.minecraft.world.item.Item item = itemStack.getItem(); - long count = itemStack.getCount(); - itemCountMap.put(item, itemCountMap.getOrDefault(item, 0L) + count); - } - } - - // 构建合并后的输入列表 - for (java.util.Map.Entry entry : itemCountMap.entrySet()) { - net.minecraft.world.item.Item item = entry.getKey(); - long count = entry.getValue(); - convertedInputIngredients.add(Ingredient.of(item)); - inputItemCounts.add(count * 64); - } - - List outputItems = new ArrayList<>(); - ItemStack output64 = output.copy(); - output64.setCount(output.getCount() * 64); - outputItems.add(output64); - - List inputFluids = new ArrayList<>(); - List outputFluids = new ArrayList<>(); - - int energy = 2000; - int processTime = 40; - - Ingredient catalyst = Ingredient.of(ModIngots.USELESS_INGOT_TIER_1.get(), - ModIngots.USELESS_INGOT_TIER_2.get(), - ModIngots.USELESS_INGOT_TIER_3.get(), - ModIngots.USELESS_INGOT_TIER_4.get(), - ModIngots.USELESS_INGOT_TIER_5.get(), - ModIngots.USELESS_INGOT_TIER_6.get(), - ModIngots.USELESS_INGOT_TIER_7.get(), - ModIngots.USELESS_INGOT_TIER_8.get(), - ModIngots.USELESS_INGOT_TIER_9.get(), - ModIngots.USEFUL_INGOT.get()); - int catalystCount = 1; - - ResourceLocation alchemyTabletId = ResourceLocation.fromNamespaceAndPath("embers", "alchemy_tablet"); - net.minecraft.world.item.Item alchemyTablet = ForgeRegistries.ITEMS.getValue(alchemyTabletId); - if (alchemyTablet == null) { - // 尝试使用物品形式的ID - alchemyTabletId = ResourceLocation.fromNamespaceAndPath("embers", "alchemy_tablet_item"); - alchemyTablet = ForgeRegistries.ITEMS.getValue(alchemyTabletId); - } - if (alchemyTablet == null) { - throw new RuntimeException("Could not find item: " + alchemyTabletId); - } - Ingredient mold = Ingredient.of(alchemyTablet); - - AdvancedAlloyFurnaceRecipe convertedRecipe = new AdvancedAlloyFurnaceRecipe( - newId, - convertedInputIngredients, - inputItemCounts, - inputFluids, - outputItems, - outputFluids, - energy, - processTime, - catalyst, - catalystCount, - mold, - false, - false - ); - - convertedRecipes.add(convertedRecipe); - } - } - } catch (Exception e) { - e.printStackTrace(); - System.err.println("Failed to convert embers alchemy recipes: " + e.getMessage()); - } - - return convertedRecipes; - } - - /** - * 将余烬地质分离器(Geologic Separator)配方转换为高级合金炉配方 - * - * @param recipeManager 游戏配方管理器 - * @return 转换后的高级合金炉配方列表 - */ - public static List convertGeoSeparatorRecipes(RecipeManager recipeManager) { - List convertedRecipes = new ArrayList<>(); - - try { - Class iMeltingRecipeClass = Class.forName("com.rekindled.embers.recipe.IMeltingRecipe"); - Class registryManagerClass = Class.forName("com.rekindled.embers.RegistryManager"); - Field meltingRecipeTypeField = registryManagerClass.getField("MELTING"); - Object meltingRecipeTypeObject = meltingRecipeTypeField.get(null); - - // 获取配方类型的get方法 - Class registryObjectClass = Class.forName("net.minecraftforge.registries.RegistryObject"); - Method getMethod = registryObjectClass.getMethod("get"); - net.minecraft.world.item.crafting.RecipeType recipeType = (net.minecraft.world.item.crafting.RecipeType) getMethod.invoke(meltingRecipeTypeObject); - - // 使用反射调用RecipeManager.getAllRecipesFor方法 - java.lang.reflect.Method getAllRecipesForMethod = RecipeManager.class.getMethod("getAllRecipesFor", net.minecraft.world.item.crafting.RecipeType.class); - Collection> allRecipes = (Collection>) getAllRecipesForMethod.invoke(recipeManager, recipeType); - - for (net.minecraft.world.item.crafting.Recipe recipe : allRecipes) { - if (iMeltingRecipeClass.isInstance(recipe)) { - Object meltingRecipe = recipe; - - ResourceLocation originalId = recipe.getId(); - - // 使用getter方法获取配方数据 - java.lang.reflect.Method getDisplayInputMethod = meltingRecipe.getClass().getMethod("getDisplayInput"); - Ingredient input = (Ingredient) getDisplayInputMethod.invoke(meltingRecipe); - - java.lang.reflect.Method getDisplayOutputMethod = meltingRecipe.getClass().getMethod("getDisplayOutput"); - FluidStack outputFluid = (FluidStack) getDisplayOutputMethod.invoke(meltingRecipe); - - // 获取bonus fluid - java.lang.reflect.Method getBonusMethod = meltingRecipe.getClass().getMethod("getBonus"); - FluidStack bonusFluid = (FluidStack) getBonusMethod.invoke(meltingRecipe); - - if (outputFluid == null || outputFluid.isEmpty()) { - continue; - } - - ResourceLocation newId = new ResourceLocation( - "useless_mod", - "embers_geo_separator_" + originalId.getNamespace() + "_" + originalId.getPath() - ); - - List inputIngredients = new ArrayList<>(); - List inputItemCounts = new ArrayList<>(); - - // 处理输入物品 - ItemStack[] items = input.getItems(); - if (items != null && items.length > 0) { - ItemStack itemStack = items[0]; - inputIngredients.add(Ingredient.of(itemStack.getItem())); - inputItemCounts.add(itemStack.getCount() * 64L); - } - - List outputItems = new ArrayList<>(); - List inputFluids = new ArrayList<>(); - List outputFluids = new ArrayList<>(); - outputFluids.add(new FluidStack(outputFluid.getFluid(), outputFluid.getAmount() * 64)); - - // 添加bonus fluid,如果存在且不为空 - if (bonusFluid != null && !bonusFluid.isEmpty()) { - outputFluids.add(new FluidStack(bonusFluid.getFluid(), bonusFluid.getAmount() * 64)); - } - - int energy = 2000; - int processTime = 40; - - Ingredient catalyst = Ingredient.of(ModIngots.USELESS_INGOT_TIER_1.get(), - ModIngots.USELESS_INGOT_TIER_2.get(), - ModIngots.USELESS_INGOT_TIER_3.get(), - ModIngots.USELESS_INGOT_TIER_4.get(), - ModIngots.USELESS_INGOT_TIER_5.get(), - ModIngots.USELESS_INGOT_TIER_6.get(), - ModIngots.USELESS_INGOT_TIER_7.get(), - ModIngots.USELESS_INGOT_TIER_8.get(), - ModIngots.USELESS_INGOT_TIER_9.get(), - ModIngots.USEFUL_INGOT.get()); - int catalystCount = 1; - - ResourceLocation geoSeparatorId = ResourceLocation.fromNamespaceAndPath("embers", "geologic_separator"); - net.minecraft.world.item.Item geoSeparator = ForgeRegistries.ITEMS.getValue(geoSeparatorId); - if (geoSeparator == null) { - // 尝试使用物品形式的ID - geoSeparatorId = ResourceLocation.fromNamespaceAndPath("embers", "geologic_separator_item"); - geoSeparator = ForgeRegistries.ITEMS.getValue(geoSeparatorId); - } - if (geoSeparator == null) { - throw new RuntimeException("Could not find item: " + geoSeparatorId); - } - Ingredient mold = Ingredient.of(geoSeparator); - - AdvancedAlloyFurnaceRecipe convertedRecipe = new AdvancedAlloyFurnaceRecipe( - newId, - inputIngredients, - inputItemCounts, - inputFluids, - outputItems, - outputFluids, - energy, - processTime, - catalyst, - catalystCount, - mold, - false, - false - ); - - convertedRecipes.add(convertedRecipe); - } - } - } catch (Exception e) { - e.printStackTrace(); - System.err.println("Failed to convert embers geo separator recipes: " + e.getMessage()); - } - - return convertedRecipes; - } - - /** - * 将余烬熔炼炉(Melter)配方转换为高级合金炉配方 - * 熔炼炉是地质分离器的基础方块,用于处理没有副产物的基础熔炼配方 - * - * @param recipeManager 游戏配方管理器 - * @return 转换后的高级合金炉配方列表 - */ - public static List convertMelterRecipes(RecipeManager recipeManager) { - List convertedRecipes = new ArrayList<>(); - - try { - Class iMeltingRecipeClass = Class.forName("com.rekindled.embers.recipe.IMeltingRecipe"); - Class registryManagerClass = Class.forName("com.rekindled.embers.RegistryManager"); - Field meltingRecipeTypeField = registryManagerClass.getField("MELTING"); - Object meltingRecipeTypeObject = meltingRecipeTypeField.get(null); - - // 获取配方类型的get方法 - Class registryObjectClass = Class.forName("net.minecraftforge.registries.RegistryObject"); - Method getMethod = registryObjectClass.getMethod("get"); - net.minecraft.world.item.crafting.RecipeType recipeType = (net.minecraft.world.item.crafting.RecipeType) getMethod.invoke(meltingRecipeTypeObject); - - // 使用反射调用RecipeManager.getAllRecipesFor方法 - java.lang.reflect.Method getAllRecipesForMethod = RecipeManager.class.getMethod("getAllRecipesFor", net.minecraft.world.item.crafting.RecipeType.class); - Collection> allRecipes = (Collection>) getAllRecipesForMethod.invoke(recipeManager, recipeType); - - for (net.minecraft.world.item.crafting.Recipe recipe : allRecipes) { - if (iMeltingRecipeClass.isInstance(recipe)) { - Object meltingRecipe = recipe; - - ResourceLocation originalId = recipe.getId(); - - // 使用getter方法获取配方数据 - java.lang.reflect.Method getDisplayInputMethod = meltingRecipe.getClass().getMethod("getDisplayInput"); - Ingredient input = (Ingredient) getDisplayInputMethod.invoke(meltingRecipe); - - java.lang.reflect.Method getDisplayOutputMethod = meltingRecipe.getClass().getMethod("getDisplayOutput"); - FluidStack outputFluid = (FluidStack) getDisplayOutputMethod.invoke(meltingRecipe); - - // 获取bonus fluid,用于判断是否为基础熔炼配方 - java.lang.reflect.Method getBonusMethod = meltingRecipe.getClass().getMethod("getBonus"); - FluidStack bonusFluid = (FluidStack) getBonusMethod.invoke(meltingRecipe); - - // 只有没有副产物的配方才由熔炼炉处理 - if (outputFluid != null && !outputFluid.isEmpty() && (bonusFluid == null || bonusFluid.isEmpty())) { - ResourceLocation newId = new ResourceLocation( - "useless_mod", - "embers_melter_" + originalId.getNamespace() + "_" + originalId.getPath() - ); - - List inputIngredients = new ArrayList<>(); - List inputItemCounts = new ArrayList<>(); - - // 处理输入物品 - ItemStack[] items = input.getItems(); - if (items != null && items.length > 0) { - ItemStack itemStack = items[0]; - inputIngredients.add(Ingredient.of(itemStack.getItem())); - inputItemCounts.add(itemStack.getCount() * 64L); - } - - List outputItems = new ArrayList<>(); - List inputFluids = new ArrayList<>(); - List outputFluids = new ArrayList<>(); - outputFluids.add(new FluidStack(outputFluid.getFluid(), outputFluid.getAmount() * 64)); - - int energy = 2000; - int processTime = 40; - - Ingredient catalyst = Ingredient.of(ModIngots.USELESS_INGOT_TIER_1.get(), - ModIngots.USELESS_INGOT_TIER_2.get(), - ModIngots.USELESS_INGOT_TIER_3.get(), - ModIngots.USELESS_INGOT_TIER_4.get(), - ModIngots.USELESS_INGOT_TIER_5.get(), - ModIngots.USELESS_INGOT_TIER_6.get(), - ModIngots.USELESS_INGOT_TIER_7.get(), - ModIngots.USELESS_INGOT_TIER_8.get(), - ModIngots.USELESS_INGOT_TIER_9.get(), - ModIngots.USEFUL_INGOT.get()); - int catalystCount = 1; - - // 尝试不同的熔炼炉物品ID,包括正确的melter - net.minecraft.world.item.Item melter = null; - ResourceLocation[] melterIds = { - ResourceLocation.fromNamespaceAndPath("embers", "melter"), - ResourceLocation.fromNamespaceAndPath("embers", "smelting_furnace") - }; - - for (ResourceLocation melterId : melterIds) { - melter = ForgeRegistries.ITEMS.getValue(melterId); - if (melter != null) { - break; - } - } - - if (melter == null) { - throw new RuntimeException("Could not find embers melter item"); - } - - Ingredient mold = Ingredient.of(melter); - - AdvancedAlloyFurnaceRecipe convertedRecipe = new AdvancedAlloyFurnaceRecipe( - newId, - inputIngredients, - inputItemCounts, - inputFluids, - outputItems, - outputFluids, - energy, - processTime, - catalyst, - catalystCount, - mold, - false, - false - ); - - convertedRecipes.add(convertedRecipe); - } - } - } - } catch (Exception e) { - e.printStackTrace(); - System.err.println("Failed to convert embers melter recipes: " + e.getMessage()); - } - - return convertedRecipes; - } - - /** - * 将余烬压印锤(Stamper)配方转换为高级合金炉配方 - * 压印锤配方的模具(stamp)将作为转化后配方的模具(mold) - * - * @param recipeManager 游戏配方管理器 - * @return 转换后的高级合金炉配方列表 - */ - public static List convertStampingRecipes(RecipeManager recipeManager) { - List convertedRecipes = new ArrayList<>(); - - try { - // 加载必要的类 - Class iStampingRecipeClass = Class.forName("com.rekindled.embers.recipe.IStampingRecipe"); - Class registryManagerClass = Class.forName("com.rekindled.embers.RegistryManager"); - Class fluidIngredientClass = Class.forName("com.rekindled.embers.recipe.FluidIngredient"); - - // 获取STAMPING RecipeType - Field stampingRecipeTypeField = registryManagerClass.getField("STAMPING"); - Object stampingRecipeTypeObject = stampingRecipeTypeField.get(null); - - // 获取配方类型的get方法 - Class registryObjectClass = Class.forName("net.minecraftforge.registries.RegistryObject"); - Method getMethod = registryObjectClass.getMethod("get"); - net.minecraft.world.item.crafting.RecipeType recipeType = (net.minecraft.world.item.crafting.RecipeType) getMethod.invoke(stampingRecipeTypeObject); - - // 使用反射调用RecipeManager.getAllRecipesFor方法 - java.lang.reflect.Method getAllRecipesForMethod = RecipeManager.class.getMethod("getAllRecipesFor", net.minecraft.world.item.crafting.RecipeType.class); - Collection> allRecipes = (Collection>) getAllRecipesForMethod.invoke(recipeManager, recipeType); - - for (net.minecraft.world.item.crafting.Recipe recipe : allRecipes) { - if (iStampingRecipeClass.isInstance(recipe)) { - Object stampingRecipe = recipe; - - ResourceLocation originalId = recipe.getId(); - - // 使用getter方法获取配方数据 - java.lang.reflect.Method getDisplayInputMethod = stampingRecipe.getClass().getMethod("getDisplayInput"); - Ingredient input = (Ingredient) getDisplayInputMethod.invoke(stampingRecipe); - - java.lang.reflect.Method getDisplayInputFluidMethod = stampingRecipe.getClass().getMethod("getDisplayInputFluid"); - Object fluidIngredient = getDisplayInputFluidMethod.invoke(stampingRecipe); - - java.lang.reflect.Method getDisplayStampMethod = stampingRecipe.getClass().getMethod("getDisplayStamp"); - Ingredient stamp = (Ingredient) getDisplayStampMethod.invoke(stampingRecipe); - - java.lang.reflect.Method getResultItemMethod = stampingRecipe.getClass().getMethod("getResultItem"); - ItemStack output = (ItemStack) getResultItemMethod.invoke(stampingRecipe); - - if (output == null || output.isEmpty()) { - continue; - } - - ResourceLocation newId = new ResourceLocation( - "useless_mod", - "embers_stamping_" + originalId.getNamespace() + "_" + originalId.getPath() - ); - - List inputIngredients = new ArrayList<>(); - List inputItemCounts = new ArrayList<>(); - List inputFluids = new ArrayList<>(); - - // 处理输入物品 - ItemStack[] items = input.getItems(); - if (items != null && items.length > 0) { - ItemStack itemStack = items[0]; - inputIngredients.add(Ingredient.of(itemStack.getItem())); - inputItemCounts.add(itemStack.getCount() * 64L); - } - - // 处理输入流体 - if (fluidIngredient != null) { - try { - // 获取FluidIngredient的流体列表 - java.lang.reflect.Method getFluidsMethod = fluidIngredientClass.getMethod("getFluids"); - List fluids = (List) getFluidsMethod.invoke(fluidIngredient); - if (fluids != null && !fluids.isEmpty()) { - // 只添加第一个FluidStack,与物品Ingredient类似,相同tag的流体应该任选其一即可 - FluidStack fluidStack = fluids.get(0); - inputFluids.add(new FluidStack(fluidStack.getFluid(), fluidStack.getAmount() * 64)); - } - } catch (Exception e) { - // 如果无法获取流体,尝试另一种方式 - try { - // 尝试获取所有流体,包括流动流体 - java.lang.reflect.Method getAllFluidsMethod = fluidIngredientClass.getMethod("getAllFluids"); - List allFluids = (List) getAllFluidsMethod.invoke(fluidIngredient); - if (allFluids != null && !allFluids.isEmpty()) { - // 只添加第一个FluidStack,与物品Ingredient类似,相同tag的流体应该任选其一即可 - FluidStack fluidStack = allFluids.get(0); - inputFluids.add(new FluidStack(fluidStack.getFluid(), fluidStack.getAmount() * 64)); - } - } catch (Exception ex) { - // 如果仍无法获取流体,跳过流体处理 - } - } - } - - List outputItems = new ArrayList<>(); - ItemStack output64 = output.copy(); - output64.setCount(output.getCount() * 64); - outputItems.add(output64); - - List outputFluids = new ArrayList<>(); - - int energy = 2000; - int processTime = 40; - - Ingredient catalyst = Ingredient.of(ModIngots.USELESS_INGOT_TIER_1.get(), - ModIngots.USELESS_INGOT_TIER_2.get(), - ModIngots.USELESS_INGOT_TIER_3.get(), - ModIngots.USELESS_INGOT_TIER_4.get(), - ModIngots.USELESS_INGOT_TIER_5.get(), - ModIngots.USELESS_INGOT_TIER_6.get(), - ModIngots.USELESS_INGOT_TIER_7.get(), - ModIngots.USELESS_INGOT_TIER_8.get(), - ModIngots.USELESS_INGOT_TIER_9.get(), - ModIngots.USEFUL_INGOT.get()); - int catalystCount = 1; - - // 使用原配方的stamp作为mold - Ingredient mold = stamp; - - AdvancedAlloyFurnaceRecipe convertedRecipe = new AdvancedAlloyFurnaceRecipe( - newId, - inputIngredients, - inputItemCounts, - inputFluids, - outputItems, - outputFluids, - energy, - processTime, - catalyst, - catalystCount, - mold, - false, - false - ); - - convertedRecipes.add(convertedRecipe); - } - } - } catch (Exception e) { - e.printStackTrace(); - System.err.println("Failed to convert embers stamping recipes: " + e.getMessage()); - } - - return convertedRecipes; - } - - /** - * 将余烬混合离心器(Mixer Centrifuge)配方转换为高级合金炉配方 - * - * @param recipeManager 游戏配方管理器 - * @return 转换后的高级合金炉配方列表 - */ - public static List convertMixingRecipes(RecipeManager recipeManager) { - List convertedRecipes = new ArrayList<>(); - - try { - // 加载必要的类 - Class iMixingRecipeClass = Class.forName("com.rekindled.embers.recipe.IMixingRecipe"); - Class registryManagerClass = Class.forName("com.rekindled.embers.RegistryManager"); - Class fluidIngredientClass = Class.forName("com.rekindled.embers.recipe.FluidIngredient"); - - // 获取MIXING RecipeType - Field mixingRecipeTypeField = registryManagerClass.getField("MIXING"); - Object mixingRecipeTypeObject = mixingRecipeTypeField.get(null); - - // 获取配方类型的get方法 - Class registryObjectClass = Class.forName("net.minecraftforge.registries.RegistryObject"); - Method getMethod = registryObjectClass.getMethod("get"); - net.minecraft.world.item.crafting.RecipeType recipeType = (net.minecraft.world.item.crafting.RecipeType) getMethod.invoke(mixingRecipeTypeObject); - - // 使用反射调用RecipeManager.getAllRecipesFor方法 - java.lang.reflect.Method getAllRecipesForMethod = RecipeManager.class.getMethod("getAllRecipesFor", net.minecraft.world.item.crafting.RecipeType.class); - Collection> allRecipes = (Collection>) getAllRecipesForMethod.invoke(recipeManager, recipeType); - - for (net.minecraft.world.item.crafting.Recipe recipe : allRecipes) { - if (iMixingRecipeClass.isInstance(recipe)) { - Object mixingRecipe = recipe; - - ResourceLocation originalId = recipe.getId(); - - // 仿照JEI集成的做法,获取输入和输出 - List inputFluids = new ArrayList<>(); - List outputFluids = new ArrayList<>(); - - // 1. 获取配方的输入流体 - 直接访问inputs字段,它是一个ArrayList - try { - // 获取配方的inputs字段 - java.lang.reflect.Field inputsField = mixingRecipe.getClass().getDeclaredField("inputs"); - inputsField.setAccessible(true); - ArrayList inputs = (ArrayList) inputsField.get(mixingRecipe); - - if (inputs != null && !inputs.isEmpty()) { - // 遍历每个FluidIngredient - for (Object input : inputs) { - if (input != null) { - // 获取FluidIngredient的流体列表 - java.lang.reflect.Method getFluidsMethod = input.getClass().getMethod("getFluids"); - List fluids = (List) getFluidsMethod.invoke(input); - if (fluids != null && !fluids.isEmpty()) { - // 只添加第一个FluidStack到输入流体列表中 - // 与物品Ingredient类似,相同tag的流体应该任选其一即可 - FluidStack fluidStack = fluids.get(0); - inputFluids.add(new FluidStack(fluidStack.getFluid(), fluidStack.getAmount() * 64)); - } - } - } - } - } catch (Exception e) { - e.printStackTrace(); - System.err.println("Failed to get input fluids: " + e.getMessage()); - } - - // 2. 获取输出流体 - 直接访问output字段,它是一个FluidOutput - try { - // 获取配方的output字段 - java.lang.reflect.Field outputField = mixingRecipe.getClass().getDeclaredField("output"); - outputField.setAccessible(true); - Object fluidOutput = outputField.get(mixingRecipe); - - if (fluidOutput != null) { - // 获取FluidOutput的流体Stack - java.lang.reflect.Method getFluidStackMethod = fluidOutput.getClass().getMethod("getFluidStack"); - FluidStack outputFluid = (FluidStack) getFluidStackMethod.invoke(fluidOutput); - if (outputFluid != null && !outputFluid.isEmpty()) { - outputFluids.add(new FluidStack(outputFluid.getFluid(), outputFluid.getAmount() * 64)); - } - } - } catch (Exception e) { - // 如果直接访问output字段失败,尝试使用getDisplayOutput()方法 - try { - java.lang.reflect.Method getDisplayOutputMethod = mixingRecipe.getClass().getMethod("getDisplayOutput"); - FluidStack displayOutput = (FluidStack) getDisplayOutputMethod.invoke(mixingRecipe); - if (displayOutput != null && !displayOutput.isEmpty()) { - outputFluids.add(new FluidStack(displayOutput.getFluid(), displayOutput.getAmount() * 64)); - } - } catch (Exception ex) { - ex.printStackTrace(); - System.err.println("Failed to get output fluid: " + ex.getMessage()); - } - } - - // 只有当有输出时才创建配方 - if (outputFluids != null && !outputFluids.isEmpty()) { - ResourceLocation newId = new ResourceLocation( - "useless_mod", - "embers_mixer_centrifuge_" + originalId.getNamespace() + "_" + originalId.getPath() - ); - - List inputIngredients = new ArrayList<>(); - List inputItemCounts = new ArrayList<>(); - // 创建空的物品输出列表,因为混合离心器只输出流体 - List outputItems = new ArrayList<>(); - - int energy = 2000; - int processTime = 40; - - Ingredient catalyst = Ingredient.of(ModIngots.USELESS_INGOT_TIER_1.get(), - ModIngots.USELESS_INGOT_TIER_2.get(), - ModIngots.USELESS_INGOT_TIER_3.get(), - ModIngots.USELESS_INGOT_TIER_4.get(), - ModIngots.USELESS_INGOT_TIER_5.get(), - ModIngots.USELESS_INGOT_TIER_6.get(), - ModIngots.USELESS_INGOT_TIER_7.get(), - ModIngots.USELESS_INGOT_TIER_8.get(), - ModIngots.USELESS_INGOT_TIER_9.get(), - ModIngots.USEFUL_INGOT.get()); - int catalystCount = 1; - - // 获取混合离心器物品作为模具 - ResourceLocation mixerCentrifugeId = ResourceLocation.fromNamespaceAndPath("embers", "mixer_centrifuge"); - net.minecraft.world.item.Item mixerCentrifuge = ForgeRegistries.ITEMS.getValue(mixerCentrifugeId); - if (mixerCentrifuge == null) { - // 尝试使用物品形式的ID - mixerCentrifugeId = ResourceLocation.fromNamespaceAndPath("embers", "mixer_centrifuge_item"); - mixerCentrifuge = ForgeRegistries.ITEMS.getValue(mixerCentrifugeId); - } - if (mixerCentrifuge == null) { - throw new RuntimeException("Could not find item: " + mixerCentrifugeId); - } - Ingredient mold = Ingredient.of(mixerCentrifuge); - - AdvancedAlloyFurnaceRecipe convertedRecipe = new AdvancedAlloyFurnaceRecipe( - newId, - inputIngredients, - inputItemCounts, - inputFluids, - outputItems, - outputFluids, - energy, - processTime, - catalyst, - catalystCount, - mold, - false, - false - ); - - convertedRecipes.add(convertedRecipe); - } - } - } - } catch (Exception e) { - e.printStackTrace(); - System.err.println("Failed to convert embers mixing recipes: " + e.getMessage()); - } - - return convertedRecipes; - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/compat/emi/AdvancedAlloyFurnaceEmiRecipe.java b/src/main/java/com/sorrowmist/useless/compat/emi/AdvancedAlloyFurnaceEmiRecipe.java deleted file mode 100644 index f92e0649..00000000 --- a/src/main/java/com/sorrowmist/useless/compat/emi/AdvancedAlloyFurnaceEmiRecipe.java +++ /dev/null @@ -1,416 +0,0 @@ -package com.sorrowmist.useless.compat.emi; - -import com.sorrowmist.useless.recipes.advancedalloyfurnace.AdvancedAlloyFurnaceRecipe; -import com.sorrowmist.useless.registry.CatalystManager; -import com.mojang.blaze3d.systems.RenderSystem; -import com.sorrowmist.useless.utils.NumberFormatUtil; -import dev.emi.emi.api.recipe.EmiRecipe; -import dev.emi.emi.api.recipe.EmiRecipeCategory; -import dev.emi.emi.api.stack.EmiIngredient; -import dev.emi.emi.api.stack.EmiStack; -import dev.emi.emi.EmiRenderHelper; -import dev.emi.emi.api.widget.AnimatedTextureWidget; -import dev.emi.emi.api.widget.SlotWidget; -import dev.emi.emi.api.widget.WidgetHolder; -import net.minecraft.ChatFormatting; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.Font; -import net.minecraft.network.chat.Component; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.crafting.Ingredient; -import net.minecraftforge.fluids.FluidStack; - -import java.util.ArrayList; -import java.util.List; - -public class AdvancedAlloyFurnaceEmiRecipe implements EmiRecipe { - private final AdvancedAlloyFurnaceRecipe recipe; - private final List inputs = new ArrayList<>(); - private final List outputs = new ArrayList<>(); - - // 槽位位置常量 - private static final int INPUT_SLOTS_START_X = 1; - private static final int INPUT_SLOTS_START_Y = 1; - private static final int INPUT_SLOT_SPACING_X = 18; - private static final int INPUT_SLOT_SPACING_Y = 18; - private static final int OUTPUT_SLOTS_START_X = 159; - private static final int OUTPUT_SLOTS_START_Y = 1; - private static final int FLUID_INPUT_X = 1; - private static final int FLUID_INPUT_Y = 55; - private static final int FLUID_INPUT_WIDTH = 52; - private static final int FLUID_INPUT_HEIGHT = 16; - private static final int FLUID_OUTPUT_X = 159; - private static final int FLUID_OUTPUT_Y = 55; - private static final int FLUID_OUTPUT_WIDTH = 52; - private static final int FLUID_OUTPUT_HEIGHT = 16; - private static final int CATALYST_SLOT_X = 80; - private static final int CATALYST_SLOT_Y = 55; - private static final int MOLD_SLOT_X = 116; - private static final int MOLD_SLOT_Y = 55; - private static final int PROGRESS_BAR_X = 90; - private static final int PROGRESS_BAR_Y = 23; - private static final int PROGRESS_BAR_WIDTH = 32; - private static final int PROGRESS_BAR_HEIGHT = 8; - private static final int PROGRESS_BAR_SOURCE_X = 90; - private static final int PROGRESS_BAR_SOURCE_Y = 73; - private static final int ENERGY_DISPLAY_X = 75; - private static final int ENERGY_DISPLAY_Y = 0; - private static final int ENERGY_DISPLAY_WIDTH = 62; - private static final int ENERGY_DISPLAY_HEIGHT = 7; - private static final int TIME_DISPLAY_X = 78; - private static final int TIME_DISPLAY_Y = 41; - private static final int TIME_DISPLAY_WIDTH = 56; - private static final int TIME_DISPLAY_HEIGHT = 10; - private static final int CATALYST_TEXT_Y = 13; - private static final int MOLD_TEXT_Y = 32; - private static final int PARALLEL_TEXT_X = 57; - private static final int PARALLEL_TEXT_Y = 5; - private static final int SLOT_SIZE = 16; - - // 纹理资源 - private static final ResourceLocation GUI_TEXTURE = ResourceLocation.fromNamespaceAndPath("useless_mod", "textures/gui/jei_advanced_alloy_furnace_gui.png"); - private static final ResourceLocation PROGRESS_TEXTURE = ResourceLocation.fromNamespaceAndPath("useless_mod", "textures/gui/jei_arrow_advanced_alloy_furnace_gui.png"); - - public AdvancedAlloyFurnaceEmiRecipe(AdvancedAlloyFurnaceRecipe recipe) { - this.recipe = recipe; - - // 处理输入物品 - for (int i = 0; i < Math.min(recipe.getInputItems().size(), 9); i++) { - Ingredient ingredient = recipe.getInputItems().get(i); - - ItemStack[] matchingStacks = ingredient.getItems(); - if (matchingStacks.length > 0) { - ItemStack[] displayStacks = new ItemStack[matchingStacks.length]; - for (int j = 0; j < matchingStacks.length; j++) { - ItemStack displayStack = matchingStacks[j].copy(); - // 让EMI读取到的数量为1,然后自己绘制格式化的数量 - displayStack.setCount(1); - displayStacks[j] = displayStack; - } - - inputs.add(EmiIngredient.of(Ingredient.of(displayStacks))); - } - } - - // 处理输出物品 - for (ItemStack stack : recipe.getOutputItems()) { - ItemStack displayStack = stack.copy(); - // 让EMI读取到的数量为1,然后自己绘制格式化的数量 - displayStack.setCount(1); - outputs.add(EmiStack.of(displayStack)); - } - - // 处理流体输入(如果有) - List inputFluids = recipe.getInputFluids(); - if (!inputFluids.isEmpty()) { - // EMI处理流体的方式可能不同,这里先添加一个占位符 - } - - // 处理流体输出(如果有) - List outputFluids = recipe.getOutputFluids(); - if (!outputFluids.isEmpty()) { - // EMI处理流体的方式可能不同,这里先添加一个占位符 - } - - // 处理催化剂 - if (recipe.requiresCatalyst()) { - inputs.add(EmiIngredient.of(recipe.getCatalyst())); - } - - // 处理模具 - if (recipe.requiresMold()) { - inputs.add(EmiIngredient.of(recipe.getMold())); - } - } - - @Override - public EmiRecipeCategory getCategory() { - return AdvancedAlloyFurnaceRecipeCategory.CATEGORY; - } - - @Override - public ResourceLocation getId() { - return recipe.getId(); - } - - @Override - public List getInputs() { - return inputs; - } - - @Override - public List getOutputs() { - return outputs; - } - - @Override - public int getDisplayWidth() { - return 212; - } - - @Override - public int getDisplayHeight() { - return 72; - } - - @Override - public void addWidgets(WidgetHolder widgets) { - // 绘制背景 - widgets.addTexture(GUI_TEXTURE, 0, 0, 212, 72, 0, 0, 212, 72, 256, 256); - - // 输入物品槽位 (9个) - 3行3列排列 - int inputIndex = 0; - for (int i = 0; i < Math.min(recipe.getInputItems().size(), 9); i++) { - int row = i / 3; - int col = i % 3; - // 物品渲染需要向上向左各移动1像素 - int x = INPUT_SLOTS_START_X + col * INPUT_SLOT_SPACING_X - 1; - int y = INPUT_SLOTS_START_Y + row * INPUT_SLOT_SPACING_Y - 1; - - EmiIngredient ingredient = inputs.get(inputIndex++); - // 添加槽位,不绘制背景(因为背景已经在GUI纹理中了) - widgets.addSlot(ingredient, x, y).drawBack(false); - - // 添加自定义绘制器来显示格式化的物品数量 - final int index = i; - widgets.addDrawable(x, y, 18, 18, (draw, mouseX, mouseY, delta) -> { - // 启用深度测试 - RenderSystem.enableDepthTest(); - - Minecraft mc = Minecraft.getInstance(); - Font font = mc.font; - - // 使用配方中的实际数量,而不是从物品堆栈中获取 - long count = recipe.getInputItemCounts().get(index); - if (count > 1) { - String text = NumberFormatUtil.formatItemCount(count); - draw.pose().pushPose(); - float scale = 0.65f; - // 自定义绘制器的坐标是相对于自己的绘制区域的,所以从(1,1)开始,而不是(x+1,y+1) - draw.pose().translate(1, 1, 200.0F); - draw.pose().scale(scale, scale, 1.0F); - - // 严格参考JEI的显示位置 - int textX = Math.round(16.0f / scale) - font.width(text); - int textY = Math.round(10.0f / scale); - - draw.drawString(font, text, textX, textY, 0xFFFFFF, true); - draw.pose().popPose(); - } - - RenderSystem.disableBlend(); - }); - } - - // 输出物品槽位 (9个) - 3行3列排列 - for (int i = 0; i < Math.min(recipe.getOutputItems().size(), 9); i++) { - int row = i / 3; - int col = i % 3; - // 物品渲染需要向上向左各移动1像素 - int x = OUTPUT_SLOTS_START_X + col * INPUT_SLOT_SPACING_X - 1; - int y = OUTPUT_SLOTS_START_Y + row * INPUT_SLOT_SPACING_Y - 1; - - EmiStack stack = outputs.get(i); - // 添加槽位,不绘制背景,不使用large属性,保持默认大小 - widgets.addSlot(stack, x, y).drawBack(false); - - // 添加自定义绘制器来显示格式化的物品数量 - final int index = i; - widgets.addDrawable(x, y, 18, 18, (draw, mouseX, mouseY, delta) -> { - // 启用深度测试 - RenderSystem.enableDepthTest(); - - Minecraft mc = Minecraft.getInstance(); - Font font = mc.font; - - // 使用配方中的实际输出数量 - int count = recipe.getOutputItems().get(index).getCount(); - if (count > 1) { - String text = NumberFormatUtil.formatItemCount(count); - draw.pose().pushPose(); - float scale = 0.65f; - // 自定义绘制器的坐标是相对于自己的绘制区域的,所以从(1,1)开始,而不是(x+1,y+1) - draw.pose().translate(1, 1, 200.0F); - draw.pose().scale(scale, scale, 1.0F); - - // 严格参考JEI的显示位置 - int textX = Math.round(16.0f / scale) - font.width(text); - int textY = Math.round(10.0f / scale); - - draw.drawString(font, text, textX, textY, 0xFFFFFF, true); - draw.pose().popPose(); - } - - RenderSystem.disableBlend(); - }); - } - - // 输入流体槽 - 使用SlotWidget处理流体的渲染和tooltips - List inputFluids = recipe.getInputFluids(); - if (!inputFluids.isEmpty()) { - int fluidCount = inputFluids.size(); - int fluidWidth = FLUID_INPUT_WIDTH / fluidCount; - - for (int i = 0; i < fluidCount; i++) { - final var fluidStack = inputFluids.get(i); - // 流体渲染位置向左上各移动一个像素 - final int fluidX = FLUID_INPUT_X - 1 + i * fluidWidth; - final int fluidY = FLUID_INPUT_Y - 1; - - // 创建流体的EmiStack,用于显示tooltips和渲染 - EmiStack emiFluidStack = EmiStack.of(fluidStack.getFluid(), fluidStack.getAmount()); - - // 添加槽位,用于处理渲染和tooltips - widgets.addSlot(emiFluidStack, fluidX, fluidY).drawBack(false); - - // 渲染流体数量文本 - widgets.addDrawable(fluidX, fluidY, fluidWidth, FLUID_INPUT_HEIGHT, (draw, mouseX, mouseY, delta) -> { - RenderSystem.enableDepthTest(); - - if (!fluidStack.isEmpty()) { - // 渲染流体数量文本 - Minecraft mc = Minecraft.getInstance(); - Font font = mc.font; - String text = NumberFormatUtil.formatFluidAmount(fluidStack.getAmount()); - draw.pose().pushPose(); - float scale = 0.65f; - // 自定义绘制器的坐标是相对于自己的绘制区域的 - draw.pose().translate(0, 0, 200.0F); - draw.pose().scale(scale, scale, 1.0F); - - int textX = Math.round(fluidWidth / scale) - font.width(text); - int textY = Math.round(FLUID_INPUT_HEIGHT / scale) - font.lineHeight; - - draw.drawString(font, text, textX, textY, 0xFFFFFF, true); - draw.pose().popPose(); - } - - // 从UI图抠取覆盖层,尺寸根据流体槽实际大小确定,渲染在流体槽上 - draw.blit(GUI_TEXTURE, 0, 0, 0, 54, fluidWidth + 2, FLUID_INPUT_HEIGHT + 2, 256, 256); - - RenderSystem.disableBlend(); - }); - } - } - - // 输出流体槽 - 使用SlotWidget处理流体的渲染和tooltips - List outputFluids = recipe.getOutputFluids(); - if (!outputFluids.isEmpty()) { - int fluidCount = outputFluids.size(); - int fluidWidth = FLUID_OUTPUT_WIDTH / fluidCount; - - for (int i = 0; i < fluidCount; i++) { - final var fluidStack = outputFluids.get(i); - // 流体渲染位置向左上各移动一个像素 - final int fluidX = FLUID_OUTPUT_X - 1 + i * fluidWidth; - final int fluidY = FLUID_OUTPUT_Y - 1; - - // 创建流体的EmiStack,用于显示tooltips和渲染 - EmiStack emiFluidStack = EmiStack.of(fluidStack.getFluid(), fluidStack.getAmount()); - - // 添加槽位,用于处理渲染和tooltips - widgets.addSlot(emiFluidStack, fluidX, fluidY).drawBack(false); - - // 渲染流体数量文本 - widgets.addDrawable(fluidX, fluidY, fluidWidth, FLUID_OUTPUT_HEIGHT, (draw, mouseX, mouseY, delta) -> { - RenderSystem.enableDepthTest(); - - if (!fluidStack.isEmpty()) { - // 渲染流体数量文本 - Minecraft mc = Minecraft.getInstance(); - Font font = mc.font; - String text = NumberFormatUtil.formatFluidAmount(fluidStack.getAmount()); - draw.pose().pushPose(); - float scale = 0.65f; - // 自定义绘制器的坐标是相对于自己的绘制区域的 - draw.pose().translate(0, 0, 200.0F); - draw.pose().scale(scale, scale, 1.0F); - - int textX = Math.round(fluidWidth / scale) - font.width(text); - int textY = Math.round(FLUID_OUTPUT_HEIGHT / scale) - font.lineHeight; - - draw.drawString(font, text, textX, textY, 0xFFFFFF, true); - draw.pose().popPose(); - } - - // 从UI图抠取覆盖层,尺寸根据流体槽实际大小确定,渲染在流体槽上 - draw.blit(GUI_TEXTURE, 0, 0, 159, 54, fluidWidth + 2, FLUID_OUTPUT_HEIGHT + 2, 256, 256); - - RenderSystem.disableBlend(); - }); - } - } - - // 催化剂槽位:只在配方需要催化剂时显示 - if (recipe.requiresCatalyst()) { - EmiIngredient catalyst = inputs.get(inputIndex++); - // 物品渲染需要向上向左各移动1像素 - widgets.addSlot(catalyst, CATALYST_SLOT_X - 1, CATALYST_SLOT_Y - 1).drawBack(false); - } - - // 模具槽位 - if (recipe.requiresMold()) { - EmiIngredient mold = inputs.get(inputIndex); - // 物品渲染需要向上向左各移动1像素 - widgets.addSlot(mold, MOLD_SLOT_X - 1, MOLD_SLOT_Y - 1).drawBack(false); - } - - // 绘制进度条 - 2秒完成一次合成进度 - widgets.addAnimatedTexture(GUI_TEXTURE, PROGRESS_BAR_X, PROGRESS_BAR_Y, PROGRESS_BAR_WIDTH, PROGRESS_BAR_HEIGHT, PROGRESS_BAR_SOURCE_X, PROGRESS_BAR_SOURCE_Y, PROGRESS_BAR_WIDTH, PROGRESS_BAR_HEIGHT, 256, 256, 2000, true, true, false); - - // 移除能量条背景和填充的自定义纹理,因为JEI中没有这些 - - // 绘制能量和时间信息 - String energyText = NumberFormatUtil.formatEnergy(recipe.getEnergy()); - String timeText = recipe.getProcessTime() + " ticks"; - - // 在能量显示区域居中显示能量(绿色) - Minecraft minecraft = Minecraft.getInstance(); - int energyTextWidth = minecraft.font.width(energyText); - int energyX = ENERGY_DISPLAY_X + (ENERGY_DISPLAY_WIDTH - energyTextWidth) / 2; - widgets.addText(Component.literal(energyText), energyX, ENERGY_DISPLAY_Y, 0x00FF00, false); - - // 在时间显示区域居中显示时间(绿色) - int timeTextWidth = minecraft.font.width(timeText); - int timeX = TIME_DISPLAY_X + (TIME_DISPLAY_WIDTH - timeTextWidth) / 2; - widgets.addText(Component.literal(timeText), timeX, TIME_DISPLAY_Y, 0x00FF00, false); - - // 修改:只有当配方允许催化剂时才显示提示 - if (recipe.requiresCatalyst() && recipe.isCatalystAllowed()) { - String catalystText = "无用锭为可选催化剂"; - // 使用自定义绘制器来实现文字缩放 - widgets.addDrawable(0, 0, 191, 54, (draw, mouseX, mouseY, delta) -> { - draw.pose().pushPose(); - float scale = 0.7f; - draw.pose().scale(scale, scale, 1.0f); - - int textWidth = minecraft.font.width(catalystText); - // 在整张 JEI 背景宽度内居中 - int centeredX = (int) ((212 / 2.0f - textWidth * scale / 2) / scale); - int y = (int) (CATALYST_TEXT_Y / scale); - - draw.drawString(minecraft.font, catalystText, centeredX, y, 0xFF0000, false); - draw.pose().popPose(); - }); - } - - // 模具提示 - if (recipe.requiresMold()) { - String moldText = "需要标志物(不会被消耗)"; - // 使用自定义绘制器来实现文字缩放 - widgets.addDrawable(0, 0, 191, 54, (draw, mouseX, mouseY, delta) -> { - draw.pose().pushPose(); - float scale = 0.7f; - draw.pose().scale(scale, scale, 1.0f); - - int textWidth = minecraft.font.width(moldText); - int centeredX = (int) ((212 / 2.0f - textWidth * scale / 2) / scale); - int y = (int) (MOLD_TEXT_Y / scale); - - draw.drawString(minecraft.font, moldText, centeredX, y, 0xFF0000, false); - draw.pose().popPose(); - }); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/compat/emi/AdvancedAlloyFurnaceRecipeCategory.java b/src/main/java/com/sorrowmist/useless/compat/emi/AdvancedAlloyFurnaceRecipeCategory.java deleted file mode 100644 index ce1ac873..00000000 --- a/src/main/java/com/sorrowmist/useless/compat/emi/AdvancedAlloyFurnaceRecipeCategory.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.sorrowmist.useless.compat.emi; - -import com.sorrowmist.useless.UselessMod; -import com.sorrowmist.useless.blocks.advancedalloyfurnace.AdvancedAlloyFurnaceBlock; -import dev.emi.emi.api.recipe.EmiRecipeCategory; -import dev.emi.emi.api.stack.EmiStack; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.item.ItemStack; - -public class AdvancedAlloyFurnaceRecipeCategory extends EmiRecipeCategory { - public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath(UselessMod.MOD_ID, "advanced_alloy_furnace"); - public static final AdvancedAlloyFurnaceRecipeCategory CATEGORY = new AdvancedAlloyFurnaceRecipeCategory(); - - public AdvancedAlloyFurnaceRecipeCategory() { - super(ID, EmiStack.of(new ItemStack(AdvancedAlloyFurnaceBlock.ADVANCED_ALLOY_FURNACE_BLOCK.get()))); - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/compat/emi/CatalystInfoCategory.java b/src/main/java/com/sorrowmist/useless/compat/emi/CatalystInfoCategory.java deleted file mode 100644 index 0a3bd421..00000000 --- a/src/main/java/com/sorrowmist/useless/compat/emi/CatalystInfoCategory.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.sorrowmist.useless.compat.emi; - -import com.sorrowmist.useless.UselessMod; -import com.sorrowmist.useless.registry.ModIngots; -import dev.emi.emi.api.recipe.EmiRecipeCategory; -import dev.emi.emi.api.stack.EmiStack; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.item.ItemStack; - -public class CatalystInfoCategory extends EmiRecipeCategory { - public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath(UselessMod.MOD_ID, "catalyst_info"); - public static final CatalystInfoCategory CATEGORY = new CatalystInfoCategory(); - - public CatalystInfoCategory() { - super(ID, EmiStack.of(new ItemStack(ModIngots.USEFUL_INGOT.get()))); - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/compat/emi/CatalystInfoEmiRecipe.java b/src/main/java/com/sorrowmist/useless/compat/emi/CatalystInfoEmiRecipe.java deleted file mode 100644 index 26352cb9..00000000 --- a/src/main/java/com/sorrowmist/useless/compat/emi/CatalystInfoEmiRecipe.java +++ /dev/null @@ -1,185 +0,0 @@ -package com.sorrowmist.useless.compat.emi; - -import com.sorrowmist.useless.registry.CatalystManager; -import com.sorrowmist.useless.registry.ModIngots; -import dev.emi.emi.api.recipe.EmiRecipe; -import dev.emi.emi.api.recipe.EmiRecipeCategory; -import dev.emi.emi.api.stack.EmiIngredient; -import dev.emi.emi.api.stack.EmiStack; -import dev.emi.emi.api.widget.SlotWidget; -import dev.emi.emi.api.widget.WidgetHolder; -import net.minecraft.ChatFormatting; -import net.minecraft.client.Minecraft; -import net.minecraft.network.chat.Component; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.item.ItemStack; - -import java.util.ArrayList; -import java.util.List; - -public class CatalystInfoEmiRecipe implements EmiRecipe { - private final List inputs = new ArrayList<>(); - private final List outputs = new ArrayList<>(); - - // 显示尺寸 - private static final int DISPLAY_WIDTH = 150; - private static final int DISPLAY_HEIGHT = 130; - - // 从 CatalystManager 获取催化剂数据 - private final ItemStack[] catalystStacks; - - // 纹理资源 - private static final ResourceLocation GUI_TEXTURE = ResourceLocation.fromNamespaceAndPath("useless_mod", "textures/gui/jei_catalyst_info_gui.png"); - - public CatalystInfoEmiRecipe() { - // 创建催化剂物品堆栈数组 - this.catalystStacks = new ItemStack[]{ - new ItemStack(ModIngots.USELESS_INGOT_TIER_1.get()), - new ItemStack(ModIngots.USELESS_INGOT_TIER_2.get()), - new ItemStack(ModIngots.USELESS_INGOT_TIER_3.get()), - new ItemStack(ModIngots.USELESS_INGOT_TIER_4.get()), - new ItemStack(ModIngots.USELESS_INGOT_TIER_5.get()), - new ItemStack(ModIngots.USELESS_INGOT_TIER_6.get()), - new ItemStack(ModIngots.USELESS_INGOT_TIER_7.get()), - new ItemStack(ModIngots.USELESS_INGOT_TIER_8.get()), - new ItemStack(ModIngots.USELESS_INGOT_TIER_9.get()), - new ItemStack(ModIngots.USEFUL_INGOT.get()) - }; - - // 添加催化剂到输入列表 - for (ItemStack stack : catalystStacks) { - inputs.add(EmiStack.of(stack)); - } - } - - @Override - public EmiRecipeCategory getCategory() { - return CatalystInfoCategory.CATEGORY; - } - - @Override - public ResourceLocation getId() { - return ResourceLocation.fromNamespaceAndPath("useless_mod", "catalyst_info"); - } - - @Override - public List getInputs() { - return inputs; - } - - @Override - public List getOutputs() { - return outputs; - } - - @Override - public int getDisplayWidth() { - return DISPLAY_WIDTH; - } - - @Override - public int getDisplayHeight() { - return DISPLAY_HEIGHT; - } - - @Override - public void addWidgets(WidgetHolder widgets) { - // 绘制背景,修复缩放问题 - widgets.addTexture(GUI_TEXTURE, 0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT, 0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_WIDTH, DISPLAY_HEIGHT); - - // 绘制标题 - 居中显示 - String titleText = "催化剂并行数信息"; - Minecraft minecraft = Minecraft.getInstance(); - int titleWidth = minecraft.font.width(titleText); - int titleX = (DISPLAY_WIDTH - titleWidth) / 2; - widgets.addText(Component.literal(titleText), titleX, 10, 0x404040, false); - - // 调整说明文字的位置,并居中显示 - String[] explanationLines = { - "• 催化剂为可选项,除有用锭外会被消耗", - "• 放入催化剂可大幅提高并行数", - "• 并行数影响消耗、产出和能量", - "• 合成无用锭时催化剂无效" - }; - - int explanationY = 85; - for (String line : explanationLines) { - int lineWidth = minecraft.font.width(line); - int lineX = (DISPLAY_WIDTH - lineWidth) / 2; - widgets.addText(Component.literal(line), lineX, explanationY, 0x404040, false); - explanationY += 10; - } - - // 2行5列网格布局 - int startX = 29; // 第一个格子左上角起点X坐标 - int startY = 24; // 第一个格子左上角起点Y坐标 - int spacing = 18; // 每个格子18*18像素 - - // 总共10个催化剂,按2行5列排列 - for (int i = 0; i < catalystStacks.length; i++) { - // 计算行和列 - int row = i / 5; // 0, 0, 0, 0, 0, 1, 1, 1, 1, 1 - int col = i % 5; // 0, 1, 2, 3, 4, 0, 1, 2, 3, 4 - - int x = startX + col * spacing; - int y = startY + row * spacing; - - ItemStack stack = catalystStacks[i]; - EmiStack emiStack = EmiStack.of(stack); - SlotWidget slot = widgets.addSlot(emiStack, x, y).drawBack(false); - - // 添加自定义工具提示 - int parallel = CatalystManager.getCatalystParallel(stack); - String catalystName = CatalystManager.getCatalystName(stack); - - slot.appendTooltip(Component.literal(catalystName).withStyle(ChatFormatting.GOLD)); - - // 检查是否为USEFUL_INGOT - if (stack.getItem() == ModIngots.USEFUL_INGOT.get()) { - slot.appendTooltip(Component.literal("并行数: 无上限").withStyle(ChatFormatting.GREEN)); - slot.appendTooltip(Component.literal("")); - slot.appendTooltip(Component.literal("效果说明:").withStyle(ChatFormatting.YELLOW)); - slot.appendTooltip(Component.literal("• 催化剂不会被消耗").withStyle(ChatFormatting.GREEN)); - slot.appendTooltip(Component.literal("• 无并行数上限").withStyle(ChatFormatting.GREEN)); - slot.appendTooltip(Component.literal("• 能量消耗不会倍增").withStyle(ChatFormatting.GREEN)); - slot.appendTooltip(Component.literal("• 处理时间保持不变").withStyle(ChatFormatting.GRAY)); - slot.appendTooltip(Component.literal("注意: 合成无用锭时催化剂无效").withStyle(ChatFormatting.RED)); - } else { - slot.appendTooltip(Component.literal("并行数: " + parallel + "倍").withStyle(ChatFormatting.GREEN)); - slot.appendTooltip(Component.literal("")); - slot.appendTooltip(Component.literal("效果说明:").withStyle(ChatFormatting.YELLOW)); - slot.appendTooltip(Component.literal("• 输入物品消耗 × " + parallel).withStyle(ChatFormatting.GRAY)); - slot.appendTooltip(Component.literal("• 输出物品数量 × " + parallel).withStyle(ChatFormatting.GRAY)); - slot.appendTooltip(Component.literal("• 能量消耗 × " + parallel).withStyle(ChatFormatting.GRAY)); - slot.appendTooltip(Component.literal("• 处理时间保持不变").withStyle(ChatFormatting.GRAY)); - // 添加黑名单说明 - slot.appendTooltip(Component.literal("⚠ 催化剂会被消耗").withStyle(ChatFormatting.RED)); - slot.appendTooltip(Component.literal("注意: 合成无用锭时催化剂无效").withStyle(ChatFormatting.RED)); - } - - // 在物品上显示并行数 - final String parallelText; - if (stack.getItem() == ModIngots.USEFUL_INGOT.get()) { - parallelText = "∞"; - } else { - parallelText = parallel + "x"; - } - // 使用自定义绘制器绘制并行数文本,与JEI一致 - widgets.addDrawable(x, y, 18, 18, (draw, mouseX, mouseY, delta) -> { - draw.pose().pushPose(); - // 自定义绘制器的坐标是相对于自己的绘制区域的,所以从(0,0)开始,而不是(x, y) - draw.pose().translate(0, 0, 200.0F); - draw.pose().scale(0.5f, 0.5f, 1.0F); - - int textWidth = minecraft.font.width(parallelText); - int scaledX = (int)((16 - textWidth * 0.5f) / 0.5f); - int scaledY = (int)(12 / 0.5f); - - draw.drawString(minecraft.font, parallelText, scaledX, scaledY, 0x00FF00, true); - draw.pose().popPose(); - }); - } - } - - -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/compat/emi/EMIPlugin.java b/src/main/java/com/sorrowmist/useless/compat/emi/EMIPlugin.java deleted file mode 100644 index b64bbdc9..00000000 --- a/src/main/java/com/sorrowmist/useless/compat/emi/EMIPlugin.java +++ /dev/null @@ -1,219 +0,0 @@ -package com.sorrowmist.useless.compat.emi; - -import com.sorrowmist.useless.UselessMod; -import com.sorrowmist.useless.blocks.advancedalloyfurnace.AdvancedAlloyFurnaceBlock; -import com.sorrowmist.useless.compat.thermal.ThermalRecipeConverter; -import com.sorrowmist.useless.recipes.ModRecipeTypes; -import com.sorrowmist.useless.registry.ModIngots; -import cofh.thermal.core.util.managers.machine.InsolatorRecipeManager; -import dev.emi.emi.api.EmiEntrypoint; -import dev.emi.emi.api.EmiPlugin; -import dev.emi.emi.api.EmiRegistry; -import dev.emi.emi.api.stack.EmiStack; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.crafting.RecipeManager; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Objects; - -// 只有在EMI存在时才会被加载 -@EmiEntrypoint -public class EMIPlugin implements EmiPlugin { - @Override - public void register(EmiRegistry registry) { - // 注册配方类别 - registry.addCategory(new AdvancedAlloyFurnaceRecipeCategory()); - registry.addCategory(new CatalystInfoCategory()); - - RecipeManager recipeManager = Objects.requireNonNull(registry.getRecipeManager()); - - // 获取所有高级合金炉配方 - List recipes = new ArrayList<>(); - recipes.addAll(recipeManager.getAllRecipesFor(ModRecipeTypes.ADVANCED_ALLOY_FURNACE_TYPE.get())); - - // 添加所有原版熔炉配方,转换为高级合金炉配方格式 - Collection smeltingRecipes = recipeManager.getAllRecipesFor( - net.minecraft.world.item.crafting.RecipeType.SMELTING - ); - - for (net.minecraft.world.item.crafting.SmeltingRecipe recipe : smeltingRecipes) { - // 将原版熔炉配方转换为高级合金炉配方 - List smeltingIngredients = recipe.getIngredients(); - List inputIngredients = new ArrayList<>(); - List inputCounts = new ArrayList<>(); - - for (net.minecraft.world.item.crafting.Ingredient ingredient : smeltingIngredients) { - inputIngredients.add(ingredient); - inputCounts.add(1); - } - - List outputItems = new ArrayList<>(); - outputItems.add(recipe.getResultItem(null)); - - List emptyFluids = new ArrayList<>(); - net.minecraft.world.item.crafting.Ingredient furnaceIngredient = net.minecraft.world.item.crafting.Ingredient.of(net.minecraft.world.item.Items.FURNACE); - - // 创建无用锭标签的催化剂,允许使用所有无用锭 - net.minecraft.tags.TagKey uselessIngotTag = - net.minecraft.tags.TagKey.create( - net.minecraft.core.registries.Registries.ITEM, - new net.minecraft.resources.ResourceLocation("useless_mod", "useless_ingots") - ); - net.minecraft.world.item.crafting.Ingredient uselessIngotCatalyst = net.minecraft.world.item.crafting.Ingredient.of(uselessIngotTag); - - // 将List转换为List - List longInputCounts = new ArrayList<>(); - for (Integer count : inputCounts) { - longInputCounts.add(count.longValue()); - } - - // 创建临时的高级合金炉配方,使用原版熔炉作为模具 - com.sorrowmist.useless.recipes.advancedalloyfurnace.AdvancedAlloyFurnaceRecipe convertedRecipe = new com.sorrowmist.useless.recipes.advancedalloyfurnace.AdvancedAlloyFurnaceRecipe( - recipe.getId(), - inputIngredients, - longInputCounts, - emptyFluids, - outputItems, - emptyFluids, - 1000, // 能量消耗 - 40, // 处理时间:熔炉配方为40tick - uselessIngotCatalyst, // 催化剂:无用锭标签 - 1, // 催化剂数量 - furnaceIngredient // 模具:原版熔炉 - ); - - recipes.add(convertedRecipe); - } - - // 添加热力配方转换 - try { - // 直接从RecipeManager获取所有InsolatorRecipe并转换 - List convertedRecipes = - ThermalRecipeConverter.convertInsolatorRecipes(recipeManager); - recipes.addAll(convertedRecipes); - - // 添加热力感应炉(Smelter)配方转换 - List smelterRecipes = - ThermalRecipeConverter.convertSmelterRecipes(recipeManager); - recipes.addAll(smelterRecipes); - - // 添加热力多驱冲压机(Press)配方转换 - List pressRecipes = - ThermalRecipeConverter.convertPressRecipes(recipeManager); - recipes.addAll(pressRecipes); - } catch (Exception e) { - // 如果热力模组未加载,捕获异常以避免崩溃 - } - - // 添加Mekanism配方转换 - try { - // 添加Mekanism冶金灌注机(Metallurgic Infuser)配方转换 - List mekanismRecipes = - com.sorrowmist.useless.compat.mekanism.MekanismRecipeConverter.convertMetallurgicInfuserRecipes(recipeManager); - recipes.addAll(mekanismRecipes); - - // 添加Mekanism富集仓(Enrichment Chamber)配方转换 - List enrichmentRecipes = - com.sorrowmist.useless.compat.mekanism.MekanismRecipeConverter.convertEnrichmentChamberRecipes(recipeManager); - recipes.addAll(enrichmentRecipes); - } catch (Exception e) { - // 如果Mekanism模组未加载,捕获异常以避免崩溃 - } - - // 添加工业先锋配方转换 - try { - // 添加工业先锋化学溶解室(Dissolution Chamber)配方转换 - List dissolutionRecipes = - com.sorrowmist.useless.compat.industrialforegoing.IndustrialForegoingRecipeConverter.convertDissolutionChamberRecipes(recipeManager); - recipes.addAll(dissolutionRecipes); - } catch (Exception e) { - // 如果工业先锋模组未加载,捕获异常以避免崩溃 - } - - // 添加血魔法配方转换 - try { - // 添加血魔法血之祭坛(Blood Altar)配方转换 - List bloodAltarRecipes = - com.sorrowmist.useless.compat.bloodmagic.BloodMagicRecipeConverter.convertBloodAltarRecipes(recipeManager); - recipes.addAll(bloodAltarRecipes); - - // 添加血魔法狱火熔炉(Soul Forge)配方转换 - List soulForgeRecipes = - com.sorrowmist.useless.compat.bloodmagic.BloodMagicRecipeConverter.convertTartaricForgeRecipes(recipeManager); - recipes.addAll(soulForgeRecipes); - - // 添加血魔法炼金术桌(Alchemy Table)配方转换 - List alchemyTableRecipes = - com.sorrowmist.useless.compat.bloodmagic.BloodMagicRecipeConverter.convertAlchemyTableRecipes(recipeManager); - recipes.addAll(alchemyTableRecipes); - } catch (Exception e) { - // 如果血魔法模组未加载,捕获异常以避免崩溃 - } - - // 添加余烬mod配方转换 - try { - // 添加余烬炼金台(Alchemy Tablet)配方转换 - List embersAlchemyRecipes = - com.sorrowmist.useless.compat.embers.EmbersRecipeConverter.convertAlchemyTabletRecipes(recipeManager); - recipes.addAll(embersAlchemyRecipes); - - // 添加余烬地质分离器(Geologic Separator)配方转换 - List embersGeoSeparatorRecipes = - com.sorrowmist.useless.compat.embers.EmbersRecipeConverter.convertGeoSeparatorRecipes(recipeManager); - recipes.addAll(embersGeoSeparatorRecipes); - - // 添加余烬熔炼炉(Melter)配方转换 - List embersMelterRecipes = - com.sorrowmist.useless.compat.embers.EmbersRecipeConverter.convertMelterRecipes(recipeManager); - recipes.addAll(embersMelterRecipes); - - // 添加余烬压印锤(Stamper)配方转换 - List embersStampingRecipes = - com.sorrowmist.useless.compat.embers.EmbersRecipeConverter.convertStampingRecipes(recipeManager); - recipes.addAll(embersStampingRecipes); - - // 添加余烬混合离心器(Mixer Centrifuge)配方转换 - List embersMixingRecipes = - com.sorrowmist.useless.compat.embers.EmbersRecipeConverter.convertMixingRecipes(recipeManager); - recipes.addAll(embersMixingRecipes); - } catch (Exception e) { - // 如果余烬mod未加载,捕获异常以避免崩溃 - } - - // 注册高级合金炉配方 - for (var recipe : recipes) { - registry.addRecipe(new AdvancedAlloyFurnaceEmiRecipe(recipe)); - } - - // 注册催化剂信息 - registry.addRecipe(new CatalystInfoEmiRecipe()); - - // 注册配方催化剂 - registry.addWorkstation(AdvancedAlloyFurnaceRecipeCategory.CATEGORY, - EmiStack.of(new ItemStack(AdvancedAlloyFurnaceBlock.ADVANCED_ALLOY_FURNACE_BLOCK.get()))); - registry.addWorkstation(CatalystInfoCategory.CATEGORY, - EmiStack.of(new ItemStack(AdvancedAlloyFurnaceBlock.ADVANCED_ALLOY_FURNACE_BLOCK.get()))); - - // 添加各个等级的催化剂作为配方催化剂 - registry.addWorkstation(CatalystInfoCategory.CATEGORY, - EmiStack.of(new ItemStack(ModIngots.USELESS_INGOT_TIER_1.get()))); - registry.addWorkstation(CatalystInfoCategory.CATEGORY, - EmiStack.of(new ItemStack(ModIngots.USELESS_INGOT_TIER_2.get()))); - registry.addWorkstation(CatalystInfoCategory.CATEGORY, - EmiStack.of(new ItemStack(ModIngots.USELESS_INGOT_TIER_3.get()))); - registry.addWorkstation(CatalystInfoCategory.CATEGORY, - EmiStack.of(new ItemStack(ModIngots.USELESS_INGOT_TIER_4.get()))); - registry.addWorkstation(CatalystInfoCategory.CATEGORY, - EmiStack.of(new ItemStack(ModIngots.USELESS_INGOT_TIER_5.get()))); - registry.addWorkstation(CatalystInfoCategory.CATEGORY, - EmiStack.of(new ItemStack(ModIngots.USELESS_INGOT_TIER_6.get()))); - registry.addWorkstation(CatalystInfoCategory.CATEGORY, - EmiStack.of(new ItemStack(ModIngots.USELESS_INGOT_TIER_7.get()))); - registry.addWorkstation(CatalystInfoCategory.CATEGORY, - EmiStack.of(new ItemStack(ModIngots.USELESS_INGOT_TIER_8.get()))); - registry.addWorkstation(CatalystInfoCategory.CATEGORY, - EmiStack.of(new ItemStack(ModIngots.USELESS_INGOT_TIER_9.get()))); - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/compat/enderio/EnderIOTravelCompat.java b/src/main/java/com/sorrowmist/useless/compat/enderio/EnderIOTravelCompat.java new file mode 100644 index 00000000..3e65e26b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/enderio/EnderIOTravelCompat.java @@ -0,0 +1,30 @@ +package com.sorrowmist.useless.compat.enderio; + +import com.enderio.base.common.handler.TravelHandler; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; + +/** Ender IO 6.2 native travel calls; enabled tool recognition lives in EnderIOTravelHandlerMixin. */ +public final class EnderIOTravelCompat { + public static final String MOD_ID = "enderio"; + + private EnderIOTravelCompat() { + } + + public static Item.Properties markAsTravelItem(Item.Properties properties) { + return properties; // 1.20.1 has no travel-item component; recognition is type/setting based. + } + + public static void setTravelItemEnabled(ItemStack stack, boolean enabled) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set( + stack, com.sorrowmist.useless.core.component.UComponents.BeefTeleportEnabledComponent, enabled); + } + + public static InteractionResult tryAnchorTeleport(Level level, Player player) { + boolean success = TravelHandler.blockTeleport(level, player); + return success ? InteractionResult.SUCCESS : InteractionResult.PASS; + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/enderio/client/EnderIOTravelClientCompat.java b/src/main/java/com/sorrowmist/useless/compat/enderio/client/EnderIOTravelClientCompat.java new file mode 100644 index 00000000..25d2702b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/enderio/client/EnderIOTravelClientCompat.java @@ -0,0 +1,9 @@ +package com.sorrowmist.useless.compat.enderio.client; + +/** Ender IO 6.2 has no travel-staff key binding or held-key latch to clear. */ +public final class EnderIOTravelClientCompat { + private EnderIOTravelClientCompat() {} + public static void suppressTravelKey() { + // Native 6.2 travel uses item interaction; the UselessMod wheel consumes its own input. + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/exdeorum/ExDeorumCompat.java b/src/main/java/com/sorrowmist/useless/compat/exdeorum/ExDeorumCompat.java new file mode 100644 index 00000000..ec9e2d46 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/exdeorum/ExDeorumCompat.java @@ -0,0 +1,228 @@ +package com.sorrowmist.useless.compat.exdeorum; + +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.BlockTags; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.enchantment.Enchantments; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.storage.loot.LootParams; +import net.minecraft.world.level.storage.loot.parameters.LootContextParams; +import net.minecraft.world.phys.Vec3; +import thedarkcolour.exdeorum.loot.HammerLootModifier; +import thedarkcolour.exdeorum.recipe.RecipeUtil; +import thedarkcolour.exdeorum.recipe.crook.CrookRecipe; +import thedarkcolour.exdeorum.recipe.hammer.HammerRecipe; + +import java.util.ArrayList; +import java.util.List; + +/** + * 造化杖对 Ex Deorum 三种工具效果的兼容实现。 + * + *

Ex Deorum 的钩子、锤子与压缩锤子效果由 {@code GlobalLootModifier} 实现,触发条件是 + * 战利品上下文中的工具属于 {@code #exdeorum:crooks}、{@code #exdeorum:hammers} 或 + * {@code #exdeorum:compressed_hammers} 物品标签。造化杖不属于上述任一标签, + * 其破坏路径亦不构造战利品上下文,因此无法借助对方既有的修饰器生效, + * 只能由本层显式查询其配方缓存并在掉落阶段改写。

+ * + *

三种模式的掉落语义与 Ex Deorum 保持一致:锤子以配方产物替换原有掉落, + * 钩子在原有掉落之外追加配方产物。开关关闭、方块无对应配方或产物数量非正时, + * 一律原样返回既有掉落。

+ * + *

模组存在性守卫必须由调用方完成,且不得调用本类的任何成员。 + * 本类引用了 exdeorum 的类型,方法体与栈映射表在类验证阶段即须解析 {@link HammerRecipe} + * 等外部类型,未安装该模组时链接本类会抛出 {@code NoClassDefFoundError}。 + * 任何“先进入本类再判断”的写法都会先触发链接,使守卫失效。调用方须以 + * {@code ModList.get().isLoaded("exdeorum")} 短路,使本类仅在模组确已加载后才被链接, + * 与 {@link com.sorrowmist.useless.compat.ftbultimine.FtbUltimineChainKeyCompat} 采用同一策略。

+ */ +public final class ExDeorumCompat { + + public static final String MOD_ID = "exdeorum"; + + private ExDeorumCompat() { + } + + /** + * 按当前启用的 Ex Deorum 模式改写掉落。 + * + *

三个开关相互独立,可同时启用:锤子与压缩锤均以配方产物替换掉落, + * 命中顺序为「压缩锤优先」,因为压缩锤配方是其自身独立登记的数据; + * 钩子在最终掉落之上追加。锤子类模式与钩子同时启用时,钩子作用于替换后的结果。

+ * + *

调用方必须已完成模组存在性守卫;本方法不再自行判断。

+ * + * @param level 世界 + * @param state 被破坏的方块状态 + * @param original 原始掉落 + * @param tool 造化杖 + * @param hammer 是否启用锤子模式 + * @param compressedHammer 是否启用压缩锤模式 + * @param crook 是否启用钩子模式 + * @return 改写后的掉落列表 + */ + public static List rewriteDrops(ServerLevel level, BlockState state, List original, + ItemStack tool, + boolean hammer, boolean compressedHammer, boolean crook, + BlockPos pos, Player player) { + List result = original; + + if (hammer || compressedHammer) { + result = applyHammer(level, state, result, tool, compressedHammer); + } + if (crook) { + result = applyCrook(level, state, result, tool, pos, player); + } + return result; + } + + /** + * 按锤子或压缩锤配方改写掉落。 + * + *

压缩锤配方与普通锤配方是两套独立数据,同一方块可能只登记其中一套, + * 因此两者不可互相回退。

+ * + * @param level 世界 + * @param state 被破坏的方块状态 + * @param original 原始掉落 + * @param tool 造化杖(用于读取时运等级) + * @param compressed 是否查询压缩锤配方 + * @return 命中配方时为配方产物,否则为原始掉落 + */ + private static List applyHammer(ServerLevel level, BlockState state, List original, + ItemStack tool, boolean compressed) { + // 锤子配方以方块的物品形式为索引键;无法取得物品形式的方块没有配方可查 + Item itemForm = state.getBlock().asItem(); + if (itemForm == Items.AIR) { + return original; + } + + HammerRecipe recipe = compressed + ? RecipeUtil.getCompressedHammerRecipe(itemForm) + : RecipeUtil.getHammerRecipe(itemForm); + if (recipe == null) { + return original; + } + + int amount; + try { + // 产物数量由战利品数值提供器决定;此处不构造完整的战利品上下文, + // 仅提供世界参数,取值只依赖随机源与配置常量,缺失方块与工具参数不影响结果。 + amount = recipe.resultAmount.getInt(RecipeUtil.emptyLootContext(level)); + } catch (Throwable ignored) { + // 提供器需要战利品上下文中的其它参数时无法取值,此时保留原有掉落 + return original; + } + + // 时运加成:与 Ex Deorum 的 HammerLootModifier 一致。 + // 被列入时运黑名单的方块不享受加成,该判定必须在计算前进行, + // 否则黑名单方块会因时运附魔获得额外产出。 + if (getFortuneLevel(level, tool) > 0) { + amount += HammerLootModifier.calculateFortuneBonus( + tool, level.getRandom(), amount == 0); + } + + // 命中配方后一律以配方产物替换原有掉落。产出为非正数时结果为空列表而非保留原掉落, + // 与 Ex Deorum 的锤子一致:其修饰器命中配方后同样返回新建列表,产出为零时即为空掉落。 + // 该语义是整合包以零产出屏蔽特定方块的手段,不可改为回退原有掉落。 + if (amount <= 0) { + return List.of(); + } + + List result = new ArrayList<>(1); + ItemStack drop = new ItemStack(recipe.result, amount); + drop.setTag(recipe.getResultNbt()); + result.add(drop); + return result; + } + + /** + * 在既有掉落之上追加钩子配方产物。 + * + *

与锤子不同,钩子是「在方块原有掉落之外按概率追加」,因此保留 {@code original}。

+ * + *

钩子是不依赖附魔模式的独立开关:造化杖的精准采集与时运是并列的模式选项, + * 而非钩子生效的前置条件,若在此因精准采集模式短路,该开关将只在时运模式下有效。

+ * + *

时运提升重掷次数(每 3 级增加一次),树叶额外重掷原版掉落表, + * 两者均对齐 {@code CrookLootModifier} 的行为。

+ * + * @param level 世界 + * @param state 被破坏的方块状态 + * @param original 原始掉落 + * @param tool 造化杖 + * @return 追加产物后的掉落列表 + */ + private static List applyCrook(ServerLevel level, BlockState state, List original, + ItemStack tool, BlockPos pos, Player player) { + List recipes = RecipeUtil.getCrookRecipes(state); + boolean leaves = state.is(BlockTags.LEAVES); + if (recipes.isEmpty() && !leaves) { + return original; + } + + RandomSource random = level.getRandom(); + int fortune = getFortuneLevel(level, tool); + int rolls = Math.max(1, Mth.ceil(fortune / 3f)); + + List result = new ArrayList<>(original); + for (CrookRecipe recipe : recipes) { + for (int i = 0; i < rolls; i++) { + if (random.nextFloat() < recipe.chance()) { + ItemStack drop = new ItemStack(recipe.result()); + drop.setTag(recipe.getResultNbt()); + result.add(drop); + } + } + } + + // 树叶额外重掷原版掉落:钩子相对其它工具的核心收益即在于此 + if (leaves) { + for (int i = 0; i < rolls; i++) { + result.addAll(reRollLeafDrops(level, state, tool, pos, player)); + } + } + return result; + } + + /** + * 重掷树叶的原版掉落表。 + * + *

重掷使用的工具被替换为屏障,以阻止重掷过程再次进入钩子配方分支, + * 否则会形成递归并无限产出。

+ */ + private static List reRollLeafDrops(ServerLevel level, BlockState state, ItemStack tool, + BlockPos pos, Player player) { + try { + ItemStack nonCrook = new ItemStack(Items.BARRIER); + if (tool.hasTag()) nonCrook.setTag(tool.getTag().copy()); + LootParams.Builder builder = new LootParams.Builder(level) + .withParameter(LootContextParams.BLOCK_STATE, state) + .withParameter(LootContextParams.TOOL, nonCrook) + .withParameter(LootContextParams.ORIGIN, Vec3.atCenterOf(pos)); + if (player != null) { + builder.withParameter(LootContextParams.THIS_ENTITY, player); + } + return state.getDrops(builder); + } catch (Throwable ignored) { + // 单个方块的重掷失败不应影响整批掉落物 + return List.of(); + } + } + + /** 读取造化杖的时运附魔等级;无该附魔时返回 0。 */ + private static int getFortuneLevel(ServerLevel level, ItemStack tool) { + try { + return tool.getEnchantmentLevel( + Enchantments.BLOCK_FORTUNE); + } catch (Throwable ignored) { + return 0; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/expandeddelight/ExpandedDelightRecipeCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/expandeddelight/ExpandedDelightRecipeCompatLoader.java new file mode 100644 index 00000000..30d6c1e0 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/expandeddelight/ExpandedDelightRecipeCompatLoader.java @@ -0,0 +1,16 @@ +package com.sorrowmist.useless.compat.expandeddelight; + +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.expandeddelight.JuicerRecipeAdapter; + +/** Registers Expanded Delight's juicer recipe adapter when the optional mod is present. */ +public final class ExpandedDelightRecipeCompatLoader { + private ExpandedDelightRecipeCompatLoader() { + } + + public static void register() { + AlloyFurnaceRecipeManager.getInstance().registerAdapter( + new JuicerRecipeAdapter(), RecipeSourceIds.EXPANDED_DELIGHT); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/extendedae/ExtendedAEPlusJeiCompat.java b/src/main/java/com/sorrowmist/useless/compat/extendedae/ExtendedAEPlusJeiCompat.java new file mode 100644 index 00000000..bd7bb1ed --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/extendedae/ExtendedAEPlusJeiCompat.java @@ -0,0 +1,11 @@ +package com.sorrowmist.useless.compat.extendedae; + +import com.extendedae_plus.util.uploadPattern.ExtendedAEPatternUploadUtil; + +public final class ExtendedAEPlusJeiCompat { + private ExtendedAEPlusJeiCompat() { + } + public static void presetAlloyFurnaceSearchKey() { + ExtendedAEPatternUploadUtil.setLastProcessingName("Omni Alloy Furnace"); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/extradelight/ExtraDelightRecipeCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/extradelight/ExtraDelightRecipeCompatLoader.java new file mode 100644 index 00000000..a9f5762f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/extradelight/ExtraDelightRecipeCompatLoader.java @@ -0,0 +1,34 @@ +package com.sorrowmist.useless.compat.extradelight; + +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.extradelight.ChillerRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.delight.extradelight.DoughShapingRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.delight.extradelight.DryingRackRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.delight.extradelight.EvaporatorRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.delight.extradelight.JuicerRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.delight.extradelight.MeltingPotRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.delight.extradelight.MixingBowlRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.delight.extradelight.MortarRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.delight.extradelight.OvenRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.delight.extradelight.VatRecipeAdapter; + +/** Registers Extra Delight's workstation recipes when the optional mod is present. */ +public final class ExtraDelightRecipeCompatLoader { + private ExtraDelightRecipeCompatLoader() { + } + + public static void register() { + AlloyFurnaceRecipeManager manager = AlloyFurnaceRecipeManager.getInstance(); + manager.registerAdapter(new OvenRecipeAdapter(), RecipeSourceIds.EXTRA_DELIGHT); + manager.registerAdapter(new DryingRackRecipeAdapter(), RecipeSourceIds.EXTRA_DELIGHT); + manager.registerAdapter(new DoughShapingRecipeAdapter(), RecipeSourceIds.EXTRA_DELIGHT); + manager.registerAdapter(new MixingBowlRecipeAdapter(), RecipeSourceIds.EXTRA_DELIGHT); + manager.registerAdapter(new MortarRecipeAdapter(), RecipeSourceIds.EXTRA_DELIGHT); + manager.registerAdapter(new VatRecipeAdapter(), RecipeSourceIds.EXTRA_DELIGHT); + manager.registerAdapter(new MeltingPotRecipeAdapter(), RecipeSourceIds.EXTRA_DELIGHT); + manager.registerAdapter(new ChillerRecipeAdapter(), RecipeSourceIds.EXTRA_DELIGHT); + manager.registerAdapter(new EvaporatorRecipeAdapter(), RecipeSourceIds.EXTRA_DELIGHT); + manager.registerAdapter(new JuicerRecipeAdapter(), RecipeSourceIds.EXTRA_DELIGHT); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/extremereactors/ExtremeReactorsRecipeCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/extremereactors/ExtremeReactorsRecipeCompatLoader.java new file mode 100644 index 00000000..4d039dee --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/extremereactors/ExtremeReactorsRecipeCompatLoader.java @@ -0,0 +1,16 @@ +package com.sorrowmist.useless.compat.extremereactors; + +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.extremereactors.ExtremeReactorsRecipeAdapter; + +/** Registers Extreme Reactors adapters only after the optional mod has been detected. */ +public final class ExtremeReactorsRecipeCompatLoader { + private ExtremeReactorsRecipeCompatLoader() { + } + + public static void register() { + AlloyFurnaceRecipeManager.getInstance().registerAdapter( + new ExtremeReactorsRecipeAdapter(), RecipeSourceIds.BIG_REACTORS); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/farmersdelight/FarmersDelightRecipeCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/farmersdelight/FarmersDelightRecipeCompatLoader.java new file mode 100644 index 00000000..c1f79ff1 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/farmersdelight/FarmersDelightRecipeCompatLoader.java @@ -0,0 +1,21 @@ +package com.sorrowmist.useless.compat.farmersdelight; + +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.CookingPotRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.delight.CuttingBoardRecipeAdapter; + +/** Registers Farmer's Delight's shared recipe types after the optional mod is detected. */ +public final class FarmersDelightRecipeCompatLoader { + private FarmersDelightRecipeCompatLoader() { + } + + public static void register() { + AlloyFurnaceRecipeManager.getInstance().registerAdapter( + new CookingPotRecipeAdapter(), + RecipeSourceIds.FARMERS_DELIGHT); + AlloyFurnaceRecipeManager.getInstance().registerAdapter( + new CuttingBoardRecipeAdapter(), + RecipeSourceIds.FARMERS_DELIGHT); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/fluxnetworks/FluxNetworksEnergyCompat.java b/src/main/java/com/sorrowmist/useless/compat/fluxnetworks/FluxNetworksEnergyCompat.java new file mode 100644 index 00000000..19a1e472 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/fluxnetworks/FluxNetworksEnergyCompat.java @@ -0,0 +1,59 @@ +package com.sorrowmist.useless.compat.fluxnetworks; + +import com.sorrowmist.useless.energy.IEnergyManager; +import com.sorrowmist.useless.init.ModBlockEntities; +import com.sorrowmist.useless.compat.forge1201.capability.RegisterCapabilitiesEvent; +import sonar.fluxnetworks.api.FluxCapabilities; +import sonar.fluxnetworks.api.energy.IFNEnergyStorage; + +public final class FluxNetworksEnergyCompat { + public static final String MOD_ID = "fluxnetworks"; + + private FluxNetworksEnergyCompat() { + } + + public static void registerCapabilities(RegisterCapabilitiesEvent event) { + event.registerBlockEntity( + FluxCapabilities.FN_ENERGY_STORAGE, + ModBlockEntities.ADVANCED_ALLOY_FURNACE.get(), + (blockEntity, side) -> new FurnaceFluxEnergyStorage(blockEntity.getEnergyManager()) + ); + event.registerBlockEntity( + FluxCapabilities.FN_ENERGY_STORAGE, + ModBlockEntities.MULTIBLOCK_ALLOY_FURNACE_CORE.get(), + (blockEntity, side) -> new FurnaceFluxEnergyStorage(blockEntity.getEnergyManager()) + ); + } + + static record FurnaceFluxEnergyStorage(IEnergyManager energy) implements IFNEnergyStorage { + @Override + public long receiveEnergyL(long maxReceive, boolean simulate) { + return this.energy.receiveEnergy(maxReceive, simulate); + } + + @Override + public long extractEnergyL(long maxExtract, boolean simulate) { + return this.energy.extractEnergy(maxExtract, simulate); + } + + @Override + public long getEnergyStoredL() { + return this.energy.getEnergyStoredLong(); + } + + @Override + public long getMaxEnergyStoredL() { + return this.energy.getMaxEnergyStoredLong(); + } + + @Override + public boolean canExtract() { + return this.energy.canExtract(); + } + + @Override + public boolean canReceive() { + return this.energy.canReceive(); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forbiddenarcanus/ForbiddenArcanusRecipeCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/forbiddenarcanus/ForbiddenArcanusRecipeCompatLoader.java new file mode 100644 index 00000000..2458b840 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forbiddenarcanus/ForbiddenArcanusRecipeCompatLoader.java @@ -0,0 +1,16 @@ +package com.sorrowmist.useless.compat.forbiddenarcanus; + +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.forbiddenarcanus.HephaestusForgeRecipeAdapter; + +/** Registers Forbidden Arcanus integrations after the optional mod has loaded. */ +public final class ForbiddenArcanusRecipeCompatLoader { + private ForbiddenArcanusRecipeCompatLoader() { + } + + public static void register() { + AlloyFurnaceRecipeManager.getInstance().registerAdapter( + new HephaestusForgeRecipeAdapter(), RecipeSourceIds.FORBIDDEN_ARCANUS); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/ItemStackAmounts.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/ItemStackAmounts.java new file mode 100644 index 00000000..d62205b9 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/ItemStackAmounts.java @@ -0,0 +1,22 @@ +package com.sorrowmist.useless.compat.forge1201; + +import java.util.ArrayList; +import java.util.List; +import net.minecraft.world.item.ItemStack; + +/** Exact quantity expansion without int overflow, clamping loss, or shared mutable tags. */ +public final class ItemStackAmounts { + private ItemStackAmounts() {} + public static List multiply(ItemStack result,int inputs) { + if (result.isEmpty() || inputs<=0) return List.of(); + long remaining=(long)result.getCount()*inputs; + int limit=Math.max(1,result.getMaxStackSize()); + List stacks=new ArrayList<>(); + while(remaining>0) { + int count=(int)Math.min(remaining,limit); + stacks.add(result.copyWithCount(count)); + remaining-=count; + } + return stacks; + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/LongMath.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/LongMath.java new file mode 100644 index 00000000..ebe701db --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/LongMath.java @@ -0,0 +1,9 @@ +package com.sorrowmist.useless.compat.forge1201; + +/** Integer-preserving range clamps for quantities larger than a float can represent. */ +public final class LongMath { + private LongMath() {} + public static long clamp(long value, long minimum, long maximum) { + return Math.max(minimum, Math.min(maximum, value)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/OptionalIntegration.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/OptionalIntegration.java new file mode 100644 index 00000000..99cd5bf4 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/OptionalIntegration.java @@ -0,0 +1,21 @@ +package com.sorrowmist.useless.compat.forge1201; + +import java.lang.reflect.InvocationTargetException; +import net.minecraftforge.fml.ModList; + +/** Optional source boundaries: no class linkage when the corresponding mod is absent. */ +public final class OptionalIntegration { + private OptionalIntegration() {} + public static T call(String modId,String className,String method,Class resultType,T absent, + Class[] signature,Object... arguments) { + if (!ModList.get().isLoaded(modId)) return absent; + try { + Object result=Class.forName(className).getMethod(method,signature).invoke(null,arguments); + return resultType==Void.class ? null : resultType.cast(result); + } catch (InvocationTargetException e) { + throw new IllegalStateException("Integration "+modId+" failed in "+className+"#"+method,e.getCause()); + } catch (ReflectiveOperationException | LinkageError e) { + throw new IllegalStateException("Installed integration "+modId+" is unavailable in this build: "+className,e); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/capability/BlockCapabilityRegistry.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/capability/BlockCapabilityRegistry.java new file mode 100644 index 00000000..b3b8af01 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/capability/BlockCapabilityRegistry.java @@ -0,0 +1,60 @@ +package com.sorrowmist.useless.compat.forge1201.capability; + +import net.minecraft.core.Direction; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.capabilities.ICapabilityProvider; +import net.minecraftforge.common.util.LazyOptional; +import org.jetbrains.annotations.Nullable; +import java.util.*; +import java.util.function.BiFunction; + +/** Registers real Forge providers, not an alternate lookup map invisible to other mods. */ +public final class BlockCapabilityRegistry { + private final Map, Map, BiFunction>> entries = new IdentityHashMap<>(); + + public synchronized void register(Capability capability, BlockEntityType type, + BiFunction factory) { + Objects.requireNonNull(capability); Objects.requireNonNull(type); Objects.requireNonNull(factory); + var byCapability = entries.computeIfAbsent(type, ignored -> new IdentityHashMap<>()); + if (byCapability.containsKey(capability)) throw new IllegalStateException("Duplicate block capability registration: " + type); + byCapability.put(capability, (entity, side) -> factory.apply((E) entity, side)); + } + + @Nullable public synchronized Provider provider(BlockEntity entity) { + var factories = entries.get(entity.getType()); + return factories == null ? null : new Provider(entity, new IdentityHashMap<>(factories)); + } + + public static final class Provider implements ICapabilityProvider { + private final BlockEntity entity; + private final Map, BiFunction> factories; + private final Map cache = new HashMap<>(); + private Provider(BlockEntity entity, Map, BiFunction> factories) { + this.entity=entity; this.factories=factories; + } + @Override public synchronized LazyOptional getCapability(Capability capability, @Nullable Direction side) { + var factory = factories.get(capability); + if (factory == null || entity.isRemoved()) return LazyOptional.empty(); + Key key = new Key(capability, side); + Object value = factory.apply(entity, side); + Entry previous = cache.get(key); + if (previous != null && previous.value == value) return previous.optional.cast(); + // Sided access can change when the GUI changes I/O settings. Invalidate the old view. + cache.remove(key); + if (previous != null) previous.optional.invalidate(); + if (value == null) return LazyOptional.empty(); + var optional = LazyOptional.of(() -> value); + cache.put(key, new Entry(value, optional)); + return optional.cast(); + } + public synchronized void invalidate() { + List old = List.copyOf(cache.values()); + cache.clear(); + old.forEach(entry -> entry.optional.invalidate()); + } + private record Key(Capability capability, @Nullable Direction side) {} + private record Entry(Object value, LazyOptional optional) {} + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/capability/CapabilityAccess.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/capability/CapabilityAccess.java new file mode 100644 index 00000000..94dec3b5 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/capability/CapabilityAccess.java @@ -0,0 +1,35 @@ +package com.sorrowmist.useless.compat.forge1201.capability; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraftforge.common.capabilities.Capability; +import org.jetbrains.annotations.Nullable; + +/** Native 1.20.1 lookups: preserve sides and never load an unloaded chunk for automation. */ +public final class CapabilityAccess { + private CapabilityAccess() {} + @Nullable public static T get(Level level, Capability capability, BlockPos pos, + @Nullable Direction side) { + if (level == null || pos == null || !level.isLoaded(pos)) return null; + return fromEntity(level.getBlockEntity(pos), capability, side); + } + @Nullable public static T get(Level level, Capability capability, BlockPos pos, + @Nullable BlockState state, @Nullable BlockEntity entity, + @Nullable Direction side) { + if (level == null || pos == null || !level.isLoaded(pos)) return null; + // A cached entity from a previous block must not receive resources. + BlockEntity current = level.getBlockEntity(pos); + return fromEntity(current, capability, side); + } + @Nullable private static T fromEntity(@Nullable BlockEntity entity, Capability capability, + @Nullable Direction side) { + return entity == null || entity.isRemoved() ? null : entity.getCapability(capability, side).orElse(null); + } + @Nullable public static T get(ItemStack stack, Capability capability) { + return stack == null || stack.isEmpty() ? null : stack.getCapability(capability).orElse(null); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/capability/RegisterCapabilitiesEvent.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/capability/RegisterCapabilitiesEvent.java new file mode 100644 index 00000000..862054af --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/capability/RegisterCapabilitiesEvent.java @@ -0,0 +1,34 @@ +package com.sorrowmist.useless.compat.forge1201.capability; + +import net.minecraft.core.Direction; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.event.AttachCapabilitiesEvent; +import net.minecraftforge.eventbus.api.Event; +import net.minecraftforge.fml.event.IModBusEvent; +import java.util.function.BiFunction; + +/** Port-only registration event. Providers are attached using Forge's public capability event. */ +public final class RegisterCapabilitiesEvent extends Event implements IModBusEvent { + private static final BlockCapabilityRegistry REGISTRY = new BlockCapabilityRegistry(); + private static boolean installed; + public static synchronized void initialize() { + if (installed) return; + MinecraftForge.EVENT_BUS.addGenericListener(BlockEntity.class, RegisterCapabilitiesEvent::attach); + installed=true; + } + private static void attach(AttachCapabilitiesEvent event) { + var provider=REGISTRY.provider(event.getObject()); + if (provider != null) { + event.addCapability(new ResourceLocation("useless_mod", "forge1201_block_capabilities"), provider); + event.addListener(provider::invalidate); + } + } + public void registerBlockEntity(Capability capability, BlockEntityType type, + BiFunction factory) { + REGISTRY.register(capability, type, factory); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/client/PortButton.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/client/PortButton.java new file mode 100644 index 00000000..3ffdcb3b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/client/PortButton.java @@ -0,0 +1,31 @@ +package com.sorrowmist.useless.compat.forge1201.client; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.network.chat.Component; +import net.minecraft.util.Mth; + +/** Native 1.20.1 button: no newer AE2 widgets, sprite atlas or resource-pack assumptions. */ +public class PortButton extends Button { + public PortButton(int x,int y,int width,int height,Component message,OnPress onPress) { + super(x,y,width,height,message,onPress,DEFAULT_NARRATION); + } + protected final int withAlpha(int rgb) { return Mth.clamp(Mth.ceil(alpha*255),0,255)<<24 | rgb; } + protected void renderSurface(GuiGraphics graphics,boolean highlighted) { + int left=getX(),top=getY(),right=left+getWidth(),bottom=top+getHeight(); + int border=withAlpha(!active?0x343440:highlighted?0x9DC4DE:0x747488); + int fill=withAlpha(!active?0x282832:highlighted?0x354F65:0x3D3D4D); + graphics.fill(left,top,right,bottom,border); + if(getWidth()>2 && getHeight()>2) graphics.fill(left+1,top+1,right-1,bottom-1,fill); + if(isFocused() && active && getWidth()>4 && getHeight()>4) + graphics.fill(left+2,bottom-3,right-2,bottom-2,withAlpha(0xAED4ED)); + } + protected void renderLabel(GuiGraphics graphics,int color) { + renderScrollingString(graphics,Minecraft.getInstance().font,2,withAlpha(color)); + } + @Override protected void renderWidget(GuiGraphics graphics,int mouseX,int mouseY,float partialTick) { + renderSurface(graphics,isHoveredOrFocused()); + renderLabel(graphics,active?0xF2F2F2:0x888894); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/component/ComponentAccess.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/component/ComponentAccess.java new file mode 100644 index 00000000..9b15702f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/component/ComponentAccess.java @@ -0,0 +1,105 @@ +package com.sorrowmist.useless.compat.forge1201.component; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.nbt.Tag; +import net.minecraft.world.item.ItemStack; +import java.util.function.UnaryOperator; +import java.util.function.Supplier; + +/** Uses vanilla ItemStack NBT so Forge saves and synchronizes values without extra packets. */ +public final class ComponentAccess { + public static final String ROOT_KEY="useless_mod:components"; + public static final String REMOVED_KEY="useless_mod:removed_components"; + private ComponentAccess() {} + public static T readTag(CompoundTag root,DataComponentType key) { + if(root==null || !root.contains(ROOT_KEY,Tag.TAG_COMPOUND)) return null; + Tag value=root.getCompound(ROOT_KEY).get(key.id().toString()); + if(value==null) return null; + return key.codec().parse(NbtOps.INSTANCE,value).result().orElse(null); + } + public static void writeTag(CompoundTag root,DataComponentType key,T value) { + clearRemoval(root,key); + if(value==null) { removeTag(root,key); return; } + Tag encoded=key.codec().encodeStart(NbtOps.INSTANCE,value).getOrThrow(false, + message->{ throw new IllegalArgumentException("Invalid component "+key.id()+": "+message); }); + CompoundTag data=root.contains(ROOT_KEY,Tag.TAG_COMPOUND)?root.getCompound(ROOT_KEY):new CompoundTag(); + data.put(key.id().toString(),encoded); root.put(ROOT_KEY,data); + } + private static void removeTag(CompoundTag root,DataComponentType key) { + if(root==null || !root.contains(ROOT_KEY,Tag.TAG_COMPOUND)) return; + CompoundTag data=root.getCompound(ROOT_KEY); data.remove(key.id().toString()); + if(data.isEmpty()) root.remove(ROOT_KEY); + } + private static boolean isRemoved(CompoundTag root,DataComponentType key) { + return root!=null && root.getCompound(REMOVED_KEY).getBoolean(key.id().toString()); + } + private static void clearRemoval(CompoundTag root,DataComponentType key) { + if(root==null || !root.contains(REMOVED_KEY,Tag.TAG_COMPOUND)) return; + CompoundTag removed=root.getCompound(REMOVED_KEY); + removed.remove(key.id().toString()); + if(removed.isEmpty()) root.remove(REMOVED_KEY); + } + private static Object prototype(ItemStack stack,DataComponentType key) { + return stack.getItem() instanceof ComponentDefaults defaults ? defaults.componentDefault(key) : null; + } + @SuppressWarnings("unchecked") + public static T get(ItemStack stack,DataComponentType key) { + if(isRemoved(stack.getTag(),key)) return null; + T value=readTag(stack.getTag(),key); + if(value!=null) return value; + return (T) prototype(stack,key); + } + public static T getOrDefault(ItemStack stack,DataComponentType key,T fallback) { + T value=get(stack,key); return value==null?fallback:value; + } + public static boolean has(ItemStack stack,DataComponentType key) { + return get(stack,key)!=null; + } + public static T set(ItemStack stack,DataComponentType key,T value) { + T old=get(stack,key); + if(value==null) remove(stack,key); else writeTag(stack.getOrCreateTag(),key,value); + return old; + } + public static T remove(ItemStack stack,DataComponentType key) { + T old=get(stack,key); + removeTag(stack.getTag(),key); + if(prototype(stack,key)!=null) { + CompoundTag root=stack.getOrCreateTag(); + CompoundTag removed=root.contains(REMOVED_KEY,Tag.TAG_COMPOUND)?root.getCompound(REMOVED_KEY):new CompoundTag(); + removed.putBoolean(key.id().toString(),true); root.put(REMOVED_KEY,removed); + } else clearRemoval(stack.getTag(),key); + return old; + } + public static T update(ItemStack stack,DataComponentType key,T fallback,UnaryOperator change) { + T value=change.apply(getOrDefault(stack,key,fallback)); set(stack,key,value); return value; + } + /** Copies detached NBT, Forge capabilities and effective defaults across item variants. */ + public static void copyState(ItemStack source,ItemStack target, + Iterable>> types) { + if(source.isEmpty() || target.isEmpty()) return; + target.deserializeNBT(source.save(new CompoundTag())); + for(var type:types) copyValue(source,target,type.get()); + } + private static void copyValue(ItemStack source,ItemStack target,DataComponentType type) { + set(target,type,get(source,type)); + } + // AE keys are immutable/hash-stable. Decode from a detached stack so even a codec + // returning mutable NBT cannot let callers mutate the key held by the crafting CPU. + public static T get(appeng.api.stacks.AEItemKey key,DataComponentType type) { + return key==null?null:get(key.toStack(1),type); + } + public static T getOrDefault(appeng.api.stacks.AEItemKey key,DataComponentType type,T fallback) { + T value=get(key,type); return value==null?fallback:value; + } + public static boolean has(appeng.api.stacks.AEItemKey key,DataComponentType type) { + return key!=null && has(key.toStack(1),type); + } + // Deferred registry objects implement Supplier; resolve them at use time, not class loading. + public static T get(ItemStack stack,Supplier> key) { return get(stack,key.get()); } + public static T getOrDefault(ItemStack stack,Supplier> key,T fallback) { return getOrDefault(stack,key.get(),fallback); } + public static boolean has(ItemStack stack,Supplier> key) { return has(stack,key.get()); } + public static T set(ItemStack stack,Supplier> key,T value) { return set(stack,key.get(),value); } + public static T remove(ItemStack stack,Supplier> key) { return remove(stack,key.get()); } + public static T update(ItemStack stack,Supplier> key,T fallback,UnaryOperator change) { return update(stack,key.get(),fallback,change); } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/component/ComponentDefaults.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/component/ComponentDefaults.java new file mode 100644 index 00000000..61e928b7 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/component/ComponentDefaults.java @@ -0,0 +1,6 @@ +package com.sorrowmist.useless.compat.forge1201.component; + +/** Supplies the prototype values that native data components provide in 1.21. */ +public interface ComponentDefaults { + Object componentDefault(DataComponentType type); +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/component/DataComponentType.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/component/DataComponentType.java new file mode 100644 index 00000000..2ad1e96e --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/component/DataComponentType.java @@ -0,0 +1,26 @@ +package com.sorrowmist.useless.compat.forge1201.component; + +import com.mojang.serialization.Codec; +import net.minecraft.resources.ResourceLocation; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; + +/** A namespaced NBT key, not a nonexistent Minecraft 1.20.1 data-component registry. */ +public final class DataComponentType { + private final Codec codec; + private ResourceLocation id; + private DataComponentType(Codec codec) { this.codec=java.util.Objects.requireNonNull(codec,"Persistent codec"); } + public Codec codec() { return codec; } + public ResourceLocation id() { return java.util.Objects.requireNonNull(id,"Unregistered component key"); } + public void bind(ResourceLocation id) { + if(this.id!=null && !this.id.equals(id)) throw new IllegalStateException("Component already bound"); + this.id=id; + } + public static Builder builder() { return new Builder<>(); } + public static final class Builder { + private Codec codec; + public Builder persistent(Codec codec) { this.codec=codec; return this; } + public Builder networkSynchronized(StreamCodec codec) { return this; } + public Builder cacheEncoding() { return this; } + public DataComponentType build() { return new DataComponentType<>(codec); } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/enderio/FilledSoulIngredient.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/enderio/FilledSoulIngredient.java new file mode 100644 index 00000000..e2c2998d --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/enderio/FilledSoulIngredient.java @@ -0,0 +1,31 @@ +package com.sorrowmist.useless.compat.forge1201.enderio; + +import com.mojang.serialization.MapCodec; +import com.enderio.base.common.item.tool.SoulVialItem; +import com.sorrowmist.useless.compat.forge1201.recipe.CodecIngredient; +import com.sorrowmist.useless.compat.forge1201.recipe.CodecIngredientSerializer; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; + +/** An already-filled vial, retaining captured entity NBT instead of enumerating exact snapshots. */ +public final class FilledSoulIngredient extends CodecIngredient { + public static final MapCodec CODEC = ResourceLocation.CODEC.fieldOf("item") + .xmap(FilledSoulIngredient::new, value -> value.item); + public static final CodecIngredientSerializer TYPE = new CodecIngredientSerializer<>(CODEC.codec()); + private final ResourceLocation item; + public FilledSoulIngredient(ResourceLocation item) { this.item = java.util.Objects.requireNonNull(item); } + public static Ingredient of(Item item) { return new FilledSoulIngredient(BuiltInRegistries.ITEM.getKey(item)); } + @Override public boolean test(ItemStack stack) { + return stack != null && !stack.isEmpty() && item.equals(BuiltInRegistries.ITEM.getKey(stack.getItem())) + && !NativeSoulStorage.getBoundSoul(stack).isEmpty(); + } + @Override public ItemStack[] getItems() { + return SoulVialItem.getAllFilled().stream().filter(this::test).map(ItemStack::copy).toArray(ItemStack[]::new); + } + @Override public CodecIngredientSerializer getSerializer() { return TYPE; } + @Override public boolean equals(Object other) { return other instanceof FilledSoulIngredient value && item.equals(value.item); } + @Override public int hashCode() { return item.hashCode(); } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/enderio/NativeSoul.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/enderio/NativeSoul.java new file mode 100644 index 00000000..beb9afbb --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/enderio/NativeSoul.java @@ -0,0 +1,29 @@ +package com.sorrowmist.useless.compat.forge1201.enderio; + +import com.enderio.api.capability.StoredEntityData; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.EntityType; + +/** Ender IO 6's stored entity data, without inventing a 1.21 soul component. */ +public final class NativeSoul { + private final StoredEntityData data; + private NativeSoul(StoredEntityData data) { this.data = copyData(data); } + public static NativeSoul from(StoredEntityData data) { return new NativeSoul(data); } + public static NativeSoul empty() { return from(StoredEntityData.empty()); } + public static NativeSoul of(EntityType type) { return of(BuiltInRegistries.ENTITY_TYPE.getKey(type)); } + public static NativeSoul of(ResourceLocation id) { return from(StoredEntityData.of(id)); } + public boolean isEmpty() { return data.getEntityType().isEmpty(); } + public ResourceLocation entityTypeId() { return data.getEntityType().orElse(null); } + public EntityType entityType() { + ResourceLocation id = entityTypeId(); + return id != null && BuiltInRegistries.ENTITY_TYPE.containsKey(id) ? BuiltInRegistries.ENTITY_TYPE.get(id) : null; + } + public NativeSoul copy() { return from(data); } + public StoredEntityData storedData() { return copyData(data); } + private static StoredEntityData copyData(StoredEntityData data) { + var result = new StoredEntityData(); + if (data != null) result.deserializeNBT(data.serializeNBT().copy()); + return result; + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/enderio/NativeSoulStorage.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/enderio/NativeSoulStorage.java new file mode 100644 index 00000000..3527dc5f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/enderio/NativeSoulStorage.java @@ -0,0 +1,36 @@ +package com.sorrowmist.useless.compat.forge1201.enderio; + +import com.enderio.api.capability.IEntityStorage; +import com.enderio.base.common.init.EIOCapabilities; +import com.enderio.base.common.init.EIOItems; +import net.minecraft.world.item.ItemStack; + +/** Uses the native ENTITY_STORAGE Forge capability; output NBT stays visible to Ender IO. */ +public final class NativeSoulStorage { + private NativeSoulStorage() {} + private static IEntityStorage storage(ItemStack stack) { + return stack == null || stack.isEmpty() ? null : stack.getCapability(EIOCapabilities.ENTITY_STORAGE).orElse(null); + } + public static boolean canBindSoul(ItemStack stack) { return storage(stack) != null; } + public static NativeSoul getBoundSoul(ItemStack stack) { + return read(storage(stack)); + } + public static boolean tryBindSoul(ItemStack stack, NativeSoul soul) { + return bind(storage(stack), soul); + } + // Delegates kept separate so the native storage format can also be regression-tested + // without the Forge capability-token class transformer in a unit-test JVM. + static NativeSoul read(IEntityStorage storage) { + return storage == null || !storage.hasStoredEntity() + ? NativeSoul.empty() : NativeSoul.from(storage.getStoredEntityData()); + } + static boolean bind(IEntityStorage storage, NativeSoul soul) { + if (storage == null || soul == null || soul.isEmpty()) return false; + storage.setStoredEntityData(soul.storedData()); + return true; + } + public static ItemStack forSoul(NativeSoul soul) { + ItemStack result = EIOItems.FILLED_SOUL_VIAL.asStack(); + return tryBindSoul(result, soul) ? result : ItemStack.EMPTY; + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/fluid/CompoundFluidIngredient.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/fluid/CompoundFluidIngredient.java new file mode 100644 index 00000000..338c0f4a --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/fluid/CompoundFluidIngredient.java @@ -0,0 +1,6 @@ +package com.sorrowmist.useless.compat.forge1201.fluid; +import java.util.List; +public final class CompoundFluidIngredient { + private CompoundFluidIngredient() {} + public static FluidIngredient of(List choices) { return FluidIngredient.any(choices); } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/fluid/DataComponentFluidIngredient.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/fluid/DataComponentFluidIngredient.java new file mode 100644 index 00000000..7152dae0 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/fluid/DataComponentFluidIngredient.java @@ -0,0 +1,7 @@ +package com.sorrowmist.useless.compat.forge1201.fluid; +import net.minecraftforge.fluids.FluidStack; +/** Source compatibility name; predicates are implemented using native Forge fluid NBT. */ +public final class DataComponentFluidIngredient { + private DataComponentFluidIngredient() {} + public static FluidIngredient of(boolean strict,FluidStack stack) { return FluidIngredient.nbt(strict,stack); } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/fluid/FluidIngredient.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/fluid/FluidIngredient.java new file mode 100644 index 00000000..c49233e1 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/fluid/FluidIngredient.java @@ -0,0 +1,164 @@ +package com.sorrowmist.useless.compat.forge1201.fluid; + +import com.google.gson.*; +import com.mojang.serialization.Codec; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeCodecs; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtUtils; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.common.crafting.CraftingHelper; +import net.minecraftforge.fluids.FluidStack; +import java.util.*; +import java.util.function.Predicate; + +/** Immutable union of fluid IDs, live tag references, or exact/partial native NBT predicates. */ +public final class FluidIngredient implements Predicate { + public static final Codec CODEC = RecipeCodecs.jsonCodec(FluidIngredient::fromJson,FluidIngredient::toJson); + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf,value) -> buf.writeUtf(value.toJson().toString(),1_048_576), + buf -> fromJson(JsonParser.parseString(buf.readUtf(1_048_576)))); + private enum Kind { FLUID, TAG, SAME_FLUID, SAME_TAG, NBT, MERGE_NBT, ANY } + private final Kind kind; + private final ResourceLocation id; + private final CompoundTag nbt; + private final boolean strict; + private final List children; + private FluidIngredient(Kind kind, ResourceLocation id, CompoundTag nbt, boolean strict, List children) { + this.kind=kind; this.id=id; this.nbt=nbt==null?null:nbt.copy(); this.strict=strict; + this.children=List.copyOf(children); + } + public static FluidIngredient single(Fluid fluid) { + if (fluid==Fluids.EMPTY) return any(List.of()); + ResourceLocation id=BuiltInRegistries.FLUID.getKey(fluid); + if (id==null) throw new IllegalArgumentException("Unregistered fluid"); + return new FluidIngredient(Kind.FLUID,id,null,false,List.of()); + } + /** As with an ordinary item ingredient, single(stack) does not imply an NBT predicate. */ + public static FluidIngredient single(FluidStack stack) { return single(stack.getFluid()); } + public static FluidIngredient tag(TagKey tag) { + if (!tag.isFor(Registries.FLUID)) throw new IllegalArgumentException("Not a fluid tag"); + return new FluidIngredient(Kind.TAG,tag.location(),null,false,List.of()); + } + public static FluidIngredient nbt(boolean strict, FluidStack stack) { + if (stack.isEmpty()) throw new IllegalArgumentException("Empty NBT fluid ingredient"); + return new FluidIngredient(Kind.NBT,BuiltInRegistries.FLUID.getKey(stack.getFluid()),stack.getTag(),strict,List.of()); + } + public static FluidIngredient any(List choices) { + Objects.requireNonNull(choices); + return choices.size()==1?choices.get(0):new FluidIngredient(Kind.ANY,null,null,false,choices); + } + @Override public boolean test(FluidStack stack) { + if (stack==null || stack.isEmpty()) return false; + return switch(kind) { + case FLUID -> id.equals(BuiltInRegistries.FLUID.getKey(stack.getFluid())); + case SAME_FLUID -> BuiltInRegistries.FLUID.get(id).isSame(stack.getFluid()); + case SAME_TAG -> { + boolean matched=false; + for(var member:BuiltInRegistries.FLUID.getTagOrEmpty(TagKey.create(Registries.FLUID,id))) + if(member.value().isSame(stack.getFluid())) { matched=true; break; } + yield matched; + } + case TAG -> stack.getFluid().builtInRegistryHolder().is(TagKey.create(Registries.FLUID,id)); + case NBT -> id.equals(BuiltInRegistries.FLUID.getKey(stack.getFluid())) + && (strict ? Objects.equals(nbt,stack.getTag()) : NbtUtils.compareNbt(nbt,stack.getTag(),true)); + case MERGE_NBT -> id.equals(BuiltInRegistries.FLUID.getKey(stack.getFluid())) + && (nbt==null || nbt.isEmpty() || stack.getTag()!=null + && stack.getTag().copy().merge(nbt).equals(stack.getTag())); + case ANY -> children.stream().anyMatch(child -> child.test(stack)); + }; + } + /** A temporarily unbound/empty tag is NOT an empty expression and must survive reloads. */ + public boolean isEmpty() { return kind==Kind.ANY && children.stream().allMatch(FluidIngredient::isEmpty); } + /** Family predicates use the general path: a single representative omits flowing variants. */ + public boolean isSimple() { + return switch(kind) { + case FLUID, TAG -> true; + case ANY -> children.stream().allMatch(FluidIngredient::isSimple); + default -> false; + }; + } + public FluidStack[] getStacks() { + if(kind==Kind.ANY) return children.stream().flatMap(c -> Arrays.stream(c.getStacks())).toArray(FluidStack[]::new); + if(kind==Kind.TAG || kind==Kind.SAME_TAG) { + List result=new ArrayList<>(); + for(var holder:BuiltInRegistries.FLUID.getTagOrEmpty(TagKey.create(Registries.FLUID,id))) + if(holder.value()!=Fluids.EMPTY) result.add(new FluidStack(holder.value(),1)); + return result.toArray(FluidStack[]::new); + } + FluidStack stack=new FluidStack(BuiltInRegistries.FLUID.get(id),1); + if(nbt!=null) stack.setTag(nbt.copy()); + return stack.isEmpty()?new FluidStack[0]:new FluidStack[]{stack}; + } + public JsonElement toJson() { + if(kind==Kind.ANY) { + JsonArray array=new JsonArray(); children.forEach(child -> array.add(child.toJson())); return array; + } + JsonObject json=new JsonObject(); + json.addProperty((kind==Kind.TAG || kind==Kind.SAME_TAG)?"tag":"fluid",id.toString()); + if(kind==Kind.NBT || kind==Kind.MERGE_NBT) { + json.addProperty("type",kind==Kind.MERGE_NBT?"useless_mod:merge_nbt_fluid":"useless_mod:nbt_fluid"); json.addProperty("strict",strict); + if(nbt!=null) json.addProperty("nbt",com.sorrowmist.useless.compat.forge1201.recipe.NbtCanonical.snbt(nbt)); + } + if(kind==Kind.SAME_FLUID || kind==Kind.SAME_TAG) + json.addProperty("type",kind==Kind.SAME_TAG?"useless_mod:same_fluid_tag":"useless_mod:same_fluid"); + return json; + } + public static FluidIngredient fromJson(JsonElement value) { return parse(value,0); } + private static FluidIngredient parse(JsonElement value,int depth) { + if(depth>64) throw new JsonSyntaxException("Fluid ingredient nesting exceeds 64"); + if(value.isJsonArray()) { + if(value.getAsJsonArray().size()>4096) throw new JsonSyntaxException("Too many fluid choices"); + List result=new ArrayList<>(); + for(JsonElement child:value.getAsJsonArray()) result.add(parse(child,depth+1)); + return any(result); + } + if(!value.isJsonObject()) throw new JsonSyntaxException("Expected a fluid ingredient object/array"); + JsonObject json=value.getAsJsonObject(); + if(json.has("components") && json.getAsJsonObject("components").size()>0) + throw new JsonSyntaxException("Use native 1.20.1 NBT instead of components"); + String type=json.has("type")?json.get("type").getAsString():""; + if(type.equals("neoforge:compound") || type.equals("useless_mod:compound_fluid")) + return parse(json.get("children"),depth+1); + if(!type.isEmpty() && !type.equals("useless_mod:nbt_fluid") && !type.equals("useless_mod:merge_nbt_fluid") && !type.equals("useless_mod:same_fluid") && !type.equals("useless_mod:same_fluid_tag")) + throw new JsonSyntaxException("Unsupported fluid predicate type: "+type); + if(json.has("tag")) { + if(json.has("fluid") || json.has("nbt")) throw new JsonSyntaxException("Ambiguous fluid tag predicate"); + if(type.equals("useless_mod:same_fluid_tag")) + return new FluidIngredient(Kind.SAME_TAG,new ResourceLocation(json.get("tag").getAsString()),null,false,List.of()); + return tag(TagKey.create(Registries.FLUID,new ResourceLocation(json.get("tag").getAsString()))); + } + ResourceLocation id=new ResourceLocation(json.get("fluid").getAsString()); + if(!BuiltInRegistries.FLUID.containsKey(id) || BuiltInRegistries.FLUID.get(id)==Fluids.EMPTY) + throw new JsonSyntaxException("Unknown fluid ingredient: "+id); + if(type.equals("useless_mod:nbt_fluid") || type.equals("useless_mod:merge_nbt_fluid") || json.has("nbt")) { + CompoundTag tag=json.has("nbt")?CraftingHelper.getNBT(json.get("nbt")):null; + return new FluidIngredient(type.equals("useless_mod:merge_nbt_fluid")?Kind.MERGE_NBT:Kind.NBT,id,tag,!json.has("strict") || json.get("strict").getAsBoolean(),List.of()); + } + if(type.equals("useless_mod:same_fluid")) return new FluidIngredient(Kind.SAME_FLUID,id,null,false,List.of()); + return single(BuiltInRegistries.FLUID.get(id)); + } + /** Structural tag introspection without replacing NBT predicates by enumerated members. */ + public java.util.Optional>> tagOnlyTags() { + if(kind==Kind.TAG) return java.util.Optional.of(List.of(TagKey.create(Registries.FLUID,id))); + if(kind!=Kind.ANY || children.isEmpty()) return java.util.Optional.empty(); + var tags=new java.util.LinkedHashSet>(); + for(var child:children) { + var nested=child.tagOnlyTags(); + if(nested.isEmpty()) return java.util.Optional.empty(); + tags.addAll(nested.get()); + } + return java.util.Optional.of(List.copyOf(tags)); + } + @Override public boolean equals(Object other) { + return other instanceof FluidIngredient value && kind==value.kind && Objects.equals(id,value.id) + && Objects.equals(nbt,value.nbt) && strict==value.strict && children.equals(value.children); + } + @Override public int hashCode() { return Objects.hash(kind,id,nbt,strict,children); } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/fluid/FluidStacks.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/fluid/FluidStacks.java new file mode 100644 index 00000000..6715bfab --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/fluid/FluidStacks.java @@ -0,0 +1,8 @@ +package com.sorrowmist.useless.compat.forge1201.fluid; +import net.minecraftforge.fluids.FluidStack; +public final class FluidStacks { + private FluidStacks() {} + public static boolean same(FluidStack first,FluidStack second) { + return first.isFluidEqual(second); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/fluid/SizedFluidIngredient.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/fluid/SizedFluidIngredient.java new file mode 100644 index 00000000..4b89f556 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/fluid/SizedFluidIngredient.java @@ -0,0 +1,30 @@ +package com.sorrowmist.useless.compat.forge1201.fluid; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import com.sorrowmist.useless.compat.forge1201.network.*; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraftforge.fluids.FluidStack; +import java.util.Arrays; +import java.util.Objects; + +public record SizedFluidIngredient(FluidIngredient ingredient,int amount) { + public static final Codec CODEC=RecordCodecBuilder.create(instance -> instance.group( + FluidIngredient.CODEC.fieldOf("ingredient").forGetter(SizedFluidIngredient::ingredient), + Codec.intRange(1,Integer.MAX_VALUE).fieldOf("amount").forGetter(SizedFluidIngredient::amount) + ).apply(instance,SizedFluidIngredient::new)); + public static final StreamCodec STREAM_CODEC=StreamCodec.composite( + FluidIngredient.STREAM_CODEC,SizedFluidIngredient::ingredient, + ByteBufCodecs.VAR_INT,SizedFluidIngredient::amount,SizedFluidIngredient::new); + public SizedFluidIngredient { + Objects.requireNonNull(ingredient); + if(amount<=0) throw new IllegalArgumentException("Fluid amount must be positive"); + } + public static SizedFluidIngredient of(FluidStack stack) { + return new SizedFluidIngredient(stack.hasTag()?FluidIngredient.nbt(true,stack):FluidIngredient.single(stack),stack.getAmount()); + } + public boolean test(FluidStack stack) { return ingredient.test(stack) && stack.getAmount()>=amount; } + public FluidStack[] getFluids() { + return Arrays.stream(ingredient.getStacks()).map(s -> new FluidStack(s,amount)).toArray(FluidStack[]::new); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/jei/AEKeyBookmarkCatalog.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/jei/AEKeyBookmarkCatalog.java new file mode 100644 index 00000000..dd93826b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/jei/AEKeyBookmarkCatalog.java @@ -0,0 +1,67 @@ +package com.sorrowmist.useless.compat.forge1201.jei; + +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import com.sorrowmist.useless.compat.forge1201.recipe.NbtCanonical; +import net.minecraft.nbt.*; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.util.*; + +/** JEI 15 restores non-item bookmarks by UID lookup, not by decoding arbitrary NBT. + * Retain keys seen by the fallback ingredient before JEI reads bookmarks next time. + * Unknown addon keys remain opaque on disk, allowing the addon to be restored later. + */ +public final class AEKeyBookmarkCatalog { + private final Map keys = new LinkedHashMap<>(); + public static String uid(AEKey key) { return uid(key.toTagGeneric()); } + private static String uid(CompoundTag tag) { + // JEI uses '&' as an unescaped separator. Base64URL also avoids newlines and + // distinguishes full NBT and key types without relying on Java hashCode. + return "useless_mod:ae_key:" + Base64.getUrlEncoder().withoutPadding().encodeToString( + NbtCanonical.snbt(tag).getBytes(StandardCharsets.UTF_8)); + } + public boolean remember(AEKey key) { + var tag = key.toTagGeneric(); + return keys.putIfAbsent(uid(tag), tag.copy()) == null; + } + public int size() { return keys.size(); } + public List decoded() { + var result = new ArrayList(); + for (var tag : keys.values()) { + var key = AEKey.fromTagGeneric(tag.copy()); + if (key != null) result.add(new GenericStack(key, 1)); + } + return List.copyOf(result); + } + public void load(Path file) throws IOException { + if (!Files.exists(file)) { keys.clear(); return; } + CompoundTag root; + try (var in = Files.newInputStream(file)) { root = NbtIo.readCompressed(in); } + if (!root.contains("keys", Tag.TAG_LIST)) throw new IOException("Invalid AE key bookmark catalogue"); + var loaded = new LinkedHashMap(); + for (var entry : root.getList("keys", Tag.TAG_COMPOUND)) { + var tag = (CompoundTag) entry; + if (!tag.isEmpty()) loaded.put(uid(tag), tag.copy()); + } + keys.clear(); keys.putAll(loaded); + } + public void save(Path file) throws IOException { + file = file.toAbsolutePath(); + Files.createDirectories(file.getParent()); + var root = new CompoundTag(); var list = new ListTag(); + keys.values().forEach(tag -> list.add(tag.copy())); + root.putInt("version", 1); root.put("keys", list); + Path temp = Files.createTempFile(file.getParent(), file.getFileName().toString(), ".tmp"); + try { + try (var out = Files.newOutputStream(temp)) { NbtIo.writeCompressed(root, out); } + var backup = file.resolveSibling(file.getFileName() + ".bak"); + if (Files.exists(file) && !Files.exists(backup)) Files.copy(file, backup); + try { Files.move(temp, file, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); } + catch (AtomicMoveNotSupportedException ignored) { + Files.move(temp, file, StandardCopyOption.REPLACE_EXISTING); + } + } finally { Files.deleteIfExists(temp); } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/jei/AEKeyEmiStack.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/jei/AEKeyEmiStack.java new file mode 100644 index 00000000..1e937171 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/jei/AEKeyEmiStack.java @@ -0,0 +1,91 @@ +package com.sorrowmist.useless.compat.forge1201.jei; + +import appeng.api.stacks.*; +import com.google.gson.*; +import com.sorrowmist.useless.compat.jei.*; +import dev.emi.emi.api.EmiRegistry; +import dev.emi.emi.api.stack.*; +import dev.emi.emi.api.stack.serializer.EmiIngredientSerializer; +import dev.emi.emi.jemi.*; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.nbt.*; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.*; +import java.util.List; + +/** Scoped JEMI bridge: native recipe identity, full AE key persistence and long quantities. + * No EMI entrypoint: JEMI must continue importing this mod's JEI recipe categories. + */ +public final class AEKeyEmiStack extends JemiStack { + private final EmiStack nativeStack; + public AEKeyEmiStack(AEKeyJeiIngredients.KeyIngredient value) { + super(AEKeyJeiIngredients.TYPE, AEKeyJeiIngredients.HELPER, AEKeyJeiIngredients.RENDERER, value); + var converted = GenericStackJeiIngredientProviders.resolveNative(new GenericStack(value.key(), 1)); + nativeStack = converted == null ? null : JemiUtil.getStack(converted.type(), converted.value()); + amount = value.amount(); + } + @Override public AEKeyEmiStack copy() { + var copy = new AEKeyEmiStack(new AEKeyJeiIngredients.KeyIngredient(ingredient.key(), Math.max(1, amount))); + copy.amount = amount; + copy.chance = chance; + copy.comparison = comparison; + copy.setRemainder(getRemainder()); + return copy; + } + @Override public Object getKey() { return nativeStack == null ? super.getKey() : nativeStack.getKey(); } + @Override public CompoundTag getNbt() { return nativeStack == null ? null : nativeStack.getNbt(); } + @Override public ItemStack getItemStack() { + if (ingredient.key() instanceof AEItemKey item) return item.toStack((int)Math.min(Integer.MAX_VALUE, amount)); + return ItemStack.EMPTY; + } + @Override public List getTooltipText() { + return AEKeyJeiIngredients.RENDERER.getTooltip( + new AEKeyJeiIngredients.KeyIngredient(ingredient.key(), Math.max(1, amount)), TooltipFlag.NORMAL); + } + @Override public List getTooltip() { + // JEMI renders the immutable ingredient; rebuild its view after EMI changes the amount. + return amount == ingredient.amount() ? originalTooltip() : copy().originalTooltip(); + } + private List originalTooltip() { + return super.getTooltip(); + } + @Override public void render(GuiGraphics gui, int x, int y, float delta, int flags) { + if ((flags & 1) != 0) { + gui.pose().pushPose(); + gui.pose().translate(x, y, 0); + AEKeyJeiIngredients.RENDERER.render(gui, ingredient); + gui.pose().popPose(); + } + if ((flags & 2) != 0 && amount > 1) { + var font = net.minecraft.client.Minecraft.getInstance().font; + String text = ingredient.key().formatAmount(amount, AmountFormat.SLOT); + gui.pose().pushPose(); gui.pose().translate(0, 0, 200); + gui.drawString(font, text, x + 17 - font.width(text), y + 9, 0xffffff, true); + gui.pose().popPose(); + } + } + public static void register(EmiRegistry registry) { + registry.addIngredientSerializer(AEKeyEmiStack.class, new EmiIngredientSerializer() { + @Override public String getType() { return "useless_mod:ae_key"; } + @Override public JsonElement serialize(AEKeyEmiStack stack) { + var json = new JsonObject(); + json.addProperty("type", getType()); + json.addProperty("key", stack.ingredient.key().toTagGeneric().toString()); + json.addProperty("amount", stack.amount); + json.addProperty("chance", stack.chance); + return json; + } + @Override public EmiIngredient deserialize(JsonElement element) { + try { + var json = element.getAsJsonObject(); + var key = AEKey.fromTagGeneric(TagParser.parseTag(json.get("key").getAsString())); + long count = json.get("amount").getAsLong(); + if (key == null || count <= 0) return EmiStack.EMPTY; + var stack = new AEKeyEmiStack(new AEKeyJeiIngredients.KeyIngredient(key, count)); + if (json.has("chance")) stack.setChance(json.get("chance").getAsFloat()); + return stack; + } catch (Exception exception) { return EmiStack.EMPTY; } + } + }); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/mekanism/NativeStrictEnergyHandler.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/mekanism/NativeStrictEnergyHandler.java new file mode 100644 index 00000000..44f564a2 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/mekanism/NativeStrictEnergyHandler.java @@ -0,0 +1,68 @@ +package com.sorrowmist.useless.compat.forge1201.mekanism; + +import com.sorrowmist.useless.energy.IEnergyManager; +import mekanism.api.Action; +import mekanism.api.energy.IEnergyConversion; +import mekanism.api.energy.IStrictEnergyHandler; +import mekanism.api.math.FloatingLong; +import java.util.Objects; + +/** Mekanism 10.4 uses fractional unsigned joules; the furnace stores signed long FE. + * Never round a fractional FE up or report more native energy than was actually transferred. */ +public class NativeStrictEnergyHandler implements IStrictEnergyHandler { + private final IEnergyManager energy; + private final IEnergyConversion conversion; + public NativeStrictEnergyHandler(IEnergyManager energy, IEnergyConversion conversion) { + this.energy=Objects.requireNonNull(energy); this.conversion=Objects.requireNonNull(conversion); + } + @Override public int getEnergyContainerCount() { return 1; } + @Override public FloatingLong getEnergy(int container) { + return container==0 ? conversion.convertFrom(energy.getEnergyStoredLong()) : FloatingLong.ZERO; + } + @Override public void setEnergy(int container, FloatingLong amount) { + if(container==0) energy.setEnergyStored(conversion.convertToAsLong(amount)); + } + @Override public FloatingLong getMaxEnergy(int container) { + return container==0 ? conversion.convertFrom(energy.getMaxEnergyStoredLong()) : FloatingLong.ZERO; + } + @Override public FloatingLong getNeededEnergy(int container) { + return container==0 ? conversion.convertFrom(Math.max(0L,energy.getMaxEnergyStoredLong()-energy.getEnergyStoredLong())) : FloatingLong.ZERO; + } + @Override public FloatingLong insertEnergy(int container,FloatingLong amount,Action action) { + return container==0 ? insertEnergy(amount,action) : amount; + } + @Override public FloatingLong insertEnergy(FloatingLong amount,Action action) { + if(amount.isZero() || !energy.canReceive() || !conversion.isEnabled()) return amount; + long candidate=Math.max(0L,conversion.convertToAsLong(amount)); + long accepted=energy.receiveEnergy(candidate,true); + long fits=bounded(accepted,amount); + if(fits<=0) return amount; + long inserted=energy.receiveEnergy(fits,action.simulate()); + return amount.subtract(conversion.convertFrom(inserted)); + } + @Override public FloatingLong extractEnergy(int container,FloatingLong amount,Action action) { + return container==0 ? extractEnergy(amount,action) : FloatingLong.ZERO; + } + @Override public FloatingLong extractEnergy(FloatingLong amount,Action action) { + if(amount.isZero() || !energy.canExtract() || !conversion.isEnabled()) return FloatingLong.ZERO; + long candidate=Math.max(0L,conversion.convertToAsLong(amount)); + long available=energy.extractEnergy(candidate,true); + long fits=bounded(available,amount); + if(fits<=0) return FloatingLong.ZERO; + return conversion.convertFrom(energy.extractEnergy(fits,action.simulate())); + } + private long bounded(long candidate,FloatingLong limit) { + if(candidate<=0) return 0; + FloatingLong converted=conversion.convertFrom(candidate); + if(converted.isZero()) return 0; // Invalid conversion must not create free energy. + if(converted.smallerOrEqual(limit)) return candidate; + // Configurable conversion precision may not round-trip exactly. Find a conservative bound. + long low=0,high=candidate; + while(low Byte.MAX_VALUE) { + result.putInt("RealCount", stack.getCount()); + result.putByte("Count", (byte)1); + } + return result; + } + public static java.util.Optional parseItem(HolderLookup.Provider lookup, CompoundTag tag) { + ItemStack result=readItem(lookup,tag); + return result.isEmpty()?java.util.Optional.empty():java.util.Optional.of(result); + } + public static ItemStack readItem(HolderLookup.Provider ignored, CompoundTag tag) { + requireNative(tag); + ItemStack result = ItemStack.of(tag.copy()); + if (!result.isEmpty() && tag.contains("RealCount", Tag.TAG_ANY_NUMERIC)) { + result.setCount(Math.max(0, tag.getInt("RealCount"))); + } + return result; + } + public static CompoundTag saveFluid(FluidStack stack, HolderLookup.Provider ignored) { + return stack == null ? new CompoundTag() : stack.writeToNBT(new CompoundTag()); + } + public static FluidStack readFluid(HolderLookup.Provider ignored, CompoundTag tag) { + requireNative(tag); + return FluidStack.loadFluidStackFromNBT(tag.copy()); + } + private static void requireNative(CompoundTag tag) { + if (tag.contains("components", Tag.TAG_COMPOUND) && !tag.getCompound("components").isEmpty()) { + throw new IllegalArgumentException("Cannot silently discard non-native 1.21 component NBT"); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/network/ByteBufCodecs.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/network/ByteBufCodecs.java new file mode 100644 index 00000000..14d398ea --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/network/ByteBufCodecs.java @@ -0,0 +1,66 @@ +package com.sorrowmist.useless.compat.forge1201.network; + +import io.netty.buffer.ByteBuf; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtOps; +import com.mojang.serialization.Codec; +import java.util.*; +import java.util.function.*; + +public final class ByteBufCodecs { + private ByteBufCodecs() {} + private static FriendlyByteBuf wrap(ByteBuf b) { return b instanceof FriendlyByteBuf f ? f : new FriendlyByteBuf(b); } + public static final StreamCodec BOOL = StreamCodec.of(ByteBuf::writeBoolean, ByteBuf::readBoolean); + public static final StreamCodec INT = StreamCodec.of(ByteBuf::writeInt, ByteBuf::readInt); + public static final StreamCodec LONG = StreamCodec.of(ByteBuf::writeLong, ByteBuf::readLong); + public static final StreamCodec FLOAT = StreamCodec.of(ByteBuf::writeFloat, ByteBuf::readFloat); + public static final StreamCodec DOUBLE = StreamCodec.of(ByteBuf::writeDouble, ByteBuf::readDouble); + public static final StreamCodec BYTE = StreamCodec.of((b,v)->b.writeByte(v), ByteBuf::readByte); + public static final StreamCodec VAR_INT = StreamCodec.of((b,v)->wrap(b).writeVarInt(v), b->wrap(b).readVarInt()); + public static final StreamCodec VAR_LONG = StreamCodec.of((b,v)->wrap(b).writeVarLong(v), b->wrap(b).readVarLong()); + public static final StreamCodec STRING_UTF8 = StreamCodec.of((b,v)->wrap(b).writeUtf(v), b->wrap(b).readUtf()); + public static final StreamCodec COMPOUND_TAG = StreamCodec.of((b,v)->wrap(b).writeNbt(v), b->wrap(b).readNbt()); + private static int checkedSize(FriendlyByteBuf buffer) { + int size=buffer.readVarInt(); + if(size<0 || size>65536) throw new IllegalArgumentException("Invalid collection length: " + size); + return size; + } + public static Function,StreamCodec>> list() { + return codec -> collection(ArrayList::new,codec); + } + public static Function,StreamCodec>> list(int maximum) { + if(maximum<0 || maximum>65536) throw new IllegalArgumentException("Invalid list limit"); + return codec -> StreamCodec.of((buffer,values)-> { + if(values.size()>maximum) throw new IllegalArgumentException("List too large"); + wrap(buffer).writeVarInt(values.size()); for(V value:values) codec.encode(buffer,value); + }, buffer -> { + int size=checkedSize(wrap(buffer)); + if(size>maximum) throw new IllegalArgumentException("List too large"); + List values=new ArrayList<>(size); for(int i=0;i> StreamCodec collection(IntFunction factory,StreamCodec codec) { + return StreamCodec.of((b,values)-> { + if(values.size()>65536) throw new IllegalArgumentException("Collection too large"); + wrap(b).writeVarInt(values.size()); for(V value:values) codec.encode(b,value); + }, b->{ int n=checkedSize(wrap(b)); C values=factory.apply(n); for(int i=0;i StreamCodec> optional(StreamCodec codec) { + return StreamCodec.of((b,v)->{ b.writeBoolean(v.isPresent()); v.ifPresent(x->codec.encode(b,x)); }, + b->b.readBoolean()?Optional.of(codec.decode(b)):Optional.empty()); + } + public static StreamCodec fromCodecWithRegistries(Codec codec) { + return fromCodec(codec); + } + public static StreamCodec fromCodec(Codec codec) { + return StreamCodec.of((b,value)-> { + var tag=codec.encodeStart(NbtOps.INSTANCE,value).getOrThrow(false, message->{throw new IllegalArgumentException(message);}); + CompoundTag envelope=new CompoundTag(); envelope.put("value",tag); wrap(b).writeNbt(envelope); + }, b->{ + CompoundTag envelope=wrap(b).readNbt(); + if(envelope==null || !envelope.contains("value")) throw new IllegalArgumentException("Missing codec payload"); + return codec.parse(NbtOps.INSTANCE,envelope.get("value")).getOrThrow(false,message->{throw new IllegalArgumentException(message);}); + }); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/network/CustomPacketPayload.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/network/CustomPacketPayload.java new file mode 100644 index 00000000..aeeaaf23 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/network/CustomPacketPayload.java @@ -0,0 +1,8 @@ +package com.sorrowmist.useless.compat.forge1201.network; + +import net.minecraft.resources.ResourceLocation; +/** Named application payload; transported by a direction-checked Forge SimpleChannel. */ +public interface CustomPacketPayload { + Type type(); + record Type(ResourceLocation id) {} +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/network/IPayloadContext.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/network/IPayloadContext.java new file mode 100644 index 00000000..fc0affd5 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/network/IPayloadContext.java @@ -0,0 +1,17 @@ +package com.sorrowmist.useless.compat.forge1201.network; + +import net.minecraft.world.entity.player.Player; +import net.minecraftforge.network.NetworkEvent; +import java.util.concurrent.CompletableFuture; + +public final class IPayloadContext { + private final NetworkEvent.Context context; + IPayloadContext(NetworkEvent.Context context) { this.context=context; } + public Player player() { + return context.getSender()!=null ? context.getSender() : ClientAccess.player(); + } + public CompletableFuture enqueueWork(Runnable task) { return context.enqueueWork(task); } + private static final class ClientAccess { + static Player player() { return net.minecraft.client.Minecraft.getInstance().player; } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/network/PacketDistributor.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/network/PacketDistributor.java new file mode 100644 index 00000000..1d2f1f30 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/network/PacketDistributor.java @@ -0,0 +1,36 @@ +package com.sorrowmist.useless.compat.forge1201.network; + +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.resources.ResourceKey; + +/** Source-compatible sending facade; actual distribution is performed by Forge. */ +public final class PacketDistributor { + private PacketDistributor() {} + private static void send(net.minecraftforge.network.PacketDistributor.PacketTarget target, CustomPacketPayload... messages) { + for(var message:messages) PayloadRegistrar.channel(message).send(target,new PayloadRegistrar.Envelope(message)); + } + public static void sendToServer(CustomPacketPayload... messages) { + for(var message:messages) PayloadRegistrar.channel(message).sendToServer(new PayloadRegistrar.Envelope(message)); + } + public static void sendToPlayer(ServerPlayer player,CustomPacketPayload... messages) { + send(net.minecraftforge.network.PacketDistributor.PLAYER.with(()->player),messages); + } + public static void sendToAllPlayers(CustomPacketPayload... messages) { + send(net.minecraftforge.network.PacketDistributor.ALL.noArg(),messages); + } + public static void sendToPlayersInDimension(ResourceKey dimension,CustomPacketPayload... messages) { + send(net.minecraftforge.network.PacketDistributor.DIMENSION.with(()->dimension),messages); + } + public static void sendToPlayersTrackingChunk(LevelChunk chunk,CustomPacketPayload... messages) { + send(net.minecraftforge.network.PacketDistributor.TRACKING_CHUNK.with(()->chunk),messages); + } + public static void sendToPlayersTrackingChunk(net.minecraft.server.level.ServerLevel level,net.minecraft.world.level.ChunkPos pos,CustomPacketPayload... messages) { + sendToPlayersTrackingChunk(level.getChunk(pos.x,pos.z),messages); + } + public static void sendToPlayersTrackingEntityAndSelf(Entity entity,CustomPacketPayload... messages) { + send(net.minecraftforge.network.PacketDistributor.TRACKING_ENTITY_AND_SELF.with(()->entity),messages); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/network/PayloadRegistrar.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/network/PayloadRegistrar.java new file mode 100644 index 00000000..e6f91b3b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/network/PayloadRegistrar.java @@ -0,0 +1,78 @@ +package com.sorrowmist.useless.compat.forge1201.network; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraftforge.network.NetworkDirection; +import net.minecraftforge.network.NetworkRegistry; +import net.minecraftforge.network.simple.SimpleChannel; + +public final class PayloadRegistrar { + private final String modId; + private String version; + private SimpleChannel channel; + private final Map> entries=new ConcurrentHashMap<>(); + private static final Map CHANNELS=new ConcurrentHashMap<>(); + record Envelope(CustomPacketPayload payload) {} + private record Entry(StreamCodec codec, + BiConsumer handler, NetworkDirection direction) { + @SuppressWarnings("unchecked") void encode(FriendlyByteBuf buffer,CustomPacketPayload payload) { codec.encode(buffer,(T)payload); } + @SuppressWarnings("unchecked") void handle(CustomPacketPayload payload, IPayloadContext context) { handler.accept((T)payload,context); } + } + PayloadRegistrar(String modId,String version) { this.modId=modId; this.version=version; } + public PayloadRegistrar versioned(String version) { + if(channel!=null) throw new IllegalStateException("Channel already registered"); + this.version=version; return this; + } + private void ensureChannel() { + if(channel!=null) return; + String protocol="forge1201-"+version; + channel=NetworkRegistry.newSimpleChannel(new ResourceLocation(modId,"payloads_"+version.replaceAll("[^a-z0-9/._-]","_")), + ()->protocol, protocol::equals,protocol::equals); + channel.registerMessage(0,Envelope.class, (message,buffer)-> { + ResourceLocation id=message.payload().type().id(); buffer.writeResourceLocation(id); + Entry entry=entries.get(id); + if(entry==null) throw new IllegalArgumentException("Unregistered payload: "+id); + entry.encode(buffer,message.payload()); + }, buffer->{ + ResourceLocation id=buffer.readResourceLocation(); Entry entry=entries.get(id); + if(entry==null) throw new IllegalArgumentException("Unknown payload: "+id); + CustomPacketPayload payload=entry.codec().decode(buffer); + if(buffer.isReadable()) throw new IllegalArgumentException("Trailing payload bytes for "+id); + return new Envelope(payload); + }, (message,supplier)-> { + var context=supplier.get(); Entry entry=entries.get(message.payload().type().id()); + if(entry==null || entry.direction()!=context.getDirection()) + throw new IllegalArgumentException("Payload received in the wrong direction"); + // NeoForge payload handlers run on the game thread by default. Forge's + // SimpleChannel invokes this consumer on Netty; dispatch before touching + // screens, inventories or levels. Existing enqueueWork calls are safe here. + context.enqueueWork(() -> entry.handle(message.payload(),new IPayloadContext(context))); + context.setPacketHandled(true); + }); + } + private PayloadRegistrar register(CustomPacketPayload.Type type, + StreamCodec codec,BiConsumer handler, NetworkDirection direction) { + ensureChannel(); + if(entries.putIfAbsent(type.id(),new Entry<>(codec,handler,direction))!=null) + throw new IllegalStateException("Duplicate payload: "+type.id()); + if(CHANNELS.putIfAbsent(type.id(),channel)!=null) + throw new IllegalStateException("Payload registered on two channels: "+type.id()); + return this; + } + public PayloadRegistrar playToServer(CustomPacketPayload.Type type, + StreamCodec codec,BiConsumer handler) { + return register(type,codec,handler,NetworkDirection.PLAY_TO_SERVER); + } + public PayloadRegistrar playToClient(CustomPacketPayload.Type type, + StreamCodec codec,BiConsumer handler) { + return register(type,codec,handler,NetworkDirection.PLAY_TO_CLIENT); + } + static SimpleChannel channel(CustomPacketPayload payload) { + SimpleChannel value=CHANNELS.get(payload.type().id()); + if(value==null) throw new IllegalStateException("Payload not registered: "+payload.type().id()); + return value; + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/network/PortCodecs.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/network/PortCodecs.java new file mode 100644 index 00000000..5aeb7e59 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/network/PortCodecs.java @@ -0,0 +1,36 @@ +package com.sorrowmist.useless.compat.forge1201.network; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.core.BlockPos; +import net.minecraft.core.GlobalPos; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.fluids.FluidStack; +import java.util.UUID; + +public final class PortCodecs { + private PortCodecs() {} + public static final StreamCodec BLOCK_POS=StreamCodec.of(FriendlyByteBuf::writeBlockPos,FriendlyByteBuf::readBlockPos); + public static final StreamCodec RESOURCE_LOCATION=StreamCodec.of(FriendlyByteBuf::writeResourceLocation,FriendlyByteBuf::readResourceLocation); + public static final StreamCodec UUID_CODEC=StreamCodec.of((buf,id)->{buf.writeLong(id.getMostSignificantBits());buf.writeLong(id.getLeastSignificantBits());},buf->new UUID(buf.readLong(),buf.readLong())); + public static final StreamCodec ITEM_STACK_OPTIONAL=StreamCodec.of((buf,stack) -> { + buf.writeBoolean(!stack.isEmpty()); + if(!stack.isEmpty()) { + // Native writeItem/save encode count as a byte; machine recipe outputs can exceed 127. + buf.writeVarInt(stack.getCount()); + var tag=stack.serializeNBT(); tag.putByte("Count",(byte)1); buf.writeNbt(tag); + } + },buf -> { + if(!buf.readBoolean()) return ItemStack.EMPTY; + int count=buf.readVarInt(); + if(count<=0) throw new IllegalArgumentException("Invalid item count: "+count); + var tag=buf.readNbt(); + if(tag==null) throw new IllegalArgumentException("Missing item NBT"); + var stack=ItemStack.of(tag); + if(stack.isEmpty()) throw new IllegalArgumentException("Invalid or unknown item in payload"); + stack.setCount(count); return stack; + }); + public static final StreamCodec ITEM_STACK=ITEM_STACK_OPTIONAL; + public static final StreamCodec FLUID_STACK=StreamCodec.of((b,v)->v.writeToPacket(b),FluidStack::readFromPacket); + public static final StreamCodec GLOBAL_POS=ByteBufCodecs.fromCodec(GlobalPos.CODEC); +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/network/RegisterPayloadHandlersEvent.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/network/RegisterPayloadHandlersEvent.java new file mode 100644 index 00000000..ba8fe2cd --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/network/RegisterPayloadHandlersEvent.java @@ -0,0 +1,11 @@ +package com.sorrowmist.useless.compat.forge1201.network; + +import net.minecraftforge.eventbus.api.Event; +import net.minecraftforge.fml.event.IModBusEvent; + +/** Fired explicitly during common setup, after Forge has attached static mod subscribers. */ +public final class RegisterPayloadHandlersEvent extends Event implements IModBusEvent { + private final String modId; + public RegisterPayloadHandlersEvent(String modId) { this.modId=modId; } + public PayloadRegistrar registrar(String version) { return new PayloadRegistrar(modId,version); } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/network/StreamCodec.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/network/StreamCodec.java new file mode 100644 index 00000000..d0525b56 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/network/StreamCodec.java @@ -0,0 +1,63 @@ +package com.sorrowmist.useless.compat.forge1201.network; + +import java.util.function.*; +/** A buffer codec only; it deliberately does not add classes to Minecraft's packages. */ +public interface StreamCodec { + V decode(B buffer); + void encode(B buffer, V value); + static StreamCodec of(BiConsumer encoder, Function decoder) { + return new StreamCodec<>() { + public V decode(B b) { return decoder.apply(b); } + public void encode(B b,V v) { encoder.accept(b,v); } + }; + } + static StreamCodec ofMember(BiConsumer encoder, Function decoder) { + return of((b,v) -> encoder.accept(v,b), decoder); + } + static StreamCodec unit(V value) { return of((b,v)->{}, b->value); } + default StreamCodec map(Function from, Function to) { + return of((b,v)->encode(b,to.apply(v)), b->from.apply(decode(b))); + } + @SuppressWarnings("unchecked") default StreamCodec cast() { return (StreamCodec) this; } + default StreamCodec apply(Function, StreamCodec> transform) { return transform.apply(this); } + static StreamCodec composite(StreamCodec c1, Function f1, Factory1 factory) { + return of((b,v)->{ c1.encode(b,f1.apply(v)); }, b->factory.create(c1.decode(b))); + } + @FunctionalInterface interface Factory1 { V create(T1 v1); } + static StreamCodec composite(StreamCodec c1, Function f1, StreamCodec c2, Function f2, Factory2 factory) { + return of((b,v)->{ c1.encode(b,f1.apply(v)); c2.encode(b,f2.apply(v)); }, b->factory.create(c1.decode(b),c2.decode(b))); + } + @FunctionalInterface interface Factory2 { V create(T1 v1,T2 v2); } + static StreamCodec composite(StreamCodec c1, Function f1, StreamCodec c2, Function f2, StreamCodec c3, Function f3, Factory3 factory) { + return of((b,v)->{ c1.encode(b,f1.apply(v)); c2.encode(b,f2.apply(v)); c3.encode(b,f3.apply(v)); }, b->factory.create(c1.decode(b),c2.decode(b),c3.decode(b))); + } + @FunctionalInterface interface Factory3 { V create(T1 v1,T2 v2,T3 v3); } + static StreamCodec composite(StreamCodec c1, Function f1, StreamCodec c2, Function f2, StreamCodec c3, Function f3, StreamCodec c4, Function f4, Factory4 factory) { + return of((b,v)->{ c1.encode(b,f1.apply(v)); c2.encode(b,f2.apply(v)); c3.encode(b,f3.apply(v)); c4.encode(b,f4.apply(v)); }, b->factory.create(c1.decode(b),c2.decode(b),c3.decode(b),c4.decode(b))); + } + @FunctionalInterface interface Factory4 { V create(T1 v1,T2 v2,T3 v3,T4 v4); } + static StreamCodec composite(StreamCodec c1, Function f1, StreamCodec c2, Function f2, StreamCodec c3, Function f3, StreamCodec c4, Function f4, StreamCodec c5, Function f5, Factory5 factory) { + return of((b,v)->{ c1.encode(b,f1.apply(v)); c2.encode(b,f2.apply(v)); c3.encode(b,f3.apply(v)); c4.encode(b,f4.apply(v)); c5.encode(b,f5.apply(v)); }, b->factory.create(c1.decode(b),c2.decode(b),c3.decode(b),c4.decode(b),c5.decode(b))); + } + @FunctionalInterface interface Factory5 { V create(T1 v1,T2 v2,T3 v3,T4 v4,T5 v5); } + static StreamCodec composite(StreamCodec c1, Function f1, StreamCodec c2, Function f2, StreamCodec c3, Function f3, StreamCodec c4, Function f4, StreamCodec c5, Function f5, StreamCodec c6, Function f6, Factory6 factory) { + return of((b,v)->{ c1.encode(b,f1.apply(v)); c2.encode(b,f2.apply(v)); c3.encode(b,f3.apply(v)); c4.encode(b,f4.apply(v)); c5.encode(b,f5.apply(v)); c6.encode(b,f6.apply(v)); }, b->factory.create(c1.decode(b),c2.decode(b),c3.decode(b),c4.decode(b),c5.decode(b),c6.decode(b))); + } + @FunctionalInterface interface Factory6 { V create(T1 v1,T2 v2,T3 v3,T4 v4,T5 v5,T6 v6); } + static StreamCodec composite(StreamCodec c1, Function f1, StreamCodec c2, Function f2, StreamCodec c3, Function f3, StreamCodec c4, Function f4, StreamCodec c5, Function f5, StreamCodec c6, Function f6, StreamCodec c7, Function f7, Factory7 factory) { + return of((b,v)->{ c1.encode(b,f1.apply(v)); c2.encode(b,f2.apply(v)); c3.encode(b,f3.apply(v)); c4.encode(b,f4.apply(v)); c5.encode(b,f5.apply(v)); c6.encode(b,f6.apply(v)); c7.encode(b,f7.apply(v)); }, b->factory.create(c1.decode(b),c2.decode(b),c3.decode(b),c4.decode(b),c5.decode(b),c6.decode(b),c7.decode(b))); + } + @FunctionalInterface interface Factory7 { V create(T1 v1,T2 v2,T3 v3,T4 v4,T5 v5,T6 v6,T7 v7); } + static StreamCodec composite(StreamCodec c1, Function f1, StreamCodec c2, Function f2, StreamCodec c3, Function f3, StreamCodec c4, Function f4, StreamCodec c5, Function f5, StreamCodec c6, Function f6, StreamCodec c7, Function f7, StreamCodec c8, Function f8, Factory8 factory) { + return of((b,v)->{ c1.encode(b,f1.apply(v)); c2.encode(b,f2.apply(v)); c3.encode(b,f3.apply(v)); c4.encode(b,f4.apply(v)); c5.encode(b,f5.apply(v)); c6.encode(b,f6.apply(v)); c7.encode(b,f7.apply(v)); c8.encode(b,f8.apply(v)); }, b->factory.create(c1.decode(b),c2.decode(b),c3.decode(b),c4.decode(b),c5.decode(b),c6.decode(b),c7.decode(b),c8.decode(b))); + } + @FunctionalInterface interface Factory8 { V create(T1 v1,T2 v2,T3 v3,T4 v4,T5 v5,T6 v6,T7 v7,T8 v8); } + static StreamCodec composite(StreamCodec c1, Function f1, StreamCodec c2, Function f2, StreamCodec c3, Function f3, StreamCodec c4, Function f4, StreamCodec c5, Function f5, StreamCodec c6, Function f6, StreamCodec c7, Function f7, StreamCodec c8, Function f8, StreamCodec c9, Function f9, Factory9 factory) { + return of((b,v)->{ c1.encode(b,f1.apply(v)); c2.encode(b,f2.apply(v)); c3.encode(b,f3.apply(v)); c4.encode(b,f4.apply(v)); c5.encode(b,f5.apply(v)); c6.encode(b,f6.apply(v)); c7.encode(b,f7.apply(v)); c8.encode(b,f8.apply(v)); c9.encode(b,f9.apply(v)); }, b->factory.create(c1.decode(b),c2.decode(b),c3.decode(b),c4.decode(b),c5.decode(b),c6.decode(b),c7.decode(b),c8.decode(b),c9.decode(b))); + } + @FunctionalInterface interface Factory9 { V create(T1 v1,T2 v2,T3 v3,T4 v4,T5 v5,T6 v6,T7 v7,T8 v8,T9 v9); } + static StreamCodec composite(StreamCodec c1, Function f1, StreamCodec c2, Function f2, StreamCodec c3, Function f3, StreamCodec c4, Function f4, StreamCodec c5, Function f5, StreamCodec c6, Function f6, StreamCodec c7, Function f7, StreamCodec c8, Function f8, StreamCodec c9, Function f9, StreamCodec c10, Function f10, Factory10 factory) { + return of((b,v)->{ c1.encode(b,f1.apply(v)); c2.encode(b,f2.apply(v)); c3.encode(b,f3.apply(v)); c4.encode(b,f4.apply(v)); c5.encode(b,f5.apply(v)); c6.encode(b,f6.apply(v)); c7.encode(b,f7.apply(v)); c8.encode(b,f8.apply(v)); c9.encode(b,f9.apply(v)); c10.encode(b,f10.apply(v)); }, b->factory.create(c1.decode(b),c2.decode(b),c3.decode(b),c4.decode(b),c5.decode(b),c6.decode(b),c7.decode(b),c8.decode(b),c9.decode(b),c10.decode(b))); + } + @FunctionalInterface interface Factory10 { V create(T1 v1,T2 v2,T3 v3,T4 v4,T5 v5,T6 v6,T7 v7,T8 v8,T9 v9,T10 v10); } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/recipe/AE2Codecs.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/recipe/AE2Codecs.java new file mode 100644 index 00000000..afe1f772 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/recipe/AE2Codecs.java @@ -0,0 +1,31 @@ +package com.sorrowmist.useless.compat.forge1201.recipe; + +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; + +/** AE2 15's native key/stack formats include third-party key types and long quantities. */ +public final class AE2Codecs { + private AE2Codecs() {} + public static final StreamCodec GENERIC_STACK_STREAM = StreamCodec.of( + (buf, value) -> GenericStack.writeBuffer(value, buf), GenericStack::readBuffer); + public static final Codec GENERIC_STACK = CompoundTag.CODEC.comapFlatMap(tag -> { + try { + GenericStack value = GenericStack.readTag(tag); + return value == null ? DataResult.error(() -> "Unknown AE2 stack/key type: " + tag) : DataResult.success(value); + } catch (RuntimeException exception) { return DataResult.error(() -> "Invalid AE2 stack: " + exception.getMessage()); } + }, GenericStack::writeTag); + public static final Codec AE_KEY = CompoundTag.CODEC.comapFlatMap(tag -> { + try { + AEKey value = AEKey.fromTagGeneric(tag); + return value == null ? DataResult.error(() -> "Unknown AE2 key type: " + tag) : DataResult.success(value); + } catch (RuntimeException exception) { return DataResult.error(() -> "Invalid AE2 key: " + exception.getMessage()); } + }, AEKey::toTagGeneric); + public static final Codec ITEM_KEY = AE_KEY.comapFlatMap( + key -> key instanceof appeng.api.stacks.AEItemKey item ? DataResult.success(item) : DataResult.error(() -> "Expected an AE2 item key"), + item -> item); +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/recipe/CodecIngredient.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/recipe/CodecIngredient.java new file mode 100644 index 00000000..317f1dfc --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/recipe/CodecIngredient.java @@ -0,0 +1,14 @@ +package com.sorrowmist.useless.compat.forge1201.recipe; + +import com.google.gson.JsonElement; +import net.minecraftforge.common.crafting.AbstractIngredient; + +/** Native Forge custom ingredient whose predicates/display stacks may depend on reloadable data. */ +public abstract class CodecIngredient> extends AbstractIngredient { + @Override public final boolean isSimple() { return false; } + // An empty display list before dynamic registries load must not erase a valid predicate. + @Override public final boolean isEmpty() { return false; } + @Override public abstract CodecIngredientSerializer getSerializer(); + @SuppressWarnings("unchecked") + @Override public final JsonElement toJson() { return getSerializer().toJson((T)this); } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/recipe/CodecIngredientSerializer.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/recipe/CodecIngredientSerializer.java new file mode 100644 index 00000000..c7f8f1cc --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/recipe/CodecIngredientSerializer.java @@ -0,0 +1,29 @@ +package com.sorrowmist.useless.compat.forge1201.recipe; + +import com.google.gson.*; +import com.mojang.serialization.Codec; +import com.mojang.serialization.JsonOps; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraftforge.common.crafting.CraftingHelper; +import net.minecraftforge.common.crafting.IIngredientSerializer; + +public final class CodecIngredientSerializer> implements IIngredientSerializer { + private final Codec codec; + public CodecIngredientSerializer(Codec codec) { this.codec=codec; } + @Override public T parse(JsonObject json) { + return codec.parse(JsonOps.INSTANCE,json).getOrThrow(false,message -> { throw new JsonSyntaxException(message); }); + } + @Override public T parse(FriendlyByteBuf buffer) { + return parse(JsonParser.parseString(buffer.readUtf(1_048_576)).getAsJsonObject()); + } + @Override public void write(FriendlyByteBuf buffer,T ingredient) { + buffer.writeUtf(toJson(ingredient).toString(),1_048_576); + } + public JsonObject toJson(T ingredient) { + JsonObject json=codec.encodeStart(JsonOps.INSTANCE,ingredient) + .getOrThrow(false,message -> { throw new JsonSyntaxException(message); }).getAsJsonObject(); + var id=CraftingHelper.getID(this); + if(id==null) throw new IllegalStateException("Custom ingredient serializer has not been registered"); + json.addProperty("type",id.toString()); return json; + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/recipe/CraftingInputs.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/recipe/CraftingInputs.java new file mode 100644 index 00000000..1459dc28 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/recipe/CraftingInputs.java @@ -0,0 +1,22 @@ +package com.sorrowmist.useless.compat.forge1201.recipe; +import net.minecraft.core.NonNullList; +import net.minecraft.world.inventory.CraftingContainer; +import net.minecraft.world.inventory.TransientCraftingContainer; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import java.util.List; +/** Real native crafting container, including empty slots, dimensions and remainder semantics. */ +public final class CraftingInputs { + private CraftingInputs() {} + public static CraftingContainer of(int width,int height,List stacks) { + if(width<=0 || height<=0 || Math.multiplyExact(width,height)!=stacks.size()) + throw new IllegalArgumentException("Crafting grid dimensions do not match slot count"); + NonNullList copy=NonNullList.withSize(stacks.size(),ItemStack.EMPTY); + for(int i=0;i inputs, ItemStack result) { + public NativeCraftingPatternData { + inputs = inputs.stream().map(ItemStack::copy).toList(); + result = result.copy(); + } + @Override public List inputs() { return inputs.stream().map(ItemStack::copy).toList(); } + @Override public ItemStack result() { return result.copy(); } + @Nullable public static NativeCraftingPatternData read(AEItemKey definition) { + if (definition == null) return null; + var tag = definition.getTag(); + if (tag == null || !tag.contains("recipe", Tag.TAG_STRING) + || !tag.contains("in", Tag.TAG_LIST) || !tag.contains("out", Tag.TAG_COMPOUND)) return null; + var id = ResourceLocation.tryParse(tag.getString("recipe")); + var list = tag.getList("in", Tag.TAG_COMPOUND); + if (id == null || list.isEmpty() || list.size() > 9) return null; + var inputs = new ArrayList(list.size()); + boolean present = false; + for (int i = 0; i < list.size(); i++) { + var value = list.getCompound(i); + var stack = value.isEmpty() ? ItemStack.EMPTY : ItemStack.of(value); + if (!value.isEmpty() && stack.isEmpty()) return null; + inputs.add(stack); present |= !stack.isEmpty(); + } + var result = ItemStack.of(tag.getCompound("out")); + return !present || result.isEmpty() ? null : new NativeCraftingPatternData(id, inputs, result); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/recipe/NativeIngredients.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/recipe/NativeIngredients.java new file mode 100644 index 00000000..ed084d17 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/recipe/NativeIngredients.java @@ -0,0 +1,47 @@ +package com.sorrowmist.useless.compat.forge1201.recipe; + +import com.google.gson.JsonElement; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.crafting.Ingredient; +import java.util.Optional; + +/** Inspect native serialized values, not private Ingredient fields or materialized tag members. */ +public final class NativeIngredients { + private NativeIngredients() {} + public static Optional> directItemTag(Ingredient ingredient) { + if(ingredient==null || !ingredient.isVanilla()) return Optional.empty(); + JsonElement value=ingredient.toJson(); + if(!value.isJsonObject() || !value.getAsJsonObject().has("tag")) return Optional.empty(); + return Optional.of(TagKey.create(Registries.ITEM,new ResourceLocation(value.getAsJsonObject().get("tag").getAsString()))); + } + /** Pure native tag unions only; never infer a custom/NBT predicate from display items. */ + public static Optional>> itemTags(Ingredient ingredient) { + if(ingredient==null || !ingredient.isVanilla()) return Optional.empty(); + JsonElement value=ingredient.toJson(); + var elements=value.isJsonArray()?value.getAsJsonArray():java.util.List.of(value); + var tags=new java.util.LinkedHashSet>(); + for(JsonElement child:elements) { + if(!child.isJsonObject() || !child.getAsJsonObject().has("tag")) return Optional.empty(); + tags.add(TagKey.create(Registries.ITEM,new ResourceLocation(child.getAsJsonObject().get("tag").getAsString()))); + } + return tags.isEmpty()?Optional.empty():Optional.of(java.util.List.copyOf(tags)); + } + public static boolean hasTag(Ingredient ingredient) { + if(ingredient==null || !ingredient.isVanilla()) return false; + JsonElement value=ingredient.toJson(); + if(value.isJsonArray()) { + for(JsonElement child:value.getAsJsonArray()) + if(child.isJsonObject() && child.getAsJsonObject().has("tag")) return true; + return false; + } + return value.isJsonObject() && value.getAsJsonObject().has("tag"); + } + public static boolean isSingleItem(Ingredient ingredient) { + if(ingredient==null || !ingredient.isVanilla() || ingredient.isEmpty()) return false; + JsonElement value=ingredient.toJson(); + return value.isJsonObject() && value.getAsJsonObject().has("item"); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/recipe/NativeProcessingPatterns.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/recipe/NativeProcessingPatterns.java new file mode 100644 index 00000000..668ac1ec --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/recipe/NativeProcessingPatterns.java @@ -0,0 +1,51 @@ +package com.sorrowmist.useless.compat.forge1201.recipe; + +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.GenericStack; +import appeng.crafting.pattern.AEProcessingPattern; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.world.item.ItemStack; +import java.util.List; + +/** AE2 15 processing payload, read by its actual AEProcessingPattern constructor. + * Only in/out are copied to wrapper items; all other item NBT remains intact. + */ +public final class NativeProcessingPatterns { + private NativeProcessingPatterns() {} + public static boolean hasPayload(ItemStack stack) { + var tag = stack.getTag(); + return tag != null && tag.contains("in", Tag.TAG_LIST) && tag.contains("out", Tag.TAG_LIST); + } + public static void write(ItemStack target, List inputs, List outputs) { + var in = encode(inputs, AEProcessingPattern.MAX_INPUT_SLOTS); + var out = encode(outputs, AEProcessingPattern.MAX_OUTPUT_SLOTS); + // Validate both sides before mutating the destination. + var tag = target.getOrCreateTag(); + tag.put("in", in); tag.put("out", out); + } + public static void copyTo(ItemStack source, ItemStack target) { + if (!hasPayload(source)) throw new IllegalArgumentException("Missing native AE2 processing payload"); + var decoded = new AEProcessingPattern(AEItemKey.of(source)); + if (decoded.getInputs().length == 0 || decoded.getOutputs().length == 0) + throw new IllegalArgumentException("Empty native AE2 processing payload"); + var tag = target.getOrCreateTag(); + tag.put("in", source.getTag().getList("in", Tag.TAG_COMPOUND).copy()); + tag.put("out", source.getTag().getList("out", Tag.TAG_COMPOUND).copy()); + } + private static ListTag encode(List stacks, int maximum) { + if (stacks == null || stacks.size() > maximum) + throw new IllegalArgumentException("Native AE2 pattern exceeds " + maximum + " slots"); + var list = new ListTag(); + boolean present = false; + for (var stack : stacks) { + if (stack == null) { list.add(new CompoundTag()); continue; } + if (stack.what() == null || stack.amount() <= 0) + throw new IllegalArgumentException("Invalid native AE2 pattern stack"); + list.add(GenericStack.writeTag(stack)); present = true; + } + if (!present) throw new IllegalArgumentException("Native AE2 pattern side must contain at least one stack"); + return list; + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/recipe/NbtCanonical.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/recipe/NbtCanonical.java new file mode 100644 index 00000000..abd55b28 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/recipe/NbtCanonical.java @@ -0,0 +1,25 @@ +package com.sorrowmist.useless.compat.forge1201.recipe; + +import net.minecraft.nbt.*; +import java.util.TreeSet; +import java.util.StringJoiner; + +/** Parseable SNBT with sorted compound keys, preserving numeric types and ordered lists/arrays. */ +public final class NbtCanonical { + private NbtCanonical() {} + public static String snbt(Tag value) { + if(value==null) return "null"; + if(value instanceof CompoundTag compound) { + StringJoiner entries=new StringJoiner(",","{","}"); + for(String key:new TreeSet<>(compound.getAllKeys())) + entries.add(StringTag.quoteAndEscape(key)+":"+snbt(compound.get(key))); + return entries.toString(); + } + if(value instanceof ListTag list) { + StringJoiner entries=new StringJoiner(",","[","]"); + for(Tag entry:list) entries.add(snbt(entry)); + return entries.toString(); + } + return value.toString(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/recipe/RecipeCodecs.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/recipe/RecipeCodecs.java new file mode 100644 index 00000000..9c63a759 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/recipe/RecipeCodecs.java @@ -0,0 +1,97 @@ +package com.sorrowmist.useless.compat.forge1201.recipe; + +import com.google.gson.*; +import com.mojang.serialization.*; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.common.crafting.CraftingHelper; +import net.minecraftforge.fluids.FluidStack; +import java.util.function.Function; + +/** Native Forge JSON/packet serializers retain custom ingredients, NBT and tag references. */ +public final class RecipeCodecs { + private RecipeCodecs() {} + public static Codec jsonCodec(Function read, Function write) { + return Codec.PASSTHROUGH.flatXmap(dynamic -> { + try { return DataResult.success(read.apply(dynamic.convert(JsonOps.INSTANCE).getValue())); } + catch (RuntimeException error) { return DataResult.error(() -> "Invalid recipe data: " + error.getMessage()); } + }, value -> { + try { return DataResult.success(new Dynamic<>(JsonOps.INSTANCE, write.apply(value))); } + catch (RuntimeException error) { return DataResult.error(() -> "Cannot encode recipe data: " + error.getMessage()); } + }); + } + + public static final Codec INGREDIENT = jsonCodec(Ingredient::fromJson, Ingredient::toJson); + // Native Ingredient.toNetwork flattens item tags into a snapshot. Our named payload retains + // source tag references across client/server identities and delegates JSON predicates to Forge. + public static final StreamCodec INGREDIENT_STREAM = + StreamCodec.of((buf,value) -> buf.writeUtf(value.toJson().toString(),1_048_576), + buf -> Ingredient.fromJson(JsonParser.parseString(buf.readUtf(1_048_576)))); + public static final Codec ITEM_STACK = jsonCodec(RecipeCodecs::readItem, RecipeCodecs::writeItem); + public static final Codec FLUID_STACK = jsonCodec(RecipeCodecs::readFluid, RecipeCodecs::writeFluid); + + private static JsonObject object(JsonElement json) { + if (!json.isJsonObject()) throw new JsonSyntaxException("Expected a stack object"); + JsonObject result = json.getAsJsonObject(); + if (result.has("components") && result.getAsJsonObject("components").size()>0) + throw new JsonSyntaxException("1.21 components cannot be silently discarded; use 1.20.1 nbt"); + return result; + } + private static int positiveCount(JsonObject json, String primary, String fallback) { + JsonElement number = json.has(primary) ? json.get(primary) : json.get(fallback); + if (number == null) return 1; + try { + int result = number.getAsBigDecimal().intValueExact(); + if (result <= 0) throw new ArithmeticException("not positive"); + return result; + } catch (RuntimeException error) { throw new JsonSyntaxException("Invalid positive int " + primary, error); } + } + private static CompoundTag readNbt(JsonObject json) { + JsonElement nbt = json.has("nbt") ? json.get("nbt") : json.has("tag") ? json.get("tag") : json.get("Tag"); + return nbt == null ? null : CraftingHelper.getNBT(nbt); + } + public static ItemStack readItem(JsonElement value) { + JsonObject json = object(value); + String field = json.has("item") ? "item" : "id"; + ResourceLocation id = new ResourceLocation(json.get(field).getAsString()); + if (!BuiltInRegistries.ITEM.containsKey(id) || id.equals(new ResourceLocation("minecraft:air"))) + throw new JsonSyntaxException("Unknown/non-craftable item: " + id); + ItemStack result = new ItemStack(BuiltInRegistries.ITEM.get(id), positiveCount(json,"count","Count")); + CompoundTag nbt = readNbt(json); + if (nbt != null) result.setTag(nbt.copy()); + return result; + } + public static JsonElement writeItem(ItemStack stack) { + if (stack.isEmpty()) throw new JsonSyntaxException("Empty recipe item stack"); + JsonObject json = new JsonObject(); + json.addProperty("item",BuiltInRegistries.ITEM.getKey(stack.getItem()).toString()); + json.addProperty("count",stack.getCount()); + if (stack.hasTag()) json.addProperty("nbt",com.sorrowmist.useless.compat.forge1201.recipe.NbtCanonical.snbt(stack.getTag())); + return json; + } + public static FluidStack readFluid(JsonElement value) { + JsonObject json = object(value); + String field = json.has("fluid") ? "fluid" : json.has("id") ? "id" : "FluidName"; + ResourceLocation id = new ResourceLocation(json.get(field).getAsString()); + if (!BuiltInRegistries.FLUID.containsKey(id) || BuiltInRegistries.FLUID.get(id) == Fluids.EMPTY) + throw new JsonSyntaxException("Unknown/empty fluid: " + id); + FluidStack result = new FluidStack(BuiltInRegistries.FLUID.get(id),positiveCount(json,"amount","Amount")); + CompoundTag nbt = readNbt(json); + if (nbt != null) result.setTag(nbt.copy()); + return result; + } + public static JsonElement writeFluid(FluidStack stack) { + if (stack.isEmpty()) throw new JsonSyntaxException("Empty recipe fluid stack"); + JsonObject json = new JsonObject(); + json.addProperty("fluid",BuiltInRegistries.FLUID.getKey(stack.getFluid()).toString()); + json.addProperty("amount",stack.getAmount()); + if (stack.hasTag()) json.addProperty("nbt",com.sorrowmist.useless.compat.forge1201.recipe.NbtCanonical.snbt(stack.getTag())); + return json; + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/recipe/RecipeHolder.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/recipe/RecipeHolder.java new file mode 100644 index 00000000..0cadb7ba --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/recipe/RecipeHolder.java @@ -0,0 +1,15 @@ +package com.sorrowmist.useless.compat.forge1201.recipe; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.crafting.Recipe; + +/** The source manager's ID is retained verbatim, including KubeJS/runtime recipe IDs. */ +public record RecipeHolder>(ResourceLocation id,T value) { + public RecipeHolder { + java.util.Objects.requireNonNull(id,"Recipe ID"); + java.util.Objects.requireNonNull(value,"Recipe"); + } + public static > RecipeHolder of(T recipe) { + return new RecipeHolder<>(recipe.getId(),recipe); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/recipe/RecipeManagerAccess.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/recipe/RecipeManagerAccess.java new file mode 100644 index 00000000..9c7c039d --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/recipe/RecipeManagerAccess.java @@ -0,0 +1,33 @@ +package com.sorrowmist.useless.compat.forge1201.recipe; + +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.Container; +import net.minecraft.world.level.Level; +import net.minecraft.resources.ResourceLocation; +import java.util.*; + +/** No independent cache: a datapack/KubeJS reload is visible on the very next lookup. */ +public final class RecipeManagerAccess { + private RecipeManagerAccess() {} + public static Collection> getRecipes(RecipeManager manager) { + List> values=new ArrayList<>(); + for(Recipe recipe:manager.getRecipes()) values.add(RecipeHolder.of(recipe)); + return List.copyOf(values); + } + public static > List> getAllRecipesFor(RecipeManager manager,RecipeType type) { + List> result=new ArrayList<>(); + for(T recipe:manager.getAllRecipesFor(type)) result.add(RecipeHolder.of(recipe)); + return List.copyOf(result); + } + public static > List> getRecipesFor(RecipeManager manager,RecipeType type,C input,Level level) { + return manager.getRecipesFor(type,input,level).stream().map(RecipeHolder::of).toList(); + } + public static > Optional> getRecipeFor(RecipeManager manager,RecipeType type,C input,Level level) { + return manager.getRecipeFor(type,input,level).map(RecipeHolder::of); + } + public static Optional> byKey(RecipeManager manager,ResourceLocation id) { + return manager.byKey(id).map(recipe->new RecipeHolder<>(recipe.getId(),recipe)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/recipe/TopLevelNbtIngredient.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/recipe/TopLevelNbtIngredient.java new file mode 100644 index 00000000..51551b2c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/recipe/TopLevelNbtIngredient.java @@ -0,0 +1,35 @@ +package com.sorrowmist.useless.compat.forge1201.recipe; + +import com.mojang.serialization.MapCodec; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.item.ItemStack; +import java.util.Objects; + +/** HNN 5 matrix matching: extra top-level keys are allowed, nested values must be exactly equal. + * Native PartialNBTIngredient recursively allows extra nested keys, which is a different predicate. */ +public final class TopLevelNbtIngredient extends CodecIngredient { + public static final MapCodec CODEC = RecipeCodecs.ITEM_STACK.fieldOf("input") + .xmap(TopLevelNbtIngredient::new,TopLevelNbtIngredient::input); + public static final CodecIngredientSerializer TYPE=new CodecIngredientSerializer<>(CODEC.codec()); + private final ItemStack input; + public TopLevelNbtIngredient(ItemStack input) { + if(input==null||input.isEmpty()) throw new IllegalArgumentException("A matrix ingredient requires an item"); + this.input=input.copyWithCount(1); + } + public ItemStack input() {return input.copy();} + @Override public boolean test(ItemStack stack) { + if(stack==null||stack.isEmpty()||!stack.is(input.getItem())) return false; + if(!input.hasTag()) return true; + CompoundTag actual=stack.getTag(); + if(actual==null) return false; + CompoundTag required=input.getTag(); + for(String key:required.getAllKeys()) if(!Objects.equals(required.get(key),actual.get(key))) return false; + return true; + } + @Override public ItemStack[] getItems() {return new ItemStack[]{input.copy()};} + @Override public CodecIngredientSerializer getSerializer() {return TYPE;} + @Override public boolean equals(Object other) { + return other instanceof TopLevelNbtIngredient ingredient && ItemStack.isSameItemSameTags(input,ingredient.input); + } + @Override public int hashCode() {return Objects.hash(input.getItem(),input.getTag());} +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/registry/DeferredBlock.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/registry/DeferredBlock.java new file mode 100644 index 00000000..b2237930 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/registry/DeferredBlock.java @@ -0,0 +1,11 @@ +package com.sorrowmist.useless.compat.forge1201.registry; + +import net.minecraft.world.level.block.Block; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.ItemLike; +import net.minecraftforge.registries.RegistryObject; + +public final class DeferredBlock extends DeferredHolder implements ItemLike { + DeferredBlock(RegistryObject value) { super(value); } + @Override public Item asItem() { return get().asItem(); } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/registry/DeferredHolder.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/registry/DeferredHolder.java new file mode 100644 index 00000000..bdeafc71 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/registry/DeferredHolder.java @@ -0,0 +1,23 @@ +package com.sorrowmist.useless.compat.forge1201.registry; + +import java.util.function.Supplier; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.registries.RegistryObject; + +/** Typed supplier backed by Forge registration, not a second game registry. */ +public class DeferredHolder implements Supplier { + protected final RegistryObject delegate; + protected final Supplier local; + protected final ResourceLocation id; + protected DeferredHolder(RegistryObject delegate) { + this.delegate = delegate; this.id = delegate.getId(); this.local = null; + } + protected DeferredHolder(ResourceLocation id, Supplier supplier) { + this.delegate = null; this.id = id; + this.local = net.minecraftforge.common.util.Lazy.of(supplier); + } + @Override public T get() { return delegate != null ? delegate.get() : local.get(); } + public T value() { return get(); } + public ResourceLocation getId() { return id; } + public boolean isBound() { return delegate == null || delegate.isPresent(); } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/registry/DeferredItem.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/registry/DeferredItem.java new file mode 100644 index 00000000..00f2bc15 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/registry/DeferredItem.java @@ -0,0 +1,13 @@ +package com.sorrowmist.useless.compat.forge1201.registry; + +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.ItemLike; +import net.minecraftforge.registries.RegistryObject; + +public final class DeferredItem extends DeferredHolder implements ItemLike { + DeferredItem(RegistryObject value) { super(value); } + @Override public Item asItem() { return get(); } + public ItemStack toStack() { return new ItemStack(get()); } + public ItemStack toStack(int count) { return new ItemStack(get(), count); } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/forge1201/registry/DeferredRegister.java b/src/main/java/com/sorrowmist/useless/compat/forge1201/registry/DeferredRegister.java new file mode 100644 index 00000000..ed42d459 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/forge1201/registry/DeferredRegister.java @@ -0,0 +1,59 @@ +package com.sorrowmist.useless.compat.forge1201.registry; + +import java.util.function.Supplier; +import java.util.ArrayList; +import java.util.Collection; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.block.Block; +import net.minecraftforge.eventbus.api.IEventBus; + +/** Keep all game registrations on Forge's native DeferredRegister. */ +public class DeferredRegister { + protected final net.minecraftforge.registries.DeferredRegister delegate; + protected final String namespace; + protected final Collection> entries = new ArrayList<>(); + protected DeferredRegister(ResourceKey> key, String namespace) { + this.delegate = net.minecraftforge.registries.DeferredRegister.create(key, namespace); + this.namespace = namespace; + } + protected DeferredRegister(String namespace) { this.delegate=null; this.namespace=namespace; } + public static DeferredRegister createLocal(String namespace) { return new DeferredRegister<>(namespace); } + public static DeferredRegister create(ResourceKey> key, String namespace) { + return new DeferredRegister<>(key, namespace); + } + public static Items createItems(String namespace) { return new Items(namespace); } + public static Blocks createBlocks(String namespace) { return new Blocks(namespace); } + public DeferredHolder register(String name, Supplier factory) { + DeferredHolder holder; + if(delegate==null) { + ResourceLocation id=new ResourceLocation(namespace,name); + holder=new DeferredHolder<>(id,()-> { + T value=factory.get(); + if(value instanceof com.sorrowmist.useless.compat.forge1201.component.DataComponentType key) key.bind(id); + return value; + }); + } else holder = new DeferredHolder<>(delegate.register(name, factory)); + entries.add(holder); return holder; + } + public void register(IEventBus bus) { if(delegate!=null) delegate.register(bus); else entries.forEach(DeferredHolder::get); } + public Collection> getEntries() { return java.util.List.copyOf(entries); } + public String getNamespace() { return namespace; } + public static final class Items extends DeferredRegister { + private Items(String namespace) { super(Registries.ITEM, namespace); } + @Override public DeferredItem register(String name, Supplier factory) { + DeferredItem holder = new DeferredItem<>(delegate.register(name, factory)); + entries.add(holder); return holder; + } + } + public static final class Blocks extends DeferredRegister { + private Blocks(String namespace) { super(Registries.BLOCK, namespace); } + @Override public DeferredBlock register(String name, Supplier factory) { + DeferredBlock holder = new DeferredBlock<>(delegate.register(name, factory)); + entries.add(holder); return holder; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/ftbchunks/BlockEditGuard.java b/src/main/java/com/sorrowmist/useless/compat/ftbchunks/BlockEditGuard.java new file mode 100644 index 00000000..f958c734 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ftbchunks/BlockEditGuard.java @@ -0,0 +1,25 @@ +package com.sorrowmist.useless.compat.ftbchunks; + +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerPlayer; + +/** + * 方块编辑保护判定的桥接口。 + * + *

与 {@code SharedOwnerBridge} 同一套手法:本接口不含任何 FTB Chunks 的类型, + * 实现类由 {@link FtbChunksProtectionLoader} 反射加载。FTB Chunks 在 + * {@code build.gradle} 中仅为 {@code implementation} 依赖,未写入 + * {@code neoforge.mods.toml},运行时可能缺席,因此该隔离是必需的。

+ */ +public interface BlockEditGuard { + + /** + * 判定该玩家是否被允许在指定位置编辑方块。 + * + *

判定以单格为单位:一次建造或破坏可能跨越多个区块,各区块的归属队伍与 + * 权限设置并不相同,不能只对起点做一次判定。

+ * + * @return {@code true} 表示允许编辑;保护判定不可用时同样返回 {@code true}。 + */ + boolean isEditAllowed(ServerPlayer player, BlockPos pos); +} diff --git a/src/main/java/com/sorrowmist/useless/compat/ftbchunks/FtbChunksProtection.java b/src/main/java/com/sorrowmist/useless/compat/ftbchunks/FtbChunksProtection.java new file mode 100644 index 00000000..10aed095 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ftbchunks/FtbChunksProtection.java @@ -0,0 +1,40 @@ +package com.sorrowmist.useless.compat.ftbchunks; + +import dev.ftb.mods.ftbchunks.api.ClaimedChunkManager; +import dev.ftb.mods.ftbchunks.api.FTBChunksAPI; +import dev.ftb.mods.ftbchunks.api.Protection; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; + +/** + * {@link BlockEditGuard} 的 FTB Chunks 实现。 + * + *

只有本类(以及它引用的 FTB Chunks 类)会在安装了该模组时被加载。

+ * + *

复用 {@code ClaimedChunkManager#shouldPreventInteraction} 而不是自行组合区块查询与 + * 队伍权限判断:前者同时涵盖全局保护开关、管理员 bypass、荒野策略、假玩家策略与 + * 保护策略覆写,自行拼装的口径会随 FTB Chunks 版本演进产生偏差。

+ */ +public final class FtbChunksProtection implements BlockEditGuard { + + @Override + public boolean isEditAllowed(ServerPlayer player, BlockPos pos) { + try { + FTBChunksAPI.API api = FTBChunksAPI.api(); + if (!api.isManagerLoaded()) { + return true; + } + ClaimedChunkManager manager = api.getManager(); + if (manager == null) { + return true; + } + return !manager.shouldPreventInteraction( + player, InteractionHand.MAIN_HAND, pos, Protection.EDIT_BLOCK, null); + } catch (RuntimeException | LinkageError notReady) { + // FTB Chunks 的 API 在初始化前会抛 NPE,管理器在服务端启动阶段也尚未建立。 + // 保护判定不可用时不介入方块编辑。 + return true; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/ftbchunks/FtbChunksProtectionLoader.java b/src/main/java/com/sorrowmist/useless/compat/ftbchunks/FtbChunksProtectionLoader.java new file mode 100644 index 00000000..bedbff82 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ftbchunks/FtbChunksProtectionLoader.java @@ -0,0 +1,45 @@ +package com.sorrowmist.useless.compat.ftbchunks; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.sorrowmist.useless.UselessMod; +import net.minecraftforge.fml.ModList; +import org.jetbrains.annotations.Nullable; + +/** + * 按需加载 {@link FtbChunksProtection}。 + * + *

常驻类只引用 {@link BlockEditGuard} 这个不含 FTB Chunks 类型的接口,实现类用 + * {@code Class.forName} 反射加载,因此未安装 FTB Chunks 的整合包里加载本类不会 + * 触发外部类型解析。

+ */ +public final class FtbChunksProtectionLoader { + public static final String MOD_ID = "ftbchunks"; + + private static final String IMPLEMENTATION = + "com.sorrowmist.useless.compat.ftbchunks.FtbChunksProtection"; + + private static volatile boolean initialized; + private static volatile BlockEditGuard guard; + + private FtbChunksProtectionLoader() { + } + + /** FTB Chunks 方块编辑保护桥;未加载该模组或初始化失败时为 {@code null}。 */ + @Nullable + public static BlockEditGuard guard() { + if (!initialized) { + initialized = true; + if (ModList.get().isLoaded(MOD_ID)) { + try { + Class implementation = Class.forName( + IMPLEMENTATION, true, FtbChunksProtectionLoader.class.getClassLoader()); + guard = (BlockEditGuard) implementation.getDeclaredConstructor().newInstance(); + } catch (ReflectiveOperationException | LinkageError exception) { + RateLimitedLogger.error(UselessMod.LOGGER, "Failed to initialise the FTB Chunks protection bridge", exception); + } + } + } + return guard; + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/ftbultimine/FtbUltimineChainKeyCompat.java b/src/main/java/com/sorrowmist/useless/compat/ftbultimine/FtbUltimineChainKeyCompat.java new file mode 100644 index 00000000..59d1fa25 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ftbultimine/FtbUltimineChainKeyCompat.java @@ -0,0 +1,42 @@ +package com.sorrowmist.useless.compat.ftbultimine; + +import dev.ftb.mods.ftbultimine.client.FTBUltimineClient; +import net.minecraft.client.KeyMapping; + +/** + * 读取 FTB Ultimine 连锁键状态的兼容层。 + * + *

造化杖被加入 {@code #ftbultimine:excluded_tools} 后,FTB 不再对它实施连锁,但其按键绑定 + * 依然存在且默认绑定在重音符上。本模组直接读取该绑定的按下状态,而不是自行注册一个同键位的 + * 绑定:后者会在按键设置界面产生冲突提示,且玩家改动 FTB 的键位后必须同步改动两处。 + * + *

本类直接引用 FTB 客户端类型,JVM 在链接本类时即须解析这些类型,因此调用方必须先以 + * {@code ModList.get().isLoaded("ftbultimine")} 判定,再调用本类方法。该判定为假时后面的 + * {@code invokestatic} 不执行,本类不会被解析,也就不会抛出 {@link NoClassDefFoundError}; + * 缺少该判定的调用会在守卫位置抛出该异常。 + * + *

守卫处的模组 id 须写字面量,不得改为引用本类的常量:常量引用是否被内联为字符串字面量 + * 取决于字段是否满足 {@code static final} 与常量表达式两个条件,一旦某次改动使其退化为运行期 + * 求值,就会生成指向本类的 {@code getstatic},守卫随之失效,而编译期不会有任何提示。 + * + *

约束:判定必须写在调用方。不得由本类提供 isLoaded 之类的方法供调用方判定——调用本类的 + * 任何方法都会触发类链接,使守卫失效。 + * + *

本类隐含一项前置条件:只允许由客户端代码调用。其依赖的 {@code FTBUltimineClient} 仅存在于 + * 客户端发行版,专用服务器上不存在调用方,因而本类不会被解析,无需在守卫处重复判定物理端。 + */ +public final class FtbUltimineChainKeyCompat { + + private FtbUltimineChainKeyCompat() { + } + + /** + * 读取 FTB 连锁键的按下状态。 + * + *

该绑定由 FTB 在客户端构造时注册,正常不会为 null;判空仅用于覆盖注册尚未完成的极早时点。 + */ + public static boolean isChainKeyDown() { + KeyMapping key = FTBUltimineClient.keyBinding; + return key != null && key.isDown(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/immersiveengineering/ImmersiveEngineeringRecipeCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/immersiveengineering/ImmersiveEngineeringRecipeCompatLoader.java new file mode 100644 index 00000000..ad5768d4 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/immersiveengineering/ImmersiveEngineeringRecipeCompatLoader.java @@ -0,0 +1,16 @@ +package com.sorrowmist.useless.compat.immersiveengineering; + +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.immersiveengineering.ImmersiveEngineeringRecipeAdapter; + +/** Registers Immersive Engineering adapters only after the optional mod has been detected. */ +public final class ImmersiveEngineeringRecipeCompatLoader { + private ImmersiveEngineeringRecipeCompatLoader() { + } + + public static void register() { + AlloyFurnaceRecipeManager.getInstance().registerAdapter( + new ImmersiveEngineeringRecipeAdapter(), RecipeSourceIds.IMMERSIVE_ENGINEERING); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/industrialforegoing/IndustrialForegoingRecipeConverter.java b/src/main/java/com/sorrowmist/useless/compat/industrialforegoing/IndustrialForegoingRecipeConverter.java deleted file mode 100644 index 0a58ad93..00000000 --- a/src/main/java/com/sorrowmist/useless/compat/industrialforegoing/IndustrialForegoingRecipeConverter.java +++ /dev/null @@ -1,156 +0,0 @@ -package com.sorrowmist.useless.compat.industrialforegoing; - -import com.sorrowmist.useless.recipes.advancedalloyfurnace.AdvancedAlloyFurnaceRecipe; -import com.sorrowmist.useless.registry.ModIngots; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.crafting.Ingredient; -import net.minecraft.world.item.crafting.RecipeManager; -import net.minecraftforge.fluids.FluidStack; -import net.minecraftforge.registries.ForgeRegistries; - -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -/** - * 工业先锋配方转换器,用于将工业先锋系列模组的配方转换为高级合金炉配方 - */ -public class IndustrialForegoingRecipeConverter { - - /** - * 将工业先锋化学溶解室(DissolutionChamber)配方转换为高级合金炉配方 - * - * @param recipeManager 游戏配方管理器 - * @return 转换后的高级合金炉配方列表 - */ - public static List convertDissolutionChamberRecipes(RecipeManager recipeManager) { - List convertedRecipes = new ArrayList<>(); - - try { - Class dissolutionChamberRecipeClass = Class.forName("com.buuz135.industrial.recipe.DissolutionChamberRecipe"); - - Collection> allRecipes = recipeManager.getRecipes(); - - for (net.minecraft.world.item.crafting.Recipe recipe : allRecipes) { - if (dissolutionChamberRecipeClass.isInstance(recipe)) { - Object dissolutionRecipe = recipe; - - ResourceLocation originalId = recipe.getId(); - - java.lang.reflect.Field inputField = dissolutionChamberRecipeClass.getField("input"); - Ingredient.Value[] inputValues = (Ingredient.Value[]) inputField.get(dissolutionRecipe); - - java.lang.reflect.Field inputFluidField = dissolutionChamberRecipeClass.getField("inputFluid"); - FluidStack inputFluid = (FluidStack) inputFluidField.get(dissolutionRecipe); - - java.lang.reflect.Field processingTimeField = dissolutionChamberRecipeClass.getField("processingTime"); - int processingTime = (int) processingTimeField.get(dissolutionRecipe); - - java.lang.reflect.Field outputField = dissolutionChamberRecipeClass.getField("output"); - ItemStack outputItem = (ItemStack) outputField.get(dissolutionRecipe); - - java.lang.reflect.Field outputFluidField = dissolutionChamberRecipeClass.getField("outputFluid"); - FluidStack outputFluid = (FluidStack) outputFluidField.get(dissolutionRecipe); - - if (outputItem == null || outputItem.isEmpty()) { - continue; - } - - ResourceLocation newId = new ResourceLocation( - "useless_mod", - "if_dissolution_" + originalId.getNamespace() + "_" + originalId.getPath() - ); - - List inputIngredients = new ArrayList<>(); - List inputItemCounts = new ArrayList<>(); - - java.util.Map itemCountMap = new java.util.HashMap<>(); - - for (Ingredient.Value inputValue : inputValues) { - ItemStack[] items = inputValue.getItems().toArray(new ItemStack[0]); - if (items != null && items.length > 0) { - ItemStack item = items[0]; - net.minecraft.world.item.Item itemType = item.getItem(); - long count = item.getCount(); - - itemCountMap.put(itemType, itemCountMap.getOrDefault(itemType, 0L) + count); - } - } - - for (java.util.Map.Entry entry : itemCountMap.entrySet()) { - net.minecraft.world.item.Item item = entry.getKey(); - long count = entry.getValue(); - inputIngredients.add(Ingredient.of(item)); - inputItemCounts.add(count * 16); - } - - List outputItems = new ArrayList<>(); - ItemStack output64 = outputItem.copy(); - output64.setCount(outputItem.getCount() * 16); - outputItems.add(output64); - - List inputFluids = new ArrayList<>(); - if (inputFluid != null && !inputFluid.isEmpty()) { - FluidStack scaledInputFluid = inputFluid.copy(); - scaledInputFluid.setAmount(inputFluid.getAmount() * 16); - inputFluids.add(scaledInputFluid); - } - - List outputFluids = new ArrayList<>(); - if (outputFluid != null && !outputFluid.isEmpty()) { - FluidStack scaledOutputFluid = outputFluid.copy(); - scaledOutputFluid.setAmount(outputFluid.getAmount() * 16); - outputFluids.add(scaledOutputFluid); - } - - int energy = processingTime * 60; - int processTime = 40; - - Ingredient catalyst = Ingredient.of(ModIngots.USELESS_INGOT_TIER_1.get(), - ModIngots.USELESS_INGOT_TIER_2.get(), - ModIngots.USELESS_INGOT_TIER_3.get(), - ModIngots.USELESS_INGOT_TIER_4.get(), - ModIngots.USELESS_INGOT_TIER_5.get(), - ModIngots.USELESS_INGOT_TIER_6.get(), - ModIngots.USELESS_INGOT_TIER_7.get(), - ModIngots.USELESS_INGOT_TIER_8.get(), - ModIngots.USELESS_INGOT_TIER_9.get(), - ModIngots.USEFUL_INGOT.get()); - int catalystCount = 1; - - ResourceLocation dissolutionChamberId = ResourceLocation.fromNamespaceAndPath("industrialforegoing", "dissolution_chamber"); - net.minecraft.world.item.Item dissolutionChamber = ForgeRegistries.ITEMS.getValue(dissolutionChamberId); - if (dissolutionChamber == null) { - throw new RuntimeException("Could not find item: " + dissolutionChamberId); - } - Ingredient mold = Ingredient.of(dissolutionChamber); - - AdvancedAlloyFurnaceRecipe convertedRecipe = new AdvancedAlloyFurnaceRecipe( - newId, - inputIngredients, - inputItemCounts, - inputFluids, - outputItems, - outputFluids, - energy, - processTime, - catalyst, - catalystCount, - mold, - false, - false - ); - - convertedRecipes.add(convertedRecipe); - } - } - } catch (Exception e) { - e.printStackTrace(); - System.err.println("Failed to convert dissolution chamber recipes: " + e.getMessage()); - } - - return convertedRecipes; - } -} diff --git a/src/main/java/com/sorrowmist/useless/compat/ironsspellbooks/IronsSpellbooksRecipeCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/ironsspellbooks/IronsSpellbooksRecipeCompatLoader.java new file mode 100644 index 00000000..6886c824 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ironsspellbooks/IronsSpellbooksRecipeCompatLoader.java @@ -0,0 +1,19 @@ +package com.sorrowmist.useless.compat.ironsspellbooks; + +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.ironsspellbooks.AlchemistCauldronRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.ironsspellbooks.AlchemistCauldronDynamicRecipeAdapter; + +/** Registers Iron's Spells 'n Spellbooks recipe compatibility. */ +public final class IronsSpellbooksRecipeCompatLoader { + private IronsSpellbooksRecipeCompatLoader() { + } + + public static void register() { + AlloyFurnaceRecipeManager.getInstance().registerAdapter( + new AlchemistCauldronRecipeAdapter(), RecipeSourceIds.IRONS_SPELLBOOKS); + AlloyFurnaceRecipeManager.getInstance().registerAdapter( + new AlchemistCauldronDynamicRecipeAdapter(), RecipeSourceIds.IRONS_SPELLBOOKS); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/itemobliterator/ItemObliteratorProtection.java b/src/main/java/com/sorrowmist/useless/compat/itemobliterator/ItemObliteratorProtection.java new file mode 100644 index 00000000..adcbe2be --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/itemobliterator/ItemObliteratorProtection.java @@ -0,0 +1,22 @@ +package com.sorrowmist.useless.compat.itemobliterator; + +import com.sorrowmist.useless.UselessMod; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.item.ItemStack; + +public final class ItemObliteratorProtection { + private static final String NAMESPACE_PREFIX = UselessMod.MODID + ":"; + + private ItemObliteratorProtection() { + } + + public static boolean isProtectedItemId(String itemId) { + return itemId != null && itemId.startsWith(NAMESPACE_PREFIX); + } + + public static boolean isProtected(ItemStack stack) { + if (stack == null || stack.isEmpty()) return false; + var itemId = BuiltInRegistries.ITEM.getKey(stack.getItem()); + return itemId != null && isProtectedItemId(itemId.toString()); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/jade/AdvancedAlloyFurnaceJadeProvider.java b/src/main/java/com/sorrowmist/useless/compat/jade/AdvancedAlloyFurnaceJadeProvider.java new file mode 100644 index 00000000..e642bfbc --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/jade/AdvancedAlloyFurnaceJadeProvider.java @@ -0,0 +1,94 @@ +package com.sorrowmist.useless.compat.jade; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.blockentities.AdvancedAlloyFurnaceBlockEntity; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.AdvancedAlloyFurnaceAeManager; +import net.minecraft.ChatFormatting; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import snownee.jade.api.BlockAccessor; +import snownee.jade.api.IBlockComponentProvider; +import snownee.jade.api.IServerDataProvider; +import snownee.jade.api.ITooltip; +import snownee.jade.api.config.IPluginConfig; + +public enum AdvancedAlloyFurnaceJadeProvider implements IBlockComponentProvider, IServerDataProvider { + INSTANCE; + + static final ResourceLocation UID = UselessMod.id("advanced_alloy_furnace_tasks"); + private static final String TAG_TASKS = "UselessModAeTasks"; + private static final String TAG_PRODUCT = "Product"; + private static final String TAG_PROGRESS = "Progress"; + private static final String TAG_MAX_PROGRESS = "MaxProgress"; + private static final String TAG_TOTAL_OUTPUT = "TotalOutput"; + private static final String TAG_STATUS_KEY = "StatusKey"; + private static final String TAG_STATUS_DETAIL = "StatusDetail"; + + @Override + public void appendTooltip(ITooltip tooltip, BlockAccessor accessor, IPluginConfig config) { + ListTag tasks = accessor.getServerData().getList(TAG_TASKS, Tag.TAG_COMPOUND); + if (tasks.isEmpty()) { + return; + } + + tooltip.add(Component.translatable("gui.useless_mod.advanced_alloy_furnace.ae_tasks", tasks.size()) + .withStyle(ChatFormatting.DARK_PURPLE)); + for (int i = 0; i < tasks.size(); i++) { + CompoundTag task = tasks.getCompound(i); + String productName = task.getString(TAG_PRODUCT); + int progress = task.getInt(TAG_PROGRESS); + int maxProgress = task.getInt(TAG_MAX_PROGRESS); + long totalOutput = task.getLong(TAG_TOTAL_OUTPUT); + String statusKey = task.getString(TAG_STATUS_KEY); + String statusDetail = task.getString(TAG_STATUS_DETAIL); + Component product = Component.literal(productName); + Component status = Component.translatable(statusKey); + + if (maxProgress > 1) { + tooltip.add(Component.translatable("gui.useless_mod.advanced_alloy_furnace.ae_task_progress_ticks", + product, totalOutput, progress, maxProgress, status)); + } else if (!statusDetail.isBlank()) { + tooltip.add(Component.translatable("gui.useless_mod.advanced_alloy_furnace.ae_task_waiting_detail", + product, totalOutput, status, createDetailComponent(statusDetail)).withStyle(ChatFormatting.YELLOW)); + } else { + tooltip.add(Component.translatable("gui.useless_mod.advanced_alloy_furnace.ae_task_waiting", + product, totalOutput, status).withStyle(ChatFormatting.YELLOW)); + } + } + } + + private Component createDetailComponent(String statusDetail) { + if (statusDetail.startsWith("gui.useless_mod.")) { + return Component.translatable(statusDetail); + } + return Component.literal(statusDetail); + } + + @Override + public void appendServerData(CompoundTag data, BlockAccessor accessor) { + if (!(accessor.getBlockEntity() instanceof AdvancedAlloyFurnaceBlockEntity furnace)) { + return; + } + + ListTag tasks = new ListTag(); + for (AdvancedAlloyFurnaceAeManager.AETaskProgress progress : furnace.getAETaskProgressList()) { + CompoundTag task = new CompoundTag(); + task.putString(TAG_PRODUCT, progress.getProductName()); + task.putInt(TAG_PROGRESS, progress.getProgress()); + task.putInt(TAG_MAX_PROGRESS, progress.getMaxProgress()); + task.putLong(TAG_TOTAL_OUTPUT, progress.getTotalOutputCount()); + task.putString(TAG_STATUS_KEY, progress.getStatusKey()); + task.putString(TAG_STATUS_DETAIL, progress.getStatusDetail()); + tasks.add(task); + } + data.put(TAG_TASKS, tasks); + } + + @Override + public ResourceLocation getUid() { + return UID; + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/jade/JadePatternStorageSnapshot.java b/src/main/java/com/sorrowmist/useless/compat/jade/JadePatternStorageSnapshot.java new file mode 100644 index 00000000..7b23ab41 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/jade/JadePatternStorageSnapshot.java @@ -0,0 +1,169 @@ +package com.sorrowmist.useless.compat.jade; + +import com.sorrowmist.useless.content.blockentities.AdvancedAlloyFurnaceBlockEntity; +import com.sorrowmist.useless.content.blockentities.RecoverableItemStackHandler; +import com.sorrowmist.useless.content.blockentities.multiblock.MePatternAssemblyBlockEntity; +import com.sorrowmist.useless.content.blockentities.multiblock.PassiveCraftingHatchBlockEntity; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.items.IItemHandler; +import snownee.jade.addon.universal.ItemStorageProvider; +import snownee.jade.api.Accessor; +import snownee.jade.api.view.ViewGroup; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.function.IntFunction; + +/** + * Provides Jade with a small, revisioned snapshot instead of making its generic item iterator + * inspect every backing slot on every request. + */ +public final class JadePatternStorageSnapshot { + private static final int MAX_DISPLAY_ITEMS = 54; + private static final Object LOCK = new Object(); + private static final Map SNAPSHOTS = new WeakHashMap<>(); + + private JadePatternStorageSnapshot() { + } + + @Nullable + public static List> getGroups(Accessor accessor) { + return getGroups(accessor.getTarget()); + } + + @Nullable + public static List> getGroups(Object target) { + CachedSnapshot snapshot = getSnapshot(target); + return snapshot == null ? null : snapshot.groups; + } + + /** Writes the same Jade payload as the universal provider and tells the caller to stop. */ + public static boolean writeData(Accessor accessor) { + Object target = accessor.getTarget(); + CachedSnapshot snapshot = getSnapshot(target); + if (snapshot == null) { + return false; + } + + CompoundTag serverData = accessor.getServerData(); + serverData.remove("JadeItemStorage"); + serverData.remove("JadeItemStorageUid"); + serverData.merge(snapshot.serialized(accessor)); + return true; + } + + @Nullable + private static CachedSnapshot getSnapshot(Object target) { + if (target instanceof AdvancedAlloyFurnaceBlockEntity furnace) { + IItemHandler handler = furnace.getItemHandler(); + return getSnapshot(target, furnace.getItemStorageRevision(), handler.getSlots(), + handler::getStackInSlot); + } + if (target instanceof MePatternAssemblyBlockEntity assembly) { + RecoverableItemStackHandler handler = assembly.getPatterns(); + return getSnapshot(target, assembly.getPatternStorageRevision(), handler.getActiveSlots(), + handler::getStackInSlot); + } + if (target instanceof PassiveCraftingHatchBlockEntity hatch) { + RecoverableItemStackHandler handler = hatch.getPatterns(); + return getSnapshot(target, hatch.getPatternStorageRevision(), hatch.getActivePatternSlots(), + handler::getStackInSlot); + } + return null; + } + + private static CachedSnapshot getSnapshot( + Object target, long revision, int slotCount, IntFunction reader) { + synchronized (LOCK) { + CachedSnapshot cached = SNAPSHOTS.get(target); + if (cached != null && cached.revision == revision && cached.slotCount == slotCount) { + return cached; + } + + List compacted = compact(reader, slotCount); + CachedSnapshot snapshot = new CachedSnapshot( + revision, + slotCount, + compacted.isEmpty() + ? List.of() + : List.of(new ViewGroup<>(List.copyOf(compacted)))); + SNAPSHOTS.put(target, snapshot); + return snapshot; + } + } + + private static List compact(IntFunction reader, int slotCount) { + List result = new ArrayList<>(); + for (int slot = 0; slot < slotCount; slot++) { + ItemStack stack = reader.apply(slot); + if (stack == null || stack.isEmpty()) { + continue; + } + + int existingIndex = -1; + for (int index = 0; index < result.size(); index++) { + if (ItemStack.isSameItemSameTags(result.get(index), stack)) { + existingIndex = index; + break; + } + } + if (existingIndex >= 0) { + ItemStack existing = result.get(existingIndex); + long total = (long) existing.getCount() + stack.getCount(); + existing.setCount((int) Math.min(Integer.MAX_VALUE, total)); + } else if (result.size() < MAX_DISPLAY_ITEMS) { + result.add(stack.copy()); + } + } + return result; + } + + private static final class CachedSnapshot { + private final long revision; + private final int slotCount; + private final List> groups; + @Nullable + private CompoundTag serializedData; + + private CachedSnapshot(long revision, int slotCount, List> groups) { + this.revision = revision; + this.slotCount = slotCount; + this.groups = groups; + } + + private CompoundTag serialized(Accessor accessor) { + if (serializedData == null) { + CompoundTag data = new CompoundTag(); + ViewGroup.saveList(data, "JadeItemStorage", groups, + stack -> serializeStack(accessor, stack)); + if (!groups.isEmpty()) { + data.putString("JadeItemStorageUid", + ItemStorageProvider.INSTANCE.getUid().toString()); + } + serializedData = data; + } + return serializedData.copy(); + } + + private static CompoundTag serializeStack(Accessor accessor, ItemStack original) { + int count = original.getCount(); + ItemStack stack = count > original.getMaxStackSize() + ? original.copyWithCount(1) + : original; + CompoundTag result = asCompound(stack.save(new CompoundTag())); + if (count > original.getMaxStackSize()) { + result.putInt("NewCount", count); + } + return result; + } + + private static CompoundTag asCompound(Tag tag) { + return tag instanceof CompoundTag compound ? compound : new CompoundTag(); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/jade/MultiblockAlloyFurnaceCoreJadeProvider.java b/src/main/java/com/sorrowmist/useless/compat/jade/MultiblockAlloyFurnaceCoreJadeProvider.java new file mode 100644 index 00000000..c673bc3e --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/jade/MultiblockAlloyFurnaceCoreJadeProvider.java @@ -0,0 +1,36 @@ +package com.sorrowmist.useless.compat.jade; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.items.MultiblockAlloyFurnaceCoreBlockItem; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import snownee.jade.api.BlockAccessor; +import snownee.jade.api.IBlockComponentProvider; +import snownee.jade.api.ITooltip; +import snownee.jade.api.Identifiers; +import snownee.jade.api.TooltipPosition; +import snownee.jade.api.config.IPluginConfig; + +public enum MultiblockAlloyFurnaceCoreJadeProvider implements IBlockComponentProvider { + INSTANCE; + + private static final ResourceLocation UID = UselessMod.id("multiblock_alloy_furnace_core_name"); + + @Override + public void appendTooltip(ITooltip tooltip, BlockAccessor accessor, IPluginConfig config) { + tooltip.remove(Identifiers.CORE_OBJECT_NAME); + tooltip.add(0, MultiblockAlloyFurnaceCoreBlockItem.rainbowName( + Component.translatable("block.useless_mod.multiblock_alloy_furnace_core")), + Identifiers.CORE_OBJECT_NAME); + } + + @Override + public int getDefaultPriority() { + return TooltipPosition.TAIL; + } + + @Override + public ResourceLocation getUid() { + return UID; + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/jade/ThermostatJadeProvider.java b/src/main/java/com/sorrowmist/useless/compat/jade/ThermostatJadeProvider.java new file mode 100644 index 00000000..83939a4c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/jade/ThermostatJadeProvider.java @@ -0,0 +1,44 @@ +package com.sorrowmist.useless.compat.jade; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.blockentities.ThermostatBlockEntity; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import snownee.jade.api.BlockAccessor; +import snownee.jade.api.IBlockComponentProvider; +import snownee.jade.api.ITooltip; +import snownee.jade.api.config.IPluginConfig; + +/** + * Jade 温度显示:悬停「温度调节器」时展示当前设定温度。 + * + *

温度已由 {@link ThermostatBlockEntity#getUpdateTag} 同步到客户端,因此这里直接读客户端 + * 方块实体即可,不需要 {@code IServerDataProvider}。

+ */ +public enum ThermostatJadeProvider implements IBlockComponentProvider { + INSTANCE; + + private static final ResourceLocation UID = UselessMod.id("temperature_regulator"); + + @Override + public void appendTooltip(ITooltip tooltip, BlockAccessor accessor, IPluginConfig config) { + if (!(accessor.getBlockEntity() instanceof ThermostatBlockEntity thermostat)) { + return; + } + if (!thermostat.isEnabled()) { + tooltip.add(Component.translatable("jade.useless_mod.thermostat.disabled") + .withStyle(ChatFormatting.GRAY)); + return; + } + long kelvin = thermostat.getTemperature(); + tooltip.add(Component.translatable("jade.useless_mod.thermostat.temperature", + kelvin, String.format("%.2f", kelvin - 273.15D)) + .withStyle(ChatFormatting.GOLD)); + } + + @Override + public ResourceLocation getUid() { + return UID; + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/jade/UselessJadePlugin.java b/src/main/java/com/sorrowmist/useless/compat/jade/UselessJadePlugin.java new file mode 100644 index 00000000..8d721500 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/jade/UselessJadePlugin.java @@ -0,0 +1,27 @@ +package com.sorrowmist.useless.compat.jade; + +import com.sorrowmist.useless.content.blockentities.AdvancedAlloyFurnaceBlockEntity; +import com.sorrowmist.useless.content.blocks.AdvancedAlloyFurnaceBlock; +import com.sorrowmist.useless.content.blocks.TemperatureRegulatorBlock; +import com.sorrowmist.useless.content.blocks.multiblock.MultiblockAlloyFurnaceCoreBlock; +import snownee.jade.api.IWailaClientRegistration; +import snownee.jade.api.IWailaCommonRegistration; +import snownee.jade.api.IWailaPlugin; +import snownee.jade.api.WailaPlugin; + +@WailaPlugin("jade") +public class UselessJadePlugin implements IWailaPlugin { + @Override + public void register(IWailaCommonRegistration registration) { + registration.registerBlockDataProvider(AdvancedAlloyFurnaceJadeProvider.INSTANCE, AdvancedAlloyFurnaceBlockEntity.class); + } + + @Override + public void registerClient(IWailaClientRegistration registration) { + registration.registerBlockComponent(AdvancedAlloyFurnaceJadeProvider.INSTANCE, AdvancedAlloyFurnaceBlock.class); + registration.registerBlockComponent(MultiblockAlloyFurnaceCoreJadeProvider.INSTANCE, + MultiblockAlloyFurnaceCoreBlock.class); + registration.registerBlockComponent(ThermostatJadeProvider.INSTANCE, + TemperatureRegulatorBlock.class); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/jei/AEKeyJeiIngredients.java b/src/main/java/com/sorrowmist/useless/compat/jei/AEKeyJeiIngredients.java new file mode 100644 index 00000000..95a05219 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/jei/AEKeyJeiIngredients.java @@ -0,0 +1,126 @@ +package com.sorrowmist.useless.compat.jei; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import appeng.api.client.AEKeyRendering; +import appeng.api.integrations.jei.IngredientConverter; +import appeng.api.integrations.jei.IngredientConverters; +import appeng.api.stacks.*; +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.compat.forge1201.jei.AEKeyBookmarkCatalog; +import mezz.jei.api.ingredients.*; +import mezz.jei.api.ingredients.subtypes.UidContext; +import mezz.jei.api.registration.IModIngredientRegistration; +import mezz.jei.api.runtime.IIngredientManager; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.TooltipFlag; +import org.jetbrains.annotations.Nullable; +import java.nio.file.Path; +import java.util.*; + +/** Actual JEI ingredient for keys with no native converter (e.g. Applied Flux energy). + * Both recipe pages and bookmarks use the AE key renderer/helper, never a fake item. + */ +public final class AEKeyJeiIngredients { + public record KeyIngredient(AEKey key, long amount) { + public KeyIngredient { Objects.requireNonNull(key); if (amount <= 0) throw new IllegalArgumentException("amount"); } + public GenericStack stack() { return new GenericStack(key, amount); } + } + public static final IIngredientType TYPE = new IIngredientType<>() { + @Override public Class getIngredientClass() { return KeyIngredient.class; } + @Override public String getUid() { return "useless_mod:ae_key"; } + }; + public static final Helper HELPER = new Helper(); + public static final Renderer RENDERER = new Renderer(); + public static final IngredientConverter CONVERTER = new IngredientConverter<>() { + @Override public IIngredientType getIngredientType() { return TYPE; } + @Override public KeyIngredient getIngredientFromStack(GenericStack stack) { + if (stack == null || stack.amount() <= 0 || GenericStackJeiIngredientProviders.resolveNative(stack) != null) + return null; + return remember(stack); + } + @Override public GenericStack getStackFromIngredient(KeyIngredient value) { return value.stack(); } + }; + private static final org.slf4j.Logger LOGGER = LogUtils.getLogger(); + private static final AEKeyBookmarkCatalog CATALOG = new AEKeyBookmarkCatalog(); + private static @Nullable Path catalogFile; + private static @Nullable IIngredientManager manager; + private static final Set registered = new HashSet<>(); + private AEKeyJeiIngredients() {} + + public static void register(IModIngredientRegistration registration, Path file) { + manager = null; + catalogFile = file; + try { CATALOG.load(file); } + catch (java.io.IOException exception) { + // Keep the unreadable original intact rather than replacing user state with an empty cache. + catalogFile = null; + RateLimitedLogger.warn(LOGGER, "Could not read AE key bookmark catalogue {}; automatic saves disabled", file, exception); + } + var values = CATALOG.decoded().stream().map(s -> new KeyIngredient(s.what(), 1)).toList(); + registered.clear(); values.forEach(value -> registered.add(HELPER.getUniqueId(value, UidContext.Ingredient))); + registration.register(TYPE, values, HELPER, RENDERER); + IngredientConverters.register(CONVERTER); + } + public static void setRuntime(@Nullable IIngredientManager value) { + manager = value; + if (value == null) return; + var additions = new ArrayList(); + for (var stack : CATALOG.decoded()) { + var ingredient = new KeyIngredient(stack.what(), 1); + if (registered.add(HELPER.getUniqueId(ingredient, UidContext.Ingredient))) additions.add(ingredient); + } + if (!additions.isEmpty()) value.addIngredientsAtRuntime(TYPE, additions); + } + public static GenericStackJeiIngredientProvider.Ingredient resolve(GenericStack stack) { + return new GenericStackJeiIngredientProvider.Ingredient(TYPE, remember(stack)); + } + private static KeyIngredient remember(GenericStack stack) { + var value = new KeyIngredient(stack.what(), stack.amount()); + if (CATALOG.remember(value.key()) && catalogFile != null) { + try { CATALOG.save(catalogFile); } + catch (java.io.IOException exception) { RateLimitedLogger.warn(LOGGER, "Could not persist AE key bookmarks {}", catalogFile, exception); } + } + String uid = HELPER.getUniqueId(value, UidContext.Ingredient); + if (manager != null && registered.add(uid)) manager.addIngredientsAtRuntime(TYPE, List.of(HELPER.normalizeIngredient(value))); + return value; + } + public static final class Helper implements IIngredientHelper { + @Override public IIngredientType getIngredientType() { return TYPE; } + @Override public String getDisplayName(KeyIngredient value) { return value.key().getDisplayName().getString(); } + @Override public String getUniqueId(KeyIngredient value, UidContext context) { return AEKeyBookmarkCatalog.uid(value.key()); } + @Override public ResourceLocation getResourceLocation(KeyIngredient value) { return value.key().getId(); } + @Override public String getDisplayModId(KeyIngredient value) { return value.key().getModId(); } + @Override public KeyIngredient copyIngredient(KeyIngredient value) { return new KeyIngredient(value.key(), value.amount()); } + @Override public KeyIngredient normalizeIngredient(KeyIngredient value) { return new KeyIngredient(value.key(), 1); } + @Override public long getAmount(KeyIngredient value) { return value.amount(); } + @Override public KeyIngredient copyWithAmount(KeyIngredient value, long amount) { return new KeyIngredient(value.key(), Math.max(1, amount)); } + @Override public boolean isValidIngredient(KeyIngredient value) { return value != null && value.key() != null && value.amount() > 0; } + @Override public String getErrorInfo(KeyIngredient value) { return value == null ? "null AEKey" : getUniqueId(value, UidContext.Ingredient); } + } + public static final class Renderer implements IIngredientRenderer { + @Override public void render(GuiGraphics gui, KeyIngredient value) { + if (value.key().getId().toString().equals("appflux:gteu")) { + // Applied Flux 1.3.2 ships no GTEU sprite. Draw an unambiguous fallback. + gui.fill(1, 1, 15, 15, 0xff25354b); + gui.fill(7, 2, 12, 6, 0xffffcf40); + gui.fill(5, 6, 10, 10, 0xffffcf40); + gui.fill(4, 10, 8, 14, 0xffffcf40); + } else if (AEKeyRendering.get(value.key().getType()) != null) { + AEKeyRendering.drawInGui(Minecraft.getInstance(), gui, 0, 0, value.key()); + } else { + gui.drawString(Minecraft.getInstance().font, "?", 4, 4, 0xffffff, false); + } + } + @Override public List getTooltip(KeyIngredient value, TooltipFlag flag) { + var lines = AEKeyRendering.get(value.key().getType()) == null + ? new ArrayList<>(List.of(value.key().getDisplayName())) + : new ArrayList<>(AEKeyRendering.getTooltip(value.key())); + if (value.amount() > 1) lines.add(Component.literal(value.key().formatAmount(value.amount(), AmountFormat.FULL))); + return lines; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/jei/AdvancedAlloyFurnaceRecipeCategory.java b/src/main/java/com/sorrowmist/useless/compat/jei/AdvancedAlloyFurnaceRecipeCategory.java index 120a1d90..e695c670 100644 --- a/src/main/java/com/sorrowmist/useless/compat/jei/AdvancedAlloyFurnaceRecipeCategory.java +++ b/src/main/java/com/sorrowmist/useless/compat/jei/AdvancedAlloyFurnaceRecipeCategory.java @@ -1,76 +1,86 @@ package com.sorrowmist.useless.compat.jei; -import com.mojang.blaze3d.systems.RenderSystem; -import com.mojang.blaze3d.vertex.PoseStack; +import appeng.api.stacks.GenericStack; import com.sorrowmist.useless.UselessMod; -import com.sorrowmist.useless.blocks.advancedalloyfurnace.AdvancedAlloyFurnaceBlock; -import com.sorrowmist.useless.recipes.advancedalloyfurnace.AdvancedAlloyFurnaceRecipe; -import com.sorrowmist.useless.registry.CatalystManager; -import com.sorrowmist.useless.registry.ModIngots; -import com.sorrowmist.useless.utils.NumberFormatUtil; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.catalyst.CatalystParallelManager; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeCatalog; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import com.sorrowmist.useless.init.ModBlocks; +import com.sorrowmist.useless.init.ModTags; +import com.sorrowmist.useless.core.config.AlloyFurnaceTierRules; import mezz.jei.api.constants.VanillaTypes; import mezz.jei.api.gui.builder.IRecipeLayoutBuilder; import mezz.jei.api.gui.builder.ITooltipBuilder; import mezz.jei.api.gui.drawable.IDrawable; import mezz.jei.api.gui.ingredient.IRecipeSlotsView; -import mezz.jei.api.gui.widgets.IRecipeExtrasBuilder; import mezz.jei.api.helpers.IGuiHelper; -import mezz.jei.api.ingredients.IIngredientRenderer; import mezz.jei.api.recipe.IFocusGroup; import mezz.jei.api.recipe.RecipeIngredientRole; import mezz.jei.api.recipe.RecipeType; import mezz.jei.api.recipe.category.IRecipeCategory; import net.minecraft.ChatFormatting; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.Font; import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.client.player.LocalPlayer; -import net.minecraft.client.renderer.RenderType; -import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; -import net.minecraft.util.Mth; import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.TooltipFlag; import net.minecraft.world.item.crafting.Ingredient; -import net.minecraftforge.client.ItemDecoratorHandler; import net.minecraftforge.fluids.FluidStack; +import mezz.jei.api.gui.builder.IRecipeSlotBuilder; +import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.List; -public class AdvancedAlloyFurnaceRecipeCategory implements IRecipeCategory { - public static final RecipeType TYPE = - RecipeType.create(UselessMod.MOD_ID, "advanced_alloy_furnace", AdvancedAlloyFurnaceRecipe.class); - // 使用新的JEI渲染贴图 +public class AdvancedAlloyFurnaceRecipeCategory implements IRecipeCategory { + public static final RecipeType TYPE = + RecipeType.create(UselessMod.MODID, "advanced_alloy_furnace", AlloyFurnaceRecipeCatalog.Entry.class); + + // 使用JEI渲染贴图 private static final ResourceLocation GUI_TEXTURE = - ResourceLocation.fromNamespaceAndPath("useless_mod", "textures/gui/jei_advanced_alloy_furnace_gui.png"); + new ResourceLocation(UselessMod.MODID, "textures/gui/jei_advanced_alloy_furnace_gui.png"); + // JEI中显示的背景区域 (212x72) private static final int DISPLAY_WIDTH = 212; private static final int DISPLAY_HEIGHT = 72; - // 槽位位置 - 根据新贴图调整 - // 输入物品槽位 (9个) - 3行3列排列 + + // 槽位位置 - 根据贴图调整 + // 输入物品槽位 (最多12个) - 3行4列排列 private static final int INPUT_SLOTS_START_X = 1; private static final int INPUT_SLOTS_START_Y = 1; private static final int INPUT_SLOT_SPACING_X = 18; private static final int INPUT_SLOT_SPACING_Y = 18; - // 输出物品槽位 (9个) - 3行3列排列 - private static final int OUTPUT_SLOTS_START_X = 159; + private static final int ITEM_GRID_COLUMNS = 4; + private static final int ITEM_GRID_ROWS = 3; + private static final int ITEM_GRID_MAX_SLOTS = ITEM_GRID_COLUMNS * ITEM_GRID_ROWS; + + // 输出物品槽位 (最多12个) - 3行4列排列 + private static final int OUTPUT_SLOTS_START_X = 141; private static final int OUTPUT_SLOTS_START_Y = 1; + // 流体槽位 private static final int FLUID_INPUT_X = 1; private static final int FLUID_INPUT_Y = 55; - private static final int FLUID_INPUT_WIDTH = 52; + private static final int FLUID_INPUT_WIDTH = 69; private static final int FLUID_INPUT_HEIGHT = 16; - private static final int FLUID_OUTPUT_X = 159; + private static final int FLUID_OUTPUT_X = 142; private static final int FLUID_OUTPUT_Y = 55; - private static final int FLUID_OUTPUT_WIDTH = 52; + private static final int FLUID_OUTPUT_WIDTH = 69; private static final int FLUID_OUTPUT_HEIGHT = 16; + // 催化剂和模具槽位 - private static final int CATALYST_SLOT_X = 80; + private static final int CATALYST_SLOT_X = 72; private static final int CATALYST_SLOT_Y = 55; - private static final int MOLD_SLOT_X = 116; + private static final int MOLD_SLOT_START_X = 89; + private static final int MOLD_SLOT_END_X = 125; private static final int MOLD_SLOT_Y = 55; + private static final int MOLD_SLOT_SPACING = 18; + // 进度条位置和尺寸 private static final int PROGRESS_BAR_X = 90; private static final int PROGRESS_BAR_Y = 23; @@ -78,185 +88,359 @@ public class AdvancedAlloyFurnaceRecipeCategory implements IRecipeCategory getRecipeType() { + public @NotNull RecipeType getRecipeType() { return TYPE; } + /** Catalog entries are wrappers, so JEI cannot infer the underlying recipe registry ID. */ + @Override + public @NotNull ResourceLocation getRegistryName(AlloyFurnaceRecipeCatalog.Entry entry) { + var identity = entry.identity(); + var id = identity.recipeId(); + return new ResourceLocation(UselessMod.MODID, "advanced_alloy_furnace/" + + entry.sourceId() + "/" + id.getNamespace() + "/" + id.getPath() + + "/" + identity.fingerprint()); + } + @Override - public Component getTitle() { - return title; + public @NotNull Component getTitle() { + return this.title; } @Override public int getWidth() { - return width; + return this.width; } @Override public int getHeight() { - return height; + return this.height; } @Override public IDrawable getIcon() { - return icon; + return this.icon; } @Override - public void setRecipe(IRecipeLayoutBuilder builder, AdvancedAlloyFurnaceRecipe recipe, IFocusGroup focuses) { - for (int i = 0; i < Math.min(recipe.getInputItems().size(), 9); i++) { - Ingredient ingredient = recipe.getInputItems().get(i); - long count = recipe.getInputItemCounts().get(i); + public void setRecipe(@NotNull IRecipeLayoutBuilder builder, + @NotNull AlloyFurnaceRecipeCatalog.Entry entry, + @NotNull IFocusGroup focuses) { + AdvancedAlloyFurnaceRecipe recipe = entry.recipe(); + // 计算配方相关的并行数信息 + int targetTier = getTargetUselessIngotTier(recipe); + boolean isUselessRecipe = targetTier > 0; + + // 输入物品槽位 + List inputs = recipe.inputs(); + List keyInputs = recipe.keyInputs(); + int inputCount = inputs.size(); + int totalInputCount = inputCount + keyInputs.size(); + + // 计算输入槽位的布局参数(支持超过12个时缩短间距) + int inputCols, inputRows; + float inputSpacingX, inputSpacingY; + + if (totalInputCount <= ITEM_GRID_MAX_SLOTS) { + // 正常布局:4x3 网格 + inputCols = ITEM_GRID_COLUMNS; + inputRows = ITEM_GRID_ROWS; + inputSpacingX = INPUT_SLOT_SPACING_X; + inputSpacingY = INPUT_SLOT_SPACING_Y; + } else { + // 缩放布局:根据数量动态调整 + inputCols = (int) Math.ceil(Math.sqrt(totalInputCount)); + if (inputCols > 6) inputCols = 6; // 最多6列 + + inputRows = (int) Math.ceil((float) totalInputCount / inputCols); + + float horizontalScale = Math.min(1.0f, + (float) (ITEM_GRID_COLUMNS - 1) / Math.max(1, inputCols - 1)); + float verticalScale = Math.min(1.0f, + (float) (ITEM_GRID_ROWS - 1) / Math.max(1, inputRows - 1)); + inputSpacingX = INPUT_SLOT_SPACING_X * horizontalScale; + inputSpacingY = INPUT_SLOT_SPACING_Y * verticalScale; + } + + // 设置输入物品槽位 + for (int i = 0; i < inputCount; i++) { + int row = i / inputCols; + int col = i % inputCols; + int x = INPUT_SLOTS_START_X + (int) (col * inputSpacingX); + int y = INPUT_SLOTS_START_Y + (int) (row * inputSpacingY); + + CountedIngredient countedIngredient = inputs.get(i); + Ingredient ingredient = countedIngredient.ingredient(); + long count = countedIngredient.count(); + int displayCount = (int) Math.min(count, Integer.MAX_VALUE); ItemStack[] matchingStacks = ingredient.getItems(); + if (matchingStacks.length == 0) { + ItemStack representative = AdapterUtils.itemRepresentative(ingredient); + matchingStacks = representative == null ? new ItemStack[0] + : new ItemStack[] {representative}; + } if (matchingStacks.length > 0) { ItemStack[] displayStacks = new ItemStack[matchingStacks.length]; for (int j = 0; j < matchingStacks.length; j++) { ItemStack displayStack = matchingStacks[j].copy(); - // 将long转换为int,因为ItemStack的count字段是int类型 - displayStack.setCount((int) Math.min(count, Integer.MAX_VALUE)); + displayStack.setCount(displayCount); displayStacks[j] = displayStack; } Ingredient displayIngredient = Ingredient.of(displayStacks); - int row = i / 3; - int col = i % 3; - int x = INPUT_SLOTS_START_X + col * INPUT_SLOT_SPACING_X; - int y = INPUT_SLOTS_START_Y + row * INPUT_SLOT_SPACING_Y; - builder.addSlot(RecipeIngredientRole.INPUT, x, y) - .addIngredients(displayIngredient) - .setCustomRenderer(VanillaTypes.ITEM_STACK, new ItemStackRenderer()); + .addIngredients(displayIngredient) + .setCustomRenderer(VanillaTypes.ITEM_STACK, ItemStackCountRenderer.INSTANCE) + .addRichTooltipCallback((slot, tooltip) -> { + if (count > 1) { + tooltip.add(Component.translatable("jei.useless_mod.tooltip.amount", formatCount(count)).withStyle(ChatFormatting.GRAY)); + } + if (totalInputCount > ITEM_GRID_MAX_SLOTS) { + tooltip.add(Component.translatable("jei.useless_mod.tooltip.key_input").withStyle(ChatFormatting.AQUA)); + } + }); } } - // 输出物品槽位 (9个) - 3行3列排列 - for (int i = 0; i < Math.min(recipe.getOutputItems().size(), 9); i++) { - // 计算槽位位置:3行3列 - int row = i / 3; - int col = i % 3; - int x = OUTPUT_SLOTS_START_X + col * INPUT_SLOT_SPACING_X; - int y = OUTPUT_SLOTS_START_Y + row * INPUT_SLOT_SPACING_Y; - - builder.addSlot(mezz.jei.api.recipe.RecipeIngredientRole.OUTPUT, x, y) - .addItemStack(recipe.getOutputItems().get(i)) - .setCustomRenderer(VanillaTypes.ITEM_STACK, new ItemStackRenderer()); + for (int i = 0; i < keyInputs.size(); i++) { + int slotIndex = inputCount + i; + int row = slotIndex / inputCols; + int col = slotIndex % inputCols; + int x = INPUT_SLOTS_START_X + (int) (col * inputSpacingX); + int y = INPUT_SLOTS_START_Y + (int) (row * inputSpacingY); + GenericStack keyInput = keyInputs.get(i); + long amount = keyInput.amount(); + IRecipeSlotBuilder slot = addKeyStack(builder, RecipeIngredientRole.INPUT, x, y, keyInput); + if (slot != null) { + slot.addRichTooltipCallback((slotView, tooltip) -> { + tooltip.add(Component.translatable("jei.useless_mod.tooltip.key_input").withStyle(ChatFormatting.AQUA)); + tooltip.add(Component.translatable("jei.useless_mod.tooltip.amount", formatCount(amount)).withStyle(ChatFormatting.GRAY)); + }); + } } - // 输入流体槽 - 使用自定义渲染器显示数量 - List inputFluids = recipe.getInputFluids(); + // 输出物品槽位 + List outputs = recipe.outputs(); + List keyOutputs = recipe.keyOutputs(); + int outputCount = outputs.size(); + int totalOutputCount = outputCount + keyOutputs.size(); + + // 计算输出槽位的布局参数(支持超过12个时缩短间距) + int outputCols, outputRows; + float outputSpacingX, outputSpacingY; + + if (totalOutputCount <= ITEM_GRID_MAX_SLOTS) { + outputCols = ITEM_GRID_COLUMNS; + outputRows = ITEM_GRID_ROWS; + outputSpacingX = INPUT_SLOT_SPACING_X; + outputSpacingY = INPUT_SLOT_SPACING_Y; + } else { + outputCols = (int) Math.ceil(Math.sqrt(totalOutputCount)); + if (outputCols > 6) outputCols = 6; + + outputRows = (int) Math.ceil((float) totalOutputCount / outputCols); + + float horizontalScale = Math.min(1.0f, + (float) (ITEM_GRID_COLUMNS - 1) / Math.max(1, outputCols - 1)); + float verticalScale = Math.min(1.0f, + (float) (ITEM_GRID_ROWS - 1) / Math.max(1, outputRows - 1)); + outputSpacingX = INPUT_SLOT_SPACING_X * horizontalScale; + outputSpacingY = INPUT_SLOT_SPACING_Y * verticalScale; + } + + for (int i = 0; i < outputCount; i++) { + int row = i / outputCols; + int col = i % outputCols; + int x = OUTPUT_SLOTS_START_X + (int) (col * outputSpacingX); + int y = OUTPUT_SLOTS_START_Y + (int) (row * outputSpacingY); + + ItemStack outputStack = outputs.get(i); + int count = outputStack.getCount(); + + builder.addSlot(RecipeIngredientRole.OUTPUT, x, y) + .addItemStack(outputStack) + .setCustomRenderer(VanillaTypes.ITEM_STACK, ItemStackCountRenderer.INSTANCE) + .addRichTooltipCallback((slot, tooltip) -> { + if (count > 1) { + tooltip.add(Component.translatable("jei.useless_mod.tooltip.amount", formatCount(count)).withStyle(ChatFormatting.GRAY)); + } + if (totalOutputCount > ITEM_GRID_MAX_SLOTS) { + tooltip.add(Component.translatable("jei.useless_mod.tooltip.key_output").withStyle(ChatFormatting.AQUA)); + } + addRecipeIdTooltip(tooltip, recipe.id()); + }); + } + + for (int i = 0; i < keyOutputs.size(); i++) { + int slotIndex = outputCount + i; + int row = slotIndex / outputCols; + int col = slotIndex % outputCols; + int x = OUTPUT_SLOTS_START_X + (int) (col * outputSpacingX); + int y = OUTPUT_SLOTS_START_Y + (int) (row * outputSpacingY); + GenericStack keyOutput = keyOutputs.get(i); + long amount = keyOutput.amount(); + IRecipeSlotBuilder slot = addKeyStack(builder, RecipeIngredientRole.OUTPUT, x, y, keyOutput); + if (slot != null) { + slot.addRichTooltipCallback((slotView, tooltip) -> { + tooltip.add(Component.translatable("jei.useless_mod.tooltip.key_output").withStyle(ChatFormatting.AQUA)); + tooltip.add(Component.translatable("jei.useless_mod.tooltip.amount", formatCount(amount)).withStyle(ChatFormatting.GRAY)); + addRecipeIdTooltip(tooltip, recipe.id()); + }); + } + } + + // 输入流体槽 - 使用JEI内置流体渲染器 + List inputFluids = recipe.inputFluids(); if (!inputFluids.isEmpty()) { int fluidCount = inputFluids.size(); int fluidWidth = FLUID_INPUT_WIDTH / fluidCount; - + for (int i = 0; i < fluidCount; i++) { - FluidStack fluid = inputFluids.get(i); + var input = inputFluids.get(i); + FluidStack[] candidates = input.getRepresentatives(); + FluidStack fluid = candidates.length == 0 + ? AdapterUtils.fluidRepresentative(input.ingredient(), 1) + : candidates[0]; + if (fluid == null || fluid.isEmpty()) continue; int x = FLUID_INPUT_X + i * fluidWidth; - builder.addSlot(mezz.jei.api.recipe.RecipeIngredientRole.INPUT, x, FLUID_INPUT_Y) - .addFluidStack(fluid.getFluid(), fluid.getAmount()) - .setCustomRenderer(mezz.jei.api.forge.ForgeTypes.FLUID_STACK, - new FluidStackRenderer(fluid.getAmount(), true, fluidWidth, FLUID_INPUT_HEIGHT)); + long amount = input.amount(); + int displayAmount = (int) Math.min(amount, Integer.MAX_VALUE); + + IRecipeSlotBuilder slot = builder.addSlot(RecipeIngredientRole.INPUT, x, FLUID_INPUT_Y) + .setFluidRenderer(displayAmount, false, fluidWidth, FLUID_INPUT_HEIGHT); + if (candidates.length == 0) { + slot.addFluidStack(fluid.getFluid(), displayAmount); + } else { + for (FluidStack candidate : candidates) { + if (candidate != null && !candidate.isEmpty()) { + slot.addFluidStack(candidate.getFluid(), displayAmount); + } + } + } + slot.addRichTooltipCallback((slotView, tooltip) -> { + tooltip.add(Component.translatable("jei.useless_mod.tooltip.amount.mb", formatCount(amount)).withStyle(ChatFormatting.GRAY)); + }); } } - // 输出流体槽 - 使用自定义渲染器显示数量 - List outputFluids = recipe.getOutputFluids(); + // 输出流体槽 - 使用JEI内置流体渲染器 + List outputFluids = recipe.outputFluids(); if (!outputFluids.isEmpty()) { int fluidCount = outputFluids.size(); int fluidWidth = FLUID_OUTPUT_WIDTH / fluidCount; - + for (int i = 0; i < fluidCount; i++) { FluidStack fluid = outputFluids.get(i); int x = FLUID_OUTPUT_X + i * fluidWidth; - builder.addSlot(mezz.jei.api.recipe.RecipeIngredientRole.OUTPUT, x, FLUID_OUTPUT_Y) - .addFluidStack(fluid.getFluid(), fluid.getAmount()) - .setCustomRenderer(mezz.jei.api.forge.ForgeTypes.FLUID_STACK, - new FluidStackRenderer(fluid.getAmount(), true, fluidWidth, FLUID_OUTPUT_HEIGHT)); + int amount = fluid.getAmount(); + + builder.addSlot(RecipeIngredientRole.OUTPUT, x, FLUID_OUTPUT_Y) + .setFluidRenderer(amount, false, fluidWidth, FLUID_OUTPUT_HEIGHT) + .addFluidStack(fluid.getFluid(), amount) + .addRichTooltipCallback((slot, tooltip) -> { + tooltip.add(Component.translatable("jei.useless_mod.tooltip.amount.mb", formatCount(amount)).withStyle(ChatFormatting.GRAY)); + addRecipeIdTooltip(tooltip, recipe.id()); + }); } } - // 催化剂槽位:只在配方需要催化剂时显示 - if (recipe.requiresCatalyst()) { - ItemStack[] catalystStacks = recipe.getCatalyst().getItems(); - if (catalystStacks.length > 0) { - ItemStack[] displayCatalystStacks = new ItemStack[catalystStacks.length]; - for (int j = 0; j < catalystStacks.length; j++) { - ItemStack displayStack = catalystStacks[j].copy(); - displayStack.setCount(recipe.getCatalystCount()); - displayCatalystStacks[j] = displayStack; + // 催化剂槽位(普通配方可用) + List catalystStacks = new ArrayList<>(); + + if (!isUselessRecipe) { + // 普通配方:使用ModTags.CATALYSTS轮询显示所有催化剂(可选) + BuiltInRegistries.ITEM.getTag(ModTags.CATALYSTS).ifPresent(tag -> { + for (var holder : tag) { + ItemStack displayStack = new ItemStack(holder.value()); + displayStack.setCount(1); + catalystStacks.add(displayStack); } - builder.addSlot(mezz.jei.api.recipe.RecipeIngredientRole.CATALYST, - CATALYST_SLOT_X, CATALYST_SLOT_Y) - .addIngredients(Ingredient.of(displayCatalystStacks)) - .setCustomRenderer(VanillaTypes.ITEM_STACK, new ItemStackRenderer()); - } + }); } - // 模具槽位 - if (recipe.requiresMold()) { - builder.addSlot(mezz.jei.api.recipe.RecipeIngredientRole.CATALYST, - MOLD_SLOT_X, MOLD_SLOT_Y) - .addIngredients(recipe.getMold()) - .setCustomRenderer(VanillaTypes.ITEM_STACK, new ItemStackRenderer()); + + if (!catalystStacks.isEmpty()) { + builder.addSlot(RecipeIngredientRole.CATALYST, CATALYST_SLOT_X, CATALYST_SLOT_Y) + .addIngredients(Ingredient.of(catalystStacks.toArray(new ItemStack[0]))) + .addRichTooltipCallback((slot, tooltip) -> { + tooltip.add(Component.translatable("jei.useless_mod.tooltip.catalyst").withStyle(ChatFormatting.GOLD)); + int tier = getTargetUselessIngotTier(recipe); + if (tier > 0) { + // 无用锭配方:显示并行数信息 + tooltip.add(Component.translatable("jei.useless_mod.tooltip.catalyst.useless_ingot.title").withStyle(ChatFormatting.YELLOW)); + tooltip.add(Component.translatable("jei.useless_mod.tooltip.catalyst.useless_ingot.desc2").withStyle(ChatFormatting.GREEN)); + tooltip.add(Component.translatable("jei.useless_mod.tooltip.catalyst.useless_ingot.desc3").withStyle(ChatFormatting.BLUE)); + } else if (!isUselessRecipe) { + // 普通配方 + tooltip.add(Component.translatable("jei.useless_mod.tooltip.catalyst.normal.desc1").withStyle(ChatFormatting.GRAY)); + tooltip.add(Component.translatable("jei.useless_mod.tooltip.catalyst.normal.desc2").withStyle(ChatFormatting.GRAY)); + tooltip.add(Component.translatable("jei.useless_mod.tooltip.catalyst.normal.desc3").withStyle(ChatFormatting.BLUE)); + } + }); } - } - @Override - public void createRecipeExtras(IRecipeExtrasBuilder builder, AdvancedAlloyFurnaceRecipe recipe, IFocusGroup focuses) { - // 不需要额外的遮罩,因为新贴图已经包含了所有视觉元素 + // Each independent mold requirement gets its own mandatory JEI slot. + List molds = recipe.molds(); + for (int index = 0; index < molds.size(); index++) { + builder.addSlot(RecipeIngredientRole.CATALYST, moldSlotX(molds.size(), index), MOLD_SLOT_Y) + .addIngredients(molds.get(index)); + } } @Override - public void draw(AdvancedAlloyFurnaceRecipe recipe, IRecipeSlotsView recipeSlotsView, GuiGraphics guiGraphics, double mouseX, double mouseY) { + public void draw(@NotNull AlloyFurnaceRecipeCatalog.Entry entry, + @NotNull IRecipeSlotsView recipeSlotsView, + @NotNull GuiGraphics guiGraphics, double mouseX, double mouseY) { + AdvancedAlloyFurnaceRecipe recipe = entry.recipe(); // 绘制背景 - background.draw(guiGraphics); + this.background.draw(guiGraphics); // 绘制进度条 - 2秒完成一次合成进度 long currentTime = System.currentTimeMillis(); @@ -264,316 +448,178 @@ public void draw(AdvancedAlloyFurnaceRecipe recipe, IRecipeSlotsView recipeSlots int progressWidth = (int) (PROGRESS_BAR_WIDTH * progressRatio); if (progressWidth > 0) { - // 从GUI_TEXTURE的PROGRESS_BAR_SOURCE_X, PROGRESS_BAR_SOURCE_Y处抠取进度条 - // 注意:纹理的实际尺寸可能大于显示区域,使用纹理的实际尺寸进行blit guiGraphics.blit(GUI_TEXTURE, - PROGRESS_BAR_X, PROGRESS_BAR_Y, - PROGRESS_BAR_SOURCE_X, PROGRESS_BAR_SOURCE_Y, - progressWidth, PROGRESS_BAR_HEIGHT, - 256, 256); // 使用纹理的实际尺寸(假设为256x256) + PROGRESS_BAR_X, PROGRESS_BAR_Y, + PROGRESS_BAR_SOURCE_X, PROGRESS_BAR_SOURCE_Y, + progressWidth, PROGRESS_BAR_HEIGHT, + 256, 256 + ); } // 绘制能量和时间信息 Minecraft minecraft = Minecraft.getInstance(); - // 格式化能量显示 - 使用新的格式化方法 - String energyText = NumberFormatUtil.formatEnergy(recipe.getEnergy()); - String timeText = recipe.getProcessTime() + " ticks"; + // 格式化能量显示 + String energyText = this.formatEnergy(recipe.energy()); + String timeText = recipe.processTime() + " ticks"; - // 在能量显示区域居中显示能量(绿色) + // 在能量显示区域居中显示能量(绿色)- 使用缩小字体 + guiGraphics.pose().pushPose(); + float energyScale = 0.8f; + guiGraphics.pose().scale(energyScale, energyScale, 1.0f); int energyTextWidth = minecraft.font.width(energyText); - int energyX = ENERGY_DISPLAY_X + (ENERGY_DISPLAY_WIDTH - energyTextWidth) / 2; - guiGraphics.drawString(minecraft.font, energyText, energyX, ENERGY_DISPLAY_Y, 0x00FF00, false); - - // 在时间显示区域居中显示时间(绿色) + int energyX = (int) ((ENERGY_DISPLAY_X + (ENERGY_DISPLAY_WIDTH - energyTextWidth * energyScale) / 2) / energyScale); + int energyY = (int) (ENERGY_DISPLAY_Y / energyScale); + guiGraphics.drawString(minecraft.font, energyText, energyX, energyY, 0x00FF00, false); + guiGraphics.pose().popPose(); + + // 在时间显示区域居中显示时间(绿色)- 使用缩小字体,位置往下移动 + guiGraphics.pose().pushPose(); + float timeScale = 0.8f; + guiGraphics.pose().scale(timeScale, timeScale, 1.0f); int timeTextWidth = minecraft.font.width(timeText); - int timeX = TIME_DISPLAY_X + (TIME_DISPLAY_WIDTH - timeTextWidth) / 2; - guiGraphics.drawString(minecraft.font, timeText, timeX, TIME_DISPLAY_Y, 0x00FF00, false); - - // 修改:只有当配方允许催化剂时才显示提示 - if (recipe.requiresCatalyst() && recipe.isCatalystAllowed()) { - guiGraphics.pose().pushPose(); - float scale = 0.7f; - guiGraphics.pose().scale(scale, scale, 1.0f); - - String catalystText = "无用锭为可选催化剂"; - int textWidth = minecraft.font.width(catalystText); - // 在整张 JEI 背景宽度内居中 - int centeredX = (int) ((DISPLAY_WIDTH / 2.0f - textWidth * scale / 2) / scale); - int y = (int) (CATALYST_TEXT_Y / scale); - - guiGraphics.drawString(minecraft.font, catalystText, centeredX, y, 0xFF0000, false); - guiGraphics.pose().popPose(); - } - -// 模具提示 - if (recipe.requiresMold()) { - guiGraphics.pose().pushPose(); - float scale = 0.7f; - guiGraphics.pose().scale(scale, scale, 1.0f); + int timeX = (int) ((TIME_DISPLAY_X + (TIME_DISPLAY_WIDTH - timeTextWidth * timeScale) / 2) / timeScale); + int timeY = (int) ((TIME_DISPLAY_Y + 1) / timeScale); + guiGraphics.drawString(minecraft.font, timeText, timeX, timeY, 0x00FF00, false); + guiGraphics.pose().popPose(); - String moldText = "需要标志物(不会被消耗)"; - int textWidth = minecraft.font.width(moldText); - int centeredX = (int) ((DISPLAY_WIDTH / 2.0f - textWidth * scale / 2) / scale); - int y = (int) (MOLD_TEXT_Y / scale); - guiGraphics.drawString(minecraft.font, moldText, centeredX, y, 0xFF0000, false); - guiGraphics.pose().popPose(); - } - // 注意:已取消流体数量显示 - } + guiGraphics.pose().pushPose(); + float scale = 0.7f; + guiGraphics.pose().scale(scale, scale, 1.0f); - // 移除旧的 CATALYST_PARALLEL_MAP,使用统一的 CatalystManager - private int getCatalystParallel(AdvancedAlloyFurnaceRecipe recipe) { - // 即使配方不需要催化剂,也可以使用催化剂提高并行数 - // 获取催化剂的第一个物品作为代表 - ItemStack[] catalystStacks = recipe.getCatalyst().getItems(); - if (catalystStacks.length > 0) { - return CatalystManager.getCatalystParallel(catalystStacks[0]); - } + int requiredTier = AlloyFurnaceTierRules.requiredTier(recipe.id(), recipe.tier()); + String requirementText = requiredTier > 0 + ? Component.translatable("jei.useless_mod.gui.tier_required", requiredTier).getString() + : Component.translatable("jei.useless_mod.gui.tier_unrestricted").getString(); + int textWidth = minecraft.font.width(requirementText); + int centeredX = (int) ((DISPLAY_WIDTH / 2.0f - textWidth * scale / 2) / scale); + int y = (int) (REQUIREMENT_TEXT_Y / scale); - // 如果配方没有定义催化剂,返回默认值1 - return 1; + guiGraphics.drawString(minecraft.font, requirementText, centeredX, y, + requiredTier > 0 ? 0xFF0000 : 0x404040, false); + guiGraphics.pose().popPose(); } // 修改工具提示方法,添加并行数信息 @Override - public void getTooltip(ITooltipBuilder tooltip, AdvancedAlloyFurnaceRecipe recipe, IRecipeSlotsView recipeSlotsView, double mouseX, double mouseY) { + public void getTooltip(@NotNull ITooltipBuilder tooltip, + @NotNull AlloyFurnaceRecipeCatalog.Entry entry, + @NotNull IRecipeSlotsView recipeSlotsView, + double mouseX, double mouseY) { + AdvancedAlloyFurnaceRecipe recipe = entry.recipe(); // 能量和时间显示的悬停提示 if (mouseX >= ENERGY_DISPLAY_X && mouseX <= ENERGY_DISPLAY_X + ENERGY_DISPLAY_WIDTH && mouseY >= ENERGY_DISPLAY_Y && mouseY <= ENERGY_DISPLAY_Y + ENERGY_DISPLAY_HEIGHT) { - tooltip.add(Component.literal("基础能量消耗: " + NumberFormatUtil.formatEnergy(recipe.getEnergy()))); - tooltip.add(Component.literal("实际能量 = 基础能量 × 并行数")); + tooltip.add(Component.translatable("jei.useless_mod.tooltip.energy.base", this.formatEnergy(recipe.energy()))); + tooltip.add(Component.translatable("jei.useless_mod.tooltip.energy.actual")); } // 时间显示区域 if (mouseX >= TIME_DISPLAY_X && mouseX <= TIME_DISPLAY_X + TIME_DISPLAY_WIDTH && mouseY >= TIME_DISPLAY_Y && mouseY <= TIME_DISPLAY_Y + TIME_DISPLAY_HEIGHT) { - tooltip.add(Component.literal("处理时间: " + recipe.getProcessTime() + " ticks")); - tooltip.add(Component.literal("处理时间不受并行数影响")); + tooltip.add(Component.translatable("jei.useless_mod.tooltip.process_time", recipe.processTime())); + tooltip.add(Component.translatable("jei.useless_mod.tooltip.process_time.note")); + tooltip.add(Component.translatable("jei.useless_mod.tooltip.process_time.coil_bonus") + .withStyle(ChatFormatting.BLUE)); + tooltip.add(Component.translatable("jei.useless_mod.tooltip.process_time.coil_useful") + .withStyle(ChatFormatting.BLUE)); } - // 并行数效果说明区域 + // 并行数效果说明区域 - 只在显示红色提示文本时(非无用锭配方且允许催化剂)才渲染 if (mouseX >= PARALLEL_TEXT_X && mouseX <= PARALLEL_TEXT_X + 120 && - mouseY >= PARALLEL_TEXT_Y && mouseY <= PARALLEL_TEXT_Y + 10) { - tooltip.add(Component.literal("并行数效果说明").withStyle(ChatFormatting.GOLD)); - tooltip.add(Component.literal("• 输入物品消耗 × 并行数").withStyle(ChatFormatting.GRAY)); - tooltip.add(Component.literal("• 输出物品数量 × 并行数").withStyle(ChatFormatting.GRAY)); - tooltip.add(Component.literal("• 能量消耗 × 并行数").withStyle(ChatFormatting.GRAY)); - tooltip.add(Component.literal("• 处理时间保持不变").withStyle(ChatFormatting.GRAY)); - tooltip.add(Component.literal("⚠ 催化剂会被消耗").withStyle(ChatFormatting.RED)); - } - - // 催化剂提示:所有配方都可以使用无用锭作为催化剂 - if (mouseX >= CATALYST_SLOT_X && mouseX <= CATALYST_SLOT_X + SLOT_SIZE && - mouseY >= CATALYST_SLOT_Y && mouseY <= CATALYST_SLOT_Y + SLOT_SIZE) { - tooltip.add(Component.literal("催化剂").withStyle(ChatFormatting.GOLD)); - tooltip.add(Component.literal("• 可以使用无用锭提高并行数").withStyle(ChatFormatting.GRAY)); - tooltip.add(Component.literal("• 催化剂会被消耗").withStyle(ChatFormatting.RED)); - tooltip.add(Component.literal("• 不同等级的无用锭提供不同的并行数").withStyle(ChatFormatting.GRAY)); + mouseY >= PARALLEL_TEXT_Y && mouseY <= PARALLEL_TEXT_Y + 10 && + !recipe.catalyst().isEmpty() && isUselessIngotRecipe(recipe)) { + tooltip.add(Component.translatable("jei.useless_mod.tooltip.parallel.title").withStyle(ChatFormatting.GOLD)); + tooltip.add(Component.translatable("jei.useless_mod.tooltip.parallel.desc1").withStyle(ChatFormatting.GRAY)); + tooltip.add(Component.translatable("jei.useless_mod.tooltip.parallel.desc2").withStyle(ChatFormatting.GRAY)); + tooltip.add(Component.translatable("jei.useless_mod.tooltip.parallel.desc3").withStyle(ChatFormatting.GRAY)); + tooltip.add(Component.translatable("jei.useless_mod.tooltip.parallel.desc4").withStyle(ChatFormatting.GRAY)); + tooltip.add(Component.translatable("jei.useless_mod.tooltip.parallel.warning").withStyle(ChatFormatting.RED)); } } - private static class ItemStackRenderer implements IIngredientRenderer { - - @Override - public void render(GuiGraphics guiGraphics, ItemStack stack) { - if (stack == null || stack.isEmpty()) return; - - // 启用深度测试 - RenderSystem.enableDepthTest(); - - // 渲染物品图标 - guiGraphics.renderFakeItem(stack, 0, 0); - Minecraft mc = Minecraft.getInstance(); - Font font = mc.font; - PoseStack pose = guiGraphics.pose(); - - // 数量文本缩放 - 使用新的格式化方法 - if (stack.getCount() > 1) { - String text = NumberFormatUtil.formatItemCount(stack.getCount()); - pose.pushPose(); - float scale = 0.65f; - pose.translate(0, 0, 200.0F); - pose.scale(scale, scale, 1.0F); - - int textX = Math.round(16.0f / scale) - font.width(text); - int textY = Math.round(10.0f / scale); - - guiGraphics.drawString(font, text, textX, textY, 0xFFFFFF, true); - pose.popPose(); - } - - // 原版逻辑(耐久条 + 冷却 + 装饰) - if (stack.isBarVisible()) { - int l = stack.getBarWidth(); - int i = stack.getBarColor(); - int j = 2; - int k = 13; - guiGraphics.fill(RenderType.guiOverlay(), j, k, j + 13, k + 2, 0xFF000000); - guiGraphics.fill(RenderType.guiOverlay(), j, k, j + l, k + 1, i | 0xFF000000); - } - - LocalPlayer player = mc.player; - float f = player == null ? 0.0F : - player.getCooldowns().getCooldownPercent(stack.getItem(), mc.getFrameTime()); - if (f > 0.0F) { - int i1 = Mth.floor(16.0F * (1.0F - f)); - int j1 = i1 + Mth.ceil(16.0F * f); - guiGraphics.fill(RenderType.guiOverlay(), 0, i1, 16, j1, Integer.MAX_VALUE); - } - - ItemDecoratorHandler.of(stack).render(guiGraphics, font, stack, 0, 0); - RenderSystem.disableBlend(); - } - - @Override - public List getTooltip(ItemStack stack, TooltipFlag flag) { - List tooltip = stack.getTooltipLines(Minecraft.getInstance().player, flag); - - // 为工具提示添加数量信息(使用缩写) - if (stack.getCount() > 1) { - // 找到显示名称的行(通常是第一行),在其后添加数量信息 - if (!tooltip.isEmpty()) { - Component displayName = tooltip.get(0); - String countText = NumberFormatUtil.formatItemCount(stack.getCount()); - tooltip.set(0, Component.literal(displayName.getString() + " ×" + countText)); - } - } - - return tooltip; - } - - @Override - public int getWidth() { - return 16; + private static void addRecipeIdTooltip(ITooltipBuilder tooltip, ResourceLocation recipeId) { + if (Screen.hasShiftDown()) { + tooltip.add(Component.translatable("jei.useless_mod.tooltip.recipe_id", recipeId.toString()) + .withStyle(ChatFormatting.GRAY)); } + } - @Override - public int getHeight() { - return 16; + // 格式化能量显示 + private String formatEnergy(long energy) { + if (energy >= 1_000_000_000_000_000_000L) { + return String.format("%.2fE FE", energy / 1_000_000_000_000_000_000.0); + } else if (energy >= 1_000_000_000_000_000L) { + return String.format("%.2fP FE", energy / 1_000_000_000_000_000.0); + } else if (energy >= 1_000_000_000_000L) { + return String.format("%.2fT FE", energy / 1_000_000_000_000.0); + } else if (energy >= 1_000_000_000L) { + return String.format("%.2fG FE", energy / 1_000_000_000.0); + } else if (energy >= 1_000_000L) { + return String.format("%.2fM FE", energy / 1_000_000.0); + } else if (energy >= 1_000L) { + return String.format("%.2fk FE", energy / 1_000.0); + } else { + return energy + " FE"; } } - private static class FluidStackRenderer implements IIngredientRenderer { - private final int capacity; - private final boolean showAmount; - private final int width; - private final int height; - - public FluidStackRenderer(int capacity, boolean showAmount, int width, int height) { - this.capacity = capacity; - this.showAmount = showAmount; - this.width = width; - this.height = height; + // 格式化物品数量 + private String formatCount(long count) { + if (count >= 1000000000) { + return String.format("%.2fG", count / 1000000000.0); + } else if (count >= 1000000) { + return String.format("%.2fM", count / 1000000.0); + } else if (count >= 1000) { + return String.format("%.2fk", count / 1000.0); + } else { + return String.valueOf(count); } + } - @Override - public void render(GuiGraphics guiGraphics, FluidStack stack) { - if (stack == null || stack.isEmpty()) return; - - // 启用深度测试 - RenderSystem.enableDepthTest(); - - // 渲染流体填充 - int fluidHeight = (int) (height * ((float) stack.getAmount() / capacity)); - if (fluidHeight > 0) { - net.minecraftforge.client.extensions.common.IClientFluidTypeExtensions fluidAttributes = - net.minecraftforge.client.extensions.common.IClientFluidTypeExtensions.of(stack.getFluid()); - ResourceLocation fluidStillTexture = fluidAttributes.getStillTexture(stack); - int fluidColor = fluidAttributes.getTintColor(stack); - - if (fluidStillTexture != null) { - TextureAtlasSprite fluidSprite = Minecraft.getInstance().getTextureAtlas(net.minecraft.world.inventory.InventoryMenu.BLOCK_ATLAS).apply(fluidStillTexture); - - float r = ((fluidColor >> 16) & 0xFF) / 255.0F; - float g = ((fluidColor >> 8) & 0xFF) / 255.0F; - float b = (fluidColor & 0xFF) / 255.0F; - float a = ((fluidColor >> 24) & 0xFF) / 255.0F; - - RenderSystem.setShaderColor(r, g, b, a); - RenderSystem.setShaderTexture(0, net.minecraft.world.inventory.InventoryMenu.BLOCK_ATLAS); - - int fluidY = height - fluidHeight; - - // 绘制流体纹理 - for (int i = 0; i < Math.ceil((double) width / 16); i++) { - for (int j = 0; j < Math.ceil((double) fluidHeight / 16); j++) { - int texWidth = Math.min(16, width - i * 16); - int texHeight = Math.min(16, fluidHeight - j * 16); - if (texWidth > 0 && texHeight > 0) { - guiGraphics.blit( - i * 16, - fluidY + j * 16, - 0, - texWidth, texHeight, - fluidSprite - ); - } - } - } - - RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); - } - } + private static int moldSlotX(int count, int index) { + if (count <= 1) return MOLD_SLOT_START_X; - // 渲染流体数量文本 - if (showAmount && stack.getAmount() > 0) { - renderFluidAmount(guiGraphics, stack); - } - - // 从UI图抠取覆盖层,尺寸根据流体槽实际大小确定,渲染在流体槽向左向上1像素位置 - RenderSystem.setShaderTexture(0, GUI_TEXTURE); - guiGraphics.blit( - GUI_TEXTURE, - -1, -1, // 向左向上1像素 - 0, 54, // UI图起点 - width + 2, height + 2, // 覆盖层大小,使用流体槽实际宽度+2,高度+2 - 256, 256 // 纹理实际尺寸 - ); + float spacing = Math.min(MOLD_SLOT_SPACING, + (MOLD_SLOT_END_X - MOLD_SLOT_START_X) / (float) (count - 1)); + return Math.round(MOLD_SLOT_START_X + index * spacing); + } - RenderSystem.disableBlend(); + private static IRecipeSlotBuilder addKeyStack(IRecipeLayoutBuilder builder, + RecipeIngredientRole role, + int x, + int y, + GenericStack stack) { + GenericStackJeiIngredientProvider.Ingredient nativeIngredient = + GenericStackJeiIngredientProviders.resolve(stack); + if (nativeIngredient != null) { + IRecipeSlotBuilder slot = builder.addSlot(role, x, y); + nativeIngredient.addTo(slot); + return slot; } - /** - * 渲染流体数量文本 - */ - private void renderFluidAmount(GuiGraphics guiGraphics, FluidStack stack) { - String text = NumberFormatUtil.formatFluidAmount(stack.getAmount()); - Minecraft mc = Minecraft.getInstance(); - Font font = mc.font; - PoseStack pose = guiGraphics.pose(); - - pose.pushPose(); - float scale = 0.65f; - pose.translate(0, 0, 200.0F); - pose.scale(scale, scale, 1.0F); - - int x = Math.round(width / scale) - font.width(text); - int y = Math.round(height / scale) - font.lineHeight; - - guiGraphics.drawString(font, text, x, y, 0xFFFFFF, true); - pose.popPose(); - } + return null; // Invalid/empty key; never advertise a wrapped item to JEI. - @Override - public List getTooltip(FluidStack stack, TooltipFlag flag) { - List tooltip = new ArrayList<>(); - tooltip.add(stack.getDisplayName()); - // 使用新的格式化方法显示流体数量 - tooltip.add(Component.literal("数量: " + NumberFormatUtil.formatFluidAmount(stack.getAmount()))); - return tooltip; - } + } - @Override - public int getWidth() { - return width; - } + // 判断是否是无用锭配方(根据输出物品判断) + private boolean isUselessIngotRecipe(AdvancedAlloyFurnaceRecipe recipe) { + return getTargetUselessIngotTier(recipe) <= 0; + } - @Override - public int getHeight() { - return height; + // 获取目标无用锭等级 + private int getTargetUselessIngotTier(AdvancedAlloyFurnaceRecipe recipe) { + List outputs = recipe.outputs(); + if (outputs.isEmpty()) return 0; + + for (ItemStack output : outputs) { + int tier = CatalystParallelManager.getTargetUselessIngotTier(output); + if (tier > 0) { + return tier; + } } + return 0; } -} \ No newline at end of file +} diff --git a/src/main/java/com/sorrowmist/useless/compat/jei/CatalystInfoCategory.java b/src/main/java/com/sorrowmist/useless/compat/jei/CatalystInfoCategory.java index 8272e611..c3a968e0 100644 --- a/src/main/java/com/sorrowmist/useless/compat/jei/CatalystInfoCategory.java +++ b/src/main/java/com/sorrowmist/useless/compat/jei/CatalystInfoCategory.java @@ -1,16 +1,16 @@ package com.sorrowmist.useless.compat.jei; -import com.mojang.blaze3d.systems.RenderSystem; import com.sorrowmist.useless.UselessMod; -import com.sorrowmist.useless.registry.CatalystManager; -import com.sorrowmist.useless.registry.ModIngots; -import mezz.jei.api.constants.VanillaTypes; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.catalyst.CatalystParallelManager; +import com.sorrowmist.useless.init.ModItems; +import com.sorrowmist.useless.init.ModTags; import mezz.jei.api.gui.builder.IRecipeLayoutBuilder; +import mezz.jei.api.gui.builder.ITooltipBuilder; import mezz.jei.api.gui.drawable.IDrawable; +import mezz.jei.api.gui.ingredient.IRecipeSlotRichTooltipCallback; +import mezz.jei.api.gui.ingredient.IRecipeSlotView; import mezz.jei.api.gui.ingredient.IRecipeSlotsView; -import mezz.jei.api.gui.ingredient.IRecipeSlotTooltipCallback; import mezz.jei.api.helpers.IGuiHelper; -import mezz.jei.api.ingredients.IIngredientRenderer; import mezz.jei.api.recipe.IFocusGroup; import mezz.jei.api.recipe.RecipeIngredientRole; import mezz.jei.api.recipe.RecipeType; @@ -18,313 +18,210 @@ import net.minecraft.ChatFormatting; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.TooltipFlag; +import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.List; -import java.util.Map; public class CatalystInfoCategory implements IRecipeCategory { public static final RecipeType TYPE = - RecipeType.create(UselessMod.MOD_ID, "catalyst_info", CatalystInfo.class); + RecipeType.create(UselessMod.MODID, "catalyst_info", CatalystInfo.class); private static final ResourceLocation GUI_TEXTURE = - ResourceLocation.fromNamespaceAndPath("useless_mod", "textures/gui/jei_catalyst_info_gui.png"); + new ResourceLocation(UselessMod.MODID, "textures/gui/jei_catalyst_info_gui.png"); // 显示尺寸 private static final int DISPLAY_WIDTH = 150; - private static final int DISPLAY_HEIGHT = 130; + private static final int DISPLAY_HEIGHT = 150; + private static final int TEXTURE_HEIGHT = 130; - // 从 CatalystManager 获取催化剂数据 - private final Map catalystData; - private final ItemStack[] catalystStacks; + // 催化剂物品堆栈列表 + private final List catalystStacks; private final IDrawable background; private final IDrawable icon; private final Component title; - private final IGuiHelper guiHelper; - - public CatalystInfoCategory(IGuiHelper guiHelper) { - this.guiHelper = guiHelper; + CatalystInfoCategory(IGuiHelper guiHelper) { // 创建一个空白的背景,尺寸与显示尺寸相同 this.background = guiHelper.createBlankDrawable(DISPLAY_WIDTH, DISPLAY_HEIGHT); - this.icon = guiHelper.createDrawableItemStack(new ItemStack(ModIngots.USEFUL_INGOT.get())); + this.icon = guiHelper.createDrawableItemStack(new ItemStack(ModItems.USEFUL_INGOT.get())); this.title = Component.translatable("jei.useless_mod.catalyst_info"); - // 从 CatalystManager 获取所有催化剂数据 - this.catalystData = CatalystManager.getAllCatalysts(); - - // 创建催化剂物品堆栈数组 - this.catalystStacks = new ItemStack[]{ - new ItemStack(ModIngots.USELESS_INGOT_TIER_1.get()), - new ItemStack(ModIngots.USELESS_INGOT_TIER_2.get()), - new ItemStack(ModIngots.USELESS_INGOT_TIER_3.get()), - new ItemStack(ModIngots.USELESS_INGOT_TIER_4.get()), - new ItemStack(ModIngots.USELESS_INGOT_TIER_5.get()), - new ItemStack(ModIngots.USELESS_INGOT_TIER_6.get()), - new ItemStack(ModIngots.USELESS_INGOT_TIER_7.get()), - new ItemStack(ModIngots.USELESS_INGOT_TIER_8.get()), - new ItemStack(ModIngots.USELESS_INGOT_TIER_9.get()), - new ItemStack(ModIngots.USEFUL_INGOT.get()) - }; + // 从 ModTags.CATALYSTS 获取所有催化剂物品 + this.catalystStacks = new ArrayList<>(); + for (Item item : BuiltInRegistries.ITEM) { + ItemStack stack = new ItemStack(item); + if (stack.is(ModTags.CATALYSTS)) { + this.catalystStacks.add(stack); + } + } } @Override - public RecipeType getRecipeType() { + public @NotNull RecipeType getRecipeType() { return TYPE; } @Override - public Component getTitle() { - return title; + public @NotNull ResourceLocation getRegistryName(CatalystInfo recipe) { + return new ResourceLocation(UselessMod.MODID, "catalyst_info"); } @Override + public @NotNull Component getTitle() { + return this.title; + } + + @Override + @Deprecated + @SuppressWarnings("removal") public IDrawable getBackground() { - return background; + return this.background; + } + + @Override + public int getWidth() { + return DISPLAY_WIDTH; + } + + @Override + public int getHeight() { + return DISPLAY_HEIGHT; } @Override public IDrawable getIcon() { - return icon; + return this.icon; } @Override - public void setRecipe(IRecipeLayoutBuilder builder, CatalystInfo recipe, IFocusGroup focuses) { + public void setRecipe(@NotNull IRecipeLayoutBuilder builder, @NotNull CatalystInfo recipe, @NotNull IFocusGroup focuses) { // 2行5列网格布局 - int startX = 29; // 第一个格子左上角起点X坐标 - int startY = 24; // 第一个格子左上角起点Y坐标 + int startX = 30; // 第一个格子左上角起点X坐标 + int startY = 25; // 第一个格子左上角起点Y坐标 int spacing = 18; // 每个格子18*18像素 - // 总共10个催化剂,按2行5列排列 - for (int i = 0; i < catalystStacks.length; i++) { - final int index = i; + // 动态计算列数(最多5列) + int columns = Math.max(1, Math.min(this.catalystStacks.size(), 5)); + int rows = (this.catalystStacks.size() + columns - 1) / columns; + + for (int i = 0; i < this.catalystStacks.size(); i++) { // 计算行和列 - int row = i / 5; // 0, 0, 0, 0, 0, 1, 1, 1, 1, 1 - int col = i % 5; // 0, 1, 2, 3, 4, 0, 1, 2, 3, 4 + int row = i / columns; + int col = i % columns; int x = startX + col * spacing; int y = startY + row * spacing; + ItemStack catalystStack = this.catalystStacks.get(i); + builder.addSlot(RecipeIngredientRole.INPUT, x, y) - .addItemStack(catalystStacks[index]) - .setCustomRenderer(VanillaTypes.ITEM_STACK, new CatalystItemStackRenderer(index)) - .addTooltipCallback(new CatalystTooltipCallback(index)); + .addItemStack(catalystStack) + .addRichTooltipCallback(new CatalystTooltipCallback(i)); } } @Override - public void draw(CatalystInfo recipe, IRecipeSlotsView recipeSlotsView, GuiGraphics guiGraphics, double mouseX, double mouseY) { + public void draw(@NotNull CatalystInfo recipe, @NotNull IRecipeSlotsView recipeSlotsView, GuiGraphics guiGraphics, double mouseX, double mouseY) { Minecraft minecraft = Minecraft.getInstance(); // 手动绘制背景,确保不缩放 - // 假设纹理尺寸与显示尺寸相同(150x120) guiGraphics.blit(GUI_TEXTURE, 0, 0, // 绘制位置 (0,0) 0, 0, // 纹理起始位置 (0,0) - DISPLAY_WIDTH, DISPLAY_HEIGHT, // 绘制尺寸 - DISPLAY_WIDTH, DISPLAY_HEIGHT); // 纹理尺寸(与绘制尺寸相同) + DISPLAY_WIDTH, TEXTURE_HEIGHT, // 绘制尺寸 + DISPLAY_WIDTH, TEXTURE_HEIGHT); // 纹理尺寸 // 绘制标题 - 居中显示 - String titleText = "催化剂并行数信息"; + String titleText = Component.translatable("jei.useless_mod.catalyst_info.title").getString(); int titleWidth = minecraft.font.width(titleText); int titleX = (DISPLAY_WIDTH - titleWidth) / 2; guiGraphics.drawString(minecraft.font, titleText, titleX, 10, 0x404040, false); // 调整说明文字的位置,并居中显示 String[] explanationLines = { - "• 催化剂为可选项,除有用锭外会被消耗", - "• 放入催化剂可大幅提高并行数", - "• 并行数影响消耗、产出和能量", - "• 合成无用锭时催化剂无效" + Component.translatable("jei.useless_mod.catalyst_info.desc1").getString(), + Component.translatable("jei.useless_mod.catalyst_info.desc2").getString(), + Component.translatable("jei.useless_mod.catalyst_info.desc3").getString(), + Component.translatable("jei.useless_mod.catalyst_info.desc4").getString(), + Component.translatable("jei.useless_mod.catalyst_info.desc5").getString(), + Component.translatable("jei.useless_mod.catalyst_info.desc6").getString(), + Component.translatable("jei.useless_mod.catalyst_info.desc7").getString(), + Component.translatable("jei.useless_mod.catalyst_info.desc8").getString() }; - int explanationY = 85; + int explanationY = 72; for (String line : explanationLines) { int lineWidth = minecraft.font.width(line); int lineX = (DISPLAY_WIDTH - lineWidth) / 2; guiGraphics.drawString(minecraft.font, line, lineX, explanationY, 0x404040, false); - explanationY += 10; + explanationY += 9; } } - // 自定义工具提示回调 - private class CatalystTooltipCallback implements IRecipeSlotTooltipCallback { - private final int catalystIndex; - - public CatalystTooltipCallback(int index) { - this.catalystIndex = index; - } - - @Override - public void onTooltip(mezz.jei.api.gui.ingredient.IRecipeSlotView recipeSlotView, List tooltip) { - if (catalystIndex < catalystStacks.length) { - ItemStack stack = catalystStacks[catalystIndex]; - int parallel = CatalystManager.getCatalystParallel(stack); - String catalystName = CatalystManager.getCatalystName(stack); - - // 清空原有工具提示,添加自定义工具提示 - tooltip.clear(); - tooltip.add(Component.literal(catalystName).withStyle(ChatFormatting.GOLD)); - - // 检查是否为USEFUL_INGOT - if (stack.getItem() == ModIngots.USEFUL_INGOT.get()) { - tooltip.add(Component.literal("并行数: 无上限").withStyle(ChatFormatting.GREEN)); - tooltip.add(Component.literal("")); - tooltip.add(Component.literal("效果说明:").withStyle(ChatFormatting.YELLOW)); - tooltip.add(Component.literal("• 催化剂不会被消耗").withStyle(ChatFormatting.GREEN)); - tooltip.add(Component.literal("• 无并行数上限").withStyle(ChatFormatting.GREEN)); - tooltip.add(Component.literal("• 能量消耗不会倍增").withStyle(ChatFormatting.GREEN)); - tooltip.add(Component.literal("• 处理时间保持不变").withStyle(ChatFormatting.GRAY)); - tooltip.add(Component.literal("注意: 合成无用锭时催化剂无效").withStyle(ChatFormatting.RED)); - } else { - tooltip.add(Component.literal("并行数: " + parallel + "倍").withStyle(ChatFormatting.GREEN)); - tooltip.add(Component.literal("")); - tooltip.add(Component.literal("效果说明:").withStyle(ChatFormatting.YELLOW)); - tooltip.add(Component.literal("• 输入物品消耗 × " + parallel).withStyle(ChatFormatting.GRAY)); - tooltip.add(Component.literal("• 输出物品数量 × " + parallel).withStyle(ChatFormatting.GRAY)); - tooltip.add(Component.literal("• 能量消耗 × " + parallel).withStyle(ChatFormatting.GRAY)); - tooltip.add(Component.literal("• 处理时间保持不变").withStyle(ChatFormatting.GRAY)); - // 添加黑名单说明 - tooltip.add(Component.literal("⚠ 催化剂会被消耗").withStyle(ChatFormatting.RED)); - tooltip.add(Component.literal("注意: 合成无用锭时催化剂无效").withStyle(ChatFormatting.RED)); - } - } - } + // 催化剂信息类 + public static class CatalystInfo { + // 这个类可以是空的,因为我们不需要额外的数据 } - // 自定义物品渲染器,显示并行数信息 - private class CatalystItemStackRenderer implements IIngredientRenderer { + // 自定义工具提示回调 + private class CatalystTooltipCallback implements IRecipeSlotRichTooltipCallback { private final int catalystIndex; - public CatalystItemStackRenderer(int index) { + CatalystTooltipCallback(int index) { this.catalystIndex = index; } @Override - public void render(GuiGraphics guiGraphics, ItemStack stack) { - if (stack == null || stack.isEmpty()) return; - - // 启用深度测试 - RenderSystem.enableDepthTest(); - - // 渲染物品图标 - 在18*18的栏位内居中显示 - guiGraphics.renderFakeItem(stack, 1, 1); - - // 渲染并行数文本 - Minecraft mc = Minecraft.getInstance(); - int parallel = CatalystManager.getCatalystParallel(stack); - String parallelText; - - // 检查是否为USEFUL_INGOT - if (stack.getItem() == ModIngots.USEFUL_INGOT.get()) { - parallelText = "∞"; - } else { - parallelText = parallel + "x"; - } - - int textWidth = mc.font.width(parallelText); - - guiGraphics.pose().pushPose(); - guiGraphics.pose().translate(0, 0, 200.0F); - guiGraphics.pose().scale(0.5f, 0.5f, 1.0F); - - int x = (int)((16 - textWidth * 0.5f) / 0.5f); - int y = (int)(12 / 0.5f); - - guiGraphics.drawString(mc.font, parallelText, x, y, 0x00FF00, true); - guiGraphics.pose().popPose(); - - RenderSystem.disableBlend(); - } - - // 使用新的getTooltip方法 - @Override - public void getTooltip(mezz.jei.api.gui.builder.ITooltipBuilder tooltip, ItemStack ingredient, TooltipFlag tooltipFlag) { - if (catalystIndex < catalystStacks.length) { - int parallel = CatalystManager.getCatalystParallel(ingredient); - String catalystName = CatalystManager.getCatalystName(ingredient); + public void onRichTooltip(@NotNull IRecipeSlotView recipeSlotView, @NotNull ITooltipBuilder tooltip) { + if (this.catalystIndex < CatalystInfoCategory.this.catalystStacks.size()) { + ItemStack stack = CatalystInfoCategory.this.catalystStacks.get(this.catalystIndex); + int tier = CatalystParallelManager.getCatalystTier(stack); + String catalystName = CatalystParallelManager.getCatalystDisplayName(stack); + // 添加自定义工具提示 tooltip.add(Component.literal(catalystName).withStyle(ChatFormatting.GOLD)); - - // 检查是否为USEFUL_INGOT - if (ingredient.getItem() == ModIngots.USEFUL_INGOT.get()) { - tooltip.add(Component.literal("并行数: 无上限").withStyle(ChatFormatting.GREEN)); - tooltip.add(Component.literal("")); - tooltip.add(Component.literal("效果说明:").withStyle(ChatFormatting.YELLOW)); - tooltip.add(Component.literal("• 催化剂不会被消耗").withStyle(ChatFormatting.GREEN)); - tooltip.add(Component.literal("• 无并行数上限").withStyle(ChatFormatting.GREEN)); - tooltip.add(Component.literal("• 能量消耗不会倍增").withStyle(ChatFormatting.GREEN)); - tooltip.add(Component.literal("• 处理时间保持不变").withStyle(ChatFormatting.GRAY)); - tooltip.add(Component.literal("注意: 合成无用锭时催化剂无效").withStyle(ChatFormatting.RED)); - } else { - tooltip.add(Component.literal("并行数: " + parallel + "倍").withStyle(ChatFormatting.GREEN)); - tooltip.add(Component.literal("")); - tooltip.add(Component.literal("效果说明:").withStyle(ChatFormatting.YELLOW)); - tooltip.add(Component.literal("• 输入物品消耗 × " + parallel).withStyle(ChatFormatting.GRAY)); - tooltip.add(Component.literal("• 输出物品数量 × " + parallel).withStyle(ChatFormatting.GRAY)); - tooltip.add(Component.literal("• 能量消耗 × " + parallel).withStyle(ChatFormatting.GRAY)); - tooltip.add(Component.literal("• 处理时间保持不变").withStyle(ChatFormatting.GRAY)); - // 添加黑名单说明 - tooltip.add(Component.literal("⚠ 催化剂会被消耗").withStyle(ChatFormatting.RED)); - tooltip.add(Component.literal("注意: 合成无用锭时催化剂无效").withStyle(ChatFormatting.RED)); - } - } - } - // 保持已弃用方法的重写以避免编译错误 - @Override - @Deprecated - public List getTooltip(ItemStack ingredient, TooltipFlag tooltipFlag) { - List tooltip = new ArrayList<>(); - if (catalystIndex < catalystStacks.length) { - int parallel = CatalystManager.getCatalystParallel(ingredient); - String catalystName = CatalystManager.getCatalystName(ingredient); - - tooltip.add(Component.literal(catalystName).withStyle(ChatFormatting.GOLD)); - // 检查是否为USEFUL_INGOT - if (ingredient.getItem() == ModIngots.USEFUL_INGOT.get()) { - tooltip.add(Component.literal("并行数: 无上限").withStyle(ChatFormatting.GREEN)); - tooltip.add(Component.literal("")); - tooltip.add(Component.literal("效果说明:").withStyle(ChatFormatting.YELLOW)); - tooltip.add(Component.literal("• 催化剂不会被消耗").withStyle(ChatFormatting.GREEN)); - tooltip.add(Component.literal("• 无并行数上限").withStyle(ChatFormatting.GREEN)); - tooltip.add(Component.literal("• 能量消耗不会倍增").withStyle(ChatFormatting.GREEN)); - tooltip.add(Component.literal("• 处理时间保持不变").withStyle(ChatFormatting.GRAY)); - tooltip.add(Component.literal("注意: 合成无用锭时催化剂无效").withStyle(ChatFormatting.RED)); + if (CatalystParallelManager.isUsefulIngot(stack)) { + tooltip.add(Component.translatable("jei.useless_mod.tooltip.catalyst.parallel.unlimited").withStyle(ChatFormatting.GREEN)); + tooltip.add(Component.empty()); + tooltip.add(Component.translatable("jei.useless_mod.tooltip.catalyst.effect.title").withStyle(ChatFormatting.YELLOW)); + tooltip.add(Component.translatable("jei.useless_mod.tooltip.catalyst.effect.not_consumed").withStyle(ChatFormatting.GREEN)); + tooltip.add(Component.translatable("jei.useless_mod.tooltip.catalyst.effect.unlimited").withStyle(ChatFormatting.GREEN)); + tooltip.add(Component.translatable("jei.useless_mod.tooltip.catalyst.effect.no_energy_mult").withStyle(ChatFormatting.GREEN)); + tooltip.add(Component.translatable("jei.useless_mod.tooltip.catalyst.effect.time_unchanged").withStyle(ChatFormatting.GRAY)); + tooltip.add(Component.translatable("jei.useless_mod.tooltip.catalyst.effect.useless_warning").withStyle(ChatFormatting.RED)); } else { - tooltip.add(Component.literal("并行数: " + parallel + "倍").withStyle(ChatFormatting.GREEN)); - tooltip.add(Component.literal("")); - tooltip.add(Component.literal("效果说明:").withStyle(ChatFormatting.YELLOW)); - tooltip.add(Component.literal("• 输入物品消耗 × " + parallel).withStyle(ChatFormatting.GRAY)); - tooltip.add(Component.literal("• 输出物品数量 × " + parallel).withStyle(ChatFormatting.GRAY)); - tooltip.add(Component.literal("• 能量消耗 × " + parallel).withStyle(ChatFormatting.GRAY)); - tooltip.add(Component.literal("• 处理时间保持不变").withStyle(ChatFormatting.GRAY)); - // 添加黑名单说明 - tooltip.add(Component.literal("⚠ 催化剂会被消耗").withStyle(ChatFormatting.RED)); - tooltip.add(Component.literal("注意: 合成无用锭时催化剂无效").withStyle(ChatFormatting.RED)); + // 普通无用锭催化剂 + int normalParallel = CatalystParallelManager.calculateParallelForNormalRecipe(stack); + tooltip.add(Component.translatable("jei.useless_mod.tooltip.catalyst.normal.parallel", normalParallel) + .withStyle(ChatFormatting.GREEN)); + tooltip.add(Component.empty()); + tooltip.add(Component.translatable("jei.useless_mod.tooltip.catalyst.cross_tier.title").withStyle(ChatFormatting.YELLOW)); + + // 显示跨阶合成信息 + if (tier >= 2 && tier <= 9) { + for (int targetTier = 1; targetTier < tier; targetTier++) { + int crossTierParallel = CatalystParallelManager.calculateParallelForUselessIngotRecipe(targetTier); + tooltip.add(Component.translatable("jei.useless_mod.tooltip.catalyst.cross_tier.format", targetTier, crossTierParallel) + .withStyle(ChatFormatting.GRAY)); + } + } + + tooltip.add(Component.empty()); + tooltip.add(Component.translatable("jei.useless_mod.tooltip.parallel.title").withStyle(ChatFormatting.YELLOW)); + tooltip.add(Component.translatable("jei.useless_mod.tooltip.parallel.desc1").withStyle(ChatFormatting.GRAY)); + tooltip.add(Component.translatable("jei.useless_mod.tooltip.parallel.desc2").withStyle(ChatFormatting.GRAY)); + tooltip.add(Component.translatable("jei.useless_mod.tooltip.parallel.desc3").withStyle(ChatFormatting.GRAY)); + tooltip.add(Component.translatable("jei.useless_mod.tooltip.parallel.desc4").withStyle(ChatFormatting.GRAY)); } } - return tooltip; - } - - @Override - public int getWidth() { - return 16; - } - - @Override - public int getHeight() { - return 16; } } - - // 催化剂信息类 - public static class CatalystInfo { - // 这个类可以是空的,因为我们不需要额外的数据 - } -} \ No newline at end of file +} diff --git a/src/main/java/com/sorrowmist/useless/compat/jei/GenericStackJeiIngredientProvider.java b/src/main/java/com/sorrowmist/useless/compat/jei/GenericStackJeiIngredientProvider.java new file mode 100644 index 00000000..5ad5da29 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/jei/GenericStackJeiIngredientProvider.java @@ -0,0 +1,43 @@ +package com.sorrowmist.useless.compat.jei; + +import appeng.api.stacks.GenericStack; +import mezz.jei.api.gui.builder.IRecipeSlotBuilder; +import mezz.jei.api.ingredients.IIngredientRenderer; +import mezz.jei.api.ingredients.IIngredientType; +import org.jetbrains.annotations.Nullable; + +/** Optional fallback bridge for an AE key type that is not registered with AE2 JEI Integration. */ +public interface GenericStackJeiIngredientProvider { + GenericStackJeiIngredientProvider NONE = new GenericStackJeiIngredientProvider() { + @Override + public @Nullable Ingredient resolve(GenericStack stack) { + return null; + } + }; + + @Nullable + Ingredient resolve(GenericStack stack); + + record Ingredient(IIngredientType type, + Object value, + @Nullable IIngredientRenderer renderer) { + public Ingredient(IIngredientType type, Object value) { + this(type, value, null); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + public java.util.Optional> clickable( + mezz.jei.api.runtime.IIngredientManager manager, net.minecraft.client.renderer.Rect2i area) { + return (java.util.Optional) manager.createClickableIngredient((IIngredientType) type, value, area, false); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + public void addTo(IRecipeSlotBuilder slot) { + IIngredientType type = this.type; + slot.addIngredient(type, this.value); + if (this.renderer != null) { + slot.setCustomRenderer(type, (IIngredientRenderer) this.renderer); + } + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/jei/GenericStackJeiIngredientProviders.java b/src/main/java/com/sorrowmist/useless/compat/jei/GenericStackJeiIngredientProviders.java new file mode 100644 index 00000000..5a0e11fb --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/jei/GenericStackJeiIngredientProviders.java @@ -0,0 +1,86 @@ +package com.sorrowmist.useless.compat.jei; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import appeng.api.integrations.jei.IngredientConverter; +import appeng.api.integrations.jei.IngredientConverters; +import appeng.api.stacks.GenericStack; +import appeng.integration.modules.jei.ItemIngredientConverter; +import appeng.integration.modules.jei.FluidIngredientConverter; +import com.mojang.logging.LogUtils; +import mezz.jei.api.constants.VanillaTypes; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.fml.ModList; +import org.jetbrains.annotations.Nullable; + +/** Use AE2 15's live converter registry, never a wrapped ItemStack as a JEI ingredient. */ +public final class GenericStackJeiIngredientProviders { + private static final org.slf4j.Logger LOGGER = LogUtils.getLogger(); + private static final ItemIngredientConverter ITEMS = new ItemIngredientConverter(); + private static final FluidIngredientConverter FLUIDS = new FluidIngredientConverter(); + private static volatile GenericStackJeiIngredientProvider optionalProvider = GenericStackJeiIngredientProvider.NONE; + private static volatile boolean initialized; + private GenericStackJeiIngredientProviders() {} + + public static synchronized void initialize() { + if (initialized) return; + if (ModList.get().isLoaded("appmek")) { + try { + optionalProvider = (GenericStackJeiIngredientProvider) Class.forName( + "com.sorrowmist.useless.compat.appmek.AppMekJeiChemicalCompat") + .getMethod("createProvider").invoke(null); + } catch (ReflectiveOperationException | LinkageError exception) { + RateLimitedLogger.warn(LOGGER, "Could not enable native Mekanism chemical JEI rendering", exception); + } + } + initialized = true; + } + + public static @Nullable GenericStackJeiIngredientProvider.Ingredient resolve(GenericStack stack) { + if (!valid(stack)) return null; + // JEMI 1.1 does not preserve helper amounts and native int stacks truncate long values. + // Its dedicated serializer stores the full key, so EMI does not need the JEI UID catalogue. + if (ModList.get() != null && ModList.get().isLoaded("emi")) return new GenericStackJeiIngredientProvider.Ingredient( + AEKeyJeiIngredients.TYPE, new AEKeyJeiIngredients.KeyIngredient(stack.what(), stack.amount())); + var nativeIngredient = resolveNative(stack); + return nativeIngredient != null ? nativeIngredient : AEKeyJeiIngredients.resolve(stack); + } + + /** Also used by the fallback converter; exclude itself to avoid recursive resolution. */ + public static @Nullable GenericStackJeiIngredientProvider.Ingredient resolveNative(GenericStack stack) { + if (!valid(stack)) return null; + var item = convert(stack, ITEMS); + if (item != null) return item; + var fluid = convert(stack, FLUIDS); + if (fluid != null) return fluid; + var chemical = optionalProvider.resolve(stack); + if (chemical != null) return chemical; + // Fetch on every call: Applied Mekanistics registers its four converters at runtime, + // after JEI's category registration, and other addons may do the same. + for (var converter : IngredientConverters.getConverters()) { + if (converter == AEKeyJeiIngredients.CONVERTER) continue; + var converted = convert(stack, converter); + if (converted != null) return converted; + } + return null; + } + + private static boolean valid(GenericStack stack) { + return stack != null && stack.what() != null && stack.amount() > 0; + } + + private static @Nullable GenericStackJeiIngredientProvider.Ingredient convert( + GenericStack stack, IngredientConverter converter) { + try { + var type = converter.getIngredientType(); + T ingredient = converter.getIngredientFromStack(stack); + if (type == null || ingredient == null || !type.getIngredientClass().isInstance(ingredient)) return null; + // A custom slot renderer cannot fix bookmark identity, so reject wrapped fallback items. + if (type == VanillaTypes.ITEM_STACK && ingredient instanceof ItemStack item + && (item.isEmpty() || GenericStack.isWrapped(item))) return null; + return new GenericStackJeiIngredientProvider.Ingredient(type, ingredient); + } catch (RuntimeException | LinkageError exception) { + return null; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/jei/ItemStackCountRenderer.java b/src/main/java/com/sorrowmist/useless/compat/jei/ItemStackCountRenderer.java new file mode 100644 index 00000000..1ef21a1e --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/jei/ItemStackCountRenderer.java @@ -0,0 +1,104 @@ +package com.sorrowmist.useless.compat.jei; + +import com.mojang.blaze3d.systems.RenderSystem; +import mezz.jei.api.ingredients.IIngredientRenderer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + * 物品槽位渲染器:以缩写形式绘制堆叠数量。 + * + *

JEI 默认经 {@code GuiGraphics#renderItemDecorations} 绘制完整数量文本, + * 四位以上数字的宽度超出 16 像素槽位并溢出到相邻槽位。本渲染器仅绘制物品图标, + * 数量改为缩写并缩小字号后右对齐绘制,使文本宽度受控。 + * + *

缩放与 Z 层偏移的处理方式参考 Applied Energistics 2 的 + * {@code appeng.client.gui.me.common.StackSizeRenderer}。 + * + *

提示文本与 JEI 内置物品渲染器保持一致,槽位自身的富提示回调不受影响。 + */ +public class ItemStackCountRenderer implements IIngredientRenderer { + public static final ItemStackCountRenderer INSTANCE = new ItemStackCountRenderer(); + + /** 槽位渲染尺寸,与 Minecraft 物品槽位一致。 */ + private static final int SLOT_SIZE = 16; + + /** 数量文本缩放系数 */ + private static final float COUNT_TEXT_SCALE = 0.666f; + + /** + * 数量文本的 Z 层偏移。 + * + *

物品模型以三维形式绘制在较高的 Z 层,文本缺少该偏移时会被模型遮挡。 + */ + private static final int COUNT_TEXT_Z_OFFSET = 200; + + /** 数量文本基线相对槽位下边缘的距离,单位为缩放前的像素。 */ + private static final float COUNT_TEXT_BASELINE_FROM_BOTTOM = 5.0f; + + @Override + public void render(GuiGraphics guiGraphics, @Nullable ItemStack ingredient) { + render(guiGraphics, ingredient, 0, 0); + } + + @Override + public void render(GuiGraphics guiGraphics, @Nullable ItemStack ingredient, int posX, int posY) { + if (ingredient == null || ingredient.isEmpty()) { + return; + } + + RenderSystem.enableDepthTest(); + guiGraphics.renderFakeItem(ingredient, posX, posY); + + int count = ingredient.getCount(); + if (count > 1) { + Font font = getFontRenderer(Minecraft.getInstance(), ingredient); + String text = formatCount(count); + + // 坐标在缩放后的坐标系中计算,故按缩放系数的倒数折算回缩放前的像素空间。 + float inverseScale = 1.0f / COUNT_TEXT_SCALE; + float textWidth = font.width(text) * COUNT_TEXT_SCALE; + int textX = (int) ((posX + SLOT_SIZE - textWidth) * inverseScale); + int textY = (int) ((posY + SLOT_SIZE - COUNT_TEXT_BASELINE_FROM_BOTTOM * COUNT_TEXT_SCALE) + * inverseScale); + + guiGraphics.pose().pushPose(); + guiGraphics.pose().translate(0.0F, 0.0F, COUNT_TEXT_Z_OFFSET); + guiGraphics.pose().scale(COUNT_TEXT_SCALE, COUNT_TEXT_SCALE, COUNT_TEXT_SCALE); + guiGraphics.drawString(font, text, textX, textY, 0xFFFFFF, true); + guiGraphics.pose().popPose(); + } + + RenderSystem.disableBlend(); + } + + @Override + public List getTooltip(ItemStack ingredient, TooltipFlag tooltipFlag) { + return ingredient.getTooltipLines(Minecraft.getInstance().player, tooltipFlag); + } + + /** + * 将数值缩写为受控宽度的文本,千位以上使用 k / M / G 后缀。 + * + * @param count 待格式化的数值 + * @return 缩写后的文本 + */ + private static String formatCount(long count) { + if (count >= 1000000000L) { + return String.format("%.2fG", count / 1000000000.0); + } else if (count >= 1000000L) { + return String.format("%.2fM", count / 1000000.0); + } else if (count >= 1000L) { + return String.format("%.2fk", count / 1000.0); + } else { + return String.valueOf(count); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/jei/JEIPlugin.java b/src/main/java/com/sorrowmist/useless/compat/jei/JEIPlugin.java index ba603558..cc5cd7ca 100644 --- a/src/main/java/com/sorrowmist/useless/compat/jei/JEIPlugin.java +++ b/src/main/java/com/sorrowmist/useless/compat/jei/JEIPlugin.java @@ -1,247 +1,433 @@ package com.sorrowmist.useless.compat.jei; +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import appeng.menu.me.items.PatternEncodingTermMenu; import com.sorrowmist.useless.UselessMod; -import com.sorrowmist.useless.blocks.advancedalloyfurnace.AdvancedAlloyFurnaceBlock; -import com.sorrowmist.useless.compat.thermal.ThermalRecipeConverter; -import com.sorrowmist.useless.recipes.ModRecipeTypes; -import com.sorrowmist.useless.registry.ModIngots; +import com.sorrowmist.useless.client.gui.ChainGroupScreen; +import com.sorrowmist.useless.client.gui.DimensionConfigScreen; +import com.sorrowmist.useless.client.gui.StaffLinkScreen; +import com.sorrowmist.useless.content.menus.DimensionConfigMenu; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeCatalog; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeIdentity; +import com.sorrowmist.useless.content.stafflink.LinkFilterSlot; +import com.sorrowmist.useless.content.stafflink.StaffLinkFilters; +import com.sorrowmist.useless.content.stafflink.StaffLinkRoute; +import com.sorrowmist.useless.init.ModBlocks; +import com.sorrowmist.useless.init.ModTags; import mezz.jei.api.IModPlugin; import mezz.jei.api.JeiPlugin; +import mezz.jei.api.gui.handlers.IGhostIngredientHandler; +import mezz.jei.api.helpers.IGuiHelper; +import mezz.jei.api.ingredients.ITypedIngredient; +import mezz.jei.api.registration.IGuiHandlerRegistration; import mezz.jei.api.registration.IRecipeCatalystRegistration; import mezz.jei.api.registration.IRecipeCategoryRegistration; import mezz.jei.api.registration.IRecipeRegistration; +import mezz.jei.api.registration.IRecipeTransferRegistration; +import mezz.jei.api.runtime.IJeiRuntime; import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.Rect2i; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.item.BlockItem; import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fml.ModList; +import org.jetbrains.annotations.NotNull; import java.util.ArrayList; -import java.util.Collection; +import java.util.LinkedHashMap; import java.util.List; -import java.util.Objects; +import java.util.Map; @JeiPlugin -public class JEIPlugin implements IModPlugin { +public final class JEIPlugin implements IModPlugin { + private static final ResourceLocation UID = UselessMod.id("jei_plugin"); + private static IJeiRuntime runtime; + private static final Map + registeredAlloyFurnaceRecipes = new LinkedHashMap<>(); + + @Override + public @NotNull ResourceLocation getPluginUid() { + return UID; + } + @Override - public ResourceLocation getPluginUid() { - return ResourceLocation.fromNamespaceAndPath(UselessMod.MOD_ID, "jei_plugin"); + public void registerIngredients(mezz.jei.api.registration.IModIngredientRegistration registration) { + GenericStackJeiIngredientProviders.initialize(); + AEKeyJeiIngredients.register(registration, net.minecraftforge.fml.loading.FMLPaths.CONFIGDIR.get() + .resolve("useless_mod/jei_ae_keys.nbt")); } @Override public void registerCategories(IRecipeCategoryRegistration registration) { - registration.addRecipeCategories(new AdvancedAlloyFurnaceRecipeCategory(registration.getJeiHelpers().getGuiHelper())); - registration.addRecipeCategories(new CatalystInfoCategory(registration.getJeiHelpers().getGuiHelper())); + GenericStackJeiIngredientProviders.initialize(); + IGuiHelper guiHelper = registration.getJeiHelpers().getGuiHelper(); + registration.addRecipeCategories(new AdvancedAlloyFurnaceRecipeCategory(guiHelper)); + registration.addRecipeCategories(new CatalystInfoCategory(guiHelper)); + } + + @Override + public void registerRecipes(@NotNull IRecipeRegistration registration) { + Level level = Minecraft.getInstance().level; + // JEMI imports a snapshot of JEI immediately after registration. Returning an empty + // list while waiting for the client tick loses the entire category in EMI. + List recipes = level == null + ? List.of() + : AlloyFurnaceRecipeCatalog.entries(level); + registeredAlloyFurnaceRecipes.clear(); + for (AlloyFurnaceRecipeCatalog.Entry recipe : recipes) { + registeredAlloyFurnaceRecipes.put(recipe.identity(), recipe); + } + registration.addRecipes(AdvancedAlloyFurnaceRecipeCategory.TYPE, recipes); + registration.addRecipes(CatalystInfoCategory.TYPE, + List.of(new CatalystInfoCategory.CatalystInfo())); + } + + + @Override + public void registerRecipeTransferHandlers(IRecipeTransferRegistration registration) { + // Only this category encodes omniversal patterns. Every other page in a pattern encoding + // terminal keeps falling through to ae2jeiintegration's universal handler, which JEI consults + // only when no category-specific handler is registered for the open menu. + registration.addRecipeTransferHandler( + new OmniversalPatternJeiTransferHandler<>( + PatternEncodingTermMenu.class, + PatternEncodingTermMenu.TYPE, + registration.getTransferHelper()), + AdvancedAlloyFurnaceRecipeCategory.TYPE); + registerWirelessTransferHandler(registration); + registerTianshuTransferHandlers(registration); } @Override - public void registerRecipes(IRecipeRegistration registration) { - RecipeManager recipeManager = Objects.requireNonNull(Minecraft.getInstance().level).getRecipeManager(); - - // 获取所有高级合金炉配方 - List recipes = new ArrayList<>(); - recipes.addAll(recipeManager.getAllRecipesFor(ModRecipeTypes.ADVANCED_ALLOY_FURNACE_TYPE.get())); - - // 添加所有原版熔炉配方,转换为高级合金炉配方格式 - Collection smeltingRecipes = recipeManager.getAllRecipesFor( - net.minecraft.world.item.crafting.RecipeType.SMELTING - ); - - for (net.minecraft.world.item.crafting.SmeltingRecipe recipe : smeltingRecipes) { - // 将原版熔炉配方转换为高级合金炉配方 - List smeltingIngredients = recipe.getIngredients(); - List inputIngredients = new ArrayList<>(); - List inputCounts = new ArrayList<>(); - - for (net.minecraft.world.item.crafting.Ingredient ingredient : smeltingIngredients) { - inputIngredients.add(ingredient); - inputCounts.add(1); + public void registerGuiHandlers(IGuiHandlerRegistration registration) { + registration.addGenericGuiContainerHandler(net.minecraft.client.gui.screens.inventory.AbstractContainerScreen.class, + new WrappedKeyGuiHandler(() -> runtime == null ? null : runtime.getIngredientManager())); + registration.addGhostIngredientHandler(DimensionConfigScreen.class, + new DimensionConfigGhostHandler()); + registration.addGhostIngredientHandler(StaffLinkScreen.class, + new StaffLinkGhostHandler()); + registration.addGhostIngredientHandler(ChainGroupScreen.class, + new ChainGroupGhostHandler()); + // 等价组界面继承了 AbstractContainerScreen,JEI 内置的容器屏 handler 会自动接管, + // 不需要(也不该)再注册 IScreenHandler——两套 handler 会互相抢。 + } + + /** + * 连锁等价组界面接受 JEI 拖拽。 + * + *

每个组行右端有一个拖拽槽,把方块拖进去就作为一条精确方块 ID 追加到该组。 + * 非方块物品直接不接受——等价组判定的是方块,收一个没有方块的物品没有意义。

+ */ + private static final class ChainGroupGhostHandler + implements IGhostIngredientHandler { + @Override + public List> getTargetsTyped(ChainGroupScreen screen, + ITypedIngredient ingredient, + boolean doStart) { + ItemStack stack = ingredient.getItemStack().orElse(ItemStack.EMPTY); + if (!(stack.getItem() instanceof BlockItem blockItem)) return List.of(); + ResourceLocation blockId = BuiltInRegistries.BLOCK.getKey(blockItem.getBlock()); + + List> targets = new ArrayList<>(screen.chainGroupCount()); + for (int index = 0; index < screen.chainGroupCount(); index++) { + if (!screen.isGroupRowVisible(index)) continue; + final int groupIndex = index; + targets.add(new Target<>() { + @Override + public Rect2i getArea() { + return new Rect2i(screen.groupDropZoneScreenX(groupIndex), + screen.groupDropZoneScreenY(groupIndex), + screen.dropZoneSize(), screen.dropZoneSize()); + } + + @Override + public void accept(I value) { + screen.addEntryFromBlock(groupIndex, blockId); + } + }); + } + return targets; + } + + @Override + public void onComplete() { + } + } + + /** + * 无线物流的过滤槽接受 JEI 拖拽。 + * + *

标记按线路的资源类型分流:物品线路收物品,流体线路收流体本身(不是装它的桶), + * 化学品线路收一只装满它的储罐。类型对不上的原料直接不接受——不接,比悄悄塞进一个 + * 语义不对的东西好。

+ * + *

拖进来的只能是具体标记。{@code #tag} / 通配符那类模式表达的是「一整类」, + * 从 JEI 拖某个具体物品过来时玩家要的显然是那个物品,所以这里不替玩家猜「你是不是想要 + * 这个物品的标签」——要模式请右键格子手打。

+ */ + private static final class StaffLinkGhostHandler + implements IGhostIngredientHandler { + @Override + public List> getTargetsTyped(StaffLinkScreen screen, + ITypedIngredient ingredient, + boolean doStart) { + // 过滤格住在覆盖层面板里。面板没开时那些坐标指向的是主界面别的地方, + // 拖过去会莫名其妙地落到空处(甚至落到底下某个控件上),所以直接不给目标。 + if (!screen.isFilterPanelOpen()) { + screen.setJeiDragActive(false); + return List.of(); + } + StaffLinkRoute config = screen.getMenu().getSelectedConfig(); + if (config == null || !config.filterApplies()) { + screen.setJeiDragActive(false); + return List.of(); } - - List outputItems = new ArrayList<>(); - outputItems.add(recipe.getResultItem(null)); - - List emptyFluids = new ArrayList<>(); - net.minecraft.world.item.crafting.Ingredient furnaceIngredient = net.minecraft.world.item.crafting.Ingredient.of(net.minecraft.world.item.Items.FURNACE); - - // 创建无用锭标签的催化剂,允许使用所有无用锭 - net.minecraft.tags.TagKey uselessIngotTag = - net.minecraft.tags.TagKey.create( - net.minecraft.core.registries.Registries.ITEM, - new net.minecraft.resources.ResourceLocation("useless_mod", "useless_ingots") - ); - net.minecraft.world.item.crafting.Ingredient uselessIngotCatalyst = net.minecraft.world.item.crafting.Ingredient.of(uselessIngotTag); - - // 将List转换为List - List longInputCounts = new ArrayList<>(); - for (Integer count : inputCounts) { - longInputCounts.add(count.longValue()); + LinkFilterSlot marker = StaffLinkFilters.fromIngredient( + config.medium(), ingredient.getIngredient()); + if (marker == null) { + screen.setJeiDragActive(false); + return List.of(); + } + // 真的要开始拖了(doStart=true)才点亮拖拽高亮;doStart=false 那次只是问「有没有目标」。 + if (doStart) { + screen.setJeiDragActive(true); + } + + // 详细面板开着时它压住中间,靶点切成它的三个槽(A 标记 / 输出端 B / 输入端 B)。 + if (screen.isDetailPanelOpen()) { + return detailTargets(screen, marker); } - - // 创建临时的高级合金炉配方,使用原版熔炉作为模具 - com.sorrowmist.useless.recipes.advancedalloyfurnace.AdvancedAlloyFurnaceRecipe convertedRecipe = new com.sorrowmist.useless.recipes.advancedalloyfurnace.AdvancedAlloyFurnaceRecipe( - recipe.getId(), - inputIngredients, - longInputCounts, - emptyFluids, - outputItems, - emptyFluids, - 1000, // 能量消耗 - 40, // 处理时间:熔炉配方为40tick - uselessIngotCatalyst, // 催化剂:无用锭标签 - 1, // 催化剂数量 - furnaceIngredient // 模具:原版熔炉 - ); - - recipes.add(convertedRecipe); + + List> targets = new ArrayList<>(StaffLinkScreen.filterSlotCount()); + for (int index = 0; index < StaffLinkScreen.filterSlotCount(); index++) { + final int slotIndex = index; + targets.add(new Target<>() { + @Override + public Rect2i getArea() { + return new Rect2i(screen.filterSlotScreenX(slotIndex), + screen.filterSlotScreenY(slotIndex), + StaffLinkScreen.filterSlotSize(), StaffLinkScreen.filterSlotSize()); + } + + @Override + public void accept(I value) { + screen.getMenu().setFilterSlot(slotIndex, marker); + } + }); + } + return targets; } - // 添加热力配方转换 - try { - // 直接从RecipeManager获取所有InsolatorRecipe并转换 - List convertedRecipes = - ThermalRecipeConverter.convertInsolatorRecipes(recipeManager); - recipes.addAll(convertedRecipes); - - // 添加热力感应炉(Smelter)配方转换 - List smelterRecipes = - ThermalRecipeConverter.convertSmelterRecipes(recipeManager); - recipes.addAll(smelterRecipes); - - // 添加热力多驱冲压机(Press)配方转换 - List pressRecipes = - ThermalRecipeConverter.convertPressRecipes(recipeManager); - recipes.addAll(pressRecipes); - } catch (Exception e) { - // 如果热力模组未加载,捕获异常以避免崩溃 + /** 详细面板的三个靶点:0 = A 标记,1 = 输出端 B,2 = 输入端 B。 */ + private List> detailTargets(StaffLinkScreen screen, LinkFilterSlot marker) { + int index = screen.getDetailIndex(); + if (index < 0 || index >= StaffLinkScreen.filterSlotCount()) { + return List.of(); + } + List> targets = new ArrayList<>(3); + for (int which = 0; which < 3; which++) { + final int slot = which; + targets.add(new Target<>() { + @Override + public Rect2i getArea() { + return new Rect2i(screen.detailSlotScreenX(slot), screen.detailSlotScreenY(slot), + StaffLinkScreen.detailSlotSize(), StaffLinkScreen.detailSlotSize()); + } + + @Override + public void accept(I value) { + if (slot == 0) { + screen.getMenu().setFilterSlot(index, + screen.getMenu().getFilterMirror().get(index).withMarker(marker)); + } else { + screen.getMenu().setFilterSlotConditionControl(index, slot == 2, marker); + } + } + }); + } + return targets; } - - // 添加Mekanism配方转换 - try { - // 添加Mekanism冶金灌注机(Metallurgic Infuser)配方转换 - List mekanismRecipes = - com.sorrowmist.useless.compat.mekanism.MekanismRecipeConverter.convertMetallurgicInfuserRecipes(recipeManager); - recipes.addAll(mekanismRecipes); - - // 添加Mekanism富集仓(Enrichment Chamber)配方转换 - List enrichmentRecipes = - com.sorrowmist.useless.compat.mekanism.MekanismRecipeConverter.convertEnrichmentChamberRecipes(recipeManager); - recipes.addAll(enrichmentRecipes); - } catch (Exception e) { - // 如果Mekanism模组未加载,捕获异常以避免崩溃 + + @Override + public void onComplete() { } - - // 添加工业先锋配方转换 - try { - // 添加工业先锋化学溶解室(Dissolution Chamber)配方转换 - List dissolutionRecipes = - com.sorrowmist.useless.compat.industrialforegoing.IndustrialForegoingRecipeConverter.convertDissolutionChamberRecipes(recipeManager); - recipes.addAll(dissolutionRecipes); - } catch (Exception e) { - // 如果工业先锋模组未加载,捕获异常以避免崩溃 + } + + private static final class DimensionConfigGhostHandler + implements IGhostIngredientHandler { + @Override + public List> getTargetsTyped(DimensionConfigScreen screen, + ITypedIngredient ingredient, + boolean doStart) { + ItemStack stack = ingredient.getItemStack().orElse(ItemStack.EMPTY); + if (!(stack.getItem() instanceof BlockItem)) return List.of(); + + List> targets = new ArrayList<>(DimensionConfigMenu.GHOST_SLOT_COUNT); + for (int index = 0; index < DimensionConfigMenu.GHOST_SLOT_COUNT; index++) { + if (!screen.getMenu().isGhostSlotActive(index)) continue; + int slotIndex = index; + DimensionConfigMenu.GhostSlot slot = screen.getMenu().getGhostSlot(slotIndex); + targets.add(new Target<>() { + @Override + public Rect2i getArea() { + return new Rect2i(screen.getGuiLeft() + slot.x, + screen.getGuiTop() + slot.y, 16, 16); + } + + @Override + public void accept(I value) { + if (value instanceof ItemStack itemStack) { + screen.getMenu().setGhostSlotFromClient(slotIndex, itemStack); + } + } + }); + } + return targets; } - - // 添加血魔法配方转换 - try { - // 添加血魔法血之祭坛(Blood Altar)配方转换 - List bloodAltarRecipes = - com.sorrowmist.useless.compat.bloodmagic.BloodMagicRecipeConverter.convertBloodAltarRecipes(recipeManager); - recipes.addAll(bloodAltarRecipes); - - // 添加血魔法狱火熔炉(Soul Forge)配方转换 - List soulForgeRecipes = - com.sorrowmist.useless.compat.bloodmagic.BloodMagicRecipeConverter.convertTartaricForgeRecipes(recipeManager); - recipes.addAll(soulForgeRecipes); - - // 添加血魔法炼金术桌(Alchemy Table)配方转换 - List alchemyTableRecipes = - com.sorrowmist.useless.compat.bloodmagic.BloodMagicRecipeConverter.convertAlchemyTableRecipes(recipeManager); - recipes.addAll(alchemyTableRecipes); - } catch (Exception e) { - // 如果血魔法模组未加载,捕获异常以避免崩溃 + + @Override + public void onComplete() { } - - // 添加余烬mod配方转换 + } + + /** + * AE2 Lightning Tech provides separate menu classes for its Tianshu terminals. JEI resolves a + * recipe transfer handler by the exact menu class, so its universal handler would otherwise win + * and encode a normal AE2 processing pattern for this category. + */ + private static void registerTianshuTransferHandlers(IRecipeTransferRegistration registration) { + if (!ModList.get().isLoaded("ae2lt")) return; + registerReflectiveTransferHandler(registration, + "com.moakiee.ae2lt.menu.TianshuPatternEncodingTermMenu"); + registerReflectiveTransferHandler(registration, + "com.moakiee.ae2lt.menu.TianshuWirelessPatternEncodingTermMenu"); + } + + @SuppressWarnings("unchecked") + private static void registerReflectiveTransferHandler( + IRecipeTransferRegistration registration, String menuClassName) { try { - // 添加余烬炼金台(Alchemy Tablet)配方转换 - List embersAlchemyRecipes = - com.sorrowmist.useless.compat.embers.EmbersRecipeConverter.convertAlchemyTabletRecipes(recipeManager); - recipes.addAll(embersAlchemyRecipes); - - // 添加余烬地质分离器(Geologic Separator)配方转换 - List embersGeoSeparatorRecipes = - com.sorrowmist.useless.compat.embers.EmbersRecipeConverter.convertGeoSeparatorRecipes(recipeManager); - recipes.addAll(embersGeoSeparatorRecipes); - - // 添加余烬熔炼炉(Melter)配方转换 - List embersMelterRecipes = - com.sorrowmist.useless.compat.embers.EmbersRecipeConverter.convertMelterRecipes(recipeManager); - recipes.addAll(embersMelterRecipes); - - // 添加余烬压印锤(Stamper)配方转换 - List embersStampingRecipes = - com.sorrowmist.useless.compat.embers.EmbersRecipeConverter.convertStampingRecipes(recipeManager); - recipes.addAll(embersStampingRecipes); - - // 添加余烬混合离心器(Mixer Centrifuge)配方转换 - List embersMixingRecipes = - com.sorrowmist.useless.compat.embers.EmbersRecipeConverter.convertMixingRecipes(recipeManager); - recipes.addAll(embersMixingRecipes); - } catch (Exception e) { - // 如果余烬mod未加载,捕获异常以避免崩溃 + Class menuClass = Class.forName(menuClassName); + if (!PatternEncodingTermMenu.class.isAssignableFrom(menuClass)) return; + var menuType = (MenuType) menuClass.getField("TYPE").get(null); + registration.addRecipeTransferHandler( + new OmniversalPatternJeiTransferHandler<>( + (Class) menuClass, + menuType, + registration.getTransferHelper()), + AdvancedAlloyFurnaceRecipeCategory.TYPE); + } catch (ReflectiveOperationException | ClassCastException exception) { + RateLimitedLogger.warn(UselessMod.LOGGER, + "Could not register the omniversal pattern transfer handler for {}.", + menuClassName, + exception); } + } - registration.addRecipes(AdvancedAlloyFurnaceRecipeCategory.TYPE, recipes); - - // 添加催化剂信息 - List catalystInfos = new ArrayList<>(); - catalystInfos.add(new CatalystInfoCategory.CatalystInfo()); - registration.addRecipes(CatalystInfoCategory.TYPE, catalystInfos); + /** + * The wireless pattern encoding terminal is a {@link PatternEncodingTermMenu} subclass, and JEI + * matches handlers on the menu's exact class, so it needs its own registration. ae2wtlib is only + * a runtime dependency here, hence the reflective lookup; without it the wireless terminal would + * silently fall back to ae2jeiintegration's universal handler and encode a plain pattern. + */ + @SuppressWarnings("unchecked") + private static void registerWirelessTransferHandler(IRecipeTransferRegistration registration) { + if (!ModList.get().isLoaded("ae2wtlib")) return; + try { + Class menuClass = Class.forName("de.mari_023.ae2wtlib.wet.WETMenu"); + if (!PatternEncodingTermMenu.class.isAssignableFrom(menuClass)) return; + var menuType = (MenuType) menuClass.getField("TYPE").get(null); + registration.addRecipeTransferHandler( + new OmniversalPatternJeiTransferHandler<>( + (Class) menuClass, + menuType, + registration.getTransferHelper()), + AdvancedAlloyFurnaceRecipeCategory.TYPE); + } catch (ReflectiveOperationException | ClassCastException exception) { + RateLimitedLogger.warn(UselessMod.LOGGER, + "Could not register the omniversal pattern transfer handler for ae2wtlib's wireless " + + "pattern encoding terminal; it will encode plain patterns instead.", + exception); + } } @Override public void registerRecipeCatalysts(IRecipeCatalystRegistration registration) { + ItemStack singleBlockFurnace = new ItemStack(ModBlocks.ADVANCED_ALLOY_FURNACE_BLOCK.get()); + registration.addRecipeCatalyst(singleBlockFurnace, AdvancedAlloyFurnaceRecipeCategory.TYPE); registration.addRecipeCatalyst( - new ItemStack(AdvancedAlloyFurnaceBlock.ADVANCED_ALLOY_FURNACE_BLOCK.get()), - AdvancedAlloyFurnaceRecipeCategory.TYPE, - CatalystInfoCategory.TYPE - ); - - // 添加各个等级的催化剂作为配方催化剂 - registration.addRecipeCatalyst( - new ItemStack(ModIngots.USELESS_INGOT_TIER_1.get()), - CatalystInfoCategory.TYPE - ); - registration.addRecipeCatalyst( - new ItemStack(ModIngots.USELESS_INGOT_TIER_2.get()), - CatalystInfoCategory.TYPE - ); - registration.addRecipeCatalyst( - new ItemStack(ModIngots.USELESS_INGOT_TIER_3.get()), - CatalystInfoCategory.TYPE - ); - registration.addRecipeCatalyst( - new ItemStack(ModIngots.USELESS_INGOT_TIER_4.get()), - CatalystInfoCategory.TYPE - ); - registration.addRecipeCatalyst( - new ItemStack(ModIngots.USELESS_INGOT_TIER_5.get()), - CatalystInfoCategory.TYPE - ); - registration.addRecipeCatalyst( - new ItemStack(ModIngots.USELESS_INGOT_TIER_6.get()), - CatalystInfoCategory.TYPE - ); + new ItemStack(ModBlocks.MULTIBLOCK_ALLOY_FURNACE_CORE.get()), + AdvancedAlloyFurnaceRecipeCategory.TYPE); + registration.addRecipeCatalyst(singleBlockFurnace, CatalystInfoCategory.TYPE); registration.addRecipeCatalyst( - new ItemStack(ModIngots.USELESS_INGOT_TIER_7.get()), - CatalystInfoCategory.TYPE - ); - registration.addRecipeCatalyst( - new ItemStack(ModIngots.USELESS_INGOT_TIER_8.get()), - CatalystInfoCategory.TYPE - ); - registration.addRecipeCatalyst( - new ItemStack(ModIngots.USELESS_INGOT_TIER_9.get()), - CatalystInfoCategory.TYPE - ); + new ItemStack(ModBlocks.MULTIBLOCK_ALLOY_FURNACE_CORE.get()), + CatalystInfoCategory.TYPE); + + BuiltInRegistries.ITEM.getTag(ModTags.CATALYSTS).ifPresent(tag -> { + for (var holder : tag) { + registration.addRecipeCatalyst(new ItemStack(holder.value()), CatalystInfoCategory.TYPE); + } + }); + } + + @Override + public void onRuntimeAvailable(@NotNull IJeiRuntime jeiRuntime) { + runtime = jeiRuntime; + AEKeyJeiIngredients.setRuntime(jeiRuntime.getIngredientManager()); + refreshAlloyFurnaceRecipes(); + } + + @Override + public void onRuntimeUnavailable() { + runtime = null; + AEKeyJeiIngredients.setRuntime(null); + registeredAlloyFurnaceRecipes.clear(); + } + + /** Adds recipes generated after JEI's initial registration, such as data-driven compat data. */ + public static void refreshAlloyFurnaceRecipes() { + // TagsUpdatedEvent is fired on the server thread during /reload, but JEI's recipe manager + // must be mutated on the client render thread. Hop back before touching JEI. + Minecraft minecraft = Minecraft.getInstance(); + if (!minecraft.isSameThread()) { + minecraft.execute(JEIPlugin::refreshAlloyFurnaceRecipes); + return; + } + + if (runtime == null) return; + + Level level = minecraft.level; + if (level == null || !AlloyFurnaceRecipeCatalog.isReady(level)) return; + + List additions = new ArrayList<>(); + for (AlloyFurnaceRecipeCatalog.Entry recipe : AlloyFurnaceRecipeCatalog.entries(level)) { + if (!registeredAlloyFurnaceRecipes.containsKey(recipe.identity())) { + registeredAlloyFurnaceRecipes.put(recipe.identity(), recipe); + additions.add(recipe); + } + } + if (!additions.isEmpty()) { + runtime.getRecipeManager().addRecipes( + AdvancedAlloyFurnaceRecipeCategory.TYPE, additions); + } } -} \ No newline at end of file + + public static IJeiRuntime getRuntime() { + return runtime; + } + + public static void showAdvancedAlloyFurnaceRecipes() { + if (runtime != null) { + runtime.getRecipesGui().showTypes(List.of(AdvancedAlloyFurnaceRecipeCategory.TYPE)); + } + } + + public static boolean isAvailable() { + return runtime != null; + } + +} diff --git a/src/main/java/com/sorrowmist/useless/compat/jei/OmniversalPatternJeiTransferHandler.java b/src/main/java/com/sorrowmist/useless/compat/jei/OmniversalPatternJeiTransferHandler.java new file mode 100644 index 00000000..1872b5de --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/jei/OmniversalPatternJeiTransferHandler.java @@ -0,0 +1,215 @@ +package com.sorrowmist.useless.compat.jei; + +import appeng.api.stacks.AEFluidKey; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.GenericStack; +import appeng.integration.modules.jeirei.EncodingHelper; +import appeng.menu.me.items.PatternEncodingTermMenu; +import com.sorrowmist.useless.compat.forge1201.OptionalIntegration; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeCatalog; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeFingerprint; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import com.sorrowmist.useless.network.SelectOmniversalPatternRecipePacket; +import mezz.jei.api.gui.ingredient.IRecipeSlotsView; +import mezz.jei.api.recipe.transfer.IRecipeTransferError; +import mezz.jei.api.recipe.transfer.IRecipeTransferHandler; +import mezz.jei.api.recipe.transfer.IRecipeTransferHandlerHelper; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * Transfers an alloy furnace recipe from JEI into AE2's pattern encoding terminal. + * + *

Registering this for {@link AdvancedAlloyFurnaceRecipeCategory#TYPE} takes precedence over + * ae2jeiintegration's universal encode handler, which JEI only consults when no category-specific + * handler matches the open menu. That is what makes the omniversal pattern exclusive to this + * category: every other JEI page still goes through the universal handler and yields a plain + * processing pattern. + * + *

The transfer itself is the same as AE2's — fill the terminal's fake slots and let the player + * press encode — except that the picked recipe's identity is sent to the server first. Only that + * identity carries the mold, which no combination of terminal slots can express, and several + * recipes may share their inputs and outputs while differing only by mold. Recording the click + * instead of guessing from the slots is what lets the player name the exact recipe they want. + * + *

JEI looks handlers up by the menu's exact class, so subclassed terminals (ae2wtlib's wireless + * one) need their own instance; hence the menu class and type are constructor arguments rather than + * constants, matching how ae2jeiintegration registers its encode handler twice. + */ +public final class OmniversalPatternJeiTransferHandler + implements IRecipeTransferHandler { + private final Class menuClass; + private final MenuType menuType; + private final IRecipeTransferHandlerHelper helper; + + public OmniversalPatternJeiTransferHandler( + Class menuClass, MenuType menuType, IRecipeTransferHandlerHelper helper) { + this.menuClass = menuClass; + this.menuType = menuType; + this.helper = helper; + } + + @Override + public Class getContainerClass() { + return menuClass; + } + + @Override + public Optional> getMenuType() { + return Optional.of(menuType); + } + + @Override + public mezz.jei.api.recipe.RecipeType getRecipeType() { + return AdvancedAlloyFurnaceRecipeCategory.TYPE; + } + + @Override + public @Nullable IRecipeTransferError transferRecipe( + T menu, + AlloyFurnaceRecipeCatalog.Entry entry, + IRecipeSlotsView recipeSlots, + Player player, + boolean maxTransfer, + boolean doTransfer) { + return transferOmniversalRecipe(menu, entry, recipeSlots, player, maxTransfer, doTransfer, this.helper); + } + + /** + * Transfers a selected alloy-furnace recipe through any AE2 pattern-encoding menu, including optional terminals + * that subclass the base menu and register their own exact JEI container type. + * + * @param menu active AE2-compatible pattern encoding menu + * @param recipe exact JEI recipe selection whose identity includes the mold + * @param recipeSlots displayed JEI slots for the selected recipe + * @param player player requesting the transfer + * @param maxTransfer whether JEI requested a maximum transfer + * @param doTransfer whether JEI is performing rather than simulating the transfer + * @param helper JEI error factory for the active registration + * @return a user-facing transfer error, or {@code null} when the transfer succeeds + */ + public static @Nullable IRecipeTransferError transferOmniversalRecipe( + PatternEncodingTermMenu menu, + AlloyFurnaceRecipeCatalog.Entry entry, + IRecipeSlotsView recipeSlots, + Player player, + boolean maxTransfer, + boolean doTransfer, + IRecipeTransferHandlerHelper helper) { + AdvancedAlloyFurnaceRecipe recipe = entry.recipe(); + List> inputs = inputOptions(recipe); + List outputs = outputs(recipe); + if (inputs == null || outputs == null || inputs.isEmpty() || outputs.isEmpty()) { + return helper.createInternalError(); + } + // AE2 merges repeated inputs before filling the slots, so this can reject a recipe that would + // just barely have fit. Refusing up front beats transferring a truncated recipe, which the + // server would reject anyway and leave as a plain pattern with no explanation. + if (inputs.size() > menu.getProcessingInputSlots().length + || outputs.size() > menu.getProcessingOutputSlots().length) { + return helper.createUserErrorWithTooltip( + Component.translatable("gui.useless_mod.omniversal_pattern.too_many_slots")); + } + if (!doTransfer) { + return null; + } + + // Sent before the slot contents so the pick is on record by the time the server sees the + // encoded pattern, including when a helper mod encodes it automatically on the same tick. + PacketDistributor.sendToServer(new SelectOmniversalPatternRecipePacket( + menu.containerId, + recipe.id(), + AlloyFurnaceRecipeFingerprint.create(recipe, player.level().registryAccess()), + entry.sourceId())); + OptionalIntegration.call("extendedae_plus", "com.sorrowmist.useless.compat.extendedae.ExtendedAEPlusJeiCompat", + "presetAlloyFurnaceSearchKey", Void.class, null, new Class[]{}); + EncodingHelper.encodeProcessingRecipe(menu, inputs, outputs); + return null; + } + + /** + * One entry per input, each listing the interchangeable stacks that satisfy it so AE2 can encode + * whichever the player's network actually stocks. Ordered like + * {@link com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.OmniversalPatternEncoding#createProcessingPattern} + * so the encoded slots line up with the recipe the server validates them against. + */ + @Nullable + public static List> inputOptions(AdvancedAlloyFurnaceRecipe recipe) { + if (recipe == null) return null; + List> inputs = new ArrayList<>(); + for (CountedIngredient input : recipe.inputs()) { + if (input == null || input.count() <= 0 || input.ingredient() == null) return null; + List options = new ArrayList<>(); + for (ItemStack option : input.ingredient().getItems()) { + if (option == null || option.isEmpty()) continue; + AEItemKey key = AEItemKey.of(option); + if (key != null) options.add(new GenericStack(key, input.count())); + } + if (options.isEmpty()) { + ItemStack representative = AdapterUtils.itemRepresentative(input.ingredient()); + AEItemKey key = representative == null ? null : AEItemKey.of(representative); + if (key != null) options.add(new GenericStack(key, input.count())); + } + if (options.isEmpty()) return null; + inputs.add(List.copyOf(options)); + } + for (LongSizedFluidIngredient input : recipe.inputFluids()) { + if (input == null || input.ingredient() == null || input.ingredient().isEmpty() + || input.amount() <= 0) return null; + List options = new ArrayList<>(); + for (FluidStack option : input.getRepresentatives()) { + if (option == null || option.isEmpty()) continue; + AEFluidKey key = AEFluidKey.of(option); + if (key != null) options.add(new GenericStack(key, input.amount())); + } + if (options.isEmpty()) { + FluidStack representative = AdapterUtils.fluidRepresentative( + input.ingredient(), 1); + AEFluidKey key = representative == null ? null : AEFluidKey.of(representative); + if (key != null) options.add(new GenericStack(key, input.amount())); + } + if (options.isEmpty()) return null; + inputs.add(List.copyOf(options)); + } + for (GenericStack stack : recipe.keyInputs()) { + if (stack == null || stack.what() == null || stack.amount() <= 0L) return null; + inputs.add(List.of(stack)); + } + return inputs; + } + + @Nullable + private static List outputs(AdvancedAlloyFurnaceRecipe recipe) { + if (recipe == null) return null; + List outputs = new ArrayList<>(); + for (ItemStack stack : recipe.outputs()) { + if (stack == null || stack.isEmpty() || stack.getCount() <= 0) return null; + GenericStack output = GenericStack.fromItemStack(stack); + if (output == null || output.what() == null || output.amount() <= 0L) return null; + outputs.add(output); + } + for (FluidStack stack : recipe.outputFluids()) { + if (stack == null || stack.isEmpty() || stack.getAmount() <= 0) return null; + GenericStack output = GenericStack.fromFluidStack(stack); + if (output == null || output.what() == null || output.amount() <= 0L) return null; + outputs.add(output); + } + for (GenericStack stack : recipe.keyOutputs()) { + if (stack == null || stack.what() == null || stack.amount() <= 0L) return null; + outputs.add(stack); + } + return outputs; + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/jei/WrappedKeyGuiHandler.java b/src/main/java/com/sorrowmist/useless/compat/jei/WrappedKeyGuiHandler.java new file mode 100644 index 00000000..5a3bc9ab --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/jei/WrappedKeyGuiHandler.java @@ -0,0 +1,35 @@ +package com.sorrowmist.useless.compat.jei; + +import appeng.api.stacks.GenericStack; +import mezz.jei.api.gui.handlers.IGuiContainerHandler; +import mezz.jei.api.runtime.IClickableIngredient; +import mezz.jei.api.runtime.IIngredientManager; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.renderer.Rect2i; +import net.minecraft.world.item.ItemStack; +import java.util.Optional; +import java.util.function.Supplier; + +/** Only our own screens: unwrap phantom AE resources before JEI's vanilla slot handler + * mistakes them for appeng:wrapped_generic_stack bookmarks. Ordinary items are untouched. + */ +public final class WrappedKeyGuiHandler implements IGuiContainerHandler> { + private final Supplier ingredients; + public WrappedKeyGuiHandler(Supplier ingredients) { this.ingredients = ingredients; } + @Override public Optional> getClickableIngredientUnderMouse( + AbstractContainerScreen screen, double mouseX, double mouseY) { + if (!screen.getClass().getName().startsWith("com.sorrowmist.useless.")) return Optional.empty(); + var slot = screen.getSlotUnderMouse(); + if (slot == null || !slot.isActive()) return Optional.empty(); + var area = new Rect2i(screen.getGuiLeft() + slot.x, screen.getGuiTop() + slot.y, 16, 16); + if (mouseX < area.getX() || mouseX >= area.getX() + 16 + || mouseY < area.getY() || mouseY >= area.getY() + 16) return Optional.empty(); + return fromWrappedSlot(slot.getItem(), area, ingredients.get()); + } + static Optional> fromWrappedSlot(ItemStack item, Rect2i area, IIngredientManager manager) { + if (manager == null || !GenericStack.isWrapped(item)) return Optional.empty(); + var stack = GenericStack.unwrapItemStack(item); + var ingredient = GenericStackJeiIngredientProviders.resolve(stack); + return ingredient == null ? Optional.empty() : ingredient.clickable(manager, area); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/kaleidoscope/cookery/KaleidoscopeCookeryRecipeCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/kaleidoscope/cookery/KaleidoscopeCookeryRecipeCompatLoader.java new file mode 100644 index 00000000..4cec0b09 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/kaleidoscope/cookery/KaleidoscopeCookeryRecipeCompatLoader.java @@ -0,0 +1,30 @@ +package com.sorrowmist.useless.compat.kaleidoscope.cookery; + +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.kaleidoscope.cookery.ChoppingBoardRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.kaleidoscope.cookery.FlexPotRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.kaleidoscope.cookery.FlexStockpotRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.kaleidoscope.cookery.MillstoneRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.kaleidoscope.cookery.PotRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.kaleidoscope.cookery.SteamerRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.kaleidoscope.cookery.StockpotRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.kaleidoscope.cookery.TeapotRecipeAdapter; + +/** Registers Kaleidoscope Cookery's independent kitchen recipe types. */ +public final class KaleidoscopeCookeryRecipeCompatLoader { + private KaleidoscopeCookeryRecipeCompatLoader() { + } + + public static void register() { + AlloyFurnaceRecipeManager manager = AlloyFurnaceRecipeManager.getInstance(); + manager.registerAdapter(new PotRecipeAdapter(), RecipeSourceIds.KALEIDOSCOPE_COOKERY); + manager.registerAdapter(new FlexPotRecipeAdapter(), RecipeSourceIds.KALEIDOSCOPE_COOKERY); + manager.registerAdapter(new StockpotRecipeAdapter(), RecipeSourceIds.KALEIDOSCOPE_COOKERY); + manager.registerAdapter(new FlexStockpotRecipeAdapter(), RecipeSourceIds.KALEIDOSCOPE_COOKERY); + manager.registerAdapter(new ChoppingBoardRecipeAdapter(), RecipeSourceIds.KALEIDOSCOPE_COOKERY); + manager.registerAdapter(new SteamerRecipeAdapter(), RecipeSourceIds.KALEIDOSCOPE_COOKERY); + manager.registerAdapter(new MillstoneRecipeAdapter(), RecipeSourceIds.KALEIDOSCOPE_COOKERY); + manager.registerAdapter(new TeapotRecipeAdapter(), RecipeSourceIds.KALEIDOSCOPE_COOKERY); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/kaleidoscope/grilling/KaleidoscopeGrillingRecipeCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/kaleidoscope/grilling/KaleidoscopeGrillingRecipeCompatLoader.java new file mode 100644 index 00000000..dbe2eff1 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/kaleidoscope/grilling/KaleidoscopeGrillingRecipeCompatLoader.java @@ -0,0 +1,16 @@ +package com.sorrowmist.useless.compat.kaleidoscope.grilling; + +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.kaleidoscope.grilling.GrillingRecipeAdapter; + +/** Registers Kaleidoscope Grilling's runtime cooking recipes. */ +public final class KaleidoscopeGrillingRecipeCompatLoader { + private KaleidoscopeGrillingRecipeCompatLoader() { + } + + public static void register() { + AlloyFurnaceRecipeManager.getInstance().registerAdapter( + new GrillingRecipeAdapter(), RecipeSourceIds.KALEIDOSCOPE_GRILLING); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/kaleidoscope/tavern/KaleidoscopeTavernRecipeCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/kaleidoscope/tavern/KaleidoscopeTavernRecipeCompatLoader.java new file mode 100644 index 00000000..c1578f50 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/kaleidoscope/tavern/KaleidoscopeTavernRecipeCompatLoader.java @@ -0,0 +1,20 @@ +package com.sorrowmist.useless.compat.kaleidoscope.tavern; + +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.kaleidoscope.tavern.BarrelRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.kaleidoscope.tavern.PressingTubRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.kaleidoscope.tavern.ShakerRecipeAdapter; + +/** Registers Kaleidoscope Tavern's barrel, pressing-tub, and shaker adapters. */ +public final class KaleidoscopeTavernRecipeCompatLoader { + private KaleidoscopeTavernRecipeCompatLoader() { + } + + public static void register() { + AlloyFurnaceRecipeManager manager = AlloyFurnaceRecipeManager.getInstance(); + manager.registerAdapter(new BarrelRecipeAdapter(), RecipeSourceIds.KALEIDOSCOPE_TAVERN); + manager.registerAdapter(new PressingTubRecipeAdapter(), RecipeSourceIds.KALEIDOSCOPE_TAVERN); + manager.registerAdapter(new ShakerRecipeAdapter(), RecipeSourceIds.KALEIDOSCOPE_TAVERN); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/kubejs/AdvancedAlloyFurnaceRecipeSchema.java b/src/main/java/com/sorrowmist/useless/compat/kubejs/AdvancedAlloyFurnaceRecipeSchema.java new file mode 100644 index 00000000..842c21d0 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/kubejs/AdvancedAlloyFurnaceRecipeSchema.java @@ -0,0 +1,49 @@ +package com.sorrowmist.useless.compat.kubejs; + +import com.google.gson.JsonObject; +import com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeCodecs; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import dev.latvian.mods.kubejs.item.InputItem; +import dev.latvian.mods.kubejs.recipe.RecipeKey; +import dev.latvian.mods.kubejs.recipe.component.*; +import dev.latvian.mods.kubejs.recipe.schema.RecipeSchema; +import dev.latvian.mods.kubejs.util.JsonIO; +import net.minecraft.world.item.crafting.Ingredient; + +/** KubeJS 2001 schema for the native 1.20.1 recipe serializer. */ +public final class AdvancedAlloyFurnaceRecipeSchema { + private static final RecipeComponent INGREDIENT=new NativeRecipeComponent<>(Ingredient.class,RecipeCodecs.INGREDIENT,from->InputItem.of(from).ingredient); + private static final RecipeComponent FLUID=new NativeRecipeComponent<>(FluidIngredient.class,FluidIngredient.CODEC,from->{ + var json=JsonIO.of(from); + if(json.isJsonPrimitive()){ + var object=new JsonObject(); String id=json.getAsString(); + object.addProperty(id.startsWith("#")?"tag":"fluid",id.startsWith("#")?id.substring(1):id); json=object; + } + return FluidIngredient.fromJson(json); + }); + private static final RecipeComponentBuilder COUNTED=RecipeComponent.builder( + INGREDIENT.key("ingredient"),NumberComponent.LONG.key("count").optional(1L)); + private static final RecipeComponentBuilder SIZED_FLUID=RecipeComponent.builder( + FLUID.key("ingredient"),NumberComponent.LONG.key("amount")); + public static final RecipeKey ID=StringComponent.ANY.key("id"); + public static final RecipeKey INGREDIENTS=COUNTED.asArray().key("ingredients").allowEmpty(); + public static final RecipeKey INPUT_FLUIDS=SIZED_FLUID.asArray().key("input_fluids").defaultOptional().allowEmpty(); + public static final RecipeKey KEY_INPUTS=GenericStackRecipeComponent.GENERIC_STACK.asArray().key("key_inputs").defaultOptional().allowEmpty(); + public static final RecipeKey OUTPUTS=ItemComponents.OUTPUT_ARRAY.key("outputs").defaultOptional().allowEmpty(); + public static final RecipeKey OUTPUT_FLUIDS=FluidComponents.OUTPUT_ARRAY.key("output_fluids").defaultOptional().allowEmpty(); + public static final RecipeKey KEY_OUTPUTS=GenericStackRecipeComponent.GENERIC_STACK.asArray().key("key_outputs").defaultOptional().allowEmpty(); + public static final RecipeKey MOLD=INGREDIENT.key("mold").optional(Ingredient.EMPTY).allowEmpty(); + public static final RecipeKey MOLDS=INGREDIENT.asArray().key("molds").defaultOptional().allowEmpty(); + public static final RecipeKey TIER=NumberComponent.ANY_INT.key("tier").optional(AdvancedAlloyFurnaceRecipe.NO_EXPLICIT_TIER); + public static final RecipeKey ENERGY=NumberComponent.LONG.key("energy").optional(2000L); + public static final RecipeKey PROCESS_TIME=NumberComponent.INT.key("process_time").optional(200); + public static final RecipeKey CATALYST=INGREDIENT.key("catalyst").optional(Ingredient.EMPTY).allowEmpty(); + public static final RecipeKey CATALYST_USES=NumberComponent.INT.key("catalyst_uses").optional(0); + public static final RecipeKey MODE=StringComponent.ANY.key("mode").optional("normal"); + private AdvancedAlloyFurnaceRecipeSchema() {} + public static final RecipeSchema SCHEMA=new RecipeSchema(ID,INGREDIENTS,INPUT_FLUIDS,KEY_INPUTS,OUTPUTS,OUTPUT_FLUIDS,KEY_OUTPUTS, + MOLD,MOLDS,TIER,ENERGY,PROCESS_TIME,CATALYST,CATALYST_USES,MODE) + .constructor(ID,INGREDIENTS,OUTPUTS).constructor(ID,INGREDIENTS,OUTPUTS,MOLD) + .constructor(ID,INGREDIENTS,OUTPUTS,MOLD,TIER).constructor(ID,INGREDIENTS,OUTPUTS,MOLD,MOLDS,TIER); +} diff --git a/src/main/java/com/sorrowmist/useless/compat/kubejs/GenericStackRecipeComponent.java b/src/main/java/com/sorrowmist/useless/compat/kubejs/GenericStackRecipeComponent.java new file mode 100644 index 00000000..711a18d8 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/kubejs/GenericStackRecipeComponent.java @@ -0,0 +1,31 @@ +package com.sorrowmist.useless.compat.kubejs; + +import appeng.api.stacks.GenericStack; +import com.google.gson.JsonElement; +import com.mojang.serialization.JsonOps; +import com.sorrowmist.useless.compat.forge1201.recipe.AE2Codecs; +import dev.latvian.mods.kubejs.recipe.RecipeJS; +import dev.latvian.mods.kubejs.recipe.component.RecipeComponent; +import dev.latvian.mods.kubejs.util.JsonIO; + +/** Uses AE2 15's native key format, including addon resource keys and long quantities. */ +public final class GenericStackRecipeComponent implements RecipeComponent { + public static final GenericStackRecipeComponent GENERIC_STACK=new GenericStackRecipeComponent(); + private GenericStackRecipeComponent() {} + @Override public Class componentClass(){return GenericStack.class;} + @Override public String componentType(){return "useless_mod:generic_stack";} + @Override public JsonElement write(RecipeJS recipe,GenericStack stack){ + if(stack==null || stack.amount()<=0) throw new IllegalArgumentException("AE stack amount must be positive"); + return AE2Codecs.GENERIC_STACK.encodeStart(JsonOps.INSTANCE,stack).getOrThrow(false,message->{throw new IllegalArgumentException(message);}); + } + @Override public GenericStack read(RecipeJS recipe,Object from){ + if(from instanceof GenericStack stack) { + if(stack.amount()<=0) throw new IllegalArgumentException("AE stack amount must be positive"); + return stack; + } + var value=AE2Codecs.GENERIC_STACK.parse(JsonOps.INSTANCE,JsonIO.of(from)).getOrThrow(false,message->{throw new IllegalArgumentException(message);}); + if(value.amount()<=0) throw new IllegalArgumentException("AE stack amount must be positive"); + return value; + } + @Override public boolean hasPriority(RecipeJS recipe,Object from){return from instanceof java.util.Map || from instanceof JsonElement || from instanceof GenericStack;} +} diff --git a/src/main/java/com/sorrowmist/useless/compat/kubejs/NativeRecipeComponent.java b/src/main/java/com/sorrowmist/useless/compat/kubejs/NativeRecipeComponent.java new file mode 100644 index 00000000..59e1fb92 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/kubejs/NativeRecipeComponent.java @@ -0,0 +1,19 @@ +package com.sorrowmist.useless.compat.kubejs; + +import com.google.gson.JsonElement; +import com.mojang.serialization.Codec; +import com.mojang.serialization.JsonOps; +import dev.latvian.mods.kubejs.recipe.RecipeJS; +import dev.latvian.mods.kubejs.recipe.component.RecipeComponent; +import dev.latvian.mods.kubejs.util.JsonIO; +import java.util.function.Function; + +/** Registry-independent recipe JSON with the native 2001 KubeJS component contract. */ +final class NativeRecipeComponent implements RecipeComponent { + private final Class type; private final Codec codec; private final Function reader; + NativeRecipeComponent(Class type,Codec codec,Function reader){this.type=type;this.codec=codec;this.reader=reader;} + NativeRecipeComponent(Class type,Codec codec){this(type,codec,from->codec.parse(JsonOps.INSTANCE,JsonIO.of(from)).getOrThrow(false,message->{throw new IllegalArgumentException(message);}));} + @Override public Class componentClass(){return type;} + @Override public T read(RecipeJS recipe,Object from){return type.isInstance(from)?type.cast(from):reader.apply(from);} + @Override public JsonElement write(RecipeJS recipe,T value){return codec.encodeStart(JsonOps.INSTANCE,value).getOrThrow(false,message->{throw new IllegalArgumentException(message);});} +} diff --git a/src/main/java/com/sorrowmist/useless/compat/kubejs/UselessKubeJSPlugin.java b/src/main/java/com/sorrowmist/useless/compat/kubejs/UselessKubeJSPlugin.java new file mode 100644 index 00000000..053139ec --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/kubejs/UselessKubeJSPlugin.java @@ -0,0 +1,28 @@ +package com.sorrowmist.useless.compat.kubejs; + +import com.sorrowmist.useless.init.ModRecipeSerializers; +import dev.latvian.mods.kubejs.util.ClassFilter; +import dev.latvian.mods.kubejs.KubeJSPlugin; +import dev.latvian.mods.kubejs.recipe.schema.RecipeComponentFactoryRegistryEvent; +import dev.latvian.mods.kubejs.recipe.schema.RegisterRecipeSchemasEvent; + +/** Registers the public KubeJS recipe API for Useless Mod recipes. */ +public final class UselessKubeJSPlugin extends KubeJSPlugin { + @Override + public void registerClasses(dev.latvian.mods.kubejs.script.ScriptType type, ClassFilter filter) { + filter.allow("com.sorrowmist.useless.compat.kubejs"); + } + + @Override + public void registerRecipeComponents(RecipeComponentFactoryRegistryEvent registry) { + registry.register("useless_mod:generic_stack", GenericStackRecipeComponent.GENERIC_STACK); + } + + @Override + public void registerRecipeSchemas(RegisterRecipeSchemasEvent registry) { + registry.register( + ModRecipeSerializers.ADVANCED_ALLOY_FURNACE_SERIALIZER.getId(), + AdvancedAlloyFurnaceRecipeSchema.SCHEMA + ); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/lychee/LycheeRecipeCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/lychee/LycheeRecipeCompatLoader.java new file mode 100644 index 00000000..fd356f85 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/lychee/LycheeRecipeCompatLoader.java @@ -0,0 +1,16 @@ +package com.sorrowmist.useless.compat.lychee; + +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.lychee.LycheeBlockCrushingRecipeAdapter; + +/** 在 Lychee 加载完成后注册其铁砧下落压碎转换。 */ +public final class LycheeRecipeCompatLoader { + private LycheeRecipeCompatLoader() { + } + + public static void register() { + AlloyFurnaceRecipeManager.getInstance().registerAdapter( + new LycheeBlockCrushingRecipeAdapter(), RecipeSourceIds.LYCHEE); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/mekanism/MekanismChemicalCompat.java b/src/main/java/com/sorrowmist/useless/compat/mekanism/MekanismChemicalCompat.java new file mode 100644 index 00000000..2e8c3dde --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/mekanism/MekanismChemicalCompat.java @@ -0,0 +1,121 @@ +package com.sorrowmist.useless.compat.mekanism; + +import appeng.api.stacks.GenericStack; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.*; +import com.sorrowmist.useless.init.ModBlockEntities; +import com.sorrowmist.useless.compat.forge1201.capability.CapabilityAccess; +import com.sorrowmist.useless.compat.forge1201.capability.RegisterCapabilitiesEvent; +import mekanism.api.Action; +import mekanism.api.chemical.Chemical; +import mekanism.api.chemical.ChemicalStack; +import mekanism.api.chemical.IChemicalHandler; +import mekanism.api.chemical.gas.*; +import mekanism.api.chemical.infuse.*; +import mekanism.api.chemical.pigment.*; +import mekanism.api.chemical.slurry.*; +import mekanism.common.capabilities.Capabilities; +import mekanism.common.tier.ChemicalTankTier; +import mekanism.common.util.ChemicalUtil; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import java.util.*; + +/** Native split capabilities share the furnace's three long-sized storage slots. */ +public final class MekanismChemicalCompat { + public static final String MOD_ID="mekanism"; + public static final int TANK_COUNT=3; + private MekanismChemicalCompat() {} + public static FurnaceChemicalStorage createStorage(long capacity,Runnable changed) { + return new MekanismChemicalStorage(TANK_COUNT,capacity,changed); + } + public static GenericStack toGenericStack(ChemicalStack stack) { + return stack == null || stack.isEmpty() ? null : ChemicalKeyProviders.get().toGenericStack(new MekanismChemicalStackView(stack)); + } + public static void registerCapabilities(RegisterCapabilitiesEvent event) { + ChemicalCompatProviders.register(new Provider()); + var type=ModBlockEntities.ADVANCED_ALLOY_FURNACE.get(); + event.registerBlockEntity(Capabilities.GAS_HANDLER,type,(be,side)->new NativeFurnaceChemicalHandlers.GasHandler(be.getInputChemicalStorage(),be.getOutputChemicalStorage(),side,be)); + event.registerBlockEntity(Capabilities.INFUSION_HANDLER,type,(be,side)->new NativeFurnaceChemicalHandlers.InfusionHandler(be.getInputChemicalStorage(),be.getOutputChemicalStorage(),side,be)); + event.registerBlockEntity(Capabilities.PIGMENT_HANDLER,type,(be,side)->new NativeFurnaceChemicalHandlers.PigmentHandler(be.getInputChemicalStorage(),be.getOutputChemicalStorage(),side,be)); + event.registerBlockEntity(Capabilities.SLURRY_HANDLER,type,(be,side)->new NativeFurnaceChemicalHandlers.SlurryHandler(be.getInputChemicalStorage(),be.getOutputChemicalStorage(),side,be)); + } + private static final class Provider implements ChemicalCompatProvider { + public FurnaceChemicalStorage createStorage(long capacity,Runnable changed) { return MekanismChemicalCompat.createStorage(capacity,changed); } + public ChemicalHandlerView getAdjacentHandler(Level level,BlockPos pos,BlockState state,BlockEntity entity,Direction side) { + return MekanismChemicalCompat.getAdjacentHandler(level,pos,state,entity,side); + } + public ChemicalStackView chemicalInItem(ItemStack item) { return MekanismChemicalCompat.chemicalInItem(item); } + public ItemStack markerForChemical(Object ingredient) { return MekanismChemicalCompat.markerForChemical(ingredient); } + public ResourceLocation chemicalIdOf(Object type) { return MekanismChemicalCompat.chemicalIdOf(type); } + } + public static ResourceLocation chemicalIdOf(Object type) { + return type instanceof Chemical chemical ? chemical.getRegistryName() : null; + } + public static ChemicalStackView chemicalInItem(ItemStack item) { + if(item == null || item.isEmpty()) return null; + List> handlers=new ArrayList<>(); + add(handlers,CapabilityAccess.get(item,Capabilities.GAS_HANDLER)); + add(handlers,CapabilityAccess.get(item,Capabilities.INFUSION_HANDLER)); + add(handlers,CapabilityAccess.get(item,Capabilities.PIGMENT_HANDLER)); + add(handlers,CapabilityAccess.get(item,Capabilities.SLURRY_HANDLER)); + for(var handler:handlers) for(int slot=0;slot stack && !stack.isEmpty() + ? ChemicalUtil.getFullChemicalTank(ChemicalTankTier.BASIC,stack.getType()) : ItemStack.EMPTY; + } + public static ChemicalHandlerView getAdjacentHandler(Level level,BlockPos pos,BlockState state,BlockEntity entity,Direction side) { + if(level == null || entity == null) return null; + List> handlers=new ArrayList<>(); + add(handlers,CapabilityAccess.get(level,Capabilities.GAS_HANDLER,pos,state,entity,side)); + add(handlers,CapabilityAccess.get(level,Capabilities.INFUSION_HANDLER,pos,state,entity,side)); + add(handlers,CapabilityAccess.get(level,Capabilities.PIGMENT_HANDLER,pos,state,entity,side)); + add(handlers,CapabilityAccess.get(level,Capabilities.SLURRY_HANDLER,pos,state,entity,side)); + return handlers.isEmpty() ? null : new HandlerView(handlers); + } + private static void add(List> handlers,IChemicalHandler handler) { + if(handler != null) handlers.add(handler); + } + + /** Route only to the matching native handler; never reinterpret a chemical form. */ + private static final class HandlerView implements ChemicalHandlerView { + final List> handlers; + HandlerView(List> handlers) { this.handlers=List.copyOf(handlers); } + private static boolean supports(IChemicalHandler h,ChemicalStack stack) { + return h.getEmptyStack().getClass().isInstance(stack); + } + @SuppressWarnings({"rawtypes","unchecked"}) + public ChemicalStackView insertChemical(ChemicalStackView view,boolean simulate) { + if(!(view instanceof MekanismChemicalStackView mek)) return view; + ChemicalStack remaining=mek.stack(); + for(IChemicalHandler handler:handlers) if(supports(handler,remaining)) + remaining=handler.insertChemical(remaining,simulate?Action.SIMULATE:Action.EXECUTE); + return new MekanismChemicalStackView(remaining); + } + public ChemicalStackView extractChemical(long amount,boolean simulate) { + for(var handler:handlers) { + var stack=handler.extractChemical(amount,simulate?Action.SIMULATE:Action.EXECUTE); + if(!stack.isEmpty()) return new MekanismChemicalStackView(stack); + } + return ChemicalStackView.EMPTY; + } + @SuppressWarnings({"rawtypes","unchecked"}) + public ChemicalStackView extractChemical(ChemicalStackView view,long amount,boolean simulate) { + if(!(view instanceof MekanismChemicalStackView mek)) return ChemicalStackView.EMPTY; + for(IChemicalHandler handler:handlers) if(supports(handler,mek.stack())) { + var extracted=handler.extractChemical(ChemicalUtil.copyWithAmount(mek.stack(),amount),simulate?Action.SIMULATE:Action.EXECUTE); + if(!extracted.isEmpty()) return new MekanismChemicalStackView(extracted); + } + return ChemicalStackView.EMPTY; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/mekanism/MekanismChemicalStackView.java b/src/main/java/com/sorrowmist/useless/compat/mekanism/MekanismChemicalStackView.java new file mode 100644 index 00000000..47703ac4 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/mekanism/MekanismChemicalStackView.java @@ -0,0 +1,57 @@ +package com.sorrowmist.useless.compat.mekanism; + +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalStackView; +import mekanism.api.chemical.ChemicalStack; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.Nullable; + +/** Mekanism-backed implementation of the core chemical view. */ +public final class MekanismChemicalStackView implements ChemicalStackView { + private final ChemicalStack stack; + + public MekanismChemicalStackView(ChemicalStack stack) { + this.stack = stack == null ? mekanism.api.chemical.gas.GasStack.EMPTY : stack.copy(); + } + + public ChemicalStack stack() { + return this.stack; + } + + @Override + public Object typeKey() { + return this.stack.isEmpty() ? ChemicalStackView.EmptyType.INSTANCE : this.stack.getType(); + } + + @Override + public long amount() { + return this.stack.getAmount(); + } + + @Override + public boolean isEmpty() { + return this.stack.isEmpty(); + } + + @Override + public ChemicalStackView copyWithAmount(long amount) { + return amount <= 0L || this.stack.isEmpty() + ? ChemicalStackView.EMPTY + : new MekanismChemicalStackView(mekanism.common.util.ChemicalUtil.copyWithAmount(this.stack,amount)); + } + + @Override + public Component displayName() { + return this.stack.isEmpty() ? Component.empty() : this.stack.getType().getTextComponent(); + } + + @Override + public @Nullable ResourceLocation icon() { + return this.stack.isEmpty() ? null : this.stack.getType().getIcon(); + } + + @Override + public int tintColor() { + return this.stack.isEmpty() ? 0xFFFFFFFF : this.stack.getChemicalTint(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/mekanism/MekanismChemicalStorage.java b/src/main/java/com/sorrowmist/useless/compat/mekanism/MekanismChemicalStorage.java new file mode 100644 index 00000000..8c89d05c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/mekanism/MekanismChemicalStorage.java @@ -0,0 +1,147 @@ +package com.sorrowmist.useless.compat.mekanism; + +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalStackView; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.FurnaceChemicalStorage; +import mekanism.api.chemical.ChemicalStack; +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; + +/** A long-sized three-tank storage backed by Mekanism ChemicalStacks. */ +public final class MekanismChemicalStorage implements FurnaceChemicalStorage { + private final ChemicalStack[] stacks; + private final Runnable onChanged; + private long capacity; + + public MekanismChemicalStorage(int tankCount, long capacity, Runnable onChanged) { + this.stacks = new ChemicalStack[Math.max(0, tankCount)]; + for (int i = 0; i < this.stacks.length; i++) { + this.stacks[i] = mekanism.api.chemical.gas.GasStack.EMPTY; + } + this.capacity = Math.max(0L, capacity); + this.onChanged = onChanged == null ? () -> { } : onChanged; + } + + @Override + public int size() { + return this.stacks.length; + } + + @Override + public long capacity(int slot) { + return validSlot(slot) ? this.capacity : 0L; + } + + @Override + public ChemicalStackView getStackInSlot(int slot) { + return validSlot(slot) ? new MekanismChemicalStackView(this.stacks[slot]) : ChemicalStackView.EMPTY; + } + + public ChemicalStack getNativeStack(int slot) { + return validSlot(slot) ? this.stacks[slot].copy() : mekanism.api.chemical.gas.GasStack.EMPTY; + } + + @Override + public ChemicalStackView insertChemical(int slot, ChemicalStackView view, boolean simulate) { + if (!validSlot(slot) || view == null || view.isEmpty() || !(view instanceof MekanismChemicalStackView mekView)) { + return view == null ? ChemicalStackView.EMPTY : view; + } + + ChemicalStack incoming = mekView.stack(); + ChemicalStack current = this.stacks[slot]; + if (!current.isEmpty() && current.getType() != incoming.getType()) { + return view; + } + + long existingAmount = current.isEmpty() ? 0L : current.getAmount(); + long available = this.capacity > existingAmount ? this.capacity - existingAmount : 0L; + long accepted = Math.min(Math.max(0L, incoming.getAmount()), available); + if (accepted <= 0L) return view; + + if (!simulate) { + if (current.isEmpty()) { + this.stacks[slot] = mekanism.common.util.ChemicalUtil.copyWithAmount(incoming,accepted); + } else { + this.stacks[slot] = mekanism.common.util.ChemicalUtil.copyWithAmount(current,saturatingAdd(current.getAmount(), accepted)); + } + this.onChanged.run(); + } + return new MekanismChemicalStackView(mekanism.common.util.ChemicalUtil.copyWithAmount(incoming,incoming.getAmount() - accepted)); + } + + @Override + public ChemicalStackView extractChemical(int slot, long amount, boolean simulate) { + if (!validSlot(slot) || amount <= 0L || this.stacks[slot].isEmpty()) { + return ChemicalStackView.EMPTY; + } + ChemicalStack current = this.stacks[slot]; + long extracted = Math.min(amount, current.getAmount()); + ChemicalStack result = mekanism.common.util.ChemicalUtil.copyWithAmount(current,extracted); + if (!simulate) { + long remaining = current.getAmount() - extracted; + this.stacks[slot] = remaining <= 0L ? mekanism.api.chemical.gas.GasStack.EMPTY : mekanism.common.util.ChemicalUtil.copyWithAmount(current,remaining); + this.onChanged.run(); + } + return new MekanismChemicalStackView(result); + } + + @Override + public void setStackInSlot(int slot, ChemicalStackView view) { + if (!validSlot(slot)) return; + ChemicalStack replacement = view instanceof MekanismChemicalStackView mekView + ? mekView.stack().copy() : mekanism.api.chemical.gas.GasStack.EMPTY; + if (replacement.getAmount() > this.capacity) { + replacement = mekanism.common.util.ChemicalUtil.copyWithAmount(replacement,this.capacity); + } + this.stacks[slot] = replacement.isEmpty() ? mekanism.api.chemical.gas.GasStack.EMPTY : replacement; + this.onChanged.run(); + } + + @Override + public void setCapacity(long capacity) { + long newCapacity = Math.max(0L, capacity); + if (this.capacity == newCapacity) return; + this.capacity = newCapacity; + boolean changed = false; + for (int i = 0; i < this.stacks.length; i++) { + ChemicalStack stack = this.stacks[i]; + if (stack.isEmpty() || stack.getAmount() <= newCapacity) continue; + this.stacks[i] = newCapacity <= 0L + ? mekanism.api.chemical.gas.GasStack.EMPTY + : mekanism.common.util.ChemicalUtil.copyWithAmount(stack,newCapacity); + changed = true; + } + if (changed) this.onChanged.run(); + } + + @Override + public void save(CompoundTag tag, String prefix, HolderLookup.Provider registries) { + for (int i = 0; i < this.stacks.length; i++) { + ChemicalStack stack = this.stacks[i]; + tag.remove(prefix + i); + if (!stack.isEmpty()) { + tag.put(prefix + i, mekanism.api.chemical.merged.BoxedChemicalStack.box(stack).write(new CompoundTag())); + } + } + } + + @Override + public void load(CompoundTag tag, String prefix, HolderLookup.Provider registries) { + for (int i = 0; i < this.stacks.length; i++) { + String name = prefix + i; + this.stacks[i] = tag.contains(name) && tag.get(name) instanceof CompoundTag compound + ? mekanism.api.chemical.merged.BoxedChemicalStack.read(compound).getChemicalStack() : mekanism.api.chemical.gas.GasStack.EMPTY; + if (this.stacks[i].getAmount() > this.capacity) { + this.stacks[i] = mekanism.common.util.ChemicalUtil.copyWithAmount(this.stacks[i],this.capacity); + } + } + this.onChanged.run(); + } + + private boolean validSlot(int slot) { + return slot >= 0 && slot < this.stacks.length; + } + + private static long saturatingAdd(long left, long right) { + return right > 0L && left > Long.MAX_VALUE - right ? Long.MAX_VALUE : left + right; + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/mekanism/MekanismCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/mekanism/MekanismCompatLoader.java new file mode 100644 index 00000000..30d1ff99 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/mekanism/MekanismCompatLoader.java @@ -0,0 +1,34 @@ +package com.sorrowmist.useless.compat.mekanism; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.sorrowmist.useless.UselessMod; +import net.minecraftforge.fml.ModList; +import com.sorrowmist.useless.compat.forge1201.capability.RegisterCapabilitiesEvent; + +/** Entry point loaded only after Mekanism has been detected. */ +public final class MekanismCompatLoader { + private MekanismCompatLoader() { + } + + public static void registerCapabilities(RegisterCapabilitiesEvent event) { + MekanismEnergyCompat.registerCapabilities(event); + MekanismChemicalCompat.registerCapabilities(event); + // 温度调节器方块的热能力,并安装「主动驱动」钩子(Mek 传热是发送方驱动,见该类注释)。 + ThermostatHeatCompat.registerCapabilities(event); + if (ModList.get().isLoaded("appmek")) { + invokeAppMekCapabilities(event); + } + } + + private static void invokeAppMekCapabilities(RegisterCapabilitiesEvent event) { + try { + Class compat = Class.forName( + "com.sorrowmist.useless.compat.appmek.AppMekChemicalCompat", true, + MekanismCompatLoader.class.getClassLoader()); + compat.getMethod("register", RegisterCapabilitiesEvent.class).invoke(null, event); + } catch (ReflectiveOperationException | LinkageError exception) { + RateLimitedLogger.error(UselessMod.LOGGER, "Failed to register AppMek chemical capabilities", exception); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/mekanism/MekanismEnergyCompat.java b/src/main/java/com/sorrowmist/useless/compat/mekanism/MekanismEnergyCompat.java new file mode 100644 index 00000000..32a6075f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/mekanism/MekanismEnergyCompat.java @@ -0,0 +1,33 @@ +package com.sorrowmist.useless.compat.mekanism; + +import com.sorrowmist.useless.energy.IEnergyManager; +import com.sorrowmist.useless.init.ModBlockEntities; +import mekanism.api.energy.IEnergyConversion; +import mekanism.common.capabilities.Capabilities; +import mekanism.common.util.UnitDisplayUtils.EnergyUnit; +import com.sorrowmist.useless.compat.forge1201.capability.RegisterCapabilitiesEvent; + +public final class MekanismEnergyCompat { + public static final String MOD_ID = "mekanism"; + + private MekanismEnergyCompat() { + } + + public static void registerCapabilities(RegisterCapabilitiesEvent event) { + event.registerBlockEntity( + Capabilities.STRICT_ENERGY, + ModBlockEntities.ADVANCED_ALLOY_FURNACE.get(), + (blockEntity, side) -> new FurnaceStrictEnergyHandler(blockEntity.getEnergyManager()) + ); + event.registerBlockEntity( + Capabilities.STRICT_ENERGY, + ModBlockEntities.MULTIBLOCK_ALLOY_FURNACE_CORE.get(), + (blockEntity, side) -> new FurnaceStrictEnergyHandler(blockEntity.getEnergyManager()) + ); + } + + static final class FurnaceStrictEnergyHandler extends com.sorrowmist.useless.compat.forge1201.mekanism.NativeStrictEnergyHandler { + FurnaceStrictEnergyHandler(IEnergyManager energy) { super(energy, EnergyUnit.FORGE_ENERGY); } + FurnaceStrictEnergyHandler(IEnergyManager energy, IEnergyConversion converter) { super(energy, converter); } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/mekanism/MekanismRecipeCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/mekanism/MekanismRecipeCompatLoader.java new file mode 100644 index 00000000..25048ece --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/mekanism/MekanismRecipeCompatLoader.java @@ -0,0 +1,64 @@ +package com.sorrowmist.useless.compat.mekanism; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.CombinerRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.CrusherRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.EnrichmentChamberRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.FluidToFluidRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.HeavyWaterRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.NutritionalLiquifierRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.PrecisionSawmillRecipeAdapter; +import com.sorrowmist.useless.core.config.ConfigManager; +import net.minecraftforge.fml.ModList; + +/** Registers Mekanism adapters that do not require an AppMek chemical key. */ +public final class MekanismRecipeCompatLoader { + private MekanismRecipeCompatLoader() { + } + + public static void register() { + AlloyFurnaceRecipeManager manager = AlloyFurnaceRecipeManager.getInstance(); + manager.registerAdapter(new CombinerRecipeAdapter(), RecipeSourceIds.MEKANISM); + manager.registerAdapter(new EnrichmentChamberRecipeAdapter(), RecipeSourceIds.MEKANISM); + manager.registerAdapter(new CrusherRecipeAdapter(), RecipeSourceIds.MEKANISM); + manager.registerAdapter(new PrecisionSawmillRecipeAdapter(), RecipeSourceIds.MEKANISM); + manager.registerAdapter(new NutritionalLiquifierRecipeAdapter(), RecipeSourceIds.MEKANISM); + manager.registerAdapter(new FluidToFluidRecipeAdapter(), RecipeSourceIds.MEKANISM); + manager.registerAdapter(new HeavyWaterRecipeAdapter(), RecipeSourceIds.MEKANISM); + + if (ModList.get().isLoaded("appmek") + && ConfigManager.isRecipeConversionEnabled(RecipeSourceIds.APP_MEK)) { + invokeAppMekRecipes(); + } + + if (ModList.get().isLoaded("mekmm")) { + invokeMoreMachineRecipes(); + } + } + + private static void invokeMoreMachineRecipes() { + try { + Class loader = Class.forName( + "com.sorrowmist.useless.compat.mekanismmoremachine.MekanismMoreMachineRecipeCompatLoader", + true, MekanismRecipeCompatLoader.class.getClassLoader()); + loader.getMethod("register").invoke(null); + } catch (ReflectiveOperationException | LinkageError exception) { + RateLimitedLogger.error(UselessMod.LOGGER, "Failed to register Mekanism More Machine recipe adapters", exception); + } + } + + private static void invokeAppMekRecipes() { + try { + Class loader = Class.forName( + "com.sorrowmist.useless.compat.appmek.AppMekRecipeCompatLoader", true, + MekanismRecipeCompatLoader.class.getClassLoader()); + loader.getMethod("register").invoke(null); + } catch (ReflectiveOperationException | LinkageError exception) { + RateLimitedLogger.error(UselessMod.LOGGER, "Failed to register AppMek-backed Mekanism recipe adapters", exception); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/mekanism/MekanismRecipeConverter.java b/src/main/java/com/sorrowmist/useless/compat/mekanism/MekanismRecipeConverter.java deleted file mode 100644 index ac5e00c3..00000000 --- a/src/main/java/com/sorrowmist/useless/compat/mekanism/MekanismRecipeConverter.java +++ /dev/null @@ -1,355 +0,0 @@ -package com.sorrowmist.useless.compat.mekanism; - -import com.sorrowmist.useless.recipes.advancedalloyfurnace.AdvancedAlloyFurnaceRecipe; -import com.sorrowmist.useless.registry.ModIngots; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.crafting.Ingredient; -import net.minecraft.world.item.crafting.RecipeManager; -import net.minecraftforge.fluids.FluidStack; -import net.minecraftforge.registries.ForgeRegistries; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -/** - * Mekanism配方转换器,用于将Mekanism系列模组的配方转换为高级合金炉配方 - */ -public class MekanismRecipeConverter { - - /** - * 将Mekanism冶金灌注机(MetallurgicInfuser)配方转换为高级合金炉配方 - * - * @param recipeManager 游戏配方管理器 - * @return 转换后的高级合金炉配方列表 - */ - public static List convertMetallurgicInfuserRecipes(RecipeManager recipeManager) { - List convertedRecipes = new ArrayList<>(); - - try { - // 获取MetallurgicInfuserRecipe类 - Class metallurgicInfuserRecipeClass = Class.forName("mekanism.common.recipe.impl.MetallurgicInfuserIRecipe"); - // 获取ItemStackToChemicalRecipe类 - Class itemStackToChemicalRecipeClass = Class.forName("mekanism.api.recipes.chemical.ItemStackToChemicalRecipe"); - // 获取InfusionStack类 - Class infusionStackClass = Class.forName("mekanism.api.chemical.infuse.InfusionStack"); - - // 获取所有配方 - Collection> allRecipes = recipeManager.getRecipes(); - - // 首先收集所有Infusion类型的转换配方 - List infusionConversionRecipes = new ArrayList<>(); - for (net.minecraft.world.item.crafting.Recipe recipe : allRecipes) { - // 收集所有ItemStackToChemicalRecipe类型的配方 - if (itemStackToChemicalRecipeClass.isInstance(recipe)) { - // 检查是否为Infusion类型的转换配方 - try { - Method getOutputMethod = itemStackToChemicalRecipeClass.getMethod("getOutputDefinition"); - List outputDefinition = (List) getOutputMethod.invoke(recipe); - if (!outputDefinition.isEmpty() && infusionStackClass.isInstance(outputDefinition.get(0))) { - infusionConversionRecipes.add(recipe); - } - } catch (Exception e) { - // 忽略异常,继续处理其他配方 - } - } - } - - // 筛选出MetallurgicInfuserRecipe类型的配方 - for (net.minecraft.world.item.crafting.Recipe recipe : allRecipes) { - // 检查配方是否为MetallurgicInfuserRecipe类型 - if (metallurgicInfuserRecipeClass.isInstance(recipe)) { - // 使用反射获取配方信息 - Object metallurgicRecipe = recipe; - - // 1. 获取配方ID - ResourceLocation originalId = recipe.getId(); - - // 2. 获取输入物品B - Method getItemInputMethod = metallurgicRecipe.getClass().getMethod("getItemInput"); - Object itemInput = getItemInputMethod.invoke(metallurgicRecipe); - Class itemStackIngredientClass = Class.forName("mekanism.api.recipes.ingredients.ItemStackIngredient"); - Method getRepresentationsMethod = itemStackIngredientClass.getMethod("getRepresentations"); - List itemRepresentations = (List) getRepresentationsMethod.invoke(itemInput); - Ingredient inputB = Ingredient.of(itemRepresentations.toArray(new ItemStack[0])); - - // 3. 获取输入灌注类型A和数量 - Method getChemicalInputMethod = metallurgicRecipe.getClass().getMethod("getChemicalInput"); - Object chemicalInput = getChemicalInputMethod.invoke(metallurgicRecipe); - Class infusionStackIngredientClass = Class.forName("mekanism.api.recipes.ingredients.ChemicalStackIngredient$InfusionStackIngredient"); - Method getChemicalRepresentationsMethod = infusionStackIngredientClass.getMethod("getRepresentations"); - List infusionRepresentations = (List) getChemicalRepresentationsMethod.invoke(chemicalInput); - - // 4. 获取输出物品C - Method getOutputDefinitionMethod = metallurgicRecipe.getClass().getMethod("getOutputDefinition"); - List outputRepresentations = (List) getOutputDefinitionMethod.invoke(metallurgicRecipe); - if (outputRepresentations.isEmpty()) { - continue; // 跳过没有输出的配方 - } - ItemStack outputC = outputRepresentations.get(0); - - // 遍历所有输入灌注类型A的表示 - for (Object infusionStackObj : infusionRepresentations) { - // 获取灌注类型A的类型和数量 - Method getTypeMethod = infusionStackClass.getMethod("getType"); - Object infuseType = getTypeMethod.invoke(infusionStackObj); - Method getAmountMethod = infusionStackClass.getMethod("getAmount"); - long requiredInfusionAmount = (long) getAmountMethod.invoke(infusionStackObj); - - // 遍历所有可以转换为A的D物品 - for (Object conversionRecipe : infusionConversionRecipes) { - // 获取转换配方的输出 - Method conversionOutputMethod = itemStackToChemicalRecipeClass.getMethod("getOutputDefinition"); - List conversionOutputs = (List) conversionOutputMethod.invoke(conversionRecipe); - if (conversionOutputs.isEmpty()) { - continue; - } - Object conversionOutput = conversionOutputs.get(0); - - // 检查转换配方的输出是否为当前需要的灌注类型A - Method conversionOutputTypeMethod = conversionOutput.getClass().getMethod("getType"); - Object conversionOutputType = conversionOutputTypeMethod.invoke(conversionOutput); - if (!conversionOutputType.equals(infuseType)) { - continue; - } - - // 获取转换配方的输出数量 - Method conversionOutputAmountMethod = conversionOutput.getClass().getMethod("getAmount"); - long conversionOutputAmount = (long) conversionOutputAmountMethod.invoke(conversionOutput); - - // 获取转换配方的输入D - Method conversionInputMethod = itemStackToChemicalRecipeClass.getMethod("getInput"); - Object conversionInput = conversionInputMethod.invoke(conversionRecipe); - Method conversionInputRepresentationsMethod = itemStackIngredientClass.getMethod("getRepresentations"); - List conversionInputRepresentations = (List) conversionInputRepresentationsMethod.invoke(conversionInput); - Ingredient inputD = Ingredient.of(conversionInputRepresentations.toArray(new ItemStack[0])); - - // 5. 计算每64个B需要消耗多少D - // 公式:每1个B需要A的数量为 requiredInfusionAmount - // 每1个D可以转换为A的数量为 conversionOutputAmount - // 所以每1个B需要D的数量为 requiredInfusionAmount / conversionOutputAmount - // 每64个B需要D的数量为 64 * requiredInfusionAmount / conversionOutputAmount - long dPer64B = (64 * requiredInfusionAmount) / conversionOutputAmount; - if ((64 * requiredInfusionAmount) % conversionOutputAmount != 0) { - dPer64B += 1; // 向上取整 - } - - // 6. 创建配方ID - ResourceLocation newId = new ResourceLocation( - "useless_mod", - "mek_metallurgic_" + originalId.getNamespace() + "_" + originalId.getPath() + "_" + infuseType.hashCode() + "_" + conversionRecipe.hashCode() - ); - - // 7. 处理输入物品和数量 - List inputIngredients = new ArrayList<>(); - List inputItemCounts = new ArrayList<>(); - - // 添加输入物品B:64个 - inputIngredients.add(inputB); - inputItemCounts.add(64L); - - // 添加输入物品D:对应的数量 - inputIngredients.add(inputD); - inputItemCounts.add(dPer64B); - - // 8. 处理输出物品:64个C - List outputItems = new ArrayList<>(); - ItemStack output64C = outputC.copy(); - output64C.setCount(outputC.getCount() * 64); - outputItems.add(output64C); - - // 9. 处理输入流体:无 - List inputFluids = new ArrayList<>(); - - // 10. 处理输出流体:无 - List outputFluids = new ArrayList<>(); - - // 11. 处理能量消耗:使用默认值 - int energy = 10000; // 默认能量消耗 - - // 12. 处理处理时间:使用默认值 - int processTime = 40; // 默认为40tick - - // 13. 处理催化剂:使用无用锭标签作为催化剂 - Ingredient catalyst = Ingredient.of(ModIngots.USELESS_INGOT_TIER_1.get(), - ModIngots.USELESS_INGOT_TIER_2.get(), - ModIngots.USELESS_INGOT_TIER_3.get(), - ModIngots.USELESS_INGOT_TIER_4.get(), - ModIngots.USELESS_INGOT_TIER_5.get(), - ModIngots.USELESS_INGOT_TIER_6.get(), - ModIngots.USELESS_INGOT_TIER_7.get(), - ModIngots.USELESS_INGOT_TIER_8.get(), - ModIngots.USELESS_INGOT_TIER_9.get(), - ModIngots.USEFUL_INGOT.get()); - int catalystCount = 1; - - // 14. 处理模具:要求放入"mekanism:metallurgic_infuser" - ResourceLocation metallurgicInfuserId = ResourceLocation.fromNamespaceAndPath("mekanism", "metallurgic_infuser"); - net.minecraft.world.item.Item metallurgicInfuser = ForgeRegistries.ITEMS.getValue(metallurgicInfuserId); - if (metallurgicInfuser == null) { - throw new RuntimeException("Could not find item: " + metallurgicInfuserId); - } - Ingredient mold = Ingredient.of(metallurgicInfuser); - - // 创建并返回转换后的高级合金炉配方 - AdvancedAlloyFurnaceRecipe convertedRecipe = new AdvancedAlloyFurnaceRecipe( - newId, - inputIngredients, - inputItemCounts, - inputFluids, - outputItems, - outputFluids, - energy, - processTime, - catalyst, - catalystCount, - mold, - false - ); - - convertedRecipes.add(convertedRecipe); - } - } - } - } - } catch (Exception e) { - // 捕获所有异常,避免崩溃 - e.printStackTrace(); - System.err.println("Failed to convert metallurgic infuser recipes: " + e.getMessage()); - } - - return convertedRecipes; - } - - /** - * 将Mekanism富集仓(EnrichmentChamber)配方转换为高级合金炉配方 - * - * @param recipeManager 游戏配方管理器 - * @return 转换后的高级合金炉配方列表 - */ - public static List convertEnrichmentChamberRecipes(RecipeManager recipeManager) { - List convertedRecipes = new ArrayList<>(); - - try { - // 获取EnrichingIRecipe类 - Class enrichingIRecipeClass = Class.forName("mekanism.common.recipe.impl.EnrichingIRecipe"); - - // 获取所有配方 - Collection> allRecipes = recipeManager.getRecipes(); - - // 筛选出EnrichingIRecipe类型的配方 - for (net.minecraft.world.item.crafting.Recipe recipe : allRecipes) { - // 检查配方是否为EnrichingIRecipe类型 - if (enrichingIRecipeClass.isInstance(recipe)) { - // 使用反射获取配方信息 - Object enrichingRecipe = recipe; - - // 1. 获取配方ID - ResourceLocation originalId = recipe.getId(); - - // 2. 获取输入物品 - Method getInputMethod = enrichingRecipe.getClass().getMethod("getInput"); - Object itemInput = getInputMethod.invoke(enrichingRecipe); - Class itemStackIngredientClass = Class.forName("mekanism.api.recipes.ingredients.ItemStackIngredient"); - Method getRepresentationsMethod = itemStackIngredientClass.getMethod("getRepresentations"); - List itemRepresentations = (List) getRepresentationsMethod.invoke(itemInput); - Ingredient inputIngredient = Ingredient.of(itemRepresentations.toArray(new ItemStack[0])); - - // 3. 获取输出物品 - Method getOutputDefinitionMethod = enrichingRecipe.getClass().getMethod("getOutputDefinition"); - List outputRepresentations = (List) getOutputDefinitionMethod.invoke(enrichingRecipe); - if (outputRepresentations.isEmpty()) { - continue; // 跳过没有输出的配方 - } - ItemStack outputItem = outputRepresentations.get(0); - - // 4. 创建配方ID - ResourceLocation newId = new ResourceLocation( - "useless_mod", - "mek_enrichment_" + originalId.getNamespace() + "_" + originalId.getPath() - ); - - // 5. 处理输入物品和数量:获取原始数量并乘以64 - List inputIngredients = new ArrayList<>(); - List inputItemCounts = new ArrayList<>(); - - inputIngredients.add(inputIngredient); - - // 获取原始输入数量 - long originalInputCount = 1; - if (!itemRepresentations.isEmpty()) { - originalInputCount = itemRepresentations.get(0).getCount(); - } - inputItemCounts.add(originalInputCount * 64); - - // 6. 处理输出物品:原始数量乘以64 - List outputItems = new ArrayList<>(); - ItemStack output64 = outputItem.copy(); - output64.setCount(outputItem.getCount() * 64); - outputItems.add(output64); - - // 7. 处理输入流体:无 - List inputFluids = new ArrayList<>(); - - // 8. 处理输出流体:无 - List outputFluids = new ArrayList<>(); - - // 9. 处理能量消耗:使用默认值 - int energy = 2000; // 默认能量消耗 - - // 10. 处理处理时间:使用默认值 - int processTime = 40; // 默认为200tick - - // 11. 处理催化剂:使用无用锭标签作为催化剂 - Ingredient catalyst = Ingredient.of(ModIngots.USELESS_INGOT_TIER_1.get(), - ModIngots.USELESS_INGOT_TIER_2.get(), - ModIngots.USELESS_INGOT_TIER_3.get(), - ModIngots.USELESS_INGOT_TIER_4.get(), - ModIngots.USELESS_INGOT_TIER_5.get(), - ModIngots.USELESS_INGOT_TIER_6.get(), - ModIngots.USELESS_INGOT_TIER_7.get(), - ModIngots.USELESS_INGOT_TIER_8.get(), - ModIngots.USELESS_INGOT_TIER_9.get(), - ModIngots.USEFUL_INGOT.get()); - int catalystCount = 1; - - // 12. 处理模具:要求放入"mekanism:enrichment_chamber" - ResourceLocation enrichmentChamberId = ResourceLocation.fromNamespaceAndPath("mekanism", "enrichment_chamber"); - net.minecraft.world.item.Item enrichmentChamber = ForgeRegistries.ITEMS.getValue(enrichmentChamberId); - if (enrichmentChamber == null) { - throw new RuntimeException("Could not find item: " + enrichmentChamberId); - } - Ingredient mold = Ingredient.of(enrichmentChamber); - - // 创建并返回转换后的高级合金炉配方 - AdvancedAlloyFurnaceRecipe convertedRecipe = new AdvancedAlloyFurnaceRecipe( - newId, - inputIngredients, - inputItemCounts, - inputFluids, - outputItems, - outputFluids, - energy, - processTime, - catalyst, - catalystCount, - mold, - false, - false - ); - - convertedRecipes.add(convertedRecipe); - } - } - } catch (Exception e) { - // 捕获所有异常,避免崩溃 - e.printStackTrace(); - System.err.println("Failed to convert enrichment chamber recipes: " + e.getMessage()); - } - - return convertedRecipes; - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/compat/mekanism/NativeFurnaceChemicalHandlers.java b/src/main/java/com/sorrowmist/useless/compat/mekanism/NativeFurnaceChemicalHandlers.java new file mode 100644 index 00000000..f681aff6 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/mekanism/NativeFurnaceChemicalHandlers.java @@ -0,0 +1,82 @@ +package com.sorrowmist.useless.compat.mekanism; + +import com.sorrowmist.useless.api.enums.FurnaceFace; +import com.sorrowmist.useless.api.enums.FurnaceFaceMode; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.*; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.io.FurnaceFaceAccessor; +import mekanism.api.Action; +import mekanism.api.chemical.Chemical; +import mekanism.api.chemical.ChemicalStack; +import mekanism.api.chemical.IChemicalHandler; +import mekanism.api.chemical.gas.*; +import mekanism.api.chemical.infuse.*; +import mekanism.api.chemical.pigment.*; +import mekanism.api.chemical.slurry.*; +import net.minecraft.core.Direction; +import java.util.*; + +/** Split native capabilities, each enforcing form, side and shared-slot ownership. */ +public final class NativeFurnaceChemicalHandlers { + private NativeFurnaceChemicalHandlers() {} + static abstract class TypedHandler,S extends ChemicalStack> implements IChemicalHandler { + final FurnaceChemicalStorage input,output; + final Direction side; + final FurnaceFaceAccessor owner; + final Class stackType; + final S empty; + TypedHandler(FurnaceChemicalStorage input,FurnaceChemicalStorage output,Direction side,FurnaceFaceAccessor owner,Class type,S empty) { + this.input=input; this.output=output; this.side=side; this.owner=owner; this.stackType=type; this.empty=empty; + } + public S getEmptyStack() { return empty; } + public int getTanks() { return input.size()+output.size(); } + FurnaceChemicalStorage storage(int slot) { return slot<0 || slot>=getTanks() ? null : slot implements IGasHandler { + GasHandler(FurnaceChemicalStorage in,FurnaceChemicalStorage out,Direction side,FurnaceFaceAccessor owner) { super(in,out,side,owner,GasStack.class,GasStack.EMPTY); } + } + + static final class InfusionHandler extends TypedHandler implements IInfusionHandler { + InfusionHandler(FurnaceChemicalStorage in,FurnaceChemicalStorage out,Direction side,FurnaceFaceAccessor owner) { super(in,out,side,owner,InfusionStack.class,InfusionStack.EMPTY); } + } + + static final class PigmentHandler extends TypedHandler implements IPigmentHandler { + PigmentHandler(FurnaceChemicalStorage in,FurnaceChemicalStorage out,Direction side,FurnaceFaceAccessor owner) { super(in,out,side,owner,PigmentStack.class,PigmentStack.EMPTY); } + } + + static final class SlurryHandler extends TypedHandler implements ISlurryHandler { + SlurryHandler(FurnaceChemicalStorage in,FurnaceChemicalStorage out,Direction side,FurnaceFaceAccessor owner) { super(in,out,side,owner,SlurryStack.class,SlurryStack.EMPTY); } + } + +} diff --git a/src/main/java/com/sorrowmist/useless/compat/mekanism/ThermostatHeatCompat.java b/src/main/java/com/sorrowmist/useless/compat/mekanism/ThermostatHeatCompat.java new file mode 100644 index 00000000..dbdcc458 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/mekanism/ThermostatHeatCompat.java @@ -0,0 +1,141 @@ +package com.sorrowmist.useless.compat.mekanism; + +import com.sorrowmist.useless.content.blockentities.ThermostatBlockEntity; +import com.sorrowmist.useless.content.blockentities.ThermostatHeatHook; +import com.sorrowmist.useless.init.ModBlockEntities; +import mekanism.api.heat.IHeatHandler; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraftforge.common.capabilities.Capability; +import com.sorrowmist.useless.compat.forge1201.capability.RegisterCapabilitiesEvent; + +/** + * 温度调节器方块的 Mekanism 热兼容。 + * + *

只引用纯 API({@code mekanism.api.heat.IHeatHandler}),不碰任何 Mekanism internal 类。 + * 能力对象自己声明:{@code mekanism:heat_handler} 这个 ResourceLocation 与 Mek 自己的 + * {@code Capabilities.HEAT} 相同,{@code BlockCapability.createSided} 对同一 RL 返回同一个对象 + * (气动工艺的 {@code MekanismIntegration} 就是这么做的),所以不必引用 {@code common} 包。

+ * + *

为什么必须「主动驱动」

+ * + *

Mek 的传热是发送方驱动:{@code ITileHeatHandler.simulateAdjacent()} 的公式是 + * {@code (自身温度 − 环境温度) / 导热系数},从不读邻居温度(读邻居温度的正确分支被作者 + * 注释掉了)。所以一个只暴露 {@code Capabilities.HEAT} 的方块只会被 Mek 机器当成「通往 300K + * 的散热片」,加不热机器。要让恒温方块真正驱动机器,必须由方块自己当发送方,每 tick 用 + * {@link IHeatHandler#handleHeat(double)} 把热推给邻居——这段就在 {@link #drive} 里。

+ */ +public final class ThermostatHeatCompat { + + /** 与 Mek {@code Capabilities.HEAT} 同 RL ⇒ 同一个能力对象。 */ + private static final Capability HEAT = mekanism.common.capabilities.Capabilities.HEAT_HANDLER; + + private static final String CACHE_KEY = "mekanism"; + + /** + * 我方导热系数,取 Mek 的 {@code AIR_INVERSE_COEFFICIENT} 同值(10000)。 + * + *

故意「绝缘」:机器按 {@code (自身温度 − 300K) / (我们的系数 + 它的系数)} 向我们排热, + * 系数取大值等于把这条通道掐掉,稳态就不会被拉到「300K 与设定值的中点」。

+ */ + private static final double INSULATION_INVERSE_CONDUCTION = 10_000.0D; + + /** 驱动步长:每 tick 走完温差的一半,收敛快且绝不超调。 */ + private static final double DRIVE_STEP = 0.5D; + + /** 低于这个温差就不再动作,省掉无意义的浮点运算。 */ + private static final double DRIVE_EPSILON = 0.001D; + + private ThermostatHeatCompat() { + } + + /** 关掉恒温时对外报告的温度:环境温度,与 Mek 的 {@code HeatAPI.AMBIENT_TEMP} 一致。 */ + private static final double AMBIENT_TEMPERATURE = 300.0D; + + public static void registerCapabilities(RegisterCapabilitiesEvent event) { + // ⚠️ 刻意不按 enabled 返回 null。 + // + // 能力在「null ↔ 非 null」之间切换时,MinecraftForge 的缓存能失效,但第三方缓存不一定—— + // Mek 的 CapabilityCache、气动的 IdentityHashMap 都只在方块变化时重新发现。 + // 表现就是「开了恒温、机器却毫无反应」。所以**始终交出适配器**,由适配器按 enabled + // 决定行为:关着的时候报告 300K,配合下面的大导热系数,几乎不参与任何交换。 + event.registerBlockEntity(HEAT, ModBlockEntities.TEMPERATURE_REGULATOR.get(), + (blockEntity, side) -> adapter(blockEntity)); + // 安装「主动驱动」;未装 Mekanism 时这个钩子保持 null,主代码的 tick 就是空操作。 + ThermostatHeatHook.driver = ThermostatHeatCompat::drive; + } + + private static IHeatHandler adapter(ThermostatBlockEntity thermostat) { + Object cached = thermostat.getHeatAdapter(CACHE_KEY); + if (cached instanceof Adapter adapter) { + return adapter; + } + Adapter created = new Adapter(thermostat); + thermostat.putHeatAdapter(CACHE_KEY, created); + return created; + } + + /** + * 主动把热推给相邻的热处理器(Mek 机器、热导体等)。 + * + *

转移量 = 温差 × 邻居热容 × 半步系数:热量单位是焦耳,邻居升温 + * {@code ΔT = transfer / 热容},所以乘上热容正好把邻居推向我们的温度,乘半步则不会超调。

+ * + *

必须先判热容 > 0:Mek 的 {@code getTotalHeatCapacity()} 在「没有电容」时返回 0, + * 拿它当乘数虽不会除零,但会传 0 热量;而真正危险的是拿它当除数(会得到 NaN)。

+ */ + private static void drive(ServerLevel level, BlockPos pos, ThermostatBlockEntity thermostat) { + double temperature = thermostat.getTemperature(); + for (Direction side : Direction.values()) { + IHeatHandler sink = com.sorrowmist.useless.compat.forge1201.capability.CapabilityAccess.get(level, HEAT, pos.relative(side), side.getOpposite()); + if (sink == null) { + continue; + } + double capacity = sink.getTotalHeatCapacity(); + if (!(capacity > 0.0D)) { + continue; + } + double delta = temperature - sink.getTotalTemperature(); + if (Math.abs(delta) < DRIVE_EPSILON) { + continue; + } + sink.handleHeat(delta * capacity * DRIVE_STEP); + } + } + + /** 恒温方块的 Mek 热处理器视角:温度恒定,收下 / 给出多少热都不变。 */ + private static final class Adapter implements IHeatHandler { + private final ThermostatBlockEntity thermostat; + + Adapter(ThermostatBlockEntity thermostat) { + this.thermostat = thermostat; + } + + @Override + public int getHeatCapacitorCount() { + return 1; + } + + @Override + public double getTemperature(int capacitor) { + return thermostat.isEnabled() ? thermostat.getTemperature() : AMBIENT_TEMPERATURE; + } + + @Override + public double getInverseConduction(int capacitor) { + return INSULATION_INVERSE_CONDUCTION; + } + + @Override + public double getHeatCapacity(int capacitor) { + return 1.0D; + } + + @Override + public void handleHeat(int capacitor, double transfer) { + // 无限恒温源:不改变自身温度。 + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/mekanismgenerators/MekanismGeneratorsCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/mekanismgenerators/MekanismGeneratorsCompatLoader.java new file mode 100644 index 00000000..e7748967 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/mekanismgenerators/MekanismGeneratorsCompatLoader.java @@ -0,0 +1,22 @@ +package com.sorrowmist.useless.compat.mekanismgenerators; + +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.generators.FissionRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.mekanismextra.NaquadahReactorRecipeAdapter; +import net.minecraftforge.fml.ModList; + +/** Keeps Mekanism Generators classes behind their optional mod boundary. */ +public final class MekanismGeneratorsCompatLoader { + private MekanismGeneratorsCompatLoader() { + } + + public static void register() { + AlloyFurnaceRecipeManager.getInstance().registerAdapter( + new FissionRecipeAdapter(), RecipeSourceIds.MEKANISM_GENERATORS); + if (ModList.get().isLoaded("mekanism_extras")) { + AlloyFurnaceRecipeManager.getInstance().registerAdapter( + new NaquadahReactorRecipeAdapter(), RecipeSourceIds.MEKANISM_GENERATORS); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/mekanismmoremachine/MekanismMoreMachineRecipeCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/mekanismmoremachine/MekanismMoreMachineRecipeCompatLoader.java new file mode 100644 index 00000000..24f1ce36 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/mekanismmoremachine/MekanismMoreMachineRecipeCompatLoader.java @@ -0,0 +1,18 @@ +package com.sorrowmist.useless.compat.mekanismmoremachine; + +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.RecyclerRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.mekanismmoremachine.AmbientGasCollectorRecipeAdapter; + +/** Registers recipe adapters backed by Mekanism More Machine. */ +public final class MekanismMoreMachineRecipeCompatLoader { + private MekanismMoreMachineRecipeCompatLoader() { + } + + public static void register() { + AlloyFurnaceRecipeManager manager = AlloyFurnaceRecipeManager.getInstance(); + manager.registerAdapter(new RecyclerRecipeAdapter(), RecipeSourceIds.MEKANISM); + manager.registerAdapter(new AmbientGasCollectorRecipeAdapter(), RecipeSourceIds.MEKANISM); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/modernindustrialization/MiEnergyBridge.java b/src/main/java/com/sorrowmist/useless/compat/modernindustrialization/MiEnergyBridge.java new file mode 100644 index 00000000..5208acdc --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/modernindustrialization/MiEnergyBridge.java @@ -0,0 +1,25 @@ +package com.sorrowmist.useless.compat.modernindustrialization; + +import com.sorrowmist.useless.api.logistics.LongEnergyHandler; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +/** + * Modern Industrialization 能量端点的桥接口。 + * + *

与 {@code AeLogisticsBridge} 同一套手法:本接口不含任何 MI 类型,实现类由 + * {@link MiEnergyCompatLoader} 反射加载,因此没装 MI 时 {@code StaffLinkTargets} 照样能加载、 + * 只是永远解析不出 EU 端点。

+ */ +public interface MiEnergyBridge { + + /** + * 该坐标的 EU 能量端点(对外表现为 FE);不是 MI 能量方块时返回 {@code null}。 + * + * @param side 指定面;{@code null} 表示不指定,由实现自行探测 + */ + @Nullable + LongEnergyHandler energyEndpoint(Level level, BlockPos pos, @Nullable Direction side); +} diff --git a/src/main/java/com/sorrowmist/useless/compat/modernindustrialization/MiEnergyCompat.java b/src/main/java/com/sorrowmist/useless/compat/modernindustrialization/MiEnergyCompat.java new file mode 100644 index 00000000..3084f807 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/modernindustrialization/MiEnergyCompat.java @@ -0,0 +1,131 @@ +package com.sorrowmist.useless.compat.modernindustrialization; + +import aztech.modern_industrialization.api.energy.EnergyApi; +import aztech.modern_industrialization.api.energy.MIEnergyStorage; +import aztech.modern_industrialization.config.MIServerConfig; +import com.sorrowmist.useless.api.logistics.LongEnergyHandler; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +/** + * {@link MiEnergyBridge} 的 Modern Industrialization 实现。 + * + *

只有本类(以及它引用的 MI / GrandPower 类)会在装了 MI 时被加载:常驻代码碰到的 + * 永远是 {@link MiEnergyBridge} 那个不含模组类型的接口。

+ * + *

为什么不能靠 MinecraftForge 的 FE 能力

+ * + *

MI 的机器只注册自己的 {@link EnergyApi#SIDED}(单位是 EU)。MI 确实也有一份「FE 视角」的 + * 通用能力({@code ILongEnergyStorage.BLOCK}),但它挂在 MI 的启动配置 + * {@code bidirectionalEnergyCompat} 下,默认是关的——指望它等于要求玩家改配置。 + * 所以这里直接取 {@code SIDED},换算由我们自己完成。

+ * + *

电压

+ * + *

MI 的电压门槛是 {@link MIEnergyStorage#canConnect}(拿仓的等级去比)。这里一次都不调, + * 等于无视电压等级。要注意的是:MI 机器每次实际能收多少仍由它自己的容量与实现决定, + * 那部分绕不过去,也不该绕——超出的量会退回源,下一 tick 继续。

+ * + *

换算比例

+ * + *

1 EU 值多少 FE 跟随 MI 自己的配置 {@code forgeEnergyPerEu}(默认 10), + * 而不是写死 4:整合包里通常还有别的 FE↔EU 桥,跟 MI 保持一致才不会凭空造电或凭空吃电。

+ */ +public final class MiEnergyCompat implements MiEnergyBridge { + private static final Direction[] DIRECTIONS = Direction.values(); + + @Override + @Nullable + public LongEnergyHandler energyEndpoint(Level level, BlockPos pos, @Nullable Direction side) { + MIEnergyStorage storage = lookup(level, pos, side); + return storage == null ? null : new EuEndpoint(storage); + } + + /** + * 取该坐标的 MI 能量存储。 + * + *

探测顺序与 {@code StaffLinkTargets#capability} 一致:指定面 → 不指定 → 逐面。 + * 逐面那一步不是多余的:MI 的输出仓只在「朝向的那一面」交出可抽出的存储, + * 传 {@code null} 会拿到 null。

+ */ + @Nullable + private static MIEnergyStorage lookup(Level level, BlockPos pos, @Nullable Direction side) { + if (level == null || pos == null || !level.isLoaded(pos)) { + return null; + } + if (side != null) { + return com.sorrowmist.useless.compat.forge1201.capability.CapabilityAccess.get(level, EnergyApi.SIDED, pos, side); + } + MIEnergyStorage unspecified = com.sorrowmist.useless.compat.forge1201.capability.CapabilityAccess.get(level, EnergyApi.SIDED, pos, null); + if (unspecified != null) { + return unspecified; + } + for (Direction direction : DIRECTIONS) { + MIEnergyStorage found = com.sorrowmist.useless.compat.forge1201.capability.CapabilityAccess.get(level, EnergyApi.SIDED, pos, direction); + if (found != null) { + return found; + } + } + return null; + } + + /** 把 MI 的 EU 存储包成本模组的 FE 长整型契约。 */ + private static final class EuEndpoint implements LongEnergyHandler { + private final MIEnergyStorage storage; + + EuEndpoint(MIEnergyStorage storage) { + this.storage = storage; + } + + /** 1 EU 值多少 FE;读 MI 的配置,取不到或非法时退回 1。 */ + private static long ratio() { + return Math.max(1L, MIServerConfig.INSTANCE.forgeEnergyPerEu.getAsInt()); + } + + @Override + public long extract(long amount, boolean simulate) { + long eu = amount / ratio(); + if (eu <= 0L) { + // 连 1 EU 都凑不出来:不搬。否则会把 3 FE 变成 1 EU 再换回 4 FE,凭空造电。 + return 0L; + } + return Math.max(0L, storage.extract(eu, simulate)) * ratio(); + } + + @Override + public long receive(long amount, boolean simulate) { + long eu = amount / ratio(); + if (eu <= 0L) { + return 0L; + } + return Math.max(0L, storage.receive(eu, simulate)) * ratio(); + } + + @Override + public long stored() { + return Math.max(0L, storage.getAmount()) * ratio(); + } + + @Override + public long capacity() { + long capacity = storage.getCapacity(); + if (capacity <= 0L) { + return Long.MAX_VALUE; + } + long scaled = capacity * ratio(); + return scaled < 0L ? Long.MAX_VALUE : scaled; + } + + @Override + public boolean canExtract() { + return storage.canExtract(); + } + + @Override + public boolean canReceive() { + return storage.canReceive(); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/modernindustrialization/MiEnergyCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/modernindustrialization/MiEnergyCompatLoader.java new file mode 100644 index 00000000..6fff7ac3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/modernindustrialization/MiEnergyCompatLoader.java @@ -0,0 +1,61 @@ +package com.sorrowmist.useless.compat.modernindustrialization; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.api.logistics.LongEnergyHandler; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; +import net.minecraftforge.fml.ModList; +import org.jetbrains.annotations.Nullable; + +/** + * 按需加载 {@link MiEnergyCompat}。 + * + *

与 {@code AeLogisticsCompatLoader} 同一套手法:常驻类只引用 {@link MiEnergyBridge} 这个 + * 不含 MI 类型的接口,实现类用 {@code Class.forName} 反射加载,从而在没装 + * Modern Industrialization 的整合包里也不会触发类解析错误。

+ */ +public final class MiEnergyCompatLoader { + public static final String MOD_ID = "modern_industrialization"; + + private static final String IMPLEMENTATION = + "com.sorrowmist.useless.compat.modernindustrialization.MiEnergyCompat"; + + private static volatile boolean initialized; + private static volatile MiEnergyBridge bridge; + + private MiEnergyCompatLoader() { + } + + /** MI 能量桥;未加载 MI 或初始化失败时为 {@code null}。 */ + @Nullable + public static MiEnergyBridge bridge() { + if (!initialized) { + initialized = true; + if (ModList.get().isLoaded(MOD_ID)) { + try { + Class implementation = Class.forName( + IMPLEMENTATION, true, MiEnergyCompatLoader.class.getClassLoader()); + bridge = (MiEnergyBridge) implementation.getDeclaredConstructor().newInstance(); + } catch (ReflectiveOperationException | LinkageError exception) { + RateLimitedLogger.error(UselessMod.LOGGER, "Failed to initialise the Modern Industrialization energy bridge", + exception); + } + } + } + return bridge; + } + + public static boolean isAvailable() { + return bridge() != null; + } + + /** 该坐标的 MI 能量端点(对外表现为 FE);未加载 MI 或不是能量方块时返回 {@code null}。 */ + @Nullable + public static LongEnergyHandler energyEndpoint(Level level, BlockPos pos, @Nullable Direction side) { + MiEnergyBridge resolved = bridge(); + return resolved == null ? null : resolved.energyEndpoint(level, pos, side); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/modernindustrialization/ModernIndustrializationRecipeCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/modernindustrialization/ModernIndustrializationRecipeCompatLoader.java new file mode 100644 index 00000000..61767ff5 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/modernindustrialization/ModernIndustrializationRecipeCompatLoader.java @@ -0,0 +1,22 @@ +package com.sorrowmist.useless.compat.modernindustrialization; + +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.modernindustrialization.ModernIndustrializationRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.modernindustrialization.ForgeHammerRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.modernindustrialization.NuclearAbsorptionRecipeAdapter; + +/** Optional Modern Industrialization entrypoint. */ +public final class ModernIndustrializationRecipeCompatLoader { + private ModernIndustrializationRecipeCompatLoader() { + } + + public static void register() { + AlloyFurnaceRecipeManager.getInstance().registerAdapter( + new ModernIndustrializationRecipeAdapter(), RecipeSourceIds.MODERN_INDUSTRIALIZATION); + AlloyFurnaceRecipeManager.getInstance().registerAdapter( + new ForgeHammerRecipeAdapter(), RecipeSourceIds.MODERN_INDUSTRIALIZATION); + AlloyFurnaceRecipeManager.getInstance().registerAdapter( + new NuclearAbsorptionRecipeAdapter(), RecipeSourceIds.MODERN_INDUSTRIALIZATION); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/NeoEcoCompat.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/NeoEcoCompat.java new file mode 100644 index 00000000..561a6d93 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/NeoEcoCompat.java @@ -0,0 +1,95 @@ +package com.sorrowmist.useless.compat.neoecoae; + +import appeng.api.networking.crafting.ICraftingProvider; +import cn.dancingsnow.neoecoae.api.me.ECOCraftingCPU; +import cn.dancingsnow.neoecoae.api.me.dispatch.ECOCraftingCpuContext; +import cn.dancingsnow.neoecoae.api.me.dispatch.ECOCraftingDispatchPolicy; +import cn.dancingsnow.neoecoae.api.me.dispatch.ECOCraftingDispatchPolicyRegistry; +import cn.dancingsnow.neoecoae.api.me.attachment.ECOCraftingJobAttachmentRegistry; +import cn.dancingsnow.neoecoae.api.me.lifecycle.ECOCraftingDispatchEvent; +import cn.dancingsnow.neoecoae.api.me.lifecycle.ECOCraftingJobContext; +import cn.dancingsnow.neoecoae.api.me.lifecycle.ECOCraftingJobResult; +import cn.dancingsnow.neoecoae.api.me.lifecycle.ECOCraftingLifecycle; +import cn.dancingsnow.neoecoae.api.me.lifecycle.ECOCraftingLifecycleListener; +import com.sorrowmist.useless.content.blockentities.AdvancedAlloyFurnaceBlockEntity; +import com.sorrowmist.useless.content.blockentities.multiblock.MePatternAssemblyBlockEntity; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.DynamicPatternCpuStateManager; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.DynamicPatternExecution; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.Nullable; + +/** P16 public-API integration for dynamic Useless Mod outputs. */ +public final class NeoEcoCompat { + public static final ResourceLocation DYNAMIC_OUTPUT_ATTACHMENT = + new ResourceLocation("useless_mod", "dynamic_component_outputs"); + + private static final ECOCraftingLifecycleListener LIFECYCLE = new Lifecycle(); + private static final ECOCraftingDispatchPolicy POLICY = new DispatchPolicy(); + private static boolean initialized; + + private NeoEcoCompat() { + } + + public static synchronized void initialize() { + if (initialized) { + return; + } + ECOCraftingJobAttachmentRegistry.register( + DYNAMIC_OUTPUT_ATTACHMENT, NeoEcoDynamicJobAttachment::new); + ECOCraftingLifecycle.register(LIFECYCLE); + ECOCraftingDispatchPolicyRegistry.register(POLICY); + initialized = true; + } + + @Nullable + static Object grid(ECOCraftingJobContext context) { + return context.cpu() instanceof ECOCraftingCPU cpu ? cpu.getGrid() : null; + } + + private static boolean isUselessProvider(ICraftingProvider provider) { + return provider instanceof AdvancedAlloyFurnaceBlockEntity + || provider instanceof MePatternAssemblyBlockEntity; + } + + private static final class Lifecycle implements ECOCraftingLifecycleListener { + @Override + public void onJobStarted(ECOCraftingJobContext context) { + DynamicPatternCpuStateManager.INSTANCE.ensureEcoJob( + context.craftingJobId(), context.cpu(), grid(context)); + } + + @Override + public void onPatternDispatched(ECOCraftingDispatchEvent event) { + DynamicPatternExecution.Resolved resolved = + DynamicPatternExecution.resolve(event.pattern()); + if (resolved == null || !isUselessProvider(event.provider())) { + return; + } + long copies = Math.multiplyExact(resolved.copies(), event.dispatchedCrafts()); + DynamicPatternCpuStateManager.INSTANCE.bindEcoDispatch( + event.job().craftingJobId(), + event.job().cpu(), + grid(event.job()), + resolved.pattern(), + event.job().finalOutput() == null ? null : event.job().finalOutput().what(), + copies); + } + + @Override + public void onJobFinished(ECOCraftingJobContext context, ECOCraftingJobResult result) { + DynamicPatternCpuStateManager.INSTANCE.clearEcoJob(context.craftingJobId()); + } + } + + private static final class DispatchPolicy implements ECOCraftingDispatchPolicy { + @Override + public boolean isProviderAvailable(ECOCraftingCpuContext context, ICraftingProvider provider) { + if (!isUselessProvider(provider)) { + return !provider.isBusy(); + } + Object grid = context.cpu() instanceof ECOCraftingCPU cpu ? cpu.getGrid() : null; + return !provider.isBusy() + && !DynamicPatternCpuStateManager.INSTANCE.hasEcoPendingOnGrid(grid); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/NeoEcoDynamicJobAttachment.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/NeoEcoDynamicJobAttachment.java new file mode 100644 index 00000000..3e297a21 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/NeoEcoDynamicJobAttachment.java @@ -0,0 +1,51 @@ +package com.sorrowmist.useless.compat.neoecoae; + +import cn.dancingsnow.neoecoae.api.me.attachment.ECOCraftingJobAttachment; +import cn.dancingsnow.neoecoae.api.me.lifecycle.ECOCraftingJobContext; +import cn.dancingsnow.neoecoae.api.me.lifecycle.ECOCraftingJobResult; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.DynamicPatternCpuStateManager; +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; + +import java.util.UUID; + +/** Persists dynamic-output reservations through NeoECOAE's public job attachment API. */ +public final class NeoEcoDynamicJobAttachment implements ECOCraftingJobAttachment { + private final UUID craftingJobId; + private final Object cpu; + private final Object grid; + + public NeoEcoDynamicJobAttachment(ECOCraftingJobContext context) { + this.craftingJobId = context.craftingJobId(); + this.cpu = context.cpu(); + this.grid = NeoEcoCompat.grid(context); + DynamicPatternCpuStateManager.INSTANCE.ensureEcoJob(craftingJobId, cpu, grid); + } + + @Override + public ResourceLocation id() { + return NeoEcoCompat.DYNAMIC_OUTPUT_ATTACHMENT; + } + + @Override + public CompoundTag save(HolderLookup.Provider registries) { + CompoundTag tag = DynamicPatternCpuStateManager.INSTANCE.writeEcoJob(craftingJobId, registries); + return tag == null ? new CompoundTag() : tag; + } + + @Override + public void load(CompoundTag tag, HolderLookup.Provider registries) { + if (tag.isEmpty()) { + DynamicPatternCpuStateManager.INSTANCE.ensureEcoJob(craftingJobId, cpu, grid); + } else { + DynamicPatternCpuStateManager.INSTANCE.readEcoJob( + craftingJobId, cpu, grid, tag, registries); + } + } + + @Override + public void clear(ECOCraftingJobResult result) { + DynamicPatternCpuStateManager.INSTANCE.clearEcoJob(craftingJobId); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/NeoEcoDynamicOutputCompat.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/NeoEcoDynamicOutputCompat.java new file mode 100644 index 00000000..c10b1482 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/NeoEcoDynamicOutputCompat.java @@ -0,0 +1,64 @@ +package com.sorrowmist.useless.compat.neoecoae; + +import appeng.api.config.Actionable; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.AEKey; +import cn.dancingsnow.neoecoae.api.me.ECOCraftingCPU; +import cn.dancingsnow.neoecoae.api.me.output.ECOCraftingOutputClaimRequest; +import cn.dancingsnow.neoecoae.api.me.output.ECOCraftingOutputClaimResult; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.DynamicPatternCpuStateManager; +import org.jetbrains.annotations.Nullable; + +/** Routes dynamic item outputs through P16's atomic output-claim API. */ +public final class NeoEcoDynamicOutputCompat { + private NeoEcoDynamicOutputCompat() { + } + + public static long claim(@Nullable Object grid, AEKey actualKey, long amount) { + if (grid == null || !(actualKey instanceof AEItemKey itemKey) || amount <= 0L) { + return 0L; + } + + DynamicPatternCpuStateManager manager = DynamicPatternCpuStateManager.INSTANCE; + DynamicPatternCpuStateManager.EcoCandidate match = null; + long simulatedAmount = 0L; + for (var candidate : manager.ecoCandidates(grid, itemKey.getId())) { + if (!(candidate.cpu() instanceof ECOCraftingCPU cpu)) { + continue; + } + ECOCraftingOutputClaimResult preview = cpu.getOutputClaimSink().claimCraftingOutput( + new ECOCraftingOutputClaimRequest( + candidate.craftingId(), + candidate.exactExpectedKey(), + actualKey, + amount, + Actionable.SIMULATE)); + if (!preview.accepted() || preview.claimedAmount() <= 0L) { + continue; + } + if (match != null) { + return 0L; + } + match = candidate; + simulatedAmount = preview.claimedAmount(); + } + if (match == null) { + return 0L; + } + + ECOCraftingCPU cpu = (ECOCraftingCPU) match.cpu(); + ECOCraftingOutputClaimResult committed = cpu.getOutputClaimSink().claimCraftingOutput( + new ECOCraftingOutputClaimRequest( + match.craftingId(), + match.exactExpectedKey(), + actualKey, + simulatedAmount, + Actionable.MODULATE)); + if (!committed.accepted() || committed.claimedAmount() <= 0L) { + return 0L; + } + DynamicPatternCpuStateManager.INSTANCE.claimEcoExact( + match.craftingId(), match.exactExpectedKey(), committed.claimedAmount()); + return committed.claimedAmount(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/NeoEcoPlanningCompat.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/NeoEcoPlanningCompat.java new file mode 100644 index 00000000..5090304e --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/NeoEcoPlanningCompat.java @@ -0,0 +1,31 @@ +package com.sorrowmist.useless.compat.neoecoae; + +import appeng.api.networking.crafting.ICraftingPlan; +import cn.dancingsnow.neoecoae.api.me.planning.ECOPlanningResultRegistry; +import net.minecraftforge.fml.ModList; + +/** Keeps ECO's confirmed execution vector intact without linking ECO on the ordinary AE2 path. */ +public final class NeoEcoPlanningCompat { + private NeoEcoPlanningCompat() { + } + + public static boolean shouldPreserveSubmissionPlan(ICraftingPlan plan) { + if (!ModList.get().isLoaded("neoecoae")) { + return false; + } + try { + return PlanningApi.shouldPreserveSubmissionPlan(plan); + } catch (NoClassDefFoundError | NoSuchMethodError unavailable) { + // Older ECO releases do not expose this API; keep their existing submission path. + return false; + } + } + + private static final class PlanningApi { + static boolean shouldPreserveSubmissionPlan(ICraftingPlan plan) { + // The API checks the complete vector inside the synchronous confirmation scope. + // ECO provenance or matching final outputs alone must not suppress other planners. + return ECOPlanningResultRegistry.shouldPreserveSubmissionPlan(plan); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/CompactMachineClassMap.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/CompactMachineClassMap.java new file mode 100644 index 00000000..a2a94208 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/CompactMachineClassMap.java @@ -0,0 +1,51 @@ +package com.sorrowmist.useless.compat.neoecoae.compact; + +import org.jetbrains.annotations.Nullable; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 「紧凑主机 → ECO 主机类」的映射表,供 AE2 的网格类索引使用。 + * + *

为什么需要它:AE2 的 {@code Grid} 用 + * {@code gridNode.getOwner().getClass()}(精确运行时类)给机器建索引, + * 而 {@code IGrid.getMachines(Class)} 是精确 key 查询。ECO 通篇依赖 + * {@code grid.getMachines(ECOComputationSystemBlockEntity.class)} / + * {@code getMachines(ECOCraftingSystemBlockEntity.class)} 来找自己的主机, + * 我们的紧凑主机是它们的子类 → 查询永远落空(CPU 因此永远不会被 CraftingService 登记)。

+ * + *

映射表让 {@code Grid.add/remove} 把紧凑主机登记在 父类 名下, + * 于是从 AE2 与 ECO 的视角看,紧凑主机就是一个标准 ECO 主机。

+ * + *

本类刻意不引用任何 ECO 类型:它会被 AE2 的 mixin 引用, + * 必须能在没有 ECO 的环境下安全加载(那时映射表是空的,等于什么都没做)。

+ */ +public final class CompactMachineClassMap { + + private static final Map, Class> PRESENTED = new ConcurrentHashMap<>(); + + private CompactMachineClassMap() {} + + /** 注册「子类在网格里应该以哪个父类的身份出现」。 */ + public static void register(Class compactClass, Class presentedClass) { + PRESENTED.put(compactClass, presentedClass); + } + + /** + * @param ownerClass {@code node.getOwner().getClass()} + * @return 该宿主在 AE2 机器索引里应该使用的 key(非紧凑方块原样返回) + */ + public static Class presentedClass(@Nullable Class ownerClass) { + if (ownerClass == null || PRESENTED.isEmpty()) { + return ownerClass; + } + Class presented = PRESENTED.get(ownerClass); + return presented == null ? ownerClass : presented; + } + + /** 该宿主是否是紧凑方块(用于让 AE2 的 CPU 列表在节点入网时刷新)。 */ + public static boolean isCompactHost(@Nullable Object owner) { + return owner != null && !PRESENTED.isEmpty() && PRESENTED.containsKey(owner.getClass()); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/NeoEcoCompactRegistry.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/NeoEcoCompactRegistry.java new file mode 100644 index 00000000..aeef2a23 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/NeoEcoCompactRegistry.java @@ -0,0 +1,310 @@ +package com.sorrowmist.useless.compat.neoecoae.compact; + +import appeng.capabilities.Capabilities; +import appeng.api.networking.IInWorldGridNodeHost; +import appeng.block.AEBaseEntityBlock; +import appeng.blockentity.AEBaseBlockEntity; +import cn.dancingsnow.neoecoae.blocks.entity.computation.ECOComputationSystemBlockEntity; +import cn.dancingsnow.neoecoae.blocks.entity.ECOMachineInterfaceBlockEntity; +import cn.dancingsnow.neoecoae.blocks.entity.crafting.ECOCraftingPatternBusBlockEntity; +import cn.dancingsnow.neoecoae.blocks.entity.crafting.ECOCraftingSystemBlockEntity; +import cn.dancingsnow.neoecoae.blocks.entity.storage.ECOStorageSystemBlockEntity; +import cn.dancingsnow.neoecoae.multiblock.calculator.NEClusterCalculator; +import cn.dancingsnow.neoecoae.multiblock.cluster.NECluster; +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.compat.neoecoae.compact.block.CompactBlockItem; +import com.sorrowmist.useless.compat.neoecoae.compact.block.CompactC9Block; +import com.sorrowmist.useless.compat.neoecoae.compact.block.CompactF9Block; +import com.sorrowmist.useless.compat.neoecoae.compact.block.CompactL9Block; +import com.sorrowmist.useless.compat.neoecoae.compact.block.CompactTicker; +import com.sorrowmist.useless.compat.neoecoae.compact.calculator.CompactComputationClusterCalculator; +import com.sorrowmist.useless.compat.neoecoae.compact.calculator.CompactCraftingClusterCalculator; +import com.sorrowmist.useless.compat.neoecoae.compact.calculator.CompactStorageClusterCalculator; +import com.sorrowmist.useless.compat.neoecoae.compact.entity.CompactC9BlockEntity; +import com.sorrowmist.useless.compat.neoecoae.compact.entity.CompactF9BlockEntity; +import com.sorrowmist.useless.compat.neoecoae.compact.entity.CompactL9BlockEntity; +import com.sorrowmist.useless.compat.neoecoae.compact.exposed.ExposedPatternBusBlockEntity; +import com.sorrowmist.useless.compat.neoecoae.compact.shadow.ShadowComputationDriveBlockEntity; +import com.sorrowmist.useless.compat.neoecoae.compact.shadow.ShadowComputationHostBlockEntity; +import com.sorrowmist.useless.compat.neoecoae.compact.shadow.ShadowComputationParallelCoreBlockEntity; +import com.sorrowmist.useless.compat.neoecoae.compact.shadow.ShadowCraftingHostBlockEntity; +import com.sorrowmist.useless.compat.neoecoae.compact.shadow.ShadowCraftingParallelCoreBlockEntity; +import com.sorrowmist.useless.compat.neoecoae.compact.shadow.ShadowCraftingWorkerBlockEntity; +import com.sorrowmist.useless.compat.neoecoae.compact.shadow.ShadowFluidInputHatchBlockEntity; +import com.sorrowmist.useless.compat.neoecoae.compact.shadow.ShadowFluidOutputHatchBlockEntity; +import com.sorrowmist.useless.compat.neoecoae.compact.shadow.ShadowInterfaceBlockEntity; +import com.sorrowmist.useless.compat.neoecoae.compact.shadow.ShadowPatternBusBlockEntity; +import com.sorrowmist.useless.compat.neoecoae.compact.shadow.ShadowThreadingCoreBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.SoundType; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraftforge.eventbus.api.IEventBus; +import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; +import net.minecraftforge.common.capabilities.ForgeCapabilities; +import com.sorrowmist.useless.compat.forge1201.capability.RegisterCapabilitiesEvent; +import com.sorrowmist.useless.compat.forge1201.registry.DeferredBlock; +import com.sorrowmist.useless.compat.forge1201.registry.DeferredHolder; +import com.sorrowmist.useless.compat.forge1201.registry.DeferredItem; +import com.sorrowmist.useless.compat.forge1201.registry.DeferredRegister; + +/** + * 无用型紧凑三方块(C9 / F9 / L9)的注册入口。 + * + *

本类只能在确认 neoecoae 已加载之后才会被类加载,因此可以安全地直接引用 ECO 的类型。 + * 三个方块都是「单方块等价于整套满配多方块」的壳:自己不带任何机器逻辑,全部把调用转给 ECO。

+ * + *

注意:ECO 用 Registrate 注册内容,而 Registrate 只是它的 {@code implementation} 依赖, + * 不在本工程的编译类路径上({@code BlockEntry} / {@code ItemEntry} 不可访问)。 + * 因此这里一律不改用 ECO 的注册条目对象,改走注册表按 id 查询。

+ */ +public final class NeoEcoCompactRegistry { + + public static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks(UselessMod.MODID); + public static final DeferredRegister.Items ITEMS = DeferredRegister.createItems(UselessMod.MODID); + public static final DeferredRegister> BLOCK_ENTITY_TYPES = + DeferredRegister.create(Registries.BLOCK_ENTITY_TYPE, UselessMod.MODID); + + private NeoEcoCompactRegistry() {} + + private static BlockBehaviour.Properties shellProperties() { + return BlockBehaviour.Properties.of() + .strength(6.0F, 1200.0F) + .sound(SoundType.METAL) + .requiresCorrectToolForDrops(); + } + + // ---------------------------------------------------------------- 方块 + + public static final DeferredBlock COMPACT_C9 = + BLOCKS.register("useless_compact_c9", () -> new CompactC9Block(shellProperties())); + + public static final DeferredBlock COMPACT_F9 = + BLOCKS.register("useless_compact_f9", () -> new CompactF9Block(shellProperties())); + + public static final DeferredBlock COMPACT_L9 = + BLOCKS.register("useless_compact_l9", () -> new CompactL9Block(shellProperties())); + + public static final DeferredItem COMPACT_C9_ITEM = ITEMS.register( + "useless_compact_c9", () -> new CompactBlockItem(COMPACT_C9.get(), new Item.Properties())); + public static final DeferredItem COMPACT_F9_ITEM = ITEMS.register( + "useless_compact_f9", () -> new CompactBlockItem(COMPACT_F9.get(), new Item.Properties())); + public static final DeferredItem COMPACT_L9_ITEM = ITEMS.register( + "useless_compact_l9", () -> new CompactBlockItem(COMPACT_L9.get(), new Item.Properties())); + + // ------------------------------------------------------ 宿主方块实体类型 + // 注意:lambda 里对自身字段一律用「类名限定」写法,避免 javac 的「初始化程序自引用」。 + + public static final DeferredHolder, BlockEntityType> COMPACT_C9_BE = + BLOCK_ENTITY_TYPES.register("useless_compact_c9", () -> BlockEntityType.Builder.of( + (pos, state) -> new CompactC9BlockEntity( + NeoEcoCompactRegistry.COMPACT_C9_BE.get(), pos, state), + COMPACT_C9.get()).build(null)); + + public static final DeferredHolder, BlockEntityType> COMPACT_F9_BE = + BLOCK_ENTITY_TYPES.register("useless_compact_f9", () -> BlockEntityType.Builder.of( + (pos, state) -> new CompactF9BlockEntity( + NeoEcoCompactRegistry.COMPACT_F9_BE.get(), pos, state), + COMPACT_F9.get()).build(null)); + + public static final DeferredHolder, BlockEntityType> COMPACT_L9_BE = + BLOCK_ENTITY_TYPES.register("useless_compact_l9", () -> BlockEntityType.Builder.of( + (pos, state) -> new CompactL9BlockEntity( + NeoEcoCompactRegistry.COMPACT_L9_BE.get(), pos, state), + COMPACT_L9.get()).build(null)); + + // ------------------------------------------------------- 影子组件实体类型 + // 影子组件永远不会由世界创建,这些类型只用于手动 new 出「不落世界的 ECO 组件」。 + + public static final DeferredHolder, BlockEntityType> + SHADOW_COMPUTATION_HOST = BLOCK_ENTITY_TYPES.register("compact_shadow_computation_host", + () -> BlockEntityType.Builder.of( + (pos, state) -> new ShadowComputationHostBlockEntity( + NeoEcoCompactRegistry.SHADOW_COMPUTATION_HOST.get(), pos, state), + COMPACT_C9.get()).build(null)); + + public static final DeferredHolder, BlockEntityType> + SHADOW_THREADING_CORE = BLOCK_ENTITY_TYPES.register("compact_shadow_threading_core", + () -> BlockEntityType.Builder.of( + (pos, state) -> new ShadowThreadingCoreBlockEntity( + NeoEcoCompactRegistry.SHADOW_THREADING_CORE.get(), pos, state), + COMPACT_C9.get()).build(null)); + + public static final DeferredHolder, BlockEntityType> + SHADOW_COMPUTATION_DRIVE = BLOCK_ENTITY_TYPES.register("compact_shadow_computation_drive", + () -> BlockEntityType.Builder.of( + (pos, state) -> new ShadowComputationDriveBlockEntity( + NeoEcoCompactRegistry.SHADOW_COMPUTATION_DRIVE.get(), pos, state), + COMPACT_C9.get()).build(null)); + + public static final DeferredHolder, BlockEntityType> + SHADOW_COMPUTATION_PARALLEL_CORE = + BLOCK_ENTITY_TYPES.register("compact_shadow_computation_parallel_core", + () -> BlockEntityType.Builder.of( + (pos, state) -> new ShadowComputationParallelCoreBlockEntity( + NeoEcoCompactRegistry.SHADOW_COMPUTATION_PARALLEL_CORE.get(), pos, state), + COMPACT_C9.get()).build(null)); + + public static final DeferredHolder, BlockEntityType> + SHADOW_CRAFTING_HOST = BLOCK_ENTITY_TYPES.register("compact_shadow_crafting_host", + () -> BlockEntityType.Builder.of( + (pos, state) -> new ShadowCraftingHostBlockEntity( + NeoEcoCompactRegistry.SHADOW_CRAFTING_HOST.get(), pos, state), + COMPACT_F9.get()).build(null)); + + public static final DeferredHolder, BlockEntityType> + SHADOW_CRAFTING_WORKER = BLOCK_ENTITY_TYPES.register("compact_shadow_crafting_worker", + () -> BlockEntityType.Builder.of( + (pos, state) -> new ShadowCraftingWorkerBlockEntity( + NeoEcoCompactRegistry.SHADOW_CRAFTING_WORKER.get(), pos, state), + COMPACT_F9.get()).build(null)); + + public static final DeferredHolder, BlockEntityType> + SHADOW_CRAFTING_PARALLEL_CORE = BLOCK_ENTITY_TYPES.register("compact_shadow_crafting_parallel_core", + () -> BlockEntityType.Builder.of( + (pos, state) -> new ShadowCraftingParallelCoreBlockEntity( + NeoEcoCompactRegistry.SHADOW_CRAFTING_PARALLEL_CORE.get(), pos, state), + COMPACT_F9.get()).build(null)); + + public static final DeferredHolder, BlockEntityType> + SHADOW_PATTERN_BUS = BLOCK_ENTITY_TYPES.register("compact_shadow_pattern_bus", + () -> BlockEntityType.Builder.of( + (pos, state) -> new ShadowPatternBusBlockEntity( + NeoEcoCompactRegistry.SHADOW_PATTERN_BUS.get(), pos, state), + COMPACT_F9.get()).build(null)); + + public static final DeferredHolder, BlockEntityType> + SHADOW_FLUID_INPUT_HATCH = BLOCK_ENTITY_TYPES.register("compact_shadow_fluid_input_hatch", + () -> BlockEntityType.Builder.of( + (pos, state) -> new ShadowFluidInputHatchBlockEntity( + NeoEcoCompactRegistry.SHADOW_FLUID_INPUT_HATCH.get(), pos, state), + COMPACT_F9.get()).build(null)); + + public static final DeferredHolder, BlockEntityType> + SHADOW_FLUID_OUTPUT_HATCH = BLOCK_ENTITY_TYPES.register("compact_shadow_fluid_output_hatch", + () -> BlockEntityType.Builder.of( + (pos, state) -> new ShadowFluidOutputHatchBlockEntity( + NeoEcoCompactRegistry.SHADOW_FLUID_OUTPUT_HATCH.get(), pos, state), + COMPACT_F9.get()).build(null)); + + public static final DeferredHolder, BlockEntityType> + COMPACT_EXPOSED_PATTERN_BUS = BLOCK_ENTITY_TYPES.register("compact_exposed_pattern_bus", + () -> BlockEntityType.Builder.of( + (pos, state) -> new ExposedPatternBusBlockEntity( + NeoEcoCompactRegistry.COMPACT_EXPOSED_PATTERN_BUS.get(), pos, state), + COMPACT_F9.get()).build(null)); + + // ---------------------------------------------------------------- 生命周期 + + public static void init(IEventBus modEventBus) { + // AE2 按宿主的「精确运行时类」建机器索引,而 ECO 用父类去查主机; + // 先把「紧凑主机 → ECO 主机类」登记好,Grid 的 mixin 会据此改写索引 key。 + CompactMachineClassMap.register(CompactC9BlockEntity.class, ECOComputationSystemBlockEntity.class); + CompactMachineClassMap.register(CompactF9BlockEntity.class, ECOCraftingSystemBlockEntity.class); + CompactMachineClassMap.register(CompactL9BlockEntity.class, ECOStorageSystemBlockEntity.class); + // 影子接口与聚合总线也各自带着「假身份」的网格节点:ECO 的界面与样板终端是按 + // ECOMachineInterfaceBlockEntity / ECOCraftingPatternBusBlockEntity 这两个类去查网格的。 + CompactMachineClassMap.register(ShadowInterfaceBlockEntity.class, ECOMachineInterfaceBlockEntity.class); + CompactMachineClassMap.register(ExposedPatternBusBlockEntity.class, ECOCraftingPatternBusBlockEntity.class); + + BLOCKS.register(modEventBus); + ITEMS.register(modEventBus); + BLOCK_ENTITY_TYPES.register(modEventBus); + modEventBus.addListener(NeoEcoCompactRegistry::onCommonSetup); + modEventBus.addListener(NeoEcoCompactRegistry::onRegisterCapabilities); + } + + /** + * AE2 只在方块暴露了 {@code IN_WORLD_GRID_NODE_HOST} 能力时才认它是网格节点宿主。 + * ECO 在自己的 {@code NEBlockEntityEntry#onRegisterCapabilies} 里做了同样的事; + * 漏掉这一步会让紧凑方块「有节点但永远接不上相邻线缆」。 + */ + private static void onRegisterCapabilities(RegisterCapabilitiesEvent event) { + event.registerBlockEntity(appeng.capabilities.Capabilities.IN_WORLD_GRID_NODE_HOST, COMPACT_C9_BE.get(), + (blockEntity, side) -> (IInWorldGridNodeHost) blockEntity); + event.registerBlockEntity(appeng.capabilities.Capabilities.IN_WORLD_GRID_NODE_HOST, COMPACT_F9_BE.get(), + (blockEntity, side) -> (IInWorldGridNodeHost) blockEntity); + event.registerBlockEntity(ForgeCapabilities.FLUID_HANDLER, COMPACT_F9_BE.get(), + (blockEntity, side) -> blockEntity.getFluidHandler()); + event.registerBlockEntity(appeng.capabilities.Capabilities.IN_WORLD_GRID_NODE_HOST, COMPACT_L9_BE.get(), + (blockEntity, side) -> (IInWorldGridNodeHost) blockEntity); + } + + private static void onCommonSetup(FMLCommonSetupEvent event) { + event.enqueueWork(() -> { + // The compact host owns a single-block fleet. Registering these factories before + // world loading prevents ECO's delayed normal structure scan from tearing it down. + registerCalculatorFactory(COMPACT_C9_BE.get(), CompactComputationClusterCalculator::new); + registerCalculatorFactory(COMPACT_F9_BE.get(), CompactCraftingClusterCalculator::new); + registerCalculatorFactory(COMPACT_L9_BE.get(), CompactStorageClusterCalculator::new); + wire(COMPACT_C9.get(), COMPACT_C9_BE.get(), CompactC9BlockEntity.class, COMPACT_C9_ITEM.get()); + wire(COMPACT_F9.get(), COMPACT_F9_BE.get(), CompactF9BlockEntity.class, COMPACT_F9_ITEM.get()); + wire(COMPACT_L9.get(), COMPACT_L9_BE.get(), CompactL9BlockEntity.class, COMPACT_L9_ITEM.get()); + }); + } + + /** + * 复刻 ECO 自己的 {@code NEBlockEntityEntry#onCommonSetup}:把方块和它的方块实体类型绑起来。 + * 影子组件不需要绑定,它们只被我们手动实例化。 + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + private static void wire(AEBaseEntityBlock block, BlockEntityType type, Class blockEntityClass, Item item) { + block.setBlockEntity( + blockEntityClass, + type, + null, + (level, pos, state, be) -> { + if (be instanceof CompactTicker ticker) { + ticker.tick(level, pos, state); + } + } + ); + AEBaseBlockEntity.registerBlockEntityItem(type, item); + } + + /** + * ECO 21.2.2 exposes calculator factories, while 21.2.0 does not. Keep the compact addon + * binary-compatible with the declared minimum and use the newer single-block hook when it + * is present at runtime. + */ + private static > void registerCalculatorFactory( + BlockEntityType type, NEClusterCalculator.Factory factory) { + try { + Class owner = Class.forName("cn.dancingsnow.neoecoae.blocks.entity.NEBlockEntity"); + Class factoryType = Class.forName( + "cn.dancingsnow.neoecoae.multiblock.calculator.NEClusterCalculator$Factory"); + owner.getMethod("registerCalculatorFactory", BlockEntityType.class, factoryType) + .invoke(null, type, factory); + } catch (ReflectiveOperationException ignored) { + // Older ECO keeps the compact host's manual lifecycle path as the fallback. + } + } + + // ------------------------------------------------------------------ 便利 + + /** 按 id 取 ECO 的物品,避免触碰 Registrate 的 {@code ItemEntry} 类型。 */ + public static Item neoEcoItem(String path) { + return BuiltInRegistries.ITEM.get(new ResourceLocation("neoecoae", path)); + } + + public static ItemStack neoEcoStack(String path) { + Item item = neoEcoItem(path); + return item == null ? ItemStack.EMPTY : new ItemStack(item); + } + + /** 影子组件用的方块状态:紧凑方块自己的默认状态(影子组件从不写入世界,状态只用于占位)。 */ + public static net.minecraft.world.level.block.state.BlockState placeholderState( + DeferredBlock block) { + return block.get().defaultBlockState(); + } + + public static BlockPos hostAnchor(BlockPos pos) { + return pos; + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/block/CompactBlockItem.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/block/CompactBlockItem.java new file mode 100644 index 00000000..f89ad5e7 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/block/CompactBlockItem.java @@ -0,0 +1,29 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.block; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.block.Block; + +import java.util.List; + +/** + * 紧凑方块的物品形态:额外挂一条说明,告诉玩家这个单方块等价于哪套满配多方块。 + * + *

说明文本直接由物品 id 推导(`block.xxx` → `tooltip.xxx`),三份语言文件里已经写好。

+ */ +public class CompactBlockItem extends BlockItem { + + public CompactBlockItem(Block block, Properties properties) { + super(block, properties); + } + + @Override + public void appendHoverText(ItemStack stack, net.minecraft.world.level.Level context, List tooltip, TooltipFlag flag) { + super.appendHoverText(stack, context, tooltip, flag); + String key = getDescriptionId().replaceFirst("^block\\.", "tooltip."); + tooltip.add(Component.translatable(key).withStyle(ChatFormatting.GRAY)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/block/CompactC9Block.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/block/CompactC9Block.java new file mode 100644 index 00000000..f5a70d6d --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/block/CompactC9Block.java @@ -0,0 +1,51 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.block; + +import cn.dancingsnow.neoecoae.blocks.computation.ECOComputationSystem; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.phys.BlockHitResult; + +/** + * 无用型紧凑 C9 的方块形态。 + * + *

直接继承 ECO 的计算系统主机方块,于是「右键开 GUI」「玩家距离校验」「掉落物带设置」 + * 这些行为全部沿用 ECO 的实现,本类只提供一个自己的方块实体。

+ */ +public class CompactC9Block extends ECOComputationSystem { + + public CompactC9Block(Properties properties) { + super(properties); + registerDefaultState(defaultBlockState().setValue(CompactInterfaceAccess.INTERFACE, false)); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + super.createBlockStateDefinition(builder); + builder.add(CompactInterfaceAccess.INTERFACE); + } + + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, + BlockHitResult hitResult) { + if (!CompactInterfaceAccess.isPlayerCloseEnough(this, level, pos, player)) { + return InteractionResult.FAIL; + } + if (player.isShiftKeyDown()) { + if (player instanceof ServerPlayer serverPlayer) { + CompactInterfaceAccess.open(this, state, pos, serverPlayer); + } + return InteractionResult.sidedSuccess(level.isClientSide); + } + if (player instanceof ServerPlayer serverPlayer) { + CompactInterfaceAccess.openMain(this, state, pos, serverPlayer); + return InteractionResult.CONSUME; + } + return InteractionResult.SUCCESS; + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/block/CompactF9Block.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/block/CompactF9Block.java new file mode 100644 index 00000000..c5196cbc --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/block/CompactF9Block.java @@ -0,0 +1,158 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.block; + +import appeng.api.crafting.PatternDetailsHelper; +import cn.dancingsnow.neoecoae.api.ECOPatternInsertionResult; +import cn.dancingsnow.neoecoae.blocks.crafting.ECOCraftingSystem; +import com.sorrowmist.useless.compat.neoecoae.compact.entity.CompactF9BlockEntity; +import com.sorrowmist.useless.core.component.ExternalInventoryReference; +import com.sorrowmist.useless.core.component.UComponents; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.ItemInteractionResult; +import net.minecraft.world.entity.LivingEntity; +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.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.storage.loot.LootParams; +import net.minecraft.world.level.storage.loot.parameters.LootContextParams; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraftforge.common.capabilities.ForgeCapabilities; +import net.minecraftforge.fluids.FluidUtil; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + * 无用型紧凑 F9 的方块形态。 + * + *

直接继承 ECO 的合成系统主机方块,交互与界面沿用 ECO 实现; + * 额外补一个「手持编码样板右键」入口——紧凑方块内部没有可交互的样板总线方块, + * 所以给玩家一条把样板送进 ECO 样板总线的路。

+ * + *

另外接管样板数据(存在外置存档里,见 {@link CompactF9BlockEntity})的搬运: + * 拆掉时把 UUID 挂到掉落物上、放下时接回来,避免拆一台满样板的主机等于烧掉 176 条总线。

+ */ +public class CompactF9Block extends ECOCraftingSystem { + + public CompactF9Block(Properties properties) { + super(properties); + registerDefaultState(defaultBlockState().setValue(CompactInterfaceAccess.INTERFACE, false)); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + super.createBlockStateDefinition(builder); + builder.add(CompactInterfaceAccess.INTERFACE); + } + + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, + BlockHitResult hitResult) { + if (!CompactInterfaceAccess.isPlayerCloseEnough(this, level, pos, player)) { + return InteractionResult.FAIL; + } + if (player.isShiftKeyDown()) { + if (player instanceof ServerPlayer serverPlayer) { + CompactInterfaceAccess.open(this, state, pos, serverPlayer); + } + return InteractionResult.sidedSuccess(level.isClientSide); + } + if (player instanceof ServerPlayer serverPlayer) { + CompactInterfaceAccess.openMain(this, state, pos, serverPlayer); + return InteractionResult.CONSUME; + } + return InteractionResult.SUCCESS; + } + + @Override + protected ItemInteractionResult useItemOn(ItemStack heldItem, BlockState state, Level level, BlockPos pos, + Player player, InteractionHand hand, BlockHitResult hit) { + if (isEncodedPattern(heldItem, level) + && level.getBlockEntity(pos) instanceof CompactF9BlockEntity compact) { + if (level.isClientSide) { + return ItemInteractionResult.SUCCESS; + } + ECOPatternInsertionResult result = compact.insertPatternFromPlayerWithResult(heldItem); + if (result == ECOPatternInsertionResult.INSERTED) { + if (!player.isCreative()) { + heldItem.shrink(1); + } + return ItemInteractionResult.sidedSuccess(false); + } + player.displayClientMessage(Component.translatable( + "gui.useless_mod.compact.f9.pattern_result." + result.name().toLowerCase()), true); + return ItemInteractionResult.sidedSuccess(false); + } + if (level.getBlockEntity(pos) instanceof CompactF9BlockEntity compact + && isFluidContainer(heldItem)) { + if (level.isClientSide) { + return ItemInteractionResult.SUCCESS; + } + // Output extraction and normal input filling use the public capability. The + // second pass is a player-only recovery path for fluid left in the input pool. + if (FluidUtil.interactWithFluidHandler(player, hand, compact.getFluidHandler()) + || FluidUtil.interactWithFluidHandler(player, hand, compact.getInputRecoveryHandler())) { + return ItemInteractionResult.sidedSuccess(false); + } + } + return super.useItemOn(heldItem, state, level, pos, player, hand, hit); + } + + /** 客户端也要能判断,否则会在服务端吞掉右键后与客户端表现不一致。 */ + private static boolean isEncodedPattern(ItemStack stack, Level level) { + return !stack.isEmpty() && PatternDetailsHelper.decodePattern(stack, level) != null; + } + + private static boolean isFluidContainer(ItemStack stack) { + return !stack.isEmpty() + && (com.sorrowmist.useless.compat.forge1201.capability.CapabilityAccess.get(stack, ForgeCapabilities.FLUID_HANDLER_ITEM) != null + || FluidUtil.getFluidContained(stack).isPresent()); + } + + // ------------------------------------------------ 外置样板数据的搬运 + + /** + * 拆掉方块时先落盘再解除占用。必须在 {@code super.onRemove} 之前做: + * 父类会把主机集群拆掉,影子总线就没了,那时候想序列化也来不及。 + */ + @Override + public void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean movedByPiston) { + if (!state.is(newState.getBlock()) && !level.isClientSide + && level.getBlockEntity(pos) instanceof CompactF9BlockEntity compact) { + compact.releasePatternStore(); + } + super.onRemove(state, level, pos, newState, movedByPiston); + } + + /** 掉落物带上外置存储的引用,玩家把机器挪个地方不会丢样板。 */ + @Override + public List getDrops(BlockState state, LootParams.Builder params) { + List drops = super.getDrops(state, params); + if (params.getOptionalParameter(LootContextParams.BLOCK_ENTITY) instanceof CompactF9BlockEntity compact) { + ExternalInventoryReference reference = compact.patternStoreReference(); + if (reference != null) { + for (ItemStack drop : drops) { + if (drop.is(asItem())) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(drop, UComponents.EXTERNAL_INVENTORY_REFERENCE.get(), reference); + break; + } + } + } + } + return drops; + } + + @Override + public void setPlacedBy(Level level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { + super.setPlacedBy(level, pos, state, placer, stack); + if (!level.isClientSide && level.getBlockEntity(pos) instanceof CompactF9BlockEntity compact) { + compact.adoptPatternStore(com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(stack, UComponents.EXTERNAL_INVENTORY_REFERENCE.get())); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/block/CompactInterfaceAccess.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/block/CompactInterfaceAccess.java new file mode 100644 index 00000000..e8a9a527 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/block/CompactInterfaceAccess.java @@ -0,0 +1,56 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.block; + +import com.lowdragmc.lowdraglib2.gui.factory.BlockUIMenuType; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.Level; + +/** + * 紧凑方块的「潜行右键开通讯接口界面」入口。 + * + *

三个紧凑方块共用同一个布尔属性 {@link #INTERFACE}。它只活在菜单 holder 的方块状态字段里: + * 服务端打开菜单时用 {@code state.setValue(INTERFACE, true)} 造一份不下世界、不写方块的 + * 伪装状态,两端 {@code createUI} 都以 holder 里这一份状态作为唯一分支依据, + * 于是「开主机面板」与「开通讯接口界面」在双端必然同构,不存在各自读潜行状态的竞态。

+ * + *

伪装的属性值不会被任何世界逻辑读取:LDLib2 的 {@code stillValid} 只比方块身份, + * ECO 的距离校验只读世界方块状态,画模型走的是世界状态。

+ */ +public final class CompactInterfaceAccess { + + /** 伪装状态位;multipart 方块状态 JSON 不引用它,所以不需要改模型。 */ + public static final BooleanProperty INTERFACE = BooleanProperty.create("useless_interface"); + + private CompactInterfaceAccess() {} + + /** 菜单 holder 里的状态位:true 表示这次要开通讯接口界面而不是主机面板。 */ + public static boolean requested(BlockState state) { + return state.hasProperty(INTERFACE) && state.getValue(INTERFACE); + } + + public static boolean isPlayerCloseEnough(Block block, Level level, BlockPos pos, Player player) { + return player.level() == level + && level.getBlockState(pos).getBlock() == block + && player.distanceToSqr(pos.getX() + 0.5D, pos.getY() + 0.5D, pos.getZ() + 0.5D) <= 64.0D; + } + + /** 服务端调用:用伪装状态自建 holder 并打开菜单。 */ + public static void open(BlockUIMenuType.BlockUI blockUI, BlockState state, BlockPos pos, ServerPlayer player) { + open(blockUI, state, pos, player, true); + } + + /** 服务端调用:显式打开普通主机面板,避免旧世界残留的伪装状态污染菜单分支。 */ + public static void openMain(BlockUIMenuType.BlockUI blockUI, BlockState state, BlockPos pos, ServerPlayer player) { + open(blockUI, state, pos, player, false); + } + + private static void open(BlockUIMenuType.BlockUI blockUI, BlockState state, BlockPos pos, + ServerPlayer player, boolean interfaceMode) { + player.openMenu(new BlockUIMenuType.BlockUIHolder(blockUI, player, pos, + state.setValue(INTERFACE, interfaceMode))); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/block/CompactL9Block.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/block/CompactL9Block.java new file mode 100644 index 00000000..ecb9f207 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/block/CompactL9Block.java @@ -0,0 +1,51 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.block; + +import cn.dancingsnow.neoecoae.blocks.storage.ECOStorageSystemBlock; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.phys.BlockHitResult; + +/** + * 无用型紧凑 L9 的方块形态。 + * + *

直接继承 ECO 的存储系统主机方块,因此「拆掉时把无限域写进掉落物、放回去时恢复」 + * 这条 ECO 原生链路对紧凑方块同样生效。

+ */ +public class CompactL9Block extends ECOStorageSystemBlock { + + public CompactL9Block(Properties properties) { + super(properties); + registerDefaultState(defaultBlockState().setValue(CompactInterfaceAccess.INTERFACE, false)); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + super.createBlockStateDefinition(builder); + builder.add(CompactInterfaceAccess.INTERFACE); + } + + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, + BlockHitResult hitResult) { + if (!CompactInterfaceAccess.isPlayerCloseEnough(this, level, pos, player)) { + return InteractionResult.FAIL; + } + if (player.isShiftKeyDown()) { + if (player instanceof ServerPlayer serverPlayer) { + CompactInterfaceAccess.open(this, state, pos, serverPlayer); + } + return InteractionResult.sidedSuccess(level.isClientSide); + } + if (player instanceof ServerPlayer serverPlayer) { + CompactInterfaceAccess.openMain(this, state, pos, serverPlayer); + return InteractionResult.CONSUME; + } + return InteractionResult.SUCCESS; + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/block/CompactTicker.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/block/CompactTicker.java new file mode 100644 index 00000000..f227e00e --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/block/CompactTicker.java @@ -0,0 +1,17 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.block; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; + +/** + * 紧凑方块实体的服务器 tick 入口。 + * + *

ECO 的各个主机基类都自己声明了 {@code tick(Level, BlockPos, BlockState)}, + * 但共同的祖先 {@code NEBlockEntity} 上没有这个方法,泛型擦除后没法统一调用, + * 因此用一个极小的接口把三家收敛起来。

+ */ +public interface CompactTicker { + + void tick(Level level, BlockPos pos, BlockState state); +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/calculator/CompactComputationClusterCalculator.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/calculator/CompactComputationClusterCalculator.java new file mode 100644 index 00000000..37713a79 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/calculator/CompactComputationClusterCalculator.java @@ -0,0 +1,27 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.calculator; + +import cn.dancingsnow.neoecoae.blocks.entity.NEBlockEntity; +import cn.dancingsnow.neoecoae.multiblock.calculator.NEComputationClusterCalculator; +import cn.dancingsnow.neoecoae.multiblock.cluster.NEComputationCluster; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; + +/** Prevents ECO's delayed multi-block scan from invalidating compact C9. */ +public final class CompactComputationClusterCalculator extends NEComputationClusterCalculator { + public CompactComputationClusterCalculator(NEBlockEntity target) { + super(target); + } + + @Override + public void calculateMultiblock(ServerLevel level, BlockPos pos) { + } + + @Override + public void updateMultiblockAfterNeighborUpdate(ServerLevel level, BlockPos pos, BlockPos changedPos) { + } + + @Override + public boolean checkMultiblockScale(BlockPos min, BlockPos max) { + return min.equals(max); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/calculator/CompactCraftingClusterCalculator.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/calculator/CompactCraftingClusterCalculator.java new file mode 100644 index 00000000..0a763fe7 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/calculator/CompactCraftingClusterCalculator.java @@ -0,0 +1,27 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.calculator; + +import cn.dancingsnow.neoecoae.blocks.entity.NEBlockEntity; +import cn.dancingsnow.neoecoae.multiblock.calculator.NECraftingClusterCalculator; +import cn.dancingsnow.neoecoae.multiblock.cluster.NECraftingCluster; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; + +/** Prevents ECO's delayed multi-block scan from invalidating compact F9. */ +public final class CompactCraftingClusterCalculator extends NECraftingClusterCalculator { + public CompactCraftingClusterCalculator(NEBlockEntity target) { + super(target); + } + + @Override + public void calculateMultiblock(ServerLevel level, BlockPos pos) { + } + + @Override + public void updateMultiblockAfterNeighborUpdate(ServerLevel level, BlockPos pos, BlockPos changedPos) { + } + + @Override + public boolean checkMultiblockScale(BlockPos min, BlockPos max) { + return min.equals(max); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/calculator/CompactHost.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/calculator/CompactHost.java new file mode 100644 index 00000000..199c85b9 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/calculator/CompactHost.java @@ -0,0 +1,14 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.calculator; + +import cn.dancingsnow.neoecoae.multiblock.cluster.NECluster; +import net.minecraft.server.level.ServerLevel; + +/** + * 由紧凑方块实体实现:在集群刚刚成型时,往集群里安装「影子组件」。 + * + *

安装完成后集群对 ECO 而言与一整套真实满配多方块没有区别。

+ */ +public interface CompactHost> { + + void installCompactComponents(C cluster, ServerLevel level); +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/calculator/CompactStorageClusterCalculator.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/calculator/CompactStorageClusterCalculator.java new file mode 100644 index 00000000..ae4d87ee --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/calculator/CompactStorageClusterCalculator.java @@ -0,0 +1,27 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.calculator; + +import cn.dancingsnow.neoecoae.blocks.entity.NEBlockEntity; +import cn.dancingsnow.neoecoae.multiblock.calculator.NEStorageClusterCalculator; +import cn.dancingsnow.neoecoae.multiblock.cluster.NEStorageCluster; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; + +/** Prevents ECO's delayed multi-block scan from invalidating compact L9. */ +public final class CompactStorageClusterCalculator extends NEStorageClusterCalculator { + public CompactStorageClusterCalculator(NEBlockEntity target) { + super(target); + } + + @Override + public void calculateMultiblock(ServerLevel level, BlockPos pos) { + } + + @Override + public void updateMultiblockAfterNeighborUpdate(ServerLevel level, BlockPos pos, BlockPos changedPos) { + } + + @Override + public boolean checkMultiblockScale(BlockPos min, BlockPos max) { + return min.equals(max); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/cluster/CompactComputationCluster.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/cluster/CompactComputationCluster.java new file mode 100644 index 00000000..847bfa14 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/cluster/CompactComputationCluster.java @@ -0,0 +1,68 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.cluster; + +import cn.dancingsnow.neoecoae.blocks.entity.NEBlockEntity; +import cn.dancingsnow.neoecoae.blocks.entity.computation.ECOComputationDriveBlockEntity; +import cn.dancingsnow.neoecoae.multiblock.cluster.NEComputationCluster; +import cn.dancingsnow.neoecoae.multiblock.cluster.NEComputationNetworkCluster; +import net.minecraft.core.BlockPos; +import org.jetbrains.annotations.Nullable; + +/** + * 紧凑 C9 的计算集群(8 台满配主机中的一台)。 + * + *

影子驱动器不存在于世界,没有可靠的「上/下列」位置信息,因此这里直接按登记顺序交替 + * 归入上下两列——ECO 的极限模式只要求「上下驱动器全部装满有效闪存晶阵」, + * 交替归列即可满足,并且仍然复用 ECO 自己的驱动器列表语义。

+ * + *

为什么要重写 {@code setNetworkCluster}:ECO 的 + * {@code NELogicalNetworkManager} 会在网格变化时把「它自己没见过的集群」的 + * {@code networkCluster} 一律置空({@code rebuildComputation} 里对集合内每个集群 + * 先 {@code setNetworkCluster(null)},只有 ≥2 台同网同频的真主机才会被重新赋值)。 + * 紧凑方块的 8 台逻辑主机是我们自己装配的、不进这个管理器,于是刚注入就被抹掉, + * 极限模式(线程/并行/存储上限)随之失效。这里只接受本类自己发起的注入。

+ */ +public class CompactComputationCluster extends NEComputationCluster { + + private boolean compactInjection; + + private int installedDrives; + + public CompactComputationCluster(BlockPos boundMin, BlockPos boundMax) { + super(boundMin, boundMax); + } + + /** 由紧凑 C9 主机注入它自己装配出来的逻辑网络(唯一允许的赋值来源)。 */ + public void injectCompactNetworkCluster(@Nullable NEComputationNetworkCluster networkCluster) { + compactInjection = true; + try { + super.setNetworkCluster(networkCluster); + } finally { + compactInjection = false; + } + } + + @Override + public void setNetworkCluster(@Nullable NEComputationNetworkCluster networkCluster) { + if (!compactInjection) { + // 忽略 ECO 逻辑网络管理器的赋值 / 置空,防止它把我们自己的 8 台网络抹掉。 + return; + } + super.setNetworkCluster(networkCluster); + } + + @Override + public void addBlockEntity(NEBlockEntity blockEntity) { + if (blockEntity instanceof ECOComputationDriveBlockEntity drive) { + installedDrives++; + if ((installedDrives & 1) == 1) { + getUpperDrives().add(drive); + } else { + drive.setLowerDrive(true); + getLowerDrives().add(drive); + } + blockEntities.add(drive); + return; + } + super.addBlockEntity(blockEntity); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/cluster/CompactCraftingCluster.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/cluster/CompactCraftingCluster.java new file mode 100644 index 00000000..d5d11a09 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/cluster/CompactCraftingCluster.java @@ -0,0 +1,45 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.cluster; + +import cn.dancingsnow.neoecoae.multiblock.cluster.NECraftingCluster; +import cn.dancingsnow.neoecoae.multiblock.cluster.NECraftingNetworkCluster; +import net.minecraft.core.BlockPos; +import org.jetbrains.annotations.Nullable; + +/** + * 紧凑 F9 的合成集群(8 台满长高能主机中的一台)。 + * + *

ECO 的合成集群登记逻辑只做 instanceof 分类,不读世界,因此无需改写; + * 影子工作核心、影子并行核心、影子样板总线直接走 ECO 原生分支。

+ * + *

为什么要重写 {@code setNetworkCluster}:同 {@link CompactComputationCluster}—— + * ECO 的 {@code NELogicalNetworkManager} 会把不进它管理的集群 + * 一律 {@code setNetworkCluster(null)},虚拟合成判定(8 台满长高能主机)就会失效。 + * 这里只接受本类自己发起的注入。

+ */ +public class CompactCraftingCluster extends NECraftingCluster { + + private boolean compactInjection; + + public CompactCraftingCluster(BlockPos boundMin, BlockPos boundMax) { + super(boundMin, boundMax); + } + + /** 由紧凑 F9 主机注入它自己装配出来的逻辑网络(唯一允许的赋值来源)。 */ + public void injectCompactNetworkCluster(@Nullable NECraftingNetworkCluster networkCluster) { + compactInjection = true; + try { + super.setNetworkCluster(networkCluster); + } finally { + compactInjection = false; + } + } + + @Override + public void setNetworkCluster(@Nullable NECraftingNetworkCluster networkCluster) { + if (!compactInjection) { + // 忽略 ECO 逻辑网络管理器的赋值 / 置空。 + return; + } + super.setNetworkCluster(networkCluster); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/cluster/CompactStorageCluster.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/cluster/CompactStorageCluster.java new file mode 100644 index 00000000..27f4040f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/cluster/CompactStorageCluster.java @@ -0,0 +1,17 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.cluster; + +import cn.dancingsnow.neoecoae.multiblock.cluster.NEStorageCluster; +import net.minecraft.core.BlockPos; + +/** + * 紧凑 L9 的存储集群:只有主机自己。 + * + *

ECO 的存储主机本身就是 {@code IStorageProvider},无限存储模式下容量来自 + * {@code ECOInfiniteStorageEngine} 而不是驱动器,所以这个集群不需要任何影子组件。

+ */ +public class CompactStorageCluster extends NEStorageCluster { + + public CompactStorageCluster(BlockPos boundMin, BlockPos boundMax) { + super(boundMin, boundMax); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/entity/CompactC9BlockEntity.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/entity/CompactC9BlockEntity.java new file mode 100644 index 00000000..4976d734 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/entity/CompactC9BlockEntity.java @@ -0,0 +1,431 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.entity; + +import appeng.api.networking.GridFlags; +import appeng.api.networking.IGrid; +import appeng.api.networking.IGridNode; +import appeng.api.networking.events.GridCraftingCpuChange; +import appeng.api.networking.ticking.IGridTickable; +import appeng.api.networking.ticking.TickRateModulation; +import appeng.api.networking.ticking.TickingRequest; +import appeng.api.orientation.BlockOrientation; +import cn.dancingsnow.neoecoae.api.ECOTier; +import cn.dancingsnow.neoecoae.blocks.entity.NEBlockEntity; +import cn.dancingsnow.neoecoae.blocks.entity.computation.ECOComputationSystemBlockEntity; +import cn.dancingsnow.neoecoae.multiblock.calculator.NEComputationClusterCalculator; +import cn.dancingsnow.neoecoae.multiblock.cluster.NEComputationCluster; +import cn.dancingsnow.neoecoae.multiblock.cluster.NEComputationNetworkCluster; +import com.lowdragmc.lowdraglib2.gui.factory.BlockUIMenuType; +import com.lowdragmc.lowdraglib2.gui.ui.ModularUI; +import com.lowdragmc.lowdraglib2.syncdata.rpc.RPCSender; +import com.sorrowmist.useless.compat.neoecoae.compact.NeoEcoCompactRegistry; +import com.sorrowmist.useless.compat.neoecoae.compact.block.CompactTicker; +import com.sorrowmist.useless.compat.neoecoae.compact.calculator.CompactHost; +import com.sorrowmist.useless.compat.neoecoae.compact.cluster.CompactComputationCluster; +import com.sorrowmist.useless.compat.neoecoae.compact.shadow.CompactInterfaceAttachment; +import com.sorrowmist.useless.compat.neoecoae.compact.shadow.ShadowComputationDriveBlockEntity; +import com.sorrowmist.useless.compat.neoecoae.compact.shadow.ShadowComputationHostBlockEntity; +import com.sorrowmist.useless.compat.neoecoae.compact.shadow.ShadowComputationParallelCoreBlockEntity; +import com.sorrowmist.useless.compat.neoecoae.compact.shadow.ShadowThreadingCoreBlockEntity; +import com.sorrowmist.useless.compat.neoecoae.compact.ui.CompactSingleBlockUI; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.RegistryAccess; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; + +/** + * 无用型紧凑 C9 —— 等价于「八联满配 C9」:8 台满长 C9 主机,每台装高能网络交换模块、 + * 上下晶阵驱动器全部装满 CE9 闪存晶阵。 + * + *

本类不做任何合成逻辑。它只做一件事:把 8 个 {@link NEComputationCluster}(第 1 个是真实主机 + * 自己,另外 7 个是只有影子组件的「影子主机」)注册进同一个 ECO + * {@link NEComputationNetworkCluster}。于是 ECO 自己的 + * {@code NEComputationNetworkCluster#isEndgameEligible()} 会自然判定为极限模式, + * 线程 / 并行 / CPU 存储三项全部按 ECO 的规则拿到 INT32 / INT64 上限。

+ */ +public class CompactC9BlockEntity extends ECOComputationSystemBlockEntity + implements CompactHost, CompactTicker, IGridTickable { + + + /** 与 ECO 自己的极限模式判定完全一致:恰好 8 台。 */ + private static final int HOSTS = 8; + /** + * 满长建造的列数。ECO 的结构定义里 {@code expandMax = computationSystemMaxLength - 4}, + * 改成配置值也不会让紧凑方块退化,因为这里每次都按当前上限重新装配。 + */ + private int buildLength() { + return Math.max(1, getMaxBuildLength()); + } + + private final List fleet = new ArrayList<>(); + /** All logical CPU cores, including the first host's cores, live only in this list. */ + private final List shadowThreadingCores = new ArrayList<>(); + private static final String SHADOW_THREADING_CORE_KEY = "useless_compact_c9_threading_cores"; + @Nullable + private ListTag pendingThreadingCoreData; + @Nullable + private NEComputationNetworkCluster compactNetworkCluster; + private boolean pendingCpuAnnounce; + private int cpuAnnounceAttempts; + private long lastCompactTick = Long.MIN_VALUE; + + /** 通讯接口界面(影子接口)的全部接线;普通右键仍然走主机自己的面板。 */ + private final CompactInterfaceAttachment compactInterface = + new CompactInterfaceAttachment<>(this, NEComputationClusterCalculator::new); + + public CompactC9BlockEntity(BlockEntityType type, BlockPos pos, BlockState blockState) { + super(type, pos, blockState, ECOTier.L9); + getMainNode().setFlags(GridFlags.REQUIRE_CHANNEL); + // 双保险:世界 ticker 与 AE2 网格 ticker 都会驱动本机逻辑(同一 tick 去重)。 + getMainNode().addService(IGridTickable.class, this); + } + + // ------------------------------------------------------------- 成形生命周期 + // + // ECO 的主机基类在构造函数里把 calculator 硬编码成多方块计算器(字段 final,换不掉), + // 所以紧凑方块不依赖计算器,而是自己接管成形:onReady 之后建出只含自己的集群, + // 并掐掉 updateMultiBlock / rebuildMultiblock,否则邻居一变 ECO 的计算器就会拆掉集群。 + + @Override + public void onReady() { + super.onReady(); + formCompactStructure(); + onGridConnectableSidesChanged(); + } + + @Override + public void updateMultiBlock(BlockPos changedPos) { + if (cluster == null || cluster.isDestroyed()) { + formCompactStructure(); + } + } + + @Override + public void rebuildMultiblock() { + if (cluster == null || cluster.isDestroyed()) { + formCompactStructure(); + } + } + + private void formCompactStructure() { + if (!(level instanceof ServerLevel serverLevel)) { + return; + } + if (cluster != null && !cluster.isDestroyed() && formed) { + return; + } + CompactComputationCluster compact = new CompactComputationCluster(worldPosition, worldPosition); + compact.addBlockEntity(this); + installCompactComponents(compact, serverLevel); + linkAll(compact); + compact.updateFormed(true); + compact.updateStatus(true); + } + + @Override + public boolean hasNormalNetworkSwitch() { + return false; + } + + @Override + public boolean hasHighEnergyNetworkSwitch() { + return true; + } + + @Override + public int getSelectedBuildLength() { + return getMaxBuildLength(); + } + + @Override + public void setSelectedBuildLength(int length) { + // 紧凑主机永远是满长,忽略外部改写。 + } + + @Override + public Set getGridConnectableSides(BlockOrientation orientation) { + return formed ? EnumSet.allOf(Direction.class) : EnumSet.noneOf(Direction.class); + } + + @Override + public void tick(Level level, BlockPos pos, BlockState state) { + super.tick(level, pos, state); + if (level instanceof ServerLevel serverLevel) { + compactServerTick(serverLevel); + } + } + + @Override + public TickingRequest getTickingRequest(IGridNode node) { + return new TickingRequest(1, 10, false); + } + + /** AE2 网格 ticker 通道:世界 ticker 万一没跑,CPU 登记在这里兜底。 */ + @Override + public TickRateModulation tickingRequest(IGridNode node, int ticksSinceLastCall) { + if (level instanceof ServerLevel serverLevel) { + compactServerTick(serverLevel); + } + return TickRateModulation.IDLE; + } + + /** 两个 tick 来源共用;同一个游戏 tick 只处理一次。 */ + private void compactServerTick(ServerLevel level) { + long now = level.getGameTime(); + if (now == lastCompactTick) { + return; + } + lastCompactTick = now; + compactInterface.tick(); + if (!pendingCpuAnnounce) { + return; + } + // 成型时节点可能尚未挂载到网格(AE2 的节点入网需等待网格 tick), + // 因此单次失败不可放弃,需重试至入网完成。 + if (cpuAnnounceAttempts++ > 100) { + pendingCpuAnnounce = false; + return; + } + pendingCpuAnnounce = !announceCpuToGrid(); + } + + /** + * 主机节点入网/换网后重新向 CraftingService 登记 CPU。 + * + *

AE2 只在收到 {@code GridCraftingCpuChange} 时才会刷新它的 CPU 集群集合 + * ({@code CraftingService.updateList}),而 ECO 是靠在那个刷新点里 + * {@code grid.getMachines(ECOComputationSystemBlockEntity.class)} 找到主机的。

+ */ + @Override + protected void onMainNodeGridChanged() { + super.onMainNodeGridChanged(); + pendingCpuAnnounce = true; + cpuAnnounceAttempts = 0; + } + + // ------------------------------------------------------------- 集群装配 + + @Override + public void installCompactComponents(NEComputationCluster cluster, ServerLevel level) { + disposeFleet(); + fleet.clear(); + shadowThreadingCores.clear(); + + // 成员 #1:真实主机自己(它的影子组件补满这台主机的硬件)。 + installHostHardware(cluster, level); + // 集群一定是我们自己创建的 CompactComputationCluster(成形逻辑在 CompactL9/C9 的 formCompactStructure 里)。 + fleet.add((CompactComputationCluster) cluster); + + // 成员 #2..#8:影子主机。 + for (int index = 1; index < HOSTS; index++) { + CompactComputationCluster shadow = buildShadowHost(level); + installHostHardware(shadow, level); + shadow.updateFormed(true); + fleet.add(shadow); + } + + restoreThreadingCoreData(level.registryAccess()); + for (CompactComputationCluster member : fleet) { + linkAll(member); + } + + NEComputationNetworkCluster network = new NEComputationNetworkCluster(); + List members = new ArrayList<>(fleet); + network.configure(members); + for (CompactComputationCluster member : fleet) { + // 走注入入口:ECO 的逻辑网络管理器会把它不认识的集群置空,普通 setter 会被我们忽略。 + member.injectCompactNetworkCluster(network); + } + + this.compactNetworkCluster = network; + this.pendingCpuAnnounce = true; + compactInterface.attach(level, cluster, getMainNode()); + } + + private CompactComputationCluster buildShadowHost(ServerLevel level) { + CompactComputationCluster shadow = new CompactComputationCluster(worldPosition, worldPosition); + ShadowComputationHostBlockEntity host = new ShadowComputationHostBlockEntity( + NeoEcoCompactRegistry.SHADOW_COMPUTATION_HOST.get(), + worldPosition, + NeoEcoCompactRegistry.placeholderState(NeoEcoCompactRegistry.COMPACT_C9)); + host.setLevel(level); + shadow.addBlockEntity(host); + return shadow; + } + + private void installHostHardware(NEComputationCluster cluster, ServerLevel level) { + for (int index = 0; index < buildLength(); index++) { + ShadowThreadingCoreBlockEntity core = new ShadowThreadingCoreBlockEntity( + NeoEcoCompactRegistry.SHADOW_THREADING_CORE.get(), + worldPosition, + NeoEcoCompactRegistry.placeholderState(NeoEcoCompactRegistry.COMPACT_C9)); + core.setLevel(level); + cluster.addBlockEntity(core); + shadowThreadingCores.add(core); + } + + ItemStack cell = NeoEcoCompactRegistry.neoEcoStack("eco_computation_cell_l9"); + for (int index = 0; index < buildLength() * 2; index++) { + ShadowComputationDriveBlockEntity drive = new ShadowComputationDriveBlockEntity( + NeoEcoCompactRegistry.SHADOW_COMPUTATION_DRIVE.get(), + worldPosition, + NeoEcoCompactRegistry.placeholderState(NeoEcoCompactRegistry.COMPACT_C9)); + drive.setLevel(level); + drive.setCellStack(cell.copy()); + cluster.addBlockEntity(drive); + } + + for (int index = 0; index < buildLength() * 2; index++) { + ShadowComputationParallelCoreBlockEntity core = new ShadowComputationParallelCoreBlockEntity( + NeoEcoCompactRegistry.SHADOW_COMPUTATION_PARALLEL_CORE.get(), + worldPosition, + NeoEcoCompactRegistry.placeholderState(NeoEcoCompactRegistry.COMPACT_C9)); + core.setLevel(level); + cluster.addBlockEntity(core); + } + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private void linkAll(NEComputationCluster cluster) { + cluster.getBlockEntities().forEachRemaining(blockEntity -> { + if (blockEntity instanceof NEBlockEntity neBlockEntity) { + neBlockEntity.updateCluster(cluster); + } + }); + } + + private void disposeFleet() { + if (level instanceof ServerLevel serverLevel && !shadowThreadingCores.isEmpty()) { + pendingThreadingCoreData = captureThreadingCoreData(serverLevel.registryAccess()); + } + compactInterface.release(); + for (CompactComputationCluster member : fleet) { + if (member != cluster && !member.isDestroyed()) { + member.injectCompactNetworkCluster(null); + member.destroy(); + } + } + } + + private ListTag captureThreadingCoreData(HolderLookup.Provider registries) { + ListTag saved = new ListTag(); + for (ShadowThreadingCoreBlockEntity core : shadowThreadingCores) { + CompoundTag coreData = new CompoundTag(); + core.saveAdditional(coreData); + saved.add(coreData); + } + return saved; + } + + private void restoreThreadingCoreData(HolderLookup.Provider registries) { + if (pendingThreadingCoreData == null) { + return; + } + int count = Math.min(pendingThreadingCoreData.size(), shadowThreadingCores.size()); + for (int index = 0; index < count; index++) { + shadowThreadingCores.get(index).loadTag(pendingThreadingCoreData.getCompound(index), registries); + } + pendingThreadingCoreData = null; + } + + /** + * ECO 的 {@code CraftingService} 只在 {@code updateCPUClusters} 里扫描主机, + * 所以成型后主动发一次网格事件,让 CPU 立刻出现在合成终端里。 + */ + private boolean announceCpuToGrid() { + IGridNode node = getActionableNode(); + if (node == null) { + return false; + } + IGrid grid = node.getGrid(); + if (grid == null) { + return false; + } + grid.postEvent(new GridCraftingCpuChange(node)); + return true; + } + + @Override + public void onChunkUnloaded() { + disposeFleet(); + fleet.clear(); + shadowThreadingCores.clear(); + compactNetworkCluster = null; + compactInterface.dispose(); + super.onChunkUnloaded(); + } + + /** 供调试/UI 读取当前装配出来的逻辑网络。 */ + @Nullable + public NEComputationNetworkCluster compactNetworkCluster() { + return compactNetworkCluster; + } + + // ------------------------------------------------ 通讯接口界面(影子接口)入口 + + @Override + public ModularUI createUI(BlockUIMenuType.BlockUIHolder holder) { + ModularUI interfaceUI = compactInterface.createUI(holder); + return interfaceUI == null ? CompactSingleBlockUI.create(holder.player, + net.minecraft.network.chat.Component.translatable("gui.useless_mod.compact.c9.title"), + net.minecraft.network.chat.Component.translatable("gui.useless_mod.compact.c9.overview"), + net.minecraft.network.chat.Component.translatable("gui.useless_mod.compact.c9.hardware")) + : interfaceUI; + } + + @Override + public void handleRPCPacket(RPCSender sender, byte[] data) { + if (!compactInterface.forwardRpc(sender, data)) { + super.handleRPCPacket(sender, data); + } + } + + @Override + public void handleSyncPacket(RegistryAccess access, BitSet changed, byte[] data, CompoundTag extra) { + if (!compactInterface.forwardSync(access, changed, data, extra)) { + super.handleSyncPacket(access, changed, data, extra); + } + } + + @Override + public void saveAdditional(CompoundTag data) { + HolderLookup.Provider registries = level == null ? net.minecraft.core.RegistryAccess.EMPTY : level.registryAccess(); + super.saveAdditional(data); + ListTag threadingData = shadowThreadingCores.isEmpty() + ? pendingThreadingCoreData : captureThreadingCoreData(registries); + if (threadingData != null && !threadingData.isEmpty()) { + data.put(SHADOW_THREADING_CORE_KEY, threadingData.copy()); + } + compactInterface.save(registries, data); + } + + @Override + public void loadTag(CompoundTag data) { + HolderLookup.Provider registries = level == null ? net.minecraft.core.RegistryAccess.EMPTY : level.registryAccess(); + super.loadTag(data); + if (data.contains(SHADOW_THREADING_CORE_KEY, net.minecraft.nbt.Tag.TAG_LIST)) { + pendingThreadingCoreData = data.getList(SHADOW_THREADING_CORE_KEY, + net.minecraft.nbt.Tag.TAG_COMPOUND).copy(); + } + compactInterface.load(registries, data); + } + + @Override + public void setRemoved() { + compactInterface.dispose(); + super.setRemoved(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/entity/CompactF9BlockEntity.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/entity/CompactF9BlockEntity.java new file mode 100644 index 00000000..3a2ba281 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/entity/CompactF9BlockEntity.java @@ -0,0 +1,835 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.entity; + +import appeng.api.networking.GridFlags; +import appeng.api.networking.IGridNode; +import appeng.api.crafting.IPatternDetails; +import appeng.api.networking.crafting.ICraftingProvider; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.KeyCounter; +import appeng.api.networking.ticking.TickRateModulation; +import appeng.api.orientation.BlockOrientation; +import cn.dancingsnow.neoecoae.api.ECOTier; +import cn.dancingsnow.neoecoae.api.ECOPatternInsertionResult; +import cn.dancingsnow.neoecoae.api.me.provider.ECOBatchDispatchContext; +import cn.dancingsnow.neoecoae.api.me.provider.ECOFastPathDispatchProvider; +import cn.dancingsnow.neoecoae.blocks.entity.NEBlockEntity; +import cn.dancingsnow.neoecoae.blocks.entity.crafting.ECOCraftingPatternBusBlockEntity; +import com.sorrowmist.useless.compat.neoecoae.compact.shadow.ShadowPatternBusBlockEntity; +import cn.dancingsnow.neoecoae.blocks.entity.crafting.ECOCraftingSystemBlockEntity; +import cn.dancingsnow.neoecoae.blocks.entity.crafting.ECOCraftingWorkerBlockEntity; +import cn.dancingsnow.neoecoae.multiblock.calculator.NECraftingClusterCalculator; +import cn.dancingsnow.neoecoae.multiblock.cluster.NECraftingCluster; +import cn.dancingsnow.neoecoae.multiblock.cluster.NECraftingNetworkCluster; +import com.lowdragmc.lowdraglib2.gui.factory.BlockUIMenuType; +import com.lowdragmc.lowdraglib2.gui.ui.ModularUI; +import com.lowdragmc.lowdraglib2.syncdata.rpc.RPCSender; +import com.sorrowmist.useless.compat.neoecoae.compact.NeoEcoCompactRegistry; +import com.sorrowmist.useless.compat.neoecoae.compact.block.CompactTicker; +import com.sorrowmist.useless.compat.neoecoae.compact.calculator.CompactHost; +import com.sorrowmist.useless.compat.neoecoae.compact.cluster.CompactCraftingCluster; +import com.sorrowmist.useless.compat.neoecoae.compact.exposed.ExposedPatternBusBlockEntity; +import com.sorrowmist.useless.compat.neoecoae.compact.provider.CompactPatternProvider; +import com.sorrowmist.useless.compat.neoecoae.compact.shadow.CompactInterfaceAttachment; +import com.sorrowmist.useless.compat.neoecoae.compact.shadow.ShadowCraftingHostBlockEntity; +import com.sorrowmist.useless.compat.neoecoae.compact.shadow.ShadowCraftingParallelCoreBlockEntity; +import com.sorrowmist.useless.compat.neoecoae.compact.shadow.ShadowCraftingWorkerBlockEntity; +import com.sorrowmist.useless.compat.neoecoae.compact.shadow.ShadowFluidInputHatchBlockEntity; +import com.sorrowmist.useless.compat.neoecoae.compact.shadow.ShadowFluidOutputHatchBlockEntity; +import com.sorrowmist.useless.compat.neoecoae.compact.shadow.ShadowNodeLink; +import com.sorrowmist.useless.compat.neoecoae.compact.ui.CompactSingleBlockUI; +import com.sorrowmist.useless.core.component.ExternalInventoryKind; +import com.sorrowmist.useless.core.component.ExternalInventoryReference; +import com.sorrowmist.useless.world.inventory.ExternalInventoryStore; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.RegistryAccess; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraftforge.fluids.capability.IFluidHandler; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; + +/** + * 无用型紧凑 F9 —— 等价于「八联满配 F9」:8 台满长 F9 主机,每台装高能网络交换模块, + * 每台 11 条物理 FX 执行通道(共 88 条)。 + * + *

本类不做任何合成逻辑。它只做一件事:装配出 8 个 {@link NECraftingCluster} + * (第 1 个是真实主机自身,另外 7 个仅带影子组件)并注册到同一个 ECO + * {@link NECraftingNetworkCluster}。于是 ECO 自己的 + * {@code CraftingCapabilitySnapshot#isVirtualTopologyEligible()} 自然成立 → + * 进入虚拟合成模式:无限批量、任务在首个 tick 完成。

+ * + *

样板仍然由 ECO 原生的智能样板总线实例解析与派单:所有影子总线聚合成一个 + * {@link CompactPatternProvider} 挂在本机网格节点上,AE2 / ECO 看到的样板提供者 + * 依旧是 ECO 的实现,只是从 176 条总线合并成了一个入口。这 176 条总线对外的「容器身份」 + * 则由一条带真实网格节点的 {@link ExposedPatternBusBlockEntity} 承担,样板访问终端与 + * 通讯接口界面看到的都是它。

+ * + *

样板数据不放在本机 NBT 里:176 条影子总线的完整持久化数据是 MB 级的, + * 而方块实体自身的 NBT 既要跟着区块存盘、又会被 {@code getUpdateTag} 整包发给客户端, + * 装满样板后将超出区块包容量上限。因此本机仅保留一个 16 字节的 UUID 引用,实际内容存放于 + * {@link ExternalInventoryStore} 里那份 UUID 键控的外置存档中(见 {@link #PATTERN_BUS_KEY})。

+ */ +public class CompactF9BlockEntity extends ECOCraftingSystemBlockEntity + implements CompactHost, CompactTicker, ICraftingProvider, ECOFastPathDispatchProvider { + + + /** 与 ECO 自己的虚拟合成判定一致:恰好 8 台。 */ + private static final int HOSTS = 8; + /** + * 外置存储载荷里那段样板总线的键名。 + * + *

该键在旧存档中是方块实体自身的标签名(历史实现将 176 条总线的完整数据直接写入主机 NBT), + * 现仅作为「迁移来源」读取一次,不再写回。

+ */ + static final String PATTERN_BUS_KEY = "useless_compact_pattern_buses"; + private static final String FLUID_INPUT_KEY = "useless_compact_f9_fluid_input"; + private static final String FLUID_OUTPUT_KEY = "useless_compact_f9_fluid_output"; + private static final String SHADOW_HOST_KEY = "useless_compact_f9_shadow_hosts"; + private static final String SHADOW_WORKER_KEY = "useless_compact_f9_shadow_workers"; + /** + * 满长建造的列数。ECO 的结构定义里 {@code expandMax = craftingSystemMaxLength - 4}, + * 而虚拟合成判定要求「实际 FX 通道数 == 该上限」,因此此处每次都按当前上限取值, + * 否则玩家修改配置后虚拟模式会静默失效。 + */ + private int buildLength() { + return Math.max(1, getMaxBuildLength()); + } + + private final CompactPatternProvider patternProvider = new CompactPatternProvider(); + private final List fleet = new ArrayList<>(); + private final List shadowWorkers = new ArrayList<>(); + private final List shadowWorkerLinks = new ArrayList<>(); + private final List shadowBuses = new ArrayList<>(); + /** 八台主机共享的冷却液输入/输出池;每台集群都有自己的仓实体引用这两个池。 */ + private final ShadowFluidInputHatchBlockEntity fluidInputHatch; + private final ShadowFluidOutputHatchBlockEntity fluidOutputHatch; + private final IFluidHandler fluidHandler; + @Nullable + private NECraftingNetworkCluster compactNetworkCluster; + private long lastCompactTick = Long.MIN_VALUE; + private int lastPatternContentRevision = Integer.MIN_VALUE; + private long lastPatternAuxiliaryRevision = Long.MIN_VALUE; + + /** 通讯接口界面(影子接口)的全部接线;普通右键仍然走主机自己的面板。 */ + private final CompactInterfaceAttachment compactInterface = + new CompactInterfaceAttachment<>(this, NECraftingClusterCalculator::new); + + /** 对外代表 176 条影子总线的聚合总线;它的节点同样靠宿主节点显式接上网格。 */ + @Nullable + private ExposedPatternBusBlockEntity exposedBus; + private final ShadowNodeLink exposedBusLink = new ShadowNodeLink(this); + /** + * 从老存档 NBT 里读出来的样板数据,只在「装配 → 恢复」之间中转一次。 + * 恢复完就置空,之后一律走外置存储;这份数据不会再被写回方块实体 NBT。 + */ + @Nullable + private ListTag pendingPatternBusData; + @Nullable + private ListTag pendingShadowHostData; + @Nullable + private ListTag pendingShadowWorkerData; + /** 本机的样板数据落点:UUID 指向外置存档,本身只占 16 字节。 */ + @Nullable + private ExternalInventoryReference patternStoreReference; + /** + * 样板内容自上次写入外置存储后有没有变过。 + * + *

176 条总线的完整数据是 MB 级的,绝不能每次区块存档都重序列化、重写一遍; + * 只有内容版本真的变了(玩家插样板、终端里改动)才写。见 {@link #compactServerTick}。

+ */ + private boolean patternStoreDirty; + + public CompactF9BlockEntity(BlockEntityType type, BlockPos pos, BlockState blockState) { + super(type, pos, blockState, ECOTier.L9); + fluidInputHatch = new ShadowFluidInputHatchBlockEntity( + NeoEcoCompactRegistry.SHADOW_FLUID_INPUT_HATCH.get(), pos, blockState); + fluidOutputHatch = new ShadowFluidOutputHatchBlockEntity( + NeoEcoCompactRegistry.SHADOW_FLUID_OUTPUT_HATCH.get(), pos, blockState); + fluidInputHatch.tank = CompactF9FluidHandler.createFleetTank(HOSTS, this::setChanged); + fluidOutputHatch.tank = CompactF9FluidHandler.createFleetTank(HOSTS, this::setChanged); + fluidHandler = new CompactF9FluidHandler(fluidInputHatch.tank, fluidOutputHatch.tank); + getMainNode().setFlags(GridFlags.REQUIRE_CHANNEL); + // 关键:网格服务必须挂在「本机自己」身上,而不是一个独立对象。 + // ECO 的样板总线就是这么做的(总线 BE 既实现 ICraftingProvider 又实现 IECOPatternStorage), + // AE2/ECO 的各种枚举都是「拿节点 → 取服务 → 和宿主比对」,独立对象会让它们认不出来。 + getMainNode() + .addService(ICraftingProvider.class, this); + } + + // ------------------------------------------------------------- 成形生命周期 + // + // ECO 的主机基类在构造函数里把 calculator 硬编码成多方块计算器(字段 final,换不掉), + // 所以紧凑方块不依赖计算器,而是自己接管成形:onReady 之后建出只含自己的集群, + // 并掐掉 updateMultiBlock / rebuildMultiblock,否则邻居一变 ECO 的计算器就会拆掉集群。 + + @Override + public void onReady() { + super.onReady(); + formCompactStructure(); + onGridConnectableSidesChanged(); + } + + @Override + public void updateMultiBlock(BlockPos changedPos) { + if (cluster == null || cluster.isDestroyed()) { + formCompactStructure(); + } + } + + @Override + public void rebuildMultiblock() { + if (cluster == null || cluster.isDestroyed()) { + formCompactStructure(); + } + } + + private void formCompactStructure() { + if (!(level instanceof ServerLevel serverLevel)) { + return; + } + if (cluster != null && !cluster.isDestroyed() && formed) { + return; + } + CompactCraftingCluster compact = new CompactCraftingCluster(worldPosition, worldPosition); + compact.addBlockEntity(this); + installCompactComponents(compact, serverLevel); + linkAll(compact); + compact.updateFormed(true); + compact.updateStatus(true); + } + + @Override + public boolean hasHighEnergyNetworkSwitch() { + return true; + } + + @Override + public int getSelectedBuildLength() { + return getMaxBuildLength(); + } + + @Override + public void setSelectedBuildLength(int length) { + // 紧凑主机永远是满长,忽略外部改写。 + } + + @Override + public Set getGridConnectableSides(BlockOrientation orientation) { + return formed ? EnumSet.allOf(Direction.class) : EnumSet.noneOf(Direction.class); + } + + @Override + public void tick(Level level, BlockPos pos, BlockState state) { + super.tick(level, pos, state); + if (level instanceof ServerLevel serverLevel) { + compactServerTick(serverLevel); + } + } + + /** + * AE2 网格 ticker 通道(ECO 的合成主机本身就是 {@code IGridTickable})。 + * + *

先跑 ECO 自己的合成 tick,再驱动我们的影子工作核心;两个 tick 来源靠 + * {@link #lastCompactTick} 去重,保证每条 FX 通道每个游戏 tick 只推进一次。

+ */ + @Override + public TickRateModulation tickingRequest(IGridNode node, int ticksSinceLastCall) { + TickRateModulation rate = super.tickingRequest(node, ticksSinceLastCall); + if (level instanceof ServerLevel serverLevel) { + compactServerTick(serverLevel); + } + return rate; + } + + /** 两个 tick 来源共用;同一个游戏 tick 只处理一次。 */ + private void compactServerTick(ServerLevel level) { + long now = level.getGameTime(); + if (now == lastCompactTick) { + return; + } + lastCompactTick = now; + exposedBusLink.tick(); + if (exposedBus != null) { + exposedBus.tick(); + int revision = exposedBus.getPatternContentRevision(); + long auxiliaryRevision = exposedBus.getAuxiliaryRevision(); + if (revision != lastPatternContentRevision || auxiliaryRevision != lastPatternAuxiliaryRevision) { + lastPatternContentRevision = revision; + lastPatternAuxiliaryRevision = auxiliaryRevision; + // 内容变了才需要回写外置存储;挂起一次写,等区块存盘/卸载时真正落盘。 + patternStoreDirty = true; + setChanged(); + } + } + compactInterface.tick(); + // 影子主机没有独立网格 ticker,仍由 ECO 原生冷却逻辑补充各自的内部缓冲。 + for (int index = 1; index < fleet.size(); index++) { + fleet.get(index).getController().tickingRequest(null, 1); + } + tickShadowWorkers(); + } + + /** 主机节点入网/换网后重新把合并后的样板提供者登记给 AE2(它只在收到刷新通知时才会重读)。 */ + @Override + protected void onMainNodeGridChanged() { + super.onMainNodeGridChanged(); + refreshPatternProvider(); + } + + /** AE2 的刷新入口不做空判断,节点不在网格上时不能调。 */ + private boolean refreshPatternProvider() { + IGridNode node = getActionableNode(); + if (node == null || node.getGrid() == null || patternProvider.isEmpty()) { + return false; + } + ICraftingProvider.requestUpdate(getMainNode()); + return true; + } + + /** + * 影子工作核心的节点接入宿主网格但不使用自身 ticker;这里手动驱动 ECO 原生的 + * {@code tickingRequest},执行与回收全部走 ECO 自己的线程实现。 + */ + private void tickShadowWorkers() { + for (ShadowNodeLink link : shadowWorkerLinks) { + link.tick(); + } + for (ECOCraftingWorkerBlockEntity worker : shadowWorkers) { + worker.tickingRequest(null, 1); + } + } + + // ------------------------------------------------------------- 集群装配 + + @Override + public void installCompactComponents(NECraftingCluster cluster, ServerLevel level) { + disposeFleet(); + resetCollections(); + // 先拿到外置存储的引用,后面的恢复才有地方读。 + ensurePatternStore(level); + + fluidInputHatch.setLevel(level); + fluidOutputHatch.setLevel(level); + cluster.addBlockEntity(fluidInputHatch); + cluster.addBlockEntity(fluidOutputHatch); + installHostHardware(cluster, level); + // 集群一定是我们自己创建的 CompactCraftingCluster。 + fleet.add((CompactCraftingCluster) cluster); + + for (int index = 1; index < HOSTS; index++) { + CompactCraftingCluster shadow = buildShadowHost(level); + installHostHardware(shadow, level); + shadow.updateFormed(true); + fleet.add(shadow); + } + + restoreShadowHostData(level.registryAccess()); + restoreShadowWorkerData(level.registryAccess()); + for (CompactCraftingCluster member : fleet) { + linkAll(member); + } + NECraftingNetworkCluster network = new NECraftingNetworkCluster(); + List members = new ArrayList<>(fleet); + network.configure(members); + for (CompactCraftingCluster member : fleet) { + // 走注入入口:ECO 的逻辑网络管理器会把它不认识的集群置空,普通 setter 会被我们忽略。 + member.injectCompactNetworkCluster(network); + } + + this.compactNetworkCluster = network; + + patternProvider.bind(shadowBuses); + restorePatternBusData(level); + for (ECOCraftingPatternBusBlockEntity bus : shadowBuses) { + // 影子总线不会走 onReady,这里用 ECO 自己的刷新入口把样板索引建起来。 + bus.flushScheduledPatternDetails(); + } + installExposedBus(level); + // 刚恢复的内容不计为「变化」:将版本基准对齐到当前值,避免存档载入时立即触发一次无谓写入。 + // (是否确实需要写入外置存储由 restorePatternBusData 判断,它可区分「迁移」与「数据本就在存储中」两种情况。) + if (exposedBus != null) { + lastPatternContentRevision = exposedBus.getPatternContentRevision(); + lastPatternAuxiliaryRevision = exposedBus.getAuxiliaryRevision(); + } + ICraftingProvider.requestUpdate(getMainNode()); + compactInterface.attach(level, cluster, getMainNode()); + + } + + private CompactCraftingCluster buildShadowHost(ServerLevel level) { + CompactCraftingCluster shadow = new CompactCraftingCluster(worldPosition, worldPosition); + ShadowCraftingHostBlockEntity host = new ShadowCraftingHostBlockEntity( + NeoEcoCompactRegistry.SHADOW_CRAFTING_HOST.get(), + worldPosition, + NeoEcoCompactRegistry.placeholderState(NeoEcoCompactRegistry.COMPACT_F9)); + host.setLevel(level); + shadow.addBlockEntity(host); + ShadowFluidInputHatchBlockEntity input = new ShadowFluidInputHatchBlockEntity( + NeoEcoCompactRegistry.SHADOW_FLUID_INPUT_HATCH.get(), worldPosition, getBlockState()); + ShadowFluidOutputHatchBlockEntity output = new ShadowFluidOutputHatchBlockEntity( + NeoEcoCompactRegistry.SHADOW_FLUID_OUTPUT_HATCH.get(), worldPosition, getBlockState()); + input.tank = fluidInputHatch.tank; + output.tank = fluidOutputHatch.tank; + input.setLevel(level); + output.setLevel(level); + shadow.addBlockEntity(input); + shadow.addBlockEntity(output); + return shadow; + } + + private ListTag captureShadowHostData(HolderLookup.Provider registries) { + ListTag saved = new ListTag(); + for (int index = 1; index < fleet.size(); index++) { + ECOCraftingSystemBlockEntity host = fleet.get(index).getController(); + host.getRootStorage().requireInit(); + CompoundTag hostData = new CompoundTag(); + host.saveManagedPersistentData(registries, hostData, false); + saved.add(hostData); + } + return saved; + } + + private ListTag captureShadowWorkerData(HolderLookup.Provider registries) { + ListTag saved = new ListTag(); + for (ECOCraftingWorkerBlockEntity worker : shadowWorkers) { + CompoundTag workerData = new CompoundTag(); + worker.saveAdditional(workerData); + saved.add(workerData); + } + return saved; + } + + private void restoreShadowHostData(HolderLookup.Provider registries) { + if (pendingShadowHostData == null) { + return; + } + for (int index = 1; index < fleet.size() && index <= pendingShadowHostData.size(); index++) { + ECOCraftingSystemBlockEntity host = fleet.get(index).getController(); + host.getRootStorage().requireInit(); + host.loadManagedPersistentData(registries, pendingShadowHostData.getCompound(index - 1)); + } + pendingShadowHostData = null; + } + + private void restoreShadowWorkerData(HolderLookup.Provider registries) { + if (pendingShadowWorkerData == null) { + return; + } + int count = Math.min(pendingShadowWorkerData.size(), shadowWorkers.size()); + for (int index = 0; index < count; index++) { + shadowWorkers.get(index).loadTag(pendingShadowWorkerData.getCompound(index), registries); + } + pendingShadowWorkerData = null; + } + + private void installHostHardware(NECraftingCluster cluster, ServerLevel level) { + for (int index = 0; index < buildLength(); index++) { + ShadowCraftingWorkerBlockEntity worker = new ShadowCraftingWorkerBlockEntity( + NeoEcoCompactRegistry.SHADOW_CRAFTING_WORKER.get(), + worldPosition, + NeoEcoCompactRegistry.placeholderState(NeoEcoCompactRegistry.COMPACT_F9)); + worker.setLevel(level); + cluster.addBlockEntity(worker); + shadowWorkers.add(worker); + ShadowNodeLink workerLink = new ShadowNodeLink(this); + workerLink.attach(worker.getMainNode(), level, getMainNode()); + shadowWorkerLinks.add(workerLink); + } + + for (int index = 0; index < buildLength() * 2; index++) { + ShadowCraftingParallelCoreBlockEntity core = new ShadowCraftingParallelCoreBlockEntity( + NeoEcoCompactRegistry.SHADOW_CRAFTING_PARALLEL_CORE.get(), + worldPosition, + NeoEcoCompactRegistry.placeholderState(NeoEcoCompactRegistry.COMPACT_F9)); + core.setLevel(level); + cluster.addBlockEntity(core); + } + + for (int index = 0; index < buildLength() * 2; index++) { + ShadowPatternBusBlockEntity bus = new ShadowPatternBusBlockEntity( + NeoEcoCompactRegistry.SHADOW_PATTERN_BUS.get(), + worldPosition, + NeoEcoCompactRegistry.placeholderState(NeoEcoCompactRegistry.COMPACT_F9)); + bus.setLevel(level); + cluster.addBlockEntity(bus); + shadowBuses.add(bus); + } + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private void linkAll(NECraftingCluster cluster) { + cluster.getBlockEntities().forEachRemaining(blockEntity -> { + if (blockEntity instanceof NEBlockEntity neBlockEntity) { + neBlockEntity.updateCluster(cluster); + } + }); + } + + /** 建出对外代表 176 条影子总线的聚合总线,并把它接上宿主节点。 */ + private void installExposedBus(ServerLevel level) { + ExposedPatternBusBlockEntity bus = new ExposedPatternBusBlockEntity( + NeoEcoCompactRegistry.COMPACT_EXPOSED_PATTERN_BUS.get(), + worldPosition, + NeoEcoCompactRegistry.placeholderState(NeoEcoCompactRegistry.COMPACT_F9)); + bus.setLevel(level); + bus.bind(shadowBuses, getMainNode()); + // 新版 ECO 按主机样板域筛选总线。内部影子总线因无节点被过滤, + // 聚合总线若不属于集群又会被目录的域检查排除。 + bus.joinPatternDomain(fleet.get(0)); + exposedBus = bus; + exposedBusLink.attach(bus.getMainNode(), level, getMainNode()); + } + + /** + * 把样板数据装回 176 条影子总线。 + * + *

数据来源两选一:老存档里那份直接写在主机 NBT 里的 {@code useless_compact_pattern_buses} + * (只在迁移时命中一次),否则读外置存储。

+ */ + private void restorePatternBusData(ServerLevel level) { + ListTag saved = pendingPatternBusData; + boolean fromLegacy = saved != null; + if (saved == null) { + saved = storedPatternBusData(level); + } + if (saved == null) { + return; + } + int count = Math.min(saved.size(), shadowBuses.size()); + for (int index = 0; index < count; index++) { + ECOCraftingPatternBusBlockEntity bus = shadowBuses.get(index); + bus.getRootStorage().requireInit(); + bus.loadManagedPersistentData(level.registryAccess(), saved.getCompound(index)); + for (int slot = 0; slot < bus.getPatternSlotCount(); slot++) { + ItemStack stack = bus.getTerminalPatternInventory().getStackInSlot(slot); + if (!stack.isEmpty()) { + bus.setPatternDirect(slot, ItemStack.EMPTY); + bus.setPatternDirect(slot, stack.copy()); + } + } + } + pendingPatternBusData = null; + // 从老 NBT 迁移来的内容还只活在内存里,必须写一次外置存储;本就读自外置存储的则不用写。 + patternStoreDirty = fromLegacy; + } + + /** 从外置存储里取出上次写进去的样板数据;没有(新机器 / 从没写过)就返回 null。 */ + @Nullable + private ListTag storedPatternBusData(ServerLevel level) { + CompoundTag payload = ExternalInventoryStore.loadRawPayload(level, patternStoreReference); + if (payload == null || !payload.contains(PATTERN_BUS_KEY, Tag.TAG_LIST)) { + return null; + } + return payload.getList(PATTERN_BUS_KEY, Tag.TAG_COMPOUND); + } + + /** 把 176 条影子总线当前的全部持久化数据序列化成一份 NBT 列表。 */ + private ListTag capturePatternBusData(HolderLookup.Provider registries) { + ListTag saved = new ListTag(); + for (ECOCraftingPatternBusBlockEntity bus : shadowBuses) { + bus.getRootStorage().requireInit(); + CompoundTag busData = new CompoundTag(); + bus.saveManagedPersistentData(registries, busData, false); + saved.add(busData); + } + return saved; + } + + /** + * 把样板数据写进外置存储。 + * + *

{@code force} 为 false 时只在内容真的变过之后才写一次——序列化 176 条总线是 MB 级的开销, + * 而区块存盘非常频繁。拆除 / 卸载 / 重新成形这类「总线马上就要被销毁」的场合才用 true 兜底。

+ */ + private void flushPatternStore(boolean force) { + if (patternStoreReference == null || shadowBuses.isEmpty()) { + return; + } + if (!force && !patternStoreDirty) { + return; + } + if (!(level instanceof ServerLevel serverLevel)) { + return; + } + CompoundTag payload = new CompoundTag(); + payload.put(PATTERN_BUS_KEY, capturePatternBusData(serverLevel.registryAccess())); + ExternalInventoryStore.saveRawPayload(serverLevel, patternStoreReference, payload); + patternStoreDirty = false; + } + + /** 拿到外置存储引用:优先沿用老存档 / 掉落物带过来的 UUID,没有才新建一份。 */ + private void ensurePatternStore(ServerLevel level) { + if (patternStoreReference != null) { + return; + } + patternStoreReference = ExternalInventoryStore.bindRawAt( + level, ExternalInventoryKind.COMPACT_PATTERN_BUS, + ExternalInventoryStore.getReference(this), worldPosition); + ExternalInventoryStore.setReference(this, patternStoreReference); + // 新认领的存储还没被写过,标记成脏,第一次落盘时把当前(含迁移来的)内容写进去。 + patternStoreDirty = true; + } + + /** + * 让本机认领掉落物带过来的那份样板数据。 + * + *

方块被拆时 UUID 会跟着掉落物走,放在新位置时把同一份数据接回来; + * 如果那份数据已经被别的坐标占着(例如创造模式复制了一台), + * {@code bindRawAt} 会分叉出一份拷贝,两台机器互不影响。

+ */ + public void adoptPatternStore(@Nullable ExternalInventoryReference requested) { + if (patternStoreReference != null || requested == null + || requested.kind() != ExternalInventoryKind.COMPACT_PATTERN_BUS) { + return; + } + if (level == null || level.isClientSide() || level.getServer() == null) { + return; + } + patternStoreReference = ExternalInventoryStore.bindRawAt( + level, ExternalInventoryKind.COMPACT_PATTERN_BUS, requested, worldPosition); + ExternalInventoryStore.setReference(this, patternStoreReference); + patternStoreDirty = true; + } + + /** 供方块掉落物携带:新位置可凭此引用接续同一份外置样板数据。 */ + @Nullable + public ExternalInventoryReference patternStoreReference() { + return patternStoreReference; + } + + /** + * 方块被拆掉时调用:先把最后一次改动写进外置存储,再解除坐标占用。 + * + *

数据本身留在文件里不动,等掉落物把它带到新位置;没人认领就当孤儿文件留着, + * 至少比直接烧掉玩家攒的样板好。

+ */ + public void releasePatternStore() { + flushPatternStore(true); + if (level != null && patternStoreReference != null) { + ExternalInventoryStore.release(level, worldPosition, patternStoreReference); + } + } + + private void disposeFleet() { + if (level instanceof ServerLevel serverLevel) { + flushPatternStore(true); + if (fleet.size() > 1) { + pendingShadowHostData = captureShadowHostData(serverLevel.registryAccess()); + } + if (!shadowWorkers.isEmpty()) { + pendingShadowWorkerData = captureShadowWorkerData(serverLevel.registryAccess()); + } + } + compactInterface.release(); + if (exposedBus != null) { + exposedBus.onChunkUnloaded(); + } + exposedBusLink.detach(); + for (ShadowNodeLink workerLink : shadowWorkerLinks) { + workerLink.detach(); + } + exposedBus = null; + for (CompactCraftingCluster member : fleet) { + if (member != cluster && !member.isDestroyed()) { + member.injectCompactNetworkCluster(null); + member.destroy(); + } + } + } + + private void resetCollections() { + fleet.clear(); + shadowWorkers.clear(); + shadowWorkerLinks.clear(); + shadowBuses.clear(); + patternProvider.bind(List.of()); + lastPatternContentRevision = Integer.MIN_VALUE; + lastPatternAuxiliaryRevision = Long.MIN_VALUE; + } + + @Override + public void onChunkUnloaded() { + disposeFleet(); + resetCollections(); + compactNetworkCluster = null; + compactInterface.dispose(); + super.onChunkUnloaded(); + } + + /** 供调试/UI 读取当前装配出来的逻辑网络。 */ + @Nullable + public NECraftingNetworkCluster compactNetworkCluster() { + return compactNetworkCluster; + } + + // ------------------------------------------------ 网格服务:样板提供者 / 样板存储 + + @Override + public List getAvailablePatterns() { + return patternProvider.getAvailablePatterns(); + } + + @Override + public boolean pushPattern(IPatternDetails patternDetails, KeyCounter[] inputHolder) { + return patternProvider.pushPattern(patternDetails, inputHolder); + } + + @Override + public boolean isBusy() { + return patternProvider.isBusy(); + } + + @Override + public Preparation eco$prepareFastPath(ECOBatchDispatchContext context) { + return patternProvider.eco$prepareFastPath(context); + } + + @Override + public Set getEmitableItems() { + return patternProvider.getEmitableItems(); + } + + /** + * 玩家手持编码样板右键时,把样板交给 ECO 的样板总线实现去解析与登记。 + * + *

样板内容、去重、容量判断全部由 ECO 的 {@code ECOCraftingPatternBusBlockEntity} 处理, + * 这里只是把「单方块没有总线界面」这件事补上一个物品入口,并在成功后刷新网格上的样板缓存。

+ */ + public boolean insertPatternFromPlayer(ItemStack stack) { + return insertPatternFromPlayerWithResult(stack) == ECOPatternInsertionResult.INSERTED; + } + + public ECOPatternInsertionResult insertPatternFromPlayerWithResult(ItemStack stack) { + if (stack.isEmpty() || patternProvider.isEmpty()) { + return ECOPatternInsertionResult.NO_TARGET; + } + ECOPatternInsertionResult result = patternProvider.insertPatternWithResult(stack); + if (result != ECOPatternInsertionResult.INSERTED) { + return result; + } + // 手插的这一下可能赶在本 tick 的版本轮询之前就被拆除/卸载,直接标脏兜底。 + patternStoreDirty = true; + refreshPatternProvider(); + return result; + } + + /** 合并后的样板提供者,暴露给外部集成查询样板数量。 */ + public CompactPatternProvider compactPatternProvider() { + return patternProvider; + } + + /** 暴露给 MinecraftForge 的 F9 流体能力:填充冷却液、抽取冷却副产物。 */ + public IFluidHandler getFluidHandler() { + return fluidHandler; + } + + /** Used by the block interaction to recover an incorrectly inserted input coolant. */ + public IFluidHandler getInputRecoveryHandler() { + return ((CompactF9FluidHandler) fluidHandler).inputRecoveryHandler(); + } + + /** Client-side bridge for ECO's chunked pattern preview transport. */ + public void receivePatternPreview(CompoundTag payload) { + compactInterface.receivePatternPreview(payload); + } + + // ------------------------------------------------ 通讯接口界面(影子接口)入口 + + @Override + public ModularUI createUI(BlockUIMenuType.BlockUIHolder holder) { + ModularUI interfaceUI = compactInterface.createUI(holder); + return interfaceUI == null ? CompactSingleBlockUI.create(holder.player, + net.minecraft.network.chat.Component.translatable("gui.useless_mod.compact.f9.title"), + net.minecraft.network.chat.Component.translatable("gui.useless_mod.compact.f9.overview"), + net.minecraft.network.chat.Component.translatable("gui.useless_mod.compact.f9.interaction")) + : interfaceUI; + } + + @Override + public void handleRPCPacket(RPCSender sender, byte[] data) { + if (!compactInterface.forwardRpc(sender, data)) { + super.handleRPCPacket(sender, data); + } + } + + @Override + public void handleSyncPacket(RegistryAccess access, BitSet changed, byte[] data, CompoundTag extra) { + if (!compactInterface.forwardSync(access, changed, data, extra)) { + super.handleSyncPacket(access, changed, data, extra); + } + } + + @Override + public void saveAdditional(CompoundTag data) { + HolderLookup.Provider registries = level == null ? net.minecraft.core.RegistryAccess.EMPTY : level.registryAccess(); + super.saveAdditional(data); + // 样板内容不再进方块实体自己的 NBT——那份 NBT 会跟着区块存盘,也会被 getUpdateTag + // 整包同步给客户端。这里只在内容真的变过之后,把数据写进 UUID 键控的外置存储。 + flushPatternStore(false); + CompoundTag inputFluid = new CompoundTag(); + fluidInputHatch.tank.writeToNBT(registries, inputFluid); + data.put(FLUID_INPUT_KEY, inputFluid); + CompoundTag outputFluid = new CompoundTag(); + fluidOutputHatch.tank.writeToNBT(registries, outputFluid); + data.put(FLUID_OUTPUT_KEY, outputFluid); + ListTag shadowHostData = fleet.size() > 1 ? captureShadowHostData(registries) : pendingShadowHostData; + if (shadowHostData != null) { + data.put(SHADOW_HOST_KEY, shadowHostData.copy()); + } + ListTag shadowWorkerData = shadowWorkers.isEmpty() + ? pendingShadowWorkerData : captureShadowWorkerData(registries); + if (shadowWorkerData != null && !shadowWorkerData.isEmpty()) { + data.put(SHADOW_WORKER_KEY, shadowWorkerData.copy()); + } + compactInterface.save(registries, data); + } + + @Override + public void loadTag(CompoundTag data) { + HolderLookup.Provider registries = level == null ? net.minecraft.core.RegistryAccess.EMPTY : level.registryAccess(); + super.loadTag(data); + if (data.contains(FLUID_INPUT_KEY, Tag.TAG_COMPOUND)) { + fluidInputHatch.tank.readFromNBT(registries, + data.getCompound(FLUID_INPUT_KEY)); + } + if (data.contains(FLUID_OUTPUT_KEY, Tag.TAG_COMPOUND)) { + fluidOutputHatch.tank.readFromNBT(registries, + data.getCompound(FLUID_OUTPUT_KEY)); + } + if (data.contains(SHADOW_HOST_KEY, Tag.TAG_LIST)) { + pendingShadowHostData = data.getList(SHADOW_HOST_KEY, Tag.TAG_COMPOUND).copy(); + } + if (data.contains(SHADOW_WORKER_KEY, Tag.TAG_LIST)) { + pendingShadowWorkerData = data.getList(SHADOW_WORKER_KEY, Tag.TAG_COMPOUND).copy(); + } + // 老存档:整份样板数据曾经直接写在这个标签里,读出来交给装配流程搬进外置存储。 + // 迁移之后本机 NBT 里不会再出现这个键(saveAdditional 不再写它)。 + if (data.contains(PATTERN_BUS_KEY, Tag.TAG_LIST)) { + pendingPatternBusData = data.getList(PATTERN_BUS_KEY, Tag.TAG_COMPOUND).copy(); + patternStoreDirty = true; + } + compactInterface.load(registries, data); + } + + @Override + public void setRemoved() { + // 兜底:正常拆除走方块的 onRemove → releasePatternStore(),那时已经强制落盘过; + // 这里只补「没走那条路就被摘掉」的情形,所以按脏标记判断,不做无谓的 MB 级重序列化。 + flushPatternStore(false); + compactInterface.dispose(); + super.setRemoved(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/entity/CompactF9FluidHandler.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/entity/CompactF9FluidHandler.java new file mode 100644 index 00000000..c2f1cfd7 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/entity/CompactF9FluidHandler.java @@ -0,0 +1,108 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.entity; + +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.capability.IFluidHandler; +import net.minecraftforge.fluids.capability.templates.FluidTank; +import org.jetbrains.annotations.NotNull; + +/** 紧凑 F9 对外的流体接口:填充进入冷却液输入仓,抽取来自冷却副产物输出仓。 */ +public final class CompactF9FluidHandler implements IFluidHandler { + + static FluidTank createFleetTank(int hosts, Runnable contentsChanged) { + return new FluidTank(Math.multiplyExact(16_000, hosts)) { + @Override + protected void onContentsChanged() { + contentsChanged.run(); + } + }; + } + + private final FluidTank input; + private final FluidTank output; + + public CompactF9FluidHandler(FluidTank input, FluidTank output) { + this.input = input; + this.output = output; + } + + @Override + public int getTanks() { + return 2; + } + + @Override + public @NotNull FluidStack getFluidInTank(int tank) { + return switch (tank) { + case 0 -> input.getFluid(); + case 1 -> output.getFluid(); + default -> FluidStack.EMPTY; + }; + } + + @Override + public int getTankCapacity(int tank) { + return switch (tank) { + case 0 -> input.getCapacity(); + case 1 -> output.getCapacity(); + default -> 0; + }; + } + + @Override + public boolean isFluidValid(int tank, @NotNull FluidStack stack) { + return switch (tank) { + case 0 -> input.isFluidValid(stack); + case 1 -> output.isFluidValid(stack); + default -> false; + }; + } + + @Override + public int fill(@NotNull FluidStack resource, @NotNull FluidAction action) { + return input.fill(resource, action); + } + + @Override + public @NotNull FluidStack drain(@NotNull FluidStack resource, @NotNull FluidAction action) { + return output.drain(resource, action); + } + + @Override + public @NotNull FluidStack drain(int maxDrain, @NotNull FluidAction action) { + return output.drain(maxDrain, action); + } + + /** Player-only recovery view for coolant accidentally left in the input pool. */ + public IFluidHandler inputRecoveryHandler() { + return new IFluidHandler() { + @Override + public int getTanks() { return 1; } + + @Override + public @NotNull FluidStack getFluidInTank(int tank) { + return tank == 0 ? input.getFluid() : FluidStack.EMPTY; + } + + @Override + public int getTankCapacity(int tank) { + return tank == 0 ? input.getCapacity() : 0; + } + + @Override + public boolean isFluidValid(int tank, @NotNull FluidStack stack) { return false; } + + @Override + public int fill(@NotNull FluidStack resource, @NotNull FluidAction action) { return 0; } + + @Override + public @NotNull FluidStack drain(@NotNull FluidStack resource, @NotNull FluidAction action) { + return input.drain(resource, action); + } + + @Override + public @NotNull FluidStack drain(int maxDrain, @NotNull FluidAction action) { + return input.drain(maxDrain, action); + } + }; + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/entity/CompactL9BlockEntity.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/entity/CompactL9BlockEntity.java new file mode 100644 index 00000000..c9a8a724 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/entity/CompactL9BlockEntity.java @@ -0,0 +1,384 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.entity; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import appeng.api.networking.IGridNode; +import appeng.api.orientation.BlockOrientation; +import appeng.api.networking.ticking.IGridTickable; +import appeng.api.networking.ticking.TickRateModulation; +import appeng.api.networking.ticking.TickingRequest; +import appeng.api.storage.IStorageProvider; +import appeng.util.inv.AppEngInternalInventory; +import cn.dancingsnow.neoecoae.api.ECOTier; +import cn.dancingsnow.neoecoae.blocks.entity.NEBlockEntity; +import cn.dancingsnow.neoecoae.blocks.entity.storage.ECOStorageSystemBlockEntity; +import cn.dancingsnow.neoecoae.impl.storage.infinite.ECOStorageHostMode; +import cn.dancingsnow.neoecoae.multiblock.calculator.NEStorageClusterCalculator; +import cn.dancingsnow.neoecoae.multiblock.cluster.NEStorageCluster; +import com.lowdragmc.lowdraglib2.gui.factory.BlockUIMenuType; +import com.lowdragmc.lowdraglib2.gui.ui.ModularUI; +import com.lowdragmc.lowdraglib2.syncdata.rpc.RPCSender; +import com.sorrowmist.useless.compat.neoecoae.compact.NeoEcoCompactRegistry; +import com.sorrowmist.useless.compat.neoecoae.compact.block.CompactTicker; +import com.sorrowmist.useless.compat.neoecoae.compact.calculator.CompactHost; +import com.sorrowmist.useless.compat.neoecoae.compact.cluster.CompactStorageCluster; +import com.sorrowmist.useless.compat.neoecoae.compact.shadow.CompactInterfaceAttachment; +import com.sorrowmist.useless.compat.neoecoae.compact.ui.CompactSingleBlockUI; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.component.DataComponents; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.util.BitSet; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +/** + * 无用型紧凑 L9 —— 等价于一台「无限存储模式已迁移完成」的 L9 存储子系统。 + * + *

自己不实现任何存储逻辑:容量、编解码、注入抽取全部来自 ECO 的 + * {@code ECOInfiniteStorageEngine} / {@code ECOInfiniteStorage}。

+ * + *

成形方式:ECO 的主机基类在构造函数里把 {@code calculator} 硬编码成了多方块计算器 + * (字段 final,无法替换),所以这里不依赖计算器,而是自己接管成形生命周期: + * {@link #onReady()} 之后调用 {@link #formCompactStructure()} 建出只含自己的集群; + * 同时停用 {@code updateMultiBlock} / {@code rebuildMultiBlock}, + * 否则邻居变化时 ECO 的多方块计算器会判定结构不合法并拆除集群。

+ */ +public class CompactL9BlockEntity extends ECOStorageSystemBlockEntity + implements CompactHost, CompactTicker, IGridTickable { + + private static final Logger LOGGER = LoggerFactory.getLogger(CompactL9BlockEntity.class); + + /** 与 ECO 控制器掉落物里使用的同一个 NBT 键。 */ + private static final String DOMAIN_TAG = "neoecoae_infinite_controller_domain"; + private static final String MODE_TAG = "neoecoae_infinite_controller_mode"; + /** ECO 的 INFINITE_COMPONENT_REQUIRED。 */ + private static final int INFINITE_COMPONENTS = 64; + /** 自愈检查周期(tick)。 */ + private static final int MAINTAIN_INTERVAL = 20; + /** 等待网格就绪的最大重试次数。 */ + private static final int ANNOUNCE_MAX_ATTEMPTS = 100; + + private boolean pendingStorageAnnounce = true; + private int storageAnnounceAttempts; + private int maintainTicker; + private long lastCompactTick = Long.MIN_VALUE; + + /** 通讯接口界面(影子接口)的全部接线;普通右键仍然走主机自己的面板。 */ + private final CompactInterfaceAttachment compactInterface = + new CompactInterfaceAttachment<>(this, NEStorageClusterCalculator::new); + + + public CompactL9BlockEntity(BlockEntityType type, BlockPos pos, BlockState blockState) { + super(type, pos, blockState, ECOTier.L9); + // 双保险:世界 ticker 与 AE2 网格 ticker 都会驱动本机逻辑(同一 tick 去重)。 + getMainNode().addService(IGridTickable.class, this); + } + + // ------------------------------------------------------------- 成形生命周期 + + @Override + public void onReady() { + super.onReady(); + formCompactStructure(); + onGridConnectableSidesChanged(); + } + + @Override + public void updateMultiBlock(BlockPos changedPos) { + // 紧凑方块的结构与邻居无关:ECO 的多方块计算器在这里只会误判并拆掉集群。 + if (cluster == null || cluster.isDestroyed()) { + formCompactStructure(); + } + } + + @Override + public void rebuildMultiblock() { + if (cluster == null || cluster.isDestroyed()) { + formCompactStructure(); + } + } + + private void formCompactStructure() { + if (!(level instanceof ServerLevel serverLevel)) { + return; + } + if (cluster != null && !cluster.isDestroyed() && formed) { + return; + } + CompactStorageCluster compact = new CompactStorageCluster(worldPosition, worldPosition); + compact.addBlockEntity(this); + installCompactComponents(compact, serverLevel); + linkAll(compact); + + compact.updateFormed(true); + compact.updateStatus(true); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private void linkAll(NEStorageCluster cluster) { + cluster.getBlockEntities().forEachRemaining(blockEntity -> { + if (blockEntity instanceof NEBlockEntity neBlockEntity) { + neBlockEntity.updateCluster(cluster); + } + }); + } + + // ------------------------------------------------------------------ tick + + @Override + public void tick(Level level, BlockPos pos, BlockState state) { + super.tick(level, pos, state); + if (level instanceof ServerLevel serverLevel) { + compactServerTick(serverLevel); + } + } + + @Override + public TickingRequest getTickingRequest(IGridNode node) { + return new TickingRequest(1, 10, false); + } + + /** AE2 网格 ticker 通道:世界 ticker 万一没跑,存储重挂与自愈在这里兜底。 */ + @Override + public TickRateModulation tickingRequest(IGridNode node, int ticksSinceLastCall) { + if (level instanceof ServerLevel serverLevel) { + compactServerTick(serverLevel); + } + return TickRateModulation.IDLE; + } + + /** 两个 tick 来源共用;同一个游戏 tick 只处理一次。 */ + private void compactServerTick(ServerLevel serverLevel) { + long now = serverLevel.getGameTime(); + if (now == lastCompactTick) { + return; + } + lastCompactTick = now; + compactInterface.tick(); + // 主机自己的 IStorageProvider 是在节点入网那一刻被 AE2 读取的,那时 formed 还是 false, + // 无限存储会被跳过;所以入网后必须补一次「重挂存储」。 + if (pendingStorageAnnounce && storageAnnounceAttempts++ < ANNOUNCE_MAX_ATTEMPTS) { + pendingStorageAnnounce = !requestStorageUpdate(); + } + if (++maintainTicker >= MAINTAIN_INTERVAL) { + maintainTicker = 0; + maintainInfiniteStorage(serverLevel); + } + } + + @Override + protected void onMainNodeGridChanged() { + super.onMainNodeGridChanged(); + // 换了网格就要重新挂存储,并重新确认无限模式。 + pendingStorageAnnounce = true; + storageAnnounceAttempts = 0; + maintainInfiniteStorage(getLevel() instanceof ServerLevel serverLevel ? serverLevel : null); + } + + private boolean requestStorageUpdate() { + IGridNode node = getActionableNode(); + if (node == null || node.getGrid() == null) { + // requestUpdate 内部不做空值判断,网格未就绪时必须等待。 + return false; + } + IStorageProvider.requestUpdate(getMainNode()); + return true; + } + + /** + * 自愈:只要方块中仍保留内建的 64 个无限组件(取出通道已由 {@link #canExtractInfiniteComponents()} 封禁), + * 这台方块就定义上是无限存储主机;ECO 的状态机若被外部因素推离 + * {@code formed_infinite},这里把它拉回来。 + */ + private void maintainInfiniteStorage(ServerLevel level) { + if (level == null || isFormedInfiniteMode()) { + return; + } + primeInfiniteComponents(); + restoreOrCreateInfiniteDomain(level); + requestStorageUpdate(); + } + + /** + * 内建的 64 个无限组件不允许被玩家取出。 + * + *

否则「取出 64 个 → 下一 tick 自动补满」将构成无限复制; + * 且紧凑 L9 的设计定位是「始终处于无限存储」,不应支持退回普通模式。

+ */ + + + @Override + public boolean canExtractInfiniteComponents() { + return false; + } + + @Override + public Set getGridConnectableSides(BlockOrientation orientation) { + return formed ? EnumSet.allOf(Direction.class) : EnumSet.noneOf(Direction.class); + } + + @Override + public int getSelectedBuildLength() { + return getMaxBuildLength(); + } + + @Override + public void setSelectedBuildLength(int length) { + // 紧凑方块没有「建造长度」概念,固定为结构上限。 + } + + /** + * 内建的 64 个无限组件属于方块本体,不能作为掉落物被复制出去。 + * + *

域本身通过 ECO 原生的 {@code applyInfiniteDomainToControllerDrop} 写进方块物品, + * 放回来时由 {@code ECOStorageSystemBlock#setPlacedBy} 恢复,所以这里不产生额外掉落是安全的。

+ */ + @Override + public void addAdditionalDrops(Level level, BlockPos pos, List drops) { + } + + // ------------------------------------------------------------- 集群装配 + + @Override + public void installCompactComponents(NEStorageCluster cluster, ServerLevel level) { + compactInterface.release(); + primeInfiniteComponents(); + restoreOrCreateInfiniteDomain(level); + compactInterface.attach(level, cluster, getMainNode()); + } + + /** + * 把 64 个无限存储组件放进主机自己那唯一的组件槽。 + * + *

该槽位在 ECO 里是私有的,这里通过反射写入,语义与玩家在主机界面里手动放入 64 个组件一致; + * 写入走 {@code setItemDirect},它会回调 {@code onChangeInventory} 把 {@code infiniteComponentsDirty} 置位。

+ */ + private void primeInfiniteComponents() { + ItemStack current = readComponentSlot(); + if (current != null && !current.isEmpty() && current.getCount() >= INFINITE_COMPONENTS) { + return; + } + ItemStack stack = NeoEcoCompactRegistry.neoEcoStack("eco_infinite_cell_component"); + stack.setCount(INFINITE_COMPONENTS); + if (!writeComponentSlot(stack)) { + RateLimitedLogger.warn(LOGGER, "Compact L9 at {} could not preload ECO infinite components", worldPosition); + } + } + + private ItemStack readComponentSlot() { + AppEngInternalInventory inventory = componentInventory(); + return inventory == null ? null : inventory.getStackInSlot(0); + } + + private boolean writeComponentSlot(ItemStack stack) { + AppEngInternalInventory inventory = componentInventory(); + if (inventory == null) { + return false; + } + inventory.setItemDirect(0, stack); + return true; + } + + private AppEngInternalInventory componentInventory() { + try { + Field field = ECOStorageSystemBlockEntity.class.getDeclaredField("infiniteComponentInventory"); + field.setAccessible(true); + Object value = field.get(this); + if (value instanceof AppEngInternalInventory inventory) { + return inventory; + } + } catch (ReflectiveOperationException ignored) { + // 落到下面返回 null,由调用方给出警告。 + } + return null; + } + + /** + * 若方块掉落物里已经带着一个无限域(ECO 的 {@code applyInfiniteDomainToControllerDrop} 写的),就沿用它; + * 否则用「维度 + 坐标」推导出一个稳定 UUID 作为新域。 + */ + private void restoreOrCreateInfiniteDomain(ServerLevel level) { + ItemStack carrier = new ItemStack(NeoEcoCompactRegistry.COMPACT_L9_ITEM.get()); + applyInfiniteDomainToControllerDrop(carrier); + CompoundTag tag = carrier.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY).copyTag(); + if (!tag.hasUUID(DOMAIN_TAG)) { + tag.putUUID(DOMAIN_TAG, stableDomainId(level)); + } + tag.putString(MODE_TAG, ECOStorageHostMode.FORMED_INFINITE.id()); + carrier.set(DataComponents.CUSTOM_DATA, CustomData.of(tag)); + restoreInfiniteDomainFromItem(carrier); + } + + private UUID stableDomainId(ServerLevel level) { + String seed = "useless_mod:compact_l9:" + level.dimension().location() + ':' + worldPosition.asLong(); + return UUID.nameUUIDFromBytes(seed.getBytes(StandardCharsets.UTF_8)); + } + + // ------------------------------------------------ 通讯接口界面(影子接口)入口 + + @Override + public ModularUI createUI(BlockUIMenuType.BlockUIHolder holder) { + ModularUI interfaceUI = compactInterface.createUI(holder); + return interfaceUI == null ? CompactSingleBlockUI.create(holder.player, + net.minecraft.network.chat.Component.translatable("gui.useless_mod.compact.l9.title"), + net.minecraft.network.chat.Component.translatable("gui.useless_mod.compact.l9.overview"), + net.minecraft.network.chat.Component.translatable("gui.useless_mod.compact.l9.interaction")) + : interfaceUI; + } + + @Override + public void handleRPCPacket(RPCSender sender, byte[] data) { + if (!compactInterface.forwardRpc(sender, data)) { + super.handleRPCPacket(sender, data); + } + } + + @Override + public void handleSyncPacket(RegistryAccess access, BitSet changed, byte[] data, CompoundTag extra) { + if (!compactInterface.forwardSync(access, changed, data, extra)) { + super.handleSyncPacket(access, changed, data, extra); + } + } + + @Override + public void saveAdditional(CompoundTag data) { + HolderLookup.Provider registries = level == null ? net.minecraft.core.RegistryAccess.EMPTY : level.registryAccess(); + super.saveAdditional(data); + compactInterface.save(registries, data); + } + + @Override + public void loadTag(CompoundTag data) { + HolderLookup.Provider registries = level == null ? net.minecraft.core.RegistryAccess.EMPTY : level.registryAccess(); + super.loadTag(data); + compactInterface.load(registries, data); + } + + @Override + public void onChunkUnloaded() { + compactInterface.dispose(); + super.onChunkUnloaded(); + } + + @Override + public void setRemoved() { + compactInterface.dispose(); + super.setRemoved(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/exposed/ExposedPatternBusBlockEntity.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/exposed/ExposedPatternBusBlockEntity.java new file mode 100644 index 00000000..04ffe7a6 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/exposed/ExposedPatternBusBlockEntity.java @@ -0,0 +1,353 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.exposed; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.implementations.blockentities.PatternContainerGroup; +import appeng.api.inventories.InternalInventory; +import appeng.api.networking.IGrid; +import appeng.api.networking.IGridNode; +import appeng.api.networking.IGridNodeListener; +import appeng.api.networking.IManagedGridNode; +import appeng.api.networking.crafting.ICraftingProvider; +import appeng.api.orientation.BlockOrientation; +import appeng.api.stacks.AEItemKey; +import cn.dancingsnow.neoecoae.api.ECOPatternInsertionResult; +import cn.dancingsnow.neoecoae.api.ECOPreparedPattern; +import cn.dancingsnow.neoecoae.blocks.entity.ECOMachineInterfaceBlockEntity; +import cn.dancingsnow.neoecoae.blocks.entity.crafting.ECOCraftingPatternBusBlockEntity; +import cn.dancingsnow.neoecoae.multiblock.cluster.NECraftingCluster; +import com.sorrowmist.useless.compat.neoecoae.compact.NeoEcoCompactRegistry; +import com.sorrowmist.useless.compat.neoecoae.compact.provider.CombinedPatternInventory; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; + +/** + * 紧凑 F9 对外暴露的「聚合样板总线」:把紧凑机内部全部影子总线的样板合成一条总线。 + * + *

ECO 的样板总线和 AE2 的样板访问终端都只认「挂在网格节点上、且登记在 + * {@code ECOCraftingPatternBusBlockEntity} 名下」的方块实体,而紧凑机里的 + * {@link com.sorrowmist.useless.compat.neoecoae.compact.shadow.ShadowPatternBusBlockEntity} + * 不落世界也没有节点。所以这里额外实例化一条有真实网格节点的总线,把内部总线的 + * 样板视图整体转出去,让界面看到的是「一台八联满配 F9 的其中一个分组」。

+ * + *

为什么它不会抢派单:本类覆写两个提供者入口,让 + * {@code isBusy()} 恒为 true、{@code getAvailablePatterns()} 始终返回空列表, + * 于是 AE2 的样板提供者列表里它始终是个「忙且没样板」的条目;真正派单的依旧是宿主 + * {@code CompactF9BlockEntity} 上的 {@code CompactPatternProvider}。它登记在集群里只为了让 + * 新版 ECO 的通讯接口与样板目录识别本机的样板域。

+ */ +public class ExposedPatternBusBlockEntity extends ECOCraftingPatternBusBlockEntity { + + private final CombinedPatternInventory mergedPatternInventory = new CombinedPatternInventory(); + private final List buses = new ArrayList<>(); + private int[] busRevisions = new int[0]; + private int[] busSlotCounts = new int[0]; + private long[] busAuxiliaryRevisions = new long[0]; + @Nullable + private InternalInventory combinedTerminalInventory; + @Nullable + private IManagedGridNode hostNode; + + public ExposedPatternBusBlockEntity(BlockEntityType type, BlockPos pos, BlockState blockState) { + super(type, pos, blockState); + // 与影子一样:不要通道、不落世界,靠宿主节点显式连接入网。 + getMainNode().setFlags(); + getMainNode().setInWorldNode(false); + } + + @Override + @Nullable + public IGrid getGrid() { + IGridNode node = getGridNode(); + return node == null ? null : node.getGrid(); + } + + /** 绑定紧凑机内部全部影子总线,并记下宿主节点(样板集变化时要刷新它的提供者缓存)。 */ + public void bind(List source, IManagedGridNode hostNode) { + buses.clear(); + buses.addAll(source); + mergedPatternInventory.bind(buses); + busRevisions = new int[buses.size()]; + busSlotCounts = new int[buses.size()]; + busAuxiliaryRevisions = new long[buses.size()]; + for (int index = 0; index < buses.size(); index++) { + busRevisions[index] = buses.get(index).getPatternContentRevision(); + busSlotCounts[index] = buses.get(index).getPatternSlotCount(); + busAuxiliaryRevisions[index] = buses.get(index).getAuxiliaryRevision(); + } + combinedTerminalInventory = null; + this.hostNode = hostNode; + } + + /** 让域过滤和样板目录认出聚合总线的归属;本类的提供者入口仍保持禁用。 */ + public void joinPatternDomain(NECraftingCluster owner) { + owner.addBlockEntity(this); + updateCluster(owner); + } + + @Override + public void onReady() { + } + + @Override + public void onMainNodeStateChanged(IGridNodeListener.State reason) { + if (reason != IGridNodeListener.State.POWER && reason != IGridNodeListener.State.GRID_BOOT) { + return; + } + IGrid grid = getMainNode().getGrid(); + if (grid == null) { + return; + } + for (ECOMachineInterfaceBlockEntity machineInterface : grid.getActiveMachines(ECOMachineInterfaceBlockEntity.class)) { + machineInterface.onPatternBusTopologyChanged(this); + } + } + + @Override + public void onChunkUnloaded() { + super.onChunkUnloaded(); + } + + @Override + public void setRemoved() { + super.setRemoved(); + } + + /** + * 宿主每 tick 驱动一次:逐条比对内部总线的内容版本。 + * + *

内部总线没有网格节点,父类那条「内容变了就通知通讯接口」的链路在它们身上是静默的, + * 而写入口不止一处(玩家点击、一键整理、迁移、还有主机自己的右键插样板), + * 所以这里用轮询兜底,任何一条写入都不会漏掉。

+ */ + public void tick() { + List changedBuses = new ArrayList<>(); + boolean layoutChanged = false; + for (int index = 0; index < buses.size(); index++) { + ECOCraftingPatternBusBlockEntity bus = buses.get(index); + bus.flushScheduledPatternDetails(); + int revision = bus.getPatternContentRevision(); + long auxiliaryRevision = bus.getAuxiliaryRevision(); + int slotCount = bus.getPatternSlotCount(); + boolean resized = busSlotCounts[index] != slotCount; + if (busRevisions[index] == revision && busAuxiliaryRevisions[index] == auxiliaryRevision && !resized) { + continue; + } + busRevisions[index] = revision; + busAuxiliaryRevisions[index] = auxiliaryRevision; + busSlotCounts[index] = slotCount; + layoutChanged |= resized; + changedBuses.add(bus); + } + if (!changedBuses.isEmpty()) { + combinedTerminalInventory = null; + if (layoutChanged) { + IGrid grid = getGrid(); + if (grid != null) { + // 一条总线的页数变化会移动后面所有槽位,即使总槽数刚好没有变化。 + for (ECOMachineInterfaceBlockEntity machineInterface + : grid.getActiveMachines(ECOMachineInterfaceBlockEntity.class)) { + machineInterface.onPatternBusInventoryChanged(this); + } + } + } else { + changedBuses.forEach(this::notifyInterfacesOf); + } + // 派单由宿主节点上的样板提供者负责,样板集一变就要让它重读合并结果。 + if (hostNode != null) { + ICraftingProvider.requestUpdate(hostNode); + } + } + } + + // ---------------------------------------------------------------- 合并视图 + + public InternalInventory getPatternSlotInventory() { + return mergedPatternInventory; + } + + @Override + public InternalInventory getTerminalPatternInventory() { + if (combinedTerminalInventory == null) { + combinedTerminalInventory = mergedPatternInventory.createTerminalView(); + } + return combinedTerminalInventory; + } + + @Override + public int getPatternSlotCount() { + return mergedPatternInventory.size(); + } + + /** 对外代表整台紧凑 F9,分组与主机过去在终端里的那条完全一致(槽位与插拔等价)。 */ + @Override + public PatternContainerGroup getTerminalGroup() { + var block = NeoEcoCompactRegistry.COMPACT_F9.get(); + return new PatternContainerGroup(AEItemKey.of(block.asItem()), block.getName(), List.of()); + } + + @Override + public void setPatternDirect(int slot, ItemStack stack) { + mergedPatternInventory.setItemDirect(slot, stack); + } + + @Override + public void beginPatternBatch() { + for (ECOCraftingPatternBusBlockEntity bus : buses) { + bus.beginPatternBatch(); + } + } + + @Override + public void endPatternBatch() { + for (ECOCraftingPatternBusBlockEntity bus : buses) { + bus.endPatternBatch(); + } + } + + // ---------------------------------------------------------------- 样板写入 + + @Override + public boolean insertPattern(ItemStack itemStack) { + return insertPatternWithResult(itemStack) == ECOPatternInsertionResult.INSERTED; + } + + @Override + public ECOPatternInsertionResult insertPatternWithResult(ItemStack itemStack) { + ECOPreparedPattern prepared = preparePattern(itemStack); + return prepared == null ? ECOPatternInsertionResult.INCOMPATIBLE : insertPreparedPattern(prepared); + } + + @Override + public ECOPatternInsertionResult insertPatternKnownUnique(ItemStack itemStack) { + ECOPreparedPattern prepared = preparePattern(itemStack); + return prepared == null ? ECOPatternInsertionResult.INCOMPATIBLE : insertPreparedPatternKnownUnique(prepared); + } + + @Override + public ECOPatternInsertionResult insertPreparedPattern(ECOPreparedPattern prepared) { + return insertIntoBuses(bus -> bus.insertPreparedPattern(prepared)); + } + + @Override + public ECOPatternInsertionResult insertPreparedPatternKnownUnique(ECOPreparedPattern prepared) { + return insertIntoBuses(bus -> bus.insertPreparedPatternKnownUnique(prepared)); + } + + /** 紧凑机的样板仍然存在真实总线里,所以这里只是把插入转给第一条腾得出位置的总线。 */ + private ECOPatternInsertionResult insertIntoBuses( + Function action) { + ECOPatternInsertionResult last = ECOPatternInsertionResult.NO_TARGET; + for (ECOCraftingPatternBusBlockEntity bus : buses) { + ECOPatternInsertionResult result = action.apply(bus); + if (result == ECOPatternInsertionResult.INSERTED) { + return result; + } + last = result; + } + return last; + } + + /** 内部总线都各自校验自己的逻辑域,去重交给样板目录做全网检查。 */ + @Override + public boolean checksLogicalDomainForDuplicates() { + return false; + } + + // ---------------------------------------------------------------- 目录 / 终端读路径 + + @Override + public List getAvailablePatterns() { + return List.of(); + } + + @Override + public boolean isBusy() { + // It belongs to the host's pattern domain for management, but dispatch stays on the host. + return true; + } + + public void refreshAdvertisedPatterns() { + refreshPatternDetailsForCatalog(); + if (hostNode != null && hostNode.getGrid() != null) { + ICraftingProvider.requestUpdate(hostNode); + } + } + + /** 内容版本取各内部总线版本的滚动哈希:任何一条变了,样板目录就会重建本视图的索引。 */ + @Override + public int getPatternContentRevision() { + int revision = 1; + for (ECOCraftingPatternBusBlockEntity bus : buses) { + revision = revision * 31 + bus.getPatternContentRevision(); + } + return revision; + } + + @Override + public void refreshPatternDetailsForCatalog() { + for (ECOCraftingPatternBusBlockEntity bus : buses) { + bus.refreshPatternDetailsForCatalog(); + } + } + + @Override + @Nullable + public IPatternDetails getDecodedPatternDetails(int slot) { + ECOCraftingPatternBusBlockEntity bus = mergedPatternInventory.busOf(slot); + return bus == null ? null : bus.getDecodedPatternDetails(mergedPatternInventory.localSlotOf(slot)); + } + + @Override + public String getPatternSearchKeywords(int slot) { + ECOCraftingPatternBusBlockEntity bus = mergedPatternInventory.busOf(slot); + return bus == null ? "" : bus.getPatternSearchKeywords(mergedPatternInventory.localSlotOf(slot)); + } + + // ---------------------------------------------------------------- 通知 + + /** 把某条内部总线的整段下标标脏(具体动了哪个槽只有那条总线自己知道)。 */ + private void notifyInterfacesOf(ECOCraftingPatternBusBlockEntity bus) { + int offset = mergedPatternInventory.offsetOf(bus); + IGrid grid = getMainNode().getGrid(); + if (offset < 0 || grid == null) { + return; + } + int count = bus.getPatternSlotCount(); + int[] slots = new int[count]; + for (int index = 0; index < count; index++) { + slots[index] = offset + index; + } + for (ECOMachineInterfaceBlockEntity machineInterface : grid.getActiveMachines(ECOMachineInterfaceBlockEntity.class)) { + machineInterface.onPatternBusInventoryChanged(this, slots); + } + } + + // ---------------------------------------------------------------- 影子卫生:绝不回写世界 + + @Override + public void updateState(boolean updateExposed) { + } + + @Override + public void markForUpdate() { + } + + @Override + public void saveChanges() { + } + + @Override + public Set getGridConnectableSides(BlockOrientation orientation) { + return EnumSet.noneOf(Direction.class); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/provider/CombinedPatternInventory.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/provider/CombinedPatternInventory.java new file mode 100644 index 00000000..3ccd71c3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/provider/CombinedPatternInventory.java @@ -0,0 +1,177 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.provider; + +import appeng.api.inventories.BaseInternalInventory; +import appeng.api.inventories.InternalInventory; +import appeng.util.inv.CombinedInternalInventory; +import cn.dancingsnow.neoecoae.blocks.entity.crafting.ECOCraftingPatternBusBlockEntity; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * 把紧凑 F9 内部所有影子样板总线的真实样板槽串成一个连续库存。 + * + *

对外的槽位编号是各总线顺序拼接后的绝对下标,读写一律经底层总线自己的 + * {@code setPatternDirect},从而保证每条总线的空槽索引、内容版本与目录通知正常更新。

+ */ +public final class CombinedPatternInventory extends BaseInternalInventory { + + private final List buses = new ArrayList<>(); + private final List offsets = new ArrayList<>(); + private int total; + + /** 绑定(或重新绑定)底层总线;对外的槽位编号是各总线顺序拼接后的绝对下标。 */ + public void bind(List source) { + buses.clear(); + offsets.clear(); + total = 0; + for (ECOCraftingPatternBusBlockEntity bus : source) { + if (bus.getPatternSlotCount() <= 0) { + continue; + } + offsets.add(total); + buses.add(bus); + total += bus.getPatternSlotCount(); + } + } + + public boolean isEmpty() { + return total <= 0; + } + + @Override + public int size() { + syncOffsets(); + return total; + } + + @Override + public ItemStack getStackInSlot(int slot) { + syncOffsets(); + int part = partOf(slot); + return part < 0 ? ItemStack.EMPTY : bus(part).getPatternSlotInventory().getStackInSlot(localSlot(part, slot)); + } + + @Override + public void setItemDirect(int slot, ItemStack stack) { + syncOffsets(); + int part = partOf(slot); + if (part < 0) { + return; + } + bus(part).setPatternDirect(localSlot(part, slot), stack); + syncOffsets(); + } + + @Override + public boolean isItemValid(int slot, ItemStack stack) { + syncOffsets(); + int part = partOf(slot); + return part >= 0 && bus(part).getPatternSlotInventory().isItemValid(localSlot(part, slot), stack); + } + + @Override + public int getSlotLimit(int slot) { + syncOffsets(); + int part = partOf(slot); + return part < 0 ? 0 : bus(part).getPatternSlotInventory().getSlotLimit(localSlot(part, slot)); + } + + /** + * 终端会在打开时固定客户端镜像的长度,因此一份会话的偏移不能随真实槽位变化。 + * 同时必须保留底层终端的插拔协议,避免隐藏的样板磁盘被覆盖或磁盘样板被无偿取出。 + */ + public InternalInventory createTerminalView() { + InternalInventory[] views = buses.stream() + .map(ECOCraftingPatternBusBlockEntity::getTerminalPatternInventory) + .toArray(InternalInventory[]::new); + int[] ends = new int[views.length]; + int running = 0; + for (int index = 0; index < views.length; index++) { + running += views[index].size(); + ends[index] = running; + } + return new CombinedInternalInventory(views) { + @Override + public InternalInventory getSlotInv(int slot) { + if (slot < 0 || slot >= size()) { + return InternalInventory.empty(); + } + for (int index = 0; index < ends.length; index++) { + if (slot < ends[index]) { + int start = index == 0 ? 0 : ends[index - 1]; + return views[index].getSlotInv(slot - start); + } + } + return InternalInventory.empty(); + } + }; + } + + /** 合并视图里该槽位所属的底层总线;越界返回 null。 */ + @Nullable + public ECOCraftingPatternBusBlockEntity busOf(int slot) { + syncOffsets(); + int part = partOf(slot); + return part < 0 ? null : bus(part); + } + + /** 合并视图槽位在所属底层总线内的下标;越界返回 -1。 */ + public int localSlotOf(int slot) { + syncOffsets(); + int part = partOf(slot); + return part < 0 ? -1 : localSlot(part, slot); + } + + /** 某条底层总线在合并视图里的起始下标;不属于本视图时返回 -1。 */ + public int offsetOf(ECOCraftingPatternBusBlockEntity bus) { + syncOffsets(); + int index = buses.indexOf(bus); + return index < 0 ? -1 : offsets.get(index); + } + + /** + * 单条总线的页数会随占用槽位增长,合并视图的下标随之整体后移; + * 对外读之前先对一次总长,避免拿旧下标去读新布局。 + */ + private void syncOffsets() { + int running = 0; + for (int index = 0; index < buses.size(); index++) { + int next = running + bus(index).getPatternSlotCount(); + if (offsets.get(index) != running) { + offsets.set(index, running); + } + running = next; + } + total = running; + } + + private ECOCraftingPatternBusBlockEntity bus(int part) { + return buses.get(part); + } + + private int localSlot(int part, int slot) { + return slot - offsets.get(part); + } + + private int partOf(int slot) { + if (slot < 0 || slot >= total) { + return -1; + } + int low = 0; + int high = offsets.size() - 1; + int found = -1; + while (low <= high) { + int middle = (low + high) >>> 1; + if (offsets.get(middle) <= slot) { + found = middle; + low = middle + 1; + } else { + high = middle - 1; + } + } + return found; + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/provider/CompactPatternProvider.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/provider/CompactPatternProvider.java new file mode 100644 index 00000000..e8211d07 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/provider/CompactPatternProvider.java @@ -0,0 +1,128 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.provider; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.networking.crafting.ICraftingProvider; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.KeyCounter; +import cn.dancingsnow.neoecoae.api.ECOPatternInsertionResult; +import cn.dancingsnow.neoecoae.api.IECOPatternStorage; +import cn.dancingsnow.neoecoae.api.me.provider.ECOBatchDispatchContext; +import cn.dancingsnow.neoecoae.api.me.provider.ECOFastPathDispatchProvider; +import cn.dancingsnow.neoecoae.blocks.entity.crafting.ECOCraftingPatternBusBlockEntity; +import net.minecraft.world.item.ItemStack; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** + * 把一整个紧凑 F9 里所有影子样板总线聚合成的单一网格服务。 + * + *

AE2 与 ECO 的网格每个服务类只认一个实例,而紧凑 F9 内部有 176 条真实样板总线; + * 这个聚合器把它们的样板集合、派单与插入接口按 ECO 原生实现逐条转发,不做任何自己的样板逻辑。

+ */ +public final class CompactPatternProvider implements ICraftingProvider, IECOPatternStorage, + ECOFastPathDispatchProvider { + + private final List buses = new ArrayList<>(); + + public void bind(List newBuses) { + buses.clear(); + buses.addAll(newBuses); + } + + public boolean isEmpty() { + return buses.isEmpty(); + } + + public List buses() { + return List.copyOf(buses); + } + + @Override + public List getAvailablePatterns() { + if (buses.isEmpty()) { + return List.of(); + } + Set union = new LinkedHashSet<>(); + for (ECOCraftingPatternBusBlockEntity bus : buses) { + union.addAll(bus.getAvailablePatterns()); + } + return List.copyOf(union); + } + + @Override + public boolean pushPattern(IPatternDetails patternDetails, KeyCounter[] inputHolder) { + for (ECOCraftingPatternBusBlockEntity bus : buses) { + if (bus.pushPattern(patternDetails, inputHolder)) { + return true; + } + } + return false; + } + + @Override + public boolean isBusy() { + if (buses.isEmpty()) { + return true; + } + for (ECOCraftingPatternBusBlockEntity bus : buses) { + if (!bus.isBusy()) { + return false; + } + } + return true; + } + + @Override + public Set getEmitableItems() { + Set union = new LinkedHashSet<>(); + for (ECOCraftingPatternBusBlockEntity bus : buses) { + union.addAll(bus.getEmitableItems()); + } + return union; + } + + /** + * 保留 ECO 样板总线的原生批量/虚拟批次派单;普通 ICraftingProvider 转发不会进入这条路径。 + */ + @Override + public Preparation eco$prepareFastPath(ECOBatchDispatchContext context) { + Preparation best = null; + for (ECOCraftingPatternBusBlockEntity bus : buses) { + Preparation candidate = bus.eco$prepareFastPath(context); + if (candidate != null && (best == null || candidate.capacity() > best.capacity())) { + best = candidate; + } + } + return best; + } + + @Override + public boolean insertPattern(ItemStack itemStack) { + return insertPatternWithResult(itemStack) == ECOPatternInsertionResult.INSERTED; + } + + @Override + public ECOPatternInsertionResult insertPatternWithResult(ItemStack itemStack) { + if (itemStack.isEmpty() || buses.isEmpty()) { + return ECOPatternInsertionResult.NO_TARGET; + } + // 只解码一次,再交给各总线的「已知唯一」快路径,避免每条总线都重扫整个逻辑域。 + ECOPatternInsertionResult last = ECOPatternInsertionResult.NO_TARGET; + for (ECOCraftingPatternBusBlockEntity bus : buses) { + ECOPatternInsertionResult result = bus.insertPatternWithResult(itemStack); + if (result == ECOPatternInsertionResult.INSERTED) { + return result; + } + last = result; + } + return last; + } + + @Override + public boolean checksLogicalDomainForDuplicates() { + return false; + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/CompactInterfaceAttachment.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/CompactInterfaceAttachment.java new file mode 100644 index 00000000..81aea632 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/CompactInterfaceAttachment.java @@ -0,0 +1,207 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.shadow; + +import appeng.api.networking.IManagedGridNode; +import cn.dancingsnow.neoecoae.multiblock.calculator.NEClusterCalculator; +import cn.dancingsnow.neoecoae.multiblock.cluster.NECluster; +import com.lowdragmc.lowdraglib2.gui.factory.BlockUIMenuType; +import com.lowdragmc.lowdraglib2.gui.ui.ModularUI; +import com.lowdragmc.lowdraglib2.syncdata.IManaged; +import com.lowdragmc.lowdraglib2.syncdata.rpc.RPCSender; +import com.sorrowmist.useless.compat.neoecoae.compact.block.CompactInterfaceAccess; +import io.netty.buffer.Unpooled; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.RegistryAccess; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.entity.BlockEntity; +import org.jetbrains.annotations.Nullable; + +import java.util.BitSet; + +/** + * 紧凑方块的「通讯接口界面」全部接线,由三个宿主方块实体各持有一个实例。 + * + *

它把 {@link ShadowInterfaceBlockEntity} 的生命周期、菜单分派、LDLib2 包转发与 + * 存档桥接收在一处;宿主只需要覆写几个入口并把调用转过来。

+ * + *

包为什么要宿主转发:LDLib2 的两类包都按 {@code (pos, BlockEntityType)} 投递, + * 而世界在宿主坐标上只会给出宿主自己,所以接收端拿到包之后必须再交给影子。

+ */ +public final class CompactInterfaceAttachment> { + + /** 影子 @Persisted 状态在宿主存档里的子标签名。 */ + private static final String PERSIST_KEY = "useless_compact_interface"; + + private final BlockEntity host; + private final IManaged hostManaged; + private final NEClusterCalculator.Factory calculator; + private final ShadowNodeLink nodeLink; + + @Nullable + private ShadowInterfaceBlockEntity shadow; + @Nullable + private C joinedCluster; + + public CompactInterfaceAttachment(BlockEntity host, NEClusterCalculator.Factory calculator) { + this.host = host; + this.hostManaged = (IManaged) host; + this.calculator = calculator; + this.nodeLink = new ShadowNodeLink(host); + } + + // ---------------------------------------------------------------- 菜单分派 + + /** + * 伪装分支可用时返回通讯接口界面;否则返回 null,由宿主回退主机面板。 + * + *

分支依据只有 {@code holder.blockState} 这一个两端都一样的值,所以两端 UI 树必然同构。

+ */ + @Nullable + public ModularUI createUI(BlockUIMenuType.BlockUIHolder holder) { + if (!CompactInterfaceAccess.requested(holder.blockState)) { + return null; + } + ShadowInterfaceBlockEntity target = shadow(); + return target.supportsInterfaceUi() ? target.createUI(holder) : null; + } + + // ---------------------------------------------------------------- 成形 / 拆除 + + /** 服务端成形后调用:让影子认宿主所在的集群,并把它接上宿主的网格节点。 */ + public void attach(ServerLevel serverLevel, C cluster, IManagedGridNode node) { + ShadowInterfaceBlockEntity target = shadow(); + if (target.compactCluster() != cluster) { + target.updateCluster(cluster); + } + // 集群里那些「按类型找接口」的逻辑(存储集群的 theInterface)只认成员表, + // 而影子是手工造的,得自己进这张表。 + if (joinedCluster != cluster) { + cluster.addBlockEntity(target); + joinedCluster = cluster; + } + nodeLink.attach(target.getMainNode(), serverLevel, node); + } + + /** + * 重新装配前调用:只解除与旧集群的登记。 + * + *

影子对象必须留着——它的 {@code @Persisted} 状态(接口模式、模糊过滤器)在存档恢复时 + * 就灌进去了,而 AE2 的网格节点一旦销毁就不能重建({@code ManagedGridNode} 的初始化数据 + * 已被消费)。重新装配用的还是同一个宿主节点,连接原封不动即可。

+ */ + public void release() { + joinedCluster = null; + } + + /** 拆方块 / 卸载区块时调用:连影子节点一起摘掉,避免留下幽灵机器。 */ + public void dispose() { + nodeLink.detach(); + shadow = null; + joinedCluster = null; + } + + /** 宿主每 tick 驱动一次:补建网格节点、把影子的脏字段推给客户端、再跑影子自己的 tick。 */ + public void tick() { + ShadowInterfaceBlockEntity target = shadow; + if (target == null) { + return; + } + nodeLink.tick(); + target.getRootStorage().requireInit(); + if (target.getRootStorage().hasDirtySyncFields()) { + target.sync(false); + } + target.tick(); + } + + // ---------------------------------------------------------------- LDLib2 包转发 + + /** @return true 表示这一包属于影子接口、已由它处理,宿主不要再处理 */ + public boolean forwardRpc(RPCSender sender, byte[] data) { + ShadowInterfaceBlockEntity target = shadow; + if (target == null) { + return false; + } + String method = rpcMethodName(data); + // 包体里只有方法名,所以「谁的方法名」就是唯一的判据。宿主自己的 RPC 只有 L9 的 + // setEcoMegaFilter,与接口界面的五个方法不相交(已按运行时 21.2.0-beta1 逐个核对), + // 因此这里不会把接口界面的交互错投给主机面板。 + if (hasRpc(hostManaged, method) || !hasRpc(target, method)) { + return false; + } + target.handleRPCPacket(sender, data); + return true; + } + + /** @return true 表示这一包是影子发出的字段同步,宿主不要再处理(下标空间不同,错处理会写坏字段) */ + public boolean forwardSync(RegistryAccess access, BitSet changed, byte[] data, CompoundTag extra) { + if (!extra.getBoolean(ShadowInterfaceBlockEntity.SYNC_MARKER)) { + return false; + } + shadow().handleSyncPacket(access, changed, data, extra); + return true; + } + + /** + * Receives ECO's chunked pattern preview on the compact host position. ECO normally looks + * up an {@code ECOMachineInterfaceBlockEntity} in the world, but the compact interface is + * an in-memory shadow and therefore needs this explicit bridge. + */ + public void receivePatternPreview(CompoundTag payload) { + shadow().getPatternPreviewSync().receive(payload); + } + + /** RPC 包体 = {@code varInt(managed 下标) + utf(方法名) + 参数},这里只需要方法名。 */ + private static String rpcMethodName(byte[] data) { + FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.wrappedBuffer(data)); + try { + buf.readVarInt(); + return buf.readUtf(); + } finally { + buf.release(); + } + } + + private static boolean hasRpc(IManaged owner, String method) { + return owner.getFieldHolder().getRpcMethodMap().containsKey(method); + } + + // ---------------------------------------------------------------- 存档桥接 + + /** 影子的 @Persisted 状态(存储接口模式、63 格模糊过滤器)代存在宿主自己的存档里。 */ + public void save(HolderLookup.Provider provider, CompoundTag tag) { + if (shadow == null) { + return; + } + CompoundTag sub = new CompoundTag(); + shadow.saveManagedPersistentData(provider, sub, false); + tag.put(PERSIST_KEY, sub); + } + + public void load(HolderLookup.Provider provider, CompoundTag tag) { + if (!tag.contains(PERSIST_KEY)) { + return; + } + shadow().loadManagedPersistentData(provider, tag.getCompound(PERSIST_KEY)); + } + + // ---------------------------------------------------------------- 影子对象 + + /** + * 双端都按需建同构的影子对象:服务端在成形时建(之后由 attach 认集群、tick 建节点), + * 客户端在收到第一包或第一次开界面时建。 + */ + private ShadowInterfaceBlockEntity shadow() { + if (shadow == null) { + shadow = new ShadowInterfaceBlockEntity<>(host.getType(), host.getBlockPos(), + host.getBlockState(), calculator); + } + // 存档恢复(loadTag)早于 setLevel,客户端则不存在 attach 过程:两条时序都需要取得真实世界, + // 影子实体才能识别菜单 viewer(isViewer 会比对 level),服务端也才能在正确的维度中 tick。 + if (shadow.getLevel() != host.getLevel()) { + shadow.setLevel(host.getLevel()); + } + return shadow; + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowComputationDriveBlockEntity.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowComputationDriveBlockEntity.java new file mode 100644 index 00000000..aef1eee3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowComputationDriveBlockEntity.java @@ -0,0 +1,41 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.shadow; + +import appeng.api.orientation.BlockOrientation; +import cn.dancingsnow.neoecoae.blocks.entity.computation.ECOComputationDriveBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; + +import java.util.EnumSet; +import java.util.Set; + +/** + * 「影子晶阵驱动器」:不落世界的 C9 计算驱动器,装着满配的 CE9 闪存晶阵。 + * + *

紧凑 C9 需要它满足「上下驱动器全部装满有效闪存晶阵」这一极限模式前置条件, + * 同时为合成任务提供 ECO 原生的 CPU 存储字节。

+ */ +public class ShadowComputationDriveBlockEntity extends ECOComputationDriveBlockEntity { + + public ShadowComputationDriveBlockEntity(BlockEntityType type, BlockPos pos, BlockState blockState) { + super(type, pos, blockState); + } + + @Override + public void updateState(boolean updateExposed) { + } + + @Override + public void markForUpdate() { + } + + @Override + public void saveChanges() { + } + + @Override + public Set getGridConnectableSides(BlockOrientation orientation) { + return EnumSet.noneOf(Direction.class); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowComputationHostBlockEntity.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowComputationHostBlockEntity.java new file mode 100644 index 00000000..98fdcb08 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowComputationHostBlockEntity.java @@ -0,0 +1,64 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.shadow; + +import appeng.api.orientation.BlockOrientation; +import cn.dancingsnow.neoecoae.api.ECOTier; +import cn.dancingsnow.neoecoae.blocks.entity.computation.ECOComputationSystemBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; + +import java.util.EnumSet; +import java.util.Set; + +/** + * 「影子主机」:一个不落在世界里的 C9 计算主机,只用来让 ECO 的 + * {@code NEComputationNetworkCluster#isEndgameEligible()} 认满 8 台满配主机。 + * + *

它把等级、建造长度、网络交换模块三项固定成满配值,其余全部沿用 ECO 的原逻辑。

+ */ +public class ShadowComputationHostBlockEntity extends ECOComputationSystemBlockEntity { + + public ShadowComputationHostBlockEntity(BlockEntityType type, BlockPos pos, BlockState blockState) { + super(type, pos, blockState, ECOTier.L9); + } + + @Override + public boolean hasNormalNetworkSwitch() { + return false; + } + + @Override + public boolean hasHighEnergyNetworkSwitch() { + return true; + } + + @Override + public int getSelectedBuildLength() { + // 紧凑主机永远是「满长」建造:极限模式判定要求 selected == max。 + return getMaxBuildLength(); + } + + @Override + public void setSelectedBuildLength(int length) { + // 固定配置,忽略外部改写。 + } + + @Override + public void updateState(boolean updateExposed) { + // 影子组件不存在于世界,屏蔽一切方块状态写入。 + } + + @Override + public void markForUpdate() { + } + + @Override + public void saveChanges() { + } + + @Override + public Set getGridConnectableSides(BlockOrientation orientation) { + return EnumSet.noneOf(Direction.class); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowComputationParallelCoreBlockEntity.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowComputationParallelCoreBlockEntity.java new file mode 100644 index 00000000..98b50e58 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowComputationParallelCoreBlockEntity.java @@ -0,0 +1,39 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.shadow; + +import appeng.api.orientation.BlockOrientation; +import cn.dancingsnow.neoecoae.api.ECOTier; +import cn.dancingsnow.neoecoae.blocks.entity.computation.ECOComputationParallelCoreBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; + +import java.util.EnumSet; +import java.util.Set; + +/** + * 「影子并行核心」:不落世界的 C9 计算并行核心,为紧凑 C9 贡献 ECO 原生的并行(加速器)数值。 + */ +public class ShadowComputationParallelCoreBlockEntity extends ECOComputationParallelCoreBlockEntity { + + public ShadowComputationParallelCoreBlockEntity(BlockEntityType type, BlockPos pos, BlockState blockState) { + super(type, pos, blockState, ECOTier.L9); + } + + @Override + public void updateState(boolean updateExposed) { + } + + @Override + public void markForUpdate() { + } + + @Override + public void saveChanges() { + } + + @Override + public Set getGridConnectableSides(BlockOrientation orientation) { + return EnumSet.noneOf(Direction.class); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowCraftingHostBlockEntity.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowCraftingHostBlockEntity.java new file mode 100644 index 00000000..d02ea506 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowCraftingHostBlockEntity.java @@ -0,0 +1,62 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.shadow; + +import appeng.api.orientation.BlockOrientation; +import cn.dancingsnow.neoecoae.api.ECOTier; +import cn.dancingsnow.neoecoae.blocks.entity.crafting.ECOCraftingSystemBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; + +import java.util.EnumSet; +import java.util.Set; + +/** + * 「影子主机」:一个不落在世界里的 F9 合成主机,只用来让 ECO 的 + * {@code CraftingCapabilitySnapshot#isVirtualTopologyEligible()} 认满 8 台满长高能主机。 + * + *

它把等级、建造长度、网络交换模块固定成满配值,其余全部沿用 ECO 原逻辑。

+ */ +public class ShadowCraftingHostBlockEntity extends ECOCraftingSystemBlockEntity { + + public ShadowCraftingHostBlockEntity(BlockEntityType type, BlockPos pos, BlockState blockState) { + super(type, pos, blockState, ECOTier.L9); + } + + @Override + public boolean hasNormalNetworkSwitch() { + return false; + } + + @Override + public boolean hasHighEnergyNetworkSwitch() { + return true; + } + + @Override + public int getSelectedBuildLength() { + // 紧凑主机永远是「满长」建造:虚拟合成模式判定要求实际 FX 通道数 == 满长值。 + return getMaxBuildLength(); + } + + @Override + public void setSelectedBuildLength(int length) { + } + + @Override + public void updateState(boolean updateExposed) { + } + + @Override + public void markForUpdate() { + } + + @Override + public void saveChanges() { + } + + @Override + public Set getGridConnectableSides(BlockOrientation orientation) { + return EnumSet.noneOf(Direction.class); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowCraftingParallelCoreBlockEntity.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowCraftingParallelCoreBlockEntity.java new file mode 100644 index 00000000..2f309813 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowCraftingParallelCoreBlockEntity.java @@ -0,0 +1,39 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.shadow; + +import appeng.api.orientation.BlockOrientation; +import cn.dancingsnow.neoecoae.api.ECOTier; +import cn.dancingsnow.neoecoae.blocks.entity.crafting.ECOCraftingParallelCoreBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; + +import java.util.EnumSet; +import java.util.Set; + +/** + * 「影子并行核心」:不落世界的 F9 并行核心,为紧凑 F9 贡献 ECO 原生的 FT 并行线程能力。 + */ +public class ShadowCraftingParallelCoreBlockEntity extends ECOCraftingParallelCoreBlockEntity { + + public ShadowCraftingParallelCoreBlockEntity(BlockEntityType type, BlockPos pos, BlockState blockState) { + super(type, pos, blockState, ECOTier.L9); + } + + @Override + public void updateState(boolean updateExposed) { + } + + @Override + public void markForUpdate() { + } + + @Override + public void saveChanges() { + } + + @Override + public Set getGridConnectableSides(BlockOrientation orientation) { + return EnumSet.noneOf(Direction.class); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowCraftingWorkerBlockEntity.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowCraftingWorkerBlockEntity.java new file mode 100644 index 00000000..026dd40f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowCraftingWorkerBlockEntity.java @@ -0,0 +1,51 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.shadow; + +import appeng.api.orientation.BlockOrientation; +import appeng.api.networking.IGridNode; +import appeng.api.networking.ticking.TickRateModulation; +import cn.dancingsnow.neoecoae.blocks.entity.crafting.ECOCraftingWorkerBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; + +import java.util.EnumSet; +import java.util.Set; + +/** + * 「影子工作核心」:不落世界的 F9 工作核心(FX 执行通道)。 + * + *

它的网格节点只用于让 ECO 线程访问宿主网格,实际执行由紧凑 F9 主机在每个服务器 tick + * 手动驱动 ECO 原生的 {@code tickingRequest(...)},避免网格 ticker 重复推进。

+ */ +public class ShadowCraftingWorkerBlockEntity extends ECOCraftingWorkerBlockEntity { + + public ShadowCraftingWorkerBlockEntity(BlockEntityType type, BlockPos pos, BlockState blockState) { + super(type, pos, blockState); + getMainNode().setFlags(); + getMainNode().setInWorldNode(false); + } + + @Override + public TickRateModulation tickingRequest(IGridNode node, int ticksSinceLastCall) { + // The host drives shadow workers explicitly after their nodes join the host grid. + return node == null ? super.tickingRequest(null, ticksSinceLastCall) : TickRateModulation.IDLE; + } + + @Override + public void updateState(boolean updateExposed) { + } + + @Override + public void markForUpdate() { + } + + @Override + public void saveChanges() { + } + + @Override + public Set getGridConnectableSides(BlockOrientation orientation) { + return EnumSet.noneOf(Direction.class); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowFluidInputHatchBlockEntity.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowFluidInputHatchBlockEntity.java new file mode 100644 index 00000000..6cc8c704 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowFluidInputHatchBlockEntity.java @@ -0,0 +1,49 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.shadow; + +import appeng.api.orientation.BlockOrientation; +import cn.dancingsnow.neoecoae.blocks.entity.crafting.ECOFluidInputHatchBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; + +import java.util.EnumSet; +import java.util.Set; + +/** + * 不落地的紧凑 F9 冷却液输入仓。 + * + *

它只提供 ECO 合成集群识别的 {@code tank},流体自动搬运和世界同步都由紧凑 F9 + * 自己接管,因此不能把它当作一个会在世界中扫描邻居的普通输入仓。

+ */ +public class ShadowFluidInputHatchBlockEntity extends ECOFluidInputHatchBlockEntity { + + public ShadowFluidInputHatchBlockEntity(BlockEntityType type, BlockPos pos, BlockState blockState) { + super(type, pos, blockState); + getMainNode().setFlags(); + getMainNode().setInWorldNode(false); + } + + @Override + public void tick(Level level, BlockPos pos, BlockState state) { + // The compact host exposes the tank through its own block capability. + } + + @Override + public void updateState(boolean updateExposed) { + } + + @Override + public void markForUpdate() { + } + + @Override + public void saveChanges() { + } + + @Override + public Set getGridConnectableSides(BlockOrientation orientation) { + return EnumSet.noneOf(Direction.class); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowFluidOutputHatchBlockEntity.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowFluidOutputHatchBlockEntity.java new file mode 100644 index 00000000..594dbbd3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowFluidOutputHatchBlockEntity.java @@ -0,0 +1,44 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.shadow; + +import appeng.api.orientation.BlockOrientation; +import cn.dancingsnow.neoecoae.blocks.entity.crafting.ECOFluidOutputHatchBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; + +import java.util.EnumSet; +import java.util.Set; + +/** 不落地的紧凑 F9 冷却液输出仓。 */ +public class ShadowFluidOutputHatchBlockEntity extends ECOFluidOutputHatchBlockEntity { + + public ShadowFluidOutputHatchBlockEntity(BlockEntityType type, BlockPos pos, BlockState blockState) { + super(type, pos, blockState); + getMainNode().setFlags(); + getMainNode().setInWorldNode(false); + } + + @Override + public void tick(Level level, BlockPos pos, BlockState state) { + // The compact host exposes the tank through its own block capability. + } + + @Override + public void updateState(boolean updateExposed) { + } + + @Override + public void markForUpdate() { + } + + @Override + public void saveChanges() { + } + + @Override + public Set getGridConnectableSides(BlockOrientation orientation) { + return EnumSet.noneOf(Direction.class); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowInterfaceBlockEntity.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowInterfaceBlockEntity.java new file mode 100644 index 00000000..040759d0 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowInterfaceBlockEntity.java @@ -0,0 +1,76 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.shadow; + +import appeng.api.orientation.BlockOrientation; +import cn.dancingsnow.neoecoae.blocks.entity.ECOMachineInterfaceBlockEntity; +import cn.dancingsnow.neoecoae.multiblock.calculator.NEClusterCalculator; +import cn.dancingsnow.neoecoae.multiblock.cluster.NECluster; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; + +import java.util.EnumSet; +import java.util.Set; + +/** + * 「影子通讯接口」:不落世界、但复用宿主身份的 ECO 通讯接口方块实体。 + * + *

为什么要复用宿主身份:LDLib2 的 RPC 包与字段同步包都按 + * {@code (BlockPos, BlockEntityType 实例)} 寻址,接收端还要做 + * {@code be.getType() == packet.blockEntityType} 的引用比较。所以这个影子对象必须用 + * 宿主自己的 {@link BlockEntityType} 与宿主坐标构造,包才会先投到宿主, + * 再由宿主 {@code CompactInterfaceAttachment} 转发进来。

+ * + *

为什么用 calculator 而不是 cluster 判定界面类型:{@code supportsXxxInterfaceUi()} + * 是「cluster instanceof 或 calculator instanceof」,而 cluster 只存在于服务端, + * calculator 是构造器注入、两端都有 ⇒ 两端界面分派必然一致(这正是第 6 轮崩溃的教训)。

+ */ +public class ShadowInterfaceBlockEntity> extends ECOMachineInterfaceBlockEntity { + + /** 影子同步包的身份标记:宿主靠它在自己的同步包里认出「这一包是影子的」。 */ + public static final String SYNC_MARKER = "useless_compact_interface_sync"; + + public ShadowInterfaceBlockEntity(BlockEntityType type, BlockPos pos, BlockState blockState, + NEClusterCalculator.Factory calculator) { + super(type, pos, blockState, calculator); + // 一个 flag 都不要:影子节点额外吃通道时,通道不足会让 AE2 的 isActive() 为假, + // grid.getActiveMachines(...) 就会漏掉它(界面样板列表与模糊过滤全空)。 + getMainNode().setFlags(); + // 影子节点不进世界也不做邻居扫描:它靠宿主节点显式连接入网, + // 以避免每个影子节点加载邻居区块、并将自身登记为占用该区块的网络节点。 + getMainNode().setInWorldNode(false); + } + + /** 供接线层判断缝合是否还有效:{@code NECluster.destroy()} 会把成员的 cluster 置空。 */ + public C compactCluster() { + return cluster; + } + + @Override + public synchronized void writeCustomSyncData(HolderLookup.Provider provider, CompoundTag tag) { + super.writeCustomSyncData(provider, tag); + tag.putBoolean(SYNC_MARKER, true); + } + + // ------------------------------------------------ 影子卫生:绝不回写世界 + // 这些入口在 ECO 里都会按 worldPosition 改宿主坐标上的真实方块,影子必须掐掉。 + + @Override + public void updateState(boolean updateExposed) { + } + + @Override + public void markForUpdate() { + } + + @Override + public void saveChanges() { + } + + @Override + public Set getGridConnectableSides(BlockOrientation orientation) { + return EnumSet.noneOf(Direction.class); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowNodeLink.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowNodeLink.java new file mode 100644 index 00000000..22fbc61c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowNodeLink.java @@ -0,0 +1,73 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.shadow; + +import appeng.api.networking.GridHelper; +import appeng.api.networking.IGridNode; +import appeng.api.networking.IManagedGridNode; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.entity.BlockEntity; + +/** + * 把一个「不落世界的影子节点」接到宿主节点上。 + * + *

影子节点不进世界,永远不会被 AE2 的邻居扫描发现,所以必须显式 + * {@link GridHelper#createConnection}。而 AE2 的节点只能创建一次、同一条连接也只能建一次 + * (重复创建会抛 {@code IllegalStateException}),所以这里把「等宿主节点就绪 → 建节点 → + * 建连」收敛成一个每 tick 幂等的入口。

+ * + *

销毁节点会连带销毁它自己的连接,因此 {@link #detach()} 不需要单独断连。

+ */ +public final class ShadowNodeLink { + + private final BlockEntity host; + private IManagedGridNode target; + private IManagedGridNode hostNode; + private ServerLevel level; + private boolean connected; + + public ShadowNodeLink(BlockEntity host) { + this.host = host; + } + + /** 记录要接上来的目标节点;换目标即重来一次。宿主节点要等 AE2 的网格 tick 才就绪,先收下即可。 */ + public void attach(IManagedGridNode target, ServerLevel level, IManagedGridNode hostNode) { + this.target = target; + this.level = level; + this.hostNode = hostNode; + this.connected = false; + } + + /** 拆方块 / 卸载区块 / 重新装配时调用:连节点一起摘掉,避免留下幽灵机器。 */ + public void detach() { + if (target != null && target.isReady()) { + target.destroy(); + } + target = null; + hostNode = null; + level = null; + connected = false; + } + + /** 宿主每 tick 调一次,直到真的连上;之后是空操作。 */ + public void tick() { + if (connected || target == null || hostNode == null || level == null) { + return; + } + IGridNode anchor = hostNode.getNode(); + if (anchor == null) { + return; + } + if (!target.isReady()) { + target.create(level, host.getBlockPos()); + } + IGridNode attached = target.getNode(); + if (attached == null) { + return; + } + try { + GridHelper.createConnection(anchor, attached); + } catch (IllegalStateException e) { + // 已经连上了:对新节点而言这和刚连上没有区别。 + } + connected = true; + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowPatternBusBlockEntity.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowPatternBusBlockEntity.java new file mode 100644 index 00000000..ff0fd50d --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowPatternBusBlockEntity.java @@ -0,0 +1,52 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.shadow; + +import appeng.api.networking.IGrid; +import appeng.api.networking.IGridNode; +import appeng.api.orientation.BlockOrientation; +import cn.dancingsnow.neoecoae.blocks.entity.crafting.ECOCraftingPatternBusBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; + +import java.util.EnumSet; +import java.util.Set; + +/** + * 「影子样板总线」:不落世界的 F9 智能样板总线。 + * + *

它保留 ECO 原生的样板库存、样板解析与派单逻辑;紧凑 F9 会把它的 + * {@code ICraftingProvider} / {@code IECOPatternStorage} 服务挂到主机的网格节点上, + * 于是玩家往紧凑方块里插的样板依然由 ECO 自己的总线实现去解析与派单。

+ */ +public class ShadowPatternBusBlockEntity extends ECOCraftingPatternBusBlockEntity { + + public ShadowPatternBusBlockEntity(BlockEntityType type, BlockPos pos, BlockState blockState) { + super(type, pos, blockState); + } + + @Override + @Nullable + public IGrid getGrid() { + IGridNode node = getGridNode(); + return node == null ? null : node.getGrid(); + } + + @Override + public void updateState(boolean updateExposed) { + } + + @Override + public void markForUpdate() { + } + + @Override + public void saveChanges() { + } + + @Override + public Set getGridConnectableSides(BlockOrientation orientation) { + return EnumSet.noneOf(Direction.class); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowThreadingCoreBlockEntity.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowThreadingCoreBlockEntity.java new file mode 100644 index 00000000..cf7bed8f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/shadow/ShadowThreadingCoreBlockEntity.java @@ -0,0 +1,41 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.shadow; + +import appeng.api.orientation.BlockOrientation; +import cn.dancingsnow.neoecoae.api.ECOTier; +import cn.dancingsnow.neoecoae.blocks.entity.computation.ECOComputationThreadingCoreBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; + +import java.util.EnumSet; +import java.util.Set; + +/** + * 「影子线程核心」:不落世界的 C9 线程核心,为紧凑 C9 提供真实的 ECO 合成线程。 + * + *

它永远是 C9 等级(每颗 64 线程),内核 {@code spawn()} / {@code deactivate()} 完全走 ECO 原逻辑。

+ */ +public class ShadowThreadingCoreBlockEntity extends ECOComputationThreadingCoreBlockEntity { + + public ShadowThreadingCoreBlockEntity(BlockEntityType type, BlockPos pos, BlockState blockState) { + super(type, pos, blockState, ECOTier.L9); + } + + @Override + public void updateState(boolean updateExposed) { + } + + @Override + public void markForUpdate() { + } + + @Override + public void saveChanges() { + } + + @Override + public Set getGridConnectableSides(BlockOrientation orientation) { + return EnumSet.noneOf(Direction.class); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/ui/CompactSingleBlockUI.java b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/ui/CompactSingleBlockUI.java new file mode 100644 index 00000000..1dd864ca --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neoecoae/compact/ui/CompactSingleBlockUI.java @@ -0,0 +1,42 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.ui; + +import com.lowdragmc.lowdraglib2.gui.ui.ModularUI; +import com.lowdragmc.lowdraglib2.gui.ui.UI; +import com.lowdragmc.lowdraglib2.gui.ui.UIElement; +import com.lowdragmc.lowdraglib2.gui.ui.data.TextWrap; +import com.lowdragmc.lowdraglib2.gui.ui.elements.TextElement; +import com.lowdragmc.lowdraglib2.gui.ui.elements.inventory.InventorySlots; +import com.lowdragmc.lowdraglib2.gui.ui.style.StylesheetManager; +import cn.dancingsnow.neoecoae.gui.theme.NEStyleSheets; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.player.Player; + +import java.util.List; + +/** + * Small single-block overview used by compact hosts. + * + *

The ECO controller screens contain a multiblock builder and a structure preview. Those + * controls are meaningless for a compact block, so the overview deliberately exposes only + * the fixed hardware, pooled capacity, and the player inventory. Detailed management remains + * on the sneaking interface entry and on the dedicated fluid/pattern item interactions.

+ */ +public final class CompactSingleBlockUI { + private CompactSingleBlockUI() {} + + public static ModularUI create(Player player, Component title, Component... lines) { + UIElement root = new UIElement().addClass("panel_bg"); + root.addChild(new TextElement().setText(title).textStyle(style -> style + .adaptiveWidth(true).adaptiveHeight(true).textWrap(TextWrap.NONE) + .textColor(0x3F3D52).textShadow(false))); + for (Component line : lines) { + root.addChild(new TextElement().setText(line).textStyle(style -> style + .adaptiveWidth(true).adaptiveHeight(true).textWrap(TextWrap.HOVER_ROLL) + .textColor(0x555266).textShadow(false))); + } + InventorySlots inventory = new InventorySlots(); + root.addChild(inventory); + return new ModularUI(UI.of(root, + List.of(StylesheetManager.INSTANCE.getStylesheetSafe(NEStyleSheets.ECO))), player); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neovitae/NeoVitaeDynamicPatternSupport.java b/src/main/java/com/sorrowmist/useless/compat/neovitae/NeoVitaeDynamicPatternSupport.java new file mode 100644 index 00000000..f7143588 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neovitae/NeoVitaeDynamicPatternSupport.java @@ -0,0 +1,568 @@ +package com.sorrowmist.useless.compat.neovitae; + +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.GenericStack; +import com.breakinblocks.neovitae.api.recipe.AraVitaeInput; +import com.breakinblocks.neovitae.api.recipe.AraVitaeRecipe; +import com.breakinblocks.neovitae.common.datacomponent.NVDataComponents; +import com.breakinblocks.neovitae.common.datamap.NVDataMaps; +import com.breakinblocks.neovitae.common.recipe.NVRecipes; +import com.breakinblocks.neovitae.common.recipe.athanor.AthanorPotionRecipe; +import com.breakinblocks.neovitae.common.recipe.athanor.AthanorRecipe; +import com.breakinblocks.neovitae.common.recipe.forge.ForgeInput; +import com.breakinblocks.neovitae.common.recipe.forge.ForgeRecipe; +import com.breakinblocks.neovitae.common.recipe.forge.ForgeSpiritusInfusionRecipe; +import com.breakinblocks.neovitae.common.recipe.forge.ForgeTransformRecipe; +import com.breakinblocks.neovitae.common.recipe.forge.ForgeUpgradeRecipe; +import com.mojang.datafixers.util.Pair; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.DynamicComponentPatternDetails; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.DynamicPatternProfile; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.PatternStackView; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.ItemIngredientAllocator; +import com.sorrowmist.useless.content.recipe.adapters.neovitae.HellfireForgeRecipeAdapter; +import net.minecraft.core.component.DataComponents; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.alchemy.PotionContents; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; + +/** Optional Neo Vitae support for component-aware AE processing patterns. */ +public final class NeoVitaeDynamicPatternSupport { + private NeoVitaeDynamicPatternSupport() { + } + + public static Optional findDynamicPatternProfile( + Level level, List patternInputs, List patternOutputs) { + return findDynamicPatternProfileLong(level, PatternStackView.fromLegacy(patternInputs, patternOutputs)); + } + + public static Optional findDynamicPatternProfileLong( + Level level, @Nullable PatternStackView pattern) { + if (level == null || pattern == null || pattern.inputs().isEmpty() + || pattern.outputs().isEmpty() || !validItemStacks(pattern)) { + return Optional.empty(); + } + + List matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(level.getRecipeManager(), NVRecipes.HELLFIRE_FORGE_TYPE.get())) { + DynamicPatternProfile profile = forgeProfileLong(holder, level, pattern); + if (profile != null && addUnique(matches, profile)) return Optional.empty(); + } + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(level.getRecipeManager(), NVRecipes.ATHANOR_TYPE.get())) { + DynamicPatternProfile profile = athanorProfileLong(holder, level, pattern); + if (profile != null && addUnique(matches, profile)) return Optional.empty(); + } + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(level.getRecipeManager(), NVRecipes.ARA_VITAE_TYPE.get())) { + DynamicPatternProfile profile = araProfileLong(holder, level, pattern); + if (profile != null && addUnique(matches, profile)) return Optional.empty(); + } + return matches.isEmpty() ? Optional.empty() : Optional.of(matches.get(0)); + } + + private static boolean addUnique(List matches, + DynamicPatternProfile profile) { + matches.add(profile); + return matches.size() > 1; + } + + @Nullable + private static DynamicPatternProfile forgeProfile( + RecipeHolder holder, Level level, + List patternInputs, List patternOutputs) { + ForgeRecipe source = holder == null ? null : holder.value(); + if (source instanceof ForgeUpgradeRecipe upgrade) { + return upgradeProfile(upgrade, level, patternInputs, patternOutputs); + } + if (source instanceof ForgeSpiritusInfusionRecipe infusion) { + return infusionProfile(infusion, level, patternInputs, patternOutputs); + } + if (source instanceof ForgeTransformRecipe transform) { + return transformProfile(transform, level, patternInputs, patternOutputs); + } + return null; + } + + @Nullable + private static DynamicPatternProfile forgeProfileLong( + RecipeHolder holder, Level level, PatternStackView pattern) { + ForgeRecipe source = holder == null ? null : holder.value(); + if (source instanceof ForgeUpgradeRecipe upgrade) { + return upgradeProfileLong(upgrade, level, pattern); + } + if (source instanceof ForgeSpiritusInfusionRecipe infusion) { + return infusionProfileLong(infusion, level, pattern); + } + if (source instanceof ForgeTransformRecipe transform) { + return transformProfileLong(transform, level, pattern); + } + return null; + } + + @Nullable + private static DynamicPatternProfile transformProfileLong( + ForgeTransformRecipe source, Level level, PatternStackView pattern) { + List catalysts = counted(source.getCatalysts()); + int targetSlot = uniqueDynamicSlotLong(pattern, source.getTransformInput(), catalysts); + if (targetSlot < 0) return null; + + ItemStack output = assemble(source, pattern.inputRepresentatives(), forgeGem(), 4, level); + if (!matchesSingleOutputLong(output, pattern.outputs())) return null; + return profile(Set.of(targetSlot), Set.of(0), + Map.of(targetSlot, ingredientMatcher(source.getTransformInput()))); + } + + @Nullable + private static DynamicPatternProfile upgradeProfileLong( + ForgeUpgradeRecipe source, Level level, PatternStackView pattern) { + List catalysts = counted(source.getCraftingIngredients()); + int targetSlot = uniqueDynamicSlotLong( + pattern, HellfireForgeRecipeAdapter::isUpgradeTarget, catalysts); + if (targetSlot < 0) return null; + + ItemStack output = assemble(source, pattern.inputRepresentatives(), forgeGem(), 4, level); + if (!matchesSingleOutputLong(output, pattern.outputs())) return null; + return profile(Set.of(targetSlot), Set.of(0), + Map.of(targetSlot, itemMatcher(HellfireForgeRecipeAdapter::isUpgradeTarget))); + } + + @Nullable + private static DynamicPatternProfile infusionProfileLong( + ForgeSpiritusInfusionRecipe source, Level level, PatternStackView pattern) { + if (pattern.inputs().size() != 2) return null; + List representatives = pattern.inputRepresentatives(); + int gemSlot = uniqueItemSlotLong(pattern, + stack -> source.getGemInput().test(stack) && isSpiritusGem(stack)); + int targetSlot = uniqueItemSlotLong(pattern, HellfireForgeRecipeAdapter::isInfusionTarget); + if (gemSlot < 0 || targetSlot < 0 || gemSlot == targetSlot) return null; + + ForgeInput input = new ForgeInput(representatives, representatives.get(gemSlot), gemSlot); + if (!source.matches(input, level)) return null; + ItemStack output = assemble(source, representatives, + representatives.get(gemSlot), gemSlot, level); + if (!matchesSingleOutputLong(output, pattern.outputs())) return null; + + return profile(Set.of(gemSlot, targetSlot), Set.of(0), Map.of( + gemSlot, itemMatcher(stack -> source.getGemInput().test(stack) + && isSpiritusGem(stack)), + targetSlot, itemMatcher(HellfireForgeRecipeAdapter::isInfusionTarget))); + } + + @Nullable + private static DynamicPatternProfile athanorProfileLong( + RecipeHolder holder, Level level, PatternStackView pattern) { + AthanorRecipe source = holder == null ? null : holder.value(); + if (!(source instanceof AthanorPotionRecipe potion) || source.getInputs().isEmpty()) return null; + + List requirements = counted(source.getInputs()); + if (!matchesPatternInputsLong(requirements, pattern)) return null; + int potionSlot = uniqueItemSlotLong(pattern, source.getInputs().get(0)::test); + if (potionSlot < 0) return null; + + PotionContents contents = pattern.inputRepresentatives().get(potionSlot) + .get(DataComponents.POTION_CONTENTS); + List expected = potionOutputs(source, contents); + if (!matchesDynamicOutputsLong(expected, pattern.outputs())) return null; + + Set outputSlots = new LinkedHashSet<>(); + for (int slot = 0; slot < expected.size(); slot++) outputSlots.add(slot); + return profile(Set.of(potionSlot), outputSlots, + Map.of(potionSlot, itemMatcher(source.getInputs().get(0)::test))); + } + + @Nullable + private static DynamicPatternProfile araProfileLong( + RecipeHolder holder, Level level, PatternStackView pattern) { + AraVitaeRecipe source = holder == null ? null : holder.value(); + if (source == null || !source.shouldCopyInputComponents() + || !matchesPatternInputsLong(counted(List.of(source.getInput())), pattern)) { + return null; + } + + int inputSlot = uniqueItemSlotLong(pattern, source.getInput()::test); + if (inputSlot < 0 || !matchesSingleOutputLong(source.getResult(), pattern.outputs())) return null; + + ItemStack output; + try { + output = source.assemble(new AraVitaeInput( + pattern.inputRepresentatives().get(inputSlot), 0), level.registryAccess()); + } catch (RuntimeException exception) { + return null; + } + if (!matchesSingleOutputLong(output, pattern.outputs())) return null; + return profile(Set.of(inputSlot), Set.of(0), + Map.of(inputSlot, itemMatcher(source.getInput()::test))); + } + + @Nullable + private static DynamicPatternProfile transformProfile( + ForgeTransformRecipe source, Level level, + List patternInputs, List patternOutputs) { + List catalysts = counted(source.getCatalysts()); + int targetSlot = uniqueDynamicSlot(patternInputs, source.getTransformInput(), catalysts); + if (targetSlot < 0 || !matchesWithOneMore(catalysts, patternInputs)) return null; + + ItemStack output = assemble(source, patternInputs, forgeGem(), 4, level); + if (!matchesSingleOutput(output, patternOutputs)) return null; + return profile(Set.of(targetSlot), Set.of(0), + Map.of(targetSlot, ingredientMatcher(source.getTransformInput()))); + } + + @Nullable + private static DynamicPatternProfile upgradeProfile( + ForgeUpgradeRecipe source, Level level, + List patternInputs, List patternOutputs) { + List catalysts = counted(source.getCraftingIngredients()); + int targetSlot = uniqueDynamicSlot(patternInputs, + HellfireForgeRecipeAdapter::isUpgradeTarget, catalysts); + if (targetSlot < 0 || !matchesWithOneMore(catalysts, patternInputs)) return null; + + ItemStack output = assemble(source, patternInputs, forgeGem(), 4, level); + if (!matchesSingleOutput(output, patternOutputs)) return null; + return profile(Set.of(targetSlot), Set.of(0), + Map.of(targetSlot, itemMatcher(HellfireForgeRecipeAdapter::isUpgradeTarget))); + } + + @Nullable + private static DynamicPatternProfile infusionProfile( + ForgeSpiritusInfusionRecipe source, Level level, + List patternInputs, List patternOutputs) { + if (patternInputs.size() != 2) return null; + + int gemSlot = uniqueItemSlot(patternInputs, + stack -> source.getGemInput().test(stack) && isSpiritusGem(stack)); + int targetSlot = uniqueItemSlot(patternInputs, + HellfireForgeRecipeAdapter::isInfusionTarget); + if (gemSlot < 0 || targetSlot < 0 || gemSlot == targetSlot + || patternInputs.get(gemSlot).getCount() != 1 + || patternInputs.get(targetSlot).getCount() != 1) { + return null; + } + + ForgeInput input = new ForgeInput(patternInputs, + patternInputs.get(gemSlot), gemSlot); + if (!source.matches(input, level)) return null; + ItemStack output = assemble(source, patternInputs, + patternInputs.get(gemSlot), gemSlot, level); + if (!matchesSingleOutput(output, patternOutputs)) return null; + + return profile(Set.of(gemSlot, targetSlot), Set.of(0), Map.of( + gemSlot, itemMatcher(stack -> source.getGemInput().test(stack) + && isSpiritusGem(stack)), + targetSlot, itemMatcher(HellfireForgeRecipeAdapter::isInfusionTarget))); + } + + @Nullable + private static DynamicPatternProfile athanorProfile( + RecipeHolder holder, Level level, + List patternInputs, List patternOutputs) { + AthanorRecipe source = holder == null ? null : holder.value(); + if (!(source instanceof AthanorPotionRecipe potion) + || source.getInputs().isEmpty()) return null; + + List requirements = counted(source.getInputs()); + if (!matchesPatternInputs(requirements, patternInputs)) return null; + int potionSlot = uniqueItemSlot(patternInputs, + source.getInputs().get(0)::test); + if (potionSlot < 0) return null; + + PotionContents contents = patternInputs.get(potionSlot).get(DataComponents.POTION_CONTENTS); + List expected = potionOutputs(source, contents); + if (!matchesDynamicOutputs(expected, patternOutputs)) return null; + + Set outputSlots = new LinkedHashSet<>(); + for (int slot = 0; slot < expected.size(); slot++) outputSlots.add(slot); + return profile(Set.of(potionSlot), outputSlots, + Map.of(potionSlot, itemMatcher(source.getInputs().get(0)::test))); + } + + @Nullable + private static DynamicPatternProfile araProfile( + RecipeHolder holder, Level level, + List patternInputs, List patternOutputs) { + AraVitaeRecipe source = holder == null ? null : holder.value(); + if (source == null || !source.shouldCopyInputComponents() + || !matchesPatternInputs(counted(List.of(source.getInput())), patternInputs)) { + return null; + } + + int inputSlot = uniqueItemSlot(patternInputs, source.getInput()::test); + if (inputSlot < 0 || !matchesSingleOutput(source.getResult(), patternOutputs)) return null; + + ItemStack output; + try { + output = source.assemble(new AraVitaeInput(patternInputs.get(inputSlot), 0), + level.registryAccess()); + } catch (RuntimeException exception) { + return null; + } + if (!matchesSingleOutput(output, patternOutputs)) return null; + return profile(Set.of(inputSlot), Set.of(0), + Map.of(inputSlot, itemMatcher(source.getInput()::test))); + } + + private static DynamicPatternProfile profile( + Set inputSlots, Set outputSlots, + Map matchers) { + return new DynamicPatternProfile(Map.of(), inputSlots, outputSlots, matchers); + } + + private static List counted(List ingredients) { + return AdapterUtils.mergeIngredients(ingredients); + } + + private static boolean matchesWithOneMore( + List staticRequirements, List inputs) { + long required = totalRequired(staticRequirements); + return required < Long.MAX_VALUE && required + 1L == totalItems(inputs); + } + + private static boolean matchesPatternInputs( + List requirements, List inputs) { + return totalRequired(requirements) == totalItems(inputs) + && ItemIngredientAllocator.matches(requirements, inputs, 1L); + } + + private static boolean matchesPatternInputsLong( + List requirements, PatternStackView pattern) { + return totalRequired(requirements) == totalItemsLong(pattern.inputs()) + && ItemIngredientAllocator.matches(requirements, List.of(), pattern.inputs(), 1L); + } + + private static int uniqueDynamicSlotLong( + PatternStackView pattern, Ingredient dynamic, + List staticRequirements) { + if (dynamic == null || dynamic.isEmpty()) return -1; + return uniqueDynamicSlotLong(pattern, dynamic::test, staticRequirements); + } + + private static int uniqueDynamicSlotLong( + PatternStackView pattern, Predicate dynamic, + List staticRequirements) { + List representatives = pattern.inputRepresentatives(); + int result = -1; + for (int slot = 0; slot < representatives.size(); slot++) { + ItemStack input = representatives.get(slot); + if (input == null || input.isEmpty() || !dynamic.test(input)) continue; + List remaining = new ArrayList<>(pattern.inputs()); + remaining.remove(slot); + if (totalRequired(staticRequirements) != totalItemsLong(remaining) + || !ItemIngredientAllocator.matches(staticRequirements, List.of(), remaining, 1L)) { + continue; + } + if (result >= 0) return -1; + result = slot; + } + return result; + } + + private static int uniqueItemSlotLong( + PatternStackView pattern, Predicate predicate) { + List representatives = pattern.inputRepresentatives(); + int result = -1; + for (int slot = 0; slot < representatives.size(); slot++) { + ItemStack input = representatives.get(slot); + if (input == null || input.isEmpty() || !predicate.test(input)) continue; + if (result >= 0) return -1; + result = slot; + } + return result; + } + + private static int uniqueDynamicSlot( + List inputs, Ingredient dynamic, List staticRequirements) { + if (dynamic == null || dynamic.isEmpty()) return -1; + return uniqueDynamicSlot(inputs, dynamic::test, staticRequirements); + } + + private static int uniqueDynamicSlot( + List inputs, Predicate dynamic, + List staticRequirements) { + int result = -1; + for (int slot = 0; slot < inputs.size(); slot++) { + ItemStack input = inputs.get(slot); + if (input == null || input.isEmpty() || !dynamic.test(input)) continue; + if (!ItemIngredientAllocator.matches(staticRequirements, + removeOne(inputs, slot), 1L)) continue; + if (result >= 0) return -1; + result = slot; + } + return result; + } + + private static int uniqueItemSlot(List inputs, Predicate predicate) { + int result = -1; + for (int slot = 0; slot < inputs.size(); slot++) { + ItemStack input = inputs.get(slot); + if (input == null || input.isEmpty() || !predicate.test(input)) continue; + if (result >= 0) return -1; + result = slot; + } + return result; + } + + private static List removeOne(List inputs, int slot) { + List result = new ArrayList<>(inputs); + ItemStack stack = result.get(slot); + if (stack == null || stack.isEmpty() || stack.getCount() <= 0) return List.of(); + if (stack.getCount() == 1) result.remove(slot); + else result.set(slot, stack.copyWithCount(stack.getCount() - 1)); + return result; + } + + private static long totalRequired(List requirements) { + long result = 0L; + for (CountedIngredient requirement : requirements) { + if (requirement == null || requirement.count() <= 0) continue; + if (result > Long.MAX_VALUE - requirement.count()) return Long.MAX_VALUE; + result += requirement.count(); + } + return result; + } + + private static long totalItems(List inputs) { + long result = 0L; + for (ItemStack input : inputs) { + if (input == null || input.isEmpty() || input.getCount() <= 0) continue; + if (result > Long.MAX_VALUE - input.getCount()) return Long.MAX_VALUE; + result += input.getCount(); + } + return result; + } + + private static long totalItemsLong(List inputs) { + long result = 0L; + for (GenericStack input : inputs) { + if (input == null || input.amount() <= 0L) continue; + if (result > Long.MAX_VALUE - input.amount()) return Long.MAX_VALUE; + result += input.amount(); + } + return result; + } + + private static boolean validItemStacks(PatternStackView pattern) { + return pattern.inputs().stream().allMatch(stack -> stack != null && stack.amount() > 0L + && stack.what() instanceof AEItemKey) + && pattern.outputs().stream().allMatch(stack -> stack != null && stack.amount() > 0L + && stack.what() instanceof AEItemKey); + } + + private static boolean matchesSingleOutputLong(ItemStack expected, List actual) { + return actual != null && actual.size() == 1 + && matchesDynamicOutputLong(expected, actual.get(0)); + } + + private static boolean matchesDynamicOutputsLong(List expected, List actual) { + if (expected == null || actual == null || expected.size() != actual.size()) return false; + for (int slot = 0; slot < expected.size(); slot++) { + if (!matchesDynamicOutputLong(expected.get(slot), actual.get(slot))) return false; + } + return true; + } + + private static boolean matchesDynamicOutputLong(ItemStack expected, GenericStack actual) { + if (expected == null || expected.isEmpty() || actual == null || actual.amount() <= 0L + || actual.amount() != expected.getCount() + || !(actual.what() instanceof AEItemKey itemKey)) { + return false; + } + return expected.getItem() == itemKey.getItem(); + } + + private static DynamicComponentPatternDetails.InputMatcher itemMatcher( + Predicate predicate) { + return input -> { + if (!(input instanceof AEItemKey itemKey)) return false; + try { + return predicate.test(itemKey.toStack(1)); + } catch (RuntimeException exception) { + return false; + } + }; + } + + private static DynamicComponentPatternDetails.InputMatcher ingredientMatcher( + Ingredient ingredient) { + return itemMatcher(ingredient::test); + } + + private static boolean isSpiritusGem(ItemStack stack) { + if (stack == null || stack.isEmpty()) return false; + Double maximum = stack.getItemHolder().getData(NVDataMaps.SPIRITUS_GEM_MAX_AMOUNTS); + return maximum != null && maximum > 0.0; + } + + private static ItemStack forgeGem() { + ItemStack gem = new ItemStack(Items.PAPER); + gem.set(NVDataComponents.SPIRITUS_AMOUNT.get(), Double.MAX_VALUE); + return gem; + } + + private static ItemStack assemble(ForgeRecipe source, List inputs, + ItemStack gem, int gemIndex, Level level) { + try { + return source.assemble(new ForgeInput(inputs, gem, gemIndex), + level.registryAccess()).copy(); + } catch (RuntimeException exception) { + return ItemStack.EMPTY; + } + } + + private static boolean matchesSingleOutput(ItemStack expected, List outputs) { + return outputs != null && outputs.size() == 1 && matchesDynamicOutput(expected, outputs.get(0)); + } + + private static boolean matchesDynamicOutputs(List expected, List actual) { + if (expected.size() != actual.size()) return false; + for (int slot = 0; slot < expected.size(); slot++) { + if (!matchesDynamicOutput(expected.get(slot), actual.get(slot))) return false; + } + return true; + } + + private static boolean matchesDynamicOutput(ItemStack expected, ItemStack actual) { + return expected != null && !expected.isEmpty() + && actual != null && !actual.isEmpty() + && expected.getItem() == actual.getItem() + && expected.getCount() == actual.getCount(); + } + + private static List potionOutputs( + AthanorRecipe source, @Nullable PotionContents potion) { + List result = new ArrayList<>(); + for (ItemStack output : source.getGuaranteedOutput()) { + result.add(potionOutput(output, potion)); + } + for (Pair output : source.getChanceOutput()) { + if (output != null && output.get(0) != null) { + result.add(potionOutput(output.get(0), potion)); + } + } + return result; + } + + private static ItemStack potionOutput(ItemStack output, @Nullable PotionContents potion) { + ItemStack result = output.copy(); + if (potion == null || !potion.hasEffects()) return result; + List effects = new ArrayList<>(); + for (MobEffectInstance effect : potion.getAllEffects()) { + effects.add(new MobEffectInstance(effect)); + } + result.set(DataComponents.POTION_CONTENTS, + new PotionContents(Optional.empty(), Optional.empty(), effects)); + return result; + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/neovitae/NeoVitaeRecipeCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/neovitae/NeoVitaeRecipeCompatLoader.java new file mode 100644 index 00000000..1cf203de --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/neovitae/NeoVitaeRecipeCompatLoader.java @@ -0,0 +1,22 @@ +package com.sorrowmist.useless.compat.neovitae; + +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.neovitae.AraVitaeRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.neovitae.AthanorRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.neovitae.HellfireForgeRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.neovitae.TabulaVitaeRecipeAdapter; + +/** Optional Neo Vitae entrypoint. This class is only loaded when Neo Vitae is present. */ +public final class NeoVitaeRecipeCompatLoader { + private NeoVitaeRecipeCompatLoader() { + } + + public static void register() { + AlloyFurnaceRecipeManager manager = AlloyFurnaceRecipeManager.getInstance(); + manager.registerAdapter(new HellfireForgeRecipeAdapter(), RecipeSourceIds.NEOVITAE); + manager.registerAdapter(new TabulaVitaeRecipeAdapter(), RecipeSourceIds.NEOVITAE); + manager.registerAdapter(new AthanorRecipeAdapter(), RecipeSourceIds.NEOVITAE); + manager.registerAdapter(new AraVitaeRecipeAdapter(), RecipeSourceIds.NEOVITAE); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/nomadsdelight/NomadsDelightRecipeCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/nomadsdelight/NomadsDelightRecipeCompatLoader.java new file mode 100644 index 00000000..3b292668 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/nomadsdelight/NomadsDelightRecipeCompatLoader.java @@ -0,0 +1,18 @@ +package com.sorrowmist.useless.compat.nomadsdelight; + +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.nomadsdelight.ButterChurnRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.delight.nomadsdelight.CurdBagRecipeAdapter; + +/** Registers Nomad's Delight's butter churn and curd bag adapters. */ +public final class NomadsDelightRecipeCompatLoader { + private NomadsDelightRecipeCompatLoader() { + } + + public static void register() { + AlloyFurnaceRecipeManager manager = AlloyFurnaceRecipeManager.getInstance(); + manager.registerAdapter(new ButterChurnRecipeAdapter(), RecipeSourceIds.NOMADS_DELIGHT); + manager.registerAdapter(new CurdBagRecipeAdapter(), RecipeSourceIds.NOMADS_DELIGHT); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/occultism/OccultismBlueprintCompat.java b/src/main/java/com/sorrowmist/useless/compat/occultism/OccultismBlueprintCompat.java new file mode 100644 index 00000000..6a30b0db --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/occultism/OccultismBlueprintCompat.java @@ -0,0 +1,68 @@ +package com.sorrowmist.useless.compat.occultism; + +import com.klikli_dev.occultism.common.blockentity.GoldenSacrificialBowlBlockEntity; +import com.klikli_dev.occultism.crafting.recipe.RitualRecipe; +import com.klikli_dev.occultism.registry.OccultismRecipes; +import com.sorrowmist.useless.core.component.RitualBlueprintPentacles; +import com.sorrowmist.useless.core.component.UComponents; +import net.minecraft.Util; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.Level; + +import java.util.LinkedHashSet; +import java.util.Set; + +public final class OccultismBlueprintCompat { + public static InteractionResult useOn(UseOnContext context) { + Level level = context.getLevel(); + ItemStack blueprint = context.getItemInHand(); + if (!(level.getBlockEntity(context.getClickedPos()) instanceof GoldenSacrificialBowlBlockEntity)) { + return InteractionResult.PASS; + } + if (com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.has(blueprint, UComponents.RITUAL_BLUEPRINT_PENTACLE.get())) { + return InteractionResult.FAIL; + } + if (level.isClientSide) { + return InteractionResult.SUCCESS; + } + + Set matches = new LinkedHashSet<>(); + for (var holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(level.getRecipeManager(), OccultismRecipes.RITUAL_TYPE.get())) { + RitualRecipe recipe = holder.value(); + if (recipe == null || recipe.getPentacle() == null + || recipe.getPentacle().validate(level, context.getClickedPos()) == null) { + continue; + } + matches.add(recipe.getPentacleId()); + } + + if (matches.isEmpty()) { + if (context.getPlayer() != null) { + context.getPlayer().displayClientMessage(Component.translatable( + "item.useless_mod.ritual_blueprint.no_pentacle"), true); + } + return InteractionResult.FAIL; + } + + RitualBlueprintPentacles pentacles = RitualBlueprintPentacles.of(matches); + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(blueprint, UComponents.RITUAL_BLUEPRINT_PENTACLE.get(), pentacles); + if (context.getPlayer() != null) { + Component message = pentacles.pentacles().size() == 1 + ? Component.translatable("item.useless_mod.ritual_blueprint.imprinted", + pentacleName(pentacles.pentacles().get(0))) + : Component.translatable("item.useless_mod.ritual_blueprint.imprinted_multiple", + pentacles.pentacles().size()); + context.getPlayer().displayClientMessage(message, true); + } + return InteractionResult.CONSUME; + } + + private static Component pentacleName(ResourceLocation pentacle) { + return Component.translatableWithFallback( + Util.makeDescriptionId("multiblock", pentacle), pentacle.toString()); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/occultism/RitualSatchelClientCompat.java b/src/main/java/com/sorrowmist/useless/compat/occultism/RitualSatchelClientCompat.java new file mode 100644 index 00000000..049f6ba1 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/occultism/RitualSatchelClientCompat.java @@ -0,0 +1,51 @@ +package com.sorrowmist.useless.compat.occultism; + +import com.klikli_dev.modonomicon.api.ModonomiconAPI; +import com.klikli_dev.modonomicon.multiblock.matcher.AnyMatcher; +import com.klikli_dev.modonomicon.multiblock.matcher.DisplayOnlyMatcher; +import com.sorrowmist.useless.network.RitualSatchelPlacePacket; +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; + +/** + * 匠心仪式挎包的客户端侧:识别魔典预览中是否存在可摆放的五芒星。 + * + *

预览信息仅存在于客户端,此处只负责识别玩家点击的仪式与格位,并将预览三元组 + * (结构 id / 锚点 / 朝向)发送至服务端;取料与摆放均在服务端完成。

+ * + *

本类仅在 occultism 已加载且处于客户端环境时才会被加载。

+ */ +public final class RitualSatchelClientCompat { + + private RitualSatchelClientCompat() { + } + + /** + * 玩家右键方块时,判断该方块是否为当前魔典预览五芒星的一格。 + * + * @return true 表示已接管该次右键,并已将摆放请求发送至服务端 + */ + public static boolean trySendPlacement(Level level, Player player, BlockPos pos) { + var preview = ModonomiconAPI.get().getCurrentPreviewMultiblock(); + // 预览未锚定时不参与摆放 + if (preview == null || !preview.isAnchored()) return false; + + var simulation = preview.multiblock().simulate(level, preview.anchor(), preview.facing(), false, false); + var target = simulation.getSecond().stream() + .filter(p -> p.getWorldPosition().equals(pos)) + .findFirst(); + if (target.isEmpty()) return false; + + // 「任意方块」与「仅显示」两类占位无需实际放置,交由原版右键链路处理 + var matcher = target.get().getStateMatcher(); + if (matcher.getType().equals(AnyMatcher.TYPE) || matcher.getType().equals(DisplayOnlyMatcher.TYPE)) { + return false; + } + + PacketDistributor.sendToServer(new RitualSatchelPlacePacket( + preview.multiblock().getId(), preview.anchor(), preview.facing())); + return true; + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/occultism/RitualSatchelCompat.java b/src/main/java/com/sorrowmist/useless/compat/occultism/RitualSatchelCompat.java new file mode 100644 index 00000000..3cc6e640 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/occultism/RitualSatchelCompat.java @@ -0,0 +1,225 @@ +package com.sorrowmist.useless.compat.occultism; + +import appeng.api.config.Actionable; +import com.klikli_dev.modonomicon.api.ModonomiconAPI; +import com.klikli_dev.modonomicon.api.multiblock.Multiblock; +import com.klikli_dev.modonomicon.multiblock.matcher.AnyMatcher; +import com.klikli_dev.modonomicon.multiblock.matcher.DisplayOnlyMatcher; +import com.klikli_dev.occultism.common.item.tool.ChalkItem; +import com.sorrowmist.useless.compat.AE2Compat; +import net.minecraft.ChatFormatting; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; + +/** + * 「匠心仪式挎包」能力的造化杖兼容实现。 + * + *

occultism 的仪式挎包(RitualSatchelItem)可在玩家用魔典预览某个五芒星后, + * 一次性摆放出整座仪式所需的方块。本实现以「玩家背包 + 工具绑定的 AE2 无线网络」 + * 替代挎包内物品栏:摆放时优先取用玩家背包内的方块,不足部分从 ME 网络提取。

+ * + *

识别与扣料规则对齐挎包 {@code RitualSatchelItem#tryPlaceBlockForMatcher}: + * 先将候选物品解析为其将放置出的方块状态,再由该格的 stateMatcher 判定是否吻合, + * 吻合时才执行放置。对粉笔而言,颜色与符号由 {@link ChalkItem#getGlyphBlock()} 决定, + * 因此该步骤即可筛选出颜色正确的粉笔。

+ * + *

本类仅在 occultism 已加载时才会被 JVM 解析:调用方一律先经 + * {@code ModList.get().isLoaded("occultism")} 守卫,避免形成硬依赖。

+ */ +public final class RitualSatchelCompat { + + public static final String MOD_ID = "occultism"; + + private RitualSatchelCompat() { + } + + /** + * 依据已预览的五芒星,从玩家背包与 AE 网络提取方块并摆放整座仪式。 + * + * @param multiblockId 魔典预览中的多方块结构 id + * @param anchor 预览锚点(玩家放置预览时的起始位置) + * @param facing 预览朝向 + */ + public static void placeFromAe(ServerLevel level, ServerPlayer player, ItemStack tool, + ResourceLocation multiblockId, BlockPos anchor, Rotation facing) { + var multiblock = ModonomiconAPI.get().getMultiblock(multiblockId); + if (multiblock == null) { + player.displayClientMessage( + Component.translatable("gui.useless_mod.ritual_satchel.unknown_multiblock") + .withStyle(ChatFormatting.YELLOW), true); + return; + } + + var simulation = multiblock.simulate(level, anchor, facing, false, false); + boolean placedAnything = false; + for (var targetMatcher : simulation.getSecond()) { + // 「任意方块」与「仅显示」两类占位不参与实际放置 + var type = targetMatcher.getStateMatcher().getType(); + if (type.equals(AnyMatcher.TYPE) || type.equals(DisplayOnlyMatcher.TYPE)) continue; + + if (placeForMatcher(level, player, tool, targetMatcher)) { + placedAnything = true; + } + } + + if (!placedAnything) { + player.displayClientMessage( + Component.translatable("gui.useless_mod.ritual_satchel.no_matching_item") + .withStyle(ChatFormatting.YELLOW), true); + } + } + + /** + * 为单个多方块占位查找匹配方块并放置:优先查询玩家背包,其次查询 AE 网络。 + * + *

放置上下文的坐标语义与挎包保持一致:命中位置取目标格中心, + * 被点击的方块为目标格上方那一格,inside 为 false; + * 而 stateMatcher 判定所用的位置仍是目标格本身。该坐标组合与挎包行为一致, + * 是粉笔颜色与符号判定正确的前提。

+ * + * @return 是否成功放置一个方块 + */ + private static boolean placeForMatcher(ServerLevel level, ServerPlayer player, ItemStack tool, + Multiblock.SimulateResult targetMatcher) { + BlockPos worldPos = targetMatcher.getWorldPosition(); + + // 目标格已是正确方块时直接跳过。 + // 缺少该判定时,重复右键会在目标格上方继续放置:目标格已有正确方块即不可替换, + // BlockPlaceContext 会把放置点上移到相邻格。 + if (targetMatcher.getStateMatcher().getStatePredicate() + .test(level, worldPos, level.getBlockState(worldPos))) { + return false; + } + + // 命中格必须取目标格本身且 inside=true,放置点才会落在 worldPos 上。 + // 若命中格改为 worldPos.above()(该位置可替换),BlockPlaceContext 会把放置点 + // 上移到命中格,导致整座仪式整体偏移一格。 + BlockHitResult placeHit = new BlockHitResult( + worldPos.getCenter(), Direction.UP, worldPos, true); + + // 优先从玩家背包取用 + Inventory inventory = player.getInventory(); + for (int i = 0; i < inventory.getContainerSize(); i++) { + ItemStack stack = inventory.getItem(i); + if (stack.isEmpty()) continue; + if (!matches(stack, player, level, worldPos, placeHit, targetMatcher)) continue; + if (consumeFromInventory(player, inventory, stack, level, placeHit)) return true; + } + + // 背包内无匹配项时,改从绑定的 AE 网络提取 + return placeFromAe(level, player, tool, worldPos, placeHit, targetMatcher); + } + + /** + * 判定一件物品是否满足该格所需方块。 + * + *

与挎包一致:先将候选物品解析为其将放置出的方块状态, + * 再由该格的状态匹配器判定。粉笔的颜色信息即在此步骤体现, + * 白色粉笔不会匹配需要其它颜色粉笔的格子。

+ * + *

粉笔额外要求能在目标格上方存活(下方有支撑),否则放置出的符号方块会立即消失。

+ */ + private static boolean matches(ItemStack stack, ServerPlayer player, ServerLevel level, BlockPos worldPos, + BlockHitResult placeHit, Multiblock.SimulateResult targetMatcher) { + // 必须使用真实玩家构造判定上下文:粉笔朝向取自玩家朝向; + // 若此处传 null,判定朝向与实际放置时不一致,会选中不匹配的物品。 + BlockPlaceContext placeContext = new BlockPlaceContext( + new UseOnContext(level, player, InteractionHand.MAIN_HAND, stack, placeHit)); + BlockState stateToPlace = resolveState(stack, placeContext); + if (stateToPlace == null) return false; + + if (!targetMatcher.getStateMatcher().getStatePredicate() + .test(level, worldPos, stateToPlace)) { + return false; + } + + // 放置点即目标格本身,存活判定同样针对该格 + return stateToPlace.canSurvive(level, worldPos); + } + + /** + * 使用玩家背包内的该件物品完成一次放置。 + * + *

直接在该玩家真实的物品栈上执行 {@code useOn},由物品自身处理 + * 耐久消耗与音效,随后回写背包。带耐久的物品(粉笔)因此只消耗少量耐久, + * 而非整件消失;创造模式或配置为不消耗时不扣除。

+ * + * @return 是否成功放置 + */ + private static boolean consumeFromInventory(ServerPlayer player, Inventory inventory, + ItemStack stack, ServerLevel level, + BlockHitResult placeHit) { + // 剩余耐久不足 1 点的物品跳过,避免放置中途损坏 + if (stack.isDamageableItem() && stack.getMaxDamage() - stack.getDamageValue() <= 1) { + return false; + } + + stack.useOn(new UseOnContext(level, player, InteractionHand.MAIN_HAND, stack, placeHit)); + // 强制回写:放置可能消耗耐久,也可能耗尽整件物品 + inventory.setChanged(); + return true; + } + + /** + * 从绑定的 AE 网络提取一件匹配方块并放置。 + * + *

提取时按网络中该件物品自身的 key 精确匹配,因此取得的物品栈保留其原有耐久; + * 放置直接作用于该真实物品栈,由物品自身决定耐久消耗,随后原样放回网络。 + * 由此耐久扣除正确,而非整件消失。

+ * + * @return 是否成功放置 + */ + private static boolean placeFromAe(ServerLevel level, ServerPlayer player, ItemStack tool, + BlockPos worldPos, BlockHitResult placeHit, + Multiblock.SimulateResult targetMatcher) { + ItemStack extracted = AE2Compat.extractMatchingFromLinkedGrid(tool, player, + candidate -> matches(candidate, player, level, worldPos, placeHit, targetMatcher)); + if (extracted == null || extracted.isEmpty()) return false; + + // 剩余耐久不足 1 点的粉笔不参与,原样放回网络 + if (extracted.isDamageableItem() + && extracted.getMaxDamage() - extracted.getDamageValue() <= 1) { + AE2Compat.tryInsertIntoLinkedGrid(tool, player, extracted, Actionable.MODULATE); + return false; + } + + extracted.useOn(new UseOnContext(level, player, InteractionHand.MAIN_HAND, extracted, placeHit)); + + // 未耗尽则携带新耐久放回网络;已耗尽则不放回,等价于消耗该件物品 + if (!extracted.isEmpty()) { + AE2Compat.tryInsertIntoLinkedGrid(tool, player, extracted, Actionable.MODULATE); + } + return true; + } + + /** + * 将候选物品解析为其将放置出的方块状态。 + * + *

普通方块经 {@link BlockItem} 的标准流程;occultism 的粉笔不属于方块物品, + * 其绘制的符号方块需单独经 {@link ChalkItem#getGlyphBlock()} 获取。

+ */ + private static BlockState resolveState(ItemStack stack, BlockPlaceContext placeContext) { + if (stack.getItem() instanceof BlockItem blockItem) { + var updated = blockItem.updatePlacementContext(placeContext); + if (updated == null) return null; + return blockItem.getBlock().getStateForPlacement(updated); + } + if (stack.getItem() instanceof ChalkItem chalkItem) { + return chalkItem.getGlyphBlock().getStateForPlacement(placeContext); + } + return null; + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/oritech/OritechRecipeCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/oritech/OritechRecipeCompatLoader.java new file mode 100644 index 00000000..1edccc5f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/oritech/OritechRecipeCompatLoader.java @@ -0,0 +1,16 @@ +package com.sorrowmist.useless.compat.oritech; + +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.oritech.OritechRecipeAdapter; + +/** Optional Oritech entrypoint. This class is only loaded when Oritech is present. */ +public final class OritechRecipeCompatLoader { + private OritechRecipeCompatLoader() { + } + + public static void register() { + AlloyFurnaceRecipeManager.getInstance().registerAdapter( + new OritechRecipeAdapter(), RecipeSourceIds.ORITECH); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/pneumaticcraft/PneumaticCraftPressureCompat.java b/src/main/java/com/sorrowmist/useless/compat/pneumaticcraft/PneumaticCraftPressureCompat.java new file mode 100644 index 00000000..92c88b58 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/pneumaticcraft/PneumaticCraftPressureCompat.java @@ -0,0 +1,141 @@ +package com.sorrowmist.useless.compat.pneumaticcraft; + +import com.sorrowmist.useless.api.logistics.LongPressureHandler; +import me.desht.pneumaticcraft.api.PNCCapabilities; +import me.desht.pneumaticcraft.api.tileentity.IAirHandlerMachine; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +/** + * {@link PressureBridge} 的气动工艺实现。 + * + *

只有本类(以及它引用的 PneumaticCraft 类型)会在装了气动工艺时被加载:常驻代码碰到的 + * 永远是 {@link PressureBridge} 那个不含模组类型的接口。

+ * + *

为什么气压端点要报体积

+ * + *

气动工艺的空气量满足 {@code air = pressure × volume}(见 {@code BasicAirHandler})。 + * 也就是说「3 bar」对不同体积的方块意味着不同的空气量。无线物流的「目标气压」要伺服的是 + * 压力而不是空气量,所以端点必须把 {@link IAirHandler#getVolume() 体积} 一起报出来, + * 由调用方换算 {@code targetAir = targetBar × volume}。

+ * + *

真空(负压)

+ * + *

{@code BasicAirHandler.addAir} 的地板是 {@code -volume},即 -1 bar 的绝对真空。因此 + * {@link #setAirTo} 允许把空气量调到负值;而 {@link #extract} 只抽到 0 bar 为止,不制造真空 + * ——它服务的是「把源端的气搬走」这条语义。

+ */ +public final class PneumaticCraftPressureCompat implements PressureBridge { + + private static final Direction[] DIRECTIONS = Direction.values(); + + @Override + @Nullable + public LongPressureHandler pressureEndpoint(Level level, BlockPos pos, @Nullable Direction side) { + IAirHandlerMachine handler = handler(level, pos, side); + return handler == null ? null : new AirEndpoint(handler); + } + + /** + * 取该坐标的空气处理器。 + * + *

{@code side == null} 时按「不限面 → 6 个方向」的顺序探测,与 + * {@code StaffLinkTargets#capability} 同一把尺子(气动设备的空气处理器常常只挂在某几个面上)。

+ */ + @Nullable + private static IAirHandlerMachine handler(Level level, BlockPos pos, @Nullable Direction side) { + if (level == null || pos == null || !level.isLoaded(pos)) { + return null; + } + if (side != null) { + return com.sorrowmist.useless.compat.forge1201.capability.CapabilityAccess.get(level, PNCCapabilities.AIR_HANDLER_MACHINE, pos, side); + } + IAirHandlerMachine unspecified = + com.sorrowmist.useless.compat.forge1201.capability.CapabilityAccess.get(level, PNCCapabilities.AIR_HANDLER_MACHINE, pos, null); + if (unspecified != null) { + return unspecified; + } + for (Direction direction : DIRECTIONS) { + IAirHandlerMachine found = + com.sorrowmist.useless.compat.forge1201.capability.CapabilityAccess.get(level, PNCCapabilities.AIR_HANDLER_MACHINE, pos, direction); + if (found != null) { + return found; + } + } + return null; + } + + /** 某个气动方块空气处理器的无线物流视角。 */ + private static final class AirEndpoint implements LongPressureHandler { + private final IAirHandlerMachine handler; + + AirEndpoint(IAirHandlerMachine handler) { + this.handler = handler; + } + + @Override + public long extract(long amount, boolean simulate) { + if (amount <= 0L) { + return 0L; + } + int before = handler.getAir(); + if (before <= 0) { + // 已经是 0 bar 或真空:没有正空气可搬,也不在搬运路径上制造真空。 + return 0L; + } + long take = Math.min(amount, before); + if (!simulate) { + handler.addAir((int) -take); + } + return take; + } + + @Override + public long receive(long amount, boolean simulate) { + if (amount <= 0L) { + return 0L; + } + // 刻意不按 maxPressure 夹取:气动方块的 addAir 本身没有上限,超压由爆炸机制处理。 + // 真正的限值由调用方(目标气压伺服)算好再传进来。 + long take = Math.min(amount, Integer.MAX_VALUE); + if (!simulate) { + handler.addAir((int) take); + } + return take; + } + + @Override + public long setAirTo(long targetAir, boolean simulate) { + int before = handler.getAir(); + long floor = -(long) handler.getVolume(); // -1 bar:硬真空 + long clamped = Math.max(targetAir, floor); + long delta = clamped - before; + if (delta == 0L) { + return 0L; + } + if (!simulate) { + handler.addAir((int) Math.max(Math.min(delta, Integer.MAX_VALUE), Integer.MIN_VALUE)); + } + return delta; + } + + @Override + public long stored() { + return handler.getAir(); + } + + @Override + public long capacity() { + // 用「危险压力」而不是 maxPressure():后者在 BasicAirHandler 里恒为 10f, + // 对 Tier1 气阀(危险压力 5 bar)明显偏大;危险压力才是正压侧真正有意义的上限。 + return (long) ((double) handler.getDangerPressure() * (double) handler.getVolume()); + } + + @Override + public long volume() { + return handler.getVolume(); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/pneumaticcraft/PneumaticCraftPressureCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/pneumaticcraft/PneumaticCraftPressureCompatLoader.java new file mode 100644 index 00000000..463d400e --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/pneumaticcraft/PneumaticCraftPressureCompatLoader.java @@ -0,0 +1,60 @@ +package com.sorrowmist.useless.compat.pneumaticcraft; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.api.logistics.LongPressureHandler; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; +import net.minecraftforge.fml.ModList; +import org.jetbrains.annotations.Nullable; + +/** + * 按需加载 {@link PneumaticCraftPressureCompat}。 + * + *

与 {@link com.sorrowmist.useless.compat.ae.AeEnergyCompatLoader} 同一套手法:常驻类只引用 + * {@link PressureBridge} 这个不含 PneumaticCraft 类型的接口,实现类用 {@code Class.forName} + * 反射加载,从而在没装气动工艺的整合包里也不会触发类解析错误。

+ */ +public final class PneumaticCraftPressureCompatLoader { + public static final String MOD_ID = "pneumaticcraft"; + + private static final String IMPLEMENTATION = + "com.sorrowmist.useless.compat.pneumaticcraft.PneumaticCraftPressureCompat"; + + private static volatile boolean initialized; + private static volatile PressureBridge bridge; + + private PneumaticCraftPressureCompatLoader() { + } + + /** 气压桥;未加载气动工艺或初始化失败时为 {@code null}。 */ + @Nullable + public static PressureBridge bridge() { + if (!initialized) { + initialized = true; + if (ModList.get().isLoaded(MOD_ID)) { + try { + Class implementation = Class.forName( + IMPLEMENTATION, true, PneumaticCraftPressureCompatLoader.class.getClassLoader()); + bridge = (PressureBridge) implementation.getDeclaredConstructor().newInstance(); + } catch (ReflectiveOperationException | LinkageError exception) { + RateLimitedLogger.error(UselessMod.LOGGER, "Failed to initialise the PneumaticCraft pressure bridge", exception); + } + } + } + return bridge; + } + + public static boolean isAvailable() { + return bridge() != null; + } + + /** 该坐标的气压端点;未加载模组或不是气压方块时返回 {@code null}。 */ + @Nullable + public static LongPressureHandler pressureEndpoint(Level level, BlockPos pos, @Nullable Direction side) { + PressureBridge resolved = bridge(); + return resolved == null ? null : resolved.pressureEndpoint(level, pos, side); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/pneumaticcraft/PneumaticCraftRecipeCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/pneumaticcraft/PneumaticCraftRecipeCompatLoader.java new file mode 100644 index 00000000..7b4ca1b8 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/pneumaticcraft/PneumaticCraftRecipeCompatLoader.java @@ -0,0 +1,25 @@ +package com.sorrowmist.useless.compat.pneumaticcraft; + +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.pneumaticcraft.PneumaticCraftRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.pneumaticcraft.PneumaticCraftSyntheticRecipeAdapter; + +/** Registers PneumaticCraft adapters after the optional mod has been detected. */ +public final class PneumaticCraftRecipeCompatLoader { + private PneumaticCraftRecipeCompatLoader() { + } + + public static void register() { + AlloyFurnaceRecipeManager manager = AlloyFurnaceRecipeManager.getInstance(); + manager.registerAdapter(PneumaticCraftRecipeAdapter.assembly(), RecipeSourceIds.PNEUMATICCRAFT); + manager.registerAdapter(PneumaticCraftRecipeAdapter.fluidMixer(), RecipeSourceIds.PNEUMATICCRAFT); + manager.registerAdapter(PneumaticCraftRecipeAdapter.heatFrameCooling(), RecipeSourceIds.PNEUMATICCRAFT); + manager.registerAdapter(PneumaticCraftRecipeAdapter.pressureChamber(), RecipeSourceIds.PNEUMATICCRAFT); + manager.registerAdapter(PneumaticCraftRecipeAdapter.refinery(), RecipeSourceIds.PNEUMATICCRAFT); + manager.registerAdapter(PneumaticCraftRecipeAdapter.thermoPlant(), RecipeSourceIds.PNEUMATICCRAFT); + manager.registerAdapter(PneumaticCraftRecipeAdapter.amadron(), RecipeSourceIds.PNEUMATICCRAFT); + manager.registerAdapter(PneumaticCraftSyntheticRecipeAdapter.etching(), RecipeSourceIds.PNEUMATICCRAFT); + manager.registerAdapter(PneumaticCraftSyntheticRecipeAdapter.uvLightBox(), RecipeSourceIds.PNEUMATICCRAFT); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/pneumaticcraft/PressureBridge.java b/src/main/java/com/sorrowmist/useless/compat/pneumaticcraft/PressureBridge.java new file mode 100644 index 00000000..3c99246c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/pneumaticcraft/PressureBridge.java @@ -0,0 +1,25 @@ +package com.sorrowmist.useless.compat.pneumaticcraft; + +import com.sorrowmist.useless.api.logistics.LongPressureHandler; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +/** + * 气动工艺气压端点的桥接口。 + * + *

与 {@link com.sorrowmist.useless.compat.ars.SourceBridge} 同一套手法:本接口不含任何 + * PneumaticCraft 类型,实现类由 {@link PneumaticCraftPressureCompatLoader} 反射加载, + * 因此没装气动工艺时 {@code StaffLinkTargets} 照样能加载、只是永远解析不出气压端点。

+ */ +public interface PressureBridge { + + /** + * 该坐标在指定面上的气压端点;不是气动设备、或该面没有空气处理器时返回 {@code null}。 + * + * @param side {@code null} 表示不限面(实现方自行决定探测顺序) + */ + @Nullable + LongPressureHandler pressureEndpoint(Level level, BlockPos pos, @Nullable Direction side); +} diff --git a/src/main/java/com/sorrowmist/useless/compat/pneumaticcraft/ThermostatHeatCompat.java b/src/main/java/com/sorrowmist/useless/compat/pneumaticcraft/ThermostatHeatCompat.java new file mode 100644 index 00000000..2cc2a0ad --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/pneumaticcraft/ThermostatHeatCompat.java @@ -0,0 +1,138 @@ +package com.sorrowmist.useless.compat.pneumaticcraft; + +import com.sorrowmist.useless.content.blockentities.ThermostatBlockEntity; +import com.sorrowmist.useless.init.ModBlockEntities; +import me.desht.pneumaticcraft.api.PNCCapabilities; +import me.desht.pneumaticcraft.api.heat.IHeatExchangerLogic; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelAccessor; +import com.sorrowmist.useless.compat.forge1201.capability.RegisterCapabilitiesEvent; + +import java.util.function.BiPredicate; + +/** + * 温度调节器方块的气动工艺热兼容。 + * + *

气动工艺的热交换是接收方驱动:机器自己的 {@code HeatExchangerLogicTicking.tick()} 会遍历 + * 相连的热交换器并调用 {@code addHeat},所以只要把 {@link IHeatExchangerLogic} 通过 + * {@code PNCCapabilities.HEAT_EXCHANGER_BLOCK} 暴露出去,机器就会自动向我们收敛——不需要我们 tick。

+ * + *

注意:气动工艺要求方块有 BlockEntity 才看得到热能力。 + * {@code HeatExchangerManager.getLogic} 走的是 {@code IOHelper.getCap(te, ...)},而它在 + * {@code te == null} 时直接返回 empty。塑料方块因此加了 BE。

+ * + *

实现照抄气动自带的 {@code HeatExchangerLogicConstant}(岩浆 / 冰用的那种):温度恒定、 + * {@code addHeat} 空操作,于是它就是一个「无限热源 / 热汇」。

+ */ +public final class ThermostatHeatCompat { + + private static final String CACHE_KEY = "pneumaticcraft"; + private static final double THERMAL_CAPACITY = 1000.0D; + /** 开启时的热阻:与气动自带方块同量级,交换很快。 */ + private static final double ACTIVE_THERMAL_RESISTANCE = 1.0D; + /** + * 关闭时的热阻:取得很大,让交换量趋近于 0。 + * + *

气动的 {@code exchange()} 是 {@code deltaTemp /= (对方热阻 + 我方热阻)},热阻取 10000 + * 就等于把这条通路掐掉——这样「始终暴露能力」才不会让所有塑料方块都变成 300K 的热汇。

+ */ + private static final double IDLE_THERMAL_RESISTANCE = 10_000.0D; + /** 关掉恒温时对外报告的温度:环境温度。 */ + private static final double AMBIENT_TEMPERATURE = 300.0D; + + private ThermostatHeatCompat() { + } + + public static void registerCapabilities(RegisterCapabilitiesEvent event) { + // ⚠️ 刻意不按 enabled 返回 null:气动的机器只在 onLoad / neighborChanged 时 + // 重新扫一遍热连接,开了恒温之后它不会重新发现我们,表现就是「设置不生效」。 + // 始终交出逻辑,由逻辑按 enabled 决定温度与热阻。 + event.registerBlockEntity(PNCCapabilities.HEAT_EXCHANGER_BLOCK, + ModBlockEntities.TEMPERATURE_REGULATOR.get(), + (blockEntity, side) -> adapter(blockEntity)); + } + + private static IHeatExchangerLogic adapter(ThermostatBlockEntity thermostat) { + Object cached = thermostat.getHeatAdapter(CACHE_KEY); + if (cached instanceof Logic logic) { + return logic; + } + Logic created = new Logic(thermostat); + thermostat.putHeatAdapter(CACHE_KEY, created); + return created; + } + + /** 恒温式热交换逻辑,语义同气动自带的 {@code HeatExchangerLogicConstant}。 */ + private static final class Logic implements IHeatExchangerLogic { + private final ThermostatBlockEntity thermostat; + + Logic(ThermostatBlockEntity thermostat) { + this.thermostat = thermostat; + } + + @Override + public void tick() { + // 恒温源不需要自 tick:邻居机器的 exchange() 会主动与我们交换。 + } + + @Override + public void initializeAsHull(Level level, BlockPos pos, + BiPredicate blockFilter, Direction... validSides) { + // 同上:连接由邻居侧建立。 + } + + @Override + public void initializeAmbientTemperature(Level level, BlockPos pos) { + } + + @Override + public void setTemperature(double temperature) { + // 恒温源:外部不能改我们的温度(设定只通过权杖界面)。 + } + + @Override + public double getTemperature() { + return thermostat.isEnabled() ? thermostat.getTemperature() : AMBIENT_TEMPERATURE; + } + + @Override + public int getTemperatureAsInt() { + return (int) getTemperature(); + } + + @Override + public double getAmbientTemperature() { + return getTemperature(); + } + + @Override + public void setThermalResistance(double thermalResistance) { + } + + @Override + public double getThermalResistance() { + return thermostat.isEnabled() ? ACTIVE_THERMAL_RESISTANCE : IDLE_THERMAL_RESISTANCE; + } + + @Override + public void setThermalCapacity(double capacity) { + } + + @Override + public double getThermalCapacity() { + return THERMAL_CAPACITY; + } + + @Override + public void addHeat(double amount) { + // 无限热容:收下 / 给出多少热都不改变自身温度。 + } + + @Override + public boolean isSideConnected(Direction side) { + return true; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/productivebees/ProductiveBeesCaptureCompat.java b/src/main/java/com/sorrowmist/useless/compat/productivebees/ProductiveBeesCaptureCompat.java new file mode 100644 index 00000000..62a6723c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/productivebees/ProductiveBeesCaptureCompat.java @@ -0,0 +1,27 @@ +package com.sorrowmist.useless.compat.productivebees; + +import cy.jdkdigital.productivebees.common.entity.bee.ProductiveBee; +import cy.jdkdigital.productivebees.util.BeeCreator; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Nullable; + +public final class ProductiveBeesCaptureCompat { + private ProductiveBeesCaptureCompat() { + } + + /** + * @return a configured egg for Productive Bees, an empty stack when its bee type is invalid, + * or {@code null} when the entity is not a Productive Bees bee + */ + @Nullable + public static ItemStack tryCreateSpawnEgg(LivingEntity entity) { + if (!(entity instanceof ProductiveBee productiveBee)) { + return null; + } + + ResourceLocation beeType = productiveBee.getBeeType(); + return beeType == null ? ItemStack.EMPTY : BeeCreator.getSpawnEgg(beeType); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/teams/FtbTeamsCompat.java b/src/main/java/com/sorrowmist/useless/compat/teams/FtbTeamsCompat.java new file mode 100644 index 00000000..6f61eb60 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/teams/FtbTeamsCompat.java @@ -0,0 +1,51 @@ +package com.sorrowmist.useless.compat.teams; + +import dev.ftb.mods.ftbteams.api.FTBTeamsAPI; +import dev.ftb.mods.ftbteams.api.Team; +import dev.ftb.mods.ftbteams.api.TeamManager; +import net.minecraft.server.level.ServerPlayer; +import org.jetbrains.annotations.Nullable; + +import java.util.Optional; +import java.util.UUID; + +/** + * {@link SharedOwnerBridge} 的 FTB Teams 实现。 + * + *

只有本类(以及它引用的 FTB Teams 类)会在装了该模组时被加载。

+ * + *

用 {@code getTeamForPlayerID} 而不是 {@code getPlayerTeamForPlayerID}:FTB Teams 会给 + * 每个玩家自动建一支「个人队伍」,后者返回的正是它——那样每个人永远都属于一支队伍, + * 也就永远共享不起来了。这里还额外要求队伍不是个人队伍。

+ */ +public final class FtbTeamsCompat implements SharedOwnerBridge { + + @Override + @Nullable + public UUID teamOwnerId(ServerPlayer player) { + try { + FTBTeamsAPI.API api = FTBTeamsAPI.api(); + if (api == null || !api.isManagerLoaded()) { + // 队伍管理器还没起来(服务端刚启动 / 客户端侧):当作没有队伍。 + return null; + } + TeamManager manager = api.getManager(); + if (manager == null) { + return null; + } + Optional team = manager.getTeamForPlayerID(player.getUUID()); + if (team.isEmpty()) { + return null; + } + Team found = team.get(); + if (found.isPlayerTeam()) { + // 单人自带的个人队伍:等同于「没有队伍」。 + return null; + } + return found.getId(); + } catch (RuntimeException | LinkageError notReady) { + // FTB Teams 的 API 在未初始化时会抛 NPE。队伍信息拿不到不该让物流崩掉。 + return null; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/teams/FtbTeamsCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/teams/FtbTeamsCompatLoader.java new file mode 100644 index 00000000..344a21c3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/teams/FtbTeamsCompatLoader.java @@ -0,0 +1,51 @@ +package com.sorrowmist.useless.compat.teams; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.sorrowmist.useless.UselessMod; +import net.minecraftforge.fml.ModList; +import org.jetbrains.annotations.Nullable; + +/** + * 按需加载 {@link FtbTeamsCompat}。 + * + *

与 {@code AeLogisticsCompatLoader} 同一套手法:常驻类只引用 {@link SharedOwnerBridge} + * 这个不含 FTB Teams 类型的接口,实现类用 {@code Class.forName} 反射加载,从而在没装 + * FTB Teams 的整合包里也不会触发类解析错误。

+ * + *

注意 FTB Teams 只是 {@code build.gradle} 里的 {@code implementation} 依赖, + * 没有写进 {@code neoforge.mods.toml},所以运行时确实可能缺席——这个隔离是必需的。

+ */ +public final class FtbTeamsCompatLoader { + public static final String MOD_ID = "ftbteams"; + + private static final String IMPLEMENTATION = "com.sorrowmist.useless.compat.teams.FtbTeamsCompat"; + + private static volatile boolean initialized; + private static volatile SharedOwnerBridge bridge; + + private FtbTeamsCompatLoader() { + } + + /** FTB Teams 桥;未加载该模组或初始化失败时为 {@code null}。 */ + @Nullable + public static SharedOwnerBridge bridge() { + if (!initialized) { + initialized = true; + if (ModList.get().isLoaded(MOD_ID)) { + try { + Class implementation = Class.forName( + IMPLEMENTATION, true, FtbTeamsCompatLoader.class.getClassLoader()); + bridge = (SharedOwnerBridge) implementation.getDeclaredConstructor().newInstance(); + } catch (ReflectiveOperationException | LinkageError exception) { + RateLimitedLogger.error(UselessMod.LOGGER, "Failed to initialise the FTB Teams bridge", exception); + } + } + } + return bridge; + } + + public static boolean isAvailable() { + return bridge() != null; + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/teams/SharedOwnerBridge.java b/src/main/java/com/sorrowmist/useless/compat/teams/SharedOwnerBridge.java new file mode 100644 index 00000000..108c5e52 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/teams/SharedOwnerBridge.java @@ -0,0 +1,24 @@ +package com.sorrowmist.useless.compat.teams; + +import net.minecraft.server.level.ServerPlayer; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; + +/** + * 「共享归属」的桥接口:某个玩家所属队伍的 UUID。 + * + *

与 {@code AeLogisticsBridge} 同一套手法:本接口不含任何队伍模组的类型, + * 实现类由 {@link FtbTeamsCompatLoader} 反射加载,因此没装 FTB Teams 时 + * {@link TeamOwners} 照样能加载、只是永远拿不到队伍。

+ */ +public interface SharedOwnerBridge { + + /** + * 该玩家所属队伍的 UUID;不在任何队伍里时返回 {@code null}。 + * + *

「单人自带的个人队伍」不算队伍——那本质上就是他自己。

+ */ + @Nullable + UUID teamOwnerId(ServerPlayer player); +} diff --git a/src/main/java/com/sorrowmist/useless/compat/teams/TeamOwners.java b/src/main/java/com/sorrowmist/useless/compat/teams/TeamOwners.java new file mode 100644 index 00000000..5ca7c66c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/teams/TeamOwners.java @@ -0,0 +1,67 @@ +package com.sorrowmist.useless.compat.teams; + +import net.minecraft.server.level.ServerPlayer; + +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.UUID; + +/** + * 解析「一个玩家当前的归属 ID」。 + * + *

优先级:FTB Teams 队伍 → 原版计分板队伍 → 玩家自己。都没有队伍时就是他本人, + * 于是每人一套独立网络;一旦组了队,队友之间共享同一批网络。

+ * + *

原版计分板队伍只有名字、没有 UUID,所以用队伍名派生一个稳定 UUID:同一支队伍的每个 + * 成员算出来都一样,正好当共享键。

+ * + *

权限判定的唯一依据是 {@link #ownerIdOf} 的相等比较:玩家能操作一张网络, + * 当且仅当他的归属 ID 与那张网络的归属 ID 相同。这样「退队」自然失去访问权, + * 「入队」自然获得访问权,不需要额外的成员表。

+ */ +public final class TeamOwners { + /** 派生 UUID 的前缀,避免与真实的玩家/队伍 UUID 撞车。 */ + private static final String VANILLA_TEAM_PREFIX = "useless_mod:team:"; + + private TeamOwners() { + } + + /** 该玩家当前的归属 ID;永远有值(最差就是他自己)。 */ + public static UUID ownerIdOf(ServerPlayer player) { + return sharedOwnerId(player).orElse(player.getUUID()); + } + + /** 该玩家所属队伍的共享 ID;没有队伍时为空。 */ + public static Optional sharedOwnerId(ServerPlayer player) { + if (player == null) { + return Optional.empty(); + } + SharedOwnerBridge ftb = FtbTeamsCompatLoader.bridge(); + if (ftb != null) { + UUID teamId = ftb.teamOwnerId(player); + if (teamId != null) { + return Optional.of(teamId); + } + } + return vanillaTeamOwnerId(player); + } + + /** 原版计分板队伍:只有名字,所以派生一个稳定 UUID 当共享键。 */ + private static Optional vanillaTeamOwnerId(ServerPlayer player) { + net.minecraft.world.scores.Team team = player.getTeam(); + if (team == null) { + return Optional.empty(); + } + String name = team.getName(); + if (name == null || name.isBlank()) { + return Optional.empty(); + } + return Optional.of(deriveTeamId(name)); + } + + /** 队伍名 → 稳定 UUID。 */ + public static UUID deriveTeamId(String teamName) { + return UUID.nameUUIDFromBytes( + (VANILLA_TEAM_PREFIX + teamName).getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/thermal/ThermalRecipeConverter.java b/src/main/java/com/sorrowmist/useless/compat/thermal/ThermalRecipeConverter.java deleted file mode 100644 index 2640daf8..00000000 --- a/src/main/java/com/sorrowmist/useless/compat/thermal/ThermalRecipeConverter.java +++ /dev/null @@ -1,521 +0,0 @@ -package com.sorrowmist.useless.compat.thermal; - -import com.sorrowmist.useless.recipes.advancedalloyfurnace.AdvancedAlloyFurnaceRecipe; -import com.sorrowmist.useless.registry.ModIngots; -import cofh.thermal.core.util.recipes.machine.InsolatorRecipe; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.stream.Collectors; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.crafting.Ingredient; -import net.minecraft.world.item.crafting.RecipeManager; -import net.minecraftforge.fluids.FluidStack; -import net.minecraftforge.registries.ForgeRegistries; - -/** - * 热力配方转换器,用于将热力系列模组的配方转换为高级合金炉配方 - */ -public class ThermalRecipeConverter { - - /** - * 将热力有机灌注机(Insolator)配方转换为高级合金炉配方 - * - * @param recipeManager 游戏配方管理器 - * @return 转换后的高级合金炉配方列表 - */ - public static List convertInsolatorRecipes(RecipeManager recipeManager) { - List convertedRecipes = new ArrayList<>(); - - // 直接获取所有配方,然后筛选出InsolatorRecipe类型的配方 - try { - // 获取InsolatorRecipe类 - Class insolatorRecipeClass = Class.forName("cofh.thermal.core.util.recipes.machine.InsolatorRecipe"); - - // 获取所有配方 - Collection> allRecipes = recipeManager.getRecipes(); - - // 筛选出InsolatorRecipe类型的配方 - for (net.minecraft.world.item.crafting.Recipe recipe : allRecipes) { - // 检查配方是否为InsolatorRecipe类型 - if (insolatorRecipeClass.isInstance(recipe)) { - InsolatorRecipe insolatorRecipe = (InsolatorRecipe) recipe; - - // 创建配方ID,使用"thermal_insolator_"前缀加上原配方ID - ResourceLocation originalId = insolatorRecipe.getId(); - ResourceLocation newId = new ResourceLocation( - "useless_mod", - "thermal_insolator_" + originalId.getNamespace() + "_" + originalId.getPath() - ); - - // 1. 处理输入物品:直接使用原配方的输入物品 - List inputIngredients = insolatorRecipe.getInputItems(); - - // 2. 处理输入物品数量:从原始配方中读取需求数量并乘以64倍 - List inputItemCounts = new ArrayList<>(); - for (Ingredient ingredient : inputIngredients) { - // 默认数量为1 - long originalCount = 1; - - // 检查是否为IngredientWithCount类型,获取原始数量 - try { - Class ingredientWithCountClass = Class.forName("cofh.lib.util.crafting.IngredientWithCount"); - if (ingredientWithCountClass.isInstance(ingredient)) { - // 使用反射获取count字段值 - java.lang.reflect.Field countField = ingredientWithCountClass.getDeclaredField("count"); - countField.setAccessible(true); - originalCount = countField.getLong(ingredient); - } - } catch (Exception e) { - // 如果不是IngredientWithCount类型,使用默认数量1 - originalCount = 1; - } - - // 使用原始数量,不需要乘以64倍 - long newCount = originalCount; - inputItemCounts.add(newCount); - } - - // 3. 处理输入流体:直接使用原配方的输入流体 - List inputFluids = new ArrayList<>(); - if (!insolatorRecipe.getInputFluids().isEmpty()) { - // 获取第一个流体输入 - cofh.lib.common.fluid.FluidIngredient fluidIngredient = insolatorRecipe.getInputFluids().get(0); - if (fluidIngredient != null) { - // 获取流体列表,然后取第一个流体作为代表 - FluidStack[] fluidStacks = fluidIngredient.getFluids(); - if (fluidStacks.length > 0) { - FluidStack fluidStack = fluidStacks[0]; - // 使用流体栈中的流体,保留原始数量 - inputFluids.add(fluidStack); - } - } - } - - // 4. 处理输出物品:概率向上取整作为输出数量 - List outputItems = new ArrayList<>(); - List originalOutputs = insolatorRecipe.getOutputItems(); - List outputChances = insolatorRecipe.getOutputItemChances(); - - for (int i = 0; i < originalOutputs.size(); i++) { - ItemStack originalOutput = originalOutputs.get(i); - float chance = i < outputChances.size() ? outputChances.get(i) : 1.0f; - - // 概率向上取整作为输出数量 - int outputCount = (int) Math.ceil(chance); - - ItemStack output = originalOutput.copy(); - output.setCount(outputCount); - outputItems.add(output); - } - - // 5. 处理输出流体:直接使用原配方的输出流体 - List outputFluids = new ArrayList<>(); - if (!insolatorRecipe.getOutputFluids().isEmpty()) { - outputFluids.add(insolatorRecipe.getOutputFluids().get(0).copy()); - } - - // 6. 处理能量消耗:使用原配方的能量值 - int energy = insolatorRecipe.getEnergy(); - - // 7. 处理处理时间:使用原配方的时间 - int processTime = 40; // 默认为40tick - - // 8. 处理催化剂:使用无用锭标签作为催化剂 - Ingredient catalyst = Ingredient.of(ModIngots.USELESS_INGOT_TIER_1.get(), - ModIngots.USELESS_INGOT_TIER_2.get(), - ModIngots.USELESS_INGOT_TIER_3.get(), - ModIngots.USELESS_INGOT_TIER_4.get(), - ModIngots.USELESS_INGOT_TIER_5.get(), - ModIngots.USELESS_INGOT_TIER_6.get(), - ModIngots.USELESS_INGOT_TIER_7.get(), - ModIngots.USELESS_INGOT_TIER_8.get(), - ModIngots.USELESS_INGOT_TIER_9.get(), - ModIngots.USEFUL_INGOT.get()); - int catalystCount = 1; - - // 9. 处理模具:要求放入"thermal:machine_insolator" - // 创建物品的ResourceLocation,然后获取对应的物品 - ResourceLocation machineInsolatorId = ResourceLocation.fromNamespaceAndPath("thermal", "machine_insolator"); - net.minecraft.world.item.Item machineInsolator = ForgeRegistries.ITEMS.getValue(machineInsolatorId); - if (machineInsolator == null) { - throw new RuntimeException("Could not find item: " + machineInsolatorId); - } - Ingredient mold = Ingredient.of(machineInsolator); - - // 创建并返回转换后的高级合金炉配方,设置isInsolatorRecipe为true - AdvancedAlloyFurnaceRecipe convertedRecipe = new AdvancedAlloyFurnaceRecipe( - newId, - inputIngredients, - inputItemCounts, - inputFluids, - outputItems, - outputFluids, - energy, - processTime, - catalyst, - catalystCount, - mold, - true - ); - - convertedRecipes.add(convertedRecipe); - } - } - } catch (Exception e) { - // 捕获所有异常,避免崩溃 - e.printStackTrace(); - System.err.println("Failed to convert insolator recipes: " + e.getMessage()); - } - - return convertedRecipes; - } - - /** - * 将热力感应炉(Smelter)配方转换为高级合金炉配方 - * - * @param recipeManager 游戏配方管理器 - * @return 转换后的高级合金炉配方列表 - */ - public static List convertSmelterRecipes(RecipeManager recipeManager) { - List convertedRecipes = new ArrayList<>(); - - // 直接获取所有配方,然后筛选出SmelterRecipe类型的配方 - try { - // 获取SmelterRecipe类 - Class smelterRecipeClass = Class.forName("cofh.thermal.core.util.recipes.machine.SmelterRecipe"); - - // 获取所有配方 - Collection> allRecipes = recipeManager.getRecipes(); - - // 筛选出SmelterRecipe类型的配方 - for (net.minecraft.world.item.crafting.Recipe recipe : allRecipes) { - // 检查配方是否为SmelterRecipe类型 - if (smelterRecipeClass.isInstance(recipe)) { - // 使用反射获取SmelterRecipe的方法 - Object smelterRecipe = recipe; - - // 1. 获取配方ID - ResourceLocation originalId = recipe.getId(); - ResourceLocation newId = new ResourceLocation( - "useless_mod", - "thermal_smelter_" + originalId.getNamespace() + "_" + originalId.getPath() - ); - - // 2. 获取输入物品 - List inputIngredients = new ArrayList<>(); - List inputItemCounts = new ArrayList<>(); - - Method getInputItemsMethod = smelterRecipeClass.getMethod("getInputItems"); - List originalInputs = (List) getInputItemsMethod.invoke(smelterRecipe); - - // 3. 处理输入物品:从原始配方中读取需求数量并乘以64倍 - for (int i = 0; i < originalInputs.size(); i++) { - Ingredient ingredient = originalInputs.get(i); - inputIngredients.add(ingredient); - - // 默认数量为1 - long originalCount = 1; - - // 检查是否为IngredientWithCount类型,获取原始数量 - try { - Class ingredientWithCountClass = Class.forName("cofh.lib.util.crafting.IngredientWithCount"); - if (ingredientWithCountClass.isInstance(ingredient)) { - // 使用反射获取count字段值 - java.lang.reflect.Field countField = ingredientWithCountClass.getDeclaredField("count"); - countField.setAccessible(true); - originalCount = countField.getLong(ingredient); - } - } catch (Exception e) { - // 如果不是IngredientWithCount类型,使用默认数量1 - originalCount = 1; - } - - // 输入数量变为原来的64倍 - long newCount = originalCount * 64; - inputItemCounts.add(newCount); - } - - // 4. 处理输入流体 - List inputFluids = new ArrayList<>(); - Method getInputFluidsMethod = smelterRecipeClass.getMethod("getInputFluids"); - List inputFluidsList = (List) getInputFluidsMethod.invoke(smelterRecipe); - - if (!inputFluidsList.isEmpty()) { - // 获取第一个流体输入 - Object fluidIngredient = inputFluidsList.get(0); - Class fluidIngredientClass = fluidIngredient.getClass(); - - // 获取流体栈数组 - Method getFluidsMethod = fluidIngredientClass.getMethod("getFluids"); - FluidStack[] fluidStacks = (FluidStack[]) getFluidsMethod.invoke(fluidIngredient); - - if (fluidStacks.length > 0) { - FluidStack fluidStack = fluidStacks[0].copy(); - // 输入流体数量乘以64倍 - fluidStack.setAmount(fluidStack.getAmount() * 64); - inputFluids.add(fluidStack); - } - } - - // 5. 处理输出物品:数量乘以64倍 - List outputItems = new ArrayList<>(); - Method getOutputItemsMethod = smelterRecipeClass.getMethod("getOutputItems"); - List originalOutputs = (List) getOutputItemsMethod.invoke(smelterRecipe); - - for (ItemStack originalOutput : originalOutputs) { - ItemStack output = originalOutput.copy(); - // 输出数量乘以64倍 - output.setCount(output.getCount() * 64); - outputItems.add(output); - } - - // 6. 处理输出流体:数量乘以64倍 - List outputFluids = new ArrayList<>(); - Method getOutputFluidsMethod = smelterRecipeClass.getMethod("getOutputFluids"); - List originalOutputFluids = (List) getOutputFluidsMethod.invoke(smelterRecipe); - - if (!originalOutputFluids.isEmpty()) { - FluidStack fluidStack = originalOutputFluids.get(0).copy(); - // 输出流体数量乘以64倍 - fluidStack.setAmount(fluidStack.getAmount() * 64); - outputFluids.add(fluidStack); - } - - // 7. 处理能量消耗:使用原配方的能量值,乘以64倍 - Method getEnergyMethod = smelterRecipeClass.getMethod("getEnergy"); - int energy = (int) getEnergyMethod.invoke(smelterRecipe) * 64; - - // 8. 处理处理时间:使用原配方的时间 - int processTime = 40; // 默认为40tick - - // 9. 处理催化剂:使用无用锭标签作为催化剂 - Ingredient catalyst = Ingredient.of(ModIngots.USELESS_INGOT_TIER_1.get(), - ModIngots.USELESS_INGOT_TIER_2.get(), - ModIngots.USELESS_INGOT_TIER_3.get(), - ModIngots.USELESS_INGOT_TIER_4.get(), - ModIngots.USELESS_INGOT_TIER_5.get(), - ModIngots.USELESS_INGOT_TIER_6.get(), - ModIngots.USELESS_INGOT_TIER_7.get(), - ModIngots.USELESS_INGOT_TIER_8.get(), - ModIngots.USELESS_INGOT_TIER_9.get(), - ModIngots.USEFUL_INGOT.get()); - int catalystCount = 1; - - // 10. 处理模具:要求放入"thermal:machine_smelter" - // 创建物品的ResourceLocation,然后获取对应的物品 - ResourceLocation machineSmelterId = ResourceLocation.fromNamespaceAndPath("thermal", "machine_smelter"); - net.minecraft.world.item.Item machineSmelter = ForgeRegistries.ITEMS.getValue(machineSmelterId); - if (machineSmelter == null) { - throw new RuntimeException("Could not find item: " + machineSmelterId); - } - Ingredient mold = Ingredient.of(machineSmelter); - - // 创建并返回转换后的高级合金炉配方 - AdvancedAlloyFurnaceRecipe convertedRecipe = new AdvancedAlloyFurnaceRecipe( - newId, - inputIngredients, - inputItemCounts, - inputFluids, - outputItems, - outputFluids, - energy, - processTime, - catalyst, - catalystCount, - mold, - false, - false - ); - - convertedRecipes.add(convertedRecipe); - } - } - } catch (Exception e) { - // 捕获所有异常,避免崩溃 - e.printStackTrace(); - System.err.println("Failed to convert smelter recipes: " + e.getMessage()); - } - - return convertedRecipes; - } - - /** - * 将热力多驱冲压机(Press)配方转换为高级合金炉配方 - * - * @param recipeManager 游戏配方管理器 - * @return 转换后的高级合金炉配方列表 - */ - public static List convertPressRecipes(RecipeManager recipeManager) { - List convertedRecipes = new ArrayList<>(); - - // 直接获取所有配方,然后筛选出PressRecipe类型的配方 - try { - // 获取PressRecipe类 - Class pressRecipeClass = Class.forName("cofh.thermal.core.util.recipes.machine.PressRecipe"); - - // 获取所有配方 - Collection> allRecipes = recipeManager.getRecipes(); - - // 筛选出PressRecipe类型的配方 - for (net.minecraft.world.item.crafting.Recipe recipe : allRecipes) { - // 检查配方是否为PressRecipe类型 - if (pressRecipeClass.isInstance(recipe)) { - // 使用反射获取PressRecipe的方法 - Object pressRecipe = recipe; - - // 1. 获取配方ID - ResourceLocation originalId = recipe.getId(); - ResourceLocation newId = new ResourceLocation( - "useless_mod", - "thermal_press_" + originalId.getNamespace() + "_" + originalId.getPath() - ); - - // 2. 获取输入物品 - List inputIngredients = new ArrayList<>(); - List inputItemCounts = new ArrayList<>(); - - Method getInputItemsMethod = pressRecipeClass.getMethod("getInputItems"); - List originalInputs = (List) getInputItemsMethod.invoke(pressRecipe); - - // 3. 处理输入物品:从原始配方中读取需求数量并乘以64倍 - for (int i = 0; i < originalInputs.size(); i++) { - Ingredient ingredient = originalInputs.get(i); - inputIngredients.add(ingredient); - - // 默认数量为1 - long originalCount = 1; - - // 检查是否为IngredientWithCount类型,获取原始数量 - try { - Class ingredientWithCountClass = Class.forName("cofh.lib.util.crafting.IngredientWithCount"); - if (ingredientWithCountClass.isInstance(ingredient)) { - // 使用反射获取count字段值 - java.lang.reflect.Field countField = ingredientWithCountClass.getDeclaredField("count"); - countField.setAccessible(true); - originalCount = countField.getLong(ingredient); - } - } catch (Exception e) { - // 如果不是IngredientWithCount类型,使用默认数量1 - originalCount = 1; - } - - // 输入数量变为原来的64倍 - long newCount = originalCount * 64; - inputItemCounts.add(newCount); - } - - // 4. 处理输入流体 - List inputFluids = new ArrayList<>(); - Method getInputFluidsMethod = pressRecipeClass.getMethod("getInputFluids"); - List inputFluidsList = (List) getInputFluidsMethod.invoke(pressRecipe); - - if (!inputFluidsList.isEmpty()) { - // 获取第一个流体输入 - Object fluidIngredient = inputFluidsList.get(0); - Class fluidIngredientClass = fluidIngredient.getClass(); - - // 获取流体栈数组 - Method getFluidsMethod = fluidIngredientClass.getMethod("getFluids"); - FluidStack[] fluidStacks = (FluidStack[]) getFluidsMethod.invoke(fluidIngredient); - - if (fluidStacks.length > 0) { - FluidStack fluidStack = fluidStacks[0].copy(); - // 输入流体数量乘以64倍 - fluidStack.setAmount(fluidStack.getAmount() * 64); - inputFluids.add(fluidStack); - } - } - - // 5. 处理输出物品:数量乘以64倍 - List outputItems = new ArrayList<>(); - Method getOutputItemsMethod = pressRecipeClass.getMethod("getOutputItems"); - List originalOutputs = (List) getOutputItemsMethod.invoke(pressRecipe); - - for (ItemStack originalOutput : originalOutputs) { - ItemStack output = originalOutput.copy(); - // 输出数量乘以64倍 - output.setCount(output.getCount() * 64); - outputItems.add(output); - } - - // 6. 处理输出流体:数量乘以64倍 - List outputFluids = new ArrayList<>(); - Method getOutputFluidsMethod = pressRecipeClass.getMethod("getOutputFluids"); - List originalOutputFluids = (List) getOutputFluidsMethod.invoke(pressRecipe); - - if (!originalOutputFluids.isEmpty()) { - FluidStack fluidStack = originalOutputFluids.get(0).copy(); - // 输出流体数量乘以64倍 - fluidStack.setAmount(fluidStack.getAmount() * 64); - outputFluids.add(fluidStack); - } - - // 7. 处理能量消耗:使用原配方的能量值,乘以64倍 - Method getEnergyMethod = pressRecipeClass.getMethod("getEnergy"); - int energy = (int) getEnergyMethod.invoke(pressRecipe) * 64; - - // 8. 处理处理时间:使用原配方的时间 - int processTime = 40; // 默认为40tick - - // 9. 处理催化剂:使用无用锭标签作为催化剂 - Ingredient catalyst = Ingredient.of(ModIngots.USELESS_INGOT_TIER_1.get(), - ModIngots.USELESS_INGOT_TIER_2.get(), - ModIngots.USELESS_INGOT_TIER_3.get(), - ModIngots.USELESS_INGOT_TIER_4.get(), - ModIngots.USELESS_INGOT_TIER_5.get(), - ModIngots.USELESS_INGOT_TIER_6.get(), - ModIngots.USELESS_INGOT_TIER_7.get(), - ModIngots.USELESS_INGOT_TIER_8.get(), - ModIngots.USELESS_INGOT_TIER_9.get(), - ModIngots.USEFUL_INGOT.get()); - int catalystCount = 1; - - // 10. 处理模具:将底部输入栏对应的物品(索引为1)作为模具 - Ingredient mold; - if (originalInputs.size() > 1) { - // 如果有底部输入栏物品,将其作为模具 - mold = originalInputs.get(1); - // 移除底部输入栏物品,不再作为输入物品 - inputIngredients.remove(1); - inputItemCounts.remove(1); - } else { - // 否则使用默认模具:金属板模具 - ResourceLocation metalMoldPlateId = ResourceLocation.fromNamespaceAndPath("useless_mod", "metal_mold_plate"); - net.minecraft.world.item.Item metalMoldPlate = ForgeRegistries.ITEMS.getValue(metalMoldPlateId); - if (metalMoldPlate == null) { - throw new RuntimeException("Could not find item: " + metalMoldPlateId); - } - mold = Ingredient.of(metalMoldPlate); - } - - // 创建并返回转换后的高级合金炉配方 - AdvancedAlloyFurnaceRecipe convertedRecipe = new AdvancedAlloyFurnaceRecipe( - newId, - inputIngredients, - inputItemCounts, - inputFluids, - outputItems, - outputFluids, - energy, - processTime, - catalyst, - catalystCount, - mold, - false - ); - - convertedRecipes.add(convertedRecipe); - } - } - } catch (Exception e) { - // 捕获所有异常,避免崩溃 - e.printStackTrace(); - System.err.println("Failed to convert press recipes: " + e.getMessage()); - } - - return convertedRecipes; - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/compat/ubesdelight/UbesDelightRecipeCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/ubesdelight/UbesDelightRecipeCompatLoader.java new file mode 100644 index 00000000..28e44c77 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ubesdelight/UbesDelightRecipeCompatLoader.java @@ -0,0 +1,16 @@ +package com.sorrowmist.useless.compat.ubesdelight; + +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.ubesdelight.BakingMatRecipeAdapter; + +/** Registers Ube's Delight recipe adapters after the optional mod is detected. */ +public final class UbesDelightRecipeCompatLoader { + private UbesDelightRecipeCompatLoader() { + } + + public static void register() { + AlloyFurnaceRecipeManager.getInstance().registerAdapter( + new BakingMatRecipeAdapter(), RecipeSourceIds.UBES_DELIGHT); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/ufo/UfoRecipeCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/ufo/UfoRecipeCompatLoader.java new file mode 100644 index 00000000..18b03fa3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/ufo/UfoRecipeCompatLoader.java @@ -0,0 +1,26 @@ +package com.sorrowmist.useless.compat.ufo; + +import com.raishxn.ufo.recipe.UniversalMultiblockMachineKind; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.ufo.DimensionalMatterAssemblerRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.ufo.QMFRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.ufo.StellarSimulationRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.ufo.UniversalMultiblockRecipeAdapter; + +/** Registers UFO adapters only after the optional UFO mod has been detected. */ +public final class UfoRecipeCompatLoader { + private UfoRecipeCompatLoader() { + } + + public static void register() { + AlloyFurnaceRecipeManager manager = AlloyFurnaceRecipeManager.getInstance(); + manager.registerAdapter(new QMFRecipeAdapter(), RecipeSourceIds.UFO); + manager.registerAdapter(new UniversalMultiblockRecipeAdapter( + UniversalMultiblockMachineKind.QMF), RecipeSourceIds.UFO); + manager.registerAdapter(new UniversalMultiblockRecipeAdapter( + UniversalMultiblockMachineKind.QUANTUM_CRYOFORGE), RecipeSourceIds.UFO); + manager.registerAdapter(new StellarSimulationRecipeAdapter(), RecipeSourceIds.UFO); + manager.registerAdapter(new DimensionalMatterAssemblerRecipeAdapter(), RecipeSourceIds.UFO); + } +} diff --git a/src/main/java/com/sorrowmist/useless/compat/youkaishomecoming/YoukaisHomecomingRecipeCompatLoader.java b/src/main/java/com/sorrowmist/useless/compat/youkaishomecoming/YoukaisHomecomingRecipeCompatLoader.java new file mode 100644 index 00000000..cd823867 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/compat/youkaishomecoming/YoukaisHomecomingRecipeCompatLoader.java @@ -0,0 +1,28 @@ +package com.sorrowmist.useless.compat.youkaishomecoming; + +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.youkaishomecoming.DryingRackRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.delight.youkaishomecoming.BasinRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.delight.youkaishomecoming.CuisineRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.delight.youkaishomecoming.KettleRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.delight.youkaishomecoming.PotCookingRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.delight.youkaishomecoming.SimpleFermentationRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.delight.youkaishomecoming.SteamingRecipeAdapter; + +/** Registers Youkai's Homecoming recipe adapters after the optional mod is detected. */ +public final class YoukaisHomecomingRecipeCompatLoader { + private YoukaisHomecomingRecipeCompatLoader() { + } + + public static void register() { + AlloyFurnaceRecipeManager manager = AlloyFurnaceRecipeManager.getInstance(); + manager.registerAdapter(new KettleRecipeAdapter(), RecipeSourceIds.YOUKAI_HOMECOMING); + manager.registerAdapter(new BasinRecipeAdapter(), RecipeSourceIds.YOUKAI_HOMECOMING); + manager.registerAdapter(new SteamingRecipeAdapter(), RecipeSourceIds.YOUKAI_HOMECOMING); + manager.registerAdapter(new CuisineRecipeAdapter(), RecipeSourceIds.YOUKAI_HOMECOMING); + manager.registerAdapter(new DryingRackRecipeAdapter(), RecipeSourceIds.YOUKAI_HOMECOMING); + manager.registerAdapter(new SimpleFermentationRecipeAdapter(), RecipeSourceIds.YOUKAI_HOMECOMING); + manager.registerAdapter(new PotCookingRecipeAdapter(), RecipeSourceIds.YOUKAI_HOMECOMING); + } +} diff --git a/src/main/java/com/sorrowmist/useless/config/ConfigManager.java b/src/main/java/com/sorrowmist/useless/config/ConfigManager.java deleted file mode 100644 index acfbbf81..00000000 --- a/src/main/java/com/sorrowmist/useless/config/ConfigManager.java +++ /dev/null @@ -1,320 +0,0 @@ -package com.sorrowmist.useless.config; - -import com.sorrowmist.useless.utils.ThermalDependencyHelper; -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.block.Blocks; -import net.minecraftforge.common.ForgeConfigSpec; -import net.minecraftforge.fml.common.Mod; - -@Mod.EventBusSubscriber -public class ConfigManager { - private static final ForgeConfigSpec.Builder BUILDER = new ForgeConfigSpec.Builder(); - public static final ForgeConfigSpec SPEC; - - // 维度生成配置 - public static final ForgeConfigSpec.ConfigValue BORDER_BLOCK; - public static final ForgeConfigSpec.ConfigValue FILL_BLOCK; - public static final ForgeConfigSpec.ConfigValue CENTER_BLOCK; - // 塑料平台生成层数 - public static final ForgeConfigSpec.IntValue PLATFORM_LAYERS; - // 塑料平台起始Y值 - public static final ForgeConfigSpec.IntValue PLATFORM_START_Y; - // 是否生成基岩层 - public static final ForgeConfigSpec.BooleanValue GENERATE_BEDROCK; - - // 效果配置 - public static final ForgeConfigSpec.BooleanValue ENABLE_POTION_EFFECTS; - public static final ForgeConfigSpec.BooleanValue ENABLE_FLIGHT_EFFECT; - - // 植物盆生长速度配置 - public static final ForgeConfigSpec.IntValue BOTANY_POT_GROWTH_MULTIPLIER; - - // 矩阵样板数量配置 - public static final ForgeConfigSpec.IntValue MATRIX_PATTERN_COUNT; - - // 牛排工具连锁挖掘配置 - public static final ForgeConfigSpec.IntValue CHAIN_MINING_RANGE_X; - public static final ForgeConfigSpec.IntValue CHAIN_MINING_RANGE_Y; - public static final ForgeConfigSpec.IntValue CHAIN_MINING_RANGE_Z; - public static final ForgeConfigSpec.IntValue CHAIN_MINING_MAX_BLOCKS; - // 牛排工具附魔等级配置 - public static final ForgeConfigSpec.IntValue FORTUNE_LEVEL; - public static final ForgeConfigSpec.IntValue LOOTING_LEVEL; - - // Mekanism 升级配置 - public static final ForgeConfigSpec.IntValue TIME_MULTIPLIER; - public static final ForgeConfigSpec.IntValue ELECTRICITY_MULTIPLIER; - public static final ForgeConfigSpec.IntValue CAPACITY_MULTIPLIER; - public static final ForgeConfigSpec.IntValue MAX_UPGRADE; - - // Thermal Parallel 配置 - 只在热力系列安装时注册 - public static final ForgeConfigSpec.BooleanValue PARALLEL_INCREASE_ITEM_CAPACITY; - public static final ForgeConfigSpec.BooleanValue PARALLEL_INCREASE_FLUID_CAPACITY; - public static final ForgeConfigSpec.BooleanValue PARALLEL_INCREASE_ENERGY_CAPACITY; - public static final ForgeConfigSpec.BooleanValue PARALLEL_INCREASE_ITEM_TRANSFER; - public static final ForgeConfigSpec.BooleanValue PARALLEL_INCREASE_FLUID_TRANSFER; - public static final ForgeConfigSpec.BooleanValue PARALLEL_INCREASE_ENERGY_TRANSFER; - public static final ForgeConfigSpec.BooleanValue PARALLEL_INCREASE_ENERGY_CONSUMPTION; - public static final ForgeConfigSpec.BooleanValue BASE_MOD_AFFECT_PARALLEL; - public static final ForgeConfigSpec.BooleanValue ADD_EXTRA_PARALLEL_AUGMENTS_TO_TAB; - - static { - BUILDER.push("维度生成设置"); - - BORDER_BLOCK = BUILDER - .comment("边框方块,若不存在则使用蓝色羊毛") - .define("边框方块", "useless_mod:aqua_glow_plastic"); - - FILL_BLOCK = BUILDER - .comment("填充方块,若不存在则使用白色羊毛") - .define("填充方块", "useless_mod:white_glow_plastic"); - - CENTER_BLOCK = BUILDER - .comment("中心方块,若不存在则使用灰色羊毛") - .define("中心方块", "useless_mod:light_gray_glow_plastic"); - - PLATFORM_LAYERS = BUILDER - .comment("塑料平台生成层数") - .defineInRange("塑料平台层数", 69, 1, 256); - - PLATFORM_START_Y = BUILDER - .comment("平台起始Y值(若无基岩实际会比该数值高1)") - .defineInRange("平台起始Y值", -64, -64, 256); - - GENERATE_BEDROCK = BUILDER - .comment("是否生成基岩层,默认生成") - .define("生成基岩层", true); - - BUILDER.pop(); - - BUILDER.push("游戏机制设置"); - - BOTANY_POT_GROWTH_MULTIPLIER = BUILDER - .comment("植物盆生长倍率 - 1.0为原版速度,2.0为2倍速度") - .defineInRange("植物盆生长倍率(仅限非附属的植物盆)", 1, 1, Integer.MAX_VALUE); - - MATRIX_PATTERN_COUNT = BUILDER - .comment("矩阵样板槽位倍数 - 减少数量时请保持槽位空!否则可能会造成样板丢失") - .defineInRange("矩阵样板槽位倍数", 1, 1, 100); - - // 牛排工具连锁挖掘配置 - BUILDER.push("牛排工具设置"); - - CHAIN_MINING_RANGE_X = BUILDER - .comment("连锁挖掘的X轴范围半径") - .defineInRange("连锁挖掘X轴范围", 8, 1, 255); - - CHAIN_MINING_RANGE_Y = BUILDER - .comment("连锁挖掘的Y轴范围半径") - .defineInRange("连锁挖掘Y轴范围", 8, 1, 255); - - CHAIN_MINING_RANGE_Z = BUILDER - .comment("连锁挖掘的Z轴范围半径") - .defineInRange("连锁挖掘Z轴范围", 8, 1, 255); - - CHAIN_MINING_MAX_BLOCKS = BUILDER - .comment("连锁挖掘的最大方块数量") - .defineInRange("连锁挖掘最大方块数量", 1000, 1, 10000); - - // 牛排工具附魔等级配置 - FORTUNE_LEVEL = BUILDER - .comment("牛排工具时运附魔等级") - .defineInRange("牛排工具时运等级", 10, 1, 127); - - LOOTING_LEVEL = BUILDER - .comment("牛排工具抢夺附魔等级") - .defineInRange("牛排工具抢夺等级", 10, 1, 127); - - BUILDER.pop(); - - BUILDER.pop(); - - BUILDER.push("Mekanism升级设置"); - - TIME_MULTIPLIER = BUILDER - .comment("速度升级增强倍率") - .defineInRange("速度平均每8个升级最终效果乘以一次该倍率", 1, 1, Integer.MAX_VALUE); - - ELECTRICITY_MULTIPLIER = BUILDER - .comment("能量升级节电增强倍率") - .defineInRange("节电平均每8个升级最终效果乘以一次该倍率", 1, 1, Integer.MAX_VALUE); - - CAPACITY_MULTIPLIER = BUILDER - .comment("能量升级储电增强倍率") - .defineInRange("储电平均每8个升级最终效果乘以一次该倍率", 1, 1, Integer.MAX_VALUE); - - MAX_UPGRADE = BUILDER - .comment("机器可接受的最大速度/能量升级数量,重启游戏生效") - .defineInRange("最大升级数量", 16, 1, 64); - - BUILDER.pop(); - - BUILDER.push("效果设置"); - - ENABLE_POTION_EFFECTS = BUILDER - .comment("是否启用药水效果(饱和、生命恢复、夜视、抗火、水下呼吸、抗性提升)") - .define("启用药水效果", true); - - ENABLE_FLIGHT_EFFECT = BUILDER - .comment("是否启用飞行效果") - .define("启用飞行效果", true); - - BUILDER.pop(); - - // 只在热力系列安装时添加 Thermal Parallel 配置 - if (ThermalDependencyHelper.isAnyThermalModLoaded()) { - BUILDER.push("Thermal Parallel 设置"); - - PARALLEL_INCREASE_ENERGY_CONSUMPTION = BUILDER - .comment("如果设置为true,则机器的能耗会随并行增多而成倍增加") - .define("并行增加能耗", false); - - BASE_MOD_AFFECT_PARALLEL = BUILDER - .comment("如果设置为true,则机器的并行数量会被整合组件带来的基础倍率增幅") - .define("基础倍率影响并行", false); - - ADD_EXTRA_PARALLEL_AUGMENTS_TO_TAB = BUILDER - .comment("如果设置为true,则更高等级的并行插件会被加进创造模式物品栏") - .define("添加额外并行增强到标签", false); - - PARALLEL_INCREASE_ITEM_CAPACITY = BUILDER - .comment("如果设置为true,则并行升级会同步提高机器的输入/输出物品存储上限") - .define("并行增加物品容量", true); - - PARALLEL_INCREASE_FLUID_CAPACITY = BUILDER - .comment("如果设置为true,则并行升级会同步提高机器的流体存储上限") - .define("并行增加流体容量", true); - - PARALLEL_INCREASE_ENERGY_CAPACITY = BUILDER - .comment("如果设置为true,则并行升级会同步提高机器的能量存储上限") - .define("并行增加能量容量", false); - - PARALLEL_INCREASE_ITEM_TRANSFER = BUILDER - .comment("如果设置为true,则并行升级会同步提高机器进行自动提取/弹出物品的速率上限") - .define("并行增加物品传输", true); - - PARALLEL_INCREASE_FLUID_TRANSFER = BUILDER - .comment("如果设置为true,则并行升级会同步提高机器进行自动提取/弹出流体的速率上限") - .define("并行增加流体传输", true); - - PARALLEL_INCREASE_ENERGY_TRANSFER = BUILDER - .comment("如果设置为true,则并行升级会同步提高机器的能量传输上限") - .define("并行增加能量传输", true); - - BUILDER.pop(); - } else { - // 如果热力系列未安装,设置默认值 - PARALLEL_INCREASE_ENERGY_CONSUMPTION = null; - BASE_MOD_AFFECT_PARALLEL = null; - ADD_EXTRA_PARALLEL_AUGMENTS_TO_TAB = null; - PARALLEL_INCREASE_ITEM_CAPACITY = null; - PARALLEL_INCREASE_FLUID_CAPACITY = null; - PARALLEL_INCREASE_ENERGY_CAPACITY = null; - PARALLEL_INCREASE_ITEM_TRANSFER = null; - PARALLEL_INCREASE_FLUID_TRANSFER = null; - PARALLEL_INCREASE_ENERGY_TRANSFER = null; - } - - SPEC = BUILDER.build(); - } - - // 获取方块方法 - public static Block getBorderBlock() { - return getBlockFromString(BORDER_BLOCK.get(), Blocks.BLUE_WOOL); - } - - public static Block getFillBlock() { - return getBlockFromString(FILL_BLOCK.get(), Blocks.WHITE_WOOL); - } - - public static Block getCenterBlock() { - return getBlockFromString(CENTER_BLOCK.get(), Blocks.GRAY_WOOL); - } - - // 获取配置值方法 - public static int getBotanyPotGrowthMultiplier() { - return BOTANY_POT_GROWTH_MULTIPLIER.get(); - } - - public static int getMatrixPatternCount() { - return MATRIX_PATTERN_COUNT.get(); - } - - public static int getTimeMultiplier() { - return TIME_MULTIPLIER.get(); - } - - public static int getElectricityMultiplier() { - return ELECTRICITY_MULTIPLIER.get(); - } - - public static int getChainMiningMaxBlocks() { - return CHAIN_MINING_MAX_BLOCKS.get(); - } - - public static int getCapacityMultiplier() { - return CAPACITY_MULTIPLIER.get(); - } - - public static int getMaxUpgrade() { - return MAX_UPGRADE.get(); - } - - // 获取效果配置 - public static boolean enablePotionEffects() { - return ENABLE_POTION_EFFECTS.get(); - } - - public static boolean enableFlightEffect() { - return ENABLE_FLIGHT_EFFECT.get(); - } - - // 获取连锁挖掘范围配置 - public static int getChainMiningRangeX() { - return CHAIN_MINING_RANGE_X.get(); - } - - public static int getChainMiningRangeY() { - return CHAIN_MINING_RANGE_Y.get(); - } - - public static int getChainMiningRangeZ() { - return CHAIN_MINING_RANGE_Z.get(); - } - - // 获取牛排工具附魔等级配置 - public static int getFortuneLevel() { - return FORTUNE_LEVEL.get(); - } - - public static int getLootingLevel() { - return LOOTING_LEVEL.get(); - } - - // 获取塑料平台层数 - public static int getPlatformLayers() { - return PLATFORM_LAYERS.get(); - } - - // 获取是否生成基岩层 - public static boolean shouldGenerateBedrock() { - return GENERATE_BEDROCK.get(); - } - - // 获取塑料平台起始Y值 - public static int getPlatformStartY() { - return PLATFORM_START_Y.get(); - } - - private static Block getBlockFromString(String blockId, Block fallback) { - try { - ResourceLocation location = ResourceLocation.parse(blockId); - Block block = BuiltInRegistries.BLOCK.get(location); - return block != Blocks.AIR ? block : fallback; - } catch (Exception e) { - return fallback; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/content/blockentities/AdvancedAlloyFurnaceBlockEntity.java b/src/main/java/com/sorrowmist/useless/content/blockentities/AdvancedAlloyFurnaceBlockEntity.java new file mode 100644 index 00000000..375664a3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/blockentities/AdvancedAlloyFurnaceBlockEntity.java @@ -0,0 +1,2084 @@ +package com.sorrowmist.useless.content.blockentities; + +import appeng.api.config.Actionable; +import appeng.api.config.PowerMultiplier; +import appeng.api.config.PowerUnits; +import appeng.api.crafting.IPatternDetails; +import appeng.api.crafting.PatternDetailsHelper; +import appeng.api.implementations.blockentities.PatternContainerGroup; +import appeng.api.inventories.InternalInventory; +import appeng.api.networking.GridFlags; +import appeng.api.networking.GridHelper; +import appeng.api.networking.IGrid; +import appeng.api.networking.IGridNode; +import appeng.api.networking.IGridNodeListener; +import appeng.api.networking.IInWorldGridNodeHost; +import appeng.api.networking.IManagedGridNode; +import appeng.api.networking.crafting.ICraftingProvider; +import appeng.api.networking.energy.IEnergyService; +import appeng.api.networking.security.IActionHost; +import appeng.api.networking.security.IActionSource; +import appeng.api.networking.storage.IStorageService; +import appeng.api.stacks.AEFluidKey; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.KeyCounter; +import appeng.api.storage.MEStorage; +import appeng.api.util.AECableType; +import appeng.blockentity.AEBaseBlockEntity; +import appeng.helpers.patternprovider.PatternContainer; +import com.sorrowmist.useless.api.enums.CatalystType; +import com.sorrowmist.useless.api.crafting.SmartDoublingCraftingProvider; +import com.sorrowmist.useless.compat.AppFluxCompat; +import com.sorrowmist.useless.api.enums.FurnaceFace; +import com.sorrowmist.useless.api.enums.FurnaceFaceMode; +import com.sorrowmist.useless.api.enums.RedstoneControlMode; +import com.sorrowmist.useless.content.blocks.AdvancedAlloyFurnaceBlock; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.AdvancedAlloyFurnaceAeManager; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.CraftingTaskContext; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.AlloyFurnaceAeHost; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.CraftingAeOutputTarget; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.GridOutputTarget; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalHandlerView; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalCompatProviders; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalKeyProvider; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalKeyProviders; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalStackView; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.FurnaceChemicalStorage; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.catalyst.CatalystEffectResolver; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.catalyst.ResolvedCatalystEffect; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.execution.AlloyFurnaceRecipeExecutor; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.io.FurnaceAutoIoController; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.io.FurnaceCombinedFluidTankHandler; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.io.FurnaceFaceAccessor; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.io.FurnaceFluidTankHandler; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.io.FurnaceInputPort; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.io.FurnaceOutputPort; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.io.FurnaceSidedFluidHandler; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.io.FurnaceSidedItemHandler; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.parallel.AlloyFurnaceParallelCalculator; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.recipe.AlloyFurnaceRecipeCalculator; +import com.sorrowmist.useless.content.menus.AdvancedAlloyFurnaceMenu; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeCatalog; +import com.sorrowmist.useless.core.config.ConfigManager; +import com.sorrowmist.useless.core.constants.NBTConstants; +import com.sorrowmist.useless.core.component.ExternalInventoryKind; +import com.sorrowmist.useless.core.component.ExternalInventoryReference; +import com.sorrowmist.useless.energy.EnergyManager; +import com.sorrowmist.useless.energy.IEnergyManager; +import com.sorrowmist.useless.init.ModBlockEntities; +import com.sorrowmist.useless.network.AETaskProgressPacket; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientGamePacketListener; +import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import com.sorrowmist.useless.world.inventory.ExternalInventoryStore; +import net.minecraftforge.energy.IEnergyStorage; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.capability.IFluidHandler; +import net.minecraftforge.fluids.capability.templates.FluidTank; +import net.minecraftforge.items.IItemHandler; +import net.minecraftforge.items.ItemStackHandler; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; + +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.CATALYST_SLOT; +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.CHEMICAL_TANK_COUNT; +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.FLUID_TANK_COUNT; +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.INPUT_SLOTS_COUNT; +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.INPUT_SLOTS_START; +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.MOLD_SLOT; +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.OUTPUT_SLOTS_COUNT; +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.OUTPUT_SLOTS_START; +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.PATTERN_SLOTS_COUNT; +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.PATTERN_SLOTS_END; +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.PATTERN_SLOTS_START; +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.TOTAL_SLOTS; + +public class AdvancedAlloyFurnaceBlockEntity extends AEBaseBlockEntity implements MenuProvider, ICraftingProvider, + SmartDoublingCraftingProvider, IInWorldGridNodeHost, + IGridNodeListener, IActionHost, AlloyFurnaceAeHost, + PatternContainer, FurnaceFaceAccessor, FurnaceAutoIoController.Context { + + public static final int MAX_FURNACE_TIER = 10; + public static final int USEFUL_INGOT_FURNACE_TIER = 10; + + // 基础容量配置 + private static final int BASE_FLUID_TANK_CAPACITY = 16000; + private static final long BASE_CHEMICAL_TANK_CAPACITY = BASE_FLUID_TANK_CAPACITY; + private static final long BASE_ENERGY_CAPACITY = 100_000L; + private static final long BASE_ENERGY_MAX_RECEIVE = 10_000L; + private static final long ENERGY_MAX_EXTRACT = 0L; + private static final int ACTIVE_COOLDOWN_TICKS = 5; + private static final int AUTO_OUTPUT_INTERVAL = 1; + private static final int DISPLAY_PARALLEL_CACHE_DURATION = 20; + // 输入未变时配方搜索的兜底重试间隔,覆盖未经变更回调的改动与配置热重载 + private static final int RECIPE_SEARCH_RETRY_TICKS = 20; + private final FluidTank[] inputFluidTanks = new FluidTank[FLUID_TANK_COUNT]; + private final FluidTank[] outputFluidTanks = new FluidTank[FLUID_TANK_COUNT]; + private final FurnaceChemicalStorage inputChemicalStorage; + private final FurnaceChemicalStorage outputChemicalStorage; + private final IEnergyManager energyManager = EnergyManager.builder() + .capacity(BASE_ENERGY_CAPACITY) + .maxReceive(BASE_ENERGY_MAX_RECEIVE) + .maxExtract(ENERGY_MAX_EXTRACT) + .onChange(this::setChanged) + .build(); + private final AdvancedAlloyFurnaceData data = new AdvancedAlloyFurnaceData(this); + private final ItemStackHandler itemHandler; + private final AdvancedAlloyFurnaceAeManager aeManager; + private final FurnaceAutoIoController autoIoController; + private final AlloyFurnaceRecipeCalculator recipeCalculator; + // ==================== AE网络支持 ==================== + private final IManagedGridNode mainNode; + private final IActionSource actionSource; + // 升级后的容量(根据阶级动态计算) + private int fluidTankCapacity = BASE_FLUID_TANK_CAPACITY; + private long chemicalTankCapacity = BASE_CHEMICAL_TANK_CAPACITY; + private long energyCapacity = BASE_ENERGY_CAPACITY; + private long energyMaxReceive = BASE_ENERGY_MAX_RECEIVE; + private int progress = 0; + private int maxProgress = 200; + private int currentParallel = 1; + private boolean hasMold = false; + @Nullable + private AdvancedAlloyFurnaceRecipe currentRecipe; + // 缓存催化剂解析结果,避免每tick重复解析 + @Nullable + private ResolvedCatalystEffect cachedCatalystEffect; + private int cachedParallel = 1; + // UI并行数缓存,减少无谓的配方查找 + private int cachedDisplayParallel = 1; + private int displayParallelCacheTick = 0; + private boolean isUselessIngotRecipe = false; + private int targetUselessIngotTier = 0; + private long accumulatedEnergy = 0; + // 活跃状态冷却计时器,用于避免配方切换时的闪烁 + private int activeCooldown = 0; + // 熔炉阶级 0-10,0为基础等级,10为有用锭 long 能量等级 + private int furnaceTier = 0; + // 自动输出计时器 + private int autoOutputTickCounter = 0; + private boolean isConnectedToAE = false; + private long itemStorageRevision; + // 状态修订号:物品/流体/化学品/阶级/配方目录变化时递增,配方搜索节流与自动 IO 退避据此判断状态是否变化 + private long recipeInputRevision; + // 上次配方搜索时的修订号与游戏时间 + private long recipeSearchRevision = -1L; + private long recipeSearchTick; + // 六个面的输入输出模式(按FurnaceFace索引) + private final FurnaceFaceMode[] faceModes = new FurnaceFaceMode[FurnaceFace.COUNT]; + private long recipeCatalogGeneration = -1L; + // 自动输入开关 + private boolean autoInputEnabled = false; + // 自动输出开关 + private boolean autoOutputEnabled = false; + // 红石控制模式 + private RedstoneControlMode redstoneControlMode = RedstoneControlMode.DISABLED; + // 产物是否回AE + private boolean returnOutputToAe = true; + private boolean dropDataCaptured; + @Nullable + private ExternalInventoryReference inventoryReference; + @Nullable + private CompoundTag pendingLegacyInventory; + private boolean externalInventoryLoaded; + /** 延迟任务数据是否已处理完毕;配方目录未就绪时保持 false,由 tick 逐 tick 重试。 */ + private boolean deferredTasksLoaded; + + public AdvancedAlloyFurnaceBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.ADVANCED_ALLOY_FURNACE.get(), pos, state); + + // 初始化物品处理器(支持高堆叠数量) + this.itemHandler = new HighStackItemStackHandler( + TOTAL_SLOTS, + MOLD_SLOT, + CATALYST_SLOT, + INPUT_SLOTS_START, + INPUT_SLOTS_COUNT, + PATTERN_SLOTS_START, + PATTERN_SLOTS_END, + this::onItemStorageChanged, + slot -> this.updateMoldState(), + null, + slot -> this.updatePatterns() + ); + + // AE2 Integration - 创建动作源 + this.actionSource = IActionSource.ofMachine(this); + + // AE2 Integration - 创建网格节点(使用GridHelper确保正确初始化) + this.mainNode = GridHelper.createManagedNode(this, this) + .setInWorldNode(true) + .setTagName("node") + .setFlags(GridFlags.REQUIRE_CHANNEL) + .addService(ICraftingProvider.class, this); + this.aeManager = new AdvancedAlloyFurnaceAeManager(this); + + this.inputChemicalStorage = ChemicalCompatProviders.get().createStorage( + BASE_CHEMICAL_TANK_CAPACITY, this::onChemicalStorageChanged); + this.outputChemicalStorage = ChemicalCompatProviders.get().createStorage( + BASE_CHEMICAL_TANK_CAPACITY, this::onChemicalStorageChanged); + + // 初始化时应用当前阶级的容量 + this.updateCapacityByTier(); + for (int i = 0; i < FLUID_TANK_COUNT; i++) { + this.inputFluidTanks[i] = this.createTank(i, true); + this.outputFluidTanks[i] = this.createTank(i, false); + } + + // 初始化所有面模式为禁止 + Arrays.fill(this.faceModes, FurnaceFaceMode.DISABLED); + + // 初始化自动输入输出控制器 + this.autoIoController = new FurnaceAutoIoController( + this, this.itemHandler, this.inputFluidTanks, this.outputFluidTanks, + this.inputChemicalStorage, this.outputChemicalStorage, pos); + + // 初始化配方匹配与并行计算器(纯只读计算,不持有运行状态) + this.recipeCalculator = new AlloyFurnaceRecipeCalculator( + this.itemHandler, this.inputFluidTanks, this.outputFluidTanks, + this.inputChemicalStorage, this.outputChemicalStorage, + ChemicalKeyProviders.get()); + } + + + /** + * 计算流体槽容量 + * 基础16000,前3阶2倍增长,之后4倍增长,9阶达到int最大值 + */ + public static int calculateFluidCapacity(int tier) { + if (tier <= 0) return BASE_FLUID_TANK_CAPACITY; + if (tier >= 9) return Integer.MAX_VALUE; + + long capacity; + if (tier <= 3) { + // 1-3阶:2倍增长 + capacity = (long) BASE_FLUID_TANK_CAPACITY * (1L << tier); + } else { + // 4-8阶:4倍增长(从第3阶的基础上) + long base = (long) BASE_FLUID_TANK_CAPACITY * 8; // 第3阶的值 + // 使用位移代替Math.pow避免浮点数精度问题 + capacity = base * (1L << (2 * (tier - 3))); + } + return capacity > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) capacity; + } + + /** Chemical tanks use the same progression, without the int-sized fluid cap. */ + public static long calculateChemicalCapacity(int tier) { + if (tier <= 0) return BASE_CHEMICAL_TANK_CAPACITY; + if (tier >= USEFUL_INGOT_FURNACE_TIER) return Long.MAX_VALUE; + + long capacity; + if (tier <= 3) { + capacity = BASE_CHEMICAL_TANK_CAPACITY * (1L << tier); + } else { + long base = BASE_CHEMICAL_TANK_CAPACITY * 8L; + int shift = 2 * (tier - 3); + capacity = shift >= Long.SIZE - 1 || base > Long.MAX_VALUE / (1L << shift) + ? Long.MAX_VALUE : base * (1L << shift); + } + return Math.max(0L, capacity); + } + + /** + * 计算能量槽容量 + * 基础100000,前3阶2倍增长,之后4倍增长;使用 long 保留完整等级曲线。 + */ + public static long calculateEnergyCapacity(int tier) { + if (tier <= 0) return BASE_ENERGY_CAPACITY; + if (tier >= USEFUL_INGOT_FURNACE_TIER) return Long.MAX_VALUE; + + long capacity; + if (tier <= 3) { + // 1-3阶:2倍增长 + capacity = (long) BASE_ENERGY_CAPACITY * (1L << tier); + } else { + // 4-8阶:4倍增长 + long base = (long) BASE_ENERGY_CAPACITY * 8; // 第3阶的值 + // 使用位移代替Math.pow避免浮点数精度问题 + capacity = base * (1L << (2 * (tier - 3))); + } + return capacity; + } + + /** + * 计算能量输入速度 + * 基础10000,前3阶2倍增长,之后4倍增长;使用 long 保留完整等级曲线。 + */ + public static long calculateEnergyReceive(int tier) { + if (tier <= 0) return BASE_ENERGY_MAX_RECEIVE; + if (tier >= USEFUL_INGOT_FURNACE_TIER) return Long.MAX_VALUE; + + long receive; + if (tier <= 3) { + // 1-3阶:2倍增长 + receive = (long) BASE_ENERGY_MAX_RECEIVE * (1L << tier); + } else { + // 4-8阶:4倍增长 + long base = (long) BASE_ENERGY_MAX_RECEIVE * 8; // 第3阶的值 + // 使用位移代替Math.pow避免浮点数精度问题 + receive = base * (1L << (2 * (tier - 3))); + } + return receive; + } + + /** + * 检查物品是否是扳手 + */ + public static boolean isWrench(ItemStack stack) { + return stack.is(net.minecraft.tags.ItemTags.create( + new net.minecraft.resources.ResourceLocation("c", "tools/wrench"))); + } + + /** + * 方块实体的每tick更新逻辑 + *

+ * 处理配方逻辑并更新方块的active状态(用于光照) + * + * @param level 世界 + * @param entity 方块实体实例 + */ + public static void tick(Level level, AdvancedAlloyFurnaceBlockEntity entity) { + if (level.isClientSide) return; + + if (!entity.deferredTasksLoaded) { + // 配方目录由后台线程构建,未就绪时解码会把完好样板判为失效并退还材料,故在此重试。 + entity.deferredTasksLoaded = entity.aeManager.loadDeferredTasks(); + } + + long currentCatalogGeneration = AlloyFurnaceRecipeCatalog.generation(); + if (entity.recipeCatalogGeneration != currentCatalogGeneration) { + entity.recipeCatalogGeneration = currentCatalogGeneration; + entity.markRecipeInputsChanged(); + entity.updatePatterns(); + } + entity.aeManager.tickPatternRefresh(); + + boolean wasActive = entity.getBlockState().getValue( + com.sorrowmist.useless.content.blocks.AdvancedAlloyFurnaceBlock.getActiveProperty()); + + // 红石控制:被阻止时跳过所有操作 + boolean blocked = entity.isRedstoneBlocked(); + + if (blocked) { + // 被阻止时,配方进度冻结但不清零 + // 不执行任何配方处理、AE任务、自动IO + boolean shouldBeActive = entity.currentRecipe != null + && (entity.progress > 0 || entity.accumulatedEnergy > 0L); + if (wasActive != shouldBeActive) { + level.setBlock(entity.worldPosition, + entity.getBlockState().setValue( + AdvancedAlloyFurnaceBlock.getActiveProperty(), + shouldBeActive + ), + 3 + ); + } + return; + } + + // 先补电再处理配方,本tick抽到的能量当tick即可使用 + entity.drawEnergyFromAeNetwork(); + + if (entity.currentRecipe == null) { + entity.tryStartNewRecipe(); + } else { + entity.processCurrentRecipe(); + } + + entity.aeManager.flushAEBatches(); + entity.aeManager.tickAETasks(); + entity.aeManager.tickUnreturnedInputs(); + entity.aeManager.tickUnreturnedOutputs(); + entity.aeManager.tickQueuedCraftingOutputs(); + + // 每tick驱动自动输入输出,连续空转时由控制器内部退避 + entity.autoOutputTickCounter++; + if (entity.autoOutputTickCounter >= AUTO_OUTPUT_INTERVAL) { + entity.autoOutputTickCounter = 0; + entity.autoIoController.tick(level); + } + + // 判断是否应该处于活跃状态 + boolean isProcessing = entity.currentRecipe != null + && (entity.progress > 0 || entity.accumulatedEnergy > 0L); + + // 如果正在处理,重置冷却计时器 + if (isProcessing) { + entity.activeCooldown = ACTIVE_COOLDOWN_TICKS; + } else if (entity.activeCooldown > 0) { + // 否则减少冷却计时器 + entity.activeCooldown--; + } + + // 活跃状态 = 正在处理 或 冷却中 + boolean shouldBeActive = isProcessing || entity.activeCooldown > 0; + + // 更新方块状态(光照) + if (wasActive != shouldBeActive) { + level.setBlock(entity.worldPosition, + entity.getBlockState().setValue( + AdvancedAlloyFurnaceBlock.getActiveProperty(), + shouldBeActive + ), + 3 + ); + } + } + + /** + * 从所在AE网络为熔炉补充能量: + *

    + *
  1. AppliedFlux 已安装且配置开启时,抽取网络存储中的FE能量(通量元件)
  2. + *
  3. 配置开启时(默认关闭),抽取AE网络自身能量作为补充(按 PowerUnits 折算,1 AE = 2 FE)
  4. + *
+ * 每tick总抽取量受熔炉容量与最大输入速率限制。 + */ + private void drawEnergyFromAeNetwork() { + boolean drawAppflux = AppFluxCompat.isLoaded() && ConfigManager.isFurnaceDrawAppfluxEnergyEnabled(); + boolean drawAe = ConfigManager.isFurnaceDrawAeEnergyEnabled(); + if (!drawAppflux && !drawAe) { + return; + } + if (!this.isConnectedToAE) { + return; + } + + // 模拟接收得到本tick可接受的能量(受容量与最大输入速率约束) + long wanted = this.energyManager.receiveEnergy(Long.MAX_VALUE, true); + if (wanted <= 0L) { + return; + } + + IGrid grid = this.mainNode.getGrid(); + if (grid == null) { + return; + } + + if (drawAppflux) { + long got = AppFluxCompat.extractFe(grid, wanted, this.actionSource); + if (got > 0) { + this.energyManager.modifyEnergy(got); + wanted -= got; + } + } + + if (drawAe && wanted > 0L) { + IEnergyService energyService = grid.getEnergyService(); + double aeWanted = PowerUnits.FE.convertTo(PowerUnits.AE, wanted); + double aeGot = energyService.extractAEPower(aeWanted, Actionable.MODULATE, PowerMultiplier.ONE); + // 以实际抽取量入账并向下取整:丢弃不足 1FE 的零头,避免虚增能量记账 + long feGot = Math.min(wanted, (long) Math.floor(PowerUnits.AE.convertTo(PowerUnits.FE, aeGot))); + if (feGot > 0L) { + this.energyManager.modifyEnergy(feGot); + } + } + } + + // 更新客户端任务进度(从网络包调用) + public void updateClientTaskProgress( + List tasks) { + this.aeManager.updateClientTaskProgress(tasks); + } + + // 发送AE任务进度到所有客户端 + public void sendAETaskProgressToClients() { + this.aeManager.sendAETaskProgressToClients(); + } + + + /** + * 根据阶级计算并更新容量 + * 使用指数增长曲线,并同步 long 容量与输入速率。 + */ + private void updateCapacityByTier() { + this.fluidTankCapacity = calculateFluidCapacity(this.furnaceTier); + this.chemicalTankCapacity = calculateChemicalCapacity(this.furnaceTier); + this.energyCapacity = calculateEnergyCapacity(this.furnaceTier); + this.energyMaxReceive = calculateEnergyReceive(this.furnaceTier); + + // 更新能量管理器 + this.energyManager.setMaxEnergyStored(this.energyCapacity); + this.energyManager.setMaxReceive(this.energyMaxReceive); + + // 更新流体槽容量 + for (int i = 0; i < FLUID_TANK_COUNT; i++) { + if (this.inputFluidTanks[i] != null) { + this.inputFluidTanks[i].setCapacity(this.fluidTankCapacity); + } + if (this.outputFluidTanks[i] != null) { + this.outputFluidTanks[i].setCapacity(this.fluidTankCapacity); + } + } + this.inputChemicalStorage.setCapacity(this.chemicalTankCapacity); + this.outputChemicalStorage.setCapacity(this.chemicalTankCapacity); + } + + private void onChemicalStorageChanged() { + this.markRecipeInputsChanged(); + this.setChanged(); + if (this.level != null && !this.level.isClientSide) { + this.level.sendBlockUpdated(this.worldPosition, this.getBlockState(), this.getBlockState(), 3); + } + } + + /** + * 获取当前熔炉阶级 + */ + public int getFurnaceTier() { + return this.furnaceTier; + } + + @Override + public int getMachineTier() { + return this.furnaceTier; + } + + public void setClientFurnaceTier(int tier) { + this.furnaceTier = Math.max(0, Math.min(MAX_FURNACE_TIER, tier)); + } + + /** + * 设置熔炉阶级(内部使用,不触发容量更新) + */ + private void setFurnaceTier(int tier) { + this.furnaceTier = Math.max(0, Math.min(MAX_FURNACE_TIER, tier)); + } + + /** + * 尝试升级熔炉 + * + * @param targetTier 目标阶级(1-10) + * @return 是否升级成功 + */ + public boolean tryUpgrade(int targetTier) { + // 只能升级到更高阶级 + if (targetTier <= this.furnaceTier) { + return false; + } + // 1-9阶由无用锭升级,第10阶由有用锭升级。 + if (targetTier < 1 || targetTier > MAX_FURNACE_TIER) { + return false; + } + this.furnaceTier = targetTier; + this.updateCapacityByTier(); + this.markRecipeInputsChanged(); + this.setChanged(); + + // 同步数据到客户端,确保界面立即更新 + if (this.level != null && !this.level.isClientSide) { + this.level.sendBlockUpdated(this.worldPosition, this.getBlockState(), this.getBlockState(), 3); + } + + return true; + } + + /** + * 获取流体槽容量 + */ + public int getFluidTankCapacity() { + return this.fluidTankCapacity; + } + + /** + * 获取能量槽容量 + */ + public long getEnergyCapacity() { + return this.energyCapacity; + } + + /** + * 获取能量输入速度 + */ + public long getEnergyMaxReceive() { + return this.energyMaxReceive; + } + + /** + * 设置物品栏中的物品(用于从NBT恢复) + * + * @param slot 槽位 + * @param stack 物品堆 + */ + public void setItemInSlot(int slot, ItemStack stack) { + if (slot >= 0 && slot < TOTAL_SLOTS) { + this.itemHandler.setStackInSlot(slot, stack); + } + } + + public void restoreInventory(CompoundTag inventoryTag, HolderLookup.Provider registries) { + this.itemHandler.deserializeNBT(inventoryTag); + this.itemStorageRevision++; + this.markRecipeInputsChanged(); + this.updateMoldState(); + this.updatePatterns(); + } + + /** + * 尝试开始新配方处理 + *

+ * 检查是否有匹配的配方,以及是否有足够的空间和输入材料。 + * 并行数在此处计算一次,避免 hasOutputSpace 和 startRecipeProcessing 重复计算。 + */ + private void tryStartNewRecipe() { + if (!this.shouldSearchRecipe()) return; + + Optional match = this.findMatchingRecipe(); + if (match.isEmpty()) return; + + AdvancedAlloyFurnaceRecipe recipe = match.get(); + if (!this.canConsumeRecipeInputs(recipe)) return; + + // 统一计算并行数(一次计算,同时校验输出空间和能量是否足够) + int parallel = this.calculateActualParallel(recipe); + if (parallel < 1) return; + + this.startRecipeProcessing(recipe); + } + + /** + * 处理当前配方 + *

+ * 每tick消耗能量并增加进度,完成时产出物品 + *

+ * 能量消耗逻辑: + * 1. 配方开始时记录初始并行数 cachedParallel + * 2. 每tick根据当前并行数计算所需能量并扣除 + * 3. 累积已消耗的能量到 accumulatedEnergy + * 4. 配方完成时根据 accumulatedEnergy 计算实际能支持的并行数 + */ + private void processCurrentRecipe() { + if (this.currentRecipe == null) return; + + // 每20tick检查一次配方是否切换,避免评分系统不稳定导致连续重启; + // 等能量时进度停在同一值,靠搜索节流避免逐 tick 重复检查 + if (this.progress % 20 == 0 && this.shouldSearchRecipe()) { + Optional bestMatch = this.findMatchingRecipe(); + if (bestMatch.isPresent() && !hasSameProcessingOutput(bestMatch.get(), this.currentRecipe)) { + this.startRecipeProcessing(bestMatch.get()); + return; + } + } + + // 使用开始配方时计算的并行数 + int actualParallel = this.cachedParallel; + + ResolvedCatalystEffect resolvedCatalystEffect = this.cachedCatalystEffect; + if (resolvedCatalystEffect == null) { + // fallback:理论上不会发生,但保留安全性 + ItemStack catalystStack = this.itemHandler.getStackInSlot(CATALYST_SLOT); + resolvedCatalystEffect = CatalystEffectResolver.resolve(this.currentRecipe, catalystStack, this.currentRecipe.processTime()); + this.cachedCatalystEffect = resolvedCatalystEffect; + } + long targetEnergy = AlloyFurnaceRecipeExecutor.calculateTargetTotalEnergy( + this.currentRecipe.energy(), actualParallel, resolvedCatalystEffect); + AlloyFurnaceRecipeExecutor.TickResult tickResult = AlloyFurnaceRecipeExecutor.consumeProgressEnergy( + this.energyManager, targetEnergy, this.progress, this.maxProgress, this.accumulatedEnergy); + + // 能量不足时暂停进度,但不重置 + this.accumulatedEnergy += tickResult.energyConsumed(); + if (!tickResult.progressAdvanced()) { + if (tickResult.energyConsumed() > 0L) { + this.setChanged(); + } + return; + } + + // 累积能量 + this.progress++; + + if (this.progress >= this.maxProgress) { + this.completeRecipe(); + } + + this.setChanged(); + } + + private static boolean hasSameProcessingOutput( + AdvancedAlloyFurnaceRecipe candidate, AdvancedAlloyFurnaceRecipe current) { + if (!candidate.id().equals(current.id()) || candidate.outputs().size() != current.outputs().size()) { + return false; + } + for (int i = 0; i < candidate.outputs().size(); i++) { + ItemStack candidateOutput = candidate.outputs().get(i); + ItemStack currentOutput = current.outputs().get(i); + if (candidateOutput.getCount() != currentOutput.getCount() + || !ItemStack.isSameItemSameTags(candidateOutput, currentOutput)) { + return false; + } + } + return true; + } + + /** + * 计算实际可用的并行数(委托给配方计算器)。 + */ + private int calculateActualParallel(AdvancedAlloyFurnaceRecipe recipe) { + return this.recipeCalculator.calculateActualParallel(recipe); + } + + /** + * 计算催化剂允许的并行数(委托给配方计算器,优先使用缓存的催化剂效果)。 + */ + private int calculateCatalystParallel(AdvancedAlloyFurnaceRecipe recipe) { + return this.recipeCalculator.calculateCatalystParallel(recipe, this.cachedCatalystEffect); + } + + /** + * 计算输入材料允许的并行数(委托给配方计算器)。 + */ + private int calculateMaterialParallel(AdvancedAlloyFurnaceRecipe recipe) { + return this.recipeCalculator.calculateMaterialParallel(recipe); + } + + /** + * 计算输出空间允许的并行数(委托给配方计算器)。 + */ + private int calculateOutputParallel(AdvancedAlloyFurnaceRecipe recipe) { + return this.recipeCalculator.calculateOutputParallel(recipe); + } + + /** + * 完成当前配方处理 + *

+ * 配方完成时进行最终结算: + * 1. 根据当前材料、输出空间、催化剂计算最大可行并行数 + * 2. 计算还需要补充的能量 = 目标并行能量 - 已消耗能量 + * 3. 尝试扣除补充能量 + * 4. 如果能量足够,产出目标并行数的产物 + * 5. 如果能量不足,根据实际能量计算可行的并行数 + *

+ * 示例: + * - 配方需要1000能量,开始3并行,已消耗3000能量 + * - 配方完成时有64份材料,催化剂支持1w并行 + * - 目标并行64,需要64000能量 + * - 还需补充能量 = 64000 - 3000 = 61000 + * - 如果能量足够,产出64份;不够则按实际能量计算 + */ + private void completeRecipe() { + long recipeEnergy = this.currentRecipe.energy(); + int initialParallel = this.cachedParallel; + + // 步骤1: 计算材料、输出空间、催化剂支持的最大并行数 + int materialSupportedParallel = this.calculateMaterialParallel(this.currentRecipe); + int outputSupportedParallel = this.calculateOutputParallel(this.currentRecipe); + int catalystSupportedParallel = this.calculateCatalystParallel(this.currentRecipe); + + int targetParallel = AlloyFurnaceParallelCalculator.calculateCompletionTargetParallel( + initialParallel, + catalystSupportedParallel, + materialSupportedParallel, + outputSupportedParallel + ); + + ResolvedCatalystEffect resolvedCatalystEffect = this.cachedCatalystEffect; + if (resolvedCatalystEffect == null) { + ItemStack catalystStack = this.itemHandler.getStackInSlot(CATALYST_SLOT); + resolvedCatalystEffect = CatalystEffectResolver.resolve(this.currentRecipe, catalystStack, this.currentRecipe.processTime()); + } + + targetParallel = (int) Math.min(targetParallel, + AlloyFurnaceParallelCalculator.calculateEnergyParallel( + this.currentRecipe, resolvedCatalystEffect)); + + AlloyFurnaceRecipeExecutor.CompletionEnergyResult completionEnergy = AlloyFurnaceRecipeExecutor.settleCompletionEnergy( + this.energyManager, + recipeEnergy, + targetParallel, + this.accumulatedEnergy, + resolvedCatalystEffect + ); + int actualParallel = completionEnergy.actualParallel(); + + // 如果没有足够的并行数(至少1),则无法完成配方 + if (actualParallel <= 0) { + this.resetProgress(); + return; + } + + // 消耗材料并产出物品 + if (!this.consumeRecipeInputs(this.currentRecipe, actualParallel)) { + this.resetProgress(); + return; + } + this.produceRecipeOutputs(this.currentRecipe, actualParallel); + + this.resetProgress(); + } + + /** + * 开始处理新配方 + *

+ * 初始化配方处理状态,计算并行数 + * + * @param recipe 要处理的配方 + */ + private void startRecipeProcessing(AdvancedAlloyFurnaceRecipe recipe) { + this.currentRecipe = recipe; + + ItemStack catalystStack = this.itemHandler.getStackInSlot(CATALYST_SLOT); + int baseTime = recipe.processTime(); + ResolvedCatalystEffect resolvedCatalystEffect = CatalystEffectResolver.resolve(recipe, catalystStack, baseTime); + this.cachedCatalystEffect = resolvedCatalystEffect; + this.maxProgress = resolvedCatalystEffect.processTime(); + + this.progress = 0; + + // 使用统一的并行计算方法 + this.cachedParallel = this.calculateActualParallel(recipe); + this.accumulatedEnergy = 0; + + this.isUselessIngotRecipe = resolvedCatalystEffect.uselessIngotRecipe(); + this.targetUselessIngotTier = resolvedCatalystEffect.targetUselessIngotTier(); + + this.setChanged(); + } + + @Override + public @NotNull Component getDisplayName() { + Component customName = this.getCustomName(); + return customName != null ? customName : Component.translatable("container.useless_mod.advanced_alloy_furnace"); + } + + @Override + public @Nullable AbstractContainerMenu createMenu(int id, @NotNull Inventory inventory, @NotNull Player player) { + return new AdvancedAlloyFurnaceMenu(id, inventory, this, this.getData()); + } + + public ContainerData getData() { + return this.data; + } + + public ItemStackHandler getItemHandler() { + ensureExternalInventory(); + return this.itemHandler; + } + + @Nullable + public ExternalInventoryReference getExternalInventoryReference() { + ensureExternalInventory(); + return inventoryReference; + } + + public void bindExternalInventory(@Nullable ExternalInventoryReference requested, + @Nullable CompoundTag legacyInventory, + HolderLookup.Provider registries) { + if (externalInventoryLoaded || level == null || level.isClientSide) return; + inventoryReference = ExternalInventoryStore.bindAt( + level, ExternalInventoryKind.FURNACE, requested, worldPosition, + itemHandler, legacyInventory, registries); + ExternalInventoryStore.setReference(this, inventoryReference); + pendingLegacyInventory = null; + externalInventoryLoaded = true; + itemStorageRevision++; + markRecipeInputsChanged(); + updateMoldState(); + updatePatterns(); + setChanged(); + } + + private void ensureExternalInventory() { + if (externalInventoryLoaded || level == null || level.isClientSide) return; + bindExternalInventory( + ExternalInventoryStore.getReference(this), + pendingLegacyInventory, + level.registryAccess()); + } + + public void saveExternalInventory() { + if (level != null && !level.isClientSide && externalInventoryLoaded) { + ExternalInventoryStore.save(level, inventoryReference, itemHandler, level.registryAccess()); + } + } + + public void releaseExternalInventory() { + saveExternalInventory(); + if (level != null) { + ExternalInventoryStore.release(level, worldPosition, inventoryReference); + } + } + + public long getItemStorageRevision() { + return this.itemStorageRevision; + } + + @Override + public long getIoStateRevision() { + return this.recipeInputRevision; + } + + /** + * 获取方向感知的物品处理器。 + *

+ * 根据面的输入输出模式限制外部物流手段的访问。 + * + * @param side 输入方向 + * @return 物品处理器 + */ + public IItemHandler getItemHandler(@Nullable Direction side) { + ensureExternalInventory(); + return new FurnaceSidedItemHandler(this.itemHandler, side, this); + } + + // ==================== 面模式管理方法 ==================== + + /** + * 获取指定逻辑面的模式。 + */ + public FurnaceFaceMode getFaceMode(FurnaceFace face) { + return this.faceModes[face.ordinal()]; + } + + /** + * 设置指定逻辑面的模式。 + */ + public void setFaceMode(FurnaceFace face, FurnaceFaceMode mode) { + this.faceModes[face.ordinal()] = mode; + this.autoIoController.wake(); + this.setChanged(); + } + + /** + * 将指定逻辑面的模式循环到下一个。 + * + * @return 新的模式 + */ + public FurnaceFaceMode cycleFaceMode(FurnaceFace face) { + return this.cycleFaceMode(face, false); + } + + public FurnaceFaceMode cycleFaceMode(FurnaceFace face, boolean reverse) { + FurnaceFaceMode current = this.faceModes[face.ordinal()]; + FurnaceFaceMode next = reverse ? current.previous() : current.next(); + this.faceModes[face.ordinal()] = next; + this.autoIoController.wake(); + this.setChanged(); + // 同步到客户端 + if (this.level != null && !this.level.isClientSide) { + this.level.sendBlockUpdated(this.worldPosition, this.getBlockState(), this.getBlockState(), 3); + } + return next; + } + + /** + * 获取所有面模式数组的副本。 + */ + public FurnaceFaceMode[] getFaceModes() { + return this.faceModes.clone(); + } + + /** + * 获取方块的水平朝向。 + */ + public Direction getFacing() { + return this.getBlockState().getValue(net.minecraft.world.level.block.state.properties.BlockStateProperties.HORIZONTAL_FACING); + } + + // ==================== 自动输入输出开关 ==================== + + public boolean isAutoInputEnabled() { + return this.autoInputEnabled; + } + + public void setAutoInputEnabled(boolean enabled) { + this.autoInputEnabled = enabled; + this.autoIoController.wake(); + this.setChanged(); + } + + public boolean isAutoOutputEnabled() { + return this.autoOutputEnabled; + } + + public void setAutoOutputEnabled(boolean enabled) { + this.autoOutputEnabled = enabled; + this.autoIoController.wake(); + this.setChanged(); + } + + /** + * 切换自动输入开关。 + */ + public boolean toggleAutoInput() { + this.autoInputEnabled = !this.autoInputEnabled; + this.autoIoController.wake(); + this.setChanged(); + if (this.level != null && !this.level.isClientSide) { + this.level.sendBlockUpdated(this.worldPosition, this.getBlockState(), this.getBlockState(), 3); + } + return this.autoInputEnabled; + } + + /** + * 切换自动输出开关。 + */ + public boolean toggleAutoOutput() { + this.autoOutputEnabled = !this.autoOutputEnabled; + this.autoIoController.wake(); + this.setChanged(); + if (this.level != null && !this.level.isClientSide) { + this.level.sendBlockUpdated(this.worldPosition, this.getBlockState(), this.getBlockState(), 3); + } + return this.autoOutputEnabled; + } + + // ==================== 红石控制 ==================== + + public RedstoneControlMode getRedstoneControlMode() { + return this.redstoneControlMode; + } + + public void setRedstoneControlMode(RedstoneControlMode mode) { + this.redstoneControlMode = mode; + this.setChanged(); + } + + public RedstoneControlMode cycleRedstoneControlMode() { + this.redstoneControlMode = this.redstoneControlMode.next(); + this.setChanged(); + if (this.level != null && !this.level.isClientSide) { + this.level.sendBlockUpdated(this.worldPosition, this.getBlockState(), this.getBlockState(), 3); + } + return this.redstoneControlMode; + } + + public boolean isRedstoneBlocked() { + boolean hasSignal = this.level != null && this.level.hasNeighborSignal(this.worldPosition); + return !this.redstoneControlMode.shouldRun(hasSignal); + } + + public boolean isReturnOutputToAe() { + return this.returnOutputToAe; + } + + public void setReturnOutputToAe(boolean value) { + this.returnOutputToAe = value; + this.setChanged(); + } + + public void cancelAllAETasks() { + this.aeManager.cancelAllTasks(); + } + + public boolean hasPersistedAETaskData() { + return this.aeManager.hasPersistedData(); + } + + public void saveAETasks(CompoundTag tag, HolderLookup.Provider registries) { + this.aeManager.saveTasks(tag, registries); + } + + public void readAETasks(CompoundTag tag) { + this.aeManager.readTasksTag(tag); + this.deferredTasksLoaded = false; + } + + public void markDropDataCaptured() { + this.dropDataCaptured = true; + } + + public boolean isDropDataCaptured() { + return this.dropDataCaptured; + } + + @Override + public void stashUnreturnedInput(AEKey key, long amount) { + this.aeManager.stashUnreturnedInput(key, amount); + } + + public void stashUnreturnedOutput(AEKey key, long amount) { + this.aeManager.stashUnreturnedOutput(key, amount); + } + + /** + * 与多方块核心、被动样板舱保持一致:AE 任务的输入/产物统一以 AEKey + long 表示, + * 不再按数量物化成 ItemStack。否则取消大批量任务时,退出路径会尝试将百万级材料写入 + * 超出容量上限的单个栈,本地槽无法容纳时会掉落为大量物品实体。 + */ + @Override + public boolean supportsLongAeAmounts() { + return true; + } + + /** + * 回退顺序的最后一级不再掉落世界,而是并入暂存的 key + 数量, + * 由 aeManager 逐 tick 重试写回(暂存随 AeTasks 一并持久化,破坏/搬运不会丢失)。 + */ + @Override + public void handleUnreturnedItem(ItemStack stack) { + if (stack == null || stack.isEmpty()) { + return; + } + AEItemKey key = AEItemKey.of(stack); + if (key != null) { + stashUnreturnedInput(key, stack.getCount()); + } + } + + public IEnergyStorage getEnergyStorage() { + return this.energyManager; + } + + public IEnergyManager getEnergyManager() { + return this.energyManager; + } + + public FluidTank getInputFluidTank(int index) { + return (index >= 0 && index < FLUID_TANK_COUNT) ? this.inputFluidTanks[index] : new FluidTank(0); + } + + public FluidTank getOutputFluidTank(int index) { + return (index >= 0 && index < FLUID_TANK_COUNT) ? this.outputFluidTanks[index] : new FluidTank(0); + } + + public FurnaceChemicalStorage getInputChemicalStorage() { + return this.inputChemicalStorage; + } + + public FurnaceChemicalStorage getOutputChemicalStorage() { + return this.outputChemicalStorage; + } + + public ChemicalStackView getInputChemical(int index) { + return this.inputChemicalStorage.getStackInSlot(index); + } + + public ChemicalStackView getOutputChemical(int index) { + return this.outputChemicalStorage.getStackInSlot(index); + } + + public long getChemicalTankCapacity() { + return this.chemicalTankCapacity; + } + + public boolean hasChemicalSupport() { + return this.inputChemicalStorage.isAvailable() && this.outputChemicalStorage.isAvailable(); + } + + public long getEnergy() { + return this.energyManager.getEnergyStoredLong(); + } + + public void setEnergy(long energy) { + this.energyManager.setEnergyStored(energy); + this.setChanged(); + } + + public long getMaxEnergy() { + return this.energyManager.getMaxEnergyStoredLong(); + } + + public void setMaxEnergy(long energy) { + this.energyManager.setMaxEnergyStored(energy); + } + + int getCurrentParallel() { + if (this.currentRecipe != null && this.progress > 0) { + return this.cachedParallel; + } + return this.calculateDisplayParallel(); + } + + void setCurrentParallel(int parallel) { + this.currentParallel = parallel; + this.setChanged(); + } + + public int getCatalystMaxParallel() { + ItemStack catalystStack = this.itemHandler.getStackInSlot(CATALYST_SLOT); + if (catalystStack.isEmpty()) return 1; + return CatalystType.fromStack(catalystStack).getNormalRecipeParallel(); + } + + @Override + public long getCraftingPatternCapacity() { + return Math.max(1L, getCatalystMaxParallel()); + } + + private int calculateDisplayParallel() { + // 使用缓存减少UI查询时的配方查找开销 + if (this.level != null) { + long currentTick = this.level.getGameTime(); + if (currentTick < this.displayParallelCacheTick + DISPLAY_PARALLEL_CACHE_DURATION) { + return this.cachedDisplayParallel; + } + this.displayParallelCacheTick = (int) currentTick; + } + + Optional match = this.findMatchingRecipe(); + if (match.isPresent()) { + this.cachedDisplayParallel = this.calculateActualParallel(match.get()); + } else { + int catalystParallel = this.getCatalystMaxParallel(); + this.cachedDisplayParallel = Math.max(1, catalystParallel); + } + return this.cachedDisplayParallel; + } + + public boolean hasMold() { + return this.hasMold; + } + + public void setHasMold(boolean hasMold) { + this.hasMold = hasMold; + this.setChanged(); + } + + public int getProgress() { + return this.progress; + } + + public void setProgress(int progress) { + this.progress = progress; + this.setChanged(); + } + + public int getMaxProgress() { + return this.maxProgress; + } + + public void setMaxProgress(int maxProgress) { + this.maxProgress = maxProgress; + this.setChanged(); + } + + int getCachedParallel() { + return this.cachedParallel; + } + + @Nullable + public AdvancedAlloyFurnaceRecipe getCurrentRecipe() { + return this.currentRecipe; + } + + private FluidTank createTank(int index, boolean isInput) { + return new FluidTank(this.fluidTankCapacity) { + @Override + protected void onContentsChanged() { + AdvancedAlloyFurnaceBlockEntity.this.markRecipeInputsChanged(); + AdvancedAlloyFurnaceBlockEntity.this.setChanged(); + if (AdvancedAlloyFurnaceBlockEntity.this.level != null + && !AdvancedAlloyFurnaceBlockEntity.this.level.isClientSide) { + AdvancedAlloyFurnaceBlockEntity.this.level.sendBlockUpdated( + AdvancedAlloyFurnaceBlockEntity.this.worldPosition, + AdvancedAlloyFurnaceBlockEntity.this.getBlockState(), + AdvancedAlloyFurnaceBlockEntity.this.getBlockState(), 3 + ); + } + } + }; + } + + public IFluidHandler getInputFluidHandler() { + return new FurnaceFluidTankHandler(this.inputFluidTanks, true); + } + + public IFluidHandler getOutputFluidHandler() { + return new FurnaceFluidTankHandler(this.outputFluidTanks, false); + } + + public IFluidHandler getCombinedFluidHandler() { + return new FurnaceCombinedFluidTankHandler(this.inputFluidTanks, this.outputFluidTanks); + } + + /** + * 获取方向感知的复合流体处理器。 + *

+ * 根据面的输入输出模式限制外部物流手段的访问。 + * + * @param side 方向 + * @return 流体处理器 + */ + public IFluidHandler getCombinedFluidHandler(@Nullable Direction side) { + if (side == null) return getCombinedFluidHandler(); + return new FurnaceSidedFluidHandler(this.inputFluidTanks, this.outputFluidTanks, side, this); + } + + public void clearFluidTank(int tankIndex, boolean isInput) { + if (tankIndex < 0 || tankIndex >= FLUID_TANK_COUNT) return; + + if (isInput) { + this.inputFluidTanks[tankIndex].setFluid(FluidStack.EMPTY); + } else { + this.outputFluidTanks[tankIndex].setFluid(FluidStack.EMPTY); + } + // setFluid 不触发 onContentsChanged,需手动标记 + this.markRecipeInputsChanged(); + this.setChanged(); + + if (this.level != null && !this.level.isClientSide) { + this.level.sendBlockUpdated(this.worldPosition, this.getBlockState(), + this.getBlockState(), 3 + ); + } + } + + public void clearChemicalTank(int tankIndex, boolean isInput) { + if (!this.hasChemicalSupport() || tankIndex < 0 || tankIndex >= CHEMICAL_TANK_COUNT) return; + if (isInput) { + this.inputChemicalStorage.setStackInSlot(tankIndex, ChemicalStackView.EMPTY); + } else { + this.outputChemicalStorage.setStackInSlot(tankIndex, ChemicalStackView.EMPTY); + } + this.onChemicalStorageChanged(); + } + + @Override + public void loadTag(CompoundTag tag) { + HolderLookup.Provider registries = level == null ? net.minecraft.core.RegistryAccess.EMPTY : level.registryAccess(); + super.loadTag(tag); + + // 加载阶级(必须在加载其他数据之前,因为会影响容量) + if (tag.contains(NBTConstants.FURNACE_TIER)) { + this.setFurnaceTier(tag.getInt(NBTConstants.FURNACE_TIER)); + this.updateCapacityByTier(); + } + + if (tag.contains(NBTConstants.INVENTORY)) { + this.pendingLegacyInventory = tag.getCompound(NBTConstants.INVENTORY).copy(); + } + + if (tag.contains(NBTConstants.ENERGY)) { + this.energyManager.setEnergyStored(tag.getLong(NBTConstants.ENERGY)); + } + + this.progress = tag.getInt(NBTConstants.PROGRESS); + this.maxProgress = tag.getInt(NBTConstants.MAX_PROGRESS); + this.currentParallel = tag.getInt(NBTConstants.CURRENT_PARALLEL); + this.hasMold = tag.getBoolean(NBTConstants.HAS_MOLD); + this.cachedParallel = tag.getInt(NBTConstants.CACHED_PARALLEL); + if (this.cachedParallel <= 0) this.cachedParallel = 1; + this.isUselessIngotRecipe = tag.getBoolean(NBTConstants.IS_USELESS_INGOT_RECIPE); + this.targetUselessIngotTier = tag.getInt(NBTConstants.TARGET_USELESS_INGOT_TIER); + this.accumulatedEnergy = tag.getLong(NBTConstants.ACCUMULATED_ENERGY); + + for (int i = 0; i < FLUID_TANK_COUNT; i++) { + String inputFluidTag = NBTConstants.getInputFluidTag(i); + if (tag.contains(inputFluidTag)) { + FluidStack fluid = com.sorrowmist.useless.compat.forge1201.nbt.NativeStackNbt.readFluid(registries, tag.getCompound(inputFluidTag)); + this.inputFluidTanks[i].setFluid(fluid); + } else { + this.inputFluidTanks[i].setFluid(FluidStack.EMPTY); + } + } + + for (int i = 0; i < FLUID_TANK_COUNT; i++) { + String outputFluidTag = NBTConstants.getOutputFluidTag(i); + if (tag.contains(outputFluidTag)) { + FluidStack fluid = com.sorrowmist.useless.compat.forge1201.nbt.NativeStackNbt.readFluid(registries, tag.getCompound(outputFluidTag)); + this.outputFluidTanks[i].setFluid(fluid); + } else { + this.outputFluidTanks[i].setFluid(FluidStack.EMPTY); + } + } + + this.inputChemicalStorage.load(tag, "InputChemical", registries); + this.outputChemicalStorage.load(tag, "OutputChemical", registries); + this.markRecipeInputsChanged(); + + // 加载AE网络节点数据 + this.mainNode.loadFromNBT(tag); + + // 加载面模式 + this.faceModes[FurnaceFace.TOP.ordinal()] = FurnaceFaceMode.byIndex(tag.getInt("FaceModeTop")); + this.faceModes[FurnaceFace.BOTTOM.ordinal()] = FurnaceFaceMode.byIndex(tag.getInt("FaceModeBottom")); + this.faceModes[FurnaceFace.FRONT.ordinal()] = FurnaceFaceMode.byIndex(tag.getInt("FaceModeFront")); + this.faceModes[FurnaceFace.BACK.ordinal()] = FurnaceFaceMode.byIndex(tag.getInt("FaceModeBack")); + this.faceModes[FurnaceFace.LEFT.ordinal()] = FurnaceFaceMode.byIndex(tag.getInt("FaceModeLeft")); + this.faceModes[FurnaceFace.RIGHT.ordinal()] = FurnaceFaceMode.byIndex(tag.getInt("FaceModeRight")); + + // 加载自动输入输出开关 + this.autoInputEnabled = tag.getBoolean("AutoInputEnabled"); + this.autoOutputEnabled = tag.getBoolean("AutoOutputEnabled"); + + // 加载红石控制模式 + this.redstoneControlMode = RedstoneControlMode.byIndex(tag.getInt("RedstoneControlMode")); + + if (tag.contains("ReturnOutputToAe")) { + this.returnOutputToAe = tag.getBoolean("ReturnOutputToAe"); + } + + if (tag.contains("PatternPriority")) { + this.aeManager.setPatternPriority(tag.getInt("PatternPriority")); + } + + // 重新解析样板槽中的样板(必须在物品加载之后) + this.updatePatterns(); + + // 记录 AE 合成任务数据,推迟到 level 可用(首 tick)时解码 + this.aeManager.readTasksTag(tag); + this.deferredTasksLoaded = false; + } + + @Override + public void saveAdditional(@NotNull CompoundTag tag) { + HolderLookup.Provider registries = level == null ? net.minecraft.core.RegistryAccess.EMPTY : level.registryAccess(); + super.saveAdditional(tag); + + tag.putInt(NBTConstants.FURNACE_TIER, this.furnaceTier); + tag.putLong(NBTConstants.ENERGY, this.energyManager.getEnergyStoredLong()); + tag.putInt(NBTConstants.PROGRESS, this.progress); + tag.putInt(NBTConstants.MAX_PROGRESS, this.maxProgress); + tag.putInt(NBTConstants.CURRENT_PARALLEL, this.currentParallel); + tag.putBoolean(NBTConstants.HAS_MOLD, this.hasMold); + tag.putInt(NBTConstants.CACHED_PARALLEL, this.cachedParallel); + tag.putBoolean(NBTConstants.IS_USELESS_INGOT_RECIPE, this.isUselessIngotRecipe); + tag.putInt(NBTConstants.TARGET_USELESS_INGOT_TIER, this.targetUselessIngotTier); + tag.putLong(NBTConstants.ACCUMULATED_ENERGY, this.accumulatedEnergy); + + for (int i = 0; i < FLUID_TANK_COUNT; i++) { + FluidStack fluid = this.inputFluidTanks[i].getFluid(); + if (!fluid.isEmpty()) { + tag.put(NBTConstants.getInputFluidTag(i), com.sorrowmist.useless.compat.forge1201.nbt.NativeStackNbt.saveFluid(fluid, registries)); + } + } + + for (int i = 0; i < FLUID_TANK_COUNT; i++) { + FluidStack fluid = this.outputFluidTanks[i].getFluid(); + if (!fluid.isEmpty()) { + tag.put(NBTConstants.getOutputFluidTag(i), com.sorrowmist.useless.compat.forge1201.nbt.NativeStackNbt.saveFluid(fluid, registries)); + } + } + + this.inputChemicalStorage.save(tag, "InputChemical", registries); + this.outputChemicalStorage.save(tag, "OutputChemical", registries); + + // 保存AE网络节点数据 + this.mainNode.saveToNBT(tag); + + // 保存样板优先级 + tag.putInt("PatternPriority", this.aeManager.getPatternPriority()); + + // 保存面模式 + tag.putInt("FaceModeTop", this.faceModes[FurnaceFace.TOP.ordinal()].ordinal()); + tag.putInt("FaceModeBottom", this.faceModes[FurnaceFace.BOTTOM.ordinal()].ordinal()); + tag.putInt("FaceModeFront", this.faceModes[FurnaceFace.FRONT.ordinal()].ordinal()); + tag.putInt("FaceModeBack", this.faceModes[FurnaceFace.BACK.ordinal()].ordinal()); + tag.putInt("FaceModeLeft", this.faceModes[FurnaceFace.LEFT.ordinal()].ordinal()); + tag.putInt("FaceModeRight", this.faceModes[FurnaceFace.RIGHT.ordinal()].ordinal()); + + // 保存自动输入输出开关 + tag.putBoolean("AutoInputEnabled", this.autoInputEnabled); + tag.putBoolean("AutoOutputEnabled", this.autoOutputEnabled); + + // 保存红石控制模式 + tag.putInt("RedstoneControlMode", this.redstoneControlMode.ordinal()); + + // 保存产物是否回AE + tag.putBoolean("ReturnOutputToAe", this.returnOutputToAe); + + // 保存 AE 合成任务(仅落盘,网络更新包会剥离) + CompoundTag aeTasksTag = new CompoundTag(); + this.aeManager.saveTasks(aeTasksTag, registries); + tag.put("AeTasks", aeTasksTag); + } + + @Nullable + @Override + public Packet getUpdatePacket() { + return ClientboundBlockEntityDataPacket.create(this); + } + + @Override + public @NotNull CompoundTag getUpdateTag() { + HolderLookup.Provider registries = level == null ? net.minecraft.core.RegistryAccess.EMPTY : level.registryAccess(); + CompoundTag tag = this.saveWithoutMetadata(); + // AE 合成任务数据体积大且客户端无需,网络更新包中剥离 + tag.remove("AeTasks"); + return tag; + } + + @Override + public void setRemoved() { + super.setRemoved(); + this.mainNode.destroy(); + this.aeManager.shutdown(); + } + + + @Override + public void clearRemoved() { + super.clearRemoved(); + GridHelper.onFirstTick(this, (be) -> { + be.mainNode.create(getLevel(), getBlockPos()); + // 节点创建后重新解析样板并通知AE网络 + be.updatePatterns(); + // level 已可用,加载持久化的 AE 合成任务;目录未就绪时由 tick 重试 + be.deferredTasksLoaded = be.aeManager.loadDeferredTasks(); + } + ); + } + + @Override + public void handleUpdateTag(@NotNull CompoundTag tag) { + super.handleUpdateTag(tag); + } + + @Override + public void onChunkUnloaded() { + super.onChunkUnloaded(); + this.mainNode.destroy(); + } + + + @Override + public void onLoad() { + super.onLoad(); + ensureExternalInventory(); + // 节点创建由clearRemoved中的GridHelper.onFirstTick处理,确保只创建一次 + } + + /** + * 查找匹配的配方(统一匹配,支持物品+流体+模具优先级) + *

+ * 每次按当前输入查询最具体配方;查询管理器会用包含数量的不可变键缓存结果。 + * + * @return 匹配的配方,如果没有则返回空 + */ + private Optional findMatchingRecipe() { + return this.recipeCalculator.findMatchingRecipe(this.level, this.furnaceTier); + } + + /** + * 配方搜索节流:判断本 tick 是否需要重新搜索配方,需要时记录本次搜索的修订号与时间。 + *

+ * 搜索要重建输入列表并跑材料分配求解,缺料、输出满或等能量时逐 tick 重跑代价很高。 + * 修订号未变说明搜索结果不会变,跳过即可;但仍每 {@link #RECIPE_SEARCH_RETRY_TICKS} tick + * 兜底重搜一次,覆盖绕过变更回调直接改动堆叠、阶级规则配置重载等修订号追踪不到的情况。 + */ + private boolean shouldSearchRecipe() { + long gameTime = this.level.getGameTime(); + long elapsed = gameTime - this.recipeSearchTick; + if (this.recipeSearchRevision == this.recipeInputRevision + && elapsed >= 0L && elapsed < RECIPE_SEARCH_RETRY_TICKS) { + return false; + } + this.recipeSearchRevision = this.recipeInputRevision; + this.recipeSearchTick = gameTime; + return true; + } + + private void markRecipeInputsChanged() { + this.recipeInputRevision++; + } + + /** + * 检查是否有足够的输入材料支持至少一次配方 + * (用于开始新配方前的检查,委托给配方计算器)。 + */ + private boolean canConsumeRecipeInputs(AdvancedAlloyFurnaceRecipe recipe) { + return this.recipeCalculator.canConsumeRecipeInputs(recipe, 1, this.furnaceTier); + } + + private boolean consumeRecipeInputs(AdvancedAlloyFurnaceRecipe recipe, int parallel) { + if (!this.recipeCalculator.canConsumeRecipeInputs(recipe, parallel, this.furnaceTier)) return false; + return FurnaceInputPort.consumeRecipeInputs( + recipe, + parallel, + this.itemHandler, + INPUT_SLOTS_START, + INPUT_SLOTS_COUNT, + this.inputFluidTanks, + FLUID_TANK_COUNT, + this.inputChemicalStorage, + ChemicalKeyProviders.get() + ); + } + + private void produceRecipeOutputs(AdvancedAlloyFurnaceRecipe recipe, int parallel) { + FurnaceOutputPort.outputRecipe( + recipe, + parallel, + this.createAeOutputPort(), + this.itemHandler, + OUTPUT_SLOTS_START, + OUTPUT_SLOTS_COUNT, + this.outputFluidTanks, + FLUID_TANK_COUNT, + this.outputChemicalStorage, + ChemicalKeyProviders.get(), + this::stashUnreturnedOutput + ); + } + + private void resetProgress() { + this.progress = 0; + this.currentRecipe = null; + this.cachedCatalystEffect = null; + this.cachedParallel = 1; + this.cachedDisplayParallel = 1; + this.displayParallelCacheTick = 0; + this.isUselessIngotRecipe = false; + this.targetUselessIngotTier = 0; + this.accumulatedEnergy = 0; + // 配方结束或中止后立即重新搜索,保证连续加工不被搜索节流拖慢 + this.markRecipeInputsChanged(); + this.setChanged(); + } + + private void updateMoldState() { + ItemStack moldStack = this.itemHandler.getStackInSlot(MOLD_SLOT); + this.hasMold = !moldStack.isEmpty(); + this.setChanged(); + } + + @Override + public void onSaveChanges(AdvancedAlloyFurnaceBlockEntity nodeOwner, IGridNode node) { + setChanged(); + } + + @Override + public void onGridChanged(AdvancedAlloyFurnaceBlockEntity nodeOwner, IGridNode node) { + isConnectedToAE = node.isActive(); + setChanged(); + } + + // ==================== AE网络支持方法 ==================== + + @Override + public void onStateChanged(AdvancedAlloyFurnaceBlockEntity nodeOwner, IGridNode node, + IGridNodeListener.State state) { + isConnectedToAE = node.isActive(); + setChanged(); + } + + public @NotNull IManagedGridNode getMainNode() { + return this.mainNode; + } + + @Override + public @Nullable IGridNode getGridNode(Direction dir) { + return this.mainNode.getNode(); + } + + @Override + public AECableType getCableConnectionType(Direction dir) { + return AECableType.SMART; + } + + @Override + public IGridNode getActionableNode() { + return this.mainNode.getNode(); + } + + public boolean isActive() { + return this.mainNode.isActive(); + } + + // AE2 Integration - 获取存储服务 + private MEStorage getStorageService() { + if (!isConnectedToAE) { + return null; + } + + IGridNode node = this.mainNode.getNode(); + if (node == null || !node.isActive()) { + return null; + } + + IGrid grid = node.getGrid(); + if (grid == null) { + return null; + } + + IStorageService storageService = grid.getService(IStorageService.class); + if (storageService == null) { + return null; + } + + return storageService.getInventory(); + } + + // AE2 Integration - 尝试输出物品到AE网络 + public long tryOutputToAE(ItemStack stack) { + if (stack.isEmpty() || !isConnectedToAE || actionSource == null) { + return 0; + } + + MEStorage storage = getStorageService(); + + if (storage == null) { + return 0; + } + + AEItemKey key = AEItemKey.of(stack); + if (key == null) { + return 0; + } + + long amount = stack.getCount(); + + return storage.insert(key, amount, Actionable.MODULATE, actionSource); + } + + // AE2 Integration - 尝试输出流体到AE网络 + public long tryOutputFluidToAE(FluidStack stack) { + if (stack.isEmpty() || !isConnectedToAE || actionSource == null) { + return 0; + } + + MEStorage storage = getStorageService(); + + if (storage == null) { + return 0; + } + + AEFluidKey key = AEFluidKey.of(stack); + if (key == null) { + return 0; + } + + long amount = stack.getAmount(); + + return storage.insert(key, amount, Actionable.MODULATE, actionSource); + } + + public long tryOutputKeyToAE(AEKey key, long amount) { + CraftingAeOutputTarget target = resolveAeOutputTarget(); + return target == null ? 0L : target.insert(key, amount); + } + + /** + * 解析一次 ME 网络写入目标,供一次「产物回网」刷写 pass 内复用。 + * + *

此前每写一个分段都重新解析一次(连接检查 + 存储服务查询 + 装配体查询)。 + * 可持续吞吐直接由「每 tick 可插入次数」决定,一 tick 可能插入数千次 ⇒ 该解析成本直接影响吞吐。

+ */ + @Override + public @Nullable CraftingAeOutputTarget resolveAeOutputTarget() { + if (!isConnectedToAE || actionSource == null) { + return null; + } + MEStorage storage = getStorageService(); + if (storage == null) { + return null; + } + IActionSource source = actionSource; + // 网格也一并解析一次(getAeGrid() 内部还有一次装配体查询)。 + // 返回的 GridOutputTarget 支持「认领提升」:刷写 pass 会把 neoecoae 的逐段认领 + // 提到逐键一次(见 CraftingAeOutputTarget#supportsClaimHoisting)。 + return new GridOutputTarget(getAeGrid(), storage, source); + } + + @Override + public long tryOutputChemicalToAE(ChemicalStackView stack) { + if (stack == null || stack.isEmpty()) return 0L; + ChemicalKeyProvider provider = ChemicalKeyProviders.get(); + appeng.api.stacks.GenericStack generic = provider.toGenericStack(stack); + return generic == null ? 0L : this.tryOutputKeyToAE(generic.what(), generic.amount()); + } + + @Override + public void handleUnreturnedChemical(ChemicalStackView stack) { + if (stack == null || stack.isEmpty()) return; + appeng.api.stacks.GenericStack generic = ChemicalKeyProviders.get().toGenericStack(stack); + if (generic != null) { + this.aeManager.stashUnreturnedInput(generic.what(), generic.amount()); + } + } + + @Override + public ChemicalHandlerView getAdjacentChemicalHandler(Level level, BlockPos pos, BlockState state, + BlockEntity entity, @Nullable Direction side) { + if (!this.hasChemicalSupport()) return null; + return ChemicalCompatProviders.get().getAdjacentHandler(level, pos, state, entity, side); + } + + public int getActiveAETaskCount() { + return this.aeManager.getActiveAETaskCount(); + } + + public int getRemainingAETaskCount(boolean craftingPattern) { + return this.aeManager.getRemainingAETaskCount(craftingPattern); + } + + /** + * 本 tick 允许单批产生的产物分段数(AIMD 控制器,转发给 AE 管理器)。 + * + *

单方块同样有合成样板的自执行回网队列,所以同样受益于「由实测反馈决定单批规模」。

+ */ + @Override + public long outputSegmentBudget() { + return this.aeManager.outputSegmentBudget(); + } + + // 获取最大AE任务数量(基于熔炉等级) + public int getMaxAETaskCount() { + return ConfigManager.getAdvancedAlloyFurnaceTierThreads(this.furnaceTier); + } + + @Override + public Iterable getPatternStacks() { + List result = new java.util.ArrayList<>(PATTERN_SLOTS_COUNT); + for (int slot = PATTERN_SLOTS_START; slot <= PATTERN_SLOTS_END; slot++) { + result.add(itemHandler.getStackInSlot(slot)); + } + return result; + } + + // AE合成任务状态设置方法(用于客户端同步) + public void setActiveAETaskCount(int value) { + this.aeManager.setActiveAETaskCount(value); + } + + public int getTotalAEProgress() { + return this.aeManager.getTotalAEProgress(); + } + + public void setTotalAEProgress(int value) { + this.aeManager.setTotalAEProgress(value); + } + + public int getTotalAEMaxProgress() { + return this.aeManager.getTotalAEMaxProgress(); + } + + public void setTotalAEMaxProgress(int value) { + this.aeManager.setTotalAEMaxProgress(value); + } + + // 获取所有AE任务进度信息(用于UI显示) + public Collection getAETaskProgressList() { + return this.aeManager.getAETaskProgressList(); + } + + + @Override + public List getAvailablePatterns() { + return this.aeManager.getAvailablePatterns(); + } + + @Override + public void onPatternsRebuilt() { + if (this.level == null || this.level.isClientSide) { + return; + } + IManagedGridNode node = this.getMainNode(); + if (node.isActive() && node.getNode() != null + && node.getNode().getGrid() != null) { + ICraftingProvider.requestUpdate(node); + } + } + + @Override + public int getPatternPriority() { + return this.aeManager.getPatternPriority(); + } + + @Override + public boolean pushPattern(IPatternDetails patternDetails, KeyCounter[] inputHolder) { + return this.aeManager.pushPattern(patternDetails, inputHolder); + } + + /** + * 数据能源原生 bigint 批次入口。 + * + *

与 {@link #pushPattern} 的区别:这里收到的是单次合成的原型 + 一个可以超过 {@code long} 的次数, + * 由 {@code AdvancedAlloyFurnaceAeManager#pushBigIntegerCraftingPattern} 一次装配后整体放大。

+ */ + public boolean pushBigIntegerCraftingPattern(IPatternDetails patternDetails, BigInteger count, + KeyCounter[] unitPrototype) { + return this.aeManager.pushBigIntegerCraftingPattern(patternDetails, count, unitPrototype); + } + + @Override + public boolean isBusy() { + return this.aeManager.isBusy(); + } + + private void updatePatterns() { + this.aeManager.updatePatterns(); + } + + private void onItemStorageChanged() { + this.itemStorageRevision++; + this.markRecipeInputsChanged(); + saveExternalInventory(); + this.setChanged(); + } + + // ==================== CraftingTaskContext 接口实现 ==================== + + @Override + public int getInputSlotsStart() { + return INPUT_SLOTS_START; + } + + @Override + public int getInputSlotsCount() { + return INPUT_SLOTS_COUNT; + } + + @Override + public int getOutputSlotsStart() { + return OUTPUT_SLOTS_START; + } + + @Override + public int getOutputSlotsCount() { + return OUTPUT_SLOTS_COUNT; + } + + @Override + public int getCatalystSlot() { + return CATALYST_SLOT; + } + + @Override + public int getMoldSlot() { + return MOLD_SLOT; + } + + @Override + public int getFluidTankCount() { + return FLUID_TANK_COUNT; + } + + @Override + public Level getLevel() { + return level; + } + + @Override + public void markChanged() { + setChanged(); + } + + @Override + public ReentrantLock getCraftingLock() { + return this.aeManager.getCraftingLock(); + } + + @Override + public FluidTank[] getInputFluidTanks() { + return inputFluidTanks; + } + + @Override + public FluidTank[] getOutputFluidTanks() { + return outputFluidTanks; + } + + @Override + public ConcurrentHashMap getAETaskProgressMap() { + return this.aeManager.getAETaskProgressMap(); + } + + @Override + public AtomicInteger getTotalAEMaxProgressAtomic() { + return this.aeManager.getTotalAEMaxProgressAtomic(); + } + + @Override + public AtomicInteger getTotalAEProgressAtomic() { + return this.aeManager.getTotalAEProgressAtomic(); + } + + // ==================== PatternContainer 接口实现 ==================== + + /** + * 样板槽位的 InternalInventory 适配器 + * 将样板槽位包装成 AE2 的 InternalInventory 接口,用于样板管理终端访问 + */ + private final InternalInventory patternInventory = new InternalInventory() { + @Override + public int size() { + return PATTERN_SLOTS_COUNT; + } + + @Override + public ItemStack getStackInSlot(int slotIndex) { + if (slotIndex < 0 || slotIndex >= PATTERN_SLOTS_COUNT) { + return ItemStack.EMPTY; + } + return itemHandler.getStackInSlot(PATTERN_SLOTS_START + slotIndex); + } + + @Override + public void setItemDirect(int slotIndex, ItemStack stack) { + if (slotIndex < 0 || slotIndex >= PATTERN_SLOTS_COUNT) { + return; + } + itemHandler.setStackInSlot(PATTERN_SLOTS_START + slotIndex, stack); + } + + @Override + public boolean isItemValid(int slot, ItemStack stack) { + // 只允许编码样板 + return !stack.isEmpty() && PatternDetailsHelper.decodePattern(stack, level) != null; + } + + @Override + public int getSlotLimit(int slot) { + return 1; // 样板槽位只能放一个物品 + } + }; + + @Override + @Nullable + public IGrid getGrid() { + return mainNode.getGrid(); + } + + @Override + @Nullable + public IGrid getAeGrid() { + return getGrid(); + } + + @Override + public boolean isVisibleInTerminal() { + // 始终在样板管理终端中显示 + return true; + } + + @Override + public InternalInventory getTerminalPatternInventory() { + return patternInventory; + } + + @Override + public long getTerminalSortOrder() { + // 按位置排序 + return (long) worldPosition.getZ() << 24 ^ (long) worldPosition.getX() << 8 ^ worldPosition.getY(); + } + + @Override + public PatternContainerGroup getTerminalGroup() { + // 使用方块本身的图标和名称 + var blockState = getBlockState(); + var block = blockState.getBlock(); + var itemStack = new ItemStack(block); + var icon = AEItemKey.of(itemStack); + + // 使用 MenuProvider 的 getDisplayName() 方法 + Component name = this.getDisplayName(); + + return new PatternContainerGroup(icon, name, List.of()); + } + +} diff --git a/src/main/java/com/sorrowmist/useless/content/blockentities/AdvancedAlloyFurnaceData.java b/src/main/java/com/sorrowmist/useless/content/blockentities/AdvancedAlloyFurnaceData.java new file mode 100644 index 00000000..9ca02701 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/blockentities/AdvancedAlloyFurnaceData.java @@ -0,0 +1,93 @@ +package com.sorrowmist.useless.content.blockentities; + +import net.minecraft.world.inventory.ContainerData; + +/** + * 高级合金炉数据同步类 + * 用于在服务端和客户端之间同步方块实体的状态数据 + */ +public class AdvancedAlloyFurnaceData implements ContainerData { + + // ==================== 数据索引常量 ==================== + public static final int DATA_ENERGY_CAPACITY_LOW = 0; + public static final int DATA_ENERGY_CAPACITY_HIGH = 1; + public static final int DATA_ENERGY_STORED_LOW = 2; + public static final int DATA_ENERGY_STORED_HIGH = 3; + public static final int DATA_PROGRESS = 4; + public static final int DATA_MAX_PROGRESS = 5; + public static final int DATA_CURRENT_PARALLEL = 6; + public static final int DATA_HAS_MOLD = 7; + // AE网络合成任务相关 + public static final int DATA_AE_ACTIVE_TASKS = 8; + public static final int DATA_AE_TOTAL_PROGRESS = 9; + public static final int DATA_AE_TOTAL_MAX_PROGRESS = 10; + public static final int DATA_FURNACE_TIER = 11; + public static final int DATA_RETURN_OUTPUT_TO_AE = 12; + public static final int DATA_COUNT = 13; + + private final AdvancedAlloyFurnaceBlockEntity entity; + + AdvancedAlloyFurnaceData(AdvancedAlloyFurnaceBlockEntity entity) { + this.entity = entity; + } + + @Override + public int get(int index) { + return switch (index) { + case DATA_ENERGY_CAPACITY_LOW -> lowBits(this.entity.getEnergyManager().getMaxEnergyStoredLong()); + case DATA_ENERGY_CAPACITY_HIGH -> highBits(this.entity.getEnergyManager().getMaxEnergyStoredLong()); + case DATA_ENERGY_STORED_LOW -> lowBits(this.entity.getEnergyManager().getEnergyStoredLong()); + case DATA_ENERGY_STORED_HIGH -> highBits(this.entity.getEnergyManager().getEnergyStoredLong()); + case DATA_PROGRESS -> this.entity.getProgress(); + case DATA_MAX_PROGRESS -> this.entity.getMaxProgress(); + case DATA_CURRENT_PARALLEL -> this.entity.getCurrentParallel(); + case DATA_HAS_MOLD -> this.entity.hasMold() ? 1 : 0; + case DATA_AE_ACTIVE_TASKS -> this.entity.getActiveAETaskCount(); + case DATA_AE_TOTAL_PROGRESS -> this.entity.getTotalAEProgress(); + case DATA_AE_TOTAL_MAX_PROGRESS -> this.entity.getTotalAEMaxProgress(); + case DATA_FURNACE_TIER -> this.entity.getFurnaceTier(); + case DATA_RETURN_OUTPUT_TO_AE -> this.entity.isReturnOutputToAe() ? 1 : 0; + default -> 0; + }; + } + + @Override + public void set(int index, int value) { + switch (index) { + case DATA_ENERGY_CAPACITY_LOW -> this.entity.setMaxEnergy(joinBits( + value, highBits(this.entity.getMaxEnergy()))); + case DATA_ENERGY_CAPACITY_HIGH -> this.entity.setMaxEnergy(joinBits( + lowBits(this.entity.getMaxEnergy()), value)); + case DATA_ENERGY_STORED_LOW -> this.entity.setEnergy(joinBits( + value, highBits(this.entity.getEnergy()))); + case DATA_ENERGY_STORED_HIGH -> this.entity.setEnergy(joinBits( + lowBits(this.entity.getEnergy()), value)); + case DATA_PROGRESS -> this.entity.setProgress(value); + case DATA_MAX_PROGRESS -> this.entity.setMaxProgress(value); + case DATA_CURRENT_PARALLEL -> this.entity.setCurrentParallel(value); + case DATA_HAS_MOLD -> this.entity.setHasMold(value > 0); + case DATA_AE_ACTIVE_TASKS -> this.entity.setActiveAETaskCount(value); + case DATA_AE_TOTAL_PROGRESS -> this.entity.setTotalAEProgress(value); + case DATA_AE_TOTAL_MAX_PROGRESS -> this.entity.setTotalAEMaxProgress(value); + case DATA_FURNACE_TIER -> this.entity.setClientFurnaceTier(value); + case DATA_RETURN_OUTPUT_TO_AE -> this.entity.setReturnOutputToAe(value > 0); + } + } + + @Override + public int getCount() { + return DATA_COUNT; + } + + public static long joinBits(int low, int high) { + return ((long) high << 32) | Integer.toUnsignedLong(low); + } + + private static int lowBits(long value) { + return (int) value; + } + + private static int highBits(long value) { + return (int) (value >>> 32); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/blockentities/HighStackItemStackHandler.java b/src/main/java/com/sorrowmist/useless/content/blockentities/HighStackItemStackHandler.java new file mode 100644 index 00000000..510d4f46 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/blockentities/HighStackItemStackHandler.java @@ -0,0 +1,256 @@ +package com.sorrowmist.useless.content.blockentities; + +import com.sorrowmist.useless.init.ModTags; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraftforge.items.ItemStackHandler; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.function.IntConsumer; + +/** + * 支持高堆叠数量的 ItemStackHandler + * 允许槽位堆叠数量达到 Integer.MAX_VALUE(约21亿) + */ +public class HighStackItemStackHandler extends ItemStackHandler { + // NBT常量 + private static final String TAG_ITEMS = "Items"; + private static final String TAG_SLOT = "Slot"; + private static final String TAG_COUNT = "RealCount"; + private static final String TAG_STACK = "Stack"; + + // 槽位常量 + private final int moldSlot; + private final int catalystSlot; + private final int inputSlotsStart; + private final int inputSlotsCount; + private final int patternSlotsStart; + private final int patternSlotsEnd; + + // 回调接口 + private final Runnable onChangedCallback; + private final IntConsumer onMoldSlotChanged; + private final IntConsumer onInputOrCatalystChanged; + private final IntConsumer onPatternSlotChanged; + + /** + * 创建高堆叠物品处理器 + * + * @param size 槽位数量 + * @param moldSlot 模具槽位索引 + * @param catalystSlot 催化剂槽位索引 + * @param inputSlotsStart 输入槽位起始索引 + * @param inputSlotsCount 输入槽位数量 + * @param patternSlotsStart 样板槽位起始索引 + * @param patternSlotsEnd 样板槽位结束索引 + * @param onChangedCallback 数据变化回调 + * @param onMoldSlotChanged 模具槽位变化回调 + * @param onInputOrCatalystChanged 输入或催化剂槽位变化回调 + * @param onPatternSlotChanged 样板槽位变化回调 + */ + public HighStackItemStackHandler(int size, int moldSlot, int catalystSlot, + int inputSlotsStart, int inputSlotsCount, + int patternSlotsStart, int patternSlotsEnd, + Runnable onChangedCallback, + IntConsumer onMoldSlotChanged, + IntConsumer onInputOrCatalystChanged, + IntConsumer onPatternSlotChanged) { + super(size); + this.moldSlot = moldSlot; + this.catalystSlot = catalystSlot; + this.inputSlotsStart = inputSlotsStart; + this.inputSlotsCount = inputSlotsCount; + this.patternSlotsStart = patternSlotsStart; + this.patternSlotsEnd = patternSlotsEnd; + this.onChangedCallback = onChangedCallback; + this.onMoldSlotChanged = onMoldSlotChanged; + this.onInputOrCatalystChanged = onInputOrCatalystChanged; + this.onPatternSlotChanged = onPatternSlotChanged; + } + + @Override + public @NotNull ItemStack insertItem(int slot, @NotNull ItemStack stack, boolean simulate) { + if (stack.isEmpty()) { + return ItemStack.EMPTY; + } + + // 处理模具重定向 + if (stack.is(ModTags.MOLDS) && slot != moldSlot) { + ItemStack moldSlotStack = this.getStackInSlot(moldSlot); + if (moldSlotStack.isEmpty()) { + return this.insertItem(moldSlot, stack, simulate); + } + } + + // 验证槽位和物品有效性 + if (slot < 0 || slot >= this.stacks.size() || !this.isItemValid(slot, stack)) { + return stack; + } + + ItemStack existing = this.stacks.get(slot); + int limit = this.getSlotLimit(slot); + + // 检查是否可以合并 + if (!existing.isEmpty() && !ItemStack.isSameItemSameTags(existing, stack)) { + return stack; + } + + // 计算可以插入的数量(不受物品默认maxStackSize限制) + int currentCount = existing.isEmpty() ? 0 : existing.getCount(); + int canInsert = limit - currentCount; + + if (canInsert <= 0) { + return stack; + } + + int toInsert = Math.min(stack.getCount(), canInsert); + + if (!simulate) { + if (existing.isEmpty()) { + this.stacks.set(slot, stack.copyWithCount(currentCount + toInsert)); + } else { + existing.grow(toInsert); + } + this.onContentsChanged(slot); + } + + if (toInsert >= stack.getCount()) { + return ItemStack.EMPTY; + } + + return stack.copyWithCount(stack.getCount() - toInsert); + } + + @Override + public int getSlotLimit(int slot) { + return slot == moldSlot ? 1 : Integer.MAX_VALUE; + } + + @Override + public boolean isItemValid(int slot, @NotNull ItemStack stack) { + return slot == catalystSlot ? stack.is(ModTags.CATALYSTS) : super.isItemValid(slot, stack); + } + + /** + * 自定义序列化方法,支持超过99的堆叠数量 + */ + @Override + public @NotNull CompoundTag serializeNBT() { + CompoundTag nbt = new CompoundTag(); + ListTag itemsTag = new ListTag(); + + for (int i = 0; i < this.stacks.size(); i++) { + ItemStack stack = this.stacks.get(i); + if (!stack.isEmpty()) { + CompoundTag itemTag = new CompoundTag(); + itemTag.putInt(TAG_SLOT, i); + itemTag.put(TAG_STACK, stack.copyWithCount(1).save(new CompoundTag())); + itemTag.putInt(TAG_COUNT, stack.getCount()); + + itemsTag.add(itemTag); + } + } + + nbt.put(TAG_ITEMS, itemsTag); + return nbt; + } + + /** + * 自定义反序列化方法,支持超过99的堆叠数量 + */ + @Override + public void deserializeNBT(CompoundTag nbt) { + + ListTag itemsTag = nbt.getList(TAG_ITEMS, Tag.TAG_COMPOUND); + + // 清空当前槽位 + Collections.fill(this.stacks, ItemStack.EMPTY); + + for (int i = 0; i < itemsTag.size(); i++) { + CompoundTag itemTag = itemsTag.getCompound(i); + int slot = itemTag.contains(TAG_SLOT, Tag.TAG_BYTE) ? itemTag.getByte(TAG_SLOT) & 255 : itemTag.getInt(TAG_SLOT); + + if (slot >= 0 && slot < this.stacks.size()) { + ItemStack stack = this.deserializeStack(itemTag); + if (!stack.isEmpty()) { + this.stacks.set(slot, stack); + } + } + } + + this.onLoad(); + } + + private ItemStack deserializeStack(CompoundTag itemTag) { + ItemStack stack = ItemStack.EMPTY; + if (itemTag.contains(TAG_STACK)) { + stack = ItemStack.of(itemTag.getCompound(TAG_STACK)); + } else if (itemTag.contains("id")) { + String itemId = itemTag.getString("id"); + Item item = BuiltInRegistries.ITEM.get(new ResourceLocation(itemId)); + if (item != Items.AIR) { + stack = new ItemStack(item); + if (itemTag.contains("components") && !itemTag.getCompound("components").isEmpty()) { + throw new IllegalArgumentException("Cannot downgrade 1.21 inventory components; use a 1.20.1 world"); + } + // Older native Forge saves may put NBT/capabilities beside the item id. + CompoundTag nativeStack=itemTag.copy(); nativeStack.putByte("Count", (byte)1); + stack=ItemStack.of(nativeStack); + } + } + + if (!stack.isEmpty()) { + stack.setCount(Math.max(0,itemTag.contains(TAG_COUNT) ? itemTag.getInt(TAG_COUNT) : itemTag.contains("Count") ? itemTag.getInt("Count") : 1)); + } + return stack; + } + + public CompoundTag serializeNBT(HolderLookup.Provider ignored) { return serializeNBT(); } + public void deserializeNBT(HolderLookup.Provider ignored, CompoundTag nbt) { deserializeNBT(nbt); } + + /** Unlike vanilla inventories, this machine must be able to consume a whole high-count batch. */ + @Override + public @NotNull ItemStack extractItem(int slot, int amount, boolean simulate) { + validateSlotIndex(slot); + if (amount <= 0) return ItemStack.EMPTY; + ItemStack existing=this.stacks.get(slot); + if (existing.isEmpty()) return ItemStack.EMPTY; + int extracted=Math.min(amount,existing.getCount()); + ItemStack result=existing.copyWithCount(extracted); + if (!simulate) { + this.stacks.set(slot,extracted==existing.getCount()?ItemStack.EMPTY:existing.copyWithCount(existing.getCount()-extracted)); + this.onContentsChanged(slot); + } + return result; + } + + @Override + protected void onContentsChanged(int slot) { + if (onChangedCallback != null) { + onChangedCallback.run(); + } + + if (slot == moldSlot && onMoldSlotChanged != null) { + onMoldSlotChanged.accept(slot); + } + + boolean isInputSlot = slot >= inputSlotsStart && slot < inputSlotsStart + inputSlotsCount; + boolean isRecipeAffectingSlot = isInputSlot || slot == moldSlot || slot == catalystSlot; + if (isRecipeAffectingSlot && onInputOrCatalystChanged != null) { + onInputOrCatalystChanged.accept(slot); + } + + // 当样板槽位变化时,更新样板列表 + if (slot >= patternSlotsStart && slot <= patternSlotsEnd && onPatternSlotChanged != null) { + onPatternSlotChanged.accept(slot); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/blockentities/OreGeneratorBlockEntity.java b/src/main/java/com/sorrowmist/useless/content/blockentities/OreGeneratorBlockEntity.java new file mode 100644 index 00000000..a9c90219 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/blockentities/OreGeneratorBlockEntity.java @@ -0,0 +1,383 @@ +package com.sorrowmist.useless.content.blockentities; + +import appeng.api.config.Actionable; +import appeng.api.networking.GridFlags; +import appeng.api.networking.GridHelper; +import appeng.api.networking.IGrid; +import appeng.api.networking.IGridNode; +import appeng.api.networking.IGridNodeListener; +import appeng.api.networking.IInWorldGridNodeHost; +import appeng.api.networking.IManagedGridNode; +import appeng.api.networking.security.IActionHost; +import appeng.api.networking.security.IActionSource; +import appeng.api.stacks.AEItemKey; +import appeng.api.storage.MEStorage; +import appeng.api.util.AECableType; +import com.sorrowmist.useless.content.menus.OreGeneratorMenu; +import com.sorrowmist.useless.core.config.ConfigManager; +import com.sorrowmist.useless.init.ModBlockEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; +import net.minecraft.network.chat.Component; +import net.minecraft.world.Containers; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraftforge.common.capabilities.ForgeCapabilities; +import net.minecraftforge.items.IItemHandler; +import net.minecraftforge.items.ItemStackHandler; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** Server-side state and AE host for the configurable ore generator. */ +public final class OreGeneratorBlockEntity extends BlockEntity + implements MenuProvider, IInWorldGridNodeHost, + IGridNodeListener, IActionHost { + public static final int OUTPUT_INTERVAL_TICKS = 20; + public static final int MAX_SAMPLE_SLOTS = 540; + public static final long DEFAULT_OUTPUT_RATE = 1L; + public static final int MENU_DATA_COUNT = 7; + + private static final TagKey RAW_MATERIALS = TagKey.create( + Registries.ITEM, + new ResourceLocation("c", "raw_materials") + ); + private static final TagKey ORES = TagKey.create( + Registries.ITEM, + new ResourceLocation("c", "ores") + ); + + private final OreGeneratorSampleHandler samples = new OreGeneratorSampleHandler( + ConfigManager::getOreGeneratorSlots, + OreGeneratorBlockEntity::isValidSample, + this::samplesChanged); + private final IManagedGridNode mainNode; + private final IActionSource actionSource; + private final ContainerData menuData = new ContainerData() { + @Override + public int get(int index) { + return switch (index) { + case 0 -> lowBits(outputRate); + case 1 -> highBits(outputRate); + case 2 -> getCountdownTicks(); + case 3 -> samples.getActiveSlots(); + case 4 -> ConfigManager.getOreGeneratorSlots(); + case 5 -> outputToAe ? 1 : 0; + case 6 -> isAeNetworkOnline() ? 1 : 0; + default -> 0; + }; + } + + @Override + public void set(int index, int value) { + // The server owns the live values. Client menus use this method as + // a packet-backed buffer populated by AbstractContainerMenu. + switch (index) { + case 0 -> outputRate = joinBits(value, highBits(outputRate)); + case 1 -> outputRate = joinBits(lowBits(outputRate), value); + case 2 -> outputTickCounter = Math.max(0, + Math.min(OUTPUT_INTERVAL_TICKS - 1, OUTPUT_INTERVAL_TICKS - value)); + case 5 -> outputToAe = value > 0; + default -> { + } + } + } + + @Override + public int getCount() { + return MENU_DATA_COUNT; + } + }; + + private long outputRate = DEFAULT_OUTPUT_RATE; + private int outputTickCounter; + private boolean outputToAe; + private boolean unloading; + + public OreGeneratorBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.ORE_GENERATOR.get(), pos, state); + this.actionSource = IActionSource.ofMachine(this); + this.mainNode = GridHelper.createManagedNode(this, this) + .setInWorldNode(true) + .setTagName("node") + .setFlags(GridFlags.REQUIRE_CHANNEL); + } + + public OreGeneratorSampleHandler getSamples() { + return samples; + } + + public long getOutputRate() { + return outputRate; + } + + public void setOutputRate(long value) { + long normalized = Math.max(1L, value); + if (outputRate != normalized) { + outputRate = normalized; + setChanged(); + } + } + + public int getCountdownTicks() { + return outputTickCounter <= 0 + ? OUTPUT_INTERVAL_TICKS + : OUTPUT_INTERVAL_TICKS - outputTickCounter; + } + + public boolean isOutputToAe() { + return outputToAe; + } + + public void setOutputToAe(boolean value) { + if (outputToAe != value) { + outputToAe = value; + setChanged(); + } + } + + public ContainerData getMenuData() { + return menuData; + } + + public boolean isAeNetworkOnline() { + IGridNode node = mainNode.getNode(); + return mainNode.isActive() && node != null && node.isActive() && node.getGrid() != null; + } + + public void tick() { + if (level == null || level.isClientSide || unloading || isRemoved()) return; + + outputTickCounter++; + if (outputTickCounter < OUTPUT_INTERVAL_TICKS) { + setChanged(); + return; + } + + outputTickCounter = 0; + for (int slot = 0; slot < samples.getActiveSlots(); slot++) { + ItemStack sample = samples.getStackInSlot(slot); + if (!isValidSample(sample)) continue; + outputSample(sample); + } + setChanged(); + } + + private void outputSample(ItemStack sample) { + long remaining = outputRate; + if (outputToAe) { + long inserted = tryOutputToAe(sample, remaining); + remaining -= Math.max(0L, Math.min(remaining, inserted)); + } + if (remaining <= 0L) return; + + // ItemStack counts are int-sized. Keep this as one stack and let the + // target handler decide how its own slots can accept that stack. + ItemStack output = createExternalOutput(sample, remaining); + if (!output.isEmpty()) { + tryExportItem(output); + } + } + + /** Returns the largest count that can be represented by one external ItemStack. */ + public static int clampExternalCount(long amount) { + if (amount <= 0L) return 0; + return amount >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) amount; + } + + static ItemStack createExternalOutput(ItemStack sample, long amount) { + int count = clampExternalCount(amount); + if (sample.isEmpty() || count == 0) return ItemStack.EMPTY; + + ItemStack output = sample.copy(); + output.setCount(count); + return output; + } + + private long tryOutputToAe(ItemStack sample, long amount) { + if (amount <= 0L) return 0L; + IGridNode node = mainNode.getNode(); + if (!mainNode.isActive() || node == null || !node.isActive()) return 0L; + IGrid grid = node.getGrid(); + if (grid == null) return 0L; + + AEItemKey key = AEItemKey.of(sample); + if (key == null) return 0L; + MEStorage storage = grid.getStorageService().getInventory(); + long inserted = storage.insert(key, amount, Actionable.MODULATE, actionSource); + return Math.max(0L, Math.min(amount, inserted)); + } + + private void tryExportItem(ItemStack stack) { + if (level == null || stack.isEmpty()) return; + + ItemStack remainder = stack; + for (Direction direction : Direction.values()) { + BlockPos adjacentPos = worldPosition.relative(direction); + IItemHandler targetHandler = com.sorrowmist.useless.compat.forge1201.capability.CapabilityAccess.get(level, + ForgeCapabilities.ITEM_HANDLER, adjacentPos, direction.getOpposite()); + if (targetHandler == null) continue; + + for (int slot = 0; slot < targetHandler.getSlots() && !remainder.isEmpty(); slot++) { + remainder = targetHandler.insertItem(slot, remainder, false); + } + if (remainder.isEmpty()) return; + } + } + + public static boolean isValidSample(ItemStack stack) { + return !stack.isEmpty() && (stack.is(RAW_MATERIALS) || stack.is(ORES)); + } + + private void samplesChanged() { + setChanged(); + } + + @Override + public @NotNull Component getDisplayName() { + return Component.translatable("container.useless_mod.ore_generator"); + } + + @Override + public @Nullable AbstractContainerMenu createMenu(int containerId, Inventory inventory, Player player) { + return new OreGeneratorMenu(containerId, inventory, worldPosition); + } + + @Override + public void load(CompoundTag tag) { + HolderLookup.Provider registries = level == null ? net.minecraft.core.RegistryAccess.EMPTY : level.registryAccess(); + super.load(tag); + if (tag.contains("Samples")) { + samples.deserializeNBT(tag.getCompound("Samples")); + normalizeSamples(); + } else if (tag.contains("Inventory")) { + // Migrate the old single-slot generator input into the first sample + // slot while deliberately discarding its former stack multiplier. + ItemStackHandler legacy = new ItemStackHandler(1); + legacy.deserializeNBT(tag.getCompound("Inventory")); + ItemStack oldSample = legacy.getStackInSlot(0); + if (isValidSample(oldSample)) samples.setStackInSlot(0, oldSample); + } + outputRate = Math.max(1L, tag.getLong("OutputRate")); + outputTickCounter = Math.max(0, + Math.min(OUTPUT_INTERVAL_TICKS - 1, tag.getInt("OutputTickCounter"))); + outputToAe = tag.getBoolean("OutputToAe"); + mainNode.loadFromNBT(tag); + } + + private void normalizeSamples() { + for (int slot = 0; slot < samples.getSlots(); slot++) { + ItemStack sample = samples.getStackInSlot(slot); + if (sample.isEmpty()) continue; + if (!isValidSample(sample)) { + samples.setStackInSlot(slot, ItemStack.EMPTY); + continue; + } + if (sample.getCount() != 1) { + ItemStack normalized = sample.copy(); + normalized.setCount(1); + samples.setStackInSlot(slot, normalized); + } + } + } + + @Override + protected void saveAdditional(CompoundTag tag) { + HolderLookup.Provider registries = level == null ? net.minecraft.core.RegistryAccess.EMPTY : level.registryAccess(); + super.saveAdditional(tag); + tag.put("Samples", samples.serializeNBT()); + tag.putLong("OutputRate", outputRate); + tag.putInt("OutputTickCounter", outputTickCounter); + tag.putBoolean("OutputToAe", outputToAe); + mainNode.saveToNBT(tag); + } + + public void drops() { + SimpleContainer inventory = new SimpleContainer(samples.getSlots()); + for (int slot = 0; slot < samples.getSlots(); slot++) { + inventory.setItem(slot, samples.getStackInSlot(slot)); + } + if (level != null) Containers.dropContents(level, worldPosition, inventory); + } + + @Override + public void onSaveChanges(OreGeneratorBlockEntity nodeOwner, IGridNode node) { + setChanged(); + } + + @Override + public void onGridChanged(OreGeneratorBlockEntity nodeOwner, IGridNode node) { + setChanged(); + } + + @Override + public void onStateChanged(OreGeneratorBlockEntity nodeOwner, IGridNode node, + IGridNodeListener.State state) { + setChanged(); + } + + public @NotNull IManagedGridNode getMainNode() { + return mainNode; + } + + @Override + public @Nullable IGridNode getGridNode(Direction direction) { + return mainNode.getNode(); + } + + @Override + public AECableType getCableConnectionType(Direction direction) { + return AECableType.SMART; + } + + @Override + public IGridNode getActionableNode() { + return mainNode.getNode(); + } + + @Override + public void clearRemoved() { + super.clearRemoved(); + unloading = false; + GridHelper.onFirstTick(this, blockEntity -> + blockEntity.mainNode.create(blockEntity.getLevel(), blockEntity.getBlockPos())); + } + + @Override + public void setRemoved() { + unloading = true; + super.setRemoved(); + mainNode.destroy(); + } + + @Override + public void onChunkUnloaded() { + unloading = true; + super.onChunkUnloaded(); + mainNode.destroy(); + } + + private static long joinBits(int low, int high) { + return Integer.toUnsignedLong(low) | (long) high << 32; + } + + private static int lowBits(long value) { + return (int) value; + } + + private static int highBits(long value) { + return (int) (value >>> 32); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/blockentities/OreGeneratorSampleHandler.java b/src/main/java/com/sorrowmist/useless/content/blockentities/OreGeneratorSampleHandler.java new file mode 100644 index 00000000..a4b79770 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/blockentities/OreGeneratorSampleHandler.java @@ -0,0 +1,32 @@ +package com.sorrowmist.useless.content.blockentities; + +import net.minecraft.world.item.ItemStack; + +import java.util.function.IntSupplier; +import java.util.function.Predicate; + +/** Sample storage for the ore generator. Every active slot holds one template item. */ +public final class OreGeneratorSampleHandler extends RecoverableItemStackHandler { + public static final int MAX_SLOTS = 540; + + public OreGeneratorSampleHandler(IntSupplier activeSlots, Predicate validator, + Runnable changeListener) { + super(MAX_SLOTS, 0, activeSlots, validator, changeListener); + } + + @Override + public int getSlotLimit(int slot) { + return 1; + } + + @Override + public void setStackInSlot(int slot, ItemStack stack) { + if (!stack.isEmpty() && isValidItem(stack)) { + ItemStack sample = stack.copy(); + sample.setCount(1); + super.setStackInSlot(slot, sample); + } else { + super.setStackInSlot(slot, ItemStack.EMPTY); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/blockentities/PagedMenuPageMemory.java b/src/main/java/com/sorrowmist/useless/content/blockentities/PagedMenuPageMemory.java new file mode 100644 index 00000000..16e1a73c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/blockentities/PagedMenuPageMemory.java @@ -0,0 +1,68 @@ +package com.sorrowmist.useless.content.blockentities; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +/** Stores one paged-menu position for each player who used a block entity. */ +public final class PagedMenuPageMemory { + private static final String TAG = "PagedMenuPages"; + + private final Map pages = new HashMap<>(); + private final Runnable markChanged; + + public PagedMenuPageMemory(Runnable markChanged) { + this.markChanged = Objects.requireNonNull(markChanged, "markChanged"); + } + + public int get(UUID playerId) { + return Math.max(0, pages.getOrDefault(playerId, 0)); + } + + public void set(UUID playerId, int page) { + Objects.requireNonNull(playerId, "playerId"); + int normalized = Math.max(0, page); + Integer previous = pages.get(playerId); + if (normalized == 0) { + if (previous == null) return; + pages.remove(playerId); + markChanged.run(); + return; + } + if (previous != null && previous == normalized) return; + pages.put(playerId, normalized); + markChanged.run(); + } + + public void load(CompoundTag tag) { + pages.clear(); + if (!tag.contains(TAG, Tag.TAG_COMPOUND)) return; + + CompoundTag savedPages = tag.getCompound(TAG); + for (String key : savedPages.getAllKeys()) { + try { + int page = savedPages.getInt(key); + if (page > 0) { + pages.put(UUID.fromString(key), page); + } + } catch (IllegalArgumentException ignored) { + // Ignore malformed player keys from edited or damaged NBT. + } + } + } + + public void save(CompoundTag tag) { + if (pages.isEmpty()) { + tag.remove(TAG); + return; + } + + CompoundTag savedPages = new CompoundTag(); + pages.forEach((playerId, page) -> savedPages.putInt(playerId.toString(), page)); + tag.put(TAG, savedPages); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/blockentities/RecoverableItemStackHandler.java b/src/main/java/com/sorrowmist/useless/content/blockentities/RecoverableItemStackHandler.java new file mode 100644 index 00000000..5c9e04c9 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/blockentities/RecoverableItemStackHandler.java @@ -0,0 +1,93 @@ +package com.sorrowmist.useless.content.blockentities; + +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.items.ItemStackHandler; + +import java.util.Objects; +import java.util.Collections; +import java.util.function.IntSupplier; +import java.util.function.Predicate; + +/** Fixed backing storage whose configured overflow becomes withdraw-only recovery storage. */ +public class RecoverableItemStackHandler extends ItemStackHandler { + public static final int MAX_SLOTS = 4096; + + private final IntSupplier activeSlots; + private final int minimumActiveSlots; + private final Predicate validator; + private final Runnable changeListener; + private int changeBatchDepth; + private boolean changePending; + + public RecoverableItemStackHandler(IntSupplier activeSlots, Predicate validator, Runnable changeListener) { + this(MAX_SLOTS, 27, activeSlots, validator, changeListener); + } + + public RecoverableItemStackHandler(int capacity, int minimumActiveSlots, IntSupplier activeSlots, + Predicate validator, Runnable changeListener) { + super(validateCapacity(capacity)); + this.activeSlots = Objects.requireNonNull(activeSlots, "activeSlots"); + this.minimumActiveSlots = Math.max(0, Math.min(capacity, minimumActiveSlots)); + this.validator = Objects.requireNonNull(validator, "validator"); + this.changeListener = Objects.requireNonNull(changeListener, "changeListener"); + } + + public int getActiveSlots() { + return Math.max(minimumActiveSlots, Math.min(getSlots(), activeSlots.getAsInt())); + } + + public boolean isRecoverySlot(int slot) { + return slot >= getActiveSlots() && slot < getSlots(); + } + + @Override + public boolean isItemValid(int slot, ItemStack stack) { + return slot >= 0 && slot < getActiveSlots() && validator.test(stack); + } + + protected final boolean isValidItem(ItemStack stack) { + return validator.test(stack); + } + + /** Groups several backing-slot writes into one inventory change callback. */ + public void withChangeBatch(Runnable action) { + changeBatchDepth++; + try { + action.run(); + } finally { + changeBatchDepth--; + if (changeBatchDepth == 0 && changePending) { + changePending = false; + changeListener.run(); + } + } + } + + @Override + protected void onContentsChanged(int slot) { + if (changeBatchDepth > 0) { + changePending = true; + } else { + changeListener.run(); + } + } + + public void deserializeNBT(HolderLookup.Provider provider, CompoundTag nbt) { deserializeNBT(nbt); } + + @Override + public void deserializeNBT(CompoundTag nbt) { + CompoundTag copy = nbt.copy(); + copy.remove("Size"); + Collections.fill(this.stacks, ItemStack.EMPTY); + super.deserializeNBT(copy); + } + + private static int validateCapacity(int capacity) { + if (capacity <= 0 || capacity > MAX_SLOTS) { + throw new IllegalArgumentException("Capacity must be between 1 and " + MAX_SLOTS); + } + return capacity; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/blockentities/ThermostatBlockEntity.java b/src/main/java/com/sorrowmist/useless/content/blockentities/ThermostatBlockEntity.java new file mode 100644 index 00000000..4171cdf0 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/blockentities/ThermostatBlockEntity.java @@ -0,0 +1,177 @@ +package com.sorrowmist.useless.content.blockentities; + +import com.sorrowmist.useless.content.blocks.TemperatureRegulatorBlock; +import com.sorrowmist.useless.init.ModBlockEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientGamePacketListener; +import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; + +/** + * 「温度调节器」方块的「恒温源」状态。 + * + *

温度调节器是一个独立的方块;本 BE 给它一个可配置的温度,语义是无限恒温源: + * 温度恒定,可被无限取热 / 放热(不守恒)。

+ * + *

本类刻意不引用任何 Mekanism / PneumaticCraft 类型。 那两个模组的热适配器由 + * {@code compat} 层反射加载后缓存进 {@link #heatAdapters}(不透明的 {@code Object}), + * 主动驱动则通过 {@link ThermostatHeatHook} 安装,因此没装对应模组时本类照常加载。

+ * + *

单位与范围

+ * + *

温度单位是开尔文,与 Mekanism({@code HeatAPI.AMBIENT_TEMP = 300})和气动工艺 + * ({@code HeatExchangerLogicTicking} 把机器自身温度夹到 0~2273)一致。

+ * + *

存 {@code long},范围 {@code [0, Long.MAX_VALUE]}。实践上没必要填太大:气动会把机器温度 + * 夹在 2273 K,超过就只是饱和;而 {@code double} 在 2^53(约 9e15)以上会丢整数精度, + * 所以超过这个量级的值只是「更大」,不再精确。

+ */ +public final class ThermostatBlockEntity extends BlockEntity { + + /** 默认温度:300 K,即 Mek 定义的「水的温度」/ 气动的环境温度。 */ + public static final long DEFAULT_TEMPERATURE = 300L; + + /** 允许的最低温度。 */ + public static final long MIN_TEMPERATURE = 0L; + + private static final String TAG_TEMPERATURE = "Temperature"; + private static final String TAG_ENABLED = "Enabled"; + + private long temperature = DEFAULT_TEMPERATURE; + private boolean enabled; + + /** + * 外部热适配器缓存,按「模组 id」区分(主代码不关心里面是什么)。 + * + *

惰性分配:绝大多数塑料方块从未被配置,也就不该为它们各建一个 map。缓存是必要的—— + * Mek 的 {@code CapabilityCache} 与气动的 {@code IdentityHashMap} 都按实例身份认连接, + * 每次能力查询都 new 一个适配器会让它们认不出是同一条热通路。

+ */ + @Nullable + private Map heatAdapters; + + public ThermostatBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.TEMPERATURE_REGULATOR.get(), pos, state); + } + + public long getTemperature() { + return temperature; + } + + public boolean isEnabled() { + return enabled; + } + + /** 取某个模组之前缓存的热适配器;没缓存过返回 {@code null}。 */ + @Nullable + public Object getHeatAdapter(String key) { + Map adapters = heatAdapters; + return adapters == null ? null : adapters.get(key); + } + + /** 缓存某个模组的热适配器。 */ + public void putHeatAdapter(String key, Object adapter) { + if (heatAdapters == null) { + heatAdapters = new HashMap<>(2); + } + heatAdapters.put(key, adapter); + } + + /** 开启 / 关闭恒温能力。 */ + public void setEnabled(boolean value) { + if (this.enabled == value) { + return; + } + this.enabled = value; + markUpdated(); + if (level != null && !level.isClientSide) { + // 提醒邻居重做一次热连接发现:气动的机器只在 onLoad / neighborChanged 时重扫, + // 不推这一下,它可能一直抱着「这里没有热交换器」的旧结论。 + level.updateNeighborsAt(worldPosition, getBlockState().getBlock()); + } + } + + /** 设定温度(开尔文);夹到 {@code [0, Long.MAX_VALUE]}。 */ + public void setTemperature(long kelvin) { + long clamped = Math.max(MIN_TEMPERATURE, kelvin); + if (this.temperature == clamped) { + return; + } + this.temperature = clamped; + markUpdated(); + } + + private void markUpdated() { + setChanged(); + if (level == null || level.isClientSide) { + return; + } + BlockState oldState = getBlockState(); + BlockState newState = TemperatureRegulatorBlock.stateFor(oldState, temperature, enabled); + if (newState != oldState) { + // ⛔ 温度档位 / 开关必须真的落到 BlockState 上:染色回调跑在区块网格 + // (RenderChunkRegion)里,那里 getBlockEntity 恒为 null,查 BE 只会永远回落到 + // 默认温度(症状:设多少度都是同一个颜色)。这一步顺带把 BlockState 与 BE 数据 + // (Jade 要读)一起同步给客户端。 + level.setBlock(worldPosition, newState, Block.UPDATE_ALL); + } else { + level.sendBlockUpdated(worldPosition, oldState, oldState, Block.UPDATE_ALL); + } + // Native Forge exposes the same live heat adapter throughout this BE's lifetime. + // Its methods read current temperature/enabled state; invalidating the whole BE here + // would permanently kill attached providers. setEnabled notifies neighbors separately. + } + + @Override + protected void saveAdditional(CompoundTag tag) { + HolderLookup.Provider registries = level == null ? net.minecraft.core.RegistryAccess.EMPTY : level.registryAccess(); + super.saveAdditional(tag); + tag.putLong(TAG_TEMPERATURE, temperature); + tag.putBoolean(TAG_ENABLED, enabled); + } + + @Override + public void load(CompoundTag tag) { + HolderLookup.Provider registries = level == null ? net.minecraft.core.RegistryAccess.EMPTY : level.registryAccess(); + super.load(tag); + // getLong 接受 TAG_ANY_NUMERIC,所以旧存档里的 INT 温度能原样读回来。 + if (tag.contains(TAG_TEMPERATURE)) { + temperature = Math.max(MIN_TEMPERATURE, tag.getLong(TAG_TEMPERATURE)); + } + if (tag.contains(TAG_ENABLED)) { + enabled = tag.getBoolean(TAG_ENABLED); + } + } + + @Override + public CompoundTag getUpdateTag() { + HolderLookup.Provider registries = level == null ? net.minecraft.core.RegistryAccess.EMPTY : level.registryAccess(); + CompoundTag tag = new CompoundTag(); + saveAdditional(tag); + return tag; + } + + @Override + public Packet getUpdatePacket() { + return ClientboundBlockEntityDataPacket.create(this); + } + + /** 摄氏温度,仅用于界面显示。 */ + public static double toCelsius(long kelvin) { + return kelvin - 273.15D; + } + + /** 把摄氏温度换算回开尔文(界面输入用)。 */ + public static long fromCelsius(double celsius) { + return Math.max(MIN_TEMPERATURE, Math.round(celsius + 273.15D)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/blockentities/ThermostatHeatHook.java b/src/main/java/com/sorrowmist/useless/content/blockentities/ThermostatHeatHook.java new file mode 100644 index 00000000..1b5df428 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/blockentities/ThermostatHeatHook.java @@ -0,0 +1,36 @@ +package com.sorrowmist.useless.content.blockentities; + +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import org.jetbrains.annotations.Nullable; + +/** + * 恒温方块「主动驱动」外部热系统的钩子。 + * + *

为什么需要它

+ * + *

Mekanism 的传热是发送方驱动:{@code ITileHeatHandler.simulateAdjacent()} 的公式只用 + * 「自身温度 − 环境温度」,从不读邻居温度。所以一个只暴露 {@code Capabilities.HEAT} + * 的方块只会被 Mek 机器当成「通往 300K 的散热片」,永远加不热机器。

+ * + *

要让恒温方块真正驱动 Mek 机器,必须由方块自己当发送方,每 tick 主动把热推给邻居。 + * 那段逻辑要用 {@code mekanism.api.heat.IHeatHandler},属于可选集成,不能出现在主代码里, + * 因此这里只留一个不依赖任何外部类型的钩子:compat 层在注册能力时安装实现, + * 没装 Mekanism 时 {@link #driver} 为 {@code null},{@link ThermostatBlockEntity} 的 + * tick 就成了空操作。

+ */ +public final class ThermostatHeatHook { + + /** 驱动实现;由可选集成层安装,未装对应模组时为 {@code null}。 */ + @Nullable + public static volatile HeatDriver driver; + + private ThermostatHeatHook() { + } + + /** 一次服务端 tick 的热驱动。 */ + @FunctionalInterface + public interface HeatDriver { + void tick(ServerLevel level, BlockPos pos, ThermostatBlockEntity thermostat); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/blockentities/multiblock/MePatternAssemblyBlockEntity.java b/src/main/java/com/sorrowmist/useless/content/blockentities/multiblock/MePatternAssemblyBlockEntity.java new file mode 100644 index 00000000..e9a9d1d6 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/blockentities/multiblock/MePatternAssemblyBlockEntity.java @@ -0,0 +1,507 @@ +package com.sorrowmist.useless.content.blockentities.multiblock; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.implementations.blockentities.PatternContainerGroup; +import appeng.api.inventories.InternalInventory; +import appeng.api.networking.GridFlags; +import appeng.api.networking.GridHelper; +import appeng.api.networking.IGrid; +import appeng.api.networking.IGridNode; +import appeng.api.networking.IGridNodeListener; +import appeng.api.networking.IInWorldGridNodeHost; +import appeng.api.networking.IManagedGridNode; +import appeng.api.networking.crafting.ICraftingProvider; +import appeng.api.networking.security.IActionHost; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.KeyCounter; +import appeng.api.util.AECableType; +import appeng.blockentity.AEBaseBlockEntity; +import appeng.core.definitions.AEItems; +import appeng.helpers.patternprovider.PatternContainer; +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.crafting.SmartDoublingCraftingProvider; +import com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerProvider; +import com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerTarget; +import com.sorrowmist.useless.content.blockentities.PagedMenuPageMemory; +import com.sorrowmist.useless.content.blockentities.RecoverableItemStackHandler; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.OmniversalPatternWarnings; +import com.sorrowmist.useless.core.config.ConfigManager; +import com.sorrowmist.useless.core.component.ExternalInventoryKind; +import com.sorrowmist.useless.core.component.ExternalInventoryReference; +import com.sorrowmist.useless.core.component.MultiblockPartData; +import com.sorrowmist.useless.world.inventory.ExternalInventoryStore; +import com.sorrowmist.useless.init.ModBlockEntities; +import com.sorrowmist.useless.init.ModItems; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import com.sorrowmist.useless.content.menus.MePatternAssemblyMenu; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.math.BigInteger; +import java.util.List; +import java.util.Objects; + +/** AE node and pattern inventory for the multiblock furnace. */ +public final class MePatternAssemblyBlockEntity extends AEBaseBlockEntity + implements ICraftingProvider, SmartDoublingCraftingProvider, IInWorldGridNodeHost, + IGridNodeListener, IActionHost, PatternContainer, MenuProvider, + AlloyFurnaceBigIntegerProvider { + private static final Logger LOGGER = LogUtils.getLogger(); + private final RecoverableItemStackHandler patterns = new RecoverableItemStackHandler( + ConfigManager::getOmniversalPatternSlots, + MePatternAssemblyBlockEntity::isAcceptedPattern, + this::inventoryChanged); + private final PagedMenuPageMemory pageMemory = new PagedMenuPageMemory(this::setChanged); + private final IManagedGridNode mainNode; + @Nullable + private BlockPos controllerPos; + private long structureGeneration; + private long patternStorageRevision; + /** + * AE2 snapshots a provider's patterns when its node is mounted. The node + * can become ready before the multiblock controller is linked (or before + * the grid finishes booting), so keep a dirty bit and retry once the node + * is actually usable. + */ + private boolean providerRefreshPending = true; + /** + * Number of patterns in the last snapshot published to AE2. An empty snapshot still has to be + * published when it clears a previous one, but not when nothing was indexed at all. + */ + private int publishedPatternCount; + private boolean unloading; + @Nullable + private ExternalInventoryReference inventoryReference; + @Nullable + private CompoundTag pendingLegacyInventory; + private boolean externalInventoryLoaded; + + public MePatternAssemblyBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.ME_PATTERN_ASSEMBLY.get(), pos, state); + mainNode = GridHelper.createManagedNode(this, this) + .setInWorldNode(true) + .setTagName("node") + .setFlags(GridFlags.REQUIRE_CHANNEL) + .addService(ICraftingProvider.class, this); + } + + private void inventoryChanged() { + if (unloading || isRemoved()) return; + if (externalInventoryLoaded && level != null && !level.isClientSide) { + ExternalInventoryStore.save(level, inventoryReference, patterns, level.registryAccess()); + } + patternStorageRevision++; + setChanged(); + if (level == null || level.isClientSide) return; + MultiblockAlloyFurnaceCoreBlockEntity controller = getController(); + if (controller != null) { + controller.updatePatterns(); + } else { + requestProviderRefresh(); + } + } + + /** + * 样板总成接受万象样板与本模组的编码样板,以及 AE2 的合成样板。 + * + *

合成样板由样板总成自己在虚拟工作台上装配(万象合金炉没有合成台),处理样板仍由 + * 万象样板体系承担,因此只放行 AE2 的合成样板物品。能否真正发布由解码结果决定: + * 解码失败或不是合成样板的栈会在重建样板快照时被跳过。

+ */ + private static boolean isAcceptedPattern(ItemStack stack) { + return stack.is(ModItems.OMNIVERSAL_PATTERN.get()) + || stack.is(AEItems.CRAFTING_PATTERN.asItem()); + } + + public RecoverableItemStackHandler getPatterns() { + ensureExternalInventory(); + return patterns; + } + + public PagedMenuPageMemory getPageMemory() { + return pageMemory; + } + + public long getPatternStorageRevision() { + return patternStorageRevision; + } + + public MultiblockPartData createItemData(HolderLookup.Provider registries) { + return MultiblockPartData.inventory(patterns, registries); + } + + public void restoreItemData(MultiblockPartData data, HolderLookup.Provider registries) { + if (data == null) return; + data.restoreInventory(patterns, registries); + inventoryChanged(); + } + + public void bindExternalInventory(@Nullable ExternalInventoryReference requested, + @Nullable CompoundTag legacyInventory, + HolderLookup.Provider registries) { + if (externalInventoryLoaded || level == null || level.isClientSide) return; + inventoryReference = ExternalInventoryStore.bindAt( + level, ExternalInventoryKind.PATTERN_ASSEMBLY, requested, worldPosition, + patterns, legacyInventory, registries); + ExternalInventoryStore.setReference(this, inventoryReference); + pendingLegacyInventory = null; + externalInventoryLoaded = true; + inventoryChanged(); + } + + private void ensureExternalInventory() { + if (externalInventoryLoaded || level == null || level.isClientSide) return; + bindExternalInventory( + ExternalInventoryStore.getReference(this), + pendingLegacyInventory, + level.registryAccess()); + } + + @Nullable + public ExternalInventoryReference getExternalInventoryReference() { + ensureExternalInventory(); + return inventoryReference; + } + + public void releaseExternalInventory() { + if (level != null) { + ExternalInventoryStore.save(level, inventoryReference, patterns, level.registryAccess()); + ExternalInventoryStore.release(level, worldPosition, inventoryReference); + } + } + + public @NotNull IManagedGridNode getMainNode() { + return mainNode; + } + + public void linkController(@Nullable BlockPos controllerPos, long generation) { + if (unloading || isRemoved()) return; + if (!Objects.equals(this.controllerPos, controllerPos) || structureGeneration != generation) { + this.controllerPos = controllerPos == null ? null : controllerPos.immutable(); + this.structureGeneration = generation; + setChanged(); + if (level == null || level.isClientSide) return; + MultiblockAlloyFurnaceCoreBlockEntity controller = getController(); + if (controller != null) { + controller.updatePatterns(); + } else { + requestProviderRefresh(); + } + } + } + + /** True when this assembly's persisted link belongs to the given core. */ + public boolean isLinkedToController(BlockPos controllerPos) { + return Objects.equals(this.controllerPos, controllerPos); + } + + /** + * Pattern assemblies are exclusive multiblock parts. A stale or unformed + * controller is deliberately not considered an owner, allowing a newly + * valid structure to claim the part after the old structure breaks. + */ + public boolean isClaimedByOtherController(BlockPos controllerPos) { + MultiblockAlloyFurnaceCoreBlockEntity controller = getController(); + return controller != null && !controller.getBlockPos().equals(controllerPos); + } + + /** Marks the AE provider cache stale for the controller's next server tick. */ + public void requestProviderRefresh() { + if (unloading || isRemoved()) return; + providerRefreshPending = true; + } + + /** + * Called by the controller every server tick. Keeping this retry here is + * intentional: AE2 may report a grid before its crafting service has + * completed booting, and a refresh during that window is discarded. + */ + public void refreshProviderIfReady() { + if (unloading || isRemoved() || !providerRefreshPending + || level == null || level.isClientSide) return; + + IGridNode node = mainNode.getNode(); + if (node == null || !mainNode.isReady() || !mainNode.isActive() + || node.getGrid() == null || !mainNode.hasGridBooted()) { + return; + } + + List available = getAvailablePatterns(); + boolean controllerUnresolvable = getController() == null; + if (controllerUnresolvable && publishedPatternCount == 0) { + // The snapshot only looks empty because the link to the controller is not resolvable + // yet. Publishing it would index this provider as unable to craft anything, so stay + // dirty and publish on a later tick instead. + OmniversalPatternWarnings.notPublished("me_pattern_assembly@" + worldPosition, "", + "the controller link is not resolvable yet"); + return; + } + + providerRefreshPending = false; + try { + ICraftingProvider.requestUpdate(mainNode); + publishedPatternCount = available.size(); + for (IPatternDetails pattern : available) { + var output = pattern.getPrimaryOutput(); + if (!node.getGrid().getCraftingService().getCraftingFor(output.what()).contains(pattern)) { + RateLimitedLogger.warn(LOGGER, "AE grid did not index multiblock alloy furnace pattern at {} (output={})", + worldPosition, output); + } + } + } catch (RuntimeException exception) { + RateLimitedLogger.warn(LOGGER, "Failed to refresh multiblock pattern provider at {}", worldPosition, exception); + // Keep the bit set so the next server tick retries after a + // transient grid/pathing update. + providerRefreshPending = true; + } + if (controllerUnresolvable) { + // A previous snapshot had to be cleared because the controller link went away; keep + // retrying so the node is not left indexed as an unlinked provider. + OmniversalPatternWarnings.notPublished("me_pattern_assembly@" + worldPosition, "", + "cleared the published patterns because the controller link is gone"); + providerRefreshPending = true; + } + } + + @Nullable + public MultiblockAlloyFurnaceCoreBlockEntity getController() { + if (unloading || isRemoved() || level == null || controllerPos == null + || !level.isLoaded(controllerPos)) return null; + if (level.getBlockEntity(controllerPos) instanceof MultiblockAlloyFurnaceCoreBlockEntity core + && core.isPatternAssemblyLinked(worldPosition, structureGeneration)) { + return core; + } + return null; + } + + @Override + public List getAvailablePatterns() { + MultiblockAlloyFurnaceCoreBlockEntity controller = getController(); + return controller == null ? List.of() : controller.getAvailablePatterns(); + } + + @Override + public int getPatternPriority() { + MultiblockAlloyFurnaceCoreBlockEntity controller = getController(); + return controller == null ? 0 : controller.getPatternPriority(); + } + + @Override + public boolean pushPattern(IPatternDetails patternDetails, KeyCounter[] inputHolder) { + MultiblockAlloyFurnaceCoreBlockEntity controller = getController(); + return controller != null && controller.pushPattern(patternDetails, inputHolder); + } + + /** 数据能源原生 bigint 批次入口(单位原型 + 可能超过 long 的次数),转发给多方块核心。 */ + public boolean pushBigIntegerCraftingPattern(IPatternDetails patternDetails, BigInteger count, + KeyCounter[] unitPrototype) { + MultiblockAlloyFurnaceCoreBlockEntity controller = getController(); + return controller != null && controller.pushBigIntegerCraftingPattern(patternDetails, count, unitPrototype); + } + + /** + * 对外公开 API({@code api.crafting.bigint})的发现入口。 + * + *

本机是这张 AE 网格上真正挂节点的方块,所以第三方 CPU 遍历网格节点时找到的是它; + * 真正的 bigint 能力在多方块核心上,这里只是把请求转发过去。核心未成形或链接未解析时返回 + * {@code null},调用方据此跳过本机。

+ */ + @Override + public @Nullable AlloyFurnaceBigIntegerTarget bigIntegerTarget() { + MultiblockAlloyFurnaceCoreBlockEntity controller = getController(); + return controller == null ? null : controller.bigIntegerTarget(); + } + + @Override + public boolean isBusy() { + MultiblockAlloyFurnaceCoreBlockEntity controller = getController(); + return controller == null || controller.isBusy(); + } + + private final InternalInventory terminalInventory = new InternalInventory() { + @Override + public int size() { + return patterns.getActiveSlots(); + } + + @Override + public ItemStack getStackInSlot(int slotIndex) { + return slotIndex >= 0 && slotIndex < patterns.getActiveSlots() + ? patterns.getStackInSlot(slotIndex) + : ItemStack.EMPTY; + } + + @Override + public void setItemDirect(int slotIndex, ItemStack stack) { + if (slotIndex >= 0 && slotIndex < patterns.getActiveSlots() + && (stack.isEmpty() || patterns.isItemValid(slotIndex, stack))) { + patterns.setStackInSlot(slotIndex, stack); + } + } + + @Override + public boolean isItemValid(int slot, ItemStack stack) { + return patterns.isItemValid(slot, stack); + } + + @Override + public int getSlotLimit(int slot) { + return 1; + } + }; + + @Override + public @Nullable IGrid getGrid() { + return unloading ? null : mainNode.getGrid(); + } + + @Override + public boolean isVisibleInTerminal() { + return true; + } + + @Override + public InternalInventory getTerminalPatternInventory() { + return terminalInventory; + } + + @Override + public long getTerminalSortOrder() { + return (long) worldPosition.getZ() << 24 ^ (long) worldPosition.getX() << 8 ^ worldPosition.getY(); + } + + @Override + public PatternContainerGroup getTerminalGroup() { + return new PatternContainerGroup(AEItemKey.of(new ItemStack(getBlockState().getBlock())), + getDisplayName(), List.of()); + } + + @Override + public Component getDisplayName() { + Component customName = getCustomName(); + return customName != null ? customName : Component.translatable("block.useless_mod.me_pattern_assembly"); + } + + @Override + public void setName(String name) { + super.setName(name); + setChanged(); + if (level == null || level.isClientSide) return; + MultiblockAlloyFurnaceCoreBlockEntity controller = getController(); + if (controller != null) { + controller.updatePatterns(); + } else { + requestProviderRefresh(); + } + } + + @Override + public AbstractContainerMenu createMenu(int containerId, Inventory inventory, Player player) { + return new MePatternAssemblyMenu(containerId, inventory, worldPosition); + } + + @Override + public void onSaveChanges(MePatternAssemblyBlockEntity nodeOwner, IGridNode node) { + if (unloading || isRemoved()) return; + setChanged(); + } + + @Override + public void onGridChanged(MePatternAssemblyBlockEntity nodeOwner, IGridNode node) { + if (unloading || isRemoved() || level == null || level.isClientSide) return; + MultiblockAlloyFurnaceCoreBlockEntity controller = getController(); + if (controller != null) controller.onAeGridChanged(); + requestProviderRefresh(); + } + + @Override + public void onStateChanged(MePatternAssemblyBlockEntity nodeOwner, IGridNode node, + IGridNodeListener.State state) { + onGridChanged(nodeOwner, node); + } + + @Override + public @Nullable IGridNode getGridNode(Direction direction) { + return mainNode.getNode(); + } + + @Override + public AECableType getCableConnectionType(Direction direction) { + return AECableType.SMART; + } + + @Override + public IGridNode getActionableNode() { + return mainNode.getNode(); + } + + @Override + public void loadTag(CompoundTag tag) { + HolderLookup.Provider registries = level == null ? net.minecraft.core.RegistryAccess.EMPTY : level.registryAccess(); + super.loadTag(tag); + pendingLegacyInventory = tag.contains("Patterns") + ? tag.getCompound("Patterns").copy() : null; + controllerPos = tag.contains("Controller") ? BlockPos.of(tag.getLong("Controller")) : null; + structureGeneration = tag.getLong("StructureGeneration"); + pageMemory.load(tag); + mainNode.loadFromNBT(tag); + } + + @Override + public void saveAdditional(CompoundTag tag) { + HolderLookup.Provider registries = level == null ? net.minecraft.core.RegistryAccess.EMPTY : level.registryAccess(); + super.saveAdditional(tag); + if (controllerPos != null) tag.putLong("Controller", controllerPos.asLong()); + tag.putLong("StructureGeneration", structureGeneration); + pageMemory.save(tag); + mainNode.saveToNBT(tag); + } + + @Override + public void clearRemoved() { + super.clearRemoved(); + unloading = false; + providerRefreshPending = true; + GridHelper.onFirstTick(this, blockEntity -> { + if (blockEntity.unloading || blockEntity.isRemoved()) return; + blockEntity.mainNode.create(getLevel(), getBlockPos()); + blockEntity.requestProviderRefresh(); + blockEntity.inventoryChanged(); + }); + } + + @Override + public void onLoad() { + super.onLoad(); + ensureExternalInventory(); + } + + @Override + public void setRemoved() { + unloading = true; + providerRefreshPending = false; + super.setRemoved(); + mainNode.destroy(); + } + + @Override + public void onChunkUnloaded() { + unloading = true; + providerRefreshPending = false; + super.onChunkUnloaded(); + mainNode.destroy(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/blockentities/multiblock/MultiblockAlloyFurnaceCoreBlockEntity.java b/src/main/java/com/sorrowmist/useless/content/blockentities/multiblock/MultiblockAlloyFurnaceCoreBlockEntity.java new file mode 100644 index 00000000..09586c44 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/blockentities/multiblock/MultiblockAlloyFurnaceCoreBlockEntity.java @@ -0,0 +1,889 @@ +package com.sorrowmist.useless.content.blockentities.multiblock; + +import appeng.api.config.Actionable; +import appeng.api.config.PowerMultiplier; +import appeng.api.config.PowerUnits; +import appeng.api.crafting.IPatternDetails; +import appeng.api.networking.IGrid; +import appeng.api.networking.IManagedGridNode; +import appeng.api.networking.energy.IEnergyService; +import appeng.api.networking.security.IActionSource; +import appeng.api.networking.storage.IStorageService; +import appeng.api.stacks.AEFluidKey; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import appeng.api.stacks.KeyCounter; +import appeng.api.storage.MEStorage; +import appeng.blockentity.crafting.IMolecularAssemblerSupportedPattern; +import com.sorrowmist.useless.api.crafting.bigint.cpu.AlloyFurnaceBigIntegerCpuBinding; +import com.sorrowmist.useless.api.enums.RedstoneControlMode; +import com.sorrowmist.useless.compat.AppFluxCompat; +import com.sorrowmist.useless.content.blocks.multiblock.MultiblockAlloyFurnaceCoreBlock; +import com.sorrowmist.useless.content.blocks.multiblock.MultiblockFurnaceActivity; +import com.sorrowmist.useless.content.blocks.multiblock.OmniversalAlloyFurnaceStructure; +import com.sorrowmist.useless.content.blocks.multiblock.UselessCoilBlock; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.AdvancedAlloyFurnaceAeManager; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.AlloyFurnaceAeHost; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.CraftingAeOutputTarget; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.GridOutputTarget; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.CraftingTaskContext; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.OmniversalBigIntegerTarget; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.OmniversalPatternDetails; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.SmartDoublingPatterns; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.catalyst.ResolvedCatalystEffect; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.parallel.AlloyFurnaceParallelCalculator; +import com.sorrowmist.useless.content.multiblock.OmniversalCoilStats; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeCatalog; +import com.sorrowmist.useless.core.config.AlloyFurnaceTierRules; +import com.sorrowmist.useless.core.config.ConfigManager; +import com.sorrowmist.useless.core.component.MultiblockRecoveryData; +import com.sorrowmist.useless.energy.EnergyManager; +import com.sorrowmist.useless.energy.IEnergyManager; +import com.sorrowmist.useless.init.ModBlockEntities; +import com.sorrowmist.useless.network.AlloyFurnaceErrorHighlightPacket; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.server.level.ServerLevel; +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.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.ChunkPos; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import com.sorrowmist.useless.content.menus.MultiblockAlloyFurnaceMenu; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.capability.templates.FluidTank; +import net.minecraftforge.items.ItemStackHandler; +import org.jetbrains.annotations.Nullable; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; + +/** Controller, energy store and persistent task owner for the multiblock furnace. */ +public final class MultiblockAlloyFurnaceCoreBlockEntity extends BlockEntity implements AlloyFurnaceAeHost, MenuProvider { + private static final int SAFETY_REVALIDATION_TICKS = 40; + public static final int MENU_DATA_COUNT = 11; + private static final ItemStackHandler EMPTY_ITEMS = new ItemStackHandler(0); + private static final FluidTank[] EMPTY_TANKS = new FluidTank[0]; + + private final IEnergyManager energy = EnergyManager.builder() + .capacity(1L).maxReceive(1L).maxExtract(0L).onChange(this::setChanged).build(); + private final AdvancedAlloyFurnaceAeManager aeManager = new AdvancedAlloyFurnaceAeManager(this); + private boolean structureDirty = true; + private boolean formed; + private int coilTier; + @Nullable + private BlockPos patternAssemblyPos; + @Nullable + private BlockPos moldHubPos; + @Nullable + private BlockPos passiveHatchPos; + private int validationTimer; + private long structureGeneration; + private boolean deferredTasksLoaded; + private long recipeCatalogGeneration = -1L; + private boolean unloading; + /** 对外公开 API 的目标视图缓存:视图很轻(核心引用 + 一个路由字符串),一个实例即可。 */ + @Nullable + private OmniversalBigIntegerTarget bigIntegerTarget; + private boolean coilActivitySynchronized; + private RedstoneControlMode redstoneControlMode = RedstoneControlMode.DISABLED; + private long automaticEnergyLimit = Long.MAX_VALUE; + private final ContainerData menuData = new ContainerData() { + @Override + public int get(int index) { + long stored = energy.getEnergyStoredLong(); + long capacity = energy.getMaxEnergyStoredLong(); + long energyLimit = getAutomaticEnergyLimit(); + return switch (index) { + case 0 -> formed ? 1 : 0; + case 1 -> coilTier; + case 2 -> (int) stored; + case 3 -> (int) (stored >>> 32); + case 4 -> (int) capacity; + case 5 -> (int) (capacity >>> 32); + case 6 -> aeManager.getActiveAETaskCount(); + case 7 -> redstoneControlMode.ordinal(); + case 8 -> getMaxAETaskCount(); + case 9 -> (int) energyLimit; + case 10 -> (int) (energyLimit >>> 32); + default -> 0; + }; + } + + @Override public void set(int index, int value) {} + @Override public int getCount() { return MENU_DATA_COUNT; } + }; + + public MultiblockAlloyFurnaceCoreBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.MULTIBLOCK_ALLOY_FURNACE_CORE.get(), pos, state); + } + + public void serverTick() { + if (unloading || isRemoved() || level == null || level.isClientSide) return; + if (structureDirty || ++validationTimer >= SAFETY_REVALIDATION_TICKS) { + validationTimer = 0; + validateStructure(); + } + if (!deferredTasksLoaded) { + // 目录未就绪时保持未加载状态,下一 tick 重试;否则完好样板会被误判失效并退还材料。 + deferredTasksLoaded = aeManager.loadDeferredTasks(); + } + long currentCatalogGeneration = AlloyFurnaceRecipeCatalog.generation(); + if (recipeCatalogGeneration != currentCatalogGeneration) { + recipeCatalogGeneration = currentCatalogGeneration; + updatePatterns(); + } + // The assembly node may have mounted before the controller was linked + // or while the AE grid was still booting. Retry the provider refresh + // after every validation/load transition without rebuilding patterns. + aeManager.tickPatternRefresh(); + MePatternAssemblyBlockEntity assembly = getAssembly(); + if (assembly != null) assembly.refreshProviderIfReady(); + boolean progressed = false; + if (isTaskExecutionEnabled()) { + drawEnergyFromAeNetwork(); + aeManager.flushAEBatches(); + progressed = aeManager.tickAETasks(); + aeManager.tickUnreturnedInputs(); + } + // 合成样板的产物必须在 AE2 记录完本批预期产物之后才能回网,所以不受上面的开关影响: + // 即使任务执行被关闭,已经装配好的产物也必须继续投递。 + aeManager.tickQueuedCraftingOutputs(); + PassiveCraftingHatchBlockEntity passiveHatch = getPassiveHatch(); + if (passiveHatch != null) { + progressed |= passiveHatch.serverTickFromController(this); + } + boolean hasWork = aeManager.hasWork() + || passiveHatch != null && passiveHatch.hasWork(); + updateDisplayedActivity(progressed, hasWork); + } + + private void updateDisplayedActivity(boolean progressed, boolean hasWork) { + MultiblockFurnaceActivity activity = + MultiblockFurnaceActivity.resolve(formed, progressed, hasWork); + BlockState state = getBlockState(); + boolean activityChanged = + state.getValue(MultiblockAlloyFurnaceCoreBlock.ACTIVITY) != activity; + if (activityChanged) { + level.setBlock(worldPosition, + state.setValue(MultiblockAlloyFurnaceCoreBlock.ACTIVITY, activity), + Block.UPDATE_CLIENTS); + } + if (activityChanged || !coilActivitySynchronized) { + setCoilsActive(activity == MultiblockFurnaceActivity.RUN); + coilActivitySynchronized = true; + } + } + + public void requestStructureValidation() { + if (unloading || isRemoved()) return; + structureDirty = true; + } + + public void validateStructure() { + if (unloading || isRemoved() || level == null) return; + structureDirty = false; + coilActivitySynchronized = false; + Direction facing = getBlockState().getValue(MultiblockAlloyFurnaceCoreBlock.FACING); + OmniversalAlloyFurnaceStructure.ValidationResult result = + OmniversalAlloyFurnaceStructure.validate(level, worldPosition, facing); + int validatedCoilTier = result.valid() ? result.coilTier() : 0; + BlockPos validatedAssemblyPos = result.valid() ? result.patternAssemblyPos() : null; + BlockPos validatedMoldHubPos = result.valid() ? result.moldHubPos() : null; + BlockPos validatedHatchPos = result.valid() ? result.passiveHatchPos() : null; + boolean stateChanged = formed != result.valid() + || coilTier != validatedCoilTier + || !java.util.Objects.equals(patternAssemblyPos, validatedAssemblyPos) + || !java.util.Objects.equals(moldHubPos, validatedMoldHubPos) + || !java.util.Objects.equals(passiveHatchPos, validatedHatchPos); + formed = result.valid(); + coilTier = validatedCoilTier; + patternAssemblyPos = validatedAssemblyPos == null ? null : validatedAssemblyPos.immutable(); + moldHubPos = validatedMoldHubPos == null ? null : validatedMoldHubPos.immutable(); + passiveHatchPos = validatedHatchPos == null ? null : validatedHatchPos.immutable(); + if (stateChanged) { + structureGeneration++; + if (formed) { + OmniversalCoilStats stats = OmniversalCoilStats.forTier(coilTier); + energy.setMaxEnergyStored(stats.energyCapacity()); + energy.setMaxReceive(stats.maxReceive()); + } + BlockState state = getBlockState(); + if (state.getValue(MultiblockAlloyFurnaceCoreBlock.FORMED) != formed) { + level.setBlock(worldPosition, state.setValue(MultiblockAlloyFurnaceCoreBlock.FORMED, formed), 3); + } + setChanged(); + } + linkFunctionalParts(); + if (stateChanged) updatePatterns(); + broadcastStructureError(result); + } + + /** + * 把「当前该修的那一个方块」下发给追踪本区块的客户端:从下往上取世界坐标 Y 最小的 mismatch, + * 同 Y 时保留 ENTRIES 遍历中先出现的(遍历顺序本身即由低到高)。 + * + *

成型时下发 {@code null} 清掉残留高亮;验证本身只在结构变化或每 40 tick 触发,天然节流。

+ */ + private void broadcastStructureError(OmniversalAlloyFurnaceStructure.ValidationResult result) { + if (!(level instanceof ServerLevel serverLevel)) return; + BlockPos lowestError = null; + if (!formed) { + for (OmniversalAlloyFurnaceStructure.Mismatch mismatch : result.mismatches()) { + BlockPos pos = mismatch.worldPos(); + if (lowestError == null || pos.getY() < lowestError.getY()) { + lowestError = pos; + } + } + } + PacketDistributor.sendToPlayersTrackingChunk(serverLevel, new ChunkPos(worldPosition), + new AlloyFurnaceErrorHighlightPacket(worldPosition, lowestError)); + } + + private void linkFunctionalParts() { + if (unloading || isRemoved() || level == null) return; + Direction facing = getBlockState().getValue(MultiblockAlloyFurnaceCoreBlock.FACING); + for (OmniversalAlloyFurnaceStructure.Entry entry : OmniversalAlloyFurnaceStructure.entries()) { + if (entry.part() != OmniversalAlloyFurnaceStructure.Part.CASING) continue; + BlockPos pos = entry.worldPos(worldPosition, facing); + if (!level.isLoaded(pos)) continue; + BlockEntity part = level.getBlockEntity(pos); + if (part instanceof MePatternAssemblyBlockEntity assembly) { + if (formed && pos.equals(patternAssemblyPos)) { + assembly.linkController(worldPosition, structureGeneration); + } else if (assembly.isLinkedToController(worldPosition)) { + assembly.linkController(null, structureGeneration); + } + } else if (part instanceof OmniversalMoldHubBlockEntity hub) { + hub.linkController(formed && pos.equals(moldHubPos) + ? worldPosition : null, structureGeneration); + } else if (part instanceof PassiveCraftingHatchBlockEntity hatch) { + if (formed && pos.equals(passiveHatchPos)) { + hatch.linkController(worldPosition, structureGeneration); + } else if (hatch.isLinkedToController(worldPosition)) { + // Keep our own raw association while invalid so removal can still + // recover buffered materials, without stealing another core's hatch. + hatch.linkController(worldPosition, structureGeneration); + } + } + } + } + + @Nullable + private PassiveCraftingHatchBlockEntity getPassiveHatch() { + if (!formed || passiveHatchPos == null || level == null || !level.isLoaded(passiveHatchPos)) { + return null; + } + return level.getBlockEntity(passiveHatchPos) instanceof PassiveCraftingHatchBlockEntity hatch + ? hatch : null; + } + + @Nullable + private MePatternAssemblyBlockEntity getAssembly() { + if (unloading || isRemoved() || level == null || patternAssemblyPos == null + || !level.isLoaded(patternAssemblyPos)) return null; + return level.getBlockEntity(patternAssemblyPos) instanceof MePatternAssemblyBlockEntity assembly + ? assembly : null; + } + + @Nullable + private OmniversalMoldHubBlockEntity getMoldHub() { + if (unloading || isRemoved() || level == null || moldHubPos == null || !level.isLoaded(moldHubPos)) { + return null; + } + return level.getBlockEntity(moldHubPos) instanceof OmniversalMoldHubBlockEntity hub ? hub : null; + } + + @Nullable + public OmniversalMoldHubBlockEntity getLinkedMoldHub() { + if (!formed) return null; + OmniversalMoldHubBlockEntity hub = getMoldHub(); + return hub != null && hub.getController() == this ? hub : null; + } + + public void onAeGridChanged() { + if (unloading || isRemoved()) return; + setChanged(); + } + + public void updatePatterns() { + if (unloading || isRemoved() || level == null || level.isClientSide) return; + aeManager.updatePatterns(); + MePatternAssemblyBlockEntity assembly = getAssembly(); + if (assembly != null) assembly.requestProviderRefresh(); + } + + public List getAvailablePatterns() { + return aeManager.getAvailablePatterns(); + } + + @Override + public void onPatternsRebuilt() { + MePatternAssemblyBlockEntity assembly = getAssembly(); + if (assembly != null) { + assembly.requestProviderRefresh(); + } + } + + public int getPatternPriority() { + return aeManager.getPatternPriority(); + } + + public boolean pushPattern(IPatternDetails pattern, KeyCounter[] inputs) { + return aeManager.pushPattern(pattern, inputs); + } + + /** 数据能源原生 bigint 批次入口(单位原型 + 可能超过 long 的次数)。 */ + public boolean pushBigIntegerCraftingPattern(IPatternDetails pattern, BigInteger count, + KeyCounter[] unitPrototype) { + return aeManager.pushBigIntegerCraftingPattern(pattern, count, unitPrototype); + } + + public boolean isBusy() { + return aeManager.isBusy(); + } + + public void cancelAllAETasks() { + aeManager.cancelAllTasks(); + } + + /** Cancels every task owned by this structure before recovery data is captured. */ + public void cancelAllTasksForRemoval() { + aeManager.cancelAllTasks(); + if (level == null) return; + setCoilsActive(false); + coilActivitySynchronized = true; + Direction facing = getBlockState().getValue(MultiblockAlloyFurnaceCoreBlock.FACING); + for (OmniversalAlloyFurnaceStructure.Entry entry : OmniversalAlloyFurnaceStructure.entries()) { + if (entry.part() != OmniversalAlloyFurnaceStructure.Part.CASING) continue; + BlockPos pos = entry.worldPos(worldPosition, facing); + if (!level.isLoaded(pos)) continue; + if (level.getBlockEntity(pos) instanceof PassiveCraftingHatchBlockEntity hatch) { + hatch.prepareForControllerRemoval(this); + } + } + } + + private void setCoilsActive(boolean active) { + if (level == null || level.isClientSide) return; + Direction facing = getBlockState().getValue(MultiblockAlloyFurnaceCoreBlock.FACING); + for (OmniversalAlloyFurnaceStructure.Entry entry + : OmniversalAlloyFurnaceStructure.entries()) { + if (entry.part() != OmniversalAlloyFurnaceStructure.Part.COIL) continue; + BlockPos pos = entry.worldPos(worldPosition, facing); + if (!level.isLoaded(pos)) continue; + BlockState coilState = level.getBlockState(pos); + if (!(coilState.getBlock() instanceof UselessCoilBlock) + || coilState.getValue(UselessCoilBlock.ACTIVE) == active) { + continue; + } + level.setBlock(pos, coilState.setValue(UselessCoilBlock.ACTIVE, active), + Block.UPDATE_CLIENTS); + } + } + + public MultiblockRecoveryData createRecoveryData() { + return new MultiblockRecoveryData( + MultiblockRecoveryData.CURRENT_VERSION, + energy.getEnergyStoredLong(), + aeManager.getUnreturnedInputsSnapshot(), + automaticEnergyLimit); + } + + public void restoreRecoveryData(MultiblockRecoveryData data) { + if (data == null) return; + energy.setMaxEnergyStored(Math.max(energy.getMaxEnergyStoredLong(), data.energy())); + energy.setEnergyStored(data.energy()); + aeManager.addUnreturnedInputs(data.contents()); + automaticEnergyLimit = data.automaticEnergyLimit(); + setChanged(); + } + + public Collection getAETaskProgressList() { + return aeManager.getAETaskProgressList(); + } + + public void updateClientTaskProgress(List tasks) { + aeManager.updateClientTaskProgress(tasks); + } + + public boolean isFormed() { + return formed; + } + + public int getCoilTier() { + return coilTier; + } + + @Override + public int getMachineTier() { + return coilTier; + } + + public long getStructureGeneration() { + return structureGeneration; + } + + public long getPassiveCraftingMaxParallel() { + return formed ? OmniversalCoilStats.forTier(coilTier).singleTaskParallel() : 1; + } + + @Override + public long getCraftingPatternCapacity() { + return Math.max(1L, getPassiveCraftingMaxParallel()); + } + + public boolean isPatternAssemblyLinked(BlockPos pos, long generation) { + return formed && patternAssemblyPos != null && patternAssemblyPos.equals(pos) + && structureGeneration == generation; + } + + public boolean isMoldHubLinked(BlockPos pos, long generation) { + return formed && moldHubPos != null && moldHubPos.equals(pos) + && structureGeneration == generation; + } + + public boolean isPassiveHatchLinked(BlockPos pos, long generation) { + return formed && passiveHatchPos != null && passiveHatchPos.equals(pos) + && structureGeneration == generation; + } + + public RedstoneControlMode getRedstoneControlMode() { + return redstoneControlMode; + } + + public ContainerData getMenuData() { + return menuData; + } + + @Override + public Component getDisplayName() { + return Component.translatable("block.useless_mod.multiblock_alloy_furnace_core"); + } + + @Override + public AbstractContainerMenu createMenu(int containerId, Inventory inventory, Player player) { + return new MultiblockAlloyFurnaceMenu(containerId, inventory, worldPosition); + } + + public void sendAETaskProgressToPlayer(ServerPlayer player) { + aeManager.sendAETaskProgressToPlayer(player); + } + + public void cycleRedstoneControlMode() { + redstoneControlMode = redstoneControlMode.next(); + setChanged(); + } + + @Override + public @Nullable IManagedGridNode getMainNode() { + MePatternAssemblyBlockEntity assembly = getAssembly(); + return assembly == null ? null : assembly.getMainNode(); + } + + @Override + public @Nullable IGrid getAeGrid() { + MePatternAssemblyBlockEntity assembly = getAssembly(); + return assembly == null ? null : assembly.getMainNode().getGrid(); + } + + @Override + public int getMaxAETaskCount() { + return formed ? OmniversalCoilStats.forTier(coilTier).threads() : 0; + } + + public int getRemainingAETaskCount(boolean craftingPattern) { + return aeManager.getRemainingAETaskCount(craftingPattern); + } + + /** + * 对外公开 API({@code api.crafting.bigint})的提交入口。 + * + *

与数据能源适配器走同一条折叠路径,区别只是多带一个 CPU 回执绑定;{@code cpuBinding} 为 + * {@code null} 时产物只切段写回 ME 网络。万象样板与 AE2 合成样板都由这一个入口处理 + * (合成样板不收能量)。

+ */ + public boolean pushBigIntegerBatch(IPatternDetails pattern, BigInteger count, + KeyCounter[] unitPrototype, + @Nullable AlloyFurnaceBigIntegerCpuBinding cpuBinding) { + return aeManager.pushBigIntegerBatch(pattern, count, unitPrototype, cpuBinding); + } + + /** + * 对外公开 API 的目标视图:第三方 CPU 通过 + * {@code AlloyFurnaceBigIntegerApi.findTargets(grid)} 拿到的就是这个对象。 + * + *

视图本身很轻(只持有本核心与一个路由字符串),实例缓存在核心上即可;机器未成形或正在卸载时 + * 返回 {@code null},这样发现流程不会拿到一台已经没了的机器。

+ */ + @Nullable + public OmniversalBigIntegerTarget bigIntegerTarget() { + if (!formed || unloading) { + return null; + } + if (bigIntegerTarget == null) { + bigIntegerTarget = new OmniversalBigIntegerTarget( + this, "useless_mod:multiblock_alloy_furnace@" + worldPosition.asLong()); + } + return bigIntegerTarget; + } + + @Override + public Iterable getPatternStacks() { + MePatternAssemblyBlockEntity assembly = getAssembly(); + if (assembly == null || !formed) return List.of(); + List result = new ArrayList<>(assembly.getPatterns().getActiveSlots()); + for (int slot = 0; slot < assembly.getPatterns().getActiveSlots(); slot++) { + result.add(assembly.getPatterns().getStackInSlot(slot)); + } + return result; + } + + @Override + public boolean canPublishPatterns() { + return formed; + } + + @Override + public boolean acceptsPattern(IPatternDetails pattern) { + IPatternDetails original = SmartDoublingPatterns.unwrap(pattern); + // 万象样板交给合金炉加工;AE2 合成样板由样板总成在虚拟工作台上自执行。 + return original instanceof OmniversalPatternDetails + || original instanceof IMolecularAssemblerSupportedPattern; + } + + @Override + public AdvancedAlloyFurnaceRecipe resolveTaskRecipe( + IPatternDetails pattern, List items, List fluids, + List keys, long operations) { + IPatternDetails original = SmartDoublingPatterns.unwrap(pattern); + return original instanceof OmniversalPatternDetails omniversal ? omniversal.recipe() : null; + } + + @Override + public boolean isTaskRecipeAvailable(AdvancedAlloyFurnaceRecipe recipe) { + return getTaskAvailability(recipe).available(); + } + + @Override + public CraftingTaskContext.TaskAvailability getTaskAvailability(AdvancedAlloyFurnaceRecipe recipe) { + if (!formed) { + return CraftingTaskContext.TaskAvailability.unavailable( + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_structure", ""); + } + if (recipe == null) { + return CraftingTaskContext.TaskAvailability.unavailable( + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_recipe", ""); + } + int requiredTier = AlloyFurnaceTierRules.requiredTier(recipe.id(), recipe.tier()); + if (coilTier < requiredTier) { + return CraftingTaskContext.TaskAvailability.unavailable( + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_tier", ""); + } + if (recipe.molds().isEmpty()) { + return CraftingTaskContext.TaskAvailability.ready(); + } + OmniversalMoldHubBlockEntity hub = getMoldHub(); + if (hub == null) { + return CraftingTaskContext.TaskAvailability.unavailable( + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_mold_hub", ""); + } + return hub.containsMolds(recipe.molds()) + ? CraftingTaskContext.TaskAvailability.ready() + : CraftingTaskContext.TaskAvailability.unavailable( + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_missing_mold", + CraftingTaskContext.describeRequiredMolds(recipe.molds())); + } + + @Override + public ResolvedCatalystEffect resolveTaskEffect(AdvancedAlloyFurnaceRecipe recipe) { + return OmniversalCoilStats.forTier(Math.max(1, coilTier)).resolveEffect(recipe); + } + + @Override + public long getTaskParallel(AdvancedAlloyFurnaceRecipe recipe, ResolvedCatalystEffect effect) { + return AlloyFurnaceParallelCalculator.calculateAeTaskParallel(recipe, effect); + } + + @Override + public boolean supportsLongAeAmounts() { + return true; + } + + /** + * 多方块支持把万象样板折叠成 BigInteger 批次(一次解析配方、产物 ×count、按 count 收能量)。 + * + *

单方块高级合金炉不覆写该方法 ⇒ 保持 {@code false},其万象样板继续走长版 counted 路径。

+ */ + @Override + public boolean supportsBigIntegerRecipeBatches() { + return true; + } + + /** + * 本 tick 允许单批产生的产物分段数(AIMD 控制器,转发给 AE 管理器)。 + * + *

用于解决超大量合成过程中交付不连续的问题:单批规模由实测反馈探测得出, + * 收敛至「一批约在一到两个 tick 内交付完成」,而非使用固定值估算。

+ */ + @Override + public long outputSegmentBudget() { + return aeManager.outputSegmentBudget(); + } + + @Override + public boolean isTaskExecutionEnabled() { + return formed && level != null + && redstoneControlMode.shouldRun(level.hasNeighborSignal(worldPosition)); + } + + @Override + public int getInputSlotsStart() { return 0; } + @Override + public int getInputSlotsCount() { return 0; } + @Override + public int getOutputSlotsStart() { return 0; } + @Override + public int getOutputSlotsCount() { return 0; } + @Override + public int getCatalystSlot() { return 0; } + @Override + public int getMoldSlot() { return 0; } + @Override + public int getFluidTankCount() { return 0; } + @Override + public Level getLevel() { return level; } + @Override + public ItemStackHandler getItemHandler() { return EMPTY_ITEMS; } + @Override + public IEnergyManager getEnergyManager() { return energy; } + public IEnergyManager getEnergyStorage() { return energy; } + public long getAutomaticEnergyLimit() { + return clampAutomaticEnergyLimit(automaticEnergyLimit, energy.getMaxEnergyStoredLong()); + } + + public void setAutomaticEnergyLimit(long limit) { + if (limit < 0L) return; + long clampedLimit = clampAutomaticEnergyLimit(limit, energy.getMaxEnergyStoredLong()); + if (automaticEnergyLimit == clampedLimit) return; + automaticEnergyLimit = clampedLimit; + setChanged(); + } + @Override + public void markChanged() { setChanged(); } + @Override + public void sendAETaskProgressToClients() { aeManager.sendAETaskProgressToClients(); } + @Override + public int getCatalystMaxParallel() { return getMaxAETaskCount(); } + @Override + public boolean isReturnOutputToAe() { return true; } + @Override + public void stashUnreturnedInput(AEKey key, long amount) { aeManager.stashUnreturnedInput(key, amount); } + @Override + public ConcurrentHashMap getAETaskProgressMap() { + return aeManager.getAETaskProgressMap(); + } + @Override + public AtomicInteger getTotalAEMaxProgressAtomic() { return aeManager.getTotalAEMaxProgressAtomic(); } + @Override + public AtomicInteger getTotalAEProgressAtomic() { return aeManager.getTotalAEProgressAtomic(); } + @Override + public ReentrantLock getCraftingLock() { return aeManager.getCraftingLock(); } + @Override + public FluidTank[] getInputFluidTanks() { return EMPTY_TANKS; } + @Override + public FluidTank[] getOutputFluidTanks() { return EMPTY_TANKS; } + + @Override + public void handleUnreturnedItem(ItemStack stack) { + AEItemKey key = AEItemKey.of(stack); + if (key != null) stashUnreturnedInput(key, stack.getCount()); + } + + @Override + public void handleUnreturnedFluid(FluidStack stack) { + AEFluidKey key = AEFluidKey.of(stack); + if (key != null) stashUnreturnedInput(key, stack.getAmount()); + } + + @Override + public long tryOutputToAE(ItemStack stack) { + AEItemKey key = AEItemKey.of(stack); + return key == null ? 0L : tryOutputKeyToAE(key, stack.getCount()); + } + + @Override + public long tryOutputFluidToAE(FluidStack stack) { + AEFluidKey key = AEFluidKey.of(stack); + return key == null ? 0L : tryOutputKeyToAE(key, stack.getAmount()); + } + + @Override + public long tryOutputKeyToAE(AEKey key, long amount) { + CraftingAeOutputTarget target = resolveAeOutputTarget(); + return target == null ? 0L : target.insert(key, amount); + } + + /** + * 解析一次 ME 网络写入目标,供一次「产物回网」刷写 pass 内复用。 + * + *

此前每写一个分段都重新解析一次({@link #getAeNetworkAccess()} 中 2 次方块实体查询 + + * 2 次分配,再加 {@link #getAeGrid()} 又一次装配体查询)。可持续吞吐直接由「每 tick 可插入次数」 + * 决定,一 tick 可能插入数千次 ⇒ 该解析成本直接影响吞吐,必须在 pass 外仅执行一次。

+ */ + @Override + public @Nullable CraftingAeOutputTarget resolveAeOutputTarget() { + AeNetworkAccess access = getAeNetworkAccess(); + if (access == null) { + return null; + } + MEStorage storage = access.storage(); + IActionSource source = access.source(); + // 网格也一并解析一次(getAeGrid() 内部又是一次装配体查询)。 + // 返回的 GridOutputTarget 支持「认领提升」:刷写 pass 会把 neoecoae 的逐段认领 + // 提到逐键一次(见 CraftingAeOutputTarget#supportsClaimHoisting)。 + return new GridOutputTarget(getAeGrid(), storage, source); + } + + @Nullable + AeNetworkAccess getAeNetworkAccess() { + if (!formed) return null; + MePatternAssemblyBlockEntity assembly = getAssembly(); + if (assembly == null || !assembly.getMainNode().isActive()) return null; + IGrid grid = assembly.getMainNode().getGrid(); + if (grid == null) return null; + var storageService = grid.getStorageService(); + return new AeNetworkAccess(storageService, IActionSource.ofMachine(assembly)); + } + + record AeNetworkAccess(IStorageService storageService, IActionSource source) { + MEStorage storage() { + return storageService.getInventory(); + } + + KeyCounter cachedInventory() { + return storageService.getCachedInventory(); + } + } + + private void drawEnergyFromAeNetwork() { + boolean drawAppflux = AppFluxCompat.isLoaded() && ConfigManager.isFurnaceDrawAppfluxEnergyEnabled(); + boolean drawAe = ConfigManager.isFurnaceDrawAeEnergyEnabled(); + if (!drawAppflux && !drawAe) return; + MePatternAssemblyBlockEntity assembly = getAssembly(); + if (assembly == null || !assembly.getMainNode().isActive()) return; + long wanted = calculateAutomaticEnergyRequest( + energy.getEnergyStoredLong(), energy.getMaxEnergyStoredLong(), + energy.getMaxReceiveLong(), automaticEnergyLimit); + IGrid grid = assembly.getMainNode().getGrid(); + if (wanted <= 0 || grid == null) return; + IActionSource source = IActionSource.ofMachine(assembly); + if (drawAppflux) { + long received = AppFluxCompat.extractFe(grid, wanted, source); + if (received > 0) { + energy.modifyEnergy(received); + wanted -= received; + } + } + if (drawAe && wanted > 0) { + IEnergyService energyService = grid.getEnergyService(); + double extracted = energyService.extractAEPower( + PowerUnits.FE.convertTo(PowerUnits.AE, wanted), Actionable.MODULATE, PowerMultiplier.ONE); + long received = Math.min(wanted, + (long) Math.floor(PowerUnits.AE.convertTo(PowerUnits.FE, extracted))); + if (received > 0) energy.modifyEnergy(received); + } + } + + static long calculateAutomaticEnergyRequest( + long stored, long capacity, long maxReceive, long configuredLimit) { + long effectiveLimit = clampAutomaticEnergyLimit(configuredLimit, capacity); + if (stored >= effectiveLimit) return 0L; + return Math.min(Math.max(0L, maxReceive), effectiveLimit - Math.max(0L, stored)); + } + + static long clampAutomaticEnergyLimit(long requested, long capacity) { + return Math.min(Math.max(0L, requested), Math.max(0L, capacity)); + } + + @Override + public void load(CompoundTag tag) { + HolderLookup.Provider registries = level == null ? net.minecraft.core.RegistryAccess.EMPTY : level.registryAccess(); + super.load(tag); + energy.deserializeNBT(tag.getCompound("Energy")); + formed = tag.getBoolean("Formed"); + coilTier = tag.getInt("CoilTier"); + structureGeneration = tag.getLong("StructureGeneration"); + patternAssemblyPos = tag.contains("PatternAssembly") + ? BlockPos.of(tag.getLong("PatternAssembly")) : null; + moldHubPos = tag.contains("MoldHub") ? BlockPos.of(tag.getLong("MoldHub")) : null; + passiveHatchPos = tag.contains("PassiveHatch") + ? BlockPos.of(tag.getLong("PassiveHatch")) : null; + redstoneControlMode = RedstoneControlMode.byIndex(tag.getInt("RedstoneControlMode")); + automaticEnergyLimit = tag.contains("AutomaticEnergyLimit", Tag.TAG_ANY_NUMERIC) + ? Math.max(0L, tag.getLong("AutomaticEnergyLimit")) + : Long.MAX_VALUE; + aeManager.setPatternPriority(tag.getInt("PatternPriority")); + aeManager.readTasksTag(tag); + structureDirty = true; + deferredTasksLoaded = false; + } + + @Override + public void clearRemoved() { + super.clearRemoved(); + unloading = false; + } + + @Override + public void onChunkUnloaded() { + unloading = true; + super.onChunkUnloaded(); + } + + @Override + protected void saveAdditional(CompoundTag tag) { + HolderLookup.Provider registries = level == null ? net.minecraft.core.RegistryAccess.EMPTY : level.registryAccess(); + super.saveAdditional(tag); + tag.putInt("DataVersion", 1); + tag.put("Energy", energy.serializeNBT()); + tag.putBoolean("Formed", formed); + tag.putInt("CoilTier", coilTier); + tag.putLong("StructureGeneration", structureGeneration); + if (patternAssemblyPos != null) tag.putLong("PatternAssembly", patternAssemblyPos.asLong()); + if (moldHubPos != null) tag.putLong("MoldHub", moldHubPos.asLong()); + if (passiveHatchPos != null) tag.putLong("PassiveHatch", passiveHatchPos.asLong()); + tag.putInt("RedstoneControlMode", redstoneControlMode.ordinal()); + tag.putLong("AutomaticEnergyLimit", automaticEnergyLimit); + tag.putInt("PatternPriority", aeManager.getPatternPriority()); + CompoundTag tasks = new CompoundTag(); + aeManager.saveTasks(tasks, registries); + tag.put("AeTasks", tasks); + } + + @Override + public void setRemoved() { + unloading = true; + super.setRemoved(); + aeManager.shutdown(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/blockentities/multiblock/OmniversalMoldHubBlockEntity.java b/src/main/java/com/sorrowmist/useless/content/blockentities/multiblock/OmniversalMoldHubBlockEntity.java new file mode 100644 index 00000000..2062a496 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/blockentities/multiblock/OmniversalMoldHubBlockEntity.java @@ -0,0 +1,282 @@ +package com.sorrowmist.useless.content.blockentities.multiblock; + +import com.sorrowmist.useless.content.blockentities.PagedMenuPageMemory; +import com.sorrowmist.useless.content.blockentities.RecoverableItemStackHandler; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeCatalog; +import com.sorrowmist.useless.content.recipe.MoldMatcher; +import com.sorrowmist.useless.core.config.ConfigManager; +import com.sorrowmist.useless.core.component.ExternalInventoryKind; +import com.sorrowmist.useless.core.component.ExternalInventoryReference; +import com.sorrowmist.useless.init.ModBlockEntities; +import com.sorrowmist.useless.init.ModTags; +import net.minecraft.core.BlockPos; +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.network.chat.Component; +import com.sorrowmist.useless.content.menus.OmniversalMoldHubMenu; +import com.sorrowmist.useless.core.component.MultiblockPartData; +import com.sorrowmist.useless.world.inventory.ExternalInventoryStore; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public final class OmniversalMoldHubBlockEntity extends BlockEntity implements MenuProvider { + private final RecoverableItemStackHandler molds = new RecoverableItemStackHandler( + ConfigManager::getOmniversalMoldSlots, + this::isValidMold, + this::moldInventoryChanged); + private final PagedMenuPageMemory pageMemory = new PagedMenuPageMemory(this::setChanged); + private final Map, Boolean> moldMatchCache = new HashMap<>(); + @Nullable + private Map cachedAvailableMolds; + @Nullable + private MoldMatcher.PreparedMolds cachedPreparedMolds; + private int cachedActiveSlots = -1; + private long cachedRecipeCatalogGeneration = -1L; + @Nullable + private BlockPos controllerPos; + private long structureGeneration; + private boolean unloading; + @Nullable + private ExternalInventoryReference inventoryReference; + @Nullable + private CompoundTag pendingLegacyInventory; + private boolean externalInventoryLoaded; + + public OmniversalMoldHubBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.OMNIVERSAL_MOLD_HUB.get(), pos, state); + } + + public RecoverableItemStackHandler getMolds() { + ensureExternalInventory(); + return molds; + } + + public PagedMenuPageMemory getPageMemory() { + return pageMemory; + } + + public MultiblockPartData createItemData(HolderLookup.Provider registries) { + return MultiblockPartData.inventory(molds, registries); + } + + public void restoreItemData(MultiblockPartData data, HolderLookup.Provider registries) { + if (data == null) return; + data.restoreInventory(molds, registries); + moldInventoryChanged(); + } + + public void bindExternalInventory(@Nullable ExternalInventoryReference requested, + @Nullable CompoundTag legacyInventory, + HolderLookup.Provider registries) { + if (level == null || level.isClientSide) return; + if (externalInventoryLoaded) { + // 放置方块时 vanilla 会先调 clearRemoved()、后调 setPlacedBy(),前者可能在物品上的引用 + // 尚未写入 BE 组件字段时抢先绑定了临时占位。此处必须用物品携带的引用覆盖它, + // 否则原数据不会被加载(拆除后重新放置将表现为内容物丢失)。 + if (requested == null || requested.equals(inventoryReference)) return; + ExternalInventoryStore.release(level, worldPosition, inventoryReference); + } + inventoryReference = ExternalInventoryStore.bindAt( + level, ExternalInventoryKind.MOLD_HUB, requested, worldPosition, + molds, legacyInventory, registries); + ExternalInventoryStore.setReference(this, inventoryReference); + pendingLegacyInventory = null; + externalInventoryLoaded = true; + moldInventoryChanged(); + } + + private void ensureExternalInventory() { + if (externalInventoryLoaded || level == null || level.isClientSide) return; + bindExternalInventory( + ExternalInventoryStore.getReference(this), + pendingLegacyInventory, + level.registryAccess()); + } + + @Nullable + public ExternalInventoryReference getExternalInventoryReference() { + ensureExternalInventory(); + return inventoryReference; + } + + public void releaseExternalInventory() { + if (level != null) { + ExternalInventoryStore.save(level, inventoryReference, molds, level.registryAccess()); + ExternalInventoryStore.release(level, worldPosition, inventoryReference); + } + } + + private boolean isValidMold(ItemStack stack) { + return !stack.isEmpty() && (stack.is(ModTags.MOLDS) + || AlloyFurnaceRecipeCatalog.isKnownMold(level, stack)); + } + + public boolean containsMold(Ingredient ingredient) { + return containsMolds(ingredient == null ? List.of() : List.of(ingredient)); + } + + /** Returns whether the active mold slots can satisfy every independent requirement. */ + public boolean containsMolds(List requirements) { + List normalized = MoldMatcher.normalizeRequirements(requirements); + if (normalized.isEmpty()) return true; + refreshMoldMatchCache(); + Boolean cached = moldMatchCache.get(normalized); + if (cached != null) return cached; + + boolean matched = getPreparedMolds().matches(normalized); + moldMatchCache.put(normalized, matched); + return matched; + } + + private Map getPreparedMoldMap() { + if (cachedAvailableMolds == null) { + int activeSlots = molds.getActiveSlots(); + Map available = new LinkedHashMap<>(); + for (int slot = 0; slot < activeSlots; slot++) { + ItemStack mold = molds.getStackInSlot(slot); + if (!mold.isEmpty()) available.put(slot, mold.copy()); + } + cachedAvailableMolds = Collections.unmodifiableMap(available); + } + return cachedAvailableMolds; + } + + private MoldMatcher.PreparedMolds getPreparedMolds() { + if (cachedPreparedMolds == null) { + cachedPreparedMolds = MoldMatcher.prepare(getPreparedMoldMap()); + } + return cachedPreparedMolds; + } + + /** + * Tests the one-to-one assignment independently of inventory storage. Each available stack is + * one device slot, regardless of its item count; augmenting paths handle overlapping ingredients. + */ + public static boolean matchesMolds(List requirements, List available) { + return MoldMatcher.matches(requirements, available); + } + + public static boolean matchesMolds( + List requirements, Map available) { + return MoldMatcher.matches(requirements, available); + } + + private void refreshMoldMatchCache() { + int activeSlots = molds.getActiveSlots(); + long catalogGeneration = AlloyFurnaceRecipeCatalog.generation(); + if (cachedActiveSlots != activeSlots || cachedRecipeCatalogGeneration != catalogGeneration) { + moldMatchCache.clear(); + cachedAvailableMolds = null; + cachedPreparedMolds = null; + cachedActiveSlots = activeSlots; + cachedRecipeCatalogGeneration = catalogGeneration; + } + } + + private void moldInventoryChanged() { + moldMatchCache.clear(); + cachedAvailableMolds = null; + cachedPreparedMolds = null; + if (externalInventoryLoaded && level != null && !level.isClientSide) { + ExternalInventoryStore.save(level, inventoryReference, molds, level.registryAccess()); + } + setChanged(); + } + + public void linkController(@Nullable BlockPos controllerPos, long generation) { + if (unloading || isRemoved()) return; + if (!java.util.Objects.equals(this.controllerPos, controllerPos) || structureGeneration != generation) { + this.controllerPos = controllerPos == null ? null : controllerPos.immutable(); + this.structureGeneration = generation; + setChanged(); + } + } + + @Nullable + public MultiblockAlloyFurnaceCoreBlockEntity getController() { + if (unloading || isRemoved() || level == null || controllerPos == null + || !level.isLoaded(controllerPos)) return null; + if (level.getBlockEntity(controllerPos) instanceof MultiblockAlloyFurnaceCoreBlockEntity core + && core.isMoldHubLinked(worldPosition, structureGeneration)) { + return core; + } + return null; + } + + @Override + public Component getDisplayName() { + return Component.translatable("block.useless_mod.omniversal_mold_hub"); + } + + @Override + public AbstractContainerMenu createMenu(int containerId, Inventory inventory, Player player) { + return new OmniversalMoldHubMenu(containerId, inventory, worldPosition); + } + + @Override + public void load(CompoundTag tag) { + HolderLookup.Provider registries = level == null ? net.minecraft.core.RegistryAccess.EMPTY : level.registryAccess(); + super.load(tag); + pendingLegacyInventory = tag.contains("Molds") + ? tag.getCompound("Molds").copy() : null; + moldMatchCache.clear(); + cachedAvailableMolds = null; + cachedPreparedMolds = null; + cachedActiveSlots = -1; + cachedRecipeCatalogGeneration = -1L; + controllerPos = tag.contains("Controller") ? BlockPos.of(tag.getLong("Controller")) : null; + structureGeneration = tag.getLong("StructureGeneration"); + pageMemory.load(tag); + } + + @Override + protected void saveAdditional(CompoundTag tag) { + HolderLookup.Provider registries = level == null ? net.minecraft.core.RegistryAccess.EMPTY : level.registryAccess(); + super.saveAdditional(tag); + if (controllerPos != null) tag.putLong("Controller", controllerPos.asLong()); + tag.putLong("StructureGeneration", structureGeneration); + pageMemory.save(tag); + } + + @Override + public void clearRemoved() { + super.clearRemoved(); + unloading = false; + // 这里**不要**调 ensureExternalInventory():放置方块时 vanilla 的调用顺序是 + // Level.setBlock → clearRemoved() → updateBlockEntityComponents → setPlacedBy(), + // 本方法执行时 BE 组件还是空的(物品上的引用要等 updateBlockEntityComponents 才搬进来), + // 提前 bindAt(null) 会抢一个空 UUID 并置 externalInventoryLoaded,让 setPlacedBy 的 + // adopt 早退 —— 结果就是拆掉重放后内容物丢失。绑定交给 setPlacedBy / onLoad 即可。 + } + + @Override + public void onLoad() { + super.onLoad(); + ensureExternalInventory(); + } + + @Override + public void onChunkUnloaded() { + unloading = true; + super.onChunkUnloaded(); + } + + @Override + public void setRemoved() { + unloading = true; + super.setRemoved(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/blockentities/multiblock/PassiveCraftingHatchBlockEntity.java b/src/main/java/com/sorrowmist/useless/content/blockentities/multiblock/PassiveCraftingHatchBlockEntity.java new file mode 100644 index 00000000..9e2214f1 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/blockentities/multiblock/PassiveCraftingHatchBlockEntity.java @@ -0,0 +1,1316 @@ +package com.sorrowmist.useless.content.blockentities.multiblock; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.networking.IGrid; +import appeng.api.stacks.AEFluidKey; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import com.sorrowmist.useless.content.blockentities.RecoverableItemStackHandler; +import com.sorrowmist.useless.content.blockentities.PagedMenuPageMemory; +import com.sorrowmist.useless.content.blocks.multiblock.UselessCoilBlock; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.AdvancedAlloyFurnaceAeManager; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.AdvancedAlloyFurnacePatternResolver; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.CraftingTask; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.CraftingTaskContext; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.OmniversalPatternDetails; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.SmartDoublingPatterns; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.PassivePatternInputTransaction; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.catalyst.ResolvedCatalystEffect; +import com.sorrowmist.useless.content.menus.PassiveCraftingHatchMenu; +import com.sorrowmist.useless.content.menus.PagedRecoverableMenu; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeCatalog; +import com.sorrowmist.useless.core.config.ConfigManager; +import com.sorrowmist.useless.core.component.ExternalInventoryKind; +import com.sorrowmist.useless.core.component.ExternalInventoryReference; +import com.sorrowmist.useless.core.component.MultiblockPartData; +import com.sorrowmist.useless.core.component.PassiveHatchSettings; +import com.sorrowmist.useless.world.inventory.ExternalInventoryStore; +import com.sorrowmist.useless.energy.EnergyManager; +import com.sorrowmist.useless.energy.IEnergyManager; +import com.sorrowmist.useless.init.ModBlockEntities; +import com.sorrowmist.useless.init.ModItems; +import com.sorrowmist.useless.network.PassiveCraftingStatusPacket; +import net.minecraft.core.BlockPos; +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.capability.templates.FluidTank; +import net.minecraftforge.items.ItemStackHandler; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; + +/** Pattern inventory and independent passive task owner for an optional multiblock casing hatch. */ +public final class PassiveCraftingHatchBlockEntity extends BlockEntity + implements MenuProvider, CraftingTaskContext { + public static final int MAX_PATTERN_SLOTS = RecoverableItemStackHandler.MAX_SLOTS; + public static final int PATTERN_SLOTS = MAX_PATTERN_SLOTS; + public static final int MENU_DATA_COUNT = 10; + public static final int MIN_INTERVAL_TICKS = 1; + public static final int MAX_INTERVAL_TICKS = 72_000; + public static final int DEFAULT_INTERVAL_TICKS = 1_200; + + private static final ItemStackHandler EMPTY_ITEMS = new ItemStackHandler(0); + private static final FluidTank[] EMPTY_TANKS = new FluidTank[0]; + + private final RecoverableItemStackHandler patterns = new RecoverableItemStackHandler( + MAX_PATTERN_SLOTS, 0, this::getActivePatternSlots, + stack -> stack.is(ModItems.OMNIVERSAL_PATTERN.get()), this::inventoryChanged); + private final PagedMenuPageMemory pageMemory = new PagedMenuPageMemory(this::setChanged); + private final Map activeTasks = new HashMap<>(); + /** + * Sparse per-slot batch sizes. A missing entry means the slot follows {@link #multiplier}, + * the global default, so clearing an override never has to guess what the default was. + */ + private final Map slotMultiplierOverrides = new HashMap<>(); + private int statusCapacity = configuredStatusCapacity(); + private SlotState[] idleStates = new SlotState[statusCapacity]; + private String[] idleDetails = new String[statusCapacity]; + private OmniversalPatternDetails[] decodedPatterns = new OmniversalPatternDetails[statusCapacity]; + private boolean[] patternDecodeCached = new boolean[statusCapacity]; + private final ConcurrentHashMap taskProgress = + new ConcurrentHashMap<>(); + private final AtomicInteger totalProgress = new AtomicInteger(); + private final AtomicInteger totalMaxProgress = new AtomicInteger(); + private final ReentrantLock craftingLock = new ReentrantLock(); + private final List localUnreturnedInputs = new ArrayList<>(); + private final IEnergyManager fallbackEnergy = EnergyManager.builder() + .capacity(1L).maxReceive(0L).maxExtract(0L).build(); + + @Nullable + private BlockPos controllerPos; + private long structureGeneration; + private int intervalTicks = DEFAULT_INTERVAL_TICKS; + private int countdownTicks = DEFAULT_INTERVAL_TICKS; + private long multiplier = 1L; + private long recipeCatalogGeneration = -1L; + @Nullable + private CompoundTag deferredTasksTag; + private boolean loading; + private boolean unloading; + private boolean statusDirty = true; + private int statusSyncTimer; + private int observedActivePatternSlots = -1; + private long patternStorageRevision; + @Nullable + private ExternalInventoryReference inventoryReference; + @Nullable + private CompoundTag pendingLegacyInventory; + private boolean externalInventoryLoaded; + + private final ContainerData menuData = new ContainerData() { + @Override + public int get(int index) { + MultiblockAlloyFurnaceCoreBlockEntity controller = getController(); + return switch (index) { + case 0 -> controller == null ? 0 : 1; + case 1 -> controller == null ? 0 : controller.getCoilTier(); + case 2 -> getActivePatternSlots(); + case 3 -> intervalTicks; + case 4 -> countdownTicks; + case 5 -> (int) multiplier; + case 6 -> (int) (multiplier >>> 32); + case 7 -> (int) getCurrentMaxParallel(); + case 8 -> (int) (getCurrentMaxParallel() >>> 32); + case 9 -> getConfiguredPatternSlots(); + default -> 0; + }; + } + + @Override + public void set(int index, int value) { + } + + @Override + public int getCount() { + return MENU_DATA_COUNT; + } + }; + + public PassiveCraftingHatchBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.PASSIVE_CRAFTING_HATCH.get(), pos, state); + for (int slot = 0; slot < statusCapacity; slot++) { + idleStates[slot] = SlotState.EMPTY; + idleDetails[slot] = ""; + } + } + + public RecoverableItemStackHandler getPatterns() { + ensureExternalInventory(); + return patterns; + } + + public PagedMenuPageMemory getPageMemory() { + return pageMemory; + } + + public long getPatternStorageRevision() { + return patternStorageRevision; + } + + public MultiblockPartData createItemData(HolderLookup.Provider registries) { + return MultiblockPartData.passiveHatch( + patterns, registries, intervalTicks, multiplier); + } + + public MultiblockPartData createSettingsItemData() { + return new MultiblockPartData(MultiblockPartData.CURRENT_VERSION, + new CompoundTag(), intervalTicks, multiplier); + } + + public void restoreItemData(MultiblockPartData data, HolderLookup.Provider registries) { + if (data == null) return; + loading = true; + try { + data.restoreInventory(patterns, registries); + } finally { + loading = false; + } + patternStorageRevision++; + intervalTicks = data.intervalTicks() > 0 + ? Math.max(MIN_INTERVAL_TICKS, Math.min(MAX_INTERVAL_TICKS, data.intervalTicks())) + : DEFAULT_INTERVAL_TICKS; + multiplier = Math.max(1L, data.multiplier()); + // MultiblockPartData predates per-slot multipliers, so a full restore starts clean. + slotMultiplierOverrides.clear(); + countdownTicks = intervalTicks; + deferredTasksTag = null; + localUnreturnedInputs.clear(); + observedActivePatternSlots = -1; + clearPatternDecodeCache(); + resetIdleStates(); + statusDirty = true; + setChanged(); + } + + public void restoreSettings(MultiblockPartData data) { + if (data == null) return; + restoreSettings(data.intervalTicks(), data.multiplier(), null); + } + + public void restoreSettings(PassiveHatchSettings settings) { + if (settings == null) return; + restoreSettings(settings.intervalTicks(), settings.multiplier(), settings.slotMultipliers()); + } + + private void restoreSettings(int requestedInterval, long requestedMultiplier, + @Nullable List requestedOverrides) { + intervalTicks = requestedInterval > 0 + ? Math.max(MIN_INTERVAL_TICKS, Math.min(MAX_INTERVAL_TICKS, requestedInterval)) + : DEFAULT_INTERVAL_TICKS; + multiplier = Math.max(1L, requestedMultiplier); + slotMultiplierOverrides.clear(); + if (requestedOverrides != null) { + for (PassiveHatchSettings.SlotMultiplier entry : requestedOverrides) { + if (entry == null || !entry.valid() || entry.slot() >= PATTERN_SLOTS) { + continue; + } + slotMultiplierOverrides.put(entry.slot(), entry.multiplier()); + } + } + countdownTicks = intervalTicks; + statusDirty = true; + setChanged(); + } + + public void bindExternalInventory(@Nullable ExternalInventoryReference requested, + @Nullable CompoundTag legacyInventory, + HolderLookup.Provider registries) { + if (level == null || level.isClientSide) return; + if (externalInventoryLoaded) { + // 放置方块时 vanilla 会先调 clearRemoved()、后调 setPlacedBy(),前者可能在物品上的引用 + // 尚未写入 BE 组件字段时抢先绑定了临时占位。此处必须用物品携带的引用覆盖它, + // 否则原数据不会被加载(拆除后重新放置将表现为内容物丢失)。 + if (requested == null || requested.equals(inventoryReference)) return; + ExternalInventoryStore.release(level, worldPosition, inventoryReference); + } + inventoryReference = ExternalInventoryStore.bindAt( + level, ExternalInventoryKind.PASSIVE_HATCH, requested, worldPosition, + patterns, legacyInventory, registries); + ExternalInventoryStore.setReference(this, inventoryReference); + pendingLegacyInventory = null; + externalInventoryLoaded = true; + inventoryChanged(); + } + + private void ensureExternalInventory() { + if (externalInventoryLoaded || level == null || level.isClientSide) return; + bindExternalInventory( + ExternalInventoryStore.getReference(this), + pendingLegacyInventory, + level.registryAccess()); + } + + @Nullable + public ExternalInventoryReference getExternalInventoryReference() { + ensureExternalInventory(); + return inventoryReference; + } + + public void releaseExternalInventory() { + if (level != null) { + ExternalInventoryStore.save(level, inventoryReference, patterns, level.registryAccess()); + ExternalInventoryStore.release(level, worldPosition, inventoryReference); + } + } + + private static int configuredStatusCapacity() { + return Math.max(1, Math.min(MAX_PATTERN_SLOTS, + ConfigManager.getOmniversalPassivePatternSlots())); + } + + private void ensureStatusCapacity() { + int configured = configuredStatusCapacity(); + if (configured <= statusCapacity) return; + int oldCapacity = statusCapacity; + statusCapacity = configured; + idleStates = java.util.Arrays.copyOf(idleStates, statusCapacity); + idleDetails = java.util.Arrays.copyOf(idleDetails, statusCapacity); + decodedPatterns = java.util.Arrays.copyOf(decodedPatterns, statusCapacity); + patternDecodeCached = java.util.Arrays.copyOf(patternDecodeCached, statusCapacity); + java.util.Arrays.fill(idleStates, oldCapacity, statusCapacity, SlotState.EMPTY); + java.util.Arrays.fill(idleDetails, oldCapacity, statusCapacity, ""); + } + + public ContainerData getMenuData() { + return menuData; + } + + public int getIntervalTicks() { + return intervalTicks; + } + + public int getCountdownTicks() { + return countdownTicks; + } + + public long getMultiplier() { + return multiplier; + } + + /** Batch size a slot actually runs at: its own override, otherwise the global default. */ + public long getSlotMultiplier(int slot) { + long override = getSlotMultiplierOverride(slot); + return override > 0L ? override : multiplier; + } + + /** {@code 0} means "follow the global multiplier"; any other value is an explicit override. */ + public long getSlotMultiplierOverride(int slot) { + if (slot < 0 || slot >= PATTERN_SLOTS) { + return 0L; + } + Long value = slotMultiplierOverrides.get(slot); + return value == null ? 0L : Math.max(0L, value); + } + + public boolean hasSlotMultiplierOverride(int slot) { + return getSlotMultiplierOverride(slot) > 0L; + } + + /** Sorted snapshot used for item data, so drops stay byte-for-byte stable. */ + public List slotMultiplierSettings() { + List result = new ArrayList<>(slotMultiplierOverrides.size()); + for (Map.Entry entry : new java.util.TreeMap<>(slotMultiplierOverrides).entrySet()) { + long value = entry.getValue() == null ? 0L : entry.getValue(); + if (value > 0L) { + result.add(new PassiveHatchSettings.SlotMultiplier(entry.getKey(), value)); + } + } + return List.copyOf(result); + } + + /** + * Pins one slot to its own batch size. {@code requested <= 0} clears the override so the slot + * returns to following the global multiplier. The value is clamped to the coil-tier parallel + * limit, because a passive batch larger than the machine could ever run is never useful. + */ + public void setSlotMultiplier(int slot, long requested) { + if (slot < 0 || slot >= PATTERN_SLOTS) { + return; + } + long clamped = requested <= 0L + ? 0L : Math.max(1L, Math.min(getCurrentMaxParallel(), requested)); + long previous = getSlotMultiplierOverride(slot); + if (clamped == previous) { + return; + } + if (clamped <= 0L) { + slotMultiplierOverrides.remove(slot); + } else { + slotMultiplierOverrides.put(slot, clamped); + } + statusDirty = true; + setChanged(); + } + + /** Drops every override, making all slots follow the global multiplier again. */ + public void clearSlotMultipliers() { + if (slotMultiplierOverrides.isEmpty()) { + return; + } + slotMultiplierOverrides.clear(); + statusDirty = true; + setChanged(); + } + + public int getActivePatternSlots() { + MultiblockAlloyFurnaceCoreBlockEntity controller = getController(); + return controller == null ? 0 : activeSlotsForCoilTier(controller.getCoilTier()); + } + + public static int activeSlotsForCoilTier(int coilTier) { + return activeSlotsForCoilTier(coilTier, ConfigManager.getOmniversalPassivePatternSlots()); + } + + static int activeSlotsForCoilTier(int coilTier, int configuredSlots) { + int tier = Math.max(0, Math.min(UselessCoilBlock.MAX_TIER, coilTier)); + int capacity = Math.max(1, Math.min(MAX_PATTERN_SLOTS, configuredSlots)); + return (capacity * tier + UselessCoilBlock.MAX_TIER - 1) / UselessCoilBlock.MAX_TIER; + } + + public int getConfiguredPatternSlots() { + return ConfigManager.getOmniversalPassivePatternSlots(); + } + + public long getCurrentMaxParallel() { + MultiblockAlloyFurnaceCoreBlockEntity controller = getController(); + return controller == null ? 1L : Math.max(1L, controller.getPassiveCraftingMaxParallel()); + } + + @Override + public long getCraftingPatternCapacity() { + return getCurrentMaxParallel(); + } + + public void applySettings(int requestedInterval, long requestedMultiplier) { + if (getController() == null) { + return; + } + int newInterval = Math.max(MIN_INTERVAL_TICKS, + Math.min(MAX_INTERVAL_TICKS, requestedInterval)); + long newMultiplier = Math.max(1L, Math.min(getCurrentMaxParallel(), requestedMultiplier)); + if (newInterval == intervalTicks && newMultiplier == multiplier) { + return; + } + intervalTicks = newInterval; + multiplier = newMultiplier; + countdownTicks = intervalTicks; + statusDirty = true; + setChanged(); + } + + public void linkController(@Nullable BlockPos newControllerPos, long generation) { + if (unloading || isRemoved()) { + return; + } + BlockPos immutable = newControllerPos == null ? null : newControllerPos.immutable(); + if (Objects.equals(controllerPos, immutable) && structureGeneration == generation) { + clampMultiplier(); + return; + } + + if (controllerPos != null && immutable != null && !controllerPos.equals(immutable)) { + MultiblockAlloyFurnaceCoreBlockEntity previousController = getRawController(); + cancelAllTasks(); + if (previousController != null) { + flushLocalUnreturnedInputs(previousController); + } + } + controllerPos = immutable; + structureGeneration = generation; + clampMultiplier(); + statusDirty = true; + setChanged(); + } + + /** True when this hatch's raw association belongs to the given core. */ + public boolean isLinkedToController(BlockPos controllerPos) { + return Objects.equals(this.controllerPos, controllerPos); + } + + /** Passive hatches can only execute work for one formed controller. */ + public boolean isClaimedByOtherController(BlockPos controllerPos) { + MultiblockAlloyFurnaceCoreBlockEntity controller = getController(); + return controller != null && !controller.getBlockPos().equals(controllerPos); + } + + @Nullable + public MultiblockAlloyFurnaceCoreBlockEntity getController() { + if (level == null || controllerPos == null || !level.isLoaded(controllerPos)) { + return null; + } + if (level.getBlockEntity(controllerPos) instanceof MultiblockAlloyFurnaceCoreBlockEntity core + && core.isFormed() + && core.isPassiveHatchLinked(worldPosition, structureGeneration)) { + return core; + } + return null; + } + + @Nullable + private MultiblockAlloyFurnaceCoreBlockEntity getRawController() { + if (level == null || controllerPos == null || !level.isLoaded(controllerPos)) { + return null; + } + return level.getBlockEntity(controllerPos) instanceof MultiblockAlloyFurnaceCoreBlockEntity core + ? core : null; + } + + /** Called by the linked controller after normal AE tasks have consumed their tick. */ + public boolean serverTickFromController(MultiblockAlloyFurnaceCoreBlockEntity controller) { + if (unloading || isRemoved() || level == null || level.isClientSide || controller != getController()) { + return false; + } + loadDeferredTasks(); + reconcileActivePatternSlots(); + flushLocalUnreturnedInputs(controller); + + long currentCatalogGeneration = AlloyFurnaceRecipeCatalog.generation(); + if (recipeCatalogGeneration != currentCatalogGeneration) { + recipeCatalogGeneration = currentCatalogGeneration; + clearPatternDecodeCache(); + resetIdleStates(); + } + + if (!controller.isTaskExecutionEnabled()) { + flushStatusUpdates(); + return false; + } + + boolean progressed = tickActiveTasks(); + if (--countdownTicks <= 0) { + countdownTicks = intervalTicks; + runPassiveCycle(controller); + setChanged(); + } + flushStatusUpdates(); + return progressed; + } + + /** Keeps viewer status current while the structure is unavailable without advancing work. */ + public void serverTickStandalone() { + if (unloading || isRemoved() || level == null || level.isClientSide || getController() != null) { + return; + } + loadDeferredTasks(); + MultiblockAlloyFurnaceCoreBlockEntity controller = getRawController(); + if (controller != null) { + flushLocalUnreturnedInputs(controller); + } + flushStatusUpdates(); + } + + private boolean tickActiveTasks() { + boolean progressed = false; + List slots = new ArrayList<>(activeTasks.keySet()); + slots.sort(Integer::compareTo); + for (int slot : slots) { + CraftingTask task = activeTasks.get(slot); + if (task == null) { + continue; + } + task.tick(); + progressed |= task.progressedLastTick(); + if (task.isProcessingComplete()) { + activeTasks.remove(slot); + setIdleState(slot, patterns.getStackInSlot(slot).isEmpty() + ? SlotState.EMPTY : SlotState.READY, ""); + setChanged(); + } + } + return progressed; + } + + public boolean hasWork() { + if (!activeTasks.isEmpty() || deferredTasksTag != null) { + return true; + } + int activeSlots = getActivePatternSlots(); + for (int slot = 0; slot < activeSlots; slot++) { + if (!patterns.getStackInSlot(slot).isEmpty()) { + return true; + } + } + return false; + } + + private void runPassiveCycle(MultiblockAlloyFurnaceCoreBlockEntity controller) { + List candidates = new ArrayList<>(); + int activeSlots = getActivePatternSlots(); + for (int slot = 0; slot < activeSlots; slot++) { + if (activeTasks.containsKey(slot)) { + continue; + } + ItemStack stack = patterns.getStackInSlot(slot); + if (stack.isEmpty()) { + setIdleState(slot, SlotState.EMPTY, ""); + continue; + } + + OmniversalPatternDetails omniversal = decodePattern(slot, stack); + if (omniversal == null) { + setIdleState(slot, SlotState.INVALID_PATTERN, ""); + continue; + } + long slotMultiplier = getSlotMultiplier(slot); + if (!amountsFit(omniversal, slotMultiplier)) { + setIdleState(slot, SlotState.INVALID_PATTERN, "amount_overflow"); + continue; + } + if (!controller.isTaskRecipeAvailable(omniversal.recipe())) { + setIdleState(slot, SlotState.MISSING_MOLD, ""); + continue; + } + candidates.add(new Candidate(slot, omniversal, slotMultiplier)); + } + + if (candidates.isEmpty()) { + return; + } + MultiblockAlloyFurnaceCoreBlockEntity.AeNetworkAccess access = controller.getAeNetworkAccess(); + if (access == null) { + for (Candidate candidate : candidates) { + setIdleState(candidate.slot, SlotState.AE_OFFLINE, ""); + } + return; + } + + List extractions = + PassivePatternInputTransaction.extractAll( + candidates.stream().map(Candidate::pattern).toList(), + candidates.stream().mapToLong(Candidate::multiplier).toArray(), + level, access.storage(), access::cachedInventory, access.source(), + this::stashUnreturnedInput); + for (int index = 0; index < candidates.size(); index++) { + Candidate candidate = candidates.get(index); + PassivePatternInputTransaction.Result extraction = extractions.get(index); + if (!extraction.successful()) { + SlotState state = extraction.failure() == PassivePatternInputTransaction.Failure.MISSING_INPUT + ? SlotState.MISSING_INPUT + : extraction.failure() == PassivePatternInputTransaction.Failure.AMOUNT_OVERFLOW + ? SlotState.INVALID_PATTERN : SlotState.AE_OFFLINE; + String detail = extraction.missingKey() == null + ? "" : extraction.missingKey().getDisplayName().getString(); + setIdleState(candidate.slot(), state, detail); + continue; + } + + CraftingTask task = new CraftingTask( + candidate.slot(), candidate.pattern(), extraction.inputs(), candidate.multiplier(), this); + if (!task.canStartNow()) { + CraftingTask.returnInputsToAE(Collections.singletonList(extraction.inputs()), this); + setIdleState(candidate.slot(), SlotState.MISSING_MOLD, task.getWaitingDetail()); + continue; + } + activeTasks.put(candidate.slot(), task); + setIdleState(candidate.slot(), SlotState.RUNNING, ""); + setChanged(); + } + } + + private static boolean amountsFit(OmniversalPatternDetails pattern, long operations) { + try { + Map totals = new HashMap<>(); + for (ItemStack output : pattern.recipe().outputs()) { + addScaledAmount(totals, AEItemKey.of(output), output.getCount(), operations); + } + for (FluidStack output : pattern.recipe().outputFluids()) { + addScaledAmount(totals, AEFluidKey.of(output), output.getAmount(), operations); + } + for (GenericStack output : pattern.recipe().keyOutputs()) { + addScaledAmount(totals, output.what(), output.amount(), operations); + } + return true; + } catch (ArithmeticException exception) { + return false; + } + } + + private static void addScaledAmount( + Map totals, @Nullable AEKey key, long amount, long operations) { + if (key == null || amount <= 0L) { + return; + } + long scaled = Math.multiplyExact(amount, operations); + totals.merge(key, scaled, Math::addExact); + } + + public void prepareForRemoval() { + loadDeferredTasks(true); + cancelAllTasks(); + MultiblockAlloyFurnaceCoreBlockEntity controller = getRawController(); + if (controller != null) { + flushLocalUnreturnedInputs(controller); + } + } + + public void prepareForControllerRemoval(MultiblockAlloyFurnaceCoreBlockEntity controller) { + if (controller == null || level == null || controller.getLevel() != level + || getRawController() != controller) { + return; + } + loadDeferredTasks(true); + cancelAllTasks(); + flushLocalUnreturnedInputs(controller); + } + + public void cancelAllTasks() { + for (CraftingTask task : new ArrayList<>(activeTasks.values())) { + task.cancel(); + } + activeTasks.clear(); + taskProgress.clear(); + totalProgress.set(0); + totalMaxProgress.set(0); + deferredTasksTag = null; + resetIdleStates(); + statusDirty = true; + setChanged(); + } + + /** + * 在 level 与配方目录均可用后加载延迟的任务数据(样板解码同时依赖二者)。 + * + * @return true 表示已处理完毕(含无待恢复数据);false 表示前提条件未满足,需稍后重试 + */ + private boolean loadDeferredTasks() { + return loadDeferredTasks(false); + } + + /** + * @param force 方块即将被移除时传 true:这是退还材料的最后机会,必须立即解码, + * 否则随后的 cancelAllTasks 会清空标签,材料将无声丢失 + */ + private boolean loadDeferredTasks(boolean force) { + CompoundTag tasksTag = deferredTasksTag; + if (tasksTag == null) { + return true; + } + if (level == null) { + return false; + } + // 目录由后台线程构建,未就绪时解码会把完好样板判为失效并退还材料,故挂起重试。 + if (!force && !AlloyFurnaceRecipeCatalog.isReady(level)) { + return false; + } + deferredTasksTag = null; + HolderLookup.Provider registries = level.registryAccess(); + ListTag tasks = tasksTag.getList("Tasks", Tag.TAG_COMPOUND); + for (int index = 0; index < tasks.size(); index++) { + CompoundTag entry = tasks.getCompound(index); + int slot = entry.getInt("Slot"); + CompoundTag taskTag = entry.getCompound("Task"); + if (slot < 0 || slot >= PATTERN_SLOTS || activeTasks.containsKey(slot)) { + CraftingTask.returnSavedMaterials(taskTag, this, registries); + continue; + } + CraftingTask task = CraftingTask.load(taskTag, level, this, registries); + if (task == null) { + CraftingTask.returnSavedMaterials(taskTag, this, registries); + } else { + activeTasks.put(slot, task); + } + } + statusDirty = true; + return true; + } + + /** + * A server-config reduction turns excess pattern slots into recovery-only + * storage. Any task already using such a slot must return its materials + * before the slot becomes inactive. + */ + private void reconcileActivePatternSlots() { + int activeSlots = getActivePatternSlots(); + if (observedActivePatternSlots == activeSlots) { + return; + } + observedActivePatternSlots = activeSlots; + boolean changed = false; + for (int slot : new ArrayList<>(activeTasks.keySet())) { + if (slot < activeSlots) { + continue; + } + CraftingTask task = activeTasks.remove(slot); + if (task != null) { + task.cancel(); + taskProgress.remove(slot); + changed = true; + } + } + resetIdleStates(); + statusDirty = true; + if (changed) { + setChanged(); + } + } + + private CompoundTag saveTasks(HolderLookup.Provider registries) { + if (deferredTasksTag != null) { + return deferredTasksTag.copy(); + } + CompoundTag result = new CompoundTag(); + ListTag tasks = new ListTag(); + activeTasks.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .forEach(entry -> { + CompoundTag task = new CompoundTag(); + task.putInt("Slot", entry.getKey()); + task.put("Task", entry.getValue().save(registries)); + tasks.add(task); + }); + result.put("Tasks", tasks); + return result; + } + + private void inventoryChanged() { + if (loading || unloading || isRemoved()) { + return; + } + patternStorageRevision++; + if (externalInventoryLoaded && level != null && !level.isClientSide) { + ExternalInventoryStore.save(level, inventoryReference, patterns, level.registryAccess()); + } + clearPatternDecodeCache(); + countdownTicks = intervalTicks; + resetIdleStates(); + statusDirty = true; + setChanged(); + } + + @Nullable + private OmniversalPatternDetails decodePattern(int slot, ItemStack stack) { + ensureStatusCapacity(); + if (slot < 0 || slot >= statusCapacity) return null; + if (!patternDecodeCached[slot]) { + IPatternDetails decoded = AdvancedAlloyFurnacePatternResolver.decode(stack, level); + decodedPatterns[slot] = decoded instanceof OmniversalPatternDetails omniversal + ? omniversal : null; + patternDecodeCached[slot] = true; + } + return decodedPatterns[slot]; + } + + private void clearPatternDecodeCache() { + java.util.Arrays.fill(decodedPatterns, null); + java.util.Arrays.fill(patternDecodeCached, false); + } + + private void resetIdleStates() { + ensureStatusCapacity(); + for (int slot = 0; slot < PATTERN_SLOTS; slot++) { + if (slot >= statusCapacity) break; + if (!activeTasks.containsKey(slot)) { + setIdleState(slot, patterns.getStackInSlot(slot).isEmpty() + ? SlotState.EMPTY : SlotState.READY, ""); + } + } + } + + private void setIdleState(int slot, SlotState state, String detail) { + ensureStatusCapacity(); + if (slot < 0 || slot >= PATTERN_SLOTS) { + return; + } + if (slot >= statusCapacity) return; + String safeDetail = detail == null ? "" : detail; + if (idleStates[slot] != state || !Objects.equals(idleDetails[slot], safeDetail)) { + idleStates[slot] = state; + idleDetails[slot] = safeDetail; + statusDirty = true; + } + } + + private List getSlotStatusSnapshot(int firstSlot, int count) { + ensureStatusCapacity(); + int start = Math.max(0, Math.min(PATTERN_SLOTS, firstSlot)); + int end = Math.max(start, Math.min(PATTERN_SLOTS, start + Math.max(0, count))); + List result = new ArrayList<>(end - start); + boolean enabled = isTaskExecutionEnabled(); + for (int slot = start; slot < end; slot++) { + CraftingTask task = activeTasks.get(slot); + long slotMultiplier = getSlotMultiplierOverride(slot); + if (task == null) { + if (slot >= statusCapacity) { + result.add(new SlotStatus(slot, SlotState.EMPTY, 0, 0, "", slotMultiplier)); + continue; + } + result.add(new SlotStatus(slot, idleStates[slot], 0, 0, idleDetails[slot], slotMultiplier)); + continue; + } + AdvancedAlloyFurnaceAeManager.AETaskProgress progress = taskProgress.get(slot); + SlotState state; + if (task.isAwaitingOutputFlush()) { + state = SlotState.WAITING_OUTPUT; + } else if (!enabled) { + state = SlotState.PAUSED; + } else if (progress != null && isWaitingForMold(progress.getStatusKey())) { + state = SlotState.MISSING_MOLD; + } else { + state = SlotState.RUNNING; + } + result.add(new SlotStatus(slot, state, + progress == null ? 0 : progress.getProgress(), + progress == null ? 1 : progress.getMaxProgress(), + progress == null ? "" : progress.getStatusDetail(), + slotMultiplier)); + } + return List.copyOf(result); + } + + private static boolean isWaitingForMold(String statusKey) { + return "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_mold".equals(statusKey) + || "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_missing_mold".equals(statusKey) + || "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_mold_hub".equals(statusKey); + } + + public void requestStatusSync() { + statusDirty = true; + statusSyncTimer = 20; + } + + private void flushStatusUpdates() { + if (!statusDirty && ++statusSyncTimer < 20) { + return; + } + statusDirty = false; + statusSyncTimer = 0; + if (!(level instanceof ServerLevel serverLevel)) { + return; + } + for (var player : serverLevel.players()) { + if (player.containerMenu instanceof PassiveCraftingHatchMenu menu + && menu.getBlockPos().equals(worldPosition)) { + PacketDistributor.sendToPlayer(player, + new PassiveCraftingStatusPacket(menu.containerId, worldPosition, + getSlotStatusSnapshot(menu.getPage() * PagedRecoverableMenu.SLOTS_PER_PAGE, + PagedRecoverableMenu.SLOTS_PER_PAGE))); + } + } + } + + private void clampMultiplier() { + MultiblockAlloyFurnaceCoreBlockEntity controller = getController(); + if (controller == null) { + return; + } + long limit = Math.max(1L, controller.getPassiveCraftingMaxParallel()); + boolean changed = false; + if (multiplier > limit) { + multiplier = limit; + changed = true; + } + for (var iterator = slotMultiplierOverrides.entrySet().iterator(); iterator.hasNext(); ) { + Map.Entry entry = iterator.next(); + long value = entry.getValue() == null ? 0L : entry.getValue(); + if (value <= 0L) { + iterator.remove(); + changed = true; + continue; + } + if (value > limit) { + entry.setValue(limit); + changed = true; + } + } + if (changed) { + statusDirty = true; + setChanged(); + } + } + + private void flushLocalUnreturnedInputs(MultiblockAlloyFurnaceCoreBlockEntity controller) { + if (localUnreturnedInputs.isEmpty()) { + return; + } + for (GenericStack stack : localUnreturnedInputs) { + controller.stashUnreturnedInput(stack.what(), stack.amount()); + } + localUnreturnedInputs.clear(); + setChanged(); + } + + @Override + public Component getDisplayName() { + return Component.translatable("block.useless_mod.passive_crafting_hatch"); + } + + @Override + public AbstractContainerMenu createMenu(int containerId, Inventory inventory, Player player) { + statusDirty = true; + return new PassiveCraftingHatchMenu(containerId, inventory, worldPosition); + } + + @Override + public void load(CompoundTag tag) { + HolderLookup.Provider registries = level == null ? net.minecraft.core.RegistryAccess.EMPTY : level.registryAccess(); + super.load(tag); + loading = true; + pendingLegacyInventory = tag.contains("Patterns") + ? tag.getCompound("Patterns").copy() : null; + loading = false; + patternStorageRevision++; + controllerPos = tag.contains("Controller") ? BlockPos.of(tag.getLong("Controller")) : null; + structureGeneration = tag.getLong("StructureGeneration"); + intervalTicks = Math.max(MIN_INTERVAL_TICKS, + Math.min(MAX_INTERVAL_TICKS, tag.getInt("IntervalTicks"))); + if (!tag.contains("IntervalTicks")) { + intervalTicks = DEFAULT_INTERVAL_TICKS; + } + multiplier = Math.max(1L, tag.getLong("Multiplier")); + slotMultiplierOverrides.clear(); + ListTag slotMultipliers = tag.getList("SlotMultipliers", Tag.TAG_COMPOUND); + for (int index = 0; index < slotMultipliers.size(); index++) { + CompoundTag entry = slotMultipliers.getCompound(index); + int slot = entry.getInt("Slot"); + long value = entry.getLong("Multiplier"); + if (slot >= 0 && slot < PATTERN_SLOTS && value > 0L) { + slotMultiplierOverrides.put(slot, value); + } + } + countdownTicks = intervalTicks; + deferredTasksTag = tag.contains("PassiveTasks") ? tag.getCompound("PassiveTasks") : null; + localUnreturnedInputs.clear(); + ListTag unreturned = tag.getList("UnreturnedInputs", Tag.TAG_COMPOUND); + for (int index = 0; index < unreturned.size(); index++) { + GenericStack stack = GenericStack.readTag(unreturned.getCompound(index)); + if (stack != null) { + localUnreturnedInputs.add(stack); + } + } + observedActivePatternSlots = -1; + clearPatternDecodeCache(); + resetIdleStates(); + pageMemory.load(tag); + } + + @Override + protected void saveAdditional(CompoundTag tag) { + HolderLookup.Provider registries = level == null ? net.minecraft.core.RegistryAccess.EMPTY : level.registryAccess(); + super.saveAdditional(tag); + tag.putInt("DataVersion", 1); + if (controllerPos != null) { + tag.putLong("Controller", controllerPos.asLong()); + } + tag.putLong("StructureGeneration", structureGeneration); + tag.putInt("IntervalTicks", intervalTicks); + tag.putLong("Multiplier", multiplier); + ListTag slotMultipliers = new ListTag(); + if (!slotMultiplierOverrides.isEmpty()) { + for (Map.Entry entry : new java.util.TreeMap<>(slotMultiplierOverrides).entrySet()) { + long value = entry.getValue() == null ? 0L : entry.getValue(); + if (value <= 0L) { + continue; + } + CompoundTag slotEntry = new CompoundTag(); + slotEntry.putInt("Slot", entry.getKey()); + slotEntry.putLong("Multiplier", value); + slotMultipliers.add(slotEntry); + } + } + tag.put("SlotMultipliers", slotMultipliers); + tag.put("PassiveTasks", saveTasks(registries)); + ListTag unreturned = new ListTag(); + for (GenericStack stack : localUnreturnedInputs) { + unreturned.add(GenericStack.writeTag(stack)); + } + tag.put("UnreturnedInputs", unreturned); + pageMemory.save(tag); + } + + @Override + public void clearRemoved() { + super.clearRemoved(); + unloading = false; + countdownTicks = intervalTicks; + statusDirty = true; + observedActivePatternSlots = -1; + clearPatternDecodeCache(); + // 这里**不要**调 ensureExternalInventory():放置方块时 vanilla 的调用顺序是 + // Level.setBlock → clearRemoved() → updateBlockEntityComponents → setPlacedBy(), + // 本方法执行时 BE 组件还是空的(物品上的引用要等 updateBlockEntityComponents 才搬进来), + // 提前 bindAt(null) 会抢一个空 UUID 并置 externalInventoryLoaded,让 setPlacedBy 的 + // adopt 早退 —— 结果就是拆掉重放后内容物丢失。绑定交给 setPlacedBy / onLoad 即可。 + } + + @Override + public void onLoad() { + super.onLoad(); + ensureExternalInventory(); + } + + @Override + public void onChunkUnloaded() { + unloading = true; + super.onChunkUnloaded(); + } + + @Override + public void setRemoved() { + unloading = true; + super.setRemoved(); + } + + @Override + public int getInputSlotsStart() { + return 0; + } + + @Override + public int getInputSlotsCount() { + return 0; + } + + @Override + public int getOutputSlotsStart() { + return 0; + } + + @Override + public int getOutputSlotsCount() { + return 0; + } + + @Override + public int getCatalystSlot() { + return 0; + } + + @Override + public int getMoldSlot() { + return 0; + } + + @Override + public int getFluidTankCount() { + return 0; + } + + @Override + public Level getLevel() { + return level; + } + + @Override + public ItemStackHandler getItemHandler() { + return EMPTY_ITEMS; + } + + @Override + public IEnergyManager getEnergyManager() { + MultiblockAlloyFurnaceCoreBlockEntity controller = getRawController(); + return controller == null ? fallbackEnergy : controller.getEnergyManager(); + } + + @Override + public void markChanged() { + setChanged(); + } + + @Override + public void sendAETaskProgressToClients() { + statusDirty = true; + } + + @Override + public int getCatalystMaxParallel() { + return (int) Math.min(Integer.MAX_VALUE, getCurrentMaxParallel()); + } + + @Override + public long tryOutputToAE(ItemStack stack) { + MultiblockAlloyFurnaceCoreBlockEntity controller = getRawController(); + return controller == null ? 0L : controller.tryOutputToAE(stack); + } + + @Override + public long tryOutputFluidToAE(FluidStack stack) { + MultiblockAlloyFurnaceCoreBlockEntity controller = getRawController(); + return controller == null ? 0L : controller.tryOutputFluidToAE(stack); + } + + @Override + public long tryOutputKeyToAE(AEKey key, long amount) { + MultiblockAlloyFurnaceCoreBlockEntity controller = getRawController(); + return controller == null ? 0L : controller.tryOutputKeyToAE(key, amount); + } + + @Override + public boolean isReturnOutputToAe() { + return true; + } + + @Override + public void stashUnreturnedInput(AEKey key, long amount) { + if (key == null || amount <= 0) { + return; + } + MultiblockAlloyFurnaceCoreBlockEntity controller = getRawController(); + if (controller != null) { + controller.stashUnreturnedInput(key, amount); + } else { + localUnreturnedInputs.add(new GenericStack(key, amount)); + setChanged(); + } + } + + @Override + public ConcurrentHashMap getAETaskProgressMap() { + return taskProgress; + } + + @Override + public AtomicInteger getTotalAEMaxProgressAtomic() { + return totalMaxProgress; + } + + @Override + public AtomicInteger getTotalAEProgressAtomic() { + return totalProgress; + } + + @Override + public ReentrantLock getCraftingLock() { + return craftingLock; + } + + @Override + public FluidTank[] getInputFluidTanks() { + return EMPTY_TANKS; + } + + @Override + public FluidTank[] getOutputFluidTanks() { + return EMPTY_TANKS; + } + + @Override + @Nullable + public IGrid getAeGrid() { + MultiblockAlloyFurnaceCoreBlockEntity controller = getRawController(); + return controller == null ? null : controller.getAeGrid(); + } + + @Override + public AdvancedAlloyFurnaceRecipe resolveTaskRecipe( + IPatternDetails pattern, List items, List fluids, + List keys, long operations) { + IPatternDetails original = SmartDoublingPatterns.unwrap(pattern); + return original instanceof OmniversalPatternDetails omniversal ? omniversal.recipe() : null; + } + + @Override + public boolean isTaskRecipeAvailable(AdvancedAlloyFurnaceRecipe recipe) { + MultiblockAlloyFurnaceCoreBlockEntity controller = getController(); + return controller != null && controller.isTaskRecipeAvailable(recipe); + } + + @Override + public CraftingTaskContext.TaskAvailability getTaskAvailability(AdvancedAlloyFurnaceRecipe recipe) { + MultiblockAlloyFurnaceCoreBlockEntity controller = getController(); + return controller == null + ? CraftingTaskContext.TaskAvailability.unavailable( + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_structure", "") + : controller.getTaskAvailability(recipe); + } + + @Override + public ResolvedCatalystEffect resolveTaskEffect(AdvancedAlloyFurnaceRecipe recipe) { + MultiblockAlloyFurnaceCoreBlockEntity controller = getRawController(); + return controller == null + ? CraftingTaskContext.super.resolveTaskEffect(recipe) + : controller.resolveTaskEffect(recipe); + } + + @Override + public long getTaskParallel(AdvancedAlloyFurnaceRecipe recipe, ResolvedCatalystEffect effect) { + MultiblockAlloyFurnaceCoreBlockEntity controller = getRawController(); + return controller == null ? 1 : controller.getTaskParallel(recipe, effect); + } + + @Override + public boolean supportsLongAeAmounts() { + return true; + } + + @Override + public boolean isTaskExecutionEnabled() { + MultiblockAlloyFurnaceCoreBlockEntity controller = getController(); + return controller != null && controller.isTaskExecutionEnabled(); + } + + @Override + public void handleUnreturnedItem(ItemStack stack) { + AEItemKey key = AEItemKey.of(stack); + if (key != null) { + stashUnreturnedInput(key, stack.getCount()); + } + } + + @Override + public void handleUnreturnedFluid(FluidStack stack) { + AEFluidKey key = AEFluidKey.of(stack); + if (key != null) { + stashUnreturnedInput(key, stack.getAmount()); + } + } + + public enum SlotState { + EMPTY, + READY, + RUNNING, + PAUSED, + MISSING_INPUT, + MISSING_MOLD, + AE_OFFLINE, + INVALID_PATTERN, + WAITING_OUTPUT + } + + public record SlotStatus(int slot, SlotState state, int progress, int maxProgress, String detail, + long multiplier) { + public SlotStatus { + state = Objects.requireNonNull(state, "state"); + progress = Math.max(0, progress); + maxProgress = Math.max(0, maxProgress); + detail = detail == null ? "" : detail; + multiplier = Math.max(0L, multiplier); + } + + public SlotStatus(int slot, SlotState state, int progress, int maxProgress, String detail) { + this(slot, state, progress, maxProgress, detail, 0L); + } + + /** True when this slot is pinned to its own batch size instead of the global default. */ + public boolean hasOwnMultiplier() { + return multiplier > 0L; + } + } + + private record Candidate(int slot, OmniversalPatternDetails pattern, long multiplier) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/blocks/AdvancedAlloyFurnaceBlock.java b/src/main/java/com/sorrowmist/useless/content/blocks/AdvancedAlloyFurnaceBlock.java new file mode 100644 index 00000000..c972852f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/blocks/AdvancedAlloyFurnaceBlock.java @@ -0,0 +1,492 @@ +package com.sorrowmist.useless.content.blocks; + +import appeng.items.tools.quartz.QuartzCuttingKnifeItem; +import appeng.menu.MenuOpener; +import appeng.menu.locator.MenuLocators; +import appeng.util.InteractionUtil; +import com.glodblock.github.extendedae.container.ContainerRenamer; +import com.sorrowmist.useless.api.enums.FurnaceFace; +import com.sorrowmist.useless.api.enums.FurnaceFaceMode; +import com.sorrowmist.useless.api.enums.RedstoneControlMode; +import com.sorrowmist.useless.content.blockentities.AdvancedAlloyFurnaceBlockEntity; +import com.sorrowmist.useless.content.blockentities.AdvancedAlloyFurnaceData; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout; +import com.sorrowmist.useless.core.component.FurnaceDataComponent; +import com.sorrowmist.useless.core.component.ExternalInventoryKind; +import com.sorrowmist.useless.core.component.ExternalInventoryReference; +import com.sorrowmist.useless.core.component.UComponents; +import com.sorrowmist.useless.core.constants.NBTConstants; +import com.sorrowmist.useless.world.inventory.ExternalInventoryStore; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.BlockPlaceContext; +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.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.level.block.state.properties.DirectionProperty; +import net.minecraft.world.level.storage.loot.LootParams; +import net.minecraft.world.level.storage.loot.parameters.LootContextParams; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraftforge.common.capabilities.ForgeCapabilities; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.FluidUtil; +import net.minecraftforge.fluids.capability.IFluidHandler; +import net.minecraftforge.items.IItemHandler; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class AdvancedAlloyFurnaceBlock extends Block implements EntityBlock { + + private static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING; + private static final BooleanProperty ACTIVE = BooleanProperty.create("active"); + + /** + * 获取活跃状态属性(用于方块实体更新光照) + * + * @return 活跃状态属性 + */ + public static BooleanProperty getActiveProperty() { + return ACTIVE; + } + + public AdvancedAlloyFurnaceBlock() { + super(BlockBehaviour.Properties.of() + .noOcclusion() + .strength(5.0F, 32768.0F) + .requiresCorrectToolForDrops() + .lightLevel(state -> state.getValue(ACTIVE) ? 15 : 7)); + + this.registerDefaultState(this.stateDefinition.any() + .setValue(FACING, Direction.NORTH) + .setValue(ACTIVE, false)); + } + + @Override + public BlockState getStateForPlacement(BlockPlaceContext context) { + return this.defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite()); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(FACING, ACTIVE); + } + + /** + * 方块被移除时的处理 + *

+ * 掉落方块实体中的所有物品 + * + * @param state 当前状态 + * @param level 世界 + * @param pos 位置 + * @param newState 新状态 + * @param isMoving 是否在移动 + */ + @Override + public void onRemove(BlockState state, @NotNull Level level, @NotNull BlockPos pos, BlockState newState, + boolean isMoving) { + if (!state.is(newState.getBlock())) { + BlockEntity blockEntity = level.getBlockEntity(pos); + if (blockEntity instanceof AdvancedAlloyFurnaceBlockEntity furnace) { + if (!level.isClientSide) { + furnace.saveExternalInventory(); + } + // 掉落物品(不包括容器内的物品,它们会保存在方块物品中) + // 注意:使用精准采集时,物品会通过 getDrops 保存到方块物品中 + // AE 任务数据不随掉落物保存,必须在此返还材料: + // onRemove 先于 setRemoved 执行,此时 AE 节点仍然存活,材料能真正写回网络 + if (!level.isClientSide && !furnace.isDropDataCaptured()) { + furnace.cancelAllAETasks(); + } + if (!level.isClientSide) { + furnace.releaseExternalInventory(); + } + } + } + super.onRemove(state, level, pos, newState, isMoving); + } + + /** + * 获取方块掉落物 + *

+ * 将方块实体数据保存到物品的Data Component中 + * 如果熔炉全空(无物品、无能量、无流体、无模具、无进度、阶级为0),则不保存tag + */ + @Override + public @NotNull List getDrops(@NotNull BlockState state, LootParams.@NotNull Builder params) { + List drops = super.getDrops(state, params); + + // 获取方块实体 + BlockEntity blockEntity = params.getOptionalParameter(LootContextParams.BLOCK_ENTITY); + if (blockEntity instanceof AdvancedAlloyFurnaceBlockEntity furnace) { + // 为每个掉落物添加方块实体数据 + for (ItemStack drop : drops) { + if (drop.is(this.asItem())) { + Component customName = furnace.getCustomName(); + if (customName != null) { + drop.setHoverName(customName); + } + + // 检查是否全空 + boolean isEmpty = isFurnaceEmpty(furnace); + + // 如果全空且不高于0阶,不保存任何数据 + if (isEmpty && furnace.getFurnaceTier() <= 0 && !furnace.hasPersistedAETaskData()) { + continue; + } + + CompoundTag blockEntityData = new CompoundTag(); + // 保存所有数据到NBT + blockEntityData.putInt(NBTConstants.FURNACE_TIER, furnace.getFurnaceTier()); + + // 保存能量 + blockEntityData.putLong(NBTConstants.ENERGY, furnace.getEnergy()); + + // 保存流体 + for (int i = 0; i < AdvancedAlloyFurnaceLayout.FLUID_TANK_COUNT; i++) { + var inputTank = furnace.getInputFluidTank(i); + if (!inputTank.getFluid().isEmpty()) { + blockEntityData.put(NBTConstants.getInputFluidTag(i), + com.sorrowmist.useless.compat.forge1201.nbt.NativeStackNbt.saveFluid(inputTank.getFluid(), params.getLevel().registryAccess())); + } + var outputTank = furnace.getOutputFluidTank(i); + if (!outputTank.getFluid().isEmpty()) { + blockEntityData.put(NBTConstants.getOutputFluidTag(i), + com.sorrowmist.useless.compat.forge1201.nbt.NativeStackNbt.saveFluid(outputTank.getFluid(), params.getLevel().registryAccess())); + } + } + furnace.getInputChemicalStorage().save( + blockEntityData, "InputChemical", params.getLevel().registryAccess()); + furnace.getOutputChemicalStorage().save( + blockEntityData, "OutputChemical", params.getLevel().registryAccess()); + + CompoundTag aeTasksTag = new CompoundTag(); + furnace.saveAETasks(aeTasksTag, params.getLevel().registryAccess()); + blockEntityData.put("AeTasks", aeTasksTag); + + // 保存其他状态 + blockEntityData.putInt(NBTConstants.PROGRESS, furnace.getProgress()); + blockEntityData.putInt(NBTConstants.MAX_PROGRESS, furnace.getMaxProgress()); + blockEntityData.putInt(NBTConstants.CURRENT_PARALLEL, + furnace.getData().get(AdvancedAlloyFurnaceData.DATA_CURRENT_PARALLEL)); + blockEntityData.putBoolean(NBTConstants.HAS_MOLD, furnace.hasMold()); + + // 保存面模式 + FurnaceFaceMode[] faceModes = furnace.getFaceModes(); + blockEntityData.putInt("FaceModeTop", faceModes[FurnaceFace.TOP.ordinal()].ordinal()); + blockEntityData.putInt("FaceModeBottom", faceModes[FurnaceFace.BOTTOM.ordinal()].ordinal()); + blockEntityData.putInt("FaceModeFront", faceModes[FurnaceFace.FRONT.ordinal()].ordinal()); + blockEntityData.putInt("FaceModeBack", faceModes[FurnaceFace.BACK.ordinal()].ordinal()); + blockEntityData.putInt("FaceModeLeft", faceModes[FurnaceFace.LEFT.ordinal()].ordinal()); + blockEntityData.putInt("FaceModeRight", faceModes[FurnaceFace.RIGHT.ordinal()].ordinal()); + + // 保存自动输入输出开关 + blockEntityData.putBoolean("AutoInputEnabled", furnace.isAutoInputEnabled()); + blockEntityData.putBoolean("AutoOutputEnabled", furnace.isAutoOutputEnabled()); + + // 保存红石控制模式 + blockEntityData.putInt("RedstoneControlMode", furnace.getRedstoneControlMode().ordinal()); + + ExternalInventoryReference inventoryReference = furnace.getExternalInventoryReference(); + if (inventoryReference != null) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(drop, UComponents.EXTERNAL_INVENTORY_REFERENCE.get(), inventoryReference); + } + + // 使用Data Component存储数据 + FurnaceDataComponent component = new FurnaceDataComponent( + furnace.getFurnaceTier(), + blockEntityData + ); + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(drop, UComponents.FURNACE_DATA.get(), component); + furnace.markDropDataCaptured(); + break; + } + } + } + + return drops; + } + + /** + * 检查熔炉是否全空 + * @param furnace 熔炉方块实体 + * @return 如果无物品、无能量、无流体、无模具、无进度返回true + */ + private static boolean isFurnaceEmpty(AdvancedAlloyFurnaceBlockEntity furnace) { + // 检查物品栏 + IItemHandler itemHandler = furnace.getItemHandler(); + if (itemHandler != null) { + for (int i = 0; i < itemHandler.getSlots(); i++) { + if (!itemHandler.getStackInSlot(i).isEmpty()) { + return false; + } + } + } + + // 检查能量 + if (furnace.getEnergy() > 0) { + return false; + } + + // 检查流体 + for (int i = 0; i < AdvancedAlloyFurnaceLayout.FLUID_TANK_COUNT; i++) { + if (!furnace.getInputFluidTank(i).getFluid().isEmpty()) { + return false; + } + if (!furnace.getOutputFluidTank(i).getFluid().isEmpty()) { + return false; + } + } + + // 检查模具 + if (furnace.hasChemicalSupport()) { + for (int i = 0; i < AdvancedAlloyFurnaceLayout.CHEMICAL_TANK_COUNT; i++) { + if (!furnace.getInputChemical(i).isEmpty() || !furnace.getOutputChemical(i).isEmpty()) { + return false; + } + } + } + + if (furnace.hasMold()) { + return false; + } + + // 检查进度 + if (furnace.getProgress() > 0) { + return false; + } + + return true; + } + + /** + * 空手右键点击方块的处理 + *

+ * 打开方块 GUI + * + * @param state 方块状态 + * @param level 世界 + * @param pos 位置 + * @param player 玩家 + * @param hitResult 命中结果 + * @return 交互结果 + */ + @Override + @SuppressWarnings("DataFlowIssue") + public @NotNull InteractionResult use( + @NotNull BlockState state, + Level level, + @NotNull BlockPos pos, + @NotNull Player player, + @NotNull InteractionHand hand, + @NotNull BlockHitResult hitResult) { + ItemStack stack = player.getItemInHand(hand); + BlockEntity blockEntity = level.getBlockEntity(pos); + if (!(blockEntity instanceof AdvancedAlloyFurnaceBlockEntity furnace)) { + return InteractionResult.PASS; + } + + if (!InteractionUtil.isInAlternateUseMode(player) && stack.getItem() instanceof QuartzCuttingKnifeItem) { + if (!level.isClientSide) { + MenuOpener.open(ContainerRenamer.TYPE, player, MenuLocators.forBlockEntity(furnace)); + } + return InteractionResult.sidedSuccess(level.isClientSide); + } + + if (level.isClientSide) { + return InteractionResult.SUCCESS; + } + + // 流体交互 + IFluidHandler fluidHandler = com.sorrowmist.useless.compat.forge1201.capability.CapabilityAccess.get(level, ForgeCapabilities.FLUID_HANDLER, pos, null); + if (fluidHandler != null && isFluidContainer(stack)) { + boolean success = FluidUtil.interactWithFluidHandler(player, hand, fluidHandler); + if (success) { + return InteractionResult.SUCCESS; + } + } + + // 打开GUI + net.minecraftforge.network.NetworkHooks.openScreen((net.minecraft.server.level.ServerPlayer) player, furnace, pos); + return InteractionResult.CONSUME; + } + + /** + * 检查物品是否是流体容器 + * + * @param stack 物品堆 + * @return 如果是流体容器返回true + */ + private static boolean isFluidContainer(ItemStack stack) { + if (stack.isEmpty()) return false; + return com.sorrowmist.useless.compat.forge1201.capability.CapabilityAccess.get(stack, ForgeCapabilities.FLUID_HANDLER_ITEM) != null + || FluidUtil.getFluidContained(stack).isPresent(); + } + + @Override + public @Nullable MenuProvider getMenuProvider(@NotNull BlockState state, Level level, @NotNull BlockPos pos) { + BlockEntity blockEntity = level.getBlockEntity(pos); + return blockEntity instanceof MenuProvider ? (MenuProvider) blockEntity : null; + } + + @Nullable + @Override + public BlockEntity newBlockEntity(@NotNull BlockPos pos, @NotNull BlockState state) { + return new AdvancedAlloyFurnaceBlockEntity(pos, state); + } + + @Nullable + @Override + public BlockEntityTicker getTicker(Level level, + @NotNull BlockState state, + @NotNull BlockEntityType type) { + return level.isClientSide ? null : (lvl, pos, st, be) -> { + if (be instanceof AdvancedAlloyFurnaceBlockEntity furnace) { + AdvancedAlloyFurnaceBlockEntity.tick(lvl, furnace); + } + }; + } + + /** + * 方块被放置时的处理 + *

+ * 从物品的Data Component中恢复方块实体数据 + */ + @Override + public void setPlacedBy(@NotNull Level level, @NotNull BlockPos pos, @NotNull BlockState state, + @Nullable net.minecraft.world.entity.LivingEntity placer, @NotNull ItemStack stack) { + super.setPlacedBy(level, pos, state, placer, stack); + + if (level.isClientSide) return; + + BlockEntity placedBlockEntity = level.getBlockEntity(pos); + if (placedBlockEntity instanceof AdvancedAlloyFurnaceBlockEntity placedFurnace) { + Component customName = stack.hasCustomHoverName() ? stack.getHoverName() : null; + if (customName != null) { + placedFurnace.setName(customName.getString()); + } + } + + // 从Data Component中恢复数据 + FurnaceDataComponent component = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(stack, UComponents.FURNACE_DATA.get()); + ExternalInventoryReference requestedReference = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(stack, UComponents.EXTERNAL_INVENTORY_REFERENCE.get()); + if (component != null) { + CompoundTag blockEntityData = component.data(); + BlockEntity blockEntity = level.getBlockEntity(pos); + + if (blockEntity instanceof AdvancedAlloyFurnaceBlockEntity furnace) { + // 恢复阶级(这会触发容量更新) + int tier = component.tier(); + if (tier > 0) { + furnace.tryUpgrade(tier); + } + + furnace.bindExternalInventory( + requestedReference, + blockEntityData.contains(NBTConstants.INVENTORY) + ? blockEntityData.getCompound(NBTConstants.INVENTORY) : null, + level.registryAccess()); + + // 恢复能量 + if (blockEntityData.contains(NBTConstants.ENERGY)) { + furnace.setEnergy(blockEntityData.getLong(NBTConstants.ENERGY)); + } + + // 恢复流体 + for (int i = 0; i < AdvancedAlloyFurnaceLayout.FLUID_TANK_COUNT; i++) { + String inputTag = NBTConstants.getInputFluidTag(i); + if (blockEntityData.contains(inputTag)) { + FluidStack fluid = com.sorrowmist.useless.compat.forge1201.nbt.NativeStackNbt.readFluid( + level.registryAccess(), blockEntityData.getCompound(inputTag)); + if (!fluid.isEmpty()) { + furnace.getInputFluidTank(i).setFluid(fluid); + } + } + + String outputTag = NBTConstants.getOutputFluidTag(i); + if (blockEntityData.contains(outputTag)) { + FluidStack fluid = com.sorrowmist.useless.compat.forge1201.nbt.NativeStackNbt.readFluid( + level.registryAccess(), blockEntityData.getCompound(outputTag)); + if (!fluid.isEmpty()) { + furnace.getOutputFluidTank(i).setFluid(fluid); + } + } + } + + // 恢复其他状态 + furnace.getInputChemicalStorage().load( + blockEntityData, "InputChemical", level.registryAccess()); + furnace.getOutputChemicalStorage().load( + blockEntityData, "OutputChemical", level.registryAccess()); + furnace.readAETasks(blockEntityData); + + if (blockEntityData.contains(NBTConstants.PROGRESS)) { + furnace.setProgress(blockEntityData.getInt(NBTConstants.PROGRESS)); + } + if (blockEntityData.contains(NBTConstants.MAX_PROGRESS)) { + furnace.setMaxProgress(blockEntityData.getInt(NBTConstants.MAX_PROGRESS)); + } + if (blockEntityData.contains(NBTConstants.HAS_MOLD)) { + furnace.setHasMold(blockEntityData.getBoolean(NBTConstants.HAS_MOLD)); + } + + // 恢复面模式 + if (blockEntityData.contains("FaceModeTop")) { + furnace.setFaceMode(FurnaceFace.TOP, FurnaceFaceMode.byIndex(blockEntityData.getInt("FaceModeTop"))); + } + if (blockEntityData.contains("FaceModeBottom")) { + furnace.setFaceMode(FurnaceFace.BOTTOM, FurnaceFaceMode.byIndex(blockEntityData.getInt("FaceModeBottom"))); + } + if (blockEntityData.contains("FaceModeFront")) { + furnace.setFaceMode(FurnaceFace.FRONT, FurnaceFaceMode.byIndex(blockEntityData.getInt("FaceModeFront"))); + } + if (blockEntityData.contains("FaceModeBack")) { + furnace.setFaceMode(FurnaceFace.BACK, FurnaceFaceMode.byIndex(blockEntityData.getInt("FaceModeBack"))); + } + if (blockEntityData.contains("FaceModeLeft")) { + furnace.setFaceMode(FurnaceFace.LEFT, FurnaceFaceMode.byIndex(blockEntityData.getInt("FaceModeLeft"))); + } + if (blockEntityData.contains("FaceModeRight")) { + furnace.setFaceMode(FurnaceFace.RIGHT, FurnaceFaceMode.byIndex(blockEntityData.getInt("FaceModeRight"))); + } + + // 恢复自动输入输出开关 + if (blockEntityData.contains("AutoInputEnabled")) { + furnace.setAutoInputEnabled(blockEntityData.getBoolean("AutoInputEnabled")); + } + if (blockEntityData.contains("AutoOutputEnabled")) { + furnace.setAutoOutputEnabled(blockEntityData.getBoolean("AutoOutputEnabled")); + } + + // 恢复红石控制模式 + if (blockEntityData.contains("RedstoneControlMode")) { + furnace.setRedstoneControlMode(RedstoneControlMode.byIndex(blockEntityData.getInt("RedstoneControlMode"))); + } + + furnace.setChanged(); + } + } else if (placedBlockEntity instanceof AdvancedAlloyFurnaceBlockEntity furnace) { + furnace.bindExternalInventory(requestedReference, null, level.registryAccess()); + } + if (placedBlockEntity instanceof AdvancedAlloyFurnaceBlockEntity furnace) { + ExternalInventoryStore.clearLegacyComponent(furnace, ExternalInventoryKind.FURNACE); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/blocks/GlowPlasticBlock.java b/src/main/java/com/sorrowmist/useless/content/blocks/GlowPlasticBlock.java new file mode 100644 index 00000000..7d91c5ec --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/blocks/GlowPlasticBlock.java @@ -0,0 +1,127 @@ +package com.sorrowmist.useless.content.blocks; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.api.enums.EnumColor; +import com.sorrowmist.useless.content.items.EndlessBeafItem; +import com.sorrowmist.useless.utils.mining.MiningUtils; +import net.minecraft.core.BlockPos; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; + +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.Item; +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.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; +import com.sorrowmist.useless.compat.forge1201.registry.DeferredBlock; +import com.sorrowmist.useless.compat.forge1201.registry.DeferredItem; +import com.sorrowmist.useless.compat.forge1201.registry.DeferredRegister; +import org.jetbrains.annotations.NotNull; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class GlowPlasticBlock extends Block implements IColoredBlock { + + public static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks(UselessMod.MODID); + public static final DeferredRegister.Items ITEMS = DeferredRegister.createItems(UselessMod.MODID); + + public static final Map> GLOW_PLASTIC_BLOCKS = new LinkedHashMap<>(); + public static final Map> GLOW_PLASTIC_BLOCK_ITEMS = new LinkedHashMap<>(); + public static final Map> PLASTIC_BLOCKS = new LinkedHashMap<>(); + public static final Map> PLASTIC_BLOCK_ITEMS = new LinkedHashMap<>(); + public static final Map> PLASTIC_CTM_BLOCKS = new LinkedHashMap<>(); + public static final Map> PLASTIC_CTM_BLOCK_ITEMS = new LinkedHashMap<>(); + public static final Map> GLOW_PLASTIC_CTM_BLOCKS = new LinkedHashMap<>(); + public static final Map> GLOW_PLASTIC_CTM_BLOCK_ITEMS = new LinkedHashMap<>(); + + public static final List>> ALL_BLOCK_MAPS = List.of( + PLASTIC_BLOCKS, GLOW_PLASTIC_BLOCKS, PLASTIC_CTM_BLOCKS, GLOW_PLASTIC_CTM_BLOCKS); + public static final List>> ALL_BLOCK_ITEM_MAPS = List.of( + PLASTIC_BLOCK_ITEMS, GLOW_PLASTIC_BLOCK_ITEMS, + PLASTIC_CTM_BLOCK_ITEMS, GLOW_PLASTIC_CTM_BLOCK_ITEMS); + + static { + for (EnumColor color : EnumColor.valuesInOrder()) { + registerVariant(color, color.getRegistryPrefix() + "_glow_plastic", + true, false, GLOW_PLASTIC_BLOCKS, GLOW_PLASTIC_BLOCK_ITEMS); + registerVariant(color, color.getRegistryPrefix() + "_plastic", + false, false, PLASTIC_BLOCKS, PLASTIC_BLOCK_ITEMS); + registerVariant(color, color.getRegistryPrefix() + "_plastic_ctm", + false, true, PLASTIC_CTM_BLOCKS, PLASTIC_CTM_BLOCK_ITEMS); + registerVariant(color, color.getRegistryPrefix() + "_glow_plastic_ctm", + true, true, GLOW_PLASTIC_CTM_BLOCKS, GLOW_PLASTIC_CTM_BLOCK_ITEMS); + } + } + + private final EnumColor color; + private final boolean glowing; + private final boolean connectedTexture; + + private GlowPlasticBlock(EnumColor color, boolean glowing, boolean connectedTexture) { + super(createProperties(color, glowing)); + this.color = color; + this.glowing = glowing; + this.connectedTexture = connectedTexture; + } + + private static BlockBehaviour.Properties createProperties(EnumColor color, boolean glowing) { + BlockBehaviour.Properties properties = BlockBehaviour.Properties.of() + .mapColor(color.getMapColor()) + .strength(5F, 6F) + .requiresCorrectToolForDrops(); + return glowing ? properties.lightLevel(state -> 15) : properties; + } + + @Override + public @NotNull InteractionResult use(@NotNull BlockState state, + @NotNull Level level, + @NotNull BlockPos pos, + Player player, + @NotNull InteractionHand hand, + @NotNull BlockHitResult hit) { + ItemStack heldItem=player.getItemInHand(hand); + if (player.isShiftKeyDown()) { + if (!level.isClientSide()) { + if (heldItem.getItem() instanceof EndlessBeafItem) { + MiningUtils.quickBreakBlock(level, pos, state, player, heldItem); + } + } + return InteractionResult.sidedSuccess(level.isClientSide()); + } + return super.use(state,level,pos,player,hand,hit); + } + + @Override + public EnumColor getColor() { + return this.color; + } + + public boolean isGlowing() { + return this.glowing; + } + + public boolean hasConnectedTexture() { + return this.connectedTexture; + } + + private static void registerVariant( + EnumColor color, String registryName, boolean glowing, boolean connectedTexture, + Map> blocks, + Map> items) { + DeferredBlock block = BLOCKS.register( + registryName, + () -> new GlowPlasticBlock(color, glowing, connectedTexture)); + blocks.put(color, block); + + DeferredItem item = ITEMS.register( + registryName, + () -> new BlockItem(block.get(), new Item.Properties())); + items.put(color, item); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/blocks/IColoredBlock.java b/src/main/java/com/sorrowmist/useless/content/blocks/IColoredBlock.java new file mode 100644 index 00000000..724b0e54 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/blocks/IColoredBlock.java @@ -0,0 +1,7 @@ +package com.sorrowmist.useless.content.blocks; + +import com.sorrowmist.useless.api.enums.EnumColor; + +public interface IColoredBlock { + EnumColor getColor(); +} diff --git a/src/main/java/com/sorrowmist/useless/content/blocks/OreGeneratorBlock.java b/src/main/java/com/sorrowmist/useless/content/blocks/OreGeneratorBlock.java new file mode 100644 index 00000000..cc1a6383 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/blocks/OreGeneratorBlock.java @@ -0,0 +1,67 @@ +package com.sorrowmist.useless.content.blocks; + +import com.sorrowmist.useless.content.blockentities.OreGeneratorBlockEntity; +import com.sorrowmist.useless.init.ModBlockEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; + +import net.minecraft.world.MenuProvider; +import net.minecraft.world.entity.player.Player; +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.phys.BlockHitResult; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class OreGeneratorBlock extends Block implements EntityBlock { + public OreGeneratorBlock() { + super(Properties.of() + .strength(3.0F, 32768.0F) + .requiresCorrectToolForDrops()); + } + + @Override + public void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean isMoving) { + if (!state.is(newState.getBlock()) + && level.getBlockEntity(pos) instanceof OreGeneratorBlockEntity generator) { + generator.drops(); + } + super.onRemove(state, level, pos, newState, isMoving); + } + + + @Override + public @NotNull InteractionResult use( + BlockState state, Level level, BlockPos pos, Player player, + InteractionHand hand, BlockHitResult hitResult) { + openMenu(level, pos, player); + return InteractionResult.sidedSuccess(level.isClientSide); + } + + private static void openMenu(Level level, BlockPos pos, Player player) { + if (!level.isClientSide && level.getBlockEntity(pos) instanceof MenuProvider provider) { + net.minecraftforge.network.NetworkHooks.openScreen((net.minecraft.server.level.ServerPlayer)player,provider,pos); + } + } + + @Nullable + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new OreGeneratorBlockEntity(pos, state); + } + + @Override + public @Nullable BlockEntityTicker getTicker( + Level level, BlockState state, BlockEntityType blockEntityType) { + return level.isClientSide ? null + : blockEntityType == ModBlockEntities.ORE_GENERATOR.get() + ? (lvl, pos, st, be) -> ((OreGeneratorBlockEntity) be).tick() + : null; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/blocks/SupervisorBlock.java b/src/main/java/com/sorrowmist/useless/content/blocks/SupervisorBlock.java new file mode 100644 index 00000000..a1d6b12d --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/blocks/SupervisorBlock.java @@ -0,0 +1,62 @@ +package com.sorrowmist.useless.content.blocks; + +import com.sorrowmist.useless.init.ModSounds; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.DirectionProperty; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.jetbrains.annotations.NotNull; + +public final class SupervisorBlock extends Block { + static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING; + private static final VoxelShape SHAPE = Block.box(0.0, 0.0, 0.0, 16.0, 16.0, 16.0); + + public SupervisorBlock(BlockBehaviour.Properties properties) { + super(properties); + registerDefaultState(stateDefinition.any().setValue(FACING, Direction.NORTH)); + } + + @Override + public BlockState getStateForPlacement(BlockPlaceContext context) { + return defaultBlockState().setValue(FACING, facingForPlacement(context.getHorizontalDirection())); + } + + static Direction facingForPlacement(Direction playerDirection) { + return playerDirection.getOpposite(); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(FACING); + } + + @Override + public @NotNull InteractionResult use(@NotNull BlockState state, + @NotNull Level level, + @NotNull BlockPos pos, + @NotNull Player player, + net.minecraft.world.InteractionHand hand, @NotNull BlockHitResult hitResult) { + if (!level.isClientSide) { + level.playSound(null, pos, ModSounds.HOWL.get(), SoundSource.BLOCKS, 1.0F, 1.0F); + } + return InteractionResult.sidedSuccess(level.isClientSide); + } + + @Override + public VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + return SHAPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/blocks/TeleportPadBlock.java b/src/main/java/com/sorrowmist/useless/content/blocks/TeleportPadBlock.java new file mode 100644 index 00000000..c6e1be99 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/blocks/TeleportPadBlock.java @@ -0,0 +1,56 @@ +package com.sorrowmist.useless.content.blocks; + +import com.sorrowmist.useless.world.teleport.AbstractDimensionTeleporter; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; + +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.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; +import java.util.function.Supplier; + +public class TeleportPadBlock extends Block { + private final Supplier teleporterSupplier; + + public TeleportPadBlock(Supplier teleporterSupplier, BlockBehaviour.Properties properties) { + super(properties); + this.teleporterSupplier = teleporterSupplier; + } + + @Override + public @NotNull InteractionResult use(@NotNull BlockState state, + @NotNull Level level, + @NotNull BlockPos pos, + @NotNull Player player, + @NotNull InteractionHand hand, + @NotNull BlockHitResult hitResult) { + ItemStack stack=player.getItemInHand(hand); + handleUse(level, pos, player); + return InteractionResult.sidedSuccess(level.isClientSide()); + } + + + private void handleUse(Level level, BlockPos pos, Player player) { + if (!level.isClientSide() && player instanceof ServerPlayer serverPlayer) { + boolean editConfiguration = serverPlayer.isShiftKeyDown(); + Objects.requireNonNull(level.getServer()).execute(() -> { + AbstractDimensionTeleporter teleporter = teleporterSupplier.get(); + if (editConfiguration) { + com.sorrowmist.useless.content.menus.DimensionConfigMenu.openForEdit( + serverPlayer, teleporter, pos); + } else { + teleporter.handleTeleport(serverPlayer, pos); + } + }); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/blocks/TemperatureRegulatorBlock.java b/src/main/java/com/sorrowmist/useless/content/blocks/TemperatureRegulatorBlock.java new file mode 100644 index 00000000..43f19f37 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/blocks/TemperatureRegulatorBlock.java @@ -0,0 +1,153 @@ +package com.sorrowmist.useless.content.blocks; + +import com.sorrowmist.useless.content.blockentities.ThermostatBlockEntity; +import com.sorrowmist.useless.content.blockentities.ThermostatHeatHook; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +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.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.level.block.state.properties.IntegerProperty; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * 「温度调节器」方块:可配置的无限恒温源。 + * + *

功能从原塑料方块上剥离而来:用权杖 Alt + 右键 打开配置界面设定温度 / 开关。 + * 温度单位是开尔文,默认 300 K。

+ * + *

外观与为什么颜色要写进 BlockState

+ * + *

模型由两层构成:外框(不染色)+ 内板({@code tintindex: 0},按温度染色)。染色公式移植自 + * 气动工艺压缩铁块(见 {@code TemperatureColors})。

+ * + *

⛔ 关键约束:染色回调里拿不到 BlockEntity。 区块网格是在 {@code RenderChunkRegion} + * 上烘焙的,它的 {@code getBlockEntity} 恒为 {@code null};因此在渲染期去查 BE 只会永远回落 + * 到默认温度(表现为「不管设多少度都是同一个颜色」)。渲染期唯一可靠的信息就是 {@link BlockState} + * 本身,所以这里把温度量化成档位存进状态,由 {@link ThermostatBlockEntity} 在温度 / 开关 + * 变化时同步进来;染色回调只读状态。

+ * + *

档位数量取 256:一是让 273~323 K 的「室温白」区间足够宽、默认 300 K 不会因为量化误差 + * 掉进青蓝色区;二是 256 × 2(开关)个状态与原版红石线(1296 个)同量级,完全可以接受。

+ * + *

传热

+ * + *

气动工艺是接收方驱动(机器自己会向我们收敛),只需暴露热能力;Mekanism 是发送方驱动, + * 必须由本方块每 tick 主动推热——见 {@link ThermostatHeatHook}。未开启({@code enabled == false}) + * 时 tick 首行即返回,几乎零开销。

+ */ +public class TemperatureRegulatorBlock extends Block implements EntityBlock { + + /** 温度档位总数(含 0)。越大颜色越平滑,代价是更多的方块状态组合。 */ + public static final int TEMPERATURE_LEVELS = 256; + + /** + * 档位映射的饱和温度(K)。 + * + *

超过这个温度后档位一律取满。取 10 000 K 是为了让 0~2273 K 这段真正会被气动 / Mek + * 用到的区间分到足够多的档位(约 58 档),高温端留出余量只是为了让「极端数值」有个确定的 + * 归宿,不是为了让颜色继续变化——气动取色公式在 2273 K 就已经走到黄色,再高不会再变。

+ */ + public static final long SATURATION_KELVIN = 10_000L; + + /** 当前温度所处的档位。染色回调读它,再换算回温度取色。 */ + public static final IntegerProperty TEMPERATURE_LEVEL = + IntegerProperty.create("temperature_level", 0, TEMPERATURE_LEVELS - 1); + + /** 恒温能力是否开启。关闭时内板不着色(保持贴图原色)。 */ + public static final BooleanProperty ENABLED = BooleanProperty.create("enabled"); + + /** 开始自发光的温度(K)。低于此温度不发光。 */ + public static final long LIGHT_START_KELVIN = 500L; + + /** 自发光达到满亮度(15)的温度(K)。 */ + public static final long LIGHT_FULL_KELVIN = 5000L; + + public TemperatureRegulatorBlock(BlockBehaviour.Properties properties) { + super(properties); + registerDefaultState(defaultBlockState() + .setValue(TEMPERATURE_LEVEL, levelForTemperature(ThermostatBlockEntity.DEFAULT_TEMPERATURE)) + .setValue(ENABLED, false)); + } + + /** 温度(K)→ 档位。超出 {@link #SATURATION_KELVIN} 时取满。 */ + public static int levelForTemperature(long kelvin) { + long clamped = Math.max(0L, Math.min(SATURATION_KELVIN, kelvin)); + return (int) (clamped * (TEMPERATURE_LEVELS - 1) / SATURATION_KELVIN); + } + + /** 档位 → 代表温度(K),用于取色。与 {@link #levelForTemperature} 互为近似逆运算。 */ + public static long temperatureForLevel(int level) { + return (long) level * SATURATION_KELVIN / (TEMPERATURE_LEVELS - 1); + } + + /** 由温度 / 开关推出对应的方块状态(保留 base 上的其它属性)。 */ + public static BlockState stateFor(BlockState base, long kelvin, boolean enabled) { + return base.setValue(TEMPERATURE_LEVEL, levelForTemperature(kelvin)) + .setValue(ENABLED, enabled); + } + + /** + * 按温度取自发光亮度(0~15),供 {@code BlockBehaviour.Properties#lightLevel} 使用。 + * + *

没有自发光的高温方块会被世界环境光压暗,看上去「不热」——这是光靠提亮贴图解决不了的。 + * 这里让温度越高越亮:{@link #LIGHT_START_KELVIN} 以下不发光(冷 / 室温),到 + * {@link #LIGHT_FULL_KELVIN} 达到满亮度 15;关闭时恒为 0。

+ */ + public static int lightForState(BlockState state) { + if (!state.getValue(ENABLED)) { + return 0; + } + long kelvin = temperatureForLevel(state.getValue(TEMPERATURE_LEVEL)); + if (kelvin <= LIGHT_START_KELVIN) { + return 0; + } + float ramp = Math.min(1f, (kelvin - LIGHT_START_KELVIN) + / (float) (LIGHT_FULL_KELVIN - LIGHT_START_KELVIN)); + return Math.round(ramp * 15f); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(TEMPERATURE_LEVEL, ENABLED); + } + + @Nullable + @Override + public BlockEntity newBlockEntity(@NotNull BlockPos pos, @NotNull BlockState state) { + return new ThermostatBlockEntity(pos, state); + } + + /** + * 恒温源的「主动驱动」tick(仅服务端)。 + * + *

Mekanism 的传热是发送方驱动(见 {@link ThermostatHeatHook}),所以必须由方块自己 + * 每 tick 把热推给邻居。未配置({@code enabled == false})时直接返回。

+ */ + @Nullable + @Override + public BlockEntityTicker getTicker(@NotNull Level level, + @NotNull BlockState state, + @NotNull BlockEntityType type) { + if (level.isClientSide) { + return null; + } + return (tickLevel, pos, tickState, blockEntity) -> { + if (!(blockEntity instanceof ThermostatBlockEntity thermostat) || !thermostat.isEnabled()) { + return; + } + ThermostatHeatHook.HeatDriver driver = ThermostatHeatHook.driver; + if (driver != null && tickLevel instanceof ServerLevel serverLevel) { + driver.tick(serverLevel, pos, thermostat); + } + }; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/blocks/UselessGlassBlock.java b/src/main/java/com/sorrowmist/useless/content/blocks/UselessGlassBlock.java new file mode 100644 index 00000000..b7cad991 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/blocks/UselessGlassBlock.java @@ -0,0 +1,83 @@ +package com.sorrowmist.useless.content.blocks; + + +import com.sorrowmist.useless.content.items.EndlessBeafItem; +import com.sorrowmist.useless.utils.mining.MiningUtils; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; + +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.SoundType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.NoteBlockInstrument; +import net.minecraft.world.phys.BlockHitResult; +import org.jetbrains.annotations.NotNull; + +/** + * 无用玻璃方块抽象基类 + * 特性: + * 1. 防爆(高爆炸抗性) + * 2. Shift+右键快速破坏并直接进入背包 + * 3. 透光(类似原版玻璃) + */ +public abstract class UselessGlassBlock extends Block { + + protected UselessGlassBlock(Properties properties) { + super(properties.instrument(NoteBlockInstrument.HAT) + .sound(SoundType.GLASS) + .noOcclusion() // 不完全遮挡视线 + // 防止生物在玻璃上刷出 + .isValidSpawn((state, getter, pos, entityType) -> false) + // 红石不导电 + .isRedstoneConductor((state, getter, pos) -> false) + // 玩家不会窒息 + .isSuffocating((state, getter, pos) -> false) + // 不阻挡视线 + .isViewBlocking((state, getter, pos) -> false)); + } + + /** + * 跳过相邻同类方块的渲染面(连接纹理效果) + */ + @Override + public boolean skipRendering(@NotNull BlockState state, + BlockState adjacentBlockState, + @NotNull Direction direction) { + return adjacentBlockState.getBlock() instanceof UselessGlassBlock; + } + + @Override + public @NotNull InteractionResult use(@NotNull BlockState state, + @NotNull Level level, + @NotNull BlockPos pos, + Player player, + @NotNull InteractionHand hand, + @NotNull BlockHitResult hit) { + ItemStack heldItem=player.getItemInHand(hand); + if (player.isShiftKeyDown()) { + if (!level.isClientSide()) { + if (heldItem.getItem() instanceof EndlessBeafItem){ + MiningUtils.quickBreakBlock(level, pos, state, player, heldItem); + } + } + return InteractionResult.sidedSuccess(level.isClientSide()); + } + return super.use(state,level,pos,player,hand,hit); + } + + @Override + public int getLightBlock(@NotNull BlockState state, @NotNull BlockGetter level, @NotNull BlockPos pos) { + return 0; // 0 表示完全透光(不衰减光线) + } + + @Override + public float getShadeBrightness(@NotNull BlockState state, @NotNull BlockGetter level, @NotNull BlockPos pos) { + return 1.0F; // 保证方块下方的阴影不会过黑 + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/blocks/multiblock/DirectionalMultiblockPartBlock.java b/src/main/java/com/sorrowmist/useless/content/blocks/multiblock/DirectionalMultiblockPartBlock.java new file mode 100644 index 00000000..a8def265 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/blocks/multiblock/DirectionalMultiblockPartBlock.java @@ -0,0 +1,45 @@ +package com.sorrowmist.useless.content.blocks.multiblock; + +import net.minecraft.core.Direction; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.DirectionProperty; + +public class DirectionalMultiblockPartBlock extends MultiblockPartBlock { + public static final DirectionProperty FACING = BlockStateProperties.FACING; + + public DirectionalMultiblockPartBlock(Properties properties) { + super(properties); + registerDefaultState(stateDefinition.any().setValue(FACING, Direction.NORTH)); + } + + @Override + public BlockState getStateForPlacement(BlockPlaceContext context) { + return defaultBlockState().setValue(FACING, + facingForPlacement(context.getNearestLookingDirection())); + } + + static Direction facingForPlacement(Direction nearestLookingDirection) { + return nearestLookingDirection.getOpposite(); + } + + @Override + public BlockState rotate(BlockState state, Rotation rotation) { + return state.setValue(FACING, rotation.rotate(state.getValue(FACING))); + } + + @Override + public BlockState mirror(BlockState state, Mirror mirror) { + return rotate(state, mirror.getRotation(state.getValue(FACING))); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(FACING); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/blocks/multiblock/MePatternAssemblyBlock.java b/src/main/java/com/sorrowmist/useless/content/blocks/multiblock/MePatternAssemblyBlock.java new file mode 100644 index 00000000..06529cfd --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/blocks/multiblock/MePatternAssemblyBlock.java @@ -0,0 +1,112 @@ +package com.sorrowmist.useless.content.blocks.multiblock; + +import appeng.items.tools.quartz.QuartzCuttingKnifeItem; +import appeng.menu.MenuOpener; +import appeng.menu.locator.MenuLocators; +import appeng.util.InteractionUtil; +import com.glodblock.github.extendedae.container.ContainerRenamer; +import com.sorrowmist.useless.content.blockentities.multiblock.MePatternAssemblyBlockEntity; +import com.sorrowmist.useless.core.component.ExternalInventoryKind; +import com.sorrowmist.useless.core.component.ExternalInventoryReference; +import com.sorrowmist.useless.core.component.MultiblockPartData; +import com.sorrowmist.useless.core.component.UComponents; +import com.sorrowmist.useless.world.inventory.ExternalInventoryStore; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.loot.LootParams; +import net.minecraft.world.level.storage.loot.parameters.LootContextParams; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public final class MePatternAssemblyBlock extends DirectionalMultiblockPartBlock implements EntityBlock { + public MePatternAssemblyBlock(Properties properties) { + super(properties); + } + + @Override + public @Nullable BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new MePatternAssemblyBlockEntity(pos, state); + } + + @Override + public void onRemove(BlockState state, Level level, BlockPos pos, + BlockState newState, boolean movedByPiston) { + if (!state.is(newState.getBlock()) && !level.isClientSide + && level.getBlockEntity(pos) instanceof MePatternAssemblyBlockEntity assembly) { + assembly.releaseExternalInventory(); + } + super.onRemove(state, level, pos, newState, movedByPiston); + } + + @Override + public List getDrops(BlockState state, LootParams.Builder params) { + List drops = super.getDrops(state, params); + if (params.getOptionalParameter(LootContextParams.BLOCK_ENTITY) + instanceof MePatternAssemblyBlockEntity assembly) { + ExternalInventoryReference reference = assembly.getExternalInventoryReference(); + Component customName = assembly.getCustomName(); + for (ItemStack drop : drops) { + if (drop.is(asItem())) { + if (reference != null) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(drop, UComponents.EXTERNAL_INVENTORY_REFERENCE.get(), reference); + } + if (customName != null) { + drop.setHoverName(customName); + } + break; + } + } + } + return drops; + } + + @Override + public void setPlacedBy(Level level, BlockPos pos, BlockState state, + @Nullable LivingEntity placer, ItemStack stack) { + super.setPlacedBy(level, pos, state, placer, stack); + if (!level.isClientSide && level.getBlockEntity(pos) instanceof MePatternAssemblyBlockEntity assembly) { + ExternalInventoryReference reference = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(stack, UComponents.EXTERNAL_INVENTORY_REFERENCE.get()); + MultiblockPartData itemData = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(stack, UComponents.MULTIBLOCK_PART_DATA.get()); + assembly.bindExternalInventory(reference, + itemData == null ? null : itemData.inventory(), level.registryAccess()); + ExternalInventoryStore.clearLegacyComponent(assembly, ExternalInventoryKind.PATTERN_ASSEMBLY); + Component customName = stack.hasCustomHoverName() ? stack.getHoverName() : null; + if (customName != null) { + assembly.setName(customName.getString()); + } + } + } + + @Override + public @NotNull InteractionResult use( @NotNull BlockState state, + Level level, @NotNull BlockPos pos, + @NotNull Player player, @NotNull InteractionHand hand, + @NotNull BlockHitResult hitResult) { + ItemStack stack = player.getItemInHand(hand); + if (level.getBlockEntity(pos) instanceof MePatternAssemblyBlockEntity assembly + && !InteractionUtil.isInAlternateUseMode(player) + && stack.getItem() instanceof QuartzCuttingKnifeItem) { + if (!level.isClientSide) { + MenuOpener.open(ContainerRenamer.TYPE, player, MenuLocators.forBlockEntity(assembly)); + } + return InteractionResult.sidedSuccess(level.isClientSide); + } + if (!level.isClientSide && level.getBlockEntity(pos) instanceof MenuProvider provider) { + net.minecraftforge.network.NetworkHooks.openScreen((net.minecraft.server.level.ServerPlayer) player, provider, pos); + } + return InteractionResult.sidedSuccess(level.isClientSide); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/blocks/multiblock/MultiblockAlloyFurnaceCoreBlock.java b/src/main/java/com/sorrowmist/useless/content/blocks/multiblock/MultiblockAlloyFurnaceCoreBlock.java new file mode 100644 index 00000000..cff99c16 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/blocks/multiblock/MultiblockAlloyFurnaceCoreBlock.java @@ -0,0 +1,124 @@ +package com.sorrowmist.useless.content.blocks.multiblock; + +import com.sorrowmist.useless.content.blockentities.multiblock.MultiblockAlloyFurnaceCoreBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.item.context.BlockPlaceContext; +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.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.level.block.state.properties.DirectionProperty; +import net.minecraft.world.level.block.state.properties.EnumProperty; +import org.jetbrains.annotations.Nullable; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.level.storage.loot.LootParams; +import net.minecraft.world.level.storage.loot.parameters.LootContextParams; +import com.sorrowmist.useless.core.component.MultiblockRecoveryData; +import com.sorrowmist.useless.core.component.UComponents; +import java.util.List; + +public final class MultiblockAlloyFurnaceCoreBlock extends Block implements EntityBlock { + public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING; + public static final BooleanProperty FORMED = BooleanProperty.create("formed"); + public static final EnumProperty ACTIVITY = + EnumProperty.create("activity", MultiblockFurnaceActivity.class); + + public MultiblockAlloyFurnaceCoreBlock(Properties properties) { + super(properties); + registerDefaultState(stateDefinition.any() + .setValue(FACING, Direction.NORTH) + .setValue(FORMED, false) + .setValue(ACTIVITY, MultiblockFurnaceActivity.IDLE)); + } + + @Override + public void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean movedByPiston) { + if (!state.is(newState.getBlock()) && !level.isClientSide + && level.getBlockEntity(pos) instanceof MultiblockAlloyFurnaceCoreBlockEntity core) { + core.cancelAllTasksForRemoval(); + } + super.onRemove(state, level, pos, newState, movedByPiston); + } + + @Override + public List getDrops(BlockState state, LootParams.Builder params) { + List drops = super.getDrops(state, params); + if (params.getOptionalParameter(LootContextParams.BLOCK_ENTITY) + instanceof MultiblockAlloyFurnaceCoreBlockEntity core) { + core.cancelAllTasksForRemoval(); + MultiblockRecoveryData recovery = core.createRecoveryData(); + if (!recovery.isEmpty()) { + for (ItemStack drop : drops) { + if (drop.is(asItem())) com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(drop, UComponents.MULTIBLOCK_RECOVERY_DATA.get(), recovery); + } + } + } + return drops; + } + + @Override + public void setPlacedBy(Level level, BlockPos pos, BlockState state, + @Nullable LivingEntity placer, ItemStack stack) { + super.setPlacedBy(level, pos, state, placer, stack); + if (!level.isClientSide && level.getBlockEntity(pos) instanceof MultiblockAlloyFurnaceCoreBlockEntity core) { + MultiblockRecoveryData recovery = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(stack, UComponents.MULTIBLOCK_RECOVERY_DATA.get()); + if (recovery != null) core.restoreRecoveryData(recovery); + } + } + + @Override + public BlockState getStateForPlacement(BlockPlaceContext context) { + return defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite()); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(FACING, FORMED, ACTIVITY); + } + + @Override + public void neighborChanged(BlockState state, Level level, BlockPos pos, Block neighborBlock, + BlockPos neighborPos, boolean movedByPiston) { + super.neighborChanged(state, level, pos, neighborBlock, neighborPos, movedByPiston); + if (level.getBlockEntity(pos) instanceof MultiblockAlloyFurnaceCoreBlockEntity core) { + core.requestStructureValidation(); + } + } + + @Override + public InteractionResult use(BlockState state, Level level, BlockPos pos, + Player player, net.minecraft.world.InteractionHand hand, BlockHitResult hitResult) { + if (!level.isClientSide && level.getBlockEntity(pos) instanceof MenuProvider provider) { + net.minecraftforge.network.NetworkHooks.openScreen((net.minecraft.server.level.ServerPlayer)player,provider,pos); + } + return InteractionResult.sidedSuccess(level.isClientSide); + } + + @Override + public @Nullable BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new MultiblockAlloyFurnaceCoreBlockEntity(pos, state); + } + + @Override + @SuppressWarnings("unchecked") + public @Nullable BlockEntityTicker getTicker( + Level level, BlockState state, BlockEntityType type) { + return level.isClientSide ? null : (lvl, pos, blockState, blockEntity) -> { + if (blockEntity instanceof MultiblockAlloyFurnaceCoreBlockEntity core) { + core.serverTick(); + } + }; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/blocks/multiblock/MultiblockFurnaceActivity.java b/src/main/java/com/sorrowmist/useless/content/blocks/multiblock/MultiblockFurnaceActivity.java new file mode 100644 index 00000000..f5789020 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/blocks/multiblock/MultiblockFurnaceActivity.java @@ -0,0 +1,31 @@ +package com.sorrowmist.useless.content.blocks.multiblock; + +import net.minecraft.util.StringRepresentable; + +public enum MultiblockFurnaceActivity implements StringRepresentable { + IDLE("idle"), + RUN("run"), + WAIT("wait"); + + private final String serializedName; + + MultiblockFurnaceActivity(String serializedName) { + this.serializedName = serializedName; + } + + public static MultiblockFurnaceActivity resolve( + boolean formed, boolean progressed, boolean hasWork) { + if (!formed) { + return IDLE; + } + if (progressed) { + return RUN; + } + return hasWork ? WAIT : IDLE; + } + + @Override + public String getSerializedName() { + return serializedName; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/blocks/multiblock/MultiblockPartBlock.java b/src/main/java/com/sorrowmist/useless/content/blocks/multiblock/MultiblockPartBlock.java new file mode 100644 index 00000000..81c69557 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/blocks/multiblock/MultiblockPartBlock.java @@ -0,0 +1,35 @@ +package com.sorrowmist.useless.content.blocks.multiblock; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; + +public class MultiblockPartBlock extends Block { + public MultiblockPartBlock(Properties properties) { + super(properties); + } + + @Override + public void neighborChanged(BlockState state, Level level, BlockPos pos, Block neighborBlock, + BlockPos neighborPos, boolean movedByPiston) { + super.neighborChanged(state, level, pos, neighborBlock, neighborPos, movedByPiston); + OmniversalAlloyFurnaceStructure.notifyNearbyCores(level, pos); + } + + @Override + public void onPlace(BlockState state, Level level, BlockPos pos, BlockState oldState, boolean movedByPiston) { + super.onPlace(state, level, pos, oldState, movedByPiston); + if (!state.is(oldState.getBlock())) { + OmniversalAlloyFurnaceStructure.notifyNearbyCores(level, pos); + } + } + + @Override + public void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean movedByPiston) { + if (!state.is(newState.getBlock())) { + OmniversalAlloyFurnaceStructure.notifyNearbyCores(level, pos); + } + super.onRemove(state, level, pos, newState, movedByPiston); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/blocks/multiblock/OmniversalAlloyFurnaceStructure.java b/src/main/java/com/sorrowmist/useless/content/blocks/multiblock/OmniversalAlloyFurnaceStructure.java new file mode 100644 index 00000000..269b2fe6 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/blocks/multiblock/OmniversalAlloyFurnaceStructure.java @@ -0,0 +1,255 @@ +package com.sorrowmist.useless.content.blocks.multiblock; + +import com.sorrowmist.useless.content.blockentities.multiblock.MultiblockAlloyFurnaceCoreBlockEntity; +import com.sorrowmist.useless.content.blockentities.multiblock.MePatternAssemblyBlockEntity; +import com.sorrowmist.useless.content.blockentities.multiblock.PassiveCraftingHatchBlockEntity; +import com.sorrowmist.useless.init.ModBlocks; +import com.sorrowmist.useless.init.ModTags; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.SectionPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** Canonical 3x4x3 template shared by validation, preview and automatic construction. */ +public final class OmniversalAlloyFurnaceStructure { + public static final int COIL_COUNT = 16; + /** Normal casing blocks required after the assembly and hub occupy two shell positions. */ + public static final int CASING_COUNT = 15; + public static final int FUNCTIONAL_CASING_COUNT = 17; + public static final BlockPos PREFERRED_PATTERN_ASSEMBLY_POS = new BlockPos(-1, 0, 0); + public static final BlockPos PREFERRED_MOLD_HUB_POS = new BlockPos(1, 0, 0); + + private static final List ENTRIES = createEntries(); + + private OmniversalAlloyFurnaceStructure() { + } + + public enum Part { + CORE, + CASING, + COIL, + AIR + } + + public enum FunctionalPart { + PATTERN_ASSEMBLY, + MOLD_HUB, + PASSIVE_HATCH + } + + public record Entry(BlockPos localPos, Part part) { + public BlockPos worldPos(BlockPos corePos, Direction facing) { + return toWorld(corePos, facing, localPos); + } + } + + public record Mismatch(BlockPos worldPos, Part expected, BlockState actual) { + } + + public record ValidationResult(boolean valid, int coilTier, + @Nullable BlockPos patternAssemblyPos, + @Nullable BlockPos moldHubPos, + @Nullable BlockPos passiveHatchPos, + List mismatches) { + public ValidationResult { + patternAssemblyPos = patternAssemblyPos == null ? null : patternAssemblyPos.immutable(); + moldHubPos = moldHubPos == null ? null : moldHubPos.immutable(); + passiveHatchPos = passiveHatchPos == null ? null : passiveHatchPos.immutable(); + mismatches = List.copyOf(mismatches); + } + } + + public static List entries() { + return ENTRIES; + } + + public static BlockPos toWorld(BlockPos corePos, Direction facing, BlockPos localPos) { + Direction horizontalFacing = facing.getAxis().isHorizontal() ? facing : Direction.NORTH; + Direction right = horizontalFacing.getClockWise(); + Direction back = horizontalFacing.getOpposite(); + return corePos.relative(right, localPos.getX()) + .above(localPos.getY()) + .relative(back, localPos.getZ()); + } + + public static ValidationResult validate(LevelReader level, BlockPos corePos, Direction facing) { + List mismatches = new ArrayList<>(); + int coilTier = 0; + int assemblyCount = 0; + int moldHubCount = 0; + int passiveHatchCount = 0; + BlockPos assemblyPos = null; + BlockPos moldHubPos = null; + BlockPos passiveHatchPos = null; + + for (Entry entry : ENTRIES) { + BlockPos worldPos = entry.worldPos(corePos, facing); + if (!isChunkLoaded(level, worldPos)) { + return new ValidationResult(false, 0, null, null, null, List.of()); + } + BlockState state = level.getBlockState(worldPos); + if (entry.part == Part.COIL) { + if (state.getBlock() instanceof UselessCoilBlock coil) { + if (coilTier == 0) { + coilTier = coil.tier(); + } else if (coilTier != coil.tier()) { + mismatches.add(new Mismatch(worldPos, Part.COIL, state)); + } + } else { + mismatches.add(new Mismatch(worldPos, Part.COIL, state)); + } + continue; + } + if (entry.part == Part.CASING) { + FunctionalPart functionalPart = functionalPart(state); + if (functionalPart == null) { + if (!state.is(ModTags.OMNIVERSAL_FURNACE_CASINGS)) { + mismatches.add(new Mismatch(worldPos, Part.CASING, state)); + } + continue; + } + switch (functionalPart) { + case PATTERN_ASSEMBLY -> { + assemblyCount++; + if (assemblyCount == 1) assemblyPos = worldPos.immutable(); + else mismatches.add(new Mismatch(worldPos, Part.CASING, state)); + } + case MOLD_HUB -> { + moldHubCount++; + if (moldHubCount == 1) moldHubPos = worldPos.immutable(); + else mismatches.add(new Mismatch(worldPos, Part.CASING, state)); + } + case PASSIVE_HATCH -> { + passiveHatchCount++; + if (passiveHatchCount == 1) passiveHatchPos = worldPos.immutable(); + else mismatches.add(new Mismatch(worldPos, Part.CASING, state)); + } + } + continue; + } + if (!matches(entry.part, state)) { + mismatches.add(new Mismatch(worldPos, entry.part, state)); + } + } + boolean functionalCountsValid = isFunctionalPartCountsValid( + assemblyCount, moldHubCount, passiveHatchCount); + // 缺功能方块时外壳本身全都合法,循环里不会产生 Mismatch,结构却判定失效。 + // 补一条指向首选位的记录,否则「未成型但没有可高亮的方块」。 + if (assemblyCount == 0) { + BlockPos pos = toWorld(corePos, facing, PREFERRED_PATTERN_ASSEMBLY_POS); + mismatches.add(new Mismatch(pos, Part.CASING, level.getBlockState(pos))); + } + if (moldHubCount == 0) { + BlockPos pos = toWorld(corePos, facing, PREFERRED_MOLD_HUB_POS); + mismatches.add(new Mismatch(pos, Part.CASING, level.getBlockState(pos))); + } + if (assemblyPos != null && isClaimedByAnotherCore(level, corePos, assemblyPos, + FunctionalPart.PATTERN_ASSEMBLY)) { + mismatches.add(new Mismatch(assemblyPos, Part.CASING, level.getBlockState(assemblyPos))); + } + if (passiveHatchPos != null && isClaimedByAnotherCore(level, corePos, passiveHatchPos, + FunctionalPart.PASSIVE_HATCH)) { + mismatches.add(new Mismatch(passiveHatchPos, Part.CASING, level.getBlockState(passiveHatchPos))); + } + return new ValidationResult(mismatches.isEmpty() && functionalCountsValid && coilTier > 0, + coilTier, assemblyPos, moldHubPos, passiveHatchPos, mismatches); + } + + public static boolean isFunctionalPartCountsValid( + int patternAssemblyCount, int moldHubCount, int passiveHatchCount) { + return patternAssemblyCount == 1 && moldHubCount == 1 && isPassiveHatchCountValid(passiveHatchCount); + } + + static boolean isPassiveHatchCountValid(int count) { + return count >= 0 && count <= 1; + } + + @Nullable + public static FunctionalPart functionalPart(BlockState state) { + if (state.is(ModBlocks.ME_PATTERN_ASSEMBLY.get())) return FunctionalPart.PATTERN_ASSEMBLY; + if (state.is(ModBlocks.OMNIVERSAL_MOLD_HUB.get())) return FunctionalPart.MOLD_HUB; + if (state.is(ModBlocks.PASSIVE_CRAFTING_HATCH.get())) return FunctionalPart.PASSIVE_HATCH; + return null; + } + + private static boolean isClaimedByAnotherCore( + LevelReader level, BlockPos corePos, BlockPos partPos, FunctionalPart part) { + if (!(level instanceof Level actualLevel)) { + return false; + } + return switch (part) { + case PATTERN_ASSEMBLY -> actualLevel.getBlockEntity(partPos) + instanceof MePatternAssemblyBlockEntity assembly + && assembly.isClaimedByOtherController(corePos); + case PASSIVE_HATCH -> actualLevel.getBlockEntity(partPos) + instanceof PassiveCraftingHatchBlockEntity hatch + && hatch.isClaimedByOtherController(corePos); + case MOLD_HUB -> false; + }; + } + + public static void notifyNearbyCores(Level level, BlockPos changedPos) { + if (level.isClientSide) return; + BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos(); + for (int x = -2; x <= 2; x++) { + for (int y = -3; y <= 0; y++) { + for (int z = -2; z <= 2; z++) { + cursor.setWithOffset(changedPos, x, y, z); + // Level#getBlockEntity loads missing chunks. During server shutdown that can + // resurrect a chunk which is already being unloaded and livelock ChunkMap. + if (!level.isLoaded(cursor)) continue; + if (level.getBlockEntity(cursor) instanceof MultiblockAlloyFurnaceCoreBlockEntity core) { + core.requestStructureValidation(); + } + } + } + } + } + + @SuppressWarnings("deprecation") + private static boolean isChunkLoaded(LevelReader level, BlockPos pos) { + return level.hasChunk(SectionPos.blockToSectionCoord(pos.getX()), + SectionPos.blockToSectionCoord(pos.getZ())); + } + + private static boolean matches(Part part, BlockState state) { + return switch (part) { + case CORE -> state.is(ModBlocks.MULTIBLOCK_ALLOY_FURNACE_CORE.get()); + case CASING -> state.is(ModTags.OMNIVERSAL_FURNACE_CASINGS); + case AIR -> state.isAir(); + case COIL -> state.getBlock() instanceof UselessCoilBlock; + }; + } + + private static List createEntries() { + List entries = new ArrayList<>(36); + entries.add(new Entry(new BlockPos(0, 0, 0), Part.CORE)); + entries.add(new Entry(PREFERRED_PATTERN_ASSEMBLY_POS, Part.CASING)); + entries.add(new Entry(PREFERRED_MOLD_HUB_POS, Part.CASING)); + + for (int z = 1; z <= 2; z++) { + for (int x = -1; x <= 1; x++) { + entries.add(new Entry(new BlockPos(x, 0, z), Part.CASING)); + } + } + for (int y = 1; y <= 2; y++) { + for (int z = 0; z <= 2; z++) { + for (int x = -1; x <= 1; x++) { + entries.add(new Entry(new BlockPos(x, y, z), x == 0 && z == 1 ? Part.AIR : Part.COIL)); + } + } + } + for (int z = 0; z <= 2; z++) { + for (int x = -1; x <= 1; x++) { + entries.add(new Entry(new BlockPos(x, 3, z), Part.CASING)); + } + } + return List.copyOf(entries); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/blocks/multiblock/OmniversalMoldHubBlock.java b/src/main/java/com/sorrowmist/useless/content/blocks/multiblock/OmniversalMoldHubBlock.java new file mode 100644 index 00000000..9ff67dde --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/blocks/multiblock/OmniversalMoldHubBlock.java @@ -0,0 +1,85 @@ +package com.sorrowmist.useless.content.blocks.multiblock; + +import com.sorrowmist.useless.content.blockentities.multiblock.OmniversalMoldHubBlockEntity; +import com.sorrowmist.useless.core.component.ExternalInventoryKind; +import com.sorrowmist.useless.core.component.ExternalInventoryReference; +import com.sorrowmist.useless.core.component.MultiblockPartData; +import com.sorrowmist.useless.core.component.UComponents; +import com.sorrowmist.useless.world.inventory.ExternalInventoryStore; +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.level.storage.loot.LootParams; +import net.minecraft.world.level.storage.loot.parameters.LootContextParams; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public final class OmniversalMoldHubBlock extends DirectionalMultiblockPartBlock implements EntityBlock { + public OmniversalMoldHubBlock(Properties properties) { + super(properties); + } + + @Override + public @Nullable BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new OmniversalMoldHubBlockEntity(pos, state); + } + + @Override + public void onRemove(BlockState state, Level level, BlockPos pos, + BlockState newState, boolean movedByPiston) { + if (!state.is(newState.getBlock()) && !level.isClientSide + && level.getBlockEntity(pos) instanceof OmniversalMoldHubBlockEntity hub) { + hub.releaseExternalInventory(); + } + super.onRemove(state, level, pos, newState, movedByPiston); + } + + @Override + public List getDrops(BlockState state, LootParams.Builder params) { + List drops = super.getDrops(state, params); + if (params.getOptionalParameter(LootContextParams.BLOCK_ENTITY) + instanceof OmniversalMoldHubBlockEntity hub) { + ExternalInventoryReference reference = hub.getExternalInventoryReference(); + for (ItemStack drop : drops) { + if (drop.is(asItem())) { + if (reference != null) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(drop, UComponents.EXTERNAL_INVENTORY_REFERENCE.get(), reference); + } + break; + } + } + } + return drops; + } + + @Override + public void setPlacedBy(Level level, BlockPos pos, BlockState state, + @Nullable LivingEntity placer, ItemStack stack) { + super.setPlacedBy(level, pos, state, placer, stack); + if (!level.isClientSide && level.getBlockEntity(pos) instanceof OmniversalMoldHubBlockEntity hub) { + ExternalInventoryReference reference = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(stack, UComponents.EXTERNAL_INVENTORY_REFERENCE.get()); + MultiblockPartData itemData = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(stack, UComponents.MULTIBLOCK_PART_DATA.get()); + hub.bindExternalInventory(reference, + itemData == null ? null : itemData.inventory(), level.registryAccess()); + ExternalInventoryStore.clearLegacyComponent(hub, ExternalInventoryKind.MOLD_HUB); + } + } + + @Override + public InteractionResult use(BlockState state, net.minecraft.world.level.Level level, + BlockPos pos, Player player, net.minecraft.world.InteractionHand hand, BlockHitResult hitResult) { + if (!level.isClientSide && level.getBlockEntity(pos) instanceof MenuProvider provider) { + net.minecraftforge.network.NetworkHooks.openScreen((net.minecraft.server.level.ServerPlayer)player,provider,pos); + } + return InteractionResult.sidedSuccess(level.isClientSide); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/blocks/multiblock/PassiveCraftingHatchBlock.java b/src/main/java/com/sorrowmist/useless/content/blocks/multiblock/PassiveCraftingHatchBlock.java new file mode 100644 index 00000000..ace56f0e --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/blocks/multiblock/PassiveCraftingHatchBlock.java @@ -0,0 +1,124 @@ +package com.sorrowmist.useless.content.blocks.multiblock; + +import com.sorrowmist.useless.content.blockentities.multiblock.PassiveCraftingHatchBlockEntity; +import com.sorrowmist.useless.core.component.ExternalInventoryKind; +import com.sorrowmist.useless.core.component.ExternalInventoryReference; +import com.sorrowmist.useless.core.component.MultiblockPartData; +import com.sorrowmist.useless.core.component.PassiveHatchSettings; +import com.sorrowmist.useless.core.component.UComponents; +import com.sorrowmist.useless.world.inventory.ExternalInventoryStore; +import net.minecraft.core.BlockPos; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.Level; +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.storage.loot.LootParams; +import net.minecraft.world.level.storage.loot.parameters.LootContextParams; +import net.minecraft.world.phys.BlockHitResult; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public final class PassiveCraftingHatchBlock extends DirectionalMultiblockPartBlock implements EntityBlock { + public PassiveCraftingHatchBlock(Properties properties) { + super(properties); + } + + @Override + public @Nullable BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new PassiveCraftingHatchBlockEntity(pos, state); + } + + @Override + @SuppressWarnings("unchecked") + public @Nullable BlockEntityTicker getTicker( + Level level, BlockState state, BlockEntityType type) { + return level.isClientSide ? null : (tickerLevel, pos, blockState, blockEntity) -> { + if (blockEntity instanceof PassiveCraftingHatchBlockEntity hatch) { + hatch.serverTickStandalone(); + } + }; + } + + @Override + public void onRemove(BlockState state, Level level, BlockPos pos, + BlockState newState, boolean movedByPiston) { + if (!state.is(newState.getBlock()) + && level.getBlockEntity(pos) instanceof PassiveCraftingHatchBlockEntity hatch) { + if (!level.isClientSide) { + hatch.prepareForRemoval(); + hatch.releaseExternalInventory(); + } + } + super.onRemove(state, level, pos, newState, movedByPiston); + } + + @Override + public List getDrops(BlockState state, LootParams.Builder params) { + List drops = super.getDrops(state, params); + if (params.getOptionalParameter(LootContextParams.BLOCK_ENTITY) + instanceof PassiveCraftingHatchBlockEntity hatch) { + hatch.prepareForRemoval(); + MultiblockPartData itemData = hatch.createSettingsItemData(); + ExternalInventoryReference reference = hatch.getExternalInventoryReference(); + List slotMultipliers = hatch.slotMultiplierSettings(); + boolean customInterval = itemData.intervalTicks() + != PassiveCraftingHatchBlockEntity.DEFAULT_INTERVAL_TICKS; + boolean customMultiplier = itemData.multiplier() != 1L; + boolean hasCustomData = reference != null || customInterval || customMultiplier + || !slotMultipliers.isEmpty(); + if (hasCustomData) { + for (ItemStack drop : drops) { + if (drop.is(asItem())) { + if (reference != null) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(drop, UComponents.EXTERNAL_INVENTORY_REFERENCE.get(), reference); + } + if (customInterval || customMultiplier || !slotMultipliers.isEmpty()) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(drop, UComponents.PASSIVE_HATCH_SETTINGS.get(), + new PassiveHatchSettings(itemData.intervalTicks(), + itemData.multiplier(), slotMultipliers)); + } + break; + } + } + } + } + return drops; + } + + @Override + public void setPlacedBy(Level level, BlockPos pos, BlockState state, + @Nullable LivingEntity placer, ItemStack stack) { + super.setPlacedBy(level, pos, state, placer, stack); + if (!level.isClientSide + && level.getBlockEntity(pos) instanceof PassiveCraftingHatchBlockEntity hatch) { + ExternalInventoryReference reference = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(stack, UComponents.EXTERNAL_INVENTORY_REFERENCE.get()); + PassiveHatchSettings settings = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(stack, UComponents.PASSIVE_HATCH_SETTINGS.get()); + MultiblockPartData itemData = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(stack, UComponents.MULTIBLOCK_PART_DATA.get()); + hatch.bindExternalInventory(reference, + itemData == null ? null : itemData.inventory(), level.registryAccess()); + if (settings != null) { + hatch.restoreSettings(settings); + } else { + hatch.restoreSettings(itemData); + } + ExternalInventoryStore.clearLegacyComponent(hatch, ExternalInventoryKind.PASSIVE_HATCH); + } + } + + @Override + public InteractionResult use(BlockState state, Level level, BlockPos pos, + Player player, net.minecraft.world.InteractionHand hand, BlockHitResult hitResult) { + if (!level.isClientSide && level.getBlockEntity(pos) instanceof MenuProvider provider) { + net.minecraftforge.network.NetworkHooks.openScreen((net.minecraft.server.level.ServerPlayer)player,provider,pos); + } + return InteractionResult.sidedSuccess(level.isClientSide); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/blocks/multiblock/UselessCoilBlock.java b/src/main/java/com/sorrowmist/useless/content/blocks/multiblock/UselessCoilBlock.java new file mode 100644 index 00000000..c77ed6d2 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/blocks/multiblock/UselessCoilBlock.java @@ -0,0 +1,43 @@ +package com.sorrowmist.useless.content.blocks.multiblock; + +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BooleanProperty; + +public final class UselessCoilBlock extends MultiblockPartBlock { + public static final int MIN_TIER = 1; + public static final int USEFUL_TIER = 10; + public static final int MAX_TIER = USEFUL_TIER; + public static final BooleanProperty ACTIVE = BooleanProperty.create("active"); + + private final int tier; + + public UselessCoilBlock(int tier, Properties properties) { + super(properties); + validateTier(tier); + this.tier = tier; + registerDefaultState(stateDefinition.any().setValue(ACTIVE, false)); + } + + public int tier() { + return tier; + } + + public static String registryName(int tier) { + validateTier(tier); + return tier == USEFUL_TIER ? "useful_coil" : "useless_coil_tier_" + tier; + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(ACTIVE); + } + + private static void validateTier(int tier) { + if (tier < MIN_TIER || tier > MAX_TIER) { + throw new IllegalArgumentException( + "Coil tier must be between " + MIN_TIER + " and " + MAX_TIER); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/entities/BeefTimeAccelerationEntity.java b/src/main/java/com/sorrowmist/useless/content/entities/BeefTimeAccelerationEntity.java new file mode 100644 index 00000000..7fb9574c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/entities/BeefTimeAccelerationEntity.java @@ -0,0 +1,274 @@ +package com.sorrowmist.useless.content.entities; + +import com.sorrowmist.useless.content.items.BeefTimeAcceleration; +import com.sorrowmist.useless.init.ModEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientGamePacketListener; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.npc.Villager; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.trading.MerchantOffer; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +import java.util.UUID; + +/** + * 「时间加速」的隐形载体实体:既是计时器,也是效果施加者与悬浮文字锚点。 + * + *

支持两类目标: + *

    + *
  • 方块目标({@code targetPos}):每 tick 额外执行 {@code 2^tickSpeed} 次 + * 方块 tick / 随机刻。
  • + *
  • 生物目标({@code TARGET_ENTITY_ID}):每 tick 额外推进目标的「计时器」 + * 数值(生长 / 繁殖冷却 / 下蛋 / 补货 / 长毛…),不跑 AI、寻路、移动与物理。 + * 真正的推进逻辑在 {@link BeefTimeAcceleration#accelerateEntityTimers}。
  • + *
+ * + *

生物目标时本实体会跟随目标(保证同区块被加载、悬浮文字位置正确)。 + */ +public class BeefTimeAccelerationEntity extends Entity { + public static final int DEFAULT_TOTAL_TIME = 600; + public static final int MAX_TICK_SPEED = 8; + + /** + * 生物目标的叠加上限:{@code 1 << 15 = 32768}。 + * + *

生物加速是纯整数加减(不循环),所以倍率不产生任何额外 CPU 开销 + * —— 这与方块版每 tick 真跑 {@code 2^tickSpeed} 次 tick 完全不同,那边必须压在小值。 + * 这里放开到 15 的理由是「原版计时器的实际天花板」:{@code AgeableMob.age} 最大 24000、 + * {@code Chicken.eggTime} 最大 12000,{@code extra >= 24000} 之后所有计时器都已在 1 tick 内 + * 走完,再大只是数字好看。上限不能超过 30,否则 {@code 1 << 31} 会变成负数。

+ */ + public static final int MAX_ENTITY_TICK_SPEED = 15; + + /** 村民补货的节流:每 20 tick(1 游戏秒)最多补一次,避免刷屏交易界面。 */ + public static final int ENTITY_RESTOCK_COOLDOWN_TICKS = 20; + + /** 生物目标用:-1 表示这是一个方块目标。 */ + private static final int NO_ENTITY_TARGET = -1; + + private static final EntityDataAccessor TICK_SPEED = + SynchedEntityData.defineId(BeefTimeAccelerationEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor REMAINING_TIME = + SynchedEntityData.defineId(BeefTimeAccelerationEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor TOTAL_TIME = + SynchedEntityData.defineId(BeefTimeAccelerationEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor TARGET_ENTITY_ID = + SynchedEntityData.defineId(BeefTimeAccelerationEntity.class, EntityDataSerializers.INT); + + private BlockPos targetPos; + /** 目标实体的 UUID:getId 在目标死亡后可能被新实体复用,UUID 作为兜底校验。 */ + private UUID targetEntityUuid; + /** 村民补货节流计数,仅服务端使用,不进 NBT。 */ + private int restockCooldown; + + public BeefTimeAccelerationEntity(EntityType entityType, Level level) { + super(entityType, level); + this.noPhysics = true; + this.setNoGravity(true); + this.setInvisible(true); + } + + public BeefTimeAccelerationEntity(Level level, BlockPos targetPos) { + this(ModEntities.BEEF_TIME_ACCELERATION.get(), level); + this.targetPos = targetPos.immutable(); + this.setPos(targetPos.getX() + 0.5D, targetPos.getY() + 0.5D, targetPos.getZ() + 0.5D); + this.setTickSpeed(1); + this.setTotalTime(DEFAULT_TOTAL_TIME); + this.setRemainingTime(DEFAULT_TOTAL_TIME); + } + + public BeefTimeAccelerationEntity(Level level, LivingEntity target) { + this(ModEntities.BEEF_TIME_ACCELERATION.get(), level); + this.targetPos = null; + this.setTargetEntityId(target.getId()); + this.targetEntityUuid = target.getUUID(); + this.snapToTarget(target); + this.setTickSpeed(1); + this.setTotalTime(DEFAULT_TOTAL_TIME); + this.setRemainingTime(DEFAULT_TOTAL_TIME); + } + + @Override + public void tick() { + super.tick(); + if (this.level().isClientSide) { + return; + } + + if (!(this.level() instanceof ServerLevel level)) { + this.discard(); + return; + } + if (this.getRemainingTime() <= 0) { + this.discard(); + return; + } + + if (this.isEntityTarget()) { + tickEntityTarget(level); + } else { + tickBlockTarget(level); + } + + this.setRemainingTime(this.getRemainingTime() - 1); + if (this.getRemainingTime() <= 0) { + this.discard(); + } + } + + /** 方块目标:额外跑方块 tick / 随机刻。 */ + private void tickBlockTarget(ServerLevel level) { + if (this.targetPos == null) { + this.discard(); + return; + } + if (!level.isLoaded(this.targetPos)) { + return; + } + + BlockState state = level.getBlockState(this.targetPos); + BlockEntity blockEntity = level.getBlockEntity(this.targetPos); + if (!BeefTimeAcceleration.isValidTarget(level, state, blockEntity)) { + this.discard(); + return; + } + + BeefTimeAcceleration.tickTarget(level, this.targetPos, state, blockEntity, this.getTickSpeed()); + } + + /** 生物目标:跟随目标并推进其计时器;目标失效(死亡 / 换维度 / 区块卸载)即自毁。 */ + private void tickEntityTarget(ServerLevel level) { + Entity found = level.getEntity(this.getTargetEntityId()); + if (!(found instanceof LivingEntity target) + || target.isRemoved() + || !target.isAlive() + || target instanceof Player + || (this.targetEntityUuid != null && !this.targetEntityUuid.equals(target.getUUID()))) { + this.discard(); + return; + } + + // 跟随目标:既保证本实体与目标同区块被加载,也让悬浮文字贴着目标渲染 + this.snapToTarget(target); + + BeefTimeAcceleration.accelerateEntityTimers(target, this.getTickSpeed()); + + // 村民补货特判:原版门槛绑在 gameTime 上,单纯推计时器无效,必须直接调 restock()。 + // 只在主 tick 调一次(不在计时器循环里),并用 needsRestock() 做闸门 + 节流,避免空刷。 + if (target instanceof Villager villager) { + if (this.restockCooldown > 0) { + this.restockCooldown--; + } else if (villager.getOffers().stream().anyMatch(MerchantOffer::needsRestock)) { + villager.restock(); + this.restockCooldown = ENTITY_RESTOCK_COOLDOWN_TICKS; + } + } + } + + private void snapToTarget(LivingEntity target) { + this.setPos(target.getX(), target.getY() + target.getBbHeight() + 0.4D, target.getZ()); + } + + @Override + protected void defineSynchedData() { + this.entityData.define(TICK_SPEED, 1); + this.entityData.define(REMAINING_TIME, DEFAULT_TOTAL_TIME); + this.entityData.define(TOTAL_TIME, DEFAULT_TOTAL_TIME); + this.entityData.define(TARGET_ENTITY_ID, NO_ENTITY_TARGET); + } + + /** 是否为「生物目标」(否则是方块目标)。 */ + public boolean isEntityTarget() { + return this.getTargetEntityId() != NO_ENTITY_TARGET; + } + + public int getTargetEntityId() { + return this.entityData.get(TARGET_ENTITY_ID); + } + + public void setTargetEntityId(int entityId) { + this.entityData.set(TARGET_ENTITY_ID, entityId); + } + + public BlockPos getTargetPos() { + return this.targetPos != null ? this.targetPos : this.blockPosition(); + } + + public void setTargetPos(BlockPos targetPos) { + this.targetPos = targetPos == null ? null : targetPos.immutable(); + } + + public int getTickSpeed() { + return this.entityData.get(TICK_SPEED); + } + + public void setTickSpeed(int tickSpeed) { + this.entityData.set(TICK_SPEED, tickSpeed); + } + + public int getRemainingTime() { + return this.entityData.get(REMAINING_TIME); + } + + public void setRemainingTime(int remainingTime) { + this.entityData.set(REMAINING_TIME, remainingTime); + } + + public int getTotalTime() { + return this.entityData.get(TOTAL_TIME); + } + + public void setTotalTime(int totalTime) { + this.entityData.set(TOTAL_TIME, totalTime); + } + + public void addTime(int ticks) { + this.setRemainingTime(this.getRemainingTime() + ticks); + } + + @Override + protected void readAdditionalSaveData(CompoundTag tag) { + if (tag.contains("targetPos")) { + this.targetPos = BlockPos.of(tag.getLong("targetPos")); + } + // 旧存档里的方块目标实体没有这个键,getInt 会返回 0(= 一个真实的实体 id), + // 会被误判成「实体目标」而立刻自毁。必须显式回退到 NO_ENTITY_TARGET。 + this.setTargetEntityId(tag.contains("targetEntityId") ? tag.getInt("targetEntityId") : NO_ENTITY_TARGET); + if (tag.hasUUID("targetEntityUuid")) { + this.targetEntityUuid = tag.getUUID("targetEntityUuid"); + } + this.setTickSpeed(tag.getInt("tickSpeed")); + this.setRemainingTime(tag.getInt("remainingTime")); + this.setTotalTime(tag.getInt("totalTime")); + } + + @Override + protected void addAdditionalSaveData(CompoundTag tag) { + if (this.targetPos != null) { + tag.putLong("targetPos", this.targetPos.asLong()); + } + tag.putInt("targetEntityId", this.getTargetEntityId()); + if (this.targetEntityUuid != null) { + tag.putUUID("targetEntityUuid", this.targetEntityUuid); + } + tag.putInt("tickSpeed", this.getTickSpeed()); + tag.putInt("remainingTime", this.getRemainingTime()); + tag.putInt("totalTime", this.getTotalTime()); + } + + @Override + public Packet getAddEntityPacket() { + return net.minecraftforge.network.NetworkHooks.getEntitySpawningPacket(this); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/items/AE2GiftPackageItem.java b/src/main/java/com/sorrowmist/useless/content/items/AE2GiftPackageItem.java new file mode 100644 index 00000000..e2df1e5a --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/items/AE2GiftPackageItem.java @@ -0,0 +1,141 @@ +package com.sorrowmist.useless.content.items; + + +import com.sorrowmist.useless.core.config.ConfigManager; +import net.minecraft.core.NonNullList; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResultHolder; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.Level; + +import net.minecraftforge.fml.ModList; +import net.minecraft.core.registries.BuiltInRegistries; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Date; +import java.util.List; + +public class AE2GiftPackageItem extends Item { + public AE2GiftPackageItem(Properties properties) { + super(properties); + } + + @Override + public @NotNull InteractionResultHolder use(@NotNull Level level, @NotNull Player player, @NotNull InteractionHand hand) { + ItemStack stack = player.getItemInHand(hand); + + if (!level.isClientSide) { + NonNullList contents = createPackageContents(); + + for (ItemStack content : contents) { + if (!content.isEmpty()) { + if (!player.getInventory().add(content)) { + player.drop(content, false); + } + } + } + + stack.shrink(1); + + if (ModList.get().isLoaded("ae2") + && player instanceof net.minecraft.server.level.ServerPlayer serverPlayer + && serverPlayer.getServer() != null) { + net.minecraft.commands.CommandSourceStack source = serverPlayer.createCommandSourceStack(); + serverPlayer.getServer().getCommands().performPrefixedCommand(source, "ae2 channelmode infinite"); + } + } + + return InteractionResultHolder.sidedSuccess(stack, level.isClientSide); + } + + private NonNullList createPackageContents() { + NonNullList contents = NonNullList.create(); + if (!ModList.get().isLoaded("ae2")) { + return contents; + } + + for (String entry : ConfigManager.getAE2GiftPackageItems()) { + addConfiguredEntry(contents, entry); + } + + return contents; + } + + private void addConfiguredEntry(NonNullList contents, String entry) { + if (entry == null) { + return; + } + + String[] parts = entry.split(",", -1); + if (parts.length != 2) { + return; + } + + ResourceLocation itemId = ResourceLocation.tryParse(parts[0].trim()); + if (itemId == null) { + return; + } + + int count; + try { + count = Integer.parseInt(parts[1].trim()); + } catch (NumberFormatException ignored) { + return; + } + if (count <= 0) { + return; + } + + Item item = BuiltInRegistries.ITEM.get(itemId); + if (item == Items.AIR) { + return; + } + + if (itemId.equals(new ResourceLocation("ae2", "quantum_entangled_singularity"))) { + while (count >= 2) { + contents.addAll(createEntangledSingularityPair(item)); + count -= 2; + } + } + + addItemStacks(contents, item, count); + } + + private void addItemStacks(NonNullList contents, Item item, int count) { + int maxStackSize = Math.max(1, item.getMaxStackSize()); + while (count > 0) { + int stackSize = Math.min(count, maxStackSize); + contents.add(new ItemStack(item, stackSize)); + count -= stackSize; + } + } + + private NonNullList createEntangledSingularityPair(Item singularityItem) { + NonNullList pair = NonNullList.create(); + long frequency = new Date().getTime() * 100 + (System.nanoTime() % 100); + + ItemStack singularity1 = new ItemStack(singularityItem, 1); + singularity1.getOrCreateTag().putLong(appeng.blockentity.qnb.QuantumBridgeBlockEntity.TAG_FREQUENCY,frequency); + + ItemStack singularity2 = new ItemStack(singularityItem, 1); + singularity2.getOrCreateTag().putLong(appeng.blockentity.qnb.QuantumBridgeBlockEntity.TAG_FREQUENCY,frequency); + + pair.add(singularity1); + pair.add(singularity2); + return pair; + } + + @Override + public void appendHoverText(@NotNull ItemStack stack, @Nullable net.minecraft.world.level.Level context, @NotNull List tooltipComponents, @NotNull TooltipFlag tooltipFlag) { + tooltipComponents.add(Component.translatable("tooltip.useless_mod.ae2_gift_package.description")); + super.appendHoverText(stack, context, tooltipComponents, tooltipFlag); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/items/AdvancedAlloyFurnaceBlockItem.java b/src/main/java/com/sorrowmist/useless/content/items/AdvancedAlloyFurnaceBlockItem.java new file mode 100644 index 00000000..ec985d95 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/items/AdvancedAlloyFurnaceBlockItem.java @@ -0,0 +1,15 @@ +package com.sorrowmist.useless.content.items; + +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.level.block.Block; + +/** + * 高级合金炉方块物品 + * 根据保存的阶级显示不同的名字 + */ +public class AdvancedAlloyFurnaceBlockItem extends BlockItem { + + public AdvancedAlloyFurnaceBlockItem(Block block, Properties properties) { + super(block, properties); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/items/BeefCraftingHandler.java b/src/main/java/com/sorrowmist/useless/content/items/BeefCraftingHandler.java new file mode 100644 index 00000000..c96a0238 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/items/BeefCraftingHandler.java @@ -0,0 +1,57 @@ +package com.sorrowmist.useless.content.items; + +import net.minecraft.world.Container; +import net.minecraft.world.item.ItemStack; + +/** + * 造化杖作为材料参与合成时,让本次产出物品变为不可破坏(无限耐久)。 + *

+ * 覆盖 3×3 工作台与 2×2 背包合成两种格子。 + *

+ * 为什么走 {@code PlayerEvent.ItemCraftedEvent} 而不是 mixin {@code ResultSlot}: + * 整合包里装了 FastWorkbench,它用 {@code dev.shadowsoffire.fastbench.util.CraftResultSlotExt + * extends ResultSlot} 覆写了 {@code ResultSlot#onTake},且不调用 {@code super} —— + * 所以注入父类 {@code ResultSlot#onTake} 的代码在运行时永远不会执行。 + * 而事件的触发点在 {@code ResultSlot#checkTakeAchievements} 里的 + * {@code EventHooks.firePlayerCraftingEvent(...)},FastWorkbench 也保留了这个调用, + * 因此事件路径是 mod 无关、可靠的。 + *

+ * 「杖子不被消耗」不在这里处理:{@link EndlessBeafItem} 已覆写 + * {@code hasCraftingRemainingItem() -> true} / {@code getCraftingRemainingItem(stack) -> stack.copy()}, + * 原版会把杖子返还到合成格。 + */ +public final class BeefCraftingHandler { + private BeefCraftingHandler() { + } + + /** + * 若合成格中含任意造化杖变体,则给产出物品打上「不可破坏」。 + * + * @param result 合成产出栈({@code ItemCraftedEvent#getCrafting()}) + * @param craftMatrix 合成格({@code ItemCraftedEvent#getInventory()}) + * @return true 表示已施加不可破坏 + */ + public static boolean applyUnbreakableToCraftResult(ItemStack result, Container craftMatrix) { + if (result == null || result.isEmpty() || craftMatrix == null) { + return false; + } + // 3x3 工作台与 2x2 背包合成都生效(不再按宽度过滤) + if (!containsEndlessBeaf(craftMatrix)) { + return false; + } + // 只加 Unbreakable,附魔 / rarity / 名称一概不动 + result.getOrCreateTag().putBoolean("Unbreakable", true); + return true; + } + + private static boolean containsEndlessBeaf(Container craftMatrix) { + int size = craftMatrix.getContainerSize(); + for (int i = 0; i < size; i++) { + ItemStack slot = craftMatrix.getItem(i); + if (slot != null && !slot.isEmpty() && slot.getItem() instanceof EndlessBeafItem) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/items/BeefCropHarvest.java b/src/main/java/com/sorrowmist/useless/content/items/BeefCropHarvest.java new file mode 100644 index 00000000..0c761e70 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/items/BeefCropHarvest.java @@ -0,0 +1,152 @@ +package com.sorrowmist.useless.content.items; + +import com.sorrowmist.useless.utils.mining.MiningUtils; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.stats.Stats; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.CocoaBlock; +import net.minecraft.world.level.block.CropBlock; +import net.minecraft.world.level.block.NetherWartBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.IntegerProperty; +import net.minecraft.world.level.block.state.properties.Property; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; + +import java.util.List; + +/** + * 造化杖的「顺手收菜」行为。 + * + *

右键成熟作物时直接把产物收下,并把作物重置回 0 龄(种子留在地里), + * 而不是把整株拔掉。这样即使在整合包装了各种右键收菜功能的情况下, + * 造化杖也不会把菜连根拔起。 + * + *

掉落计算完全走原版战利品表({@link Block#getDrops}),因此时运等附魔 + * 依然生效;同时扣除一颗种子以模拟「种子留在地里」。 + * 支持的作物与主流右键收菜模组保持一致:{@link CropBlock}(含绝大多数模组作物)、 + * {@link NetherWartBlock} 与 {@link CocoaBlock}。 + */ +public final class BeefCropHarvest { + + private BeefCropHarvest() { + } + + /** 判断该方块状态是否为「可收获的成熟作物」。 */ + public static boolean isHarvestable(BlockState state) { + if (state.getBlock() instanceof CropBlock crop) { + return crop.isMaxAge(state); + } + if (state.getBlock() instanceof NetherWartBlock) { + return state.getValue(NetherWartBlock.AGE) >= NetherWartBlock.MAX_AGE; + } + if (state.getBlock() instanceof CocoaBlock) { + return state.getValue(CocoaBlock.AGE) >= CocoaBlock.MAX_AGE; + } + return false; + } + + /** + * 服务端执行一次「收获但保留植株」,把产物收集进 {@code out}。 + * + *

只负责「算出掉落 + 重置作物」,产物去向交由调用方决定,这样收菜才能和挖掘/杀怪 + * 共用同一套「AE 存储优先 / 范围磁力」处理。

+ * + * @return 是否真的收获成功(非成熟作物或不受支持的方块返回 false) + */ + public static boolean harvestAndCollect(ServerLevel level, BlockPos pos, Player player, ItemStack tool, + List out) { + BlockState state = level.getBlockState(pos); + if (!isHarvestable(state)) { + return false; + } + + BlockState replanted = replantState(state); + if (replanted == null || replanted == state) { + return false; + } + + Item seedItem = cloneItem(state, level, pos, player); + List drops = Block.getDrops(state, level, pos, level.getBlockEntity(pos), player, tool); + + level.setBlockAndUpdate(pos, replanted); + player.awardStat(Stats.BLOCK_MINED.get(state.getBlock())); + player.awardStat(Stats.ITEM_USED.get(tool.getItem())); + player.causeFoodExhaustion(0.005F); + + boolean seedKept = false; + for (ItemStack drop : drops) { + if (drop.isEmpty()) { + continue; + } + // 留一颗种子在地里(与右键收菜模组一致) + if (!seedKept && seedItem != null && drop.getItem() == seedItem) { + seedKept = true; + drop.shrink(1); + } + if (drop.isEmpty()) { + continue; + } + out.add(drop); + } + + level.playSound(null, pos, SoundEvents.CROP_BREAK, SoundSource.BLOCKS, 1.0F, 1.0F); + return true; + } + + /** + * 服务端执行一次「收获但保留植株」,产物按工具的 AE 存储优先 / 范围磁力设置处理。 + * + * @return 是否真的收获成功(非成熟作物或不受支持的方块返回 false) + */ + public static boolean harvest(ServerLevel level, BlockPos pos, Player player, ItemStack tool) { + List drops = new java.util.ArrayList<>(); + if (!harvestAndCollect(level, pos, player, tool, drops)) { + return false; + } + if (!drops.isEmpty()) { + MiningUtils.handleDrops(player, MiningUtils.mergeItemStacks(drops), tool, Vec3.atCenterOf(pos)); + } + return true; + } + + /** + * 计算重置后的方块状态:优先保留其它属性(例如农夫乐事番茄的 ropelogged), + * 只在找不到 age 属性时才退回默认状态。 + */ + private static BlockState replantState(BlockState state) { + IntegerProperty age = findAgeProperty(state); + if (age != null) { + return state.setValue(age, 0); + } + if (state.getBlock() instanceof CropBlock crop) { + return crop.getStateForAge(0); + } + return null; + } + + /** 该方块「留在地里」的那颗种子/种苗物品,找不到时返回 null。 */ + private static Item cloneItem(BlockState state, ServerLevel level, BlockPos pos, Player player) { + // 作物的克隆物品就是它的种子(CropBlock#getBaseSeedId),命中信息对作物没有意义, + // 这里用一个朝上的正面命中占位。 + BlockHitResult hit = new BlockHitResult(Vec3.atCenterOf(pos), Direction.UP, pos, false); + ItemStack clone = state.getCloneItemStack(hit, level, pos, player); + return clone.isEmpty() ? null : clone.getItem(); + } + + private static IntegerProperty findAgeProperty(BlockState state) { + for (Property property : state.getProperties()) { + if (property instanceof IntegerProperty integerProperty && "age".equals(property.getName())) { + return integerProperty; + } + } + return null; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/items/BeefFlintAndSteel.java b/src/main/java/com/sorrowmist/useless/content/items/BeefFlintAndSteel.java new file mode 100644 index 00000000..d1bdac65 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/items/BeefFlintAndSteel.java @@ -0,0 +1,89 @@ +package com.sorrowmist.useless.content.items; + +import net.minecraft.advancements.CriteriaTriggers; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseFireBlock; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.CampfireBlock; +import net.minecraft.world.level.block.CandleBlock; +import net.minecraft.world.level.block.CandleCakeBlock; +import net.minecraft.world.level.block.TntBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; + +/** + * 造化杖的「打火石」行为。 + * + *

与 {@link net.minecraft.world.item.FlintAndSteelItem} 保持一致的两条路径: + *

    + *
  • 右键可点亮的方块(营火、蜡烛、蜡烛蛋糕)→ 直接点亮;
  • + *
  • 否则在点击面的相邻位置尝试放火,位置不合适则不生效。
  • + *
+ * + *

造化杖本身不可损坏,因此不扣耐久,其余判定与音效完全照抄原版。 + */ +public final class BeefFlintAndSteel { + + private BeefFlintAndSteel() { + } + + /** + * 尝试执行一次打火石点火。 + * + * @return {@link InteractionResult#PASS} 表示当前位置打火石没有可做的事, + * 交由后续行为链继续处理。 + */ + public static InteractionResult tryUse(UseOnContext ctx) { + Level level = ctx.getLevel(); + Player player = ctx.getPlayer(); + BlockPos pos = ctx.getClickedPos(); + BlockState state = level.getBlockState(pos); + + // TNT 的引爆入口位于 TntBlock 重写的 useItemOn 内,且该重写仅识别 Items.FLINT_AND_STEEL 与 + // Items.FIRE_CHARGE 两种物品;造化杖不属于二者,该重写会退化为 super 调用,TNT 方块因此完全不响应。 + // 缺少本分支时,对 TNT 右键只会落入下方的放火路径,在点击面的相邻位置生成火焰而非引爆方块。 + if (state.getBlock() instanceof TntBlock tnt) { + tnt.onCaughtFire(state, level, pos, ctx.getClickedFace(), player); + level.setBlock(pos, Blocks.AIR.defaultBlockState(), 11); + awardStat(level, player, ctx.getItemInHand()); + return InteractionResult.sidedSuccess(level.isClientSide); + } + + if (CampfireBlock.canLight(state) || CandleBlock.canLight(state) || CandleCakeBlock.canLight(state)) { + level.playSound(player, pos, SoundEvents.FLINTANDSTEEL_USE, SoundSource.BLOCKS, 1.0F, + level.getRandom().nextFloat() * 0.4F + 0.8F); + if (!level.isClientSide) { + level.setBlock(pos, state.setValue(BlockStateProperties.LIT, Boolean.TRUE), 11); + awardStat(level, player, ctx.getItemInHand()); + } + return InteractionResult.sidedSuccess(level.isClientSide); + } + + BlockPos firePos = pos.relative(ctx.getClickedFace()); + if (!BaseFireBlock.canBePlacedAt(level, firePos, ctx.getHorizontalDirection())) { + return InteractionResult.PASS; + } + + level.playSound(player, firePos, SoundEvents.FLINTANDSTEEL_USE, SoundSource.BLOCKS, 1.0F, + level.getRandom().nextFloat() * 0.4F + 0.8F); + if (!level.isClientSide) { + level.setBlock(firePos, BaseFireBlock.getState(level, firePos), 11); + awardStat(level, player, ctx.getItemInHand()); + } + return InteractionResult.sidedSuccess(level.isClientSide); + } + + private static void awardStat(Level level, Player player, ItemStack tool) { + if (player instanceof ServerPlayer serverPlayer) { + CriteriaTriggers.ITEM_USED_ON_BLOCK.trigger(serverPlayer, BlockPos.ZERO, tool); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/items/BeefMagnetHandler.java b/src/main/java/com/sorrowmist/useless/content/items/BeefMagnetHandler.java new file mode 100644 index 00000000..2046b3a2 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/items/BeefMagnetHandler.java @@ -0,0 +1,150 @@ +package com.sorrowmist.useless.content.items; + +import com.sorrowmist.useless.core.config.ConfigManager; +import com.sorrowmist.useless.utils.UComponentUtils; +import com.sorrowmist.useless.utils.mining.MiningUtils; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.TickTask; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.ExperienceOrb; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 击杀实体后的范围磁力吸附。 + * + *

把击杀点周围配置范围内的掉落物和经验球全部收给玩家。掉落物走 + * {@link MiningUtils#handleDrops(Player, List, ItemStack)},因此自动继承工具上的 + * "AE存储优先" 开关:已绑定无线接入点且开关打开时先存入 AE 网络,未能存入的部分进入背包。

+ * + *

标准生物死亡由 {@code LivingDeathEvent} 登记扫描;强制击杀的兜底分支和非生物实体不会触发该事件, + * 因此调用方需要主动调用 {@link #scheduleSweep}。同一 tick 内的多次登记会合并成一个包围盒, + * AoE 一次打死几十只怪也只扫一遍。

+ */ +public final class BeefMagnetHandler { + /** 每个玩家最多有一个待执行的扫描,键为玩家 UUID。仅在服务端主线程访问。 */ + private static final Map PENDING = new ConcurrentHashMap<>(); + + private BeefMagnetHandler() { + } + + public static boolean isEnabled(ItemStack tool) { + return UComponentUtils.isBeefMagnetEnabled(tool); + } + + /** + * 实体被击杀时调用,用配置的磁力半径扫描死亡点周围。 + * + * @param level 实体所在世界 + * @param player 击杀者 + * @param tool 击杀所用的工具 + * @param deathPos 死亡位置(范围中心) + */ + public static void onEntityKilled(ServerLevel level, Player player, ItemStack tool, Vec3 deathPos) { + scheduleSweep(level, player, tool, deathPos, + ConfigManager.getBeefMagnetRangeX(), + ConfigManager.getBeefMagnetRangeY(), + ConfigManager.getBeefMagnetRangeZ()); + } + + /** + * 登记一次范围吸附。 + * + *

掉落物与经验球是在 {@code LivingEntity#die} 里、死亡事件之后才生成的,所以这里延后一 tick + * 再扫描。同一玩家、同一 tick 的重复登记会把包围盒并起来而不是排队多次扫描。

+ * + * @param center 范围中心 + * @param rangeX X 轴半径 + * @param rangeY Y 轴半径 + * @param rangeZ Z 轴半径 + */ + public static void scheduleSweep(ServerLevel level, Player player, ItemStack tool, Vec3 center, + double rangeX, double rangeY, double rangeZ) { + if (!UComponentUtils.shouldCollectDrops(tool)) { + return; + } + MinecraftServer server = level.getServer(); + if (server == null) { + return; + } + + AABB box = AABB.ofSize(center, rangeX * 2 + 1, rangeY * 2 + 1, rangeZ * 2 + 1); + int targetTick = server.getTickCount() + 1; + UUID playerId = player.getUUID(); + + PendingSweep pending = PENDING.get(playerId); + if (pending != null && pending.tick == targetTick && pending.level == level) { + // 同一 tick 内的其它击杀:并入已登记的范围,避免重复扫描 + pending.box = pending.box.minmax(box); + return; + } + + PendingSweep scheduled = new PendingSweep(targetTick, level, box); + PENDING.put(playerId, scheduled); + server.tell(new TickTask(targetTick, () -> { + PENDING.remove(playerId, scheduled); + sweep(scheduled.level, player, tool, scheduled.box); + })); + } + + private static void sweep(ServerLevel level, Player player, ItemStack tool, AABB area) { + if (player.isRemoved() || !player.isAlive()) { + return; + } + + // 1. 掉落物 + List drops = new ArrayList<>(); + for (ItemEntity itemEntity : level.getEntitiesOfClass(ItemEntity.class, area)) { + if (!itemEntity.isAlive() || itemEntity.getItem().isEmpty() || isThrownByPlayer(itemEntity, player)) { + continue; + } + drops.add(itemEntity.getItem().copy()); + itemEntity.discard(); + } + if (!drops.isEmpty()) { + MiningUtils.handleDrops(player, MiningUtils.mergeItemStacks(drops), tool, area.getCenter()); + } + + // 2. 经验球:磁力开启时才自动收取。每次清零拾取冷却,绕过原版"每2 tick 只能吃一个"的限制, + // 同时保留经验修补等原版行为 + if (!isEnabled(tool)) { + return; + } + for (ExperienceOrb orb : level.getEntitiesOfClass(ExperienceOrb.class, area)) { + if (!orb.isAlive()) { + continue; + } + player.takeXpDelay = 0; + orb.playerTouch(player); + } + } + + /** + * 判断掉落物是不是玩家自己刚扔出来的,避免把主动丢弃的东西吸回背包。 + */ + private static boolean isThrownByPlayer(ItemEntity itemEntity, Player player) { + return itemEntity.hasPickUpDelay() && itemEntity.getOwner() == player; + } + + /** 一次待执行的扫描:目标 tick、世界和累积的包围盒。 */ + private static final class PendingSweep { + private final int tick; + private final ServerLevel level; + private AABB box; + + private PendingSweep(int tick, ServerLevel level, AABB box) { + this.tick = tick; + this.level = level; + this.box = box; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/items/BeefRipen.java b/src/main/java/com/sorrowmist/useless/content/items/BeefRipen.java new file mode 100644 index 00000000..9fade4ea --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/items/BeefRipen.java @@ -0,0 +1,435 @@ +package com.sorrowmist.useless.content.items; + +import com.sorrowmist.useless.utils.mining.MiningUtils; +import com.sorrowmist.useless.utils.mining.RightClickChainer; +import net.minecraft.advancements.CriteriaTriggers; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.stats.Stats; +import net.minecraft.util.RandomSource; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.BonemealableBlock; +import net.minecraft.world.level.block.FlowerBlock; +import net.minecraft.world.level.block.MultifaceBlock; +import net.minecraft.world.level.block.VineBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.phys.Vec3; +import net.minecraftforge.common.ForgeHooks; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.entity.player.BonemealEvent; + +import javax.annotation.Nullable; +import java.util.List; + +/** + * 造化杖的「催熟 / 强制生长」行为。 + * + *

由两个互相独立的开关驱动,玩家自行抉择: + * + *

    + *
  • 催熟({@code beef_ripen}):只走原版骨粉判定。方块实现了 + * {@link BonemealableBlock} 且当前是有效目标时,循环 {@code performBonemeal} + * 直到长满。覆盖作物、树苗、菌类、藤蔓等。对原版骨粉无效的方块不起作用 + * ——甘蔗、仙人掌就没实现 {@code BonemealableBlock}。
  • + *
  • 强制生长({@code beef_force_grow}):改用「随机刻」推进方块。 + * 凡是 {@code isRandomlyTicking()} 的方块都能被推起来,因此甘蔗、仙人掌、 + * 竹子这些骨粉无效的方块也长;南瓜/西瓜茎长满后靠随机刻结果实,同样靠它补上。
  • + *
+ * + *

两个开关可以同时开:催熟先把骨粉能做的做完,没推进时才轮到强制生长补刀。 + * 只开强制生长也能单独工作(此时作物靠随机刻慢慢长)。 + * + *

强制生长为什么不设白名单:它本质是「无条件推进随机刻」,而随机刻行为 + * 不止生长——树叶会枯萎掉落、火会蔓延、雪冰会融化、耕地会退化成泥土、紫颂花在无法 + * 继续生长时会打掉自己。任何白名单都只能覆盖原版方块、对模组方块失明。与其猜,不如 + * 把判断权交给玩家:默认关闭 + tooltip 里明确写清代价。 + * + *

两条路径的「停止条件」刻意不同: + *

    + *
  • 骨粉路径严格:方块状态一旦没变化就立刻停。草方块长花、菌岩蔓延这类 + * 传播型方块的 {@code isValidBonemealTarget} 会一直为真,不严格停就会 + * 重复几十次、刷出一整片花海并造成卡顿。
  • + *
  • 随机刻路径宽容:一次作用里连推 {@link #MAX_RANDOM_TICKS} 次随机刻。 + * 因为这类生长本身带随机概率(仙人掌每刻只有一定概率长龄,茎结果实更是概率事件), + * 一次没动静不代表不会长;而每次随机刻只是几次方块读取,成本极低。 + * 唯一的例外是草方块/菌岩这类「向邻居散布」的方块,见 {@link #SPREADER_RANDOM_TICKS}。
  • + *
+ * + *

按住连锁键(Tab)时会整片作用,扫描规则与「顺手收菜」共用 + * ({@link RightClickChainer} 的等价组 + X/Y/Z 范围 + 数量上限)。 + * + *

与打火石 / 顺手收菜保持一致:潜行右键时让出这次交互,交给其它模组处理。 + * 造化杖本身不可损坏,因此不消耗任何物品。 + */ +public final class BeefRipen { + + /** 骨粉路径:单个方块最多施加多少次骨粉效果(正常情况下 2~4 次即可长满)。 */ + private static final int MAX_STEPS = 32; + + /** 随机刻路径:普通方块最多推进多少次随机刻。 */ + private static final int MAX_RANDOM_TICKS = 128; + + /** + * 随机刻路径:传播型方块(草方块、菌岩)单独限到很少几次。 + * + *

它们每次随机刻都要在周围扫一片区域做散布尝试(草方块一次约 128 次), + * 而自身方块状态永远不变——因此会一直吃满上限,给 128 次就变成 + * 上万次大范围散布尝试,纯属浪费且会卡顿。其余方块每次随机刻只有几次方块读取, + * 给满额几乎无成本。 + */ + private static final int SPREADER_RANDOM_TICKS = 4; + + /** 骨粉粒子(happy villager)的 LevelEvent id。 */ + private static final int BONE_MEAL_PARTICLES = 2005; + + /** + * 藤蔓「强制蔓延」每次作用最多新增几株。 + * + *

按住连锁键(Tab)时整片作用,每株都会按这个上限放大,取值不宜太大。

+ */ + private static final int MAX_VINE_SPREAD_PER_ACTION = 4; + + private BeefRipen() { + } + + /** + * 尝试执行一次催熟 / 强制生长(按住连锁键时整片作用)。 + * + * @return {@link InteractionResult#PASS} 表示该位置没有可作用的目标, + * 交由后续行为链继续处理。 + */ + public static InteractionResult tryUse(UseOnContext ctx) { + ItemStack stack = ctx.getItemInHand(); + Player player = ctx.getPlayer(); + if (player == null || player.isShiftKeyDown()) { + return InteractionResult.PASS; + } + + boolean bonemealMode = EndlessBeafItem.isRipenEnabled(stack); + boolean forceGrowMode = EndlessBeafItem.isForceGrowEnabled(stack); + if (!bonemealMode && !forceGrowMode) { + return InteractionResult.PASS; + } + + Level level = ctx.getLevel(); + BlockPos pos = ctx.getClickedPos(); + if (!canRipen(level.getBlockState(pos), level, pos, stack)) { + return InteractionResult.PASS; + } + + // 两端都消费本次交互,避免客户端反复摆动 + if (level.isClientSide) { + return InteractionResult.SUCCESS; + } + if (!(level instanceof ServerLevel serverLevel)) { + return InteractionResult.PASS; + } + + // 按住连锁键时整片作用;Tab 状态由服务端维护,两端判定一致 + int grown = RightClickChainer.shouldChain(player, stack) + ? RightClickChainer.ripenBlocks(serverLevel, pos, player, stack) + : (ripenAt(serverLevel, pos, player, stack) ? 1 : 0); + + if (grown == 0) { + return InteractionResult.PASS; + } + + // 整片只响一次,避免叠成噪音(粒子在 ripenAt 里逐块播) + serverLevel.playSound(null, pos, SoundEvents.BONE_MEAL_USE, SoundSource.BLOCKS, 1.0F, 1.0F); + if (player instanceof ServerPlayer serverPlayer) { + CriteriaTriggers.ITEM_USED_ON_BLOCK.trigger(serverPlayer, pos, stack); + serverPlayer.awardStat(Stats.ITEM_USED.get(stack.getItem())); + } + return InteractionResult.sidedSuccess(false); + } + + /** + * 该方块状态当前是否可被作用(客户端也用它做交互预测)。 + * + *

判定完全取决于两个开关:催熟只认骨粉目标与「特殊催生目标」(藤蔓 / 花), + * 强制生长只认「会随机刻」。

+ */ + public static boolean canRipen(BlockState state, LevelReader level, BlockPos pos, ItemStack stack) { + if (EndlessBeafItem.isRipenEnabled(stack) + && (isBonemealTarget(state, level, pos) || isSpecialRipenTarget(state))) { + return true; + } + return EndlessBeafItem.isForceGrowEnabled(stack) && state.isRandomlyTicking(); + } + + /** + * 「特殊催生目标」:既不是原版骨粉目标、也不靠随机刻的植物,需要自写逻辑。 + * + *
    + *
  • 藤蔓 {@link VineBlock}:不是 {@code BonemealableBlock},催熟路径完全覆盖不到; + * 强制生长虽能靠随机刻推它,但受游戏规则 {@code doVinesSpread}、25% 概率与 + * 「9×3×9 内最多 5 株」三重限制。
  • + *
  • 花 {@link FlowerBlock}:既不是 {@code BonemealableBlock},也没有 + * {@code randomTicks()},两个开关原本都无效。原版对花本身没有任何骨粉行为。
  • + *
+ * + *

凋灵玫瑰 {@code WitherRoseBlock} 继承自 {@link FlowerBlock},同样包含在内。

+ */ + private static boolean isSpecialRipenTarget(BlockState state) { + Block block = state.getBlock(); + return block instanceof VineBlock || block instanceof FlowerBlock; + } + + /** + * 服务端作用于单个方块:先走骨粉路径,再按需走随机刻路径。 + * + *

骨粉路径发 {@link BonemealEvent}(语义就是「玩家在施骨粉」,让其它模组可以否决); + * 随机刻路径不发该事件——它不是骨粉——改由 {@code ForgeHooks.canCropGrow} 把关, + * 这也正是 {@code randomTick} 自身会查的那个生长许可钩子。 + * + * @return 是否真的产生了作用(没有可作用目标、或事件被取消且未标记成功时返回 false) + */ + public static boolean ripenAt(ServerLevel level, BlockPos pos, Player player, ItemStack stack) { + return ripenAt(level, pos, player, stack, null); + } + + /** + * 与 {@link #ripenAt(ServerLevel, BlockPos, Player, ItemStack)} 相同,但可以把花的掉落物 + * 收集到 {@code dropCollector} 里延迟提交。 + * + *

连锁(Tab)时由 {@code RightClickChainer#ripenBlocks} 传入一个列表、整片收集后 + * 只调一次 {@code MiningUtils.handleDrops},避免逐株向 AE 发上千次请求; + * 传 {@code null} 表示立即走统一掉落出口。

+ */ + public static boolean ripenAt(ServerLevel level, BlockPos pos, Player player, ItemStack stack, + @Nullable List dropCollector) { + boolean bonemealMode = EndlessBeafItem.isRipenEnabled(stack); + boolean forceGrowMode = EndlessBeafItem.isForceGrowEnabled(stack); + + BlockState state = level.getBlockState(pos); + if (!canRipen(state, level, pos, stack)) { + return false; + } + + if (bonemealMode && isBonemealTarget(state, level, pos)) { + BonemealEvent event = new BonemealEvent(player, level, pos, state, stack); + if (MinecraftForge.EVENT_BUS.post(event)) { + return event.getResult() == net.minecraftforge.eventbus.api.Event.Result.ALLOW; + } + } + if (!ForgeHooks.onCropsGrowPre(level, pos, state, true)) { + return false; + } + + boolean bonemealApplied = false; + boolean changed = false; + + // ---------- 路径 1:骨粉(严格停止) ---------- + if (bonemealMode) { + for (int i = 0; i < MAX_STEPS; i++) { + BlockState before = level.getBlockState(pos); + if (!(before.getBlock() instanceof BonemealableBlock growable) + || !growable.isValidBonemealTarget(level, pos, before,false)) { + break; + } + + growable.performBonemeal(level, level.getRandom(), pos, before); + bonemealApplied = true; + + // 传播型骨粉(草方块长花、菌岩蔓延等)不会改变自身方块状态,只作用一次即可。 + if (level.getBlockState(pos) == before) { + break; + } + changed = true; + } + } + + // ---------- 路径 1.5:藤蔓 / 花的特殊催生(仅「催熟」开关下) ---------- + // 两者都不是原版骨粉目标:藤蔓不是 BonemealableBlock,花既非骨粉目标也无随机刻, + // 路径 1、2 都覆盖不到。挂在「催熟」开关下,与用户对「催生」的认知一致。 + if (bonemealMode && !changed) { + BlockState current = level.getBlockState(pos); + if (current.getBlock() instanceof VineBlock) { + changed = ripenVine(level, pos, current); + } else if (current.getBlock() instanceof FlowerBlock) { + changed = ripenFlower(level, pos, current, player, stack, dropCollector); + } + } + + // ---------- 路径 2:随机刻(按方块类型给上限) ---------- + // 只在骨粉没能让方块状态前进时才走:既覆盖骨粉不支持的甘蔗/仙人掌/竹子, + // 也覆盖「长满后靠随机刻结果」的南瓜/西瓜茎。 + if (forceGrowMode && !changed && level.getBlockState(pos).isRandomlyTicking()) { + int limit = randomTickLimit(level.getBlockState(pos)); + for (int i = 0; i < limit; i++) { + BlockState before = level.getBlockState(pos); + before.randomTick(level, pos, level.getRandom()); + if (level.getBlockState(pos) != before) { + changed = true; + } + } + } + + // 完全没动作(例如本来就长满、又没有随机刻可推)时返回 false,交回后续行为链 + if (!changed && !bonemealApplied) { + return false; + } + + level.levelEvent(BONE_MEAL_PARTICLES, pos, 0); + return true; + } + + /** 该方块当前是否为有效的原版骨粉目标。 */ + private static boolean isBonemealTarget(BlockState state, LevelReader level, BlockPos pos) { + return state.getBlock() instanceof BonemealableBlock target + && target.isValidBonemealTarget(level, pos, state,false); + } + + /** + * 单次作用里最多推进多少次随机刻。 + * + *

普通方块给 {@link #MAX_RANDOM_TICKS}:这类生长本身带随机概率(仙人掌每刻只有一定 + * 概率长龄,茎结果实更是概率事件),需要足够多次尝试才可靠;而每次随机刻只是几次方块读取, + * 成本极低。 + * + *

草方块 / 菌岩这类「向邻居散布」的方块单独限到 {@link #SPREADER_RANDOM_TICKS}。 + * 这里直接借用原版 {@link BonemealableBlock#getType()} 的判定,不需要自己维护白名单—— + * 它本来就是原版为区分「自身生长」与「向邻居散布」而提供的信号。 + */ + private static int randomTickLimit(BlockState state) { + if (state.getBlock() instanceof net.minecraft.world.level.block.GrassBlock + || state.getBlock() instanceof net.minecraft.world.level.block.NyliumBlock) { + return SPREADER_RANDOM_TICKS; + } + return MAX_RANDOM_TICKS; + } + + /** + * 藤蔓的「强制蔓延」:不看游戏规则 {@code doVinesSpread}、不看原版 25% 概率、 + * 也不看原版「9×3×9 内最多 5 株」的上限,每次点击必定长出新藤蔓。 + * + *

原版逻辑在 {@code VineBlock#randomTick}(protected,无法直接调用),这里用公开 API + * 复刻它的三条分支:向上补面、向水平邻格蔓延、向下复制一份。每次调用最多新增 + * {@link #MAX_VINE_SPREAD_PER_ACTION} 株。

+ * + *

成功判定直接取 {@code level.setBlock} 的返回值 —— 蔓延改的可能是邻居方块, + * 只比较 {@code pos} 自身状态会漏判。

+ */ + private static boolean ripenVine(ServerLevel level, BlockPos pos, BlockState state) { + RandomSource random = level.getRandom(); + boolean acted = false; + + for (int placed = 0; placed < MAX_VINE_SPREAD_PER_ACTION; placed++) { + BlockState current = level.getBlockState(pos); + if (!(current.getBlock() instanceof VineBlock)) { + break; + } + + // ① 上方能附着 → 给自己补一个 UP 面(藤蔓自上方垂下) + if (!current.getValue(VineBlock.UP) + && MultifaceBlock.canAttachTo(level, Direction.DOWN, pos.above(), + level.getBlockState(pos.above()))) { + acted |= level.setBlock(pos, current.setValue(VineBlock.UP, true), 3); + continue; + } + + Direction direction = Direction.Plane.HORIZONTAL.getRandomDirection(random); + BlockPos side = pos.relative(direction); + BlockState sideState = level.getBlockState(side); + + if (sideState.isAir()) { + // 在空气邻格放一株新藤蔓:找一个相邻的实体方块作为附着面 + if (tryPlaceVineAt(level, side, random)) { + acted = true; + continue; + } + } else if (!current.getValue(VineBlock.getPropertyForFace(direction)) + && MultifaceBlock.canAttachTo(level, direction, side, sideState)) { + // 邻格能支撑 → 给自己这一面也长上藤蔓 + acted |= level.setBlock(pos, current.setValue(VineBlock.getPropertyForFace(direction), true), 3); + continue; + } + + // ③ 向下蔓延:把当前各面复制一份到下方(下方是空气或藤蔓) + BlockPos below = pos.below(); + BlockState belowState = level.getBlockState(below); + if (belowState.isAir() || belowState.getBlock() instanceof VineBlock) { + BlockState base = belowState.getBlock() instanceof VineBlock + ? belowState + : Blocks.VINE.defaultBlockState(); + BlockState copied = copyVineFaces(current, base, random); + if (hasHorizontalFace(copied)) { + acted |= level.setBlock(below, copied, 3); + } + } + } + + return acted; + } + + /** + * 在空气格 {@code pos} 放一株藤蔓:找一个相邻实体方块作为附着面,必要时退化为一株自上方垂下的藤蔓。 + * + * @return 是否真的放下了 + */ + private static boolean tryPlaceVineAt(ServerLevel level, BlockPos pos, RandomSource random) { + for (Direction face : Direction.Plane.HORIZONTAL) { + BlockPos anchor = pos.relative(face); + if (MultifaceBlock.canAttachTo(level, face, anchor, level.getBlockState(anchor))) { + return level.setBlock(pos, Blocks.VINE.defaultBlockState() + .setValue(VineBlock.getPropertyForFace(face), true), 3); + } + } + if (MultifaceBlock.canAttachTo(level, Direction.DOWN, pos.above(), level.getBlockState(pos.above()))) { + return level.setBlock(pos, Blocks.VINE.defaultBlockState().setValue(VineBlock.UP, true), 3); + } + return false; + } + + /** 复刻原版 {@code VineBlock#copyRandomFaces}:随机把源藤蔓的水平面复制到目标上。 */ + private static BlockState copyVineFaces(BlockState source, BlockState target, RandomSource random) { + BlockState result = target; + for (Direction direction : Direction.Plane.HORIZONTAL) { + if (random.nextBoolean()) { + BooleanProperty property = VineBlock.getPropertyForFace(direction); + if (source.getValue(property)) { + result = result.setValue(property, true); + } + } + } + return result; + } + + private static boolean hasHorizontalFace(BlockState state) { + return state.getValue(VineBlock.NORTH) || state.getValue(VineBlock.EAST) + || state.getValue(VineBlock.SOUTH) || state.getValue(VineBlock.WEST); + } + + /** + * 花的「催生」:只产出该花的掉落物,花本体原地保留、也不增殖新花。 + * + *

掉落走统一出口 {@code MiningUtils.handleDrops}(AE 存储优先 → 范围磁力), + * 与挖掘、杀怪、收菜保持一致。连锁时改为收集到 {@code dropCollector} 延迟提交。

+ */ + private static boolean ripenFlower(ServerLevel level, BlockPos pos, BlockState state, Player player, + ItemStack stack, @Nullable List dropCollector) { + List drops = Block.getDrops(state, level, pos, null, player, stack); + if (drops.isEmpty()) { + return false; + } + + if (dropCollector != null) { + dropCollector.addAll(drops); + } else { + MiningUtils.handleDrops(player, MiningUtils.mergeItemStacks(drops), stack, Vec3.atCenterOf(pos)); + } + return true; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/items/BeefTeleportHandler.java b/src/main/java/com/sorrowmist/useless/content/items/BeefTeleportHandler.java new file mode 100644 index 00000000..a8b010b1 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/items/BeefTeleportHandler.java @@ -0,0 +1,198 @@ +package com.sorrowmist.useless.content.items; + +import com.sorrowmist.useless.compat.enderio.EnderIOTravelCompat; +import com.sorrowmist.useless.core.config.ConfigManager; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.Pose; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.entity.EntityTeleportEvent; + +import java.util.Optional; + +/** + * Handles the beef tool's built-in blink when Ender IO is not available. + * + *

The destination calculation and collision checks are adapted from Ender IO's + * {@code TravelHandler#teleportPosition}: + * Ender IO TravelHandler.

+ */ +final class BeefTeleportHandler { + private BeefTeleportHandler() { + } + + /** + * 读取本次闪现使用的最大距离。 + * + *

距离由本模组的 {@code beef_short_teleport_range} 配置项决定,不再委托 Ender IO 的 + * {@code TravelHandler.shortTeleport}:后者的距离固定取自其自身配置,无法受本模组控制。

+ */ + private static int blinkRange() { + return ConfigManager.getBeefShortTeleportRange(); + } + + /** + * 锚点传送(不潜行时的右键传送),只在装载 Ender IO 时可用。 + */ + static InteractionResult tryAnchorTeleport(Level level, Player player, boolean enderIoLoaded) { + return enderIoLoaded + ? EnderIOTravelCompat.tryAnchorTeleport(level, player) + : InteractionResult.PASS; + } + + /** + * 短距闪现(默认 24 格,距离由 {@code beef_short_teleport_range} 配置)。 + * + *

潜行判定已经上移到快捷键路径,这里不再读取潜行状态:闪现由独立快捷键触发, + * 服务端收到按键包后直接调用本方法。落点始终由本模组计算,以保证配置项生效—— + * Ender IO 的 {@code TravelHandler.shortTeleport} 只读取其自身配置,不受本模组控制。

+ */ + static InteractionResult tryShortTeleport(Level level, Player player) { + Optional target = teleportPosition(level, player); + if (target.isEmpty()) { + return InteractionResult.PASS; + } + + // The server performs the actual teleport. The client only confirms that its local + // collision view has a valid destination so the interaction packet is sent normally. + if (level.isClientSide()) { + return InteractionResult.SUCCESS; + } + + Optional eventTarget = postTeleportEvent(player, target.get()); + if (eventTarget.isEmpty()) { + level.playSound(null, player.blockPosition(), SoundEvents.DISPENSER_FAIL, + SoundSource.PLAYERS, 1.0F, 1.0F); + return InteractionResult.SUCCESS; + } + + Vec3 destination = eventTarget.get(); + player.teleportTo(destination.x(), destination.y(), destination.z()); + if (player instanceof ServerPlayer serverPlayer) { + serverPlayer.connection.resetPosition(); + } + player.resetFallDistance(); + if (player.isInWall()) { + player.setPose(Pose.SWIMMING); + } + player.playNotifySound(SoundEvents.ENDERMAN_TELEPORT, SoundSource.PLAYERS, 1.0F, 1.0F); + return InteractionResult.SUCCESS; + } + + private static Optional teleportPosition(Level level, Player player) { + BlockPos target = null; + double floorHeight = 0.0D; + + Vec3 playerPosition = player.getEyePosition(); + Vec3 look = player.getLookAngle().normalize(); + Vec3 end = playerPosition.add(look.scale(blinkRange())); + ClipContext clipContext = new ClipContext( + playerPosition, + end, + ClipContext.Block.OUTLINE, + ClipContext.Fluid.NONE, + (net.minecraft.world.entity.Entity)null); + BlockHitResult hit = level.clip(clipContext); + + if (hit.getType() == HitResult.Type.MISS) { + target = hit.getBlockPos(); + } else if (hit.getType() == HitResult.Type.BLOCK) { + Direction direction = hit.getDirection(); + if (direction == Direction.UP) { + target = hit.getBlockPos(); + } else if (direction == Direction.DOWN) { + target = hit.getBlockPos().below((int) Math.ceil(player.getBbHeight())); + } else { + target = hit.getBlockPos().relative(direction); + if (level.getBlockState(target).getCollisionShape(level, target).isEmpty()) { + target = target.below(); + } + } + } + + // When the first collision is close, scan through it to find the next open position. + if (playerPosition.distanceToSqr(hit.getLocation()) < 9.0D) { + Vec3 traverseFrom = hit.getLocation().add(look.scale(0.01D)); + BlockPos failPosition = new BlockPos(0, Integer.MAX_VALUE, 0); + boolean aimingUp = look.y > 0.5D; + BlockPos traversedTarget = BlockGetter.traverseBlocks( + traverseFrom, + end, + clipContext, + (traverseContext, traversePosition) -> { + if (!aimingUp) { + BlockPos below = traversalCheck(level, traversePosition.below()); + if (below != null) { + return below; + } + } + return traversalCheck(level, traversePosition); + }, + failContext -> failPosition); + if (traversedTarget != failPosition) { + target = traversedTarget.immutable(); + } + } + + if (target != null) { + Optional ground = isTeleportPositionClear(level, target.below()); + if (ground.isPresent()) { + floorHeight = ground.get(); + } else { + target = null; + } + } + + if (target == null || player.blockPosition().distManhattan(target) < 2) { + return Optional.empty(); + } + return Optional.of(Vec3.atBottomCenterOf(target).add(0.0D, floorHeight, 0.0D)); + } + + private static BlockPos traversalCheck(Level level, BlockPos position) { + BlockState state = level.getBlockState(position); + if (state.getCollisionShape(level, position).isEmpty() + && isTeleportPositionClear(level, position.below()).isPresent()) { + return position; + } + return null; + } + + private static Optional isTeleportPositionClear(BlockGetter level, BlockPos target) { + if (level.isOutsideBuildHeight(target)) { + return Optional.empty(); + } + + BlockPos above = target.above(); + double height = level.getBlockState(above).getCollisionShape(level, above).max(Direction.Axis.Y); + if (height <= 0.2D) { + return Optional.of(Math.max(height, 0.0D)); + } + + above = above.above(); + if (level.getBlockState(above).getCollisionShape(level, above).isEmpty()) { + return Optional.of(Math.max(height, 0.0D)); + } + return Optional.empty(); + } + + private static Optional postTeleportEvent(Player player, Vec3 target) { + EntityTeleportEvent event = new EntityTeleportEvent(player, target.x(), target.y(), target.z()); + if (MinecraftForge.EVENT_BUS.post(event)) { + return Optional.empty(); + } + return Optional.of(new Vec3(event.getTargetX(), event.getTargetY(), event.getTargetZ())); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/items/BeefTimeAcceleration.java b/src/main/java/com/sorrowmist/useless/content/items/BeefTimeAcceleration.java new file mode 100644 index 00000000..065fa6a9 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/items/BeefTimeAcceleration.java @@ -0,0 +1,257 @@ +package com.sorrowmist.useless.content.items; + +import com.sorrowmist.useless.api.entity.BeeTimerAccess; +import com.sorrowmist.useless.api.entity.TurtleTimerAccess; + +import com.sorrowmist.useless.content.entities.BeefTimeAccelerationEntity; +import com.sorrowmist.useless.core.component.UComponents; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.RandomSource; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.AgeableMob; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.animal.Bee; +import net.minecraft.world.entity.animal.Chicken; +import net.minecraft.world.entity.animal.Sheep; +import net.minecraft.world.entity.animal.Turtle; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.AABB; + +import java.util.List; + +public final class BeefTimeAcceleration { + private static final int RANDOM_TICK_CHANCE = 1365; + + private BeefTimeAcceleration() { + } + + public static InteractionResult tryUse(UseOnContext context) { + ItemStack stack = context.getItemInHand(); + Player player = context.getPlayer(); + if (!shouldBlockOtherRightClick(stack, player)) { + return InteractionResult.PASS; + } + + Level level = context.getLevel(); + BlockPos pos = context.getClickedPos(); + + if (level.isClientSide) { + return InteractionResult.SUCCESS; + } + if (!(level instanceof ServerLevel serverLevel)) { + return InteractionResult.PASS; + } + + BlockState state = serverLevel.getBlockState(pos); + BlockEntity blockEntity = serverLevel.getBlockEntity(pos); + if (!isValidTarget(serverLevel, state, blockEntity)) { + return InteractionResult.sidedSuccess(level.isClientSide); + } + + BlockPos immutablePos = pos.immutable(); + List entities = serverLevel.getEntitiesOfClass( + BeefTimeAccelerationEntity.class, + new AABB(immutablePos), + entity -> entity.getTargetPos().equals(immutablePos) + ); + BeefTimeAccelerationEntity effect = entities.stream().findFirst().orElse(null); + if (effect == null) { + serverLevel.addFreshEntity(new BeefTimeAccelerationEntity(serverLevel, immutablePos)); + playSound(serverLevel, immutablePos, 1); + return InteractionResult.sidedSuccess(level.isClientSide); + } + + int nextSpeed = nextTickSpeed(effect.getTickSpeed(), BeefTimeAccelerationEntity.MAX_TICK_SPEED); + if (nextSpeed < 0) { + return InteractionResult.sidedSuccess(level.isClientSide); + } + + effect.setTickSpeed(nextSpeed); + effect.setRemainingTime(refreshRemainingTime(effect.getTotalTime(), effect.getRemainingTime())); + playSound(serverLevel, immutablePos, nextSpeed); + return InteractionResult.sidedSuccess(level.isClientSide); + } + + /** + * 对「生物」施加时间加速(潜行右键生物,由「生物加速」模式驱动)。 + * + *

与方块版共用同一套载体实体({@link BeefTimeAccelerationEntity})、叠加速度、 + * 续期与悬浮文字,只是目标换成实体。重复右键同一只生物会叠加速度并续期。

+ */ + public static InteractionResult tryUseOnEntity(ItemStack stack, Player player, LivingEntity target) { + if (target instanceof Player) { + return InteractionResult.PASS; + } + + Level level = target.level(); + if (level.isClientSide) { + // 纯预测,零副作用 + return InteractionResult.SUCCESS; + } + if (!(level instanceof ServerLevel serverLevel)) { + return InteractionResult.PASS; + } + if (target.isRemoved() || !target.isAlive()) { + return InteractionResult.PASS; + } + + // 注意查询盒必须放大:载体悬在目标头顶(y + 身高 + 0.4), + // 落在目标包围盒之外,用 new AABB(blockPosition()) 永远查不到 → 每次右键都会重复新建。 + List found = serverLevel.getEntitiesOfClass( + BeefTimeAccelerationEntity.class, + target.getBoundingBox().inflate(2.0D), + entity -> entity.isEntityTarget() && entity.getTargetEntityId() == target.getId() + ); + BeefTimeAccelerationEntity effect = found.stream().findFirst().orElse(null); + if (effect == null) { + serverLevel.addFreshEntity(new BeefTimeAccelerationEntity(serverLevel, target)); + playSound(serverLevel, target.blockPosition(), 1); + return InteractionResult.sidedSuccess(false); + } + + int nextSpeed = nextTickSpeed(effect.getTickSpeed(), BeefTimeAccelerationEntity.MAX_ENTITY_TICK_SPEED); + if (nextSpeed < 0) { + return InteractionResult.sidedSuccess(false); + } + + effect.setTickSpeed(nextSpeed); + effect.setRemainingTime(refreshRemainingTime(effect.getTotalTime(), effect.getRemainingTime())); + playSound(serverLevel, target.blockPosition(), nextSpeed); + return InteractionResult.sidedSuccess(false); + } + + /** + * 只推进生物身上的「计时器」类数值,不跑 AI / 寻路 / 移动 / 物理。 + * + *

因此生物不会走得变快、不会抖动,开销也远小于额外跑 tick。推进量 + * {@code extra = 1 << min(tickSpeed, MAX_ENTITY_TICK_SPEED)}。

+ * + *

覆盖:幼年成长与繁殖后冷却({@code AgeableMob.age})、鸡下蛋间隔、 + * 羊剪毛后长毛、蜜蜂不疲倦/立刻进巢/采蜜、海龟下蛋倒计时; + * 装了新生魔艺时还覆盖其工作生物——德格米 / 探宝蟹 / 旋风精灵(见 + * {@link ArsCreatureTimeCompatLoader},它们的进度多在各自的方块实体上)。

+ * + *

刻意不推 {@code Animal.inLove}:它是「发情窗口」(600 tick), + * {@code BreedGoal} 靠它驱动配对,推快会让窗口瞬间归零、动物永远来不及找到配偶, + * 等于禁止繁殖。真正的「繁殖冷却」是繁殖成功后的 {@code setAge(6000)},已由 age 覆盖。 + * 也刻意不推 {@code LivingEntity.tickEffects()}(药水时长)。

+ */ + public static void accelerateEntityTimers(LivingEntity target, int tickSpeed) { + int extra = 1 << Math.min(tickSpeed, BeefTimeAccelerationEntity.MAX_ENTITY_TICK_SPEED); + + // ① 幼年成长(age < 0 向 0 推进)+ 繁殖后冷却(age > 0 向 0 递减) + // 覆盖绝大多数动物、村民与蝌蚪;繁殖成功后 Animal 会 setAge(6000) 也走这里。 + if (target instanceof AgeableMob ageable) { + int age = ageable.getAge(); + if (age != 0) { + int step = Math.min(Math.abs(age), extra); + ageable.setAge(age < 0 ? age + step : age - step); + } + } + + // ② 鸡下蛋间隔:eggTime 是 public 字段,扣到 0 后由原版 aiStep 在下一 tick 下蛋并重置 + if (target instanceof Chicken chicken && !chicken.isBaby() && !chicken.isChickenJockey()) { + chicken.eggTime = Math.max(0, chicken.eggTime - extra); + } + + // ③ 羊:剪毛后长毛。原版是「吃草后长毛」的事件驱动,这里直接恢复(保留毛色) + if (target instanceof Sheep sheep && !sheep.isBaby() && sheep.isSheared()) { + sheep.setSheared(false); + } + + // ④ 蜜蜂:不再「疲倦」、允许立刻进巢;采蜜状态需访问接口(setHasNectar 为包级私有) + if (target instanceof Bee bee) { + bee.resetTicksWithoutNectarSinceExitingHive(); + bee.setStayOutOfHiveCountdown(0); + if (!bee.hasNectar()) { + ((BeeTimerAccess) bee).uselessMod$setBeeHasNectar(true); + } + } + + // ⑤ 海龟:下蛋倒计时(layEggCounter 为包级私有,需访问接口) + if (target instanceof Turtle turtle && turtle.isLayingEgg()) { + TurtleTimerAccess accessor = (TurtleTimerAccess) turtle; + accessor.uselessMod$setLayEggCounter(accessor.uselessMod$getLayEggCounter() + extra); + } + + // ⑥ 新生魔艺的工作生物:德格米 / 探宝蟹 / 旋风精灵(进度多在它们的方块实体上)。 + // 同样只写数值、不跑 AI;未加载 Ars Nouveau 时反射桥为 null,本行是空操作。 + com.sorrowmist.useless.compat.forge1201.OptionalIntegration.call("ars_nouveau","com.sorrowmist.useless.compat.ars.ArsCreatureTimeCompatLoader","accelerate",Void.class,null,new Class[]{net.minecraft.world.entity.LivingEntity.class,int.class},target,extra); + } + + public static boolean shouldBlockOtherRightClick(ItemStack stack, Player player) { + return player != null + && player.isShiftKeyDown() + && com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.BeefTimeAccelerationEnabledComponent.get(), false); + } + + static int nextTickSpeed(int currentTickSpeed, int maxTickSpeed) { + int next = currentTickSpeed + 1; + return next <= maxTickSpeed ? next : -1; + } + + static int refreshRemainingTime(int remainingTicks) { + return refreshRemainingTime(BeefTimeAccelerationEntity.DEFAULT_TOTAL_TIME, remainingTicks); + } + + static int refreshRemainingTime(int totalTime, int remainingTicks) { + return remainingTicks + Math.max(0, totalTime - remainingTicks) / 2; + } + + public static boolean isValidTarget(ServerLevel level, BlockState state, BlockEntity blockEntity) { + if (blockEntity == null) { + return state.isRandomlyTicking(); + } + return state.getTicker(level, blockEntity.getType()) != null; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + public static void tickTarget(ServerLevel level, + BlockPos pos, + BlockState state, + BlockEntity blockEntity, + int tickSpeed) { + int extraTicks = 1 << tickSpeed; + if (blockEntity != null) { + BlockEntityTicker ticker = state.getTicker(level, blockEntity.getType()); + if (ticker == null) { + return; + } + for (int i = 0; i < extraTicks; i++) { + ticker.tick(level, pos, state, blockEntity); + } + return; + } + + if (!state.isRandomlyTicking()) { + return; + } + RandomSource random = level.getRandom(); + for (int i = 0; i < extraTicks; i++) { + if (random.nextInt(RANDOM_TICK_CHANCE) == 0) { + state.randomTick(level, pos, random); + } + } + } + + private static void playSound(ServerLevel level, BlockPos pos, int tickSpeed) { + level.playSound( + null, + pos, + SoundEvents.NOTE_BLOCK_IRON_XYLOPHONE.value(), + SoundSource.PLAYERS, + 1.0F, + (float) Math.pow(2.0D, (tickSpeed - 5) / 12.0D) + ); + } + +} diff --git a/src/main/java/com/sorrowmist/useless/content/items/BeefToolVariants.java b/src/main/java/com/sorrowmist/useless/content/items/BeefToolVariants.java new file mode 100644 index 00000000..5b468e31 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/items/BeefToolVariants.java @@ -0,0 +1,79 @@ +package com.sorrowmist.useless.content.items; + +import com.sorrowmist.useless.api.enums.tool.ToolTypeMode; +import com.sorrowmist.useless.core.component.UComponents; +import com.sorrowmist.useless.init.ModItems; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public final class BeefToolVariants { + private BeefToolVariants() {} + + public static boolean isBaseVariant(ItemStack stack) { + return stack.getItem() == ModItems.ENDLESS_BEAF_ITEM.get() + || stack.getItem() == ModItems.ENDLESS_BEAF_ITEM_NO_WRENCH.get(); + } + + /** + * 判断物品是否为造化杖(牛排工具)的任意变体。 + *

+ * 凡属于 {@link EndlessBeafItem} 的物品都算,包含基础形态、关闭扳手标记的 + * {@code endless_beaf_item_no_wrench} 以及各工具模式子物品。 + */ + public static boolean isBeafTool(@Nullable ItemStack stack) { + return stack != null && !stack.isEmpty() && stack.getItem() instanceof EndlessBeafItem; + } + + /** + * 枚举造化杖的全部变体物品。 + *

+ * 以 {@link EndlessBeafItem} 类型为准直接从注册表收集,保证与 + * {@link #isBeafTool(ItemStack)} 的判定范围始终一致:新增变体(例如关闭扳手标记的 + * {@code endless_beaf_item_no_wrench})后,需要枚举具体物品的场景(模具 Ingredient、 + * JEI 展示等)不会漏项。 + */ + public static List allVariantItems() { + List items = new ArrayList<>(); + for (Item item : BuiltInRegistries.ITEM) { + if (item instanceof EndlessBeafItem) { + items.add(item); + } + } + return List.copyOf(items); + } + + public static boolean isWrenchTagEnabled(ItemStack stack) { + return com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.WrenchTagEnabledComponent.get(), true); + } + + public static ItemStack withWrenchTag(ItemStack source, boolean enabled) { + ItemStack result = new ItemStack( + enabled ? ModItems.ENDLESS_BEAF_ITEM.get() : ModItems.ENDLESS_BEAF_ITEM_NO_WRENCH.get()); + UComponents.copyState(source, result); + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(result, UComponents.WrenchTagEnabledComponent.get(), enabled); + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(result, UComponents.CurrentToolTypeComponent.get(), ToolTypeMode.NONE_MODE); + return result; + } + + public static ItemStack createForToolMode(ItemStack source, ToolTypeMode mode) { + ItemStack result = switch (mode) { + case NONE_MODE -> withWrenchTag(source, isWrenchTagEnabled(source)); + case WRENCH_MODE -> new ItemStack(ModItems.ENDLESS_BEAF_WRENCH.get()); + case SCREWDRIVER_MODE -> new ItemStack(ModItems.ENDLESS_BEAF_SCREWDRIVER.get()); + case MALLET_MODE -> new ItemStack(ModItems.ENDLESS_BEAF_MALLET.get()); + case CROWBAR_MODE -> new ItemStack(ModItems.ENDLESS_BEAF_CROWBAR.get()); + case HAMMER_MODE -> new ItemStack(ModItems.ENDLESS_BEAF_HAMMER.get()); + case OMNITOOL_MODE -> ItemStack.EMPTY; + }; + if (!result.isEmpty()) { + UComponents.copyState(source, result); + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(result, UComponents.CurrentToolTypeComponent.get(), mode); + } + return result; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/items/EndlessBeafItem.java b/src/main/java/com/sorrowmist/useless/content/items/EndlessBeafItem.java new file mode 100644 index 00000000..bdeedbe6 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/items/EndlessBeafItem.java @@ -0,0 +1,2070 @@ +package com.sorrowmist.useless.content.items; + +import com.sorrowmist.useless.api.enums.tool.EnchantMode; +import com.sorrowmist.useless.api.enums.tool.ToolTypeMode; +import com.sorrowmist.useless.client.BeefAutoClicker; +import com.sorrowmist.useless.client.BeefTooltipPager; +import com.sorrowmist.useless.client.TooltipPageState; +import com.sorrowmist.useless.content.blocks.GlowPlasticBlock; +import com.sorrowmist.useless.content.blocks.UselessGlassBlock; +import com.sorrowmist.useless.compat.enderio.EnderIOTravelCompat; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeCatalog; +import com.sorrowmist.useless.core.common.KeyBindings; +import com.sorrowmist.useless.core.component.UComponents; +import com.sorrowmist.useless.core.config.BeefToolProtectionManager; +import com.sorrowmist.useless.core.config.ConfigManager; +import com.sorrowmist.useless.init.ModDamageTypes; +import com.sorrowmist.useless.init.ModTags; +import com.sorrowmist.useless.utils.EnchantmentUtil; +import com.sorrowmist.useless.utils.UselessItemUtils; +import com.sorrowmist.useless.utils.mining.MiningUtils; +import com.sorrowmist.useless.utils.mining.RightClickChainer; +import net.minecraft.ChatFormatting; +import net.minecraft.advancements.CriteriaTriggers; +import net.minecraft.client.KeyMapping; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.particles.BlockParticleOption; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.tags.BlockTags; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.InteractionResultHolder; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.AgeableMob; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.HumanoidArm; +import net.minecraft.world.entity.LightningBolt; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.OwnableEntity; +import net.minecraft.world.entity.Pose; +import net.minecraft.world.entity.ai.attributes.AttributeInstance; +import net.minecraft.world.entity.ai.attributes.AttributeModifier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.boss.enderdragon.EnderDragon; +import net.minecraft.world.entity.boss.wither.WitherBoss; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.entity.monster.Creeper; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.entity.projectile.ProjectileUtil; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Rarity; +import net.minecraft.world.item.TieredItem; +import net.minecraft.world.item.Tiers; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.item.UseAnim; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.item.enchantment.Enchantments; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.BrushableBlock; +import net.minecraft.world.level.block.CandleCakeBlock; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BrushableBlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.gameevent.GameEvent; +import net.minecraft.world.level.entity.EntityLookup; +import net.minecraft.world.level.entity.LevelEntityGetter; +import net.minecraft.world.level.entity.LevelEntityGetterAdapter; +import net.minecraft.world.level.entity.PersistentEntitySectionManager; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.entity.PartEntity; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.common.ToolActions; +import net.minecraftforge.common.ToolAction; +import net.minecraftforge.common.IForgeShearable; +import net.minecraftforge.common.util.Lazy; +import net.minecraftforge.event.entity.living.LivingDeathEvent; +import net.minecraftforge.event.entity.living.LivingDropsEvent; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import com.sorrowmist.useless.compat.forge1201.component.ComponentDefaults; +import com.sorrowmist.useless.compat.forge1201.component.DataComponentType; +import java.util.stream.Collectors; + +public class EndlessBeafItem extends TieredItem implements ComponentDefaults { + private static final String AE2LT_NATURAL_LIGHTNING_TAG = "ae2lt.natural_weather_lightning"; + private static final Map FORCE_KILL_CONTEXTS = new ConcurrentHashMap<>(); + // 范围伤害重入保护:避免范围内实体受伤时再次触发范围伤害 + private static final Set AOE_DAMAGE_CONTEXT = ConcurrentHashMap.newKeySet(); + private static final int MAX_STANDARD_DAMAGE_ATTEMPTS = 100; + private static final int TELEPORT_COOLDOWN_TICKS = 5; + private final ToolTypeMode toolType; + private final boolean wrenchTagEnabled; + + public EndlessBeafItem() { + this(ToolTypeMode.NONE_MODE, true); + } + + public EndlessBeafItem(@Nullable ToolTypeMode toolType) { + this(toolType, true); + } + + public EndlessBeafItem(@Nullable ToolTypeMode toolType, boolean wrenchTagEnabled) { + super(Tiers.NETHERITE, createProperties(wrenchTagEnabled)); + this.toolType = toolType; + this.wrenchTagEnabled = wrenchTagEnabled; + } + + private static Item.Properties createProperties(boolean wrenchTagEnabled) { + return new Item.Properties().stacksTo(1).rarity(Rarity.EPIC); + } + + @Override + public Object componentDefault(DataComponentType type) { + if (!"useless_mod".equals(type.id().getNamespace())) return null; + return switch (type.id().getPath()) { + case "enchant_mode" -> EnchantMode.FORTUNE; + case "tool_type" -> ToolTypeMode.NONE_MODE; + case "wrench_tag_enabled" -> wrenchTagEnabled; + case "construction_wand_core" -> com.sorrowmist.useless.api.enums.tool.ConstructionWandCoreMode.DEFAULT; + case "beef_invulnerability_enabled", "beef_magnet_enabled", "beef_crop_harvest", + "beef_shears", "beef_flint_and_steel" -> true; + case "chain_mining", "force_mining", "auto_smelt", "force_kill_enabled", "beef_time_acceleration_enabled", "beef_advanced_stealth_enabled", "beef_capture_enabled", "beef_malum_spirit_enabled", "beef_mystical_agriculture_enabled", "beef_beheading_enabled", "beef_teleport_enabled", "beef_aoe_damage_enabled", "beef_kill_aura", "beef_protect_mode", "beef_farmland_mode", "beef_ripen", "beef_force_grow", "beef_entity_time_acceleration", "ae_storage_priority", "ae_network_connect", "beef_ritual_satchel", "exdeorum_crook", "exdeorum_hammer", "exdeorum_compressed_hammer", "beef_wireless_logistics", "construction_wand_enabled" -> false; + default -> null; + }; + } + + public static boolean isTeleportEnabled(ItemStack stack) { + return com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.BeefTeleportEnabledComponent.get(), false); + } + + public static void setTeleportEnabled(ItemStack stack, boolean enabled) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.BeefTeleportEnabledComponent.get(), enabled); + if (ModList.get().isLoaded(EnderIOTravelCompat.MOD_ID)) { + EnderIOTravelCompat.setTravelItemEnabled(stack, enabled); + } + } + + /** + * 右键泥土/草方块时的优先行为。 + * + *

false(默认)= 铲子优先,泥土/草方块变成草径;true = 锄头优先,变成耕地。 + * 造化杖同时具备铲子与锄头能力,而两条工具动作链是「谁先生效谁赢」, + * 所以用这个开关决定顺序。 + */ + public static boolean isFarmlandMode(ItemStack stack) { + return com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.BeefFarmlandModeComponent.get(), false); + } + + public static void setFarmlandMode(ItemStack stack, boolean enabled) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.BeefFarmlandModeComponent.get(), enabled); + } + + /** + * 「右键泥土」当前状态的文案:锄头优先(→ 耕地)或 铲子优先(→ 草径)。 + * 物品 tooltip 与模式轮盘共用同一份文案,避免两处口径不一致。 + */ + public static Component farmlandStateText(ItemStack stack) { + boolean farmland = isFarmlandMode(stack); + return Component.translatable(farmland + ? "tooltip.useless_mod.beef_farmland_state.hoe" + : "tooltip.useless_mod.beef_farmland_state.shovel" + ).withStyle(farmland ? ChatFormatting.GREEN : ChatFormatting.YELLOW); + } + + /** + * 是否由造化杖自己完成「右键收菜」:收获成熟作物并把种子留在地里。 + * + *

用于避免部分整合包的右键收菜功能在造化杖上把作物连根拔起。 + */ + public static boolean isCropHarvestEnabled(ItemStack stack) { + return com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.BeefCropHarvestComponent.get(), true); + } + + public static void setCropHarvestEnabled(ItemStack stack, boolean enabled) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.BeefCropHarvestComponent.get(), enabled); + } + + /** + * 是否启用剪刀功能:允许对羊、哞菇等 {@link IForgeShearable} 实体剪毛/剪掉落, + * 并对外声明 {@code DEFAULT_SHEARS_ACTIONS} 能力。 + */ + public static boolean isShearsEnabled(ItemStack stack) { + return com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.BeefShearsComponent.get(), true); + } + + public static void setShearsEnabled(ItemStack stack, boolean enabled) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.BeefShearsComponent.get(), enabled); + } + + /** + * 是否启用打火石功能:允许点燃营火/蜡烛,或在可放置火的位置点火。 + * + *

默认开启,与其他辅助功能(收菜、剪刀)保持一致;右键只有瞄准 + * 营火/蜡烛或可点火位置时才会生效,不影响普通方块交互。

+ */ + public static boolean isFlintAndSteelEnabled(ItemStack stack) { + return com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.BeefFlintAndSteelComponent.get(), true); + } + + public static void setFlintAndSteelEnabled(ItemStack stack, boolean enabled) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.BeefFlintAndSteelComponent.get(), enabled); + } + + /** + * 是否启用催熟功能:右键可骨粉方块时一键催到成熟,右键幼年动物时直接催至成年。 + * + *

默认关闭;潜行右键时把这次交互让给其它模组(与收菜、打火石一致)。

+ */ + public static boolean isRipenEnabled(ItemStack stack) { + return com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.BeefRipenComponent.get(), false); + } + + public static void setRipenEnabled(ItemStack stack, boolean enabled) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.BeefRipenComponent.get(), enabled); + } + + /** + * 是否启用「强制生长」:催熟时额外用随机刻推进方块。 + * + *

催熟本身只走原版骨粉判定,对甘蔗、仙人掌、竹子这类原版骨粉无效的方块 + * 不起作用。开启本项后会改用随机刻把它们也推起来。 + * + *

注意它不挑方块:凡是会随机刻的方块都会被推,包括树叶枯萎、火蔓延、 + * 雪冰融化、耕地退化。因此默认关闭,是否开启由玩家自行抉择。

+ */ + public static boolean isForceGrowEnabled(ItemStack stack) { + return com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.BeefForceGrowComponent.get(), false); + } + + public static void setForceGrowEnabled(ItemStack stack, boolean enabled) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.BeefForceGrowComponent.get(), enabled); + } + + /** + * 是否启用「生物加速」:潜行右键生物时加速其「计时器」类数值。 + * + *

只改数值,不跑 AI / 寻路 / 移动 / 物理:幼年成长与繁殖后冷却走 + * {@code AgeableMob.age},鸡下蛋走 {@code Chicken.eggTime},羊剪毛后长毛、 + * 村民补货(原版门槛绑在 {@code gameTime} 上,必须直接调 {@code restock()})、 + * 蜜蜂采蜜、海龟下蛋。

+ * + *

与「保护名单」的 Shift 手势互斥:本项开启时 Shift+右键生物被解释为加速, + * 保护名单手势让位(Ctrl 按种类的手势不受影响)。

+ */ + public static boolean isEntityTimeAccelerationEnabled(ItemStack stack) { + return com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.BeefEntityTimeAccelerationComponent.get(), false); + } + + public static void setEntityTimeAccelerationEnabled(ItemStack stack, boolean enabled) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.BeefEntityTimeAccelerationComponent.get(), enabled); + } + + /** + * 是否启用无线物流模式。 + * + *

开启后潜行右键容器方块可把它绑进/解绑出这把杖的物流网络;网络的搬运规则在 + * 独立界面里配置,真正的搬运由服务端引擎执行。

+ */ + public static boolean isStaffLinkEnabled(ItemStack stack) { + return com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.StaffLinkEnabledComponent.get(), false); + } + + public static void setStaffLinkEnabled(ItemStack stack, boolean enabled) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.StaffLinkEnabledComponent.get(), enabled); + } + + /** + * 是否启用杀戮光环。 + * + *

开启后手持造化杖每 20 tick 对「范围伤害」配置范围内生物结算一次攻击伤害; + * 范围与目标上限沿用范围伤害的配置,并兼容强制击杀与范围磁力。

+ */ + public static boolean isKillAuraEnabled(ItemStack stack) { + return com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.BeefKillAuraComponent.get(), false); + } + + public static void setKillAuraEnabled(ItemStack stack, boolean enabled) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.BeefKillAuraComponent.get(), enabled); + } + + /** + * 是否处于「保护名单模式」。 + * + *

开启时暂停杀戮光环与范围伤害的结算({@link #damageArea} 直接返回), + * 这样玩家可以走到怪堆里从容地用 Ctrl/Shift + 右键把生物加进保护名单, + * 而不会出现「刚瞄准就被光环打死」的情况。

+ */ + public static boolean isProtectModeEnabled(ItemStack stack) { + return com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.BeefProtectModeComponent.get(), false); + } + + public static void setProtectModeEnabled(ItemStack stack, boolean enabled) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.BeefProtectModeComponent.get(), enabled); + } + + /** Native 1.20.1 tools use the Unbreakable NBT flag, not a zero max-damage value. */ + private static void migrateLegacyDurability(ItemStack stack) { + stack.getOrCreateTag().remove("Damage"); + stack.getOrCreateTag().putBoolean("Unbreakable", true); + } + + /** Keeps the tool's fixed enchantments aligned with its selected mode and server config. */ + public static void refreshEnchantments(ItemStack stack, Level level) { + if (stack.isEmpty() || level == null || level.isClientSide()) { + return; + } + + EnchantMode mode = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, + UComponents.EnchantModeComponent.get(), EnchantMode.FORTUNE); + HolderLookup.Provider lookup = level.registryAccess(); + EnchantmentUtil.applyEnchantment( + stack, lookup, Enchantments.SILK_TOUCH, mode == EnchantMode.SILK_TOUCH ? 1 : 0); + EnchantmentUtil.applyEnchantment( + stack, lookup, Enchantments.BLOCK_FORTUNE, + mode == EnchantMode.FORTUNE ? ConfigManager.getFortuneLevel() : 0); + EnchantmentUtil.applyEnchantment( + stack, lookup, Enchantments.MOB_LOOTING, ConfigManager.getLootingLevel()); + } + + /** + * 右键触发的传送入口。 + * + *

短距闪现已改由独立快捷键驱动(见 {@link #performShortTeleport}),这里只剩不潜行时的 + * Ender IO 锚点传送;潜行时直接放行,避免与快捷键重复触发同一次传送。

+ */ + public static InteractionResult tryTeleport(Level level, Player player, ItemStack stack) { + if (player.isShiftKeyDown()) { + return InteractionResult.PASS; + } + if (!isTeleportEnabled(stack) || player.getCooldowns().isOnCooldown(stack.getItem())) { + return InteractionResult.PASS; + } + InteractionResult result = BeefTeleportHandler.tryAnchorTeleport( + level, + player, + ModList.get().isLoaded(EnderIOTravelCompat.MOD_ID)); + if (result != InteractionResult.PASS) { + player.getCooldowns().addCooldown(stack.getItem(), TELEPORT_COOLDOWN_TICKS); + } + return result; + } + + /** + * 快捷键触发的短距闪现入口(服务端执行)。 + * + *

按键包只上报「按下了」,落点由服务端自行计算与校验,客户端不参与目的地判定。

+ */ + public static void performShortTeleport(ServerPlayer player) { + ItemStack stack = player.getMainHandItem(); + if (!(stack.getItem() instanceof EndlessBeafItem)) { + return; + } + tryShortTeleport(player.level(), player, stack); + } + + /** + * 执行一次短距闪现,成功时套用物品冷却。 + */ + public static InteractionResult tryShortTeleport(Level level, Player player, ItemStack stack) { + if (!isTeleportEnabled(stack) || player.getCooldowns().isOnCooldown(stack.getItem())) { + return InteractionResult.PASS; + } + InteractionResult result = BeefTeleportHandler.tryShortTeleport(level, player); + if (result != InteractionResult.PASS) { + player.getCooldowns().addCooldown(stack.getItem(), TELEPORT_COOLDOWN_TICKS); + } + return result; + } + + @Override + public @NotNull InteractionResultHolder use( + @NotNull Level level, + @NotNull Player player, + @NotNull InteractionHand hand) { + ItemStack stack = player.getItemInHand(hand); + InteractionResult teleportResult = tryTeleport(level, player, stack); + if (teleportResult != InteractionResult.PASS) { + return InteractionResultHolder.sidedSuccess(stack, level.isClientSide()); + } + return super.use(level, player, hand); + } + + public static AttributeModifier createAttackSpeedModifier() { + return new AttributeModifier(Item.BASE_ATTACK_SPEED_UUID, "useless_mod_base_attack_speed", + 2.0D, AttributeModifier.Operation.ADDITION); + } + + public static AttributeModifier createAttackDamageModifier() { + return createAttackDamageModifier(AlloyFurnaceRecipeCatalog.currentRecipeCount()); + } + + static AttributeModifier createAttackDamageModifier(int recipeCount) { + return new AttributeModifier( + Item.BASE_ATTACK_DAMAGE_UUID, + "useless_mod_base_attack_damage", + recipeCount + Tiers.NETHERITE.getAttackDamageBonus(), + AttributeModifier.Operation.ADDITION); + } + + public static void refreshAttackDamage(LivingEntity entity) { + if (!(entity.getMainHandItem().getItem() instanceof EndlessBeafItem)) return; + + AttributeInstance attackDamage = entity.getAttribute(Attributes.ATTACK_DAMAGE); + if (attackDamage != null) { + AttributeModifier modifier = createAttackDamageModifier(); + AttributeModifier existing = attackDamage.getModifier(modifier.getId()); + if (existing == null || Double.compare(modifier.getAmount(), existing.getAmount()) != 0 + || modifier.getOperation() != existing.getOperation()) { + attackDamage.removeModifier(modifier.getId()); + attackDamage.addTransientModifier(modifier); + } + } + } + + /** + * 「以造化杖进行攻击」时应造成的伤害,供杀戮光环 / 范围伤害使用。 + * + *

为什么不能直接用 {@code player.getAttributeValue(ATTACK_DAMAGE)}:那个属性只反映 + * 主手物品 —— 造化杖的 {@code base_attack_damage} 修正挂在 + * {@code EquipmentSlot.MAINHAND} 上(见 {@code UselessMod#onItemAttributeModifiers}), + * 杖一旦放进副手 / 背包,属性值就退化成空手 1.0,光环打出来等于「没用杖打」。

+ * + *

做法:先扣掉「当前主手武器」贡献的那份,再补上「本杖」的那份 —— 两者用的是同一个修饰符 id + * ({@link Item#BASE_ATTACK_DAMAGE_UUID}),所以杖就在主手时两份相抵、结果与原来完全一致; + * 不在主手时即等效于「把这把杖当作主手武器」。玩家自身的其它修正(药水、护甲等)原样保留。

+ * + *

已知边角:少数模组武器用自定义 id 施加攻击力,此时扣不掉那一份(光环会多算它的伤害)。 + * 原版武器与造化杖本身都走 {@code base_attack_damage},不受影响。

+ */ + public static float staffAttackDamage(Player player) { + double damage = player.getAttributeValue(Attributes.ATTACK_DAMAGE); + AttributeInstance instance = player.getAttribute(Attributes.ATTACK_DAMAGE); + if (instance != null) { + AttributeModifier mainHandWeapon = instance.getModifier(Item.BASE_ATTACK_DAMAGE_UUID); + if (mainHandWeapon != null) { + damage -= mainHandWeapon.getAmount(); + } + } + damage += createAttackDamageModifier().getAmount(); + return (float) Math.max(0.0D, damage); + } + + @Override + public @NotNull ItemStack getCraftingRemainingItem(ItemStack stack) { + // 返回物品本身,使其在合成后保留在工作台中 + return stack.copy(); + } + + @Override + public boolean hasCraftingRemainingItem(@NotNull ItemStack stack) { + // 确保该物品有剩余物品(即本身) + return true; + } + + @Override + public boolean doesSneakBypassUse(@NotNull ItemStack stack, + @NotNull LevelReader level, + @NotNull BlockPos pos, + @NotNull Player player) { + Block block = level.getBlockState(pos).getBlock(); + if (block instanceof GlowPlasticBlock || block instanceof UselessGlassBlock) { + return true; + } + return super.doesSneakBypassUse(stack, level, pos, player); + } + + @Override + public void setDamage(@NotNull ItemStack stack, int damage) { + // 阻止任何耐久度设置 + super.setDamage(stack, 0); + } + + @Override + public boolean canPerformAction(@NotNull ItemStack stack, @NotNull ToolAction ability) { + // 基础工具能力(所有工具都有) + if (ToolActions.DEFAULT_AXE_ACTIONS.contains(ability) || + ToolActions.DEFAULT_PICKAXE_ACTIONS.contains(ability) || + ToolActions.DEFAULT_SHOVEL_ACTIONS.contains(ability) || + ToolActions.DEFAULT_HOE_ACTIONS.contains(ability) || + ability == ToolAction.get("brush_brush") || + ability == ToolActions.SWORD_SWEEP) { + return true; + } + + // 剪刀能力受「剪刀功能」开关控制:关掉后不再对外声明剪刀能力 + if (ToolActions.DEFAULT_SHEARS_ACTIONS.contains(ability)) { + return isShearsEnabled(stack); + } + + // 打火石能力,同样受「打火石功能」开关控制。 + // + // 必须对外声明,否则依赖「物品能否点火」判断的模组不会介入交互: + // 以神秘学(Occultism) 为例,它的灵火(Spiritfire) 生成处理器会检查 + // 手上物品是否具备点火能力,只有具备时才拦下这次右键、把地上的 + // 魔鬼之梦果换成灵火;否则会落到造化杖自己的打火石逻辑上,直接在 + // 果实所在位置放一团普通火,把果实烧掉。 + // + // 注意命名:MinecraftForge 21.1 的正式能力名是 firestarter_light,而 + // Occultism 仍在检查旧版 Forge 时代的 light_fire / light_campfire, + // 两者经 ToolAction.get() 得到的是不同实例,因此三个都要声明。 + if (ability == ToolAction.get("firestarter_light") + || ability == ToolAction.get("light_fire") + || ability == ToolAction.get("light_campfire")) { + return isFlintAndSteelEnabled(stack); + } + + // 根据工具类型返回特定能力 + if (this.toolType == null) { + return false; + } + + // GT部分适配 + return switch (this.toolType) { + case NONE_MODE, OMNITOOL_MODE -> false; + case WRENCH_MODE -> ability == ToolAction.get("wrench_rotate") || + ability == ToolAction.get("wrench_configure") || + ability == ToolAction.get("wrench_configure_all") || + ability == ToolAction.get("wrench_configure_items") || + ability == ToolAction.get("wrench_configure_fluids") || + ability == ToolAction.get("wrench_dig") || + ability == ToolAction.get("wrench_dismantle") || + ability == ToolAction.get("wrench_connect"); + + case SCREWDRIVER_MODE -> ability == ToolAction.get("screwdriver_configure") || + ability == ToolAction.get("interact_with_cover"); + + case MALLET_MODE -> ability == ToolAction.get("mallet_pause") || + ability == ToolAction.get("mallet_configure") || + ability == ToolAction.get("interact_with_cover"); + + case CROWBAR_MODE -> ability == ToolAction.get("crowbar_rotate") || + ability == ToolAction.get("crowbar_remove_cover") || + ability == ToolAction.get("crowbar_dig"); + + case HAMMER_MODE -> ability == ToolAction.get("hammer_dig") || + ability == ToolAction.get("hammer_mute"); + }; + } + + @Override + public int getEnchantmentValue(@NotNull ItemStack stack) { + return -1; // 允许被附魔 + } + + @Override + public boolean isDamageable(@NotNull ItemStack stack) { + return false; // 物品不可损坏 + } + + @Override + public boolean hurtEnemy(@NotNull ItemStack stack, @NotNull LivingEntity target, @NotNull LivingEntity attacker) { + if (attacker instanceof Player player) { + boolean forceKilled = forceKillLivingEntity(stack, target, player); + applyAoeDamage(stack, player, target); + if (forceKilled) { + return true; + } + } + return super.hurtEnemy(stack, target, attacker); + } + + @Override + public boolean onLeftClickEntity(@NotNull ItemStack stack, @NotNull Player player, @NotNull Entity entity) { + Entity target = getForceKillTarget(entity); + if (target instanceof LivingEntity livingEntity && forceKillLivingEntity(stack, livingEntity, player)) { + applyAoeDamage(stack, player, target); + return true; + } + if (!(target instanceof LivingEntity) && forceKillNonLivingEntity(stack, target, player)) { + applyAoeDamage(stack, player, target); + return true; + } + return super.onLeftClickEntity(stack, player, entity); + } + + /** + * 共享的范围结算:以 {@code center} 为中心,对范围内生物结算一次攻击伤害;命中后登记磁力吸附。 + * + *

「范围伤害」(左键命中触发)与「杀戮光环」(每 20 tick 触发)共用本方法, + * 因此保护名单、强制击杀、范围磁力这三件事只需在这里接一次,两条路径同时生效。

+ * + * @param stack 工具 + * @param player 攻击者(同时作为重入保护键与友军判定方) + * @param level 服务端世界 + * @param center 范围中心(左键=命中点;光环=玩家自身) + * @param excluded 需要跳过的实体(左键=直接命中的主目标,避免重复结算;光环=null) + * @param requireAoeFlag true=要求「范围伤害」开关为开(左键路径);false=不门控(光环路径) + * @return 实际命中的目标数 + */ + private static int damageArea(ItemStack stack, Player player, ServerLevel level, Vec3 center, + @Nullable Entity excluded, boolean requireAoeFlag) { + // 保护名单模式:光环与范围伤害都停手,让玩家能从容地把生物加进名单 + if (isProtectModeEnabled(stack)) { + return 0; + } + if (requireAoeFlag && !com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.BeefAoeDamageEnabledComponent, false)) { + return 0; + } + // 重入保护:左键范围伤害与杀戮光环共用同一把锁,避免同一次结算里互相嵌套 + if (!AOE_DAMAGE_CONTEXT.add(player.getUUID())) { + return 0; + } + + try { + double rangeX = ConfigManager.getBeefAoeDamageRangeX(); + double rangeY = ConfigManager.getBeefAoeDamageRangeY(); + double rangeZ = ConfigManager.getBeefAoeDamageRangeZ(); + AABB area = AABB.ofSize(center, rangeX * 2 + 1, rangeY * 2 + 1, rangeZ * 2 + 1); + + float damage = staffAttackDamage(player); + DamageSource damageSource = ModDamageTypes.beefTool(level, player); + int maxTargets = ConfigManager.getBeefAoeDamageMaxTargets(); + int hit = 0; + + for (LivingEntity victim : level.getEntitiesOfClass(LivingEntity.class, area)) { + if (hit >= maxTargets) { + break; + } + if (victim == excluded || !victim.isAlive()) { + continue; + } + // 统一的保护判定:玩家/盟友/自己的宠物与坐骑/手动维护的保护名单 + if (isProtectedTarget(player, victim)) { + continue; + } + + hit++; + if (forceKillLivingEntity(stack, victim, player)) { + continue; + } + victim.invulnerableTime = 0; + victim.hurt(damageSource, damage); + } + + if (hit > 0) { + // 主动登记一次吸附:范围取「AoE 半径 + 磁力半径」,保证最外圈被打死的怪的掉落也能覆盖到。 + // 与单体死亡事件登记合并,覆盖强杀兜底和范围内普通伤害产生的掉落。 + BeefMagnetHandler.scheduleSweep(level, player, stack, center, + rangeX + ConfigManager.getBeefMagnetRangeX(), + rangeY + ConfigManager.getBeefMagnetRangeY(), + rangeZ + ConfigManager.getBeefMagnetRangeZ()); + } + return hit; + } finally { + AOE_DAMAGE_CONTEXT.remove(player.getUUID()); + } + } + + /** + * 左键范围伤害:以命中目标为中心结算。语义与抽出 {@link #damageArea} 之前完全一致。 + * + * @param primary 直接命中的目标(作为范围中心,本身不重复结算) + */ + private static void applyAoeDamage(ItemStack stack, Player player, Entity primary) { + if (!(player.level() instanceof ServerLevel level)) { + return; + } + damageArea(stack, player, level, primary.position(), primary, true); + } + + /** + * 杀戮光环:由 {@code EventHandler#onPlayerTick} 按「杀戮光环间隔」配置周期性调用, + * 以玩家自身为中心结算一次范围伤害。 + * + *

生效条件:主手 / 副手 / 快捷栏 / 主背包 任意一处带着一把「光环已开」的造化杖即可, + * 不必手持。那把杖由 {@code onPlayerTick} 的合并扫描({@link UselessItemUtils#scanStaff}) + * 顺手取出并传入,因此这里不再自己走一遍背包。

+ * + *

结算期间会把这把杖顶到主手,结算完立刻还原(见 {@link #withStaffInMainHand})—— + * 因为「这一下是用杖打的」这件事,下游有一大票系统都只按主手武器解析: + * 原版抢夺附魔({@code EnchantmentHelper.getSlotItems} 只查 MAINHAND 槽)、 + * 本模组的斩首 / 捕捉 / 击杀磁力 / 掉落下游({@code EventHandler} 里那几个 + * {@code player.getMainHandItem()})、以及 Malum 的精魂 mixin。不顶上去,它们全都看不到这把杖。

+ * + *

范围沿用「范围伤害」的配置,因此打开「强制击杀」时走强杀、「范围磁力」开启时自动吸附掉落。

+ * + * @param killAuraStaff 身上第一把开着光环的造化杖,没有则为 {@code null} + */ + public static void tickKillAura(Player player, @Nullable ItemStack killAuraStaff) { + if (killAuraStaff == null || killAuraStaff.isEmpty()) { + return; + } + if (player.isSpectator() || player.isDeadOrDying()) { + return; + } + if (!(player.level() instanceof ServerLevel level)) { + return; + } + withStaffInMainHand(player, killAuraStaff, + () -> damageArea(killAuraStaff, player, level, player.position(), null, false)); + } + + /** + * 在「把这把杖当作主手武器」的前提下跑一段结算,跑完立即还原主手。 + * + *

为什么需要:伤害本身走 {@link #staffAttackDamage} 已不依赖主手,但「附魔 / 精魂 / 捕捉 / + * 斩首 / 击杀磁力」这些下游一律读 {@code player.getMainHandItem()}。把杖临时顶到主手, + * 等于让它们看到的就是「玩家正握着这把杖挥砍」。

+ * + *

为什么没有副作用:装备同步与属性重算是每 tick 在 + * {@code LivingEntity#detectEquipmentUpdates} 里按「与上一 tick 的差值」做的,本方法在 + * {@code PlayerTickEvent.Post} 里同一 tick 内换上去又换回来 ⇒ 差值恒为零 ⇒ + * 不发装备包、客户端看不到换手。{@code onEquipItem} 的装备音只在物品是 {@code Equipable} + * 且槽位匹配时播放(杖不是),振动事件 {@code doesEmitEquipEvent} 对玩家只在护甲槽为真, + * 所以两者都不会触发。

+ * + *

杖已经在主手时直接执行,不做任何改动。

+ */ + private static void withStaffInMainHand(Player player, ItemStack staff, Runnable action) { + ItemStack previous = player.getMainHandItem(); + boolean swapped = previous != staff; + if (swapped) { + player.setItemInHand(InteractionHand.MAIN_HAND, staff); + } + try { + action.run(); + } finally { + if (swapped) { + player.setItemInHand(InteractionHand.MAIN_HAND, previous); + } + } + } + + private static Entity getForceKillTarget(Entity entity) { + return unwrapPartEntity(entity); + } + + /** + * 拆掉 {@link PartEntity}(末影龙之类的部位实体)取本体。 + * + *

客户端手势与服务端解析共用:命中部位实体时要落到本体上, + * 否则保护名单里记的 UUID / 种类会与真正参与结算的实体对不上。

+ */ + public static Entity unwrapPartEntity(@Nullable Entity entity) { + if (entity instanceof PartEntity partEntity) { + return partEntity.getParent(); + } + return entity; + } + + public static void observeForceKillDeath(LivingDeathEvent event) { + ForceKillContext context = FORCE_KILL_CONTEXTS.get(event.getEntity().getUUID()); + if (context != null) { + context.deathEventAttempted = true; + } + } + + public static boolean handleForceKillDeath(LivingDeathEvent event) { + ForceKillContext context = FORCE_KILL_CONTEXTS.get(event.getEntity().getUUID()); + if (context == null) { + return false; + } + + context.deathEventAttempted = true; + context.deathEventCanceled = event.isCanceled(); + if (!event.isCanceled() && !context.captureHandled) { + UselessItemUtils.tryCaptureSpawnEgg(context.target, context.stack, context.player); + context.captureHandled = true; + } + return true; + } + + public static boolean handleForceKillMagnetDeath(LivingDeathEvent event) { + ForceKillContext context = FORCE_KILL_CONTEXTS.get(event.getEntity().getUUID()); + if (context == null) { + return false; + } + + if (event.isCanceled()) { + context.deathEventCanceled = true; + } else if (!context.magnetScheduled) { + scheduleMagnetSweep(context.level, context.player, context.stack, context.deathPos); + context.magnetScheduled = true; + } + return true; + } + + public static void observeForceKillDrops(LivingDropsEvent event) { + ForceKillContext context = FORCE_KILL_CONTEXTS.get(event.getEntity().getUUID()); + if (context != null) { + context.dropsAttempted = true; + } + } + + private static boolean forceKillLivingEntity(ItemStack stack, LivingEntity target, Player player) { + if (target.level().isClientSide || target instanceof Player || !target.isAlive() + || isForceKillBlacklisted(target) + // 保护名单与「自己的宠物/坐骑」同样拦下直接强杀:保护应当是全局语义 + || isProtectedTarget(player, target) + || !com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.ForceKillEnabledComponent, false)) { + return false; + } + + ServerLevel level = (ServerLevel) target.level(); + DamageSource damageSource = ModDamageTypes.beefTool(level, player); + ForceKillContext context = new ForceKillContext(level, target, stack, player, damageSource); + if (FORCE_KILL_CONTEXTS.putIfAbsent(target.getUUID(), context) != null) { + return false; + } + + try { + executeForceKill(context); + + if (!isDeathCommitted(target) && !context.deathEventAttempted) { + rememberPositiveHealth(context); + target.setHealth(0.0F); + target.die(damageSource); + restoreCanceledDeathHealth(context); + } + + if (isDeathCommitted(target)) { + settleForceKillEffects(context); + return true; + } + + if (context.deathEventAttempted) { + context.deathEventCanceled = true; + } + restoreCanceledDeathHealth(context); + executeMaxHealthFallback(context); + executeFallbackDeath(context); + settleForceKillEffects(context); + + if (!target.isRemoved()) { + removeEntityFromServerStorage(context.level, target); + context.removalCommitted = target.isRemoved(); + } + return target.dead || context.removalCommitted; + } finally { + FORCE_KILL_CONTEXTS.remove(target.getUUID(), context); + } + } + + /** 用配置的磁力半径登记一次吸附。 */ + private static void scheduleMagnetSweep(ServerLevel level, Player player, ItemStack stack, Vec3 center) { + BeefMagnetHandler.scheduleSweep(level, player, stack, center, + ConfigManager.getBeefMagnetRangeX(), + ConfigManager.getBeefMagnetRangeY(), + ConfigManager.getBeefMagnetRangeZ()); + } + + private static void executeForceKill(ForceKillContext context) { + LivingEntity target = context.target; + if (target instanceof EnderDragon dragon) { + clearVanillaInvulnerability(target); + dragon.hurt(dragon.head, context.damageSource, getForceKillDamage(target)); + if (shouldStopStandardDamage(context)) { + return; + } + } + + for (int i = 0; i < MAX_STANDARD_DAMAGE_ATTEMPTS; i++) { + if (shouldStopStandardDamage(context)) { + break; + } + rememberPositiveHealth(context); + context.standardDamageAttempts++; + clearVanillaInvulnerability(target); + target.hurt(context.damageSource, getForceKillDamage(target)); + } + } + + private static boolean shouldStopStandardDamage(ForceKillContext context) { + LivingEntity target = context.target; + return target.isRemoved() + || target.dead + || target.isDeadOrDying() + || context.deathEventAttempted; + } + + private static void clearVanillaInvulnerability(LivingEntity target) { + target.invulnerableTime = 0; + if (target instanceof WitherBoss wither) { + wither.setInvulnerableTicks(0); + } + } + + private static boolean isDeathCommitted(LivingEntity target) { + return target.dead || target.isRemoved(); + } + + private static void rememberPositiveHealth(ForceKillContext context) { + float health = context.target.getHealth(); + if (Float.isFinite(health) && health > 0.0F) { + context.lastPositiveHealth = health; + } + } + + private static void restoreCanceledDeathHealth(ForceKillContext context) { + LivingEntity target = context.target; + if (target.isRemoved() || target.dead || !target.isDeadOrDying()) { + return; + } + + float health = Math.min(context.lastPositiveHealth, target.getMaxHealth()); + if (Float.isFinite(health) && health > 0.0F) { + target.setHealth(health); + } + } + + private static void executeMaxHealthFallback(ForceKillContext context) { + LivingEntity target = context.target; + if (target.isRemoved() || target.dead) { + return; + } + + AttributeInstance maxHealth = target.getAttribute(Attributes.MAX_HEALTH); + if (maxHealth != null) { + maxHealth.setBaseValue(0.0D); + } + target.setLastHurtByPlayer(context.player); + target.setHealth(0.0F); + target.die(context.damageSource); + } + + private static void executeFallbackDeath(ForceKillContext context) { + LivingEntity target = context.target; + if (target.isRemoved() || target.dead) { + return; + } + + boolean dropLoot = !context.dropsAttempted; + context.dropsAttempted = true; + if (!target.isDeadOrDying()) { + target.setHealth(0.0F); + } + forceDie(context.level, target, context.damageSource, dropLoot); + } + + private static void settleForceKillEffects(ForceKillContext context) { + if (!context.captureHandled) { + UselessItemUtils.tryCaptureSpawnEgg(context.target, context.stack, context.player); + context.captureHandled = true; + } + + if (!context.magnetScheduled) { + scheduleMagnetSweep(context.level, context.player, context.stack, context.deathPos); + context.magnetScheduled = true; + } + } + + private static void forceDie(ServerLevel level, LivingEntity victim, DamageSource source, boolean dropLoot) { + if (victim.isRemoved() || victim.dead) { + return; + } + + LivingEntity killer = victim.getKillCredit(); + if (victim.deathScore >= 0 && killer != null) { + killer.awardKillScore(victim, victim.deathScore, source); + } + if (victim.isSleeping()) { + victim.stopSleeping(); + } + + victim.dead = true; + victim.getCombatTracker().recheckStatus(); + + Entity sourceEntity = source.getEntity(); + if (sourceEntity == null || sourceEntity.killedEntity(level, victim)) { + victim.gameEvent(GameEvent.ENTITY_DIE); + if (dropLoot) { + victim.dropAllDeathLoot(source); + } + } + level.broadcastEntityEvent(victim, (byte) 3); + victim.setPose(Pose.DYING); + } + + private static void removeEntityFromServerStorage(ServerLevel level, Entity target) { + if (target instanceof Player || target.isRemoved()) { + return; + } + + PersistentEntitySectionManager entityManager = level.entityManager; + EntityLookup visibleEntities = entityManager.visibleEntityStorage; + visibleEntities.byId.remove(target.getId()); + visibleEntities.byId.int2ObjectEntrySet().removeIf(entry -> entry.getValue() == target); + visibleEntities.byUuid.remove(target.getUUID()); + visibleEntities.byUuid.entrySet().removeIf(entry -> entry.getValue() == target); + entityManager.knownUuids.remove(target.getUUID()); + + LevelEntityGetter entityGetter = entityManager.entityGetter; + if (entityGetter instanceof LevelEntityGetterAdapter adapter) { + adapter.visibleEntities.byId.remove(target.getId()); + adapter.visibleEntities.byUuid.remove(target.getUUID()); + } + + level.entityTickList.remove(target); + target.setRemoved(Entity.RemovalReason.DISCARDED); + level.getChunkSource().removeEntity(target); + } + + private static float getForceKillDamage(LivingEntity target) { + float damage = target.getHealth() + target.getAbsorptionAmount() + target.getMaxHealth() + 1.0F; + if (!Float.isFinite(damage)) { + return 1024.0F; + } + return Math.max(damage, 1024.0F); + } + + private static boolean forceKillNonLivingEntity(ItemStack stack, Entity target, Player player) { + if (target.level().isClientSide || target.isRemoved() || isForceKillBlacklisted(target) + || !isForceKillNonLivingWhitelisted(target) + || !com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.ForceKillEnabledComponent, false)) { + return false; + } + + if (target.level() instanceof ServerLevel level) { + Vec3 deathPos = target.position(); + target.setRemoved(Entity.RemovalReason.DISCARDED); + if (!target.isRemoved()) { + return false; + } + scheduleMagnetSweep(level, player, stack, deathPos); + return true; + } + return false; + } + + private static final class ForceKillContext { + private final ServerLevel level; + private final LivingEntity target; + private final ItemStack stack; + private final Player player; + private final DamageSource damageSource; + private final Vec3 deathPos; + private float lastPositiveHealth; + private int standardDamageAttempts; + private boolean deathEventAttempted; + private boolean deathEventCanceled; + private boolean dropsAttempted; + private boolean captureHandled; + private boolean magnetScheduled; + private boolean removalCommitted; + + private ForceKillContext(ServerLevel level, LivingEntity target, ItemStack stack, + Player player, DamageSource damageSource) { + this.level = level; + this.target = target; + this.stack = stack; + this.player = player; + this.damageSource = damageSource; + this.deathPos = target.position(); + this.lastPositiveHealth = target.getHealth(); + } + } + + private static boolean isForceKillBlacklisted(Entity entity) { + return ConfigManager.getBeefToolForceKillBlacklist().contains(getEntityId(entity)); + } + + private static boolean isForceKillNonLivingWhitelisted(Entity entity) { + return ConfigManager.getBeefToolForceKillNonLivingWhitelist().contains(getEntityId(entity)); + } + + /** 实体类型注册名(如 {@code minecraft:villager}),保护名单与强制击杀黑名单共用同一套 key。 */ + public static String getEntityId(Entity entity) { + return entity.getType().builtInRegistryHolder().key().location().toString(); + } + + /** + * 「保护名单手势」是否可用:杀戮光环、范围伤害或保护名单模式至少开一个。 + * + *

供客户端手势判定与服务端二次校验共用:只有这三个开关之一开着时, + * Ctrl/Shift + 右键生物才被解释为「切换保护名单」,否则完全让给原版交互。

+ */ + public static boolean isProtectGestureAvailable(ItemStack stack) { + return com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.BeefAoeDamageEnabledComponent, false) + || com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.BeefKillAuraComponent, false) + || isProtectModeEnabled(stack); + } + + /** + * 统一的保护判定:光环、范围伤害与强制击杀都以此为准。 + * + *

覆盖五类:玩家自己、其它玩家、同队盟友、自动保护的宠物与坐骑, + * 以及玩家手动维护的「种类 / 个体」保护名单。

+ * + *

注意 {@code player.isAlliedTo(victim)} 保护不了自己的宠物 —— + * {@code TamableAnimal#isAlliedTo} 判断的是「对方是不是我的主人」(方向相反), + * 而 {@code AbstractHorse} 完全没有覆写,所以归属判定必须走 {@link OwnableEntity#getOwnerUUID()}。

+ */ + public static boolean isProtectedTarget(Player player, Entity victim) { + if (victim == player || victim instanceof Player) { + return true; + } + if (player.isAlliedTo(victim)) { + return true; + } + // 自动保护:自己驯服的宠物(狼/猫/鹦鹉/已驯服的马等) + if (victim instanceof OwnableEntity ownable && player.getUUID().equals(ownable.getOwnerUUID())) { + return true; + } + // 自动保护:当前骑乘的坐骑(未驯服的猪/船等不属于 OwnableEntity) + if (victim == player.getVehicle()) { + return true; + } + + BeefToolProtectionManager.Protection protection = BeefToolProtectionManager.protectionFor(player); + if (protection.isEmpty()) { + return false; + } + if (protection.types().contains(getEntityId(victim))) { + return true; + } + return protection.entities().contains(victim.getUUID()); + } + + @Override + public void onUseTick(@NotNull Level level, + @NotNull LivingEntity livingEntity, + @NotNull ItemStack stack, + int remainingUseDuration) { + + if (!(livingEntity instanceof Player player) || remainingUseDuration < 0) { + livingEntity.releaseUsingItem(); + return; + } + + HitResult hitResult = ProjectileUtil.getHitResultOnViewVector( + player, + e -> !e.isSpectator() && e.isPickable(), + player.getBlockReach() + ); + + if (!(hitResult instanceof BlockHitResult blockHit) + || hitResult.getType() != HitResult.Type.BLOCK) { + livingEntity.releaseUsingItem(); + return; + } + + int i = this.getUseDuration(stack) - remainingUseDuration + 1; + boolean doBrushTick = i % 10 == 5; + + if (!doBrushTick) { + return; + } + + BlockPos blockPos = blockHit.getBlockPos(); + BlockState blockState = level.getBlockState(blockPos); + + /* ---------- 客户端:粒子 & 音效 ---------- */ + HumanoidArm arm = livingEntity.getUsedItemHand() == InteractionHand.MAIN_HAND + ? player.getMainArm() + : player.getMainArm().getOpposite(); + + if (blockState.shouldSpawnParticlesOnBreak() + && blockState.getRenderShape() != RenderShape.INVISIBLE) { + this.spawnBrushParticles( + level, + blockHit, + blockState, + livingEntity.getViewVector(0.0F), + arm + ); + } + + SoundEvent sound = blockState.getBlock() instanceof BrushableBlock brushable + ? brushable.getBrushSound() + : SoundEvents.BRUSH_GENERIC; + + level.playSound(player, blockPos, sound, SoundSource.BLOCKS); + + /* ---------- 服务端:正常刷取 + 战利品直收 ---------- */ + if (!level.isClientSide) { + BlockEntity blockEntity = level.getBlockEntity(blockPos); + if (blockEntity instanceof BrushableBlockEntity brushable) { + + // 刷取前记录已有掉落 + AABB area = new AABB(blockPos).inflate(3.0); + Set before = level.getEntitiesOfClass(ItemEntity.class, area) + .stream() + .map(Entity::getUUID) + .collect(Collectors.toSet()); + + boolean finished = brushable.brush( + level.getGameTime(), + player, + blockHit.getDirection() + ); + + // 只有刷完那一刻才回收掉落 + if (finished) { + List collected = new java.util.ArrayList<>(); + level.getEntitiesOfClass(ItemEntity.class, area).stream() + .filter(e -> !before.contains(e.getUUID())) + .forEach(entity -> { + ItemStack drop = entity.getItem().copy(); + if (!drop.isEmpty()) { + collected.add(drop); + } + entity.discard(); + }); + // 产物走统一的「AE 存储优先 / 范围磁力」处理,与挖掘、杀怪保持一致 + MiningUtils.handleDrops(player, MiningUtils.mergeItemStacks(collected), stack, + Vec3.atCenterOf(blockPos)); + } + } + } + } + + @Override + public @NotNull InteractionResult useOn(UseOnContext ctx) { + Level world = ctx.getLevel(); + Player player = ctx.getPlayer(); + + if (player == null) return InteractionResult.PASS; + + InteractionResult teleportResult = tryTeleport(world, player, ctx.getItemInHand()); + if (teleportResult != InteractionResult.PASS) return teleportResult; + + // 匠心仪式挎包模式:右键魔典预览的五芒星,从 AE 网络取出材料摆放整座仪式。 + // 预览仅存在于客户端,因此由客户端识别目标格并将预览信息发送至服务端。 + if (world.isClientSide && !player.isShiftKeyDown() + && com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(ctx.getItemInHand(), UComponents.BeefRitualSatchelComponent.get(), false) + && ModList.get().isLoaded("occultism") + && com.sorrowmist.useless.compat.forge1201.OptionalIntegration.call( + "occultism", "com.sorrowmist.useless.compat.occultism.RitualSatchelClientCompat", + "trySendPlacement", Boolean.class, false, + new Class[]{Level.class, Player.class, BlockPos.class}, world, player, ctx.getClickedPos())) { + return InteractionResult.SUCCESS; + } + + InteractionResult lightningCollectorResult = trySummonLightningForCollector(ctx.getLevel(), ctx.getClickedPos(), ctx.getPlayer()); + if (lightningCollectorResult != InteractionResult.PASS) return lightningCollectorResult; + + InteractionResult timeAccelerationResult = BeefTimeAcceleration.tryUse(ctx); + if (timeAccelerationResult != InteractionResult.PASS) return timeAccelerationResult; + + // ============================================================ + // 0. 打火石功能 (点亮营火/蜡烛,或在点击面放火;潜行时让给其它模组) + // ============================================================ + if (isFlintAndSteelEnabled(ctx.getItemInHand()) && !player.isShiftKeyDown()) { + InteractionResult flintResult = BeefFlintAndSteel.tryUse(ctx); + if (flintResult != InteractionResult.PASS) return flintResult; + } + + // ============================================================ + // 1. 刷子功能 (对 BrushableBlock 生效) + // ============================================================ + HitResult hitresult = ProjectileUtil.getHitResultOnViewVector( + player, (p) -> !p.isSpectator() && p.isPickable(), player.getBlockReach()); + + if (hitresult instanceof BlockHitResult blockHit && hitresult.getType() == HitResult.Type.BLOCK) { + if (world.getBlockState(blockHit.getBlockPos()).getBlock() instanceof BrushableBlock) { + player.startUsingItem(ctx.getHand()); + return InteractionResult.CONSUME; + } + } + + // 按住连锁键(Tab)时,下面的右键操作也会连锁: + // 等价组 / 范围 / 数量上限与连锁挖掘完全共用(潜行时不连锁,只作用一块)。 + ItemStack tool = ctx.getItemInHand(); + boolean chainUse = RightClickChainer.shouldChain(player, tool); + + // ============================================================ + // 2. 顺手收菜(右键成熟作物:收获并将种子保留在地里,不连根拔除) + // ============================================================ + if (isCropHarvestEnabled(tool) && !player.isShiftKeyDown() + && BeefCropHarvest.isHarvestable(world.getBlockState(ctx.getClickedPos()))) { + if (world instanceof ServerLevel serverLevel) { + if (chainUse) { + RightClickChainer.harvestCrops(serverLevel, ctx.getClickedPos(), player, tool); + } else { + BeefCropHarvest.harvest(serverLevel, ctx.getClickedPos(), player, tool); + } + } + // 两端均消费本次交互,避免客户端重复触发 + return InteractionResult.sidedSuccess(world.isClientSide); + } + + // ============================================================ + // 3. 催熟 / 强制生长 (右键可骨粉方块一键催到成熟;开启强制生长后 + // 额外用随机刻推进甘蔗、仙人掌、竹子等骨粉无效的方块;按住连锁键时整片作用) + // ============================================================ + // 与上面的打火石一致,先判开关再进行为,避免连点模式下每次都白做一次方块查询 + if ((isRipenEnabled(tool) || isForceGrowEnabled(tool)) && !player.isShiftKeyDown()) { + InteractionResult ripenResult = BeefRipen.tryUse(ctx); + if (ripenResult != InteractionResult.PASS) return ripenResult; + } + + // ============================================================ + // 4. 统一工具行为链 (铲子 / 锄头 顺序由耕地模式决定,随后是斧头) + // ============================================================ + boolean farmlandMode = isFarmlandMode(tool); + ToolAction firstSoilAction = farmlandMode ? ToolActions.HOE_TILL : ToolActions.SHOVEL_FLATTEN; + SoundEvent firstSoilSound = farmlandMode ? SoundEvents.HOE_TILL : SoundEvents.SHOVEL_FLATTEN; + ToolAction secondSoilAction = farmlandMode ? ToolActions.SHOVEL_FLATTEN : ToolActions.HOE_TILL; + SoundEvent secondSoilSound = farmlandMode ? SoundEvents.SHOVEL_FLATTEN : SoundEvents.HOE_TILL; + + // 4.1 土壤交互(耕地模式:锄头优先;草径模式:铲子优先) + InteractionResult res = this.tryToolAction(ctx, firstSoilAction, firstSoilSound, chainUse); + if (res != InteractionResult.PASS) return res; + + res = this.tryToolAction(ctx, secondSoilAction, secondSoilSound, chainUse); + if (res != InteractionResult.PASS) return res; + + // 4.2 斧头 (剥皮) + res = this.tryToolAction(ctx, ToolActions.AXE_STRIP, SoundEvents.AXE_STRIP, chainUse); + if (res != InteractionResult.PASS) return res; + + // 4.3 斧头 (刮铜) + res = this.tryScrapeOrWaxOff(ctx, ToolActions.AXE_SCRAPE, SoundEvents.AXE_SCRAPE, 3005, chainUse); + if (res != InteractionResult.PASS) return res; + + // 4.4 斧头 (去蜡) + return this.tryScrapeOrWaxOff(ctx, ToolActions.AXE_WAX_OFF, SoundEvents.AXE_WAX_OFF, 3004, chainUse); + } + + public static InteractionResult trySummonLightningForCollector(Level level, BlockPos clickedPos, @Nullable Player player) { + if (!level.getBlockState(clickedPos).is(net.minecraft.world.level.block.Blocks.LIGHTNING_ROD)) { + return InteractionResult.PASS; + } + + if (level instanceof ServerLevel serverLevel) { + LightningBolt bolt = EntityType.LIGHTNING_BOLT.create(serverLevel); + if (bolt == null) return InteractionResult.FAIL; + + Vec3 target = Vec3.atBottomCenterOf(clickedPos.above()); + bolt.moveTo(target.x, target.y, target.z); + if (player instanceof ServerPlayer serverPlayer) { + bolt.setCause(serverPlayer); + } + // ae2lt 存在时会读取此 NBT 键以识别自然天气闪电;不存在时该键被忽略,不影响功能 + bolt.getPersistentData().putBoolean(AE2LT_NATURAL_LIGHTNING_TAG, true); + serverLevel.addFreshEntity(bolt); + } + + return InteractionResult.sidedSuccess(level.isClientSide()); + } + + @Override + public @NotNull InteractionResult onItemUseFirst(@NotNull ItemStack stack, @NotNull UseOnContext ctx) { + InteractionResult teleportResult = tryTeleport(ctx.getLevel(), ctx.getPlayer(), stack); + if (teleportResult != InteractionResult.PASS) return teleportResult; + + InteractionResult timeAccelerationResult = BeefTimeAcceleration.tryUse(ctx); + if (timeAccelerationResult != InteractionResult.PASS) return timeAccelerationResult; + + // 蜡烛蛋糕的点亮必须在 useItemOn 之前处理:CandleCakeBlock.useItemOn 仅在物品为 + // Items.FLINT_AND_STEEL 或 Items.FIRE_CHARGE 时返回 SKIP_DEFAULT_BLOCK_INTERACTION, + // 造化杖不属于二者,该方块会返回 PASS_TO_DEFAULT_BLOCK_INTERACTION 并转交 + // CandleCakeBlock.useWithoutItem 处理;玩家饱食度未满时因此先吃掉一块蛋糕而非点亮蜡烛。 + // 在此提前点亮可绕开该分支,行为与原版打火石保持一致。 + Player player = ctx.getPlayer(); + BlockState clickedState = ctx.getLevel().getBlockState(ctx.getClickedPos()); + if (player != null && isFlintAndSteelEnabled(stack) && !player.isShiftKeyDown() + && clickedState.getBlock() instanceof CandleCakeBlock && CandleCakeBlock.canLight(clickedState)) { + InteractionResult flintResult = BeefFlintAndSteel.tryUse(ctx); + if (flintResult != InteractionResult.PASS) return flintResult; + } + + return InteractionResult.PASS; + } + + @Override + @SuppressWarnings("all") + public float getDestroySpeed(@NotNull ItemStack stack, @NotNull BlockState state) { + // 基础工具速度 + float configSpeed = (float) ConfigManager.getBeefToolMiningSpeed(); + float baseSpeed = configSpeed > 0 ? configSpeed : Tiers.NETHERITE.getSpeed(); + + float hardness = state.getDestroySpeed(null, null); + if (hardness < 0) { + // hardness == -1 是模组圈里通行的「伪不可破坏」约定:原版层面不可破坏,是否可挖 + // 由方块自己重写的 getDestroyProgress 决定。ATM 的三种矿与 ancient_stone 系列 + // 就是 strength(-1.0f, ...) 配一段自写进度公式(hasCorrectToolForDrops ? 250 : 1500)。 + // + // 这里原来直接 return 0.0F,会让 getDigSpeed 归零,进而让那类方块的破坏进度恒为 0 + // —— 表现就是「方块完全挖不动」,与掉落判定无关。 + // + // 但只返回 baseSpeed 又会明显偏慢:普通方块走原版的「硬度 x 30」除数,而这类方块 + // 自写的除数大得多(ATM 用 250,且额外 /2),同样的 baseSpeed 会慢十几倍。所以这里 + // 用一个「名义硬度」替代哨兵值 -1,让 baseSpeed x 硬度 这条公式继续成立: + // ATM 矿:getDigSpeed / 2 / 250 = (baseSpeed x 15) / 500 ≈ 0.30/tick(约 3.3 tick) + // 普通方块:baseSpeed / 30 ≈ 0.33/tick(约 3 tick) + // 两者的实际挖掘耗时因此基本一致。名义硬度可用 beef_tool_pseudo_hardness 调整。 + // + // 原版基岩 / 屏障 / 传送门框不会因此变得可挖:BlockBehaviour.getDestroyProgress + // 在 destroySpeed == -1 时直接 return 0(字节码确认),不会使用此处返回的速度值。 + return baseSpeed * (float) ConfigManager.getBeefToolPseudoHardness(); + } + + float speed = baseSpeed * hardness; + + // 防止 NaN / 极端情况 + if (speed <= 0 || Float.isNaN(speed) || Float.isInfinite(speed)) { + return baseSpeed; + } + + return speed; + } + + /** + * 造化杖是「无等级万能工具」:不继承任何工具等级的 incorrect_for_*_tool 限制。 + * + *

原版 工具规则按顺序取「第一条命中的规则」,而所有新增挖掘等级的模组都会把 + * 自己的方块并入 {@code #minecraft:incorrect_for__tool}。若把某个等级的拒绝清单写进 + * 规则表,就等于给造化杖挂上一份会随整合包变化的黑名单(AllTheModium 的 vibranium / + * unobtainium 矿即因此被误伤,表现为方块被移除但不掉落、连锁挖掘直接跳过)。 + * + *

这里反过来声明「永远是正确工具」,任何模组、任何未来新增的挖掘等级都自动生效, + * 代码里不需要出现任何模组专属 ID。需要保留进度门槛时,把方块加进 + * {@link ModTags#BEEF_TOOL_TIER_LOCKED} 即可;也可以用配置项 + * {@code beef_tool_ignores_tool_tier} 整体回退到原版语义。 + */ + @Override + public boolean isCorrectToolForDrops(@NotNull ItemStack stack, @NotNull BlockState state) { + if (ConfigManager.isBeefToolIgnoresToolTier() && !state.is(ModTags.BEEF_TOOL_TIER_LOCKED)) { + return true; + } + // 开关关闭 / 方块在锁定标签内:回到「下界合金层级」的原版语义 + return (state.is(BlockTags.MINEABLE_WITH_PICKAXE) || state.is(BlockTags.MINEABLE_WITH_AXE) + || state.is(BlockTags.MINEABLE_WITH_SHOVEL) || state.is(BlockTags.MINEABLE_WITH_HOE)) + && net.minecraftforge.common.TierSortingRegistry.isCorrectTierForDrops(getTier(), state); + } + + @Override + @SuppressWarnings("all") + public @NotNull InteractionResult interactLivingEntity(@NotNull ItemStack stack, + @NotNull Player player, + @NotNull LivingEntity entity, + @NotNull InteractionHand hand) { + InteractionResult teleportResult = tryTeleport(entity.level(), player, stack); + if (teleportResult != InteractionResult.PASS) return teleportResult; + + // 生物加速(独立模式):潜行右键生物 → 只推它的「计时器」,不跑 AI / 寻路 / 移动。 + // 必须排在下面「时间加速拦截」之前:两个模式可同时开启,作用对象分别是方块与生物。 + if (isEntityTimeAccelerationEnabled(stack) && player.isShiftKeyDown()) { + if (entity instanceof Player) return InteractionResult.PASS; + return BeefTimeAcceleration.tryUseOnEntity(stack, player, entity); + } + + if (BeefTimeAcceleration.shouldBlockOtherRightClick(stack, player)) { + return InteractionResult.FAIL; + } + + // 打火石能力对苦力怕生效:原版由 Creeper.mobInteract 依据 #minecraft:creeper_igniters + // 物品标签处理,造化杖不属于该标签,该分支不会命中,需在物品侧补齐同等行为。 + // 已点燃的苦力怕不再重复触发,否则引信会被反复重置而无法爆炸。 + if (isFlintAndSteelEnabled(stack) && !player.isShiftKeyDown() + && entity instanceof Creeper creeper && !creeper.isIgnited()) { + boolean isClient = entity.level().isClientSide(); + entity.level().playSound(player, entity.getX(), entity.getY(), entity.getZ(), + SoundEvents.FLINTANDSTEEL_USE, entity.getSoundSource(), 1.0F, + entity.level().getRandom().nextFloat() * 0.4F + 0.8F); + if (!isClient) { + creeper.ignite(); + } + return InteractionResult.sidedSuccess(isClient); + } + + // 催熟:右键幼年动物直接催至成年 + if (isRipenEnabled(stack) && !player.isShiftKeyDown() + && entity instanceof AgeableMob ageable && ageable.isBaby()) { + boolean isClient = entity.level().isClientSide(); + if (!isClient) { + ageable.setBaby(false); + if (entity.level() instanceof ServerLevel serverLevel) { + serverLevel.sendParticles( + ParticleTypes.HAPPY_VILLAGER, + entity.getX(), + entity.getY() + entity.getBbHeight() * 0.5D, + entity.getZ(), + 12, 0.3D, 0.3D, 0.3D, 0.0D + ); + serverLevel.playSound(null, entity.blockPosition(), SoundEvents.BONE_MEAL_USE, + SoundSource.PLAYERS, 1.0F, 1.0F); + } + } + return InteractionResult.sidedSuccess(isClient); + } + + if (isShearsEnabled(stack) && entity instanceof IForgeShearable target) { + BlockPos pos = entity.blockPosition(); + boolean isClient = entity.level().isClientSide(); + if (target.isShearable(stack, entity.level(), pos)) { + List drops = target.onSheared(player, stack, entity.level(), pos, + net.minecraft.world.item.enchantment.EnchantmentHelper.getItemEnchantmentLevel( + Enchantments.BLOCK_FORTUNE, stack)); + if (!isClient) { + // 产物走统一的「AE 存储优先 / 范围磁力」处理,与挖掘、杀怪保持一致 + MiningUtils.handleDrops(player, MiningUtils.mergeItemStacks(drops), stack, entity.position()); + } + entity.gameEvent(GameEvent.SHEAR, player); + return InteractionResult.sidedSuccess(isClient); + } + } + return InteractionResult.PASS; + } + + @Override + public void inventoryTick(@NotNull ItemStack pStack, + @NotNull Level pLevel, + @NotNull Entity pEntity, + int pSlotId, + boolean pIsSelected) { + super.inventoryTick(pStack, pLevel, pEntity, pSlotId, pIsSelected); + // 旧存档的杖子带着 durability(0) 留下的 MAX_DAMAGE=0 / DAMAGE=0,会在下一次 + // 耐久结算时被直接销毁;每 tick 就地迁移一次,剥离后本判定立即不再命中。 + if (!pLevel.isClientSide) { + migrateLegacyDurability(pStack); + } + // 注意不能用 getInventory().items 判断携带:那只有主背包 36 格,不含副手 + if (pEntity instanceof Player player + && UselessItemUtils.hasItemInInventory(player, this)) { + UselessItemUtils.applyEndlessBeafEffects(player); + } + } + + @Override + public void onCraftedBy(@NotNull ItemStack stack, @NotNull Level level, @NotNull Player player) { + super.onCraftedBy(stack, level, player); + refreshEnchantments(stack, level); + } + + @Override + @OnlyIn(Dist.CLIENT) + public @NotNull UseAnim getUseAnimation(@NotNull ItemStack stack) { + Player player = Minecraft.getInstance().player; + Level level = Minecraft.getInstance().level; + + if (player != null && level != null && player.isUsingItem()) { + HitResult hitResult = ProjectileUtil.getHitResultOnViewVector( + player, + e -> !e.isSpectator() && e.isPickable(), + player.getBlockReach() + ); + + if (hitResult instanceof BlockHitResult blockHit + && hitResult.getType() == HitResult.Type.BLOCK) { + BlockPos blockPos = blockHit.getBlockPos(); + BlockState blockState = level.getBlockState(blockPos); + if (blockState.getBlock() instanceof BrushableBlock) { + return UseAnim.BRUSH; + } + } + } + return super.getUseAnimation(stack); + } + + @Override + public int getUseDuration(@NotNull ItemStack stack) { + return 20; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void appendHoverText(@NotNull ItemStack stack, @NotNull net.minecraft.world.level.Level context, + @NotNull List tooltipComponents, + @NotNull TooltipFlag tooltipFlag) { + // 提示行数已超过屏幕可用高度,原版会把超出部分裁掉。此处先把全部内容写进暂存列表, + // 再按当前页裁取并追加翻页指示,避免逐行改动各分支的写入目标。 + List fullTooltip = new ArrayList<>(); + this.buildFullTooltip(stack, context, fullTooltip, tooltipFlag); + // 置位可见性标记:按键事件无法得知提示框是否真的在绘制,只能由渲染路径告知 + TooltipPageState.markTooltipVisible(); + // 绑定本次渲染的物品栈:提示框渲染与按键事件分属两个阶段,按键处理期间只能凭 + // 主手/副手物品重新推断被查看的物品;把引用记录在此,按键入口可直接取用, + // 避免提示框渲染的并非手持物品(如创造模式物品栏)时页码写到错误的键上。 + TooltipPageState.setViewedStack(stack); + + int totalPages = BeefTooltipPager.pageCount(fullTooltip.size()); + int[] range = BeefTooltipPager.pageRange(stack, fullTooltip.size(), totalPages); + tooltipComponents.addAll(fullTooltip.subList(range[0], range[1])); + if (totalPages > 1) { + // 页码由 pageRange 回绕后写回,直接读取即为当前页 + int page = BeefTooltipPager.getPage(stack); + tooltipComponents.add(Component.empty()); + tooltipComponents.add(BeefTooltipPager.pageIndicator(page, totalPages) + .withStyle(ChatFormatting.DARK_GRAY)); + } + + super.appendHoverText(stack, context, tooltipComponents, tooltipFlag); + } + + /** + * 构建造化杖的完整提示内容。 + * + *

与 {@link #appendHoverText} 分离,使分页逻辑只需处理「全部行」这一种输入。

+ */ + @OnlyIn(Dist.CLIENT) + private void buildFullTooltip(@NotNull ItemStack stack, @NotNull net.minecraft.world.level.Level context, + @NotNull List tooltipComponents, + @NotNull TooltipFlag tooltipFlag) { + if (ModList.get().isLoaded("gtceu")) { + ToolTypeMode currentToolType = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, + UComponents.CurrentToolTypeComponent.get(), + ToolTypeMode.NONE_MODE + ); + + tooltipComponents.add(Component.translatable("tooltip.useless_mod.current_tool_mode") + .append(": ") + .append(currentToolType.getTooltip()) + .withStyle(ChatFormatting.GOLD)); + tooltipComponents.add(Component.empty()); + } + + // 增强连锁挖矿模式 + boolean chainMiningEnabled = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.EnhancedChainMiningComponent.get(), false); + tooltipComponents.add(Component.translatable("tooltip.useless_mod.enhanced_chain_mining_mode") + .append(": ") + .append(Component.translatable( + chainMiningEnabled ? "tooltip.useless_mod.enable" : + "tooltip.useless_mod.disable" + ).withStyle(chainMiningEnabled ? ChatFormatting.GREEN : ChatFormatting.GRAY)) + .withStyle(ChatFormatting.GREEN)); + + // 强制挖掘状态 + boolean forceMiningEnabled = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.ForceMiningComponent.get(), false); + tooltipComponents.add(Component.translatable("tooltip.useless_mod.force_mining_mode") + .append(": ") + .append(Component.translatable( + forceMiningEnabled ? "tooltip.useless_mod.enable" : + "tooltip.useless_mod.disable" + ).withStyle(forceMiningEnabled ? ChatFormatting.GREEN : ChatFormatting.GRAY)) + .withStyle(ChatFormatting.RED)); + + boolean forceKillEnabled = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.ForceKillEnabledComponent.get(), false); + tooltipComponents.add(Component.translatable("tooltip.useless_mod.force_kill_enabled_mode") + .append(": ") + .append(Component.translatable( + forceKillEnabled ? "tooltip.useless_mod.enable" : + "tooltip.useless_mod.disable" + ).withStyle(forceKillEnabled ? ChatFormatting.GOLD : ChatFormatting.GRAY)) + .withStyle(ChatFormatting.DARK_RED)); + + boolean beefTimeAccelerationEnabled = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.BeefTimeAccelerationEnabledComponent.get(), false); + tooltipComponents.add(Component.translatable("tooltip.useless_mod.time_acceleration_mode") + .append(": ") + .append(Component.translatable( + beefTimeAccelerationEnabled ? "tooltip.useless_mod.enable" : + "tooltip.useless_mod.disable" + ).withStyle(beefTimeAccelerationEnabled ? ChatFormatting.GREEN : ChatFormatting.GRAY)) + .withStyle(ChatFormatting.AQUA)); + + boolean beefEntityTimeAccelerationEnabled = isEntityTimeAccelerationEnabled(stack); + tooltipComponents.add(Component.translatable("tooltip.useless_mod.beef_entity_time_acceleration_mode") + .append(": ") + .append(Component.translatable( + beefEntityTimeAccelerationEnabled ? "tooltip.useless_mod.enable" : + "tooltip.useless_mod.disable" + ).withStyle(beefEntityTimeAccelerationEnabled ? ChatFormatting.GREEN + : ChatFormatting.GRAY)) + .withStyle(ChatFormatting.AQUA)); + + Boolean beefInvulnerabilityValue = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(stack, UComponents.BeefInvulnerabilityEnabledComponent.get()); + boolean beefInvulnerabilityEnabled = beefInvulnerabilityValue != null + ? beefInvulnerabilityValue + : stack.getItem() instanceof EndlessBeafItem; + tooltipComponents.add(Component.translatable("tooltip.useless_mod.beef_invulnerability_mode") + .append(": ") + .append(Component.translatable( + beefInvulnerabilityEnabled ? "tooltip.useless_mod.enable" : + "tooltip.useless_mod.disable" + ).withStyle(beefInvulnerabilityEnabled ? ChatFormatting.GREEN : ChatFormatting.GRAY)) + .withStyle(ChatFormatting.DARK_PURPLE)); + + boolean beefAdvancedStealthEnabled = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, + UComponents.BeefAdvancedStealthEnabledComponent.get(), false); + tooltipComponents.add(Component.translatable("tooltip.useless_mod.beef_advanced_stealth_mode") + .append(": ") + .append(Component.translatable( + beefAdvancedStealthEnabled ? "tooltip.useless_mod.enable" : + "tooltip.useless_mod.disable" + ).withStyle(beefAdvancedStealthEnabled ? ChatFormatting.GREEN : ChatFormatting.GRAY)) + .withStyle(ChatFormatting.LIGHT_PURPLE)); + + boolean beefCaptureEnabled = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.BeefCaptureEnabledComponent.get(), false); + tooltipComponents.add(Component.translatable("tooltip.useless_mod.beef_capture_mode") + .append(": ") + .append(Component.translatable( + beefCaptureEnabled ? "tooltip.useless_mod.enable" : + "tooltip.useless_mod.disable" + ).withStyle(beefCaptureEnabled ? ChatFormatting.GREEN : ChatFormatting.GRAY)) + .withStyle(ChatFormatting.DARK_GREEN)); + + boolean beefTeleportEnabled = isTeleportEnabled(stack); + tooltipComponents.add(Component.translatable("tooltip.useless_mod.beef_teleport_mode") + .append(": ") + .append(Component.translatable( + beefTeleportEnabled ? "tooltip.useless_mod.enable" : + "tooltip.useless_mod.disable" + ).withStyle(beefTeleportEnabled ? ChatFormatting.GREEN : ChatFormatting.GRAY)) + .withStyle(ChatFormatting.LIGHT_PURPLE)); + + boolean beefAoeDamageEnabled = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.BeefAoeDamageEnabledComponent.get(), false); + tooltipComponents.add(Component.translatable("tooltip.useless_mod.beef_aoe_damage_mode") + .append(": ") + .append(Component.translatable( + beefAoeDamageEnabled ? "tooltip.useless_mod.enable" : + "tooltip.useless_mod.disable" + ).withStyle(beefAoeDamageEnabled ? ChatFormatting.GREEN : ChatFormatting.GRAY)) + .withStyle(ChatFormatting.RED)); + + boolean beefKillAuraEnabled = isKillAuraEnabled(stack); + tooltipComponents.add(Component.translatable("tooltip.useless_mod.beef_kill_aura_mode") + .append(": ") + .append(Component.translatable( + beefKillAuraEnabled ? "tooltip.useless_mod.enable" : + "tooltip.useless_mod.disable" + ).withStyle(beefKillAuraEnabled ? ChatFormatting.GREEN : ChatFormatting.GRAY)) + .withStyle(ChatFormatting.DARK_RED)); + + boolean beefProtectModeEnabled = isProtectModeEnabled(stack); + tooltipComponents.add(Component.translatable("tooltip.useless_mod.beef_protect_toggle_mode") + .append(": ") + .append(Component.translatable( + beefProtectModeEnabled ? "tooltip.useless_mod.enable" : + "tooltip.useless_mod.disable" + ).withStyle(beefProtectModeEnabled ? ChatFormatting.GREEN : ChatFormatting.GRAY)) + .withStyle(ChatFormatting.BLUE)); + + BeefToolProtectionManager.Protection protection = BeefToolProtectionManager.clientProtection(); + tooltipComponents.add(Component.translatable("tooltip.useless_mod.beef_protect_mode") + .append(": ") + .append(Component.translatable("tooltip.useless_mod.beef_protect_count", + protection.types().size(), protection.entities().size()) + .withStyle(protection.isEmpty() + ? ChatFormatting.GRAY : ChatFormatting.AQUA)) + .withStyle(ChatFormatting.BLUE)); + + boolean beefMagnetEnabled = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.BeefMagnetEnabledComponent.get(), true); + tooltipComponents.add(Component.translatable("tooltip.useless_mod.beef_magnet_mode") + .append(": ") + .append(Component.translatable( + beefMagnetEnabled ? "tooltip.useless_mod.enable" : + "tooltip.useless_mod.disable" + ).withStyle(beefMagnetEnabled ? ChatFormatting.GREEN : ChatFormatting.GRAY)) + .withStyle(ChatFormatting.YELLOW)); + + // AE存储优先状态(仅当AE2模组存在时) + if (ModList.get().isLoaded("ae2")) { + boolean aeStorageEnabled = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.AEStoragePriorityComponent.get(), false); + tooltipComponents.add(Component.translatable("tooltip.useless_mod.ae_storage_priority_mode") + .append(": ") + .append(Component.translatable( + aeStorageEnabled ? "tooltip.useless_mod.enable" : + "tooltip.useless_mod.disable" + ).withStyle(aeStorageEnabled ? ChatFormatting.GREEN : ChatFormatting.GRAY)) + .withStyle(ChatFormatting.BLUE)); + + // AE 连接模式:右键有 AE 节点的机器,把它并入工具绑定的那张网 + boolean aeConnectEnabled = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.AeNetworkConnectComponent.get(), false); + tooltipComponents.add(Component.translatable("tooltip.useless_mod.ae_network_connect_mode") + .append(": ") + .append(Component.translatable( + aeConnectEnabled ? "tooltip.useless_mod.enable" : + "tooltip.useless_mod.disable" + ).withStyle(aeConnectEnabled ? ChatFormatting.GREEN : ChatFormatting.GRAY)) + .withStyle(ChatFormatting.BLUE)); + } + + // 剪刀功能:开启后可剪羊毛/剪掉落,并对外声明剪刀能力 + boolean beefShears = isShearsEnabled(stack); + tooltipComponents.add(Component.translatable("tooltip.useless_mod.beef_shears_mode") + .append(": ") + .append(Component.translatable( + beefShears ? "tooltip.useless_mod.enable" : + "tooltip.useless_mod.disable" + ).withStyle(beefShears ? ChatFormatting.GREEN : ChatFormatting.GRAY)) + .withStyle(ChatFormatting.AQUA)); + + // 打火石功能:开启后右键可点亮营火/蜡烛,或在点击面放火 + boolean beefFlintAndSteel = isFlintAndSteelEnabled(stack); + tooltipComponents.add(Component.translatable("tooltip.useless_mod.beef_flint_and_steel_mode") + .append(": ") + .append(Component.translatable( + beefFlintAndSteel ? "tooltip.useless_mod.enable" : + "tooltip.useless_mod.disable" + ).withStyle(beefFlintAndSteel ? ChatFormatting.GREEN : ChatFormatting.GRAY)) + .withStyle(ChatFormatting.RED)); + + // 右键泥土:锄头优先(变耕地)/ 铲子优先(变草径) + tooltipComponents.add(Component.translatable("tooltip.useless_mod.beef_farmland_mode") + .append(": ") + .append(farmlandStateText(stack)) + .withStyle(ChatFormatting.GOLD)); + + // 顺手收菜:右键成熟作物时收获并保留种子于耕地 + boolean beefCropHarvest = isCropHarvestEnabled(stack); + tooltipComponents.add(Component.translatable("tooltip.useless_mod.beef_crop_harvest_mode") + .append(": ") + .append(Component.translatable( + beefCropHarvest ? "tooltip.useless_mod.enable" : + "tooltip.useless_mod.disable" + ).withStyle(beefCropHarvest ? ChatFormatting.GREEN : ChatFormatting.GRAY)) + .withStyle(ChatFormatting.GREEN)); + + // 催熟:右键可骨粉方块一键催到成熟,右键幼年动物直接催至成年 + boolean beefRipen = isRipenEnabled(stack); + tooltipComponents.add(Component.translatable("tooltip.useless_mod.beef_ripen_mode") + .append(": ") + .append(Component.translatable( + beefRipen ? "tooltip.useless_mod.enable" : + "tooltip.useless_mod.disable" + ).withStyle(beefRipen ? ChatFormatting.GREEN : ChatFormatting.GRAY)) + .withStyle(ChatFormatting.GOLD)); + + // 强制生长:催熟时额外用随机刻推进(可作用于甘蔗/仙人掌/竹子等骨粉无效的方块) + boolean beefForceGrow = isForceGrowEnabled(stack); + tooltipComponents.add(Component.translatable("tooltip.useless_mod.beef_force_grow_mode") + .append(": ") + .append(Component.translatable( + beefForceGrow ? "tooltip.useless_mod.enable" : + "tooltip.useless_mod.disable" + ).withStyle(beefForceGrow ? ChatFormatting.GREEN : ChatFormatting.GRAY)) + .withStyle(beefForceGrow ? ChatFormatting.GOLD + : ChatFormatting.DARK_GRAY)); + + // 连点:客户端会话级开关(不绑物品),开启后按配置速率重复触发右键 + boolean beefAutoClick = BeefAutoClicker.isEnabled(); + tooltipComponents.add(Component.translatable("tooltip.useless_mod.beef_auto_click_mode") + .append(": ") + .append(Component.translatable( + beefAutoClick ? "tooltip.useless_mod.enable" : + "tooltip.useless_mod.disable" + ).withStyle(beefAutoClick ? ChatFormatting.GREEN : ChatFormatting.GRAY)) + .withStyle(beefAutoClick ? ChatFormatting.LIGHT_PURPLE + : ChatFormatting.DARK_PURPLE)); + + tooltipComponents.add(Component.empty()); + + // 3. 动态按键提示(Shift 展开) + if (Screen.hasShiftDown()) { + // 附魔切换 + this.addKeyTooltip(tooltipComponents, KeyBindings.SWITCH_SILK_TOUCH_KEY, + "tooltip.useless_mod.key.switch_silk_touch" + ); + this.addKeyTooltip(tooltipComponents, KeyBindings.SWITCH_FORTUNE_KEY, + "tooltip.useless_mod.key.switch_fortune" + ); + + // 模式开关 + this.addKeyTooltip(tooltipComponents, KeyBindings.TOGGLE_CHAIN_MODE_KEY, + "tooltip.useless_mod.key.toggle_chain_mode" + ); + this.addKeyTooltip(tooltipComponents, KeyBindings.SWITCH_FORCE_MINING_KEY, + "tooltip.useless_mod.key.switch_force_mining" + ); + this.addKeyTooltip(tooltipComponents, KeyBindings.SWITCH_FARMLAND_MODE_KEY, + "tooltip.useless_mod.key.switch_farmland_mode" + ); + this.addKeyTooltip(tooltipComponents, KeyBindings.TOGGLE_CROP_HARVEST_KEY, + "tooltip.useless_mod.key.toggle_crop_harvest" + ); + this.addKeyTooltip(tooltipComponents, KeyBindings.TOGGLE_SHEARS_KEY, + "tooltip.useless_mod.key.toggle_shears" + ); + this.addKeyTooltip(tooltipComponents, KeyBindings.TOGGLE_FLINT_AND_STEEL_KEY, + "tooltip.useless_mod.key.toggle_flint_and_steel" + ); + this.addKeyTooltip(tooltipComponents, KeyBindings.TOGGLE_AUTO_CLICK_KEY, + "tooltip.useless_mod.key.toggle_auto_click" + ); + this.addKeyTooltip(tooltipComponents, KeyBindings.TOGGLE_KILL_AURA_KEY, + "tooltip.useless_mod.key.toggle_kill_aura" + ); + + // 触发按键 + this.addKeyTooltip(tooltipComponents, KeyBindings.TRIGGER_CHAIN_MINING_KEY, + "tooltip.useless_mod.key.trigger_chain_mining" + ); + this.addKeyTooltip(tooltipComponents, KeyBindings.TRIGGER_CHAIN_MINING_KEY, + "tooltip.useless_mod.key.chain_use" + ); + this.addKeyTooltip(tooltipComponents, KeyBindings.TRIGGER_FORCE_MINING_KEY, + "tooltip.useless_mod.key.trigger_force_mining" + ); + + // UI + this.addKeyTooltip(tooltipComponents, KeyBindings.SWITCH_MODE_WHEEL_KEY, + "tooltip.useless_mod.key.open_mode_wheel" + ); + tooltipComponents.add(Component.empty()); + } else { + // 未按 Shift 时显示提示 + tooltipComponents.add(Component.translatable("tooltip.useless_mod.press_shift_for_keys") + .withStyle(ChatFormatting.GRAY)); + } + + // 4. 其他静态功能提示 + tooltipComponents.add( + Component.translatable("tooltip.useless_mod.fast_break_plastic").withStyle(ChatFormatting.GREEN)); + if (ModList.get().isLoaded("ae2")) { + tooltipComponents.add( + Component.translatable("tooltip.useless_mod.ae_storage_priority_bind_hint").withStyle(ChatFormatting.GREEN)); + tooltipComponents.add( + Component.translatable("tooltip.useless_mod.ae_network_connect_hint").withStyle(ChatFormatting.BLUE)); + } + tooltipComponents.add( + Component.translatable("tooltip.useless_mod.festive_affix").withStyle(ChatFormatting.BLUE)); + tooltipComponents.add( + Component.translatable("tooltip.useless_mod.auto_collect").withStyle(ChatFormatting.GREEN)); + tooltipComponents.add( + Component.translatable("tooltip.useless_mod.time_acceleration_hint").withStyle(ChatFormatting.LIGHT_PURPLE)); + tooltipComponents.add( + Component.translatable("tooltip.useless_mod.beef_teleport_hint").withStyle(ChatFormatting.LIGHT_PURPLE)); + tooltipComponents.add( + Component.translatable("tooltip.useless_mod.beef_farmland_hint").withStyle(ChatFormatting.GOLD)); + tooltipComponents.add( + Component.translatable("tooltip.useless_mod.beef_crop_harvest_hint").withStyle(ChatFormatting.GREEN)); + tooltipComponents.add( + Component.translatable("tooltip.useless_mod.beef_shears_hint").withStyle(ChatFormatting.AQUA)); + tooltipComponents.add( + Component.translatable("tooltip.useless_mod.beef_flint_and_steel_hint").withStyle(ChatFormatting.GOLD)); + tooltipComponents.add( + Component.translatable("tooltip.useless_mod.beef_ripen_hint").withStyle(ChatFormatting.GOLD)); + tooltipComponents.add( + Component.translatable("tooltip.useless_mod.beef_force_grow_hint").withStyle(ChatFormatting.RED)); + tooltipComponents.add( + Component.translatable("tooltip.useless_mod.beef_auto_click_hint").withStyle(ChatFormatting.LIGHT_PURPLE)); + tooltipComponents.add( + Component.translatable("tooltip.useless_mod.beef_kill_aura_hint").withStyle(ChatFormatting.DARK_RED)); + tooltipComponents.add( + Component.translatable("tooltip.useless_mod.beef_protect_hint").withStyle(ChatFormatting.BLUE)); + tooltipComponents.add( + Component.translatable("tooltip.useless_mod.beef_entity_time_acceleration_hint") + .withStyle(ChatFormatting.LIGHT_PURPLE)); + tooltipComponents.add( + Component.translatable("tooltip.useless_mod.beef_protect_toggle_hint").withStyle(ChatFormatting.BLUE)); + + // 可选:增强连锁说明 + // tooltipComponents.add(Component.translatable("tooltip.useless_mod.enhanced_chain_description").withStyle(ChatFormatting.BLUE)); + + super.appendHoverText(stack, context, tooltipComponents, tooltipFlag); + } + + @Override + @OnlyIn(Dist.CLIENT) + public @NotNull Component getName(@NotNull ItemStack stack) { + Level level = Minecraft.getInstance().level; + // 根据模式添加后缀 + if (level != null + && level.isClientSide + && net.minecraft.world.item.enchantment.EnchantmentHelper.getItemEnchantmentLevel( + Enchantments.BLOCK_FORTUNE, stack) != 0) { + return Component.translatable("item.useless_mod.endless_beaf_item.fortune"); + } else { + return Component.translatable("item.useless_mod.endless_beaf_item.silk_touch"); + } + } + + @Override + public boolean isFoil(@NotNull ItemStack stack) { + return true; // 始终显示附魔光效 + } + + @Override + public boolean isEnchantable(@NotNull ItemStack stack) { + return true; // 允许被附魔 + } + + /** + * 通用工具动作逻辑 + */ + private InteractionResult tryToolAction(UseOnContext ctx, ToolAction ability, SoundEvent sound, boolean chain) { + Level world = ctx.getLevel(); + BlockPos pos = ctx.getClickedPos(); + BlockState modified = world.getBlockState(pos).getToolModifiedState(ctx, ability, false); + if (modified != null) { + if (chain) { + // 连锁:交给 RightClickChainer 按连锁范围整片处理 + return RightClickChainer.applyToolAction(ctx, ability, sound, -1); + } + world.playSound(ctx.getPlayer(), pos, sound, SoundSource.BLOCKS, 1.0F, 1.0F); + if (!world.isClientSide) { + world.setBlock(pos, modified, 11); + if (ctx.getPlayer() instanceof ServerPlayer sp) { + CriteriaTriggers.ITEM_USED_ON_BLOCK.trigger(sp, pos, ctx.getItemInHand()); + } + } + return InteractionResult.sidedSuccess(world.isClientSide); + } + return InteractionResult.PASS; + } + + /** + * 针对铜块刮擦和去蜡的特殊逻辑 (带 LevelEvent 粒子效果) + */ + private InteractionResult tryScrapeOrWaxOff(UseOnContext ctx, ToolAction ability, SoundEvent sound, + int levelEvent, boolean chain) { + Level world = ctx.getLevel(); + BlockPos pos = ctx.getClickedPos(); + BlockState modified = world.getBlockState(pos).getToolModifiedState(ctx, ability, false); + if (modified != null) { + if (chain) { + // 连锁:交给 RightClickChainer 按连锁范围整片处理(粒子按块给,音效只响一次) + return RightClickChainer.applyToolAction(ctx, ability, sound, levelEvent); + } + world.playSound(ctx.getPlayer(), pos, sound, SoundSource.BLOCKS, 1.0F, 1.0F); + world.levelEvent(ctx.getPlayer(), levelEvent, pos, 0); + if (!world.isClientSide) { + world.setBlock(pos, modified, 11); + if (ctx.getPlayer() instanceof ServerPlayer sp) { + CriteriaTriggers.ITEM_USED_ON_BLOCK.trigger(sp, pos, ctx.getItemInHand()); + } + } + return InteractionResult.sidedSuccess(world.isClientSide); + } + return InteractionResult.PASS; + } + + private void spawnBrushParticles(Level level, + BlockHitResult hitResult, + BlockState state, + Vec3 pos, + HumanoidArm arm) { + int i = arm == HumanoidArm.RIGHT ? 1 : -1; + int j = level.getRandom().nextInt(7, 12); + BlockParticleOption blockparticleoption = new BlockParticleOption(ParticleTypes.BLOCK, state); + Direction direction = hitResult.getDirection(); + Vec3 vec3 = hitResult.getLocation(); + + for (int k = 0; k < j; ++k) { + level.addParticle(blockparticleoption, + vec3.x - (double) (direction == Direction.WEST ? 1.0E-6F : 0.0F), + vec3.y, + vec3.z - (double) (direction == Direction.NORTH ? 1.0E-6F : 0.0F), + (direction.getAxis() == Direction.Axis.X ? 0.0 : + direction.getStepX()) * i * 3.0 * level.getRandom().nextDouble(), + 0.0, + (direction.getAxis() == Direction.Axis.Z ? 0.0 : + direction.getStepZ()) * i * 3.0 * level.getRandom().nextDouble() + ); + } + } + + /** + * 安全添加带按键名的提示行 + */ + private void addKeyTooltip(List tooltip, Lazy keyLazy, String translationKey) { + KeyMapping key = keyLazy.get(); + String keyName = key.getTranslatedKeyMessage().getString().toUpperCase(Locale.ROOT); + + // 处理冲突或未绑定情况 + if (key.isUnbound()) { + keyName = "Unbound"; + } + + tooltip.add(Component.translatable(translationKey, keyName).withStyle(ChatFormatting.LIGHT_PURPLE)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/items/IngotItem.java b/src/main/java/com/sorrowmist/useless/content/items/IngotItem.java new file mode 100644 index 00000000..42008be1 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/items/IngotItem.java @@ -0,0 +1,64 @@ +package com.sorrowmist.useless.content.items; + +import com.sorrowmist.useless.content.blockentities.AdvancedAlloyFurnaceBlockEntity; +import com.sorrowmist.useless.content.blocks.AdvancedAlloyFurnaceBlock; +import net.minecraft.core.BlockPos; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.NotNull; + +public class IngotItem extends Item { + private final int furnaceTier; + + public IngotItem(Properties properties, int furnaceTier) { + super(properties); + if (furnaceTier < 1 || furnaceTier > AdvancedAlloyFurnaceBlockEntity.MAX_FURNACE_TIER) { + throw new IllegalArgumentException("Invalid furnace tier: " + furnaceTier); + } + this.furnaceTier = furnaceTier; + } + + /** + * 在方块上使用物品时的处理 + * 当玩家潜行右键万象合金炉时,手动触发升级逻辑 + */ + @Override + public @NotNull InteractionResult useOn(@NotNull UseOnContext context) { + Level level = context.getLevel(); + BlockPos pos = context.getClickedPos(); + BlockState state = level.getBlockState(pos); + Player player = context.getPlayer(); + ItemStack stack = context.getItemInHand(); + + // 检查是否是万象合金炉且玩家正在潜行 + if (state.getBlock() instanceof AdvancedAlloyFurnaceBlock && player != null && player.isShiftKeyDown()) { + if (level.isClientSide) { + return InteractionResult.SUCCESS; + } + + // 获取方块实体 + if (level.getBlockEntity(pos) instanceof AdvancedAlloyFurnaceBlockEntity furnace) { + int currentTier = furnace.getFurnaceTier(); + if (this.furnaceTier > currentTier) { + if (furnace.tryUpgrade(this.furnaceTier)) { + stack.shrink(1); + return InteractionResult.SUCCESS; + } + } + // 升级失败(阶级相同或更低),阻止继续打开界面。 + return InteractionResult.CONSUME; + } + } + + return super.useOn(context); + } + + public int getFurnaceTier() { + return this.furnaceTier; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/items/MultiblockAlloyFurnaceCoreBlockItem.java b/src/main/java/com/sorrowmist/useless/content/items/MultiblockAlloyFurnaceCoreBlockItem.java new file mode 100644 index 00000000..2d400a7d --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/items/MultiblockAlloyFurnaceCoreBlockItem.java @@ -0,0 +1,82 @@ +package com.sorrowmist.useless.content.items; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.block.Block; + +import java.util.List; + +public final class MultiblockAlloyFurnaceCoreBlockItem extends BlockItem { + private static final ChatFormatting[] NAME_COLORS = { + ChatFormatting.RED, + ChatFormatting.GOLD, + ChatFormatting.YELLOW, + ChatFormatting.GREEN, + ChatFormatting.AQUA, + ChatFormatting.BLUE, + ChatFormatting.LIGHT_PURPLE + }; + private static final ChatFormatting[] PRAYER_LINE_COLORS = { + ChatFormatting.LIGHT_PURPLE, + ChatFormatting.LIGHT_PURPLE, + ChatFormatting.LIGHT_PURPLE, + ChatFormatting.LIGHT_PURPLE, + ChatFormatting.AQUA, + ChatFormatting.AQUA, + ChatFormatting.AQUA, + ChatFormatting.BLUE, + ChatFormatting.BLUE, + ChatFormatting.BLUE, + ChatFormatting.GOLD, + ChatFormatting.GOLD, + ChatFormatting.RED, + ChatFormatting.RED, + ChatFormatting.YELLOW, + ChatFormatting.YELLOW, + ChatFormatting.YELLOW, + ChatFormatting.GREEN, + ChatFormatting.GREEN, + ChatFormatting.DARK_PURPLE, + ChatFormatting.WHITE, + ChatFormatting.WHITE + }; + private static final int PRAYER_LINE_COUNT = 22; + + public MultiblockAlloyFurnaceCoreBlockItem(Block block, Item.Properties properties) { + super(block, properties); + } + + @Override + public Component getName(ItemStack stack) { + return rainbowName(super.getName(stack)); + } + + @Override + public void appendHoverText(ItemStack stack, net.minecraft.world.level.Level context, + List tooltip, TooltipFlag flag) { + super.appendHoverText(stack, context, tooltip, flag); + for (int line = 1; line <= PRAYER_LINE_COUNT; line++) { + tooltip.add(Component.translatable( + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer." + line) + .withStyle(PRAYER_LINE_COLORS[line - 1])); + } + } + + public static Component rainbowName(Component source) { + String text = ChatFormatting.stripFormatting(source.getString()); + int offset = (int) Math.floor((System.currentTimeMillis() & 16383L) / 80.0D) + % NAME_COLORS.length; + MutableComponent result = Component.empty(); + for (int index = 0; index < text.length(); index++) { + int colorIndex = (index + NAME_COLORS.length - offset) % NAME_COLORS.length; + result.append(Component.literal(String.valueOf(text.charAt(index))) + .withStyle(NAME_COLORS[colorIndex])); + } + return result; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/items/OmniversalPatternConverterItem.java b/src/main/java/com/sorrowmist/useless/content/items/OmniversalPatternConverterItem.java new file mode 100644 index 00000000..e1ee5ce7 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/items/OmniversalPatternConverterItem.java @@ -0,0 +1,110 @@ +package com.sorrowmist.useless.content.items; + +import appeng.api.implementations.menuobjects.IMenuItem; +import appeng.api.implementations.menuobjects.ItemMenuHost; +import appeng.items.AEBaseItem; +import appeng.menu.MenuOpener; +import net.minecraft.core.BlockPos; +import appeng.menu.locator.MenuLocators; +import com.sorrowmist.useless.content.blockentities.AdvancedAlloyFurnaceBlockEntity; +import com.sorrowmist.useless.content.blockentities.multiblock.MultiblockAlloyFurnaceCoreBlockEntity; +import com.sorrowmist.useless.content.blockentities.multiblock.OmniversalMoldHubBlockEntity; +import com.sorrowmist.useless.content.menus.ContainerPatternConverter; +import com.sorrowmist.useless.content.menus.HostPatternConverter; +import com.sorrowmist.useless.core.component.UComponents; +import net.minecraft.core.GlobalPos; +import net.minecraft.network.chat.Component; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.InteractionResultHolder; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** Converts ordinary AE2 processing patterns using the molds in a linked hub. */ +public final class OmniversalPatternConverterItem extends AEBaseItem implements IMenuItem { + public OmniversalPatternConverterItem() { + super(new Item.Properties().stacksTo(1)); + } + + @Override + public @NotNull InteractionResultHolder use( + @NotNull Level level, @NotNull Player player, @NotNull InteractionHand hand) { + if (player.isShiftKeyDown()) { + if (!level.isClientSide()) { + ItemStack stack = player.getItemInHand(hand); + if (com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(stack, UComponents.PATTERN_CONVERTER_LINK_TARGET.get()) != null) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.remove(stack, UComponents.PATTERN_CONVERTER_LINK_TARGET.get()); + player.displayClientMessage(Component.translatable( + "message.useless_mod.pattern_converter.unlinked"), true); + } else { + player.displayClientMessage(Component.translatable( + "message.useless_mod.pattern_converter.unbound"), true); + } + } + return new InteractionResultHolder<>( + InteractionResult.sidedSuccess(level.isClientSide()), player.getItemInHand(hand)); + } + if (!level.isClientSide()) { + MenuOpener.open(ContainerPatternConverter.TYPE, player, MenuLocators.forHand(player, hand)); + } + return new InteractionResultHolder<>( + InteractionResult.sidedSuccess(level.isClientSide()), player.getItemInHand(hand)); + } + + @Override + public InteractionResult onItemUseFirst(ItemStack stack, UseOnContext context) { + Player player = context.getPlayer(); + Level level = context.getLevel(); + if (player == null || !player.isShiftKeyDown()) return InteractionResult.PASS; + + BlockEntity targetBlockEntity = level.getBlockEntity(context.getClickedPos()); + if (isLinkableTarget(targetBlockEntity)) { + if (!level.isClientSide()) { + GlobalPos target = GlobalPos.of(level.dimension(), context.getClickedPos()); + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.PATTERN_CONVERTER_LINK_TARGET.get(), target); + player.displayClientMessage(Component.translatable( + "message.useless_mod.pattern_converter.linked", + targetBlockEntity.getBlockState().getBlock().getName()), true); + } + return InteractionResult.sidedSuccess(level.isClientSide()); + } + + return InteractionResult.PASS; + } + + @Override + public void appendHoverText( + ItemStack stack, net.minecraft.world.level.Level context, List tooltip, TooltipFlag flag) { + GlobalPos target = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(stack, UComponents.PATTERN_CONVERTER_LINK_TARGET.get()); + if (target == null) { + tooltip.add(Component.translatable("tooltip.useless_mod.pattern_converter.unlinked")); + tooltip.add(Component.translatable("tooltip.useless_mod.pattern_converter.bind_hint")); + } else { + tooltip.add(Component.translatable( + "tooltip.useless_mod.pattern_converter.linked", + target.dimension().location().toString(), + target.pos().getX(), target.pos().getY(), target.pos().getZ())); + } + } + + private static boolean isLinkableTarget(@Nullable BlockEntity blockEntity) { + return blockEntity instanceof OmniversalMoldHubBlockEntity + || blockEntity instanceof AdvancedAlloyFurnaceBlockEntity + || blockEntity instanceof MultiblockAlloyFurnaceCoreBlockEntity; + } + + @Override + public @Nullable ItemMenuHost getMenuHost( + Player player, int slot, ItemStack stack, @Nullable BlockPos pos) { + return new HostPatternConverter(player, slot, stack); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/items/OmniversalPatternItem.java b/src/main/java/com/sorrowmist/useless/content/items/OmniversalPatternItem.java new file mode 100644 index 00000000..c0ba52e9 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/items/OmniversalPatternItem.java @@ -0,0 +1,16 @@ +package com.sorrowmist.useless.content.items; +import appeng.api.crafting.IPatternDetails; +import appeng.api.stacks.AEItemKey; +import appeng.crafting.pattern.EncodedPatternItem; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.OmniversalPatternDetails; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; + +/** AE2 15 recognizes EncodedPatternItem directly through its native pattern decoder. */ +public final class OmniversalPatternItem extends EncodedPatternItem { + public OmniversalPatternItem(){super(new Properties().stacksTo(1));} + @Override public IPatternDetails decode(ItemStack stack,Level level,boolean tryRecovery){ + return decode(AEItemKey.of(stack),level); + } + @Override public IPatternDetails decode(AEItemKey key,Level level){return OmniversalPatternDetails.decode(key,level);} +} diff --git a/src/main/java/com/sorrowmist/useless/content/items/RitualBlueprintItem.java b/src/main/java/com/sorrowmist/useless/content/items/RitualBlueprintItem.java new file mode 100644 index 00000000..b8bbf8fb --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/items/RitualBlueprintItem.java @@ -0,0 +1,60 @@ +package com.sorrowmist.useless.content.items; + +import com.sorrowmist.useless.core.component.RitualBlueprintPentacles; +import com.sorrowmist.useless.core.component.UComponents; +import net.minecraft.ChatFormatting; +import net.minecraft.Util; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.Level; + +import java.util.List; + +/** Imprints the validated Occultism pentacles at a ritual bowl onto an alloy-furnace mold. */ +public final class RitualBlueprintItem extends Item { + public RitualBlueprintItem(Properties properties) { + super(properties); + } + + @Override + public InteractionResult useOn(UseOnContext context) { + return com.sorrowmist.useless.compat.forge1201.OptionalIntegration.call("occultism", + "com.sorrowmist.useless.compat.occultism.OccultismBlueprintCompat","useOn",InteractionResult.class, + InteractionResult.PASS,new Class[]{UseOnContext.class},context); + } + + @Override + public void appendHoverText( + ItemStack stack, net.minecraft.world.level.Level context, List tooltip, TooltipFlag flag) { + RitualBlueprintPentacles pentacles = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(stack, UComponents.RITUAL_BLUEPRINT_PENTACLE.get()); + if (pentacles == null || pentacles.isEmpty()) { + tooltip.add(Component.translatable("item.useless_mod.ritual_blueprint.blank") + .withStyle(ChatFormatting.GRAY)); + return; + } + if (pentacles.pentacles().size() == 1) { + tooltip.add(Component.translatable("item.useless_mod.ritual_blueprint.pentacle", + pentacleName(pentacles.pentacles().get(0))) + .withStyle(ChatFormatting.DARK_PURPLE)); + return; + } + tooltip.add(Component.translatable("item.useless_mod.ritual_blueprint.pentacles", + pentacles.pentacles().size()) + .withStyle(ChatFormatting.DARK_PURPLE)); + for (ResourceLocation pentacle : pentacles.pentacles()) { + tooltip.add(Component.translatable("item.useless_mod.ritual_blueprint.pentacle_entry", + pentacleName(pentacle)) + .withStyle(ChatFormatting.DARK_PURPLE)); + } + } + + private static Component pentacleName(ResourceLocation pentacle) { + return Component.translatableWithFallback( + Util.makeDescriptionId("multiblock", pentacle), pentacle.toString()); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/AdvancedAlloyFurnaceAeManager.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/AdvancedAlloyFurnaceAeManager.java new file mode 100644 index 00000000..ea5b23da --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/AdvancedAlloyFurnaceAeManager.java @@ -0,0 +1,2665 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.networking.IManagedGridNode; +import appeng.api.stacks.AEFluidKey; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import appeng.api.stacks.KeyCounter; +import appeng.blockentity.crafting.IMolecularAssemblerSupportedPattern; +import appeng.crafting.CraftingEvent; +import appeng.menu.AutoCraftingMenu; +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerOutput; +import com.sorrowmist.useless.api.crafting.bigint.cpu.AlloyFurnaceBigIntegerBatchContext; +import com.sorrowmist.useless.api.crafting.bigint.cpu.AlloyFurnaceBigIntegerBatchResult; +import com.sorrowmist.useless.api.crafting.bigint.cpu.AlloyFurnaceBigIntegerCpuAdapter; +import com.sorrowmist.useless.api.crafting.bigint.cpu.AlloyFurnaceBigIntegerCpuAdapters; +import com.sorrowmist.useless.api.crafting.bigint.cpu.AlloyFurnaceBigIntegerCpuBinding; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.catalyst.ResolvedCatalystEffect; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalStackView; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.FurnaceChemicalStorage; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.io.FurnaceOutputPort; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeCatalog; +import com.sorrowmist.useless.core.config.ConfigManager; +import com.sorrowmist.useless.energy.IEnergyManager; +import com.sorrowmist.useless.network.AETaskProgressPacket; +import it.unimi.dsi.fastutil.objects.Object2LongMap; +import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.NonNullList; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.inventory.TransientCraftingContainer; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.inventory.CraftingContainer; +import net.minecraft.world.level.Level; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; + +/** + * 高级合金炉的 AE 任务调度器。 + * 负责管理样板、任务队列、批量合并、活跃任务和客户端进度同步。 + */ +public final class AdvancedAlloyFurnaceAeManager { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final int UNRETURNED_RETRY_TICKS = 20; + /** + * 合成样板的虚拟工作台格数。注意 AE2 传给供应器的输入计数器是压缩后的 + * (相同原料会并成一个输入位),长度不固定,网格才是固定 9 格。 + */ + private static final int CRAFTING_GRID_SIZE = 9; + /** + * 一个倍率批次里最多真实装配多少次。 + * + *

正常配方首次铺料后材料即为规整的重复,一次即可折叠整批;仅当一批中混有不同变体 + * (例如耐久各异的工具)时才需多次尝试,直至剩余材料重新规整。该上限仅用于兜底 + * 异常输入,避免主线程长时间阻塞。

+ */ + private static final int MAX_CRAFTING_PROBES = 64; + /** + * 回网队列里单条 pending 的持久化格式版本。 + * + *

1(隐式,无 {@code Format} 标签)= 逐段写的 {@code GenericStack} 列表; + * 2 = 按键聚合的 BigInteger(见 {@link CraftingAeAmountAccumulator#writeCompactTag})。

+ * + *

为什么必须换格式:产物总量可以远超 {@code long},按 {@code Long.MAX} 切段后 + * 1e22 个物品就是上千条 NBT,而队列深度等于线程数 —— 整条队列能到 GB 级, + * 每次区块存盘都要重写。聚合后每个键只占一条,重载时再展开,语义完全等价。

+ */ + private static final byte QUEUED_OUTPUT_FORMAT_COMPACT = 2; + + /** AE 存储 API 的单次上限,也是切段粒度(与数据能源的 {@code PHYSICAL_CHUNK} 一致)。 */ + private static final BigInteger MAX_OUTPUT_CHUNK = BigInteger.valueOf(Long.MAX_VALUE); + /** + * 连续多少个键被拒收(插入返回 0)就认为网络整体饱和,提前结束本轮。 + * + *

缺少该上限时,「网络不可达 + 键数量较多」的场景会退化为逐键无效重试。正常键数很小,影响可忽略。

+ */ + private static final int MAX_EMPTY_KEY_PROBES = 8; + + // ==================== 回网单批规模:AIMD 控制器 ==================== + // + // 为什么用控制器而不是公式:单批规模该多大取决于「交付能力」与「调度侧派发频率」, + // 前者随存储/网络变化,后者是调度侧内部行为 —— 两者都无法在编译期算准。 + // 所以改成**用实测反馈探测**(TCP 拥塞控制那一套): + // 慢启动:无积压时每 tick ×10,快速探到可用上限; + // 乘法减小:一旦积压超过「一批在飞」的规模就减半,并退出慢启动; + // 线性回升:之后无积压时逐步 +step 缓升。 + // 收敛点是「一批大约一两个 tick 就交付完」,也就是最平滑的形态。 + + /** + * 尚未测得插入成本时使用的初始估计(纳秒/段)。 + * + *

偏大 ⇒ 起步批次偏小、爬坡慢;偏小 ⇒ 首批偏大、可能积压。取 1000ns(略保守于实测的 + * 650ns):起步批次约为目标的 2/3,若干批后即由实测值取代,既不浪费爬坡时间也不引入风险。

+ */ + private static final double DEFAULT_INSERT_NANOS_PER_CHUNK = 1_000.0D; + /** 实测插入成本的平滑系数(新样本占 1/8)。 */ + private static final double INSERT_COST_SMOOTHING = 0.125D; + + + /** + * 预算探测的采样间隔(段)。 + * + *

不能逐段取时间的原因:{@code System.nanoTime()} 在 Windows 上单次约 100~150ns, + * 而一 tick 可能插入 24 万段 —— 逐段取一对时间戳即 48 万次调用 ≈ 50~70ms, + * 即埋点开销高于被测代码本身,日志中的「insertUs=87ms」绝大部分来自该开销。 + * 每 {@code 256} 段采样一次后开销降至 1/256,预算判定精度损失可忽略(256 段 ≈ 0.1ms)。

+ */ + private static final long BUDGET_PROBE_INTERVAL_CHUNKS = 256L; + + /** + * 有效样本要求的最少交付段数。 + * + *

需要该门槛的原因:样本为 {@code 插入耗时 ÷ 交付段数},而每 tick 存在固定开销 + * (首次插入的缓存冷、跨存储遍历等)。交付段数很少时固定开销会主导样本 —— + * 实测日志中出现过 {@code deliveredChunks=1, insertUs=41},即 41µs/段, + * 为真实值(0.65µs)的 63 倍。此类样本会使单批预算骤降, + * 随后缓慢回升 —— 即用户反馈的「降档幅度过大、回升缓慢」。 + * 要求至少 256 段后,固定开销被摊薄至可忽略(41µs/256 ≈ 160ns)。

+ */ + private static final long MIN_INSERT_SAMPLE_CHUNKS = 1_024L; + /** + * 单次采样允许「成本估计」上升的最大比例 —— 也就是限制降档幅度。 + * + *

必须非对称的原因:成本估计升高通常来自噪声(首次插入缓存冷、GC、 + * 跨存储遍历的固定开销),而非真实变慢;一旦升高,单批预算即按比例下降。 + * 不限制时会出现「降档幅度过大、回升缓慢」的现象。 + * 因此:升档不限速(成本下降立即放大批次,不浪费爬坡时间), + * 降档每次最多 1%(噪声最多造成 1% 损失,且下一批即可恢复)。

+ * + *

代价:存储确实永久变慢时,预算需约 70 批才能收敛 —— 期间积压会略有增长, + * 由队列条目数硬上限({@link #hardOutputQueueCap()})兜底。取该幅度是因为噪声单侧 + * (只会使耗时偏高),而真实变慢会持续出现,缓慢收敛即可。

+ */ + private static final double MAX_INSERT_COST_RISE_PER_SAMPLE = 1.01D; + /** 成本估计下限:防止除零与荒谬的大批次。 */ + private static final double MIN_INSERT_NANOS_PER_CHUNK = 50.0D; + /** + * 安全系数:单批按「半 tick 可交付量」定尺。 + * + *

调度侧可能一 tick 派发不止一批;取一半可保证到达 ≤ 交付,积压自然收敛在一两批。

+ */ + private static final long OUTPUT_BUDGET_SAFETY_DIVISOR = 2L; + /** + * 回网积压硬上限相对「单批预算上限」的倍数:仅作内存 / NBT 保护。 + * + *

控制器正常工作时积压收敛在「一两批在飞」(≈2×当前预算),离这里极远。

+ */ + private static final long OUTPUT_BACKLOG_HARD_MULTIPLIER = 8L; + /** + * 每台机器每 tick 的产物回网时间预算(纳秒),由配置项 + * {@code advanced_alloy_furnace.ae_output_return_budget_millis} 给出。 + * + *

它直接决定可持续合成速度:AE2 存储接口单次只能写一个 {@code long} 分段, + * 产物必须逐段插入,所以每 tick 能插多少次就决定了能跑多快。

+ * + *

实际生效值还要按 {@link AlloyFurnaceTickBudget#scaledFlushBudget(long)} 动态收窄: + * 全局预算被打满时按比例缩小(下限 250µs)。

+ */ + private static long flushBudgetNanos() { + return ConfigManager.getAdvancedAlloyFurnaceAeOutputReturnBudgetMillis() * 1_000_000L; + } + + /** + * 批次成熟窗口(tick):把 AE 的连续推送合并成一个任务,避免每次推送都单独开工。 + * 由配置项 {@code advanced_alloy_furnace.ae_batch_ripe_ticks} 控制,0 表示推送即刻投入执行。 + */ + private static int batchRipeTicks() { + return ConfigManager.getAdvancedAlloyFurnaceAeBatchRipeTicks(); + } + + private final AlloyFurnaceAeHost owner; + private final ConcurrentHashMap activeTasks = new ConcurrentHashMap<>(); + private final Map> activeTasksByPattern = new HashMap<>(); + private final ReentrantLock craftingLock = new ReentrantLock(); + private final ConcurrentHashMap aeTaskProgressMap = new ConcurrentHashMap<>(); + private final List clientTaskProgressList = new ArrayList<>(); + private final Map aePendingBatches = new HashMap<>(); + private final List patterns = new ArrayList<>(); + private boolean patternRefreshPending; + private final AtomicInteger activeAETaskCount = new AtomicInteger(0); + private final AtomicInteger totalAEProgress = new AtomicInteger(0); + private final AtomicInteger totalAEMaxProgress = new AtomicInteger(0); + // 返还失败的输入暂存(防丢失),逐 tick 重试写回 AE 网络;仅服务端主线程访问 + private final List unreturnedInputs = new ArrayList<>(); + private final List unreturnedOutputs = new ArrayList<>(); + /** + * 合成样板自执行的产物队列。 + * + *

合成样板由本机在虚拟 3×3 工作台上装配(万象合金炉没有合成台)。AE2 只在 + * {@code pushPattern} 返回 true 之后才把本批的预期产物写入 CPU 的 {@code waitingFor}, + * 所以产物必须至少延后一个 tick 再注入网络,否则会被写进通用存储、绕过当前合成任务, + * 表现为任务永久等待 + 产物翻倍。

+ */ + private final List queuedCraftingOutputs = new ArrayList<>(); + /** + * 回网积压的真实工作量:队列里所有条目还欠的产物总量(精确 BigInteger)。 + * + *

为什么不能用「条目数」当积压度量:一个条目可以装着上百万段 + * (线圈并行上百万时,单批产物就是百万段),条目数离阈值差着六个数量级, + * 按条目数降级永远不触发 —— 表现为「首批过大、降频追不上积压」。

+ * + *

用金额而不是「分段计数」是为了零漂移:入队加总量、交付减实际交付量, + * 两者都是精确值;而分段计数在「部分吸收」时无法精确扣减,会越用越偏。 + * 需要分段口径时按 {@code 金额 / Long.MAX} 现算即可。

+ * + *

只在服务端主线程维护:入队加、交付减、读档重算。

+ */ + private BigInteger pendingOutputAmount = BigInteger.ZERO; + /** + * 实测「写回一个分段」的平均耗时(纳秒/段,指数平滑)。 + * + *

单批分段预算由它前馈算出:{@code 时间预算 ÷ 本值 ÷ 安全系数}。 + * 没有闭环就没有振荡,也不会像反馈环那样卡在低位;存储快慢一变它会自动跟上。

+ */ + private double measuredInsertNanosPerChunk = DEFAULT_INSERT_NANOS_PER_CHUNK; + /** 是否已经采到过有效样本(首个有效样本直接采纳,不走平滑,避免慢慢爬坡)。 */ + private boolean insertCostMeasured; + /** 当前是否处于「回网积压」状态(用于状态跃迁日志的迟滞,避免冗余输出)。 */ + private boolean outputBacklogNoticed; + private int unreturnedInputRetryTimer = 0; + private int unreturnedOutputRetryTimer = 0; + private int patternPriority = 0; + private int nextTaskId = 0; + // 延迟加载的任务数据(loadTag 时 level 尚不可用,需推迟到首 tick 解码样板) + @Nullable + private CompoundTag deferredTasksTag = null; + + public AdvancedAlloyFurnaceAeManager(AlloyFurnaceAeHost owner) { + this.owner = owner; + } + + /** + * 方块实体被移除时的清理。 + * 注意:不做取消返还 —— 任务与批次已随 NBT 持久化(区块卸载后会恢复), + * 在这里返还会造成卸载复制;且 setRemoved 时 AE 节点已销毁,材料无法写回网络。 + * 主动取消只有两个入口:玩家在 GUI 点取消(AECancelPacket),以及方块被真正破坏时 + * AdvancedAlloyFurnaceBlock.onRemove 在节点销毁前调用。 + */ + public void shutdown() { + } + + public void cancelAllTasks() { + this.activeTasks.values().forEach(CraftingTask::cancel); + this.activeTasks.clear(); + this.activeTasksByPattern.clear(); + this.activeAETaskCount.set(0); + this.totalAEProgress.set(0); + this.totalAEMaxProgress.set(0); + this.aeTaskProgressMap.clear(); + + // 待启动批次的输入尚未转为任务,同样需要返还,否则材料会随 clear() 丢失 + List pendingInputs = new ArrayList<>(); + synchronized (this.aePendingBatches) { + for (PendingAEBatch batch : this.aePendingBatches.values()) { + pendingInputs.addAll(batch.drain()); + } + this.aePendingBatches.clear(); + } + CraftingTask.returnInputsToAE(pendingInputs, this.owner); + + // 合成样板的产物是已经装配完成的实物,AE 任务取消后必须写回网络,否则材料将无故丢失。 + // 写不进去的部分留在队列里(随 NBT 持久化),等后续 tick 或重载后继续重试。 + // 大数批次先给 CPU 侧一个终局信号:产物照样会强制写回,不会丢,只是不再走逐 tick 节奏。 + for (PendingCraftingOutput pending : this.queuedCraftingOutputs) { + notifyCpuBatchCancelled(pending); + } + this.flushQueuedCraftingOutputs(true); + + this.owner.markChanged(); + // 全部清空后主动同步一次,否则客户端会残留已取消的排队任务 + this.sendAETaskProgressToClients(); + } + + // ==================== 未返还输入暂存 ==================== + + /** 暂存返还失败的输入(如 AE 不可达时的化学品/流体),逐 tick 重试写回网络 */ + public void stashUnreturnedInput(AEKey key, long amount) { + if (key == null || amount <= 0) { + return; + } + addUnreturnedAmount(this.unreturnedInputs, key, amount); + this.owner.markChanged(); + } + + /** Stores normal recipe output that fits neither AE nor an output chemical slot yet. */ + public void stashUnreturnedOutput(AEKey key, long amount) { + if (key == null || amount <= 0) { + return; + } + addUnreturnedAmount(this.unreturnedOutputs, key, amount); + this.owner.markChanged(); + } + + /** 定期把暂存的未返还输入重试写回 AE 网络 */ + public void tickUnreturnedInputs() { + if (this.unreturnedInputs.isEmpty()) { + return; + } + if (++this.unreturnedInputRetryTimer < UNRETURNED_RETRY_TICKS) { + return; + } + this.unreturnedInputRetryTimer = 0; + + boolean changed = false; + var it = this.unreturnedInputs.listIterator(); + while (it.hasNext()) { + GenericStack gs = it.next(); + long requested = gs.amount(); + GenericStack remainder; + if (gs.what() instanceof AEItemKey || gs.what() instanceof AEFluidKey) { + remainder = FurnaceOutputPort.outputKeyWithRemainder( + gs, + this.owner.createAeOutputPort(), + this.owner.getItemHandler(), + this.owner.getInputSlotsStart(), + this.owner.getInputSlotsCount(), + this.owner.getInputFluidTanks(), + this.owner.getFluidTankCount(), + this.owner.getInputChemicalStorage(), + this.owner.getChemicalKeyProvider()); + } else { + long inserted = clampInserted(this.owner.tryOutputKeyToAE(gs.what(), requested), requested); + long remaining = requested - inserted; + if (remaining > 0L && this.owner.getChemicalKeyProvider().isChemicalKey(gs.what())) { + remaining -= insertChemicalFallback(gs.what(), remaining); + } + remainder = remaining > 0L ? new GenericStack(gs.what(), remaining) : null; + } + if (remainder != null && remainder.amount() == requested) { + continue; + } + changed = true; + if (remainder == null) { + it.remove(); + } else { + it.set(remainder); + } + } + if (changed) { + this.owner.markChanged(); + } + } + + /** Retries normal chemical outputs without ever placing them into input slots. */ + public void tickUnreturnedOutputs() { + if (this.unreturnedOutputs.isEmpty()) { + return; + } + if (++this.unreturnedOutputRetryTimer < UNRETURNED_RETRY_TICKS) { + return; + } + this.unreturnedOutputRetryTimer = 0; + + boolean changed = false; + var it = this.unreturnedOutputs.listIterator(); + while (it.hasNext()) { + GenericStack gs = it.next(); + long requested = gs.amount(); + GenericStack remainder = FurnaceOutputPort.outputKeyWithRemainder( + gs, + this.owner.createAeOutputPort(), + this.owner.getItemHandler(), + this.owner.getOutputSlotsStart(), + this.owner.getOutputSlotsCount(), + this.owner.getOutputFluidTanks(), + this.owner.getFluidTankCount(), + this.owner.getOutputChemicalStorage(), + this.owner.getChemicalKeyProvider()); + if (remainder != null && remainder.amount() == requested) { + continue; + } + changed = true; + if (remainder == null) { + it.remove(); + } else { + it.set(remainder); + } + } + if (changed) { + this.owner.markChanged(); + } + } + + private long insertChemicalFallback(AEKey key, long amount) { + if (amount <= 0L) return 0L; + ChemicalStackView view = this.owner.getChemicalKeyProvider() + .fromGenericStack(new GenericStack(key, amount)); + if (view == null || view.isEmpty()) return 0L; + + long inserted = insertChemical(this.owner.getInputChemicalStorage(), view); + if (inserted < amount) { + inserted += insertChemical(this.owner.getOutputChemicalStorage(), + view.copyWithAmount(amount - inserted)); + } + return Math.min(amount, Math.max(0L, inserted)); + } + + private static long insertChemical(FurnaceChemicalStorage storage, ChemicalStackView view) { + if (storage == null || !storage.isAvailable() || view == null || view.isEmpty()) return 0L; + ChemicalStackView remainder = storage.insertChemical(view, false); + return Math.max(0L, view.amount() - remainder.amount()); + } + + private static long clampInserted(long inserted, long requested) { + return Math.max(0L, Math.min(requested, inserted)); + } + + // ==================== 持久化 ==================== + + /** + * 将活跃任务与待启动批次序列化到 NBT。 + */ + public void saveTasks(CompoundTag tag, HolderLookup.Provider registries) { + ListTag tasksTag = new ListTag(); + for (CraftingTask task : this.activeTasks.values()) { + tasksTag.add(task.save(registries)); + } + tag.put("ActiveTasks", tasksTag); + + ListTag pendingTag = new ListTag(); + synchronized (this.aePendingBatches) { + for (PendingAEBatch batch : this.aePendingBatches.values()) { + CompoundTag batchTag = batch.save(registries); + if (batchTag != null) { + pendingTag.add(batchTag); + } + } + } + tag.put("PendingBatches", pendingTag); + + ListTag unreturnedTag = new ListTag(); + for (GenericStack gs : this.unreturnedInputs) { + unreturnedTag.add(GenericStack.writeTag(gs)); + } + tag.put("UnreturnedInputs", unreturnedTag); + + ListTag unreturnedOutputsTag = new ListTag(); + for (GenericStack gs : this.unreturnedOutputs) { + unreturnedOutputsTag.add(GenericStack.writeTag(gs)); + } + tag.put("UnreturnedOutputs", unreturnedOutputsTag); + + ListTag craftingOutputsTag = new ListTag(); + for (PendingCraftingOutput pending : this.queuedCraftingOutputs) { + CompoundTag entry = new CompoundTag(); + entry.putLong("QueuedTick", pending.queuedTick); + entry.putByte("Format", QUEUED_OUTPUT_FORMAT_COMPACT); + // 内存里就是按键 BigInteger 账本,与紧凑格式 1:1,直接写即可(无需再聚合)。 + entry.put("Outputs", pending.ledger.writeCompactTag(registries)); + craftingOutputsTag.add(entry); + } + tag.put("QueuedCraftingOutputs", craftingOutputsTag); + tag.putInt("NextTaskId", this.nextTaskId); + } + + public boolean hasPersistedData() { + return !this.activeTasks.isEmpty() + || !this.aePendingBatches.isEmpty() + || !this.unreturnedInputs.isEmpty() + || !this.unreturnedOutputs.isEmpty() + || !this.queuedCraftingOutputs.isEmpty() + || this.deferredTasksTag != null; + } + + /** + * 记录任务 NBT,推迟到 level 可用时(首 tick)再解码。 + */ + public void readTasksTag(CompoundTag tag) { + if (tag.contains("AeTasks")) { + this.deferredTasksTag = tag.getCompound("AeTasks"); + } + } + + /** + * 在 level 与配方目录均可用后加载延迟的任务数据(样板解码同时依赖二者)。 + * + *

目录在服务端启动阶段才构建完毕,期间查询路径一律返回未就绪。若在目录就绪前解码, + * {@code resolvePattern} 会因快照未就绪返回空,随即把完好的样板判为 + * 「missing or has changed」并退还材料,任务因此被取消。故此处挂起,由调用方在后续 tick 重试。

+ * + * @return true 表示已处理完毕(含无待恢复数据);false 表示前提条件未满足,需稍后重试 + */ + public boolean loadDeferredTasks() { + CompoundTag tag = this.deferredTasksTag; + if (tag == null) { + return true; + } + Level level = this.owner.getLevel(); + if (level == null) { + return false; + } + if (!AlloyFurnaceRecipeCatalog.isReady(level)) { + return false; + } + this.deferredTasksTag = null; + HolderLookup.Provider registries = level.registryAccess(); + + this.nextTaskId = tag.getInt("NextTaskId"); + + ListTag tasksTag = tag.getList("ActiveTasks", Tag.TAG_COMPOUND); + for (int i = 0; i < tasksTag.size(); i++) { + CompoundTag taskTag = tasksTag.getCompound(i); + try { + CraftingTask task = CraftingTask.load(taskTag, level, this.owner, registries); + if (task != null) { + this.activeTasks.put(task.getTaskId(), task); + this.activeTasksByPattern.computeIfAbsent( + PatternExecutionKey.of(task.getPattern(), task.getComponentInputKeys()), + k -> new ArrayList<>()).add(task); + this.activeAETaskCount.incrementAndGet(); + } else { + CraftingTask.returnSavedMaterials(taskTag, this.owner, registries); + } + } catch (RuntimeException exception) { + RateLimitedLogger.error(LOGGER, "Failed to restore an Advanced Alloy Furnace AE task; returning its saved materials", exception); + CraftingTask.returnSavedMaterials(taskTag, this.owner, registries); + } + } + + ListTag pendingTag = tag.getList("PendingBatches", Tag.TAG_COMPOUND); + synchronized (this.aePendingBatches) { + for (int i = 0; i < pendingTag.size(); i++) { + CompoundTag batchTag = pendingTag.getCompound(i); + try { + PendingAEBatch batch = PendingAEBatch.load(batchTag, level, registries); + if (batch != null && batch.pattern != null) { + this.aePendingBatches.put(PendingPatternExecutionKey.of( + batch.pattern, batch.operationsPerPush, batch.getComponentInputKeys()), batch); + } else { + returnSavedBatchMaterials(batchTag, registries); + } + } catch (RuntimeException exception) { + RateLimitedLogger.error(LOGGER, "Failed to restore an Advanced Alloy Furnace AE batch; returning its saved materials", exception); + returnSavedBatchMaterials(batchTag, registries); + } + } + } + + ListTag unreturnedTag = tag.getList("UnreturnedInputs", Tag.TAG_COMPOUND); + CraftingAeAmountAccumulator unreturnedInputAmounts = new CraftingAeAmountAccumulator(); + for (int i = 0; i < unreturnedTag.size(); i++) { + GenericStack gs = GenericStack.readTag(unreturnedTag.getCompound(i)); + if (gs != null && gs.amount() > 0L) { + unreturnedInputAmounts.add(gs); + } + } + this.unreturnedInputs.addAll(unreturnedInputAmounts.segments()); + + ListTag unreturnedOutputsTag = tag.getList("UnreturnedOutputs", Tag.TAG_COMPOUND); + CraftingAeAmountAccumulator unreturnedOutputAmounts = new CraftingAeAmountAccumulator(); + for (int i = 0; i < unreturnedOutputsTag.size(); i++) { + GenericStack gs = GenericStack.readTag(unreturnedOutputsTag.getCompound(i)); + if (gs != null && gs.amount() > 0L) { + unreturnedOutputAmounts.add(gs); + } + } + this.unreturnedOutputs.addAll(unreturnedOutputAmounts.segments()); + + ListTag craftingOutputsTag = tag.getList("QueuedCraftingOutputs", Tag.TAG_COMPOUND); + for (int i = 0; i < craftingOutputsTag.size(); i++) { + CompoundTag entry = craftingOutputsTag.getCompound(i); + ListTag outputTag = entry.getList("Outputs", Tag.TAG_COMPOUND); + // 老存档没有 Format 标签(getByte 返回 0),自动走逐段格式的读取分支。 + CraftingAeAmountAccumulator amounts = entry.getByte("Format") >= QUEUED_OUTPUT_FORMAT_COMPACT + ? CraftingAeAmountAccumulator.readCompactTag(registries, outputTag) + : readLegacyCraftingOutputs(registries, outputTag); + if (!amounts.isEmpty()) { + // 重载后重新计时:这批产物尚未回网,必须再等至少一个 tick 才能注入。 + // 账本直接交给条目(构造函数会拷贝一份隔离)。 + enqueueCraftingOutput(new PendingCraftingOutput( + level.getGameTime(), amounts)); + } + } + return true; + } + + /** 旧存档格式(Format < 2):逐段写的 {@code GenericStack} 列表,每个 {@code Long.MAX} 一段。 */ + private static CraftingAeAmountAccumulator readLegacyCraftingOutputs( + HolderLookup.Provider registries, ListTag outputTag) { + CraftingAeAmountAccumulator amounts = new CraftingAeAmountAccumulator(); + for (int index = 0; index < outputTag.size(); index++) { + GenericStack stack = GenericStack.readTag(outputTag.getCompound(index)); + if (stack != null && stack.amount() > 0L) { + amounts.add(stack); + } + } + return amounts; + } + + private void returnSavedBatchMaterials(CompoundTag tag, HolderLookup.Provider registries) { + List inputs = new ArrayList<>(); + ListTag craftsTag = tag.getList("Crafts", Tag.TAG_COMPOUND); + for (int i = 0; i < craftsTag.size(); i++) { + inputs.add(readKeyCounters(registries, craftsTag.getCompound(i))); + } + CraftingTask.returnInputsToAE(inputs, this.owner); + } + + public void updateClientTaskProgress(List tasks) { + synchronized (this.clientTaskProgressList) { + this.clientTaskProgressList.clear(); + for (var taskData : tasks) { + this.clientTaskProgressList.add(new AETaskProgress( + taskData.productName, + taskData.progress, + taskData.maxProgress, + taskData.craftCount, + taskData.totalOutputCount, + taskData.statusKey, + taskData.statusDetail + )); + } + } + } + + public void sendAETaskProgressToClients() { + Level level = this.owner.getLevel(); + if (!(level instanceof ServerLevel serverLevel)) return; + + var packet = createTaskProgressPacket(); + PacketDistributor.sendToPlayersTrackingChunk(serverLevel, + new net.minecraft.world.level.ChunkPos(this.owner.getBlockPos()), packet); + } + + public void sendAETaskProgressToPlayer(ServerPlayer player) { + if (player == null || !(this.owner.getLevel() instanceof ServerLevel)) return; + PacketDistributor.sendToPlayer(player, createTaskProgressPacket()); + } + + private AETaskProgressPacket createTaskProgressPacket() { + List taskDataList = new ArrayList<>(); + for (var entry : this.aeTaskProgressMap.entrySet()) { + AETaskProgress progress = entry.getValue(); + taskDataList.add(new AETaskProgressPacket.TaskProgressData( + progress.getProductName(), + progress.getProgress(), + progress.getMaxProgress(), + progress.getCraftCount(), + progress.getTotalOutputCount(), + progress.getStatusKey(), + progress.getStatusDetail() + )); + } + + synchronized (this.aePendingBatches) { + for (PendingAEBatch batch : this.aePendingBatches.values()) { + AETaskProgress progress = this.createPendingProgress(batch); + taskDataList.add(new AETaskProgressPacket.TaskProgressData( + progress.getProductName(), + progress.getProgress(), + progress.getMaxProgress(), + progress.getCraftCount(), + progress.getTotalOutputCount(), + progress.getStatusKey(), + progress.getStatusDetail() + )); + } + } + + return new AETaskProgressPacket(this.owner.getBlockPos(), taskDataList); + } + + public List getAvailablePatterns() { + return Collections.unmodifiableList(this.patterns); + } + + public int getPatternPriority() { + return this.patternPriority; + } + + public void setPatternPriority(int value) { + this.patternPriority = value; + } + + public boolean pushPattern(IPatternDetails patternDetails, KeyCounter[] inputHolder) { + SmartDoublingPatterns.Resolved execution = SmartDoublingPatterns.resolve(patternDetails); + IPatternDetails original = execution.pattern(); + + if (this.owner.getMainNode() == null || !this.owner.getMainNode().isActive() + || !this.patterns.contains(original) + || execution.operationsPerPush() > SmartDoublingPatterns.maximumSafeMultiplier(original)) { + return false; + } + + // AE2 合成样板:万象合金炉没有合成台,由本机在虚拟 3×3 工作台上自执行。 + // 倍率来自推送方:AE2 会把样板包装成“一次推送代表 N 次合成”,此时输入与预期产物都已按 N 放大。 + if (original instanceof IMolecularAssemblerSupportedPattern craftingPattern) { + return this.pushCraftingPattern(craftingPattern, execution.operationsPerPush(), inputHolder); + } + + synchronized (this.aePendingBatches) { + PendingAEBatch batch = this.findOrCreateBatch( + original, execution.operationsPerPush(), inputHolder); + batch.add(inputHolder); + } + this.sendAETaskProgressToClients(); + return true; + } + + public boolean isBusy() { + // 只有到「硬上限」才报忙:在此之前单批规模由 AIMD 控制器收敛,容量不会归零。 + // 报忙会让 AE2 直接跳过本供应器(又一个二元门),把控制器的意义抵消掉。 + return this.activeTasks.size() >= this.owner.getMaxAETaskCount() + || isOutputBacklogFull(); + } + + /** + * 合成样板「回网队列」的容量 = 本机的线圈线程数。 + * + *

线程数既是炉子的并行度、也是它一 tick 能接多少个合成样板窗口:三位一体按窗口派发, + * 每个窗口的产物要在这里排一次队再回网,所以队列深度跟线程数对齐(想更快就升级线圈线程数)。 + * 超过这个深度时同时通过 {@code isBusy()} 与容量上报挡住,避免材料滞留在队列里。

+ */ + private int craftingPatternQueueCap() { + return Math.max(1, this.owner.getMaxAETaskCount()); + } + + /** + * 单批分段预算的天花板 = 本机线程数。 + * + *

这是物理上限(一份窗口最多产出一段),不是人为旋钮。原来还额外取了一个固定常量 + * (先后是 20480 / 131072 / 524288)作天花板,但它与「时间预算」重复: + * 时间预算已限定每 tick 能交付多少,前馈公式又把它换算成段数,再叠一层常量只会变成隐藏瓶颈 + * ——实测已两次出现该常量恒等于 {@code segmentBudget} 的情况。该常量已删除。

+ */ + private long outputSegmentBudgetCap() { + return Math.max(1L, this.owner.getMaxAETaskCount()); + } + + /** 回网队列的条目数硬上限 —— 这是唯一真正对应内存占用的维度。 */ + private int hardOutputQueueCap() { + long cap = (long) craftingPatternQueueCap() * OUTPUT_BACKLOG_HARD_MULTIPLIER; + return (int) Math.max(2L, Math.min(Integer.MAX_VALUE, cap)); + } + + /** + * 回网队列是否已满(真拒收)。 + * + *

只按「条目数」判定,不看分段数。 原因:

+ *
    + *
  • 待回网账本是 {@code Map},内存是 O(键数) 而不是 O(段数) —— + * 分段数({@code Σ ceil(量 / Long.MAX)})是推导出来的数字,不是分配量, + * 以分段数作为内存占用的代理指标是错误的。
  • + *
  • 单批的段数本身可以很大:例如 32Y(3.2e25)份约为 347 万段, + * 这属于正常体量,不应按「积压异常」拒收。
  • + *
  • 「一批太大导致回网耗时」已由每 tick 时间预算管住:回网在预算处停下、 + * 跨 tick 摊开,不会阻塞 tick。
  • + *
+ * + *

此前按分段数判定的 {@code HARD_OUTPUT_CHUNK_BUDGET} 即因该误判, + * 将大数批次视为积压并拒收,是外部 CPU 侧「提交被拒」的根因。该常量已删除。

+ */ + private boolean isOutputBacklogFull() { + return this.queuedCraftingOutputs.size() >= hardOutputQueueCap(); + } + + /** 把产物总量换算成 {@code long} 分段数(饱和 long)。 */ + private static long backlogChunks(BigInteger amount) { + if (amount == null || amount.signum() <= 0) { + return 0L; + } + BigInteger[] quotientAndRemainder = amount.divideAndRemainder(MAX_OUTPUT_CHUNK); + long whole = quotientAndRemainder[0].compareTo(MAX_OUTPUT_CHUNK) >= 0 + ? Long.MAX_VALUE : quotientAndRemainder[0].longValueExact(); + return saturatingAdd(whole, quotientAndRemainder[1].signum() > 0 ? 1L : 0L); + } + + /** 增减回网积压总量(不会低于 0)。 */ + private void addPendingOutputAmount(BigInteger delta) { + if (delta == null || delta.signum() == 0) { + return; + } + BigInteger updated = this.pendingOutputAmount.add(delta); + this.pendingOutputAmount = updated.signum() <= 0 ? BigInteger.ZERO : updated; + } + + /** + * 把一条待回网产物入队,并同步积压总量。 + * + *

所有入队都必须走这里 —— 漏一处就会让积压度量失真(进而影响 {@link #outputSegmentBudget()} 的判断)。

+ */ + private void enqueueCraftingOutput(PendingCraftingOutput pending) { + this.queuedCraftingOutputs.add(pending); + addPendingOutputAmount(pending.ledger.totalAmount()); + } + + /** + * 本 tick 允许单个批次产生的分段数。 + * + *

由宿主方块实体通过 {@link CraftingTaskContext#outputSegmentBudget()} 转发给上层, + * 再传进 {@link AlloyFurnaceBigIntegerCrafting#maximumSegmentedCount}。

+ * + *

前馈算出,不用反馈环:{@code 时间预算 ÷ 实测单段插入耗时 ÷ 安全系数} + * —— 也就是「半个 tick 能交付多少段」。单批恰好能在一两个 tick 内交付完, + * 既不会因首批过大而长时间积压,也不会因单批过小而浪费吞吐。

+ * + *

为什么删掉反馈环:先后试过「开关式 AIMD」与「比例调节」两版反馈控制 —— + * 前者必然锯齿(拥塞信号与被控量同量级),后者增长太慢且目标自引用导致恢复力弱、会卡在低位。 + * 而交付能力其实可以直接测出来(实测单段插入 0.65µs),所以不需要「试」: + * 前馈没有闭环就没有振荡,也不会卡死。

+ */ + public long outputSegmentBudget() { + // 用「实际生效的预算」(已按全局降频收窄)而不是配置原值:批次规模自动跟随真实交付能力, + // 降频时批次同步变小 —— 所以不再需要叠一层固定天花板常量。 + long effectiveNanos = AlloyFurnaceTickBudget.scaledFlushBudget(flushBudgetNanos()); + long perTick = (long) (effectiveNanos / Math.max(1.0D, this.measuredInsertNanosPerChunk)); + return Math.max(1L, Math.min(outputSegmentBudgetCap(), + perTick / OUTPUT_BUDGET_SAFETY_DIVISOR)); + } + + + /** + * Returns the number of provider slots that can accept another physical AE submission. + * Processing patterns use active furnace tasks. Crafting patterns finish synchronously; + * their queue only provides bounded output backpressure. + * + *

两个分支都返回真实剩余槽位而不是「未满就返回满」:上层会把每一条剩余槽位 + * 当成一条独立的 TARGETED route 上报给三位一体,报多了等于超卖自己的并发。

+ */ + public int getRemainingAETaskCount(boolean craftingPattern) { + int maximum = Math.max(0, this.owner.getMaxAETaskCount()); + if (craftingPattern) { + // 以「硬上限」而非软阈值计算槽位:回网积压不应使可用槽位直接归零 + //(否则调度侧会停顿一拍再重启,表现为合成不连续);单批规模由 AIMD 控制器收敛。 + int cap = hardOutputQueueCap(); + return Math.max(0, cap - Math.min(cap, this.queuedCraftingOutputs.size())); + } + int occupied = Math.max(0, this.activeAETaskCount.get()); + return Math.max(0, maximum - Math.min(maximum, occupied)); + } + + // ==================== 合成样板自执行 ==================== + + /** + * 在虚拟 3×3 工作台上装配合成样板,并把产物交给延迟回网队列。 + * + *

检验与铺料都在输入副本上进行:任何一步失败都直接返回 {@code false}, + * AE2 抽出的原材料保持原样,由上层继续寻找其它供应器或重试。

+ * + *

倍率 N > 1 时按「可重复的一份」折叠:铺一份料后,如果剩下的材料逐键都恰好是这一份消耗量的 + * (N-1) 倍,说明整批的每一份输入完全一样,于是只装配一次、把产物整体放大 N 倍。一批里混着不同 + * 变体(例如耐久各异的工具)时这条不变量先不成立,此时就真实地一份一份装配,直到剩下的材料再次 + * 变成整齐的重复 —— 这样连混合变体也能得到与逐份装配一致的结果。探针次数有上限,真撞上病态输入时 + * 按最近一份的产出整体放大,语义与批量合并保持一致。

+ * + *

可复用输入(注魔水晶这类)单独处理:它们的返还物仍能当同一个槽的输入 + * (见 {@link ReusablePatternInputs}),所以 AE2 的合成计划对它们只算 1 份、也就只给 1 份。 + * 这类槽位从「这一批消耗了什么」里摘出来单独存放,供每一轮装配复用;返还物整批只记一次、 + * 不参与倍率放大 —— AE2 的 {@code expectedContainerItems} 只期望收回那一份。 + * 这与 AE2 自己的 {@code limitQty}(遇到容器物品就一次只模拟一件)语义一致。

+ * + * @param operationsPerPush 本次推送代表的合成次数,恒为正 + */ + private boolean pushCraftingPattern(IMolecularAssemblerSupportedPattern pattern, long operationsPerPush, + KeyCounter[] inputHolder) { + Level level = this.owner.getLevel(); + if (level == null || level.isClientSide || !this.owner.isTaskExecutionEnabled()) { + return false; + } + if (inputHolder == null) { + return false; + } + // 背压:上一批产物尚未写回网络时不接收新批次,避免材料滞留在队列中。 + // 只在「硬上限」拒收 —— 在此之前单批规模由 AIMD 控制器收敛,容量不会归零, + // 所以正常永远走不到这里(它只是网络长期不可达时的兜底)。 + if (isOutputBacklogFull()) { + return false; + } + + KeyCounter[] working = copyCounters(inputHolder); + // 可复用输入(注魔水晶这类「用完还回来、且还回来还能用」的槽):AE2 的合成计划对它只算 1 份 + // (见 ReusablePatternInputs),所以整批复用同一份、账本里只记一次返还,倍率不能放大它。 + // 关卡从本机取(owner.getLevel()),与计划期从供应器方块实体取到的是同一台机器。 + // + // ⚠️ 这里<只能降级、不能拒收>:本方法返回 false 会让 AE2 下一 tick 拿同一批料重试同一个 + // 供应器,而重试条件与上一次完全相同 —— 于是每 tick 都拒收,任务永远不推进 + // (表现为「下单后不合成」,且日志里一行错误都没有)。 + Map reusableSlots = ReusablePatternInputs.reusableRemainders(pattern, level); + KeyCounter[] reusableStock = new KeyCounter[inputHolder.length]; + Map reusableRemainders = new LinkedHashMap<>(); + Set reusableOutputKeys = new LinkedHashSet<>(); + boolean hasReusableInputs = false; + for (var reusable : reusableSlots.entrySet()) { + int slot = reusable.getKey(); + if (slot < 0 || slot >= inputHolder.length || working[slot] == null || working[slot].isEmpty()) { + // 输入计数器与样板输入不匹配,或 AE2 这次没给这份催化剂:跳过该槽的特殊处理, + // 交给下面的普通路径。绝不拒收。 + continue; + } + KeyCounter stock = new KeyCounter(); + stock.addAll(working[slot]); + reusableStock[slot] = stock; + // 从 working 里摘掉:它不该被算成「这一批消耗掉的材料」, + // 否则 matchesBatchShape 会看到它没被消耗而放弃整批折叠。 + working[slot].clear(); + hasReusableInputs = true; + // 返还键直接采用<计划期同一函数>算出的那一个,不再拿实际抽到的键去 getRemainingKey: + // 后者对「计划期候选之外的变体」会返回 null,而这里一旦拒收就是永久卡死。 + AEKey plannedRemainder = reusable.getValue(); + for (var entry : stock) { + long amount = entry.getLongValue(); + if (amount <= 0L) { + continue; + } + reusableRemainders.merge(plannedRemainder, amount, + AdvancedAlloyFurnaceAeManager::saturatingAdd); + // 收到的那批键本身也要摘掉:带耐久返还的物品附了「耐久」时有概率原样返还, + // 那时装配产生的返还键就是收到的那一个 —— 不摘掉会跟着倍率放大成 N 份。 + reusableOutputKeys.add(entry.getKey()); + } + reusableOutputKeys.add(plannedRemainder); + } + + // 产出一律先并入 BigInteger 账本再切段:倍率可以到 long 上限,逐条 long 累加一定会丢账。 + CraftingAeAmountAccumulator produced = new CraftingAeAmountAccumulator(); + // 不含返还物的那份产出:探针兜底放大时绝不能再把返还物乘一遍。 + List lastUnitOutputs = null; + boolean reusableRemainderRecorded = false; + long craftsLeft = Math.max(1L, operationsPerPush); + int probes = 0; + + while (craftsLeft > 0L && probes < MAX_CRAFTING_PROBES) { + probes++; + Object2LongMap before = snapshotAmounts(working); + List grid = emptyCraftingGrid(); + // 可复用槽位由 reusableStock 单独供料;每轮新建合并副本,扣料只扣在副本上。 + KeyCounter[] fillSource = hasReusableInputs + ? mergeWithReusableStock(working, reusableStock) : working; + pattern.fillCraftingGrid(fillSource, (slot, stack) -> { + if (slot >= 0 && slot < CRAFTING_GRID_SIZE) { + grid.set(slot, stack); + } + }); + if (hasReusableInputs) { + for (int slot = 0; slot < working.length; slot++) { + if (!reusableSlots.containsKey(slot)) { + working[slot] = fillSource[slot]; + } + } + } + + long repeat = 1L; + if (matchesBatchShape(before, working, craftsLeft)) { + // 整批材料都是这一份的整齐重复,可以一次算完 + repeat = craftsLeft; + craftsLeft = 0L; + clearCounters(working); + } else { + craftsLeft--; + } + + List unitOutputs = assembleCraftingPattern(pattern, grid, level); + if (unitOutputs == null) { + return false; + } + lastUnitOutputs = withoutKeys(unitOutputs, reusableOutputKeys); + accumulateScaled(produced, lastUnitOutputs, repeat); + if (!reusableRemainderRecorded) { + // 返还物整批只记一次、不随倍率放大:AE2 对这类输入只期望收回抽出去的那几份。 + for (var entry : reusableRemainders.entrySet()) { + produced.add(entry.getKey(), BigInteger.valueOf(entry.getValue())); + } + reusableRemainderRecorded = true; + } + } + + if (craftsLeft > 0L) { + // 探针预算耗尽:按最近一份的产出整体放大(兜底语义与三位一体式批量一致) + if (lastUnitOutputs == null) { + return false; + } + accumulateScaled(produced, lastUnitOutputs, craftsLeft); + } + if (produced.isEmpty()) { + return false; + } + + // 能量:与万象样板、bigint 合成样板共用同一套口径。合成样板未绑定配方,单份基准能耗由 + // craftingPatternUnitEnergy 依当前催化剂的处理时间算出;有用线圈下 + // energyMultipliesWithParallel 为 false,整批只收一次固定能耗,与 operationsPerPush 无关。 + // + // 与上面的可复用输入不同,这里的 false 是「能量不足,稍后重试」:重试条件随能量补充而变化, + // 不会形成每 tick 恒定拒收的死循环。 + ResolvedCatalystEffect energyEffect = this.owner.resolveTaskEffect(null); + long totalEnergy = AlloyFurnaceBigIntegerCrafting.totalEnergy( + AlloyFurnaceBigIntegerCrafting.craftingPatternUnitEnergy(energyEffect), + BigInteger.valueOf(Math.max(1L, operationsPerPush)), + energyEffect); + if (totalEnergy > 0L) { + IEnergyManager energy = this.owner.getEnergyManager(); + if (totalEnergy > energy.getEnergyStoredLong() || !energy.tryConsumeEnergy(totalEnergy)) { + return false; + } + } + + // 走到这里才算接收:AE2 随后会把本批预期产物写入 CPU 的 waitingFor。 + for (KeyCounter counter : inputHolder) { + counter.clear(); + } + enqueueCraftingOutput(new PendingCraftingOutput( + level.getGameTime(), produced)); + this.owner.markChanged(); + return true; + } + + // ==================== 原生 bigint(exact)批次 ==================== + + /** + * 接收数据能源 3.3.0 的原生 bigint 批次({@code BigIntegerCraftingProviderAdapter})。 + * + *

与 {@link #pushPattern} 的长版倍率路径有两处关键差异:

+ *
    + *
  • 拿到的 prototype 是单次合成的原型:DE 只把次数通过 {@code exactCount()} 交给我们, + * 剩下的 {@code count-1} 份材料由 DE 自己的 BigInteger 账本扣除,所以我们只消费手里这一份原型;
  • + *
  • 批次是「完整的同质批」:DE 保证整批输入完全一致,因此装配一次再整体 ×count 就是精确结果 + * —— 不需要 {@link #matchesBatchShape} 那套逐键折叠探测。
  • + *
+ * + *

只处理合成样板;其它样板返回 {@code false},继续由长版 counted 路径服务。

+ */ + /** + * 数据能源适配器的 bigint 提交入口({@code BigIntegerPush},无 CPU 回执)。 + * + * @see #pushBigIntegerBatch(IPatternDetails, BigInteger, KeyCounter[], AlloyFurnaceBigIntegerCpuBinding) + */ + public boolean pushBigIntegerCraftingPattern(IPatternDetails patternDetails, + BigInteger count, + KeyCounter[] unitPrototype) { + return pushBigIntegerBatch(patternDetails, count, unitPrototype, null); + } + + /** + * 原生 bigint 批次的统一入口:按样板种类路由到对应的折叠实现。 + * + *
    + *
  • 万象样板({@link OmniversalPatternDetails}):解析绑定配方一次 → 产物 ×count, + * 按 {@code count × 单份能耗} 收能量;
  • + *
  • AE2 合成样板({@link IMolecularAssemblerSupportedPattern}):在虚拟 3×3 工作台上 + * 装配一次 → 产物 ×count,不收能量;
  • + *
  • 其它样板返回 {@code false},由长版 counted 路径继续服务。
  • + *
+ * + * @param cpuBinding CPU 侧回执绑定;{@code null} 表示产物只切段写回 ME 网络 + */ + public boolean pushBigIntegerBatch(IPatternDetails patternDetails, + BigInteger count, + KeyCounter[] unitPrototype, + @Nullable AlloyFurnaceBigIntegerCpuBinding cpuBinding) { + Level level = this.owner.getLevel(); + if (level == null || level.isClientSide || !this.owner.isTaskExecutionEnabled()) { + return false; + } + if (count == null || count.signum() <= 0 || unitPrototype == null) { + return false; + } + IPatternDetails original = SmartDoublingPatterns.unwrap(patternDetails); + if (original instanceof OmniversalPatternDetails omniversal) { + return commitOmniversalBatch(omniversal, count, unitPrototype, cpuBinding); + } + return original instanceof IMolecularAssemblerSupportedPattern craftingPattern + && commitCraftingBatch(craftingPattern, count, unitPrototype, cpuBinding); + } + + /** + * 合成样板的 bigint 折叠:在虚拟 3×3 工作台上装配一次,产物整体 ×count。 + * + *

能量按「单份基准能耗 × 并行口径」计收,与万象样板共用 + * {@link AlloyFurnaceBigIntegerCrafting#totalEnergy(long, BigInteger, ResolvedCatalystEffect)}: + * 合成样板未绑定配方,单份基准能耗由 + * {@link AlloyFurnaceBigIntegerCrafting#craftingPatternUnitEnergy(ResolvedCatalystEffect)} 依当前 + * 催化剂的处理时间算出。有用线圈下 + * {@code energyMultipliesWithParallel} 为 {@code false},整批只收一次固定能耗,与 count 无关。

+ */ + private boolean commitCraftingBatch(IMolecularAssemblerSupportedPattern craftingPattern, + BigInteger count, + KeyCounter[] unitPrototype, + @Nullable AlloyFurnaceBigIntegerCpuBinding cpuBinding) { + Level level = this.owner.getLevel(); + if (level == null || level.isClientSide) { + return false; + } + // 背压:只在「硬上限」拒收。在此之前单批规模由 AIMD 控制器收敛,容量不会归零。 + if (isOutputBacklogFull()) { + return false; + } + + // 先把产出算出来(纯计算、不改状态),再扣能量 + long useless$bigintStarted = System.nanoTime(); + KeyCounter[] working = copyCounters(unitPrototype); + List grid = emptyCraftingGrid(); + craftingPattern.fillCraftingGrid(working, (slot, stack) -> { + if (slot >= 0 && slot < CRAFTING_GRID_SIZE) { + grid.set(slot, stack); + } + }); + List unitOutputs = assembleCraftingPattern(craftingPattern, grid, level); + if (unitOutputs == null) { + return false; + } + + CraftingAeAmountAccumulator produced = new CraftingAeAmountAccumulator(); + accumulateScaled(produced, unitOutputs, count); + if (produced.isEmpty()) { + return false; + } + // 能量:口径与万象样板、长版任务完全一致(与并行相关时按 count 放大)。 + ResolvedCatalystEffect effect = this.owner.resolveTaskEffect(null); + long totalEnergy = AlloyFurnaceBigIntegerCrafting.totalEnergy( + AlloyFurnaceBigIntegerCrafting.craftingPatternUnitEnergy(effect), count, effect); + if (totalEnergy > 0L) { + IEnergyManager energy = this.owner.getEnergyManager(); + if (totalEnergy > energy.getEnergyStoredLong() || !energy.tryConsumeEnergy(totalEnergy)) { + return false; + } + } + + // 走到这里才算接收:清空收到的单位原型(count-1 份由调用方的账本扣除),产物进回网队列。 + AlloyFurnaceBigIntegerBatchContext cpuContext = cpuBinding == null + ? null : buildCpuContext(craftingPattern, count, totalEnergy, cpuBinding); + PendingCraftingOutput pending = new PendingCraftingOutput( + level.getGameTime(), + produced, + AlloyFurnaceBigIntegerCrafting.scaledOutputs(unitOutputs, count), + cpuContext, + cpuBinding == null ? null : cpuBinding.adapterId()); + for (KeyCounter counter : unitPrototype) { + counter.clear(); + } + enqueueCraftingOutput(pending); + if (cpuContext != null) { + notifyCpuBatchAdmitted(cpuContext, cpuBinding.adapterId()); + } + // 报账给「每 tick 时间预算」:超预算时后续批次的窗口预算会被自动收窄 + AlloyFurnaceTickBudget.addWork(System.nanoTime() - useless$bigintStarted); + this.owner.markChanged(); + return true; + } + + // ==================== 万象样板的原生 bigint(exact)批次 ==================== + + /** + * 接收万象样板的原生 bigint 批次(折叠语义)。 + * + *

与合成样板那条 bigint 路径的区别:万象样板绑定了一个合金炉配方,所以「单份产出」直接来自 + * 配方(主产物 + 流体 + 隐藏键产出),代价是本机要按 {@code count × 单份能耗} 收能量; + * 合成样板则是虚拟工作台装配一次再放大,不收能量。

+ * + *

检查顺序不能改:配方可用性(档次/模具)、回网背压、能量这三类可能失败的事全部排在 + * 「清空原型」之前 —— 返回 {@code false} 时调用方手里的原型完好无损,它自己扣掉的那 + * {@code count - 1} 份也能安全回滚。

+ * + * @param count 份数,可以超过 {@code long} + * @param unitPrototype 单次推送的原型(不是 ×count 的整批材料) + * @param cpuBinding CPU 侧回执绑定;{@code null} 表示产物只切段写回 ME 网络 + */ + private boolean commitOmniversalBatch(OmniversalPatternDetails pattern, + BigInteger count, + KeyCounter[] unitPrototype, + @Nullable AlloyFurnaceBigIntegerCpuBinding cpuBinding) { + Level level = this.owner.getLevel(); + if (level == null || level.isClientSide || !this.owner.isTaskExecutionEnabled()) { + return false; + } + if (!this.owner.supportsBigIntegerRecipeBatches()) { + return false; + } + AdvancedAlloyFurnaceRecipe recipe = pattern.recipe(); + if (recipe == null || !this.owner.isTaskRecipeAvailable(recipe)) { + return false; + } + // 背压:与合成样板共用「回网队列」,同样只在硬上限拒收(折叠语义不建长任务,不占 activeTasks 名额)。 + if (isOutputBacklogFull()) { + return false; + } + long manualOperations = SmartDoublingPatterns.manualOperationsPerPattern(recipe, pattern); + if (manualOperations <= 0L) { + return false; + } + List unitOutputs = AlloyFurnaceBigIntegerCrafting.unitOutputs(recipe, manualOperations); + if (unitOutputs.isEmpty()) { + return false; + } + // 先把产出算出来(纯计算、不改状态),再扣能量 —— 这样任何一条失败路径都不会「扣了能量却不接收」。 + CraftingAeAmountAccumulator produced = new CraftingAeAmountAccumulator(); + accumulateScaled(produced, unitOutputs, count); + if (produced.isEmpty()) { + return false; + } + // 能量:口径与长版任务的 calculateTargetTotalEnergy 完全一致(与并行相关时按 count 放大)。 + ResolvedCatalystEffect effect = this.owner.resolveTaskEffect(recipe); + long totalEnergy = AlloyFurnaceBigIntegerCrafting.totalEnergy(recipe, count, effect); + if (totalEnergy > 0L) { + IEnergyManager energy = this.owner.getEnergyManager(); + if (totalEnergy > energy.getEnergyStoredLong() || !energy.tryConsumeEnergy(totalEnergy)) { + return false; + } + } + + long useless$bigintStarted = System.nanoTime(); + + // 走到这里才算接收:只消费手里那一份原型(count-1 份由调用方的 BigInteger 账本扣除), + // 产物进回网队列。回执上下文先建好,成功入队后再通知 CPU 侧「已受理」。 + AlloyFurnaceBigIntegerBatchContext cpuContext = cpuBinding == null + ? null : buildCpuContext(pattern, count, totalEnergy, cpuBinding); + PendingCraftingOutput pending = new PendingCraftingOutput( + level.getGameTime(), + produced, + AlloyFurnaceBigIntegerCrafting.scaledOutputs(unitOutputs, count), + cpuContext, + cpuBinding == null ? null : cpuBinding.adapterId()); + for (KeyCounter counter : unitPrototype) { + counter.clear(); + } + enqueueCraftingOutput(pending); + if (cpuContext != null) { + notifyCpuBatchAdmitted(cpuContext, cpuBinding.adapterId()); + } + // 报账给「每 tick 时间预算」:超预算时后续批次的窗口预算会被自动收窄 + AlloyFurnaceTickBudget.addWork(System.nanoTime() - useless$bigintStarted); + this.owner.markChanged(); + return true; + } + + /** 组装一次大数批次的 CPU 回执上下文(合成样板与万象样板共用)。 */ + private @NotNull AlloyFurnaceBigIntegerBatchContext buildCpuContext( + IPatternDetails pattern, + BigInteger count, + long energyCharged, + AlloyFurnaceBigIntegerCpuBinding binding) { + return new AlloyFurnaceBigIntegerBatchContext( + UUID.randomUUID(), + binding.cpuToken(), + AlloyFurnaceBigIntegerCrafting.machineIdentity( + "machine@" + this.owner.getBlockPos().asLong(), + this.owner.getLevel(), + this.owner.getBlockPos()), + pattern, + count, + AlloyFurnaceBigIntegerCrafting.plannedOutputs(pattern, count), + energyCharged); + } + + // ==================== CPU 侧回执 ==================== + + private static void notifyCpuBatchAdmitted(AlloyFurnaceBigIntegerBatchContext context, + ResourceLocation adapterId) { + AlloyFurnaceBigIntegerCpuAdapter adapter = + AlloyFurnaceBigIntegerCpuAdapters.find(adapterId).orElse(null); + if (adapter != null) { + runCpuCallback(() -> adapter.onBatchAdmitted(context), adapterId, "onBatchAdmitted"); + } + } + + /** + * 产物全部回网后通知 CPU 侧。 + * + *

实际产出直接取入队时算好的 BigInteger 精确值,不去累加 long 分段 —— 那会溢出。

+ */ + private static void notifyCpuBatchCompleted(PendingCraftingOutput pending) { + AlloyFurnaceBigIntegerBatchContext context = pending.cpuContext; + ResourceLocation adapterId = pending.cpuAdapterId; + if (context == null || adapterId == null || pending.cpuNotified) { + return; + } + // 取消流程可能已经发过 CANCELLED;一个批次只能有一个终局。 + pending.cpuNotified = true; + AlloyFurnaceBigIntegerCpuAdapter adapter = + AlloyFurnaceBigIntegerCpuAdapters.find(adapterId).orElse(null); + if (adapter == null) { + return; + } + List outputs = pending.bigIntegerOutputs; + if (outputs != null && !outputs.isEmpty()) { + runCpuCallback(() -> adapter.onBatchOutputs(context, outputs), adapterId, "onBatchOutputs"); + } + runCpuCallback(() -> adapter.onBatchFinished(context, AlloyFurnaceBigIntegerBatchResult.SUCCESS), + adapterId, "onBatchFinished"); + } + + /** 批次被取消时给 CPU 侧一个明确的终局信号,避免它一直等回执。 */ + private static void notifyCpuBatchCancelled(PendingCraftingOutput pending) { + AlloyFurnaceBigIntegerBatchContext context = pending.cpuContext; + ResourceLocation adapterId = pending.cpuAdapterId; + if (context == null || adapterId == null || pending.cpuNotified) { + return; + } + pending.cpuNotified = true; + AlloyFurnaceBigIntegerCpuAdapter adapter = + AlloyFurnaceBigIntegerCpuAdapters.find(adapterId).orElse(null); + if (adapter != null) { + runCpuCallback(() -> adapter.onBatchFinished(context, AlloyFurnaceBigIntegerBatchResult.CANCELLED), + adapterId, "onBatchFinished"); + } + } + + /** + * 回调必须隔离:第三方适配器抛异常绝不能影响机器的产物回网。 + * 记日志并吞掉,语义与 AE2 对供应器的容错一致。 + */ + /** + * 调 {@link AlloyFurnaceBigIntegerCpuAdapter#claimOutputs},把异常吞掉并记日志。 + * + *

契约要求实现不抛异常(见接口 Javadoc);真抛了也不能让回网中断 —— + * 那会导致整台机器停滞。返回 {@link BigInteger#ZERO} 表示本次未接管,退回逐段写回。

+ */ + private static @NotNull BigInteger claimOutputsFromCpu(@NotNull AlloyFurnaceBigIntegerCpuAdapter adapter, + @NotNull AlloyFurnaceBigIntegerBatchContext context, + @NotNull AEKey key, + @NotNull BigInteger amount) { + try { + Map taken = adapter.claimOutputs(context, + List.of(new AlloyFurnaceBigIntegerOutput(key, amount))); + if (taken == null) { + return BigInteger.ZERO; + } + BigInteger accepted = taken.get(key); + return accepted == null || accepted.signum() <= 0 ? BigInteger.ZERO : accepted; + } catch (RuntimeException exception) { + RateLimitedLogger.error(LOGGER, "BigInteger crafting CPU adapter {} threw in claimOutputs", adapter.id(), exception); + return BigInteger.ZERO; + } + } + + private static void runCpuCallback(Runnable callback, ResourceLocation adapterId, String name) { + try { + callback.run(); + } catch (RuntimeException exception) { + RateLimitedLogger.error(LOGGER, "BigInteger crafting CPU adapter {} threw in {}", adapterId, name, exception); + } + } + + /** + * 判断铺完一份之后,剩下的材料是否正好是这一份消耗量的 (craftsLeft - 1) 倍 —— 逐键比对。 + * + *

成立时整批的每一份输入完全相同(含同键返还的模具与催化剂),所以「装配一次再整体 ×N」 + * 与真的装配 N 次结果一致。只要有一个键的剩余量对不上(例如一批里混着不同耐久的工具), + * 就返回 {@code false},交由调用方继续逐份装配。

+ */ + private static boolean matchesBatchShape(Object2LongMap before, KeyCounter[] after, long craftsLeft) { + Object2LongMap remaining = snapshotAmounts(after); + long previousCrafts = craftsLeft - 1L; + for (var entry : before.object2LongEntrySet()) { + long left = remaining.getLong(entry.getKey()); + long consumed = entry.getLongValue() - left; + if (consumed <= 0L) { + return false; + } + long expected; + try { + expected = Math.multiplyExact(consumed, previousCrafts); + } catch (ArithmeticException exception) { + return false; + } + if (left != expected) { + return false; + } + } + // 逐键核对完之后只允许「before 之外的键」为空:铺料前的材料必须被完整解释掉。 + // 注意不能用 remaining.isEmpty()——remaining 是铺完一份后剩下的 (craftsLeft-1) 份材料, + // 它天然非空,那样写会让倍率折叠永不生效(每次推送都退化成最多 64 次真实装配)。 + for (var entry : remaining.object2LongEntrySet()) { + if (!before.containsKey(entry.getKey())) { + return false; + } + } + return true; + } + + /** + * 装配一份,返回「主产物 + 容器余料」。 + * + *

与 AE2 分子装配室的装配流程保持一致:用工作台自身的定位输入裁掉空白边距, + * 先触发合成事件再取余料。异常或空产物都返回 {@code null},调用方据此放弃本次接管。

+ */ + @Nullable + private List assembleCraftingPattern(IMolecularAssemblerSupportedPattern pattern, + List grid, Level level) { + TransientCraftingContainer container = new TransientCraftingContainer(new AutoCraftingMenu(), 3, 3); + for (int slot = 0; slot < CRAFTING_GRID_SIZE; slot++) { + container.setItem(slot, grid.get(slot).copy()); + } + CraftingContainer craftingInput = container; + + ItemStack output; + NonNullList remainders; + try { + output = pattern.assemble(craftingInput, level); + if (output.isEmpty()) { + return null; + } + if(level instanceof ServerLevel server) output.onCraftedBy(level,net.minecraftforge.common.util.FakePlayerFactory.getMinecraft(server),output.getCount()); + CraftingEvent.fireAutoCraftingEvent(level, pattern, output, container); + remainders = pattern.getRemainingItems(craftingInput); + } catch (RuntimeException exception) { + RateLimitedLogger.warn(LOGGER, "Failed to self-assemble a crafting pattern at {}", this.owner.getBlockPos(), exception); + return null; + } + + List unitOutputs = new ArrayList<>(remainders.size() + 1); + collectProduced(unitOutputs, output, 1L); + for (ItemStack remainder : remainders) { + collectProduced(unitOutputs, remainder, 1L); + } + return unitOutputs.isEmpty() ? null : unitOutputs; + } + + private static List emptyCraftingGrid() { + List grid = new ArrayList<>(CRAFTING_GRID_SIZE); + for (int slot = 0; slot < CRAFTING_GRID_SIZE; slot++) { + grid.add(ItemStack.EMPTY); + } + return grid; + } + + /** + * 填格用的合并副本:消耗品取 {@code working},可复用槽位并入 {@code reusableStock}。 + * + *

每轮都要新建副本:{@code fillCraftingGrid} 会从传进去的计数器里扣料,扣在副本上才不会把 + * 「这一批消耗了什么」和「这份催化剂还在手里」两件事搞混。用 {@link KeyCounter#addAll} 而不是 + * 逐个 {@code add},保住底层映射结构(原因见 {@link #copyCounters} 的说明)。

+ */ + private static KeyCounter[] mergeWithReusableStock(KeyCounter[] working, KeyCounter[] reusableStock) { + KeyCounter[] merged = new KeyCounter[working.length]; + for (int slot = 0; slot < working.length; slot++) { + KeyCounter counter = new KeyCounter(); + if (working[slot] != null) { + counter.addAll(working[slot]); + } + if (reusableStock[slot] != null) { + counter.addAll(reusableStock[slot]); + } + merged[slot] = counter; + } + return merged; + } + + /** + * 把可复用槽位的产出从单份产出里摘掉。 + * + *

{@code excluded} 同时包含「收到的那批键」与「它们的返还键」:返还物在正常情况是受损后的 + * 另一个键,但附了「耐久」的物品有概率原样返还,那时装配产生的就是收到的那一个键。 + * 两种都要摘掉,否则它会被当成普通产物跟着倍率放大成 N 份。

+ * + *

摘掉之后由调用方按计划期算好的返还键精确补记一次,与 AE2 的 + * {@code expectedContainerItems} 对齐。

+ */ + private static List withoutKeys(List outputs, Set excluded) { + if (excluded.isEmpty()) { + return outputs; + } + List filtered = new ArrayList<>(outputs.size()); + for (GenericStack output : outputs) { + if (output != null && !excluded.contains(output.what())) { + filtered.add(output); + } + } + return filtered; + } + + private static void clearCounters(KeyCounter[] counters) { + for (KeyCounter counter : counters) { + if (counter != null) { + counter.clear(); + } + } + } + + /** 把一组输入计数器按 AE 键汇总;键仍保留各自的变体身份。 */ + private static Object2LongMap snapshotAmounts(KeyCounter[] counters) { + Object2LongOpenHashMap amounts = new Object2LongOpenHashMap<>(); + for (KeyCounter counter : counters) { + if (counter == null) { + continue; + } + for (var entry : counter) { + if (entry.getKey() == null || entry.getLongValue() <= 0L) { + continue; + } + long existing = amounts.getLong(entry.getKey()); + amounts.put(entry.getKey(), existing > Long.MAX_VALUE - entry.getLongValue() + ? Long.MAX_VALUE : existing + entry.getLongValue()); + } + } + return amounts; + } + + /** + * 把一份装配的产物流按倍率并入 BigInteger 账本。 + * + *

倍率可以一路到 {@code long} 上限,单键总量会超过 {@code long}:这里只用 + * {@link BigInteger} 做乘法与累加,最后由 {@link CraftingAeAmountAccumulator#segments()} + * 切成若干 long 条目。绝不能像旧实现那样在 {@code Math.multiplyExact} 溢出时丢弃条目—— + * 少记的产物就是 AE2 账本上对不上的缺口。(手法与 Data Energistics 内部 + * {@code TrinityItemAmount.multiply} 的分段放大一致。)

+ */ + private static void accumulateScaled(CraftingAeAmountAccumulator target, + List unitOutputs, + long multiplier) { + accumulateScaled(target, unitOutputs, BigInteger.valueOf(Math.max(1L, multiplier))); + } + + /** BigInteger 倍率版本:数据能源的原生 bigint 批次一次可能交付超过 {@code long} 的次数。 */ + private static void accumulateScaled(CraftingAeAmountAccumulator target, + List unitOutputs, + BigInteger multiplier) { + if (multiplier.signum() <= 0) { + throw new IllegalArgumentException("Crafting batch multiplier must be positive"); + } + for (GenericStack unit : unitOutputs) { + if (unit.amount() <= 0L) { + continue; + } + target.add(unit.what(), BigInteger.valueOf(unit.amount()).multiply(multiplier)); + } + } + + /** + * 把已经装配完成、等待回网的合成样板产物写入 AE。 + * + *

该方法只由方块实体 tick 调用;provider 的 push/commit 已在此之前返回, + * 所以不需要额外等待一个 tick。写不进去的部分留在队列里逐 tick 重试, + * 队列满时通过批次数背压新的合成提交,直到产物真正回网。

+ */ + public void tickQueuedCraftingOutputs() { + flushQueuedCraftingOutputs(false); + } + + /** + * 把已装配完成、等待回网的产物写入 AE。 + * + *

BigInteger 优先:整条可回网队列先按键聚合成一本账,每个键只投递一次,且只在真正 + * 插入的那一刻才把余额切成 {@code long} 分段(AE 存储 API 只有 long,这是硬边界)。 + * 投递成功后把实际投递量按队列 FIFO 归因回各条目,条目余额清零才移除并发终局回调。

+ * + *

归因是精确的:令可回网条目为 {@code e₁..eₙ},键 k 的余额为 {@code Eᵢ}、聚合为 + * {@code T=ΣEᵢ}、本轮投递为 {@code D∈[0,T]};按 FIFO 依次扣 {@code min(剩余, Eᵢ)}, + * 因 {@code D ≤ ΣEᵢ} 故累计扣减恰为 {@code D},单条扣减又不会超过其实际余额 ⇒ 不丢不重。

+ * + * @param force true 时忽略「必须晚一个 tick」的保护,且不受每 tick 预算与单键分段上限约束。 + * 只用于取消/拆除:此时 CPU 已放弃这批产物(落进通用存储也不会被错认), + * 但方块实体与 NBT 随后即被移除,产物须尽量一次性全部写回,否则将永久丢失。 + */ + private void flushQueuedCraftingOutputs(boolean force) { + if (this.queuedCraftingOutputs.isEmpty()) { + return; + } + Level level = this.owner.getLevel(); + if (level == null || level.isClientSide) { + return; + } + long now = level.getGameTime(); + + // 1) 收集本轮可刷条目,保持队列 FIFO 顺序(归因依赖这个顺序)。 + List flushable = new ArrayList<>(); + for (PendingCraftingOutput pending : this.queuedCraftingOutputs) { + if (force || pending.queuedTick < now) { + flushable.add(pending); + } + } + if (flushable.isEmpty()) { + return; + } + + // 2) 跨条目按键聚合:整条队列里同一个键只投递一次。 + CraftingAeAmountAccumulator total = new CraftingAeAmountAccumulator(); + for (PendingCraftingOutput pending : flushable) { + total.addAll(pending.ledger); + } + if (total.isEmpty()) { + return; + } + + // 3) 解析一次网络写入目标,整趟刷写复用。 + // 此前每写一个分段都重新解析(多方块侧 2 次方块实体查询 + 2 次分配), + // 而可持续吞吐直接由「每 tick 可插入次数」决定(一 tick 数千次)⇒ 不缓存将直接影响吞吐。 + // 不可达时直接返回且不修改任何状态(旧实现会逐段做无效重试并标脏)。 + CraftingAeOutputTarget target = this.owner.resolveAeOutputTarget(); + if (target == null) { + return; + } + // 认领提升:见 CraftingAeOutputTarget#supportsClaimHoisting —— 把 neoecoae 兼容层的认领 + // 从「每段一次」提升到「每键一次」,可减少 N-1 次 synchronized 与集合分配 + //(N ≈ 6 千/tick @8ms 预算,≈ 7 万/tick @100ms 预算)。 + final boolean hoistClaim = target.supportsClaimHoisting(); + + // 落网前的整批接收机会(见 AlloyFurnaceBigIntegerCpuAdapter#claimOutputs): + // 这是唯一能跳过逐段插入的通道 —— 愿意整批收下的 CPU 直接把量接走,我们只把余额写回网络。 + // 只在「整趟绑定的都是同一个适配器」时才给:多 CPU 混绑同一台机器时无法安全归因,退回逐段写回。 + AlloyFurnaceBigIntegerCpuAdapter handoverAdapter = null; + AlloyFurnaceBigIntegerBatchContext handoverContext = null; + ResourceLocation sharedAdapterId = null; + for (PendingCraftingOutput pending : flushable) { + ResourceLocation id = pending.cpuAdapterId; + if (id == null) { + continue; + } + if (sharedAdapterId == null) { + sharedAdapterId = id; + handoverAdapter = AlloyFurnaceBigIntegerCpuAdapters.find(id).orElse(null); + handoverContext = pending.cpuContext; + } else if (!sharedAdapterId.equals(id)) { + handoverAdapter = null; + handoverContext = null; + break; + } + } + + // 4) 逐键投递。offer 取 min(余额, Long.MAX_VALUE):存储 API 单次只有 long。 + long flushStarted = System.nanoTime(); + // 预算按全局系数动态收窄(下限 250µs):回网预算是每台机器的, + // 写死会让 N 台机器各自吃满基准值(10 台就是 40ms/tick)。 + long flushBudgetNanos = AlloyFurnaceTickBudget.scaledFlushBudget(flushBudgetNanos()); + boolean deliveredAny = false; + int emptyKeyProbes = 0; + BigInteger deliveredTotal = BigInteger.ZERO; + // 插入耗时先本地累计,整趟结束再上报一次:addWork 是 synchronized, + // 而一 tick 可能插数千次,逐段加锁本身就是可观开销。 + long insertWorkNanos = 0L; + /** 真实插入调用次数(与「被直取走的段数」区分开,用于估算单段成本)。 */ + long insertCalls = 0L; + for (var entry : total.snapshotEntries()) { + if (!force && insertWorkNanos >= flushBudgetNanos) { + break; // 预算已用尽:本 tick 到此为止(首个键因累计为 0 必定会被尝试) + } + AEKey key = entry.getKey(); + BigInteger remaining = entry.getValue(); + // 认领提升:按整键总量认领一次(见上方 hoistClaim)。 + // 认领量由兼容层自己的待满足需求封顶、与传入量无关,所以与「逐段各认领一次」结果完全相同。 + if (hoistClaim) { + long offerWhole = remaining.min(MAX_OUTPUT_CHUNK).longValueExact(); + long claimed = clampInserted(target.claim(key, offerWhole), offerWhole); + if (claimed > 0L) { + deliveredAny = true; + emptyKeyProbes = 0; + remaining = remaining.subtract(BigInteger.valueOf(claimed)); + } + } + // 整批接收:把本键的**全部**余额交给绑定的 CPU 适配器(不切段)。 + // 收下的量从待回网账本扣除,只有余额才走下面的逐段插入。 + if (handoverAdapter != null && handoverContext != null && remaining.signum() > 0) { + BigInteger accepted = claimOutputsFromCpu(handoverAdapter, handoverContext, key, remaining); + if (accepted.signum() > 0) { + deliveredAny = true; + emptyKeyProbes = 0; + remaining = remaining.subtract(accepted.min(remaining)); + } + } + // 不设段数上限:**只由时间预算决定何时停**。 + // 直接拿累计插入耗时当预算信号,这样每段只需一对 nanoTime(原来还要额外的预算检查)。 + // 循环条件在插入前判定,所以第一个分段总会尝试(预算为 0 时也进得去)。 + long chunksSinceProbe = 0L; + while (remaining.signum() > 0 && (force || insertWorkNanos < flushBudgetNanos)) { + BigInteger offer = remaining.min(MAX_OUTPUT_CHUNK); + long offerLong = offer.longValueExact(); // ≤ Long.MAX_VALUE,安全 + long inserted = clampInserted( + hoistClaim ? target.insertRaw(key, offerLong) : target.insert(key, offerLong), + offerLong); + insertCalls++; + if (inserted <= 0L) { + break; // 本键被拒收:本轮不再尝试它 + } + deliveredAny = true; + emptyKeyProbes = 0; + remaining = remaining.subtract(BigInteger.valueOf(inserted)); + // 按间隔采样预算信号(见 BUDGET_PROBE_INTERVAL_CHUNKS:逐段取时间比插入本身还贵)。 + if (++chunksSinceProbe >= BUDGET_PROBE_INTERVAL_CHUNKS) { + chunksSinceProbe = 0L; + insertWorkNanos = System.nanoTime() - flushStarted; + } + if (inserted < offerLong) { + break; // 只吸收了部分 ⇒ 存储已饱和,继续投递不会再有进展 + } + } + + // 4) 把本键实际投递量按 FIFO 归因回各条目(队首条目先被清空)。 + BigInteger delivered = entry.getValue().subtract(remaining); + if (delivered.signum() > 0) { + attributeDeliveredOutputs(flushable, key, delivered); + deliveredTotal = deliveredTotal.add(delivered); + } else if (++emptyKeyProbes >= MAX_EMPTY_KEY_PROBES) { + break; // 连续多个键都拒收 ⇒ 网络整体饱和,提前收手 + } + + if (!force && insertWorkNanos >= flushBudgetNanos) { + break; // 预算已用尽:跳出键循环 + } + } + + // 整趟墙钟即插入耗时,收尾取一次并上报。 + insertWorkNanos = System.nanoTime() - flushStarted; + AlloyFurnaceTickBudget.addWork(insertWorkNanos); + + // 更新实测单段插入成本:单批分段预算由它前馈算出。 + // 门槛见 MIN_INSERT_SAMPLE_CHUNKS:样本太小时固定开销会主导,会把预算砸下去。 + if (insertCalls >= MIN_INSERT_SAMPLE_CHUNKS && insertWorkNanos > 0L) { + double sample = (double) insertWorkNanos / (double) insertCalls; + double previous = this.measuredInsertNanosPerChunk; + double updated = this.insertCostMeasured + // 首个有效样本直接采纳:否则需从初始估计逐步收敛,期间数十批次的测量结果不可用。 + ? previous * (1.0D - INSERT_COST_SMOOTHING) + sample * INSERT_COST_SMOOTHING + : sample; + // 非对称:成本(= 耗时)上升受限,等价于「降档每次最多 5%」; + // 下降不限速,所以批次放大是即时的。 + updated = Math.min(updated, previous * MAX_INSERT_COST_RISE_PER_SAMPLE); + this.measuredInsertNanosPerChunk = Math.max(MIN_INSERT_NANOS_PER_CHUNK, updated); + this.insertCostMeasured = true; + } + + // 5) 清空条目:产物全部回网后发一次终局回调,并从队列移除。 + // 没有投递任何内容时这里不会命中,也不会 markChanged(旧实现会做无效重试并标脏)。 + boolean changed = deliveredAny; + if (deliveredTotal.signum() > 0) { + // 同步积压总量(精确减):它是 AIMD 控制器的度量来源。 + addPendingOutputAmount(deliveredTotal.negate()); + } + var iterator = this.queuedCraftingOutputs.iterator(); + while (iterator.hasNext()) { + PendingCraftingOutput pending = iterator.next(); + if (pending.isEmpty()) { + iterator.remove(); + // 排空延迟 = 从入队到被完全接走用了几个 tick。 + // 这是外部 CPU 自适应窗口能否继续放大的关键量(它要求「一批 ≤2 tick 返回完」), + // 直接量出来,不再靠推测。 + + // 产物全部回网:给 CPU 侧发实际产出与终局通知(没有绑定时是空操作)。 + notifyCpuBatchCompleted(pending); + changed = true; + } + } + if (changed) { + this.owner.markChanged(); + } + } + + /** + * 把某个键本轮实际投递的量按队列 FIFO 记回各条目。 + * + *

调用方保证 {@code delivered ≤ Σᵢ entryᵢ.ledger.amount(key)}(因为 delivered 由聚合余额 + * 逐次扣减得到)。逐条取 {@code min(剩余投递量, 该条余额)} 扣减,累计恰好等于 {@code delivered} + * —— 既不会少记(丢失物品),也不会多记(虚增扣减)。

+ */ + private static void attributeDeliveredOutputs(List entries, + AEKey key, BigInteger delivered) { + BigInteger left = delivered; + for (PendingCraftingOutput pending : entries) { + if (left.signum() <= 0) { + break; + } + left = left.subtract(pending.ledger.consume(key, left)); + } + } + + /** + * 复制 AE2 传入的输入计数器。 + * + *

必须用 {@link KeyCounter#addAll}:它保留同一主键下的变体子表与迭代顺序, + * 直接遍历后逐个 add 会丢失该结构,从而在可替代输入上选中与 AE2 不同的变体。

+ */ + private static KeyCounter[] copyCounters(KeyCounter[] source) { + KeyCounter[] copy = new KeyCounter[source.length]; + for (int index = 0; index < source.length; index++) { + KeyCounter counter = new KeyCounter(); + KeyCounter original = source[index]; + if (original != null) { + counter.addAll(original); + } + copy[index] = counter; + } + return copy; + } + + /** + * 把一次装配的单份产出按倍率累加进队列。单份产出乘倍率后可能远超单栈上限, + * 所以一律以 {@link GenericStack} 的 long 数量表达,不做物化。 + */ + private static void collectProduced(List target, ItemStack stack, long multiplier) { + if (stack == null || stack.isEmpty() || stack.getCount() <= 0) { + return; + } + AEItemKey key = AEItemKey.of(stack); + if (key == null) { + return; + } + long amount; + try { + amount = Math.multiplyExact((long) stack.getCount(), Math.max(1L, multiplier)); + } catch (ArithmeticException exception) { + // 倍率在包装阶段已由 maximumSafeMultiplier 校验,此处仅作保护:宁可少记也不写入错误账目 + return; + } + mergeProduced(target, key, amount); + } + + private static void mergeProduced(List target, AEKey key, long amount) { + for (int index = 0; index < target.size(); index++) { + GenericStack existing = target.get(index); + if (existing.what().equals(key)) { + long merged = existing.amount() > Long.MAX_VALUE - amount + ? Long.MAX_VALUE : existing.amount() + amount; + target.set(index, new GenericStack(key, merged)); + return; + } + } + target.add(new GenericStack(key, amount)); + } + + public boolean tickAETasks() { + boolean progressed = false; + CraftingTask mergeTarget; + synchronized (this.aePendingBatches) { + var it = this.aePendingBatches.entrySet().iterator(); + while (it.hasNext()) { + var entry = it.next(); + PendingAEBatch pending = entry.getValue(); + mergeTarget = this.findExistingTask(PatternExecutionKey.of( + pending.pattern, pending.getComponentInputKeys())); + if (mergeTarget != null) { + List inputs = pending.drain(); + if (inputs.isEmpty()) { + it.remove(); + continue; + } + if (mergeTarget.addMergedBatch(inputs, pending.operationsPerPush)) { + it.remove(); + } else { + pending.statusKey = "gui.useless_mod.advanced_alloy_furnace.ae_task_status.queued"; + pending.statusDetail = ""; + pending.allInputs.addAll(inputs); + } + } + } + } + + Iterator> iterator = this.activeTasks.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + CraftingTask task = entry.getValue(); + task.tick(); + progressed |= task.progressedLastTick(); + if (task.isProcessingComplete()) { + iterator.remove(); + this.removeFromPatternIndex(task); + this.activeAETaskCount.decrementAndGet(); + this.owner.markChanged(); + } + } + + this.rebalanceTasks(); + return progressed; + } + + public boolean hasWork() { + if (!this.activeTasks.isEmpty() || this.deferredTasksTag != null + || !this.unreturnedInputs.isEmpty() || !this.unreturnedOutputs.isEmpty() + || !this.queuedCraftingOutputs.isEmpty()) { + return true; + } + synchronized (this.aePendingBatches) { + return !this.aePendingBatches.isEmpty(); + } + } + + /** + * 空闲线程再分配:当活跃任务数未达上限时,把已有任务队列尾部的子任务拆出, + * 放到新的空闲线程并行运行。 + */ + private void rebalanceTasks() { + int maxTasks = this.owner.getMaxAETaskCount(); + while (this.activeAETaskCount.get() < maxTasks) { + CraftingTask donor = this.findSplittableTask(); + if (donor == null) { + break; + } + CraftingTask split = donor.splitLastSubTask(this.nextTaskId++); + if (split == null) { + break; + } + this.registerActiveTask(split); + } + } + + private CraftingTask findSplittableTask() { + for (CraftingTask task : this.activeTasks.values()) { + if (task.hasQueuedSubTasks()) { + return task; + } + } + return null; + } + + private void registerActiveTask(CraftingTask task) { + this.activeTasks.put(task.getTaskId(), task); + this.activeTasksByPattern.computeIfAbsent( + PatternExecutionKey.of(task.getPattern(), task.getComponentInputKeys()), + k -> new ArrayList<>()).add(task); + this.activeAETaskCount.incrementAndGet(); + this.owner.markChanged(); + } + + private void removeFromPatternIndex(CraftingTask task) { + PatternExecutionKey key = PatternExecutionKey.of( + task.getPattern(), task.getComponentInputKeys()); + List list = this.activeTasksByPattern.get(key); + if (list != null) { + list.remove(task); + if (list.isEmpty()) { + this.activeTasksByPattern.remove(key); + } + } + } + + public void flushAEBatches() { + flushAEBatches(this::sendAETaskProgressToClients); + } + + void flushAEBatches(Runnable syncProgress) { + List ripe; + // 一个活跃任务都没有时,成熟窗口没有可合并的对象,只是纯粹的等待开销,直接放行。 + // 后续推送仍会由 tickAETasks 合并进刚启动的任务,批量语义不变。 + boolean idle = this.activeTasks.isEmpty(); + synchronized (this.aePendingBatches) { + var it = this.aePendingBatches.entrySet().iterator(); + ripe = new ArrayList<>(); + while (it.hasNext()) { + var entry = it.next(); + PendingAEBatch batch = entry.getValue(); + if (!idle && --batch.ripeTimer > 0) { + continue; + } + ripe.add(batch); + it.remove(); + } + } + + boolean requeued = false; + for (PendingAEBatch batch : ripe) { + if (!this.flushBatch(batch)) { + this.requeueBatch(batch); + requeued = true; + } + } + // A retry is removed from aePendingBatches while it is evaluated. Sync only + // after it has been reinserted, otherwise clients briefly receive an empty list. + if (requeued) { + syncProgress.run(); + } + } + + public void updatePatterns() { + this.patternRefreshPending = true; + } + + /** Rebuilds and publishes the provider at most once during a server tick. */ + public void tickPatternRefresh() { + if (!this.patternRefreshPending) { + return; + } + Level level = this.owner.getLevel(); + if (level == null || level.isClientSide) { + return; + } + if (!isPublishReady()) { + // AE2 snapshots what a provider can craft, so publishing before the node joined a grid + // would index this machine as unable to craft anything. Keep the request pending. + return; + } + if (!AlloyFurnaceRecipeCatalog.isReady(level)) { + // 目录仍在后台构建(服务器启动阶段实测耗时 30 秒以上)。此时解码无法取得配方, + // 会将完好的样板误判为「missing or has changed」并直接丢弃——实测启动阶段有 6 条 + // 样板因此失效。应保持请求挂起,待目录就绪后的 tick 再重建。 + return; + } + rebuildPatterns(); + try { + this.owner.onPatternsRebuilt(); + } catch (RuntimeException exception) { + RateLimitedLogger.warn(LOGGER, "Failed to publish alloy furnace patterns at {}", this.owner.getBlockPos(), exception); + return; + } + this.patternRefreshPending = false; + } + + /** True when the owner's node is on a grid, i.e. when AE2 can actually index its patterns. */ + private boolean isPublishReady() { + IManagedGridNode node = this.owner.getMainNode(); + return node != null && node.isActive() && node.getNode() != null + && node.getNode().getGrid() != null; + } + + /** Rebuilds the provider snapshot without touching AE's live grid index. */ + public void rebuildPatterns() { + Level level = this.owner.getLevel(); + // AE providers and the dynamic recipe catalog are server-authoritative. Client block + // entity synchronization may invoke inventory callbacks repeatedly while a GUI is open. + if (level == null || level.isClientSide) { + return; + } + this.patterns.clear(); + if (!this.owner.canPublishPatterns()) { + OmniversalPatternWarnings.notPublished("alloy furnace " + this.owner.getBlockPos(), "", + "this machine cannot publish patterns yet"); + return; + } + + int seen = 0; + int decoded = 0; + int slot = -1; + for (ItemStack stack : this.owner.getPatternStacks()) { + slot++; + if (!stack.isEmpty()) { + seen++; + try { + IPatternDetails pattern = AdvancedAlloyFurnacePatternResolver.decode(stack, level); + if (pattern != null && this.owner.acceptsPattern(pattern)) { + this.patterns.add(pattern); + decoded++; + } else { + OmniversalPatternWarnings.notPublished("alloy furnace " + this.owner.getBlockPos(), + slot, pattern == null + ? "the pattern could not be decoded" + : "the pattern is not accepted by this machine"); + } + } catch (RuntimeException exception) { + // A malformed pattern must not prevent the remaining + // slots from being published to AE2. + RateLimitedLogger.warn(LOGGER, "Failed to decode alloy furnace pattern at {} (item={})", + this.owner.getBlockPos(), stack.getItem(), exception); + } + } + } + + } + + public List getUnreturnedInputsSnapshot() { + return List.copyOf(this.unreturnedInputs); + } + + public List getUnreturnedOutputsSnapshot() { + return List.copyOf(this.unreturnedOutputs); + } + + public void addUnreturnedInputs(List stacks) { + if (stacks == null) return; + if (stacks.isEmpty()) return; + mergeUnreturnedAmounts(this.unreturnedInputs, stacks); + this.owner.markChanged(); + } + + public void addUnreturnedOutputs(List stacks) { + if (stacks == null) return; + if (stacks.isEmpty()) return; + mergeUnreturnedAmounts(this.unreturnedOutputs, stacks); + this.owner.markChanged(); + } + + private static void addUnreturnedAmount(List target, AEKey key, long amount) { + CraftingAeAmountAccumulator accumulator = CraftingAeAmountAccumulator.fromGenericStacks(target); + accumulator.add(key, amount); + target.clear(); + target.addAll(accumulator.segments()); + } + + private static void mergeUnreturnedAmounts( + List target, + List additions) { + CraftingAeAmountAccumulator accumulator = CraftingAeAmountAccumulator.fromGenericStacks(target); + for (GenericStack addition : additions) { + if (addition != null && addition.what() != null && addition.amount() > 0L) { + accumulator.add(addition); + } + } + target.clear(); + target.addAll(accumulator.segments()); + } + + public int getActiveAETaskCount() { + return this.activeAETaskCount.get(); + } + + public void setActiveAETaskCount(int value) { + this.activeAETaskCount.set(value); + } + + public int getTotalAEProgress() { + return this.totalAEProgress.get(); + } + + public void setTotalAEProgress(int value) { + this.totalAEProgress.set(value); + } + + public int getTotalAEMaxProgress() { + return this.totalAEMaxProgress.get(); + } + + public void setTotalAEMaxProgress(int value) { + this.totalAEMaxProgress.set(value); + } + + public Collection getAETaskProgressList() { + Level level = this.owner.getLevel(); + if (level != null && level.isClientSide) { + synchronized (this.clientTaskProgressList) { + return new ArrayList<>(this.clientTaskProgressList); + } + } + List result = new ArrayList<>(this.aeTaskProgressMap.values()); + synchronized (this.aePendingBatches) { + for (PendingAEBatch batch : this.aePendingBatches.values()) { + result.add(this.createPendingProgress(batch)); + } + } + return result; + } + + public ConcurrentHashMap getAETaskProgressMap() { + return this.aeTaskProgressMap; + } + + public AtomicInteger getTotalAEMaxProgressAtomic() { + return this.totalAEMaxProgress; + } + + public AtomicInteger getTotalAEProgressAtomic() { + return this.totalAEProgress; + } + + public ReentrantLock getCraftingLock() { + return this.craftingLock; + } + + private PendingAEBatch findOrCreateBatch( + IPatternDetails patternDetails, long operationsPerPush, + @Nullable KeyCounter[] inputHolder) { + PendingPatternExecutionKey key = PendingPatternExecutionKey.of( + patternDetails, operationsPerPush, + AdvancedAlloyFurnacePatternPolicy.componentInputKeys(patternDetails, inputHolder)); + PendingAEBatch existing = this.aePendingBatches.get(key); + if (existing != null) { + return existing; + } + PendingAEBatch batch = new PendingAEBatch(patternDetails, operationsPerPush); + this.aePendingBatches.put(key, batch); + return batch; + } + + private void requeueBatch(PendingAEBatch batch) { + synchronized (this.aePendingBatches) { + KeyCounter[] firstInput = batch.allInputs.isEmpty() ? null : batch.allInputs.get(0); + PendingAEBatch target = this.findOrCreateBatch( + batch.pattern, batch.operationsPerPush, firstInput); + target.allInputs.addAll(batch.allInputs); + target.statusKey = batch.statusKey; + target.statusDetail = batch.statusDetail; + target.ripeTimer = batchRipeTicks(); + } + } + + private boolean flushBatch(PendingAEBatch batch) { + List allInputs = batch.drain(); + if (allInputs.isEmpty() || batch.pattern == null) return true; + + if (this.activeAETaskCount.get() >= this.owner.getMaxAETaskCount()) { + batch.allInputs.addAll(allInputs); + batch.statusKey = "gui.useless_mod.advanced_alloy_furnace.ae_task_status.queued"; + batch.statusDetail = ""; + return false; + } + + long maximumTaskCrafts = SmartDoublingPatterns.maximumSafeMultiplier(batch.pattern); + List> taskBatches = splitInputBatches( + allInputs, batch.operationsPerPush, maximumTaskCrafts); + List firstBatch = taskBatches.get(0); + KeyCounter[] merged = this.mergeKeyCounters(firstBatch); + + int taskId = this.nextTaskId++; + IPatternDetails taskPattern = batch.pattern; + long totalCrafts = multiplyExactPositive(firstBatch.size(), batch.operationsPerPush); + + CraftingTask task = new CraftingTask(taskId, taskPattern, merged, totalCrafts, this.owner); + if (!task.canStartNow()) { + batch.allInputs.addAll(allInputs); + batch.statusKey = task.getWaitingStatusKey(); + batch.statusDetail = task.getWaitingDetail(); + return false; + } + + List queuedInputs = new ArrayList<>(); + for (int index = 1; index < taskBatches.size(); index++) { + queuedInputs.addAll(taskBatches.get(index)); + } + if (!queuedInputs.isEmpty() + && !task.addMergedBatch(queuedInputs, batch.operationsPerPush)) { + batch.allInputs.addAll(allInputs); + batch.statusKey = task.getWaitingStatusKey(); + batch.statusDetail = task.getWaitingDetail(); + return false; + } + + this.registerActiveTask(task); + return true; + } + + /** + * Splits pushed AE inputs before either total operations or any individual key amount would + * exceed the long range. Individual pushes are left intact when they already use a full long. + */ + static List> splitInputBatches( + List allInputs, long operationsPerPush, long maximumTaskCrafts) { + if (allInputs == null || allInputs.isEmpty()) { + return List.of(); + } + + long safeOperationsPerPush = Math.max(1L, operationsPerPush); + long safeMaximumTaskCrafts = Math.max(1L, maximumTaskCrafts); + List> result = new ArrayList<>(); + List current = new ArrayList<>(); + Object2LongOpenHashMap currentAmounts = new Object2LongOpenHashMap<>(); + long currentCrafts = 0L; + boolean currentRequiresIsolation = false; + + for (KeyCounter[] input : allInputs) { + Object2LongOpenHashMap inputAmounts = collectExactKeyAmounts(input); + boolean fitsCurrentBatch = !current.isEmpty() + && !currentRequiresIsolation + && safeOperationsPerPush <= safeMaximumTaskCrafts - currentCrafts + && inputAmounts != null + && canAddExact(currentAmounts, inputAmounts); + if (!current.isEmpty() && !fitsCurrentBatch) { + result.add(current); + current = new ArrayList<>(); + currentAmounts = new Object2LongOpenHashMap<>(); + currentCrafts = 0L; + currentRequiresIsolation = false; + } + + current.add(input); + if (inputAmounts == null) { + // A single push can contain repeated full-range keys in distinct holders. Keep + // it as-is, but never combine it with another push and lose the extra amount. + currentRequiresIsolation = true; + } else { + addExact(currentAmounts, inputAmounts); + } + currentCrafts += safeOperationsPerPush; + } + + if (!current.isEmpty()) { + result.add(current); + } + return result; + } + + private KeyCounter[] mergeKeyCounters(List allInputs) { + if (allInputs.isEmpty()) return new KeyCounter[0]; + if (allInputs.size() == 1) return allInputs.get(0); + + Object2LongOpenHashMap merged = new Object2LongOpenHashMap<>(); + for (KeyCounter[] counters : allInputs) { + if (counters == null) continue; + for (KeyCounter counter : counters) { + if (counter == null) continue; + for (var entry : counter) { + long amount = entry.getLongValue(); + if (amount <= 0L) continue; + long current = merged.getLong(entry.getKey()); + if (current > Long.MAX_VALUE - amount) { + throw new IllegalStateException("Attempted to merge AE inputs beyond long range"); + } + merged.put(entry.getKey(), current + amount); + } + } + } + + KeyCounter result = new KeyCounter(); + for (var entry : merged.object2LongEntrySet()) { + result.add(entry.getKey(), entry.getLongValue()); + } + return new KeyCounter[]{result}; + } + + @Nullable + private static Object2LongOpenHashMap collectExactKeyAmounts(KeyCounter[] counters) { + Object2LongOpenHashMap result = new Object2LongOpenHashMap<>(); + if (counters == null) return result; + for (KeyCounter counter : counters) { + if (counter == null) continue; + for (var entry : counter) { + long amount = entry.getLongValue(); + if (amount <= 0L) continue; + long current = result.getLong(entry.getKey()); + if (current > Long.MAX_VALUE - amount) { + return null; + } + result.put(entry.getKey(), current + amount); + } + } + return result; + } + + private static boolean canAddExact(Object2LongMap target, Object2LongMap additions) { + for (var entry : additions.object2LongEntrySet()) { + if (target.getLong(entry.getKey()) > Long.MAX_VALUE - entry.getLongValue()) { + return false; + } + } + return true; + } + + private static void addExact(Object2LongMap target, Object2LongMap additions) { + for (var entry : additions.object2LongEntrySet()) { + target.put(entry.getKey(), target.getLong(entry.getKey()) + entry.getLongValue()); + } + } + + private CraftingTask findExistingTask(PatternExecutionKey patternKey) { + List list = this.activeTasksByPattern.get(patternKey); + if (list == null) { + return null; + } + for (CraftingTask task : list) { + if (!task.isProcessingComplete()) { + return task; + } + } + return null; + } + + private AETaskProgress createPendingProgress(PendingAEBatch batch) { + long craftCount = calculateTotalCrafts(batch.allInputs.size(), batch.operationsPerPush); + long outputAmount = 1L; + if (batch.pattern != null && (batch.pattern.getOutputs().length != 0)) { + outputAmount = batch.pattern.getOutputs()[0].amount(); + } + long totalOutputCount = saturatingMultiply(outputAmount, craftCount); + return new AETaskProgress(this.getPatternProductName(batch.pattern), 0, 1, craftCount, totalOutputCount, + batch.statusKey, batch.statusDetail); + } + + private String getPatternProductName(IPatternDetails pattern) { + if (pattern == null || (pattern.getOutputs().length == 0)) { + return "Unknown"; + } + return pattern.getOutputs()[0].what().getDisplayName().getString(); + } + + // AE任务进度信息类 + public static class AETaskProgress { + private final String productName; + private final long outputCount; // 单次产出数量 + private volatile int progress; + private volatile int maxProgress; + private volatile long craftCount; + private volatile long totalOutputCount; // 最终产物总数 = 合成次数 × 单次产出数量 + private volatile String statusKey; + private volatile String statusDetail; + + public AETaskProgress(String productName, int maxProgress, long craftCount, long totalOutputCount) { + this(productName, 0, maxProgress, craftCount, totalOutputCount, + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.processing", ""); + } + + public AETaskProgress(String productName, int progress, int maxProgress, long craftCount, long totalOutputCount) { + this(productName, progress, maxProgress, craftCount, totalOutputCount, + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.processing", ""); + } + + public AETaskProgress(String productName, int progress, int maxProgress, long craftCount, long totalOutputCount, String statusKey, String statusDetail) { + this.productName = productName; + this.progress = progress; + this.maxProgress = maxProgress; + this.craftCount = craftCount; + this.totalOutputCount = totalOutputCount; + this.outputCount = craftCount > 0 ? totalOutputCount / craftCount : 1; // 计算单次产出数量 + this.statusKey = statusKey; + this.statusDetail = statusDetail; + } + + public String getProductName() {return productName;} + + public int getProgress() {return progress;} + + public void setProgress(int progress) {this.progress = progress;} + + public int getMaxProgress() {return maxProgress;} + + public void setMaxProgress(int maxProgress) {this.maxProgress = maxProgress;} + + public long getCraftCount() {return craftCount;} + + public long getTotalOutputCount() {return totalOutputCount;} + + public String getStatusKey() {return statusKey;} + + public String getStatusDetail() {return statusDetail;} + + public void setStatus(String statusKey, String statusDetail) { + this.statusKey = statusKey; + this.statusDetail = statusDetail; + } + + // 更新合成次数和最终产物总数(用于任务合并)。 + public void updateCraftCount(long newCraftCount) { + this.craftCount = newCraftCount; + this.totalOutputCount = saturatingMultiply(newCraftCount, outputCount); + } + } + + /** + * 一次自执行(合成样板装配 / 万象样板折叠)的产出,等待在安全时机注入 AE 网络。 + * + *

BigInteger 优先:产物以按键 {@link CraftingAeAmountAccumulator 余额账本} 保存, + * 不预先物化 long 分段。旧实现把产物按 {@code Long.MAX_VALUE} 切成 {@code List} + * 再逐段插入,最坏情况(队列深度 = 线程数、每条约千段)会达到每 tick 千万次插入调用; + * 现在只在真正插入的那一刻才切段,且同一个键在整条队列里只投递一次。

+ * + *

大数批次额外带两样东西:{@code bigIntegerOutputs} 是本批产出的 BigInteger 精确值 + * (回调专用 —— long 分段相加会溢出,且它与投递结果无关,commit 时就算好了); + * {@code cpuContext} + {@code cpuAdapterId} 用来在产物全部回网后驱动 + * {@code onBatchOutputs} / {@code onBatchFinished}。

+ * + *

CPU 回执绑定不随 NBT 持久化:产物队列本身正常存盘(物品不会丢失),但重载后不再补发 + * 回调。此为刻意设计 —— 回调仅是本会话内的便利信号,产物最终仍经 ME 网络正常入账, + * 调用方以网络实际入账为准。

+ */ + static final class PendingCraftingOutput { + final long queuedTick; + /** 剩余待回网的按键 BigInteger 余额。回网刷新时原地扣减,扣空即条目完成。 */ + final CraftingAeAmountAccumulator ledger; + /** 本批的实际产出(BigInteger 精确值);非大数批次为 {@code null}。 */ + @Nullable + final List bigIntegerOutputs; + /** CPU 回执上下文;没有绑定时为 {@code null}。 */ + @Nullable + final AlloyFurnaceBigIntegerBatchContext cpuContext; + /** 已注册的适配器 id,与 {@link #cpuContext} 同时存在。 */ + @Nullable + final ResourceLocation cpuAdapterId; + /** + * 是否已经给 CPU 侧发过终局通知。 + * + *

必须有这个位:取消流程会先发 {@code CANCELLED},紧接着 + * {@link #flushQueuedCraftingOutputs(boolean)} 以 {@code force=true} 强制回网, + * 若这一条刚好被刷空就会再发一次 {@code SUCCESS} —— 同一个批次出现两个互相矛盾的终局。 + * 一个批次只能有一个终局。

+ */ + boolean cpuNotified; + + PendingCraftingOutput(long queuedTick, CraftingAeAmountAccumulator ledger) { + this(queuedTick, ledger, null, null, null); + } + + PendingCraftingOutput(long queuedTick, CraftingAeAmountAccumulator ledger, + @Nullable List bigIntegerOutputs) { + this(queuedTick, ledger, bigIntegerOutputs, null, null); + } + + PendingCraftingOutput(long queuedTick, CraftingAeAmountAccumulator ledger, + @Nullable List bigIntegerOutputs, + @Nullable AlloyFurnaceBigIntegerBatchContext cpuContext, + @Nullable ResourceLocation cpuAdapterId) { + this.queuedTick = queuedTick; + // 拷贝隔离:调用方的 produced 账本随后即被丢弃,但不能与队列条目共享可变状态。 + this.ledger = ledger.copy(); + this.bigIntegerOutputs = bigIntegerOutputs; + this.cpuContext = cpuContext; + this.cpuAdapterId = cpuAdapterId; + } + + /** 本条目是否已全部回网(账本扣空)。 */ + boolean isEmpty() { + return this.ledger.isEmpty(); + } + } + + static final class PendingAEBatch { + final IPatternDetails pattern; + final long operationsPerPush; + final List allInputs = new ArrayList<>(); + int ripeTimer = batchRipeTicks(); + String statusKey = "gui.useless_mod.advanced_alloy_furnace.ae_task_status.queued"; + String statusDetail = ""; + + PendingAEBatch(IPatternDetails pattern, long operationsPerPush) { + this.pattern = pattern; + this.operationsPerPush = Math.max(1L, operationsPerPush); + } + + void add(KeyCounter[] input) { + // 不重置 ripeTimer:成熟计时从批次创建(或重排)起算。 + // 若每次推送都重置,AE CPU 对超大请求持续推送时批次永远不会成熟, + // 任务迟迟不启动,表现为“一直不合成”。 + this.allInputs.add(input); + } + + List drain() { + List result = new ArrayList<>(this.allInputs); + this.allInputs.clear(); + return result; + } + + CompoundTag save(HolderLookup.Provider registries) { + if (this.pattern == null) { + return null; + } + CompoundTag tag = new CompoundTag(); + tag.put("Pattern", this.pattern.getDefinition().toTag()); + tag.putLong("OperationsPerPush", this.operationsPerPush); + ListTag craftsTag = new ListTag(); + for (KeyCounter[] counters : this.allInputs) { + craftsTag.add(writeKeyCounters(registries, counters)); + } + tag.put("Crafts", craftsTag); + return tag; + } + + @Nullable + static PendingAEBatch load(CompoundTag tag, Level level, HolderLookup.Provider registries) { + appeng.api.stacks.AEItemKey definition = appeng.api.stacks.AEItemKey.fromTag(tag.getCompound("Pattern")); + if (definition == null) { + return null; + } + IPatternDetails pattern = AdvancedAlloyFurnacePatternResolver.decode(definition.toStack(), level); + if (pattern == null) { + return null; + } + long operationsPerPush = tag.contains("OperationsPerPush", Tag.TAG_ANY_NUMERIC) + ? Math.max(1L, tag.getLong("OperationsPerPush")) + : 1L; + PendingAEBatch batch = new PendingAEBatch(pattern, operationsPerPush); + ListTag craftsTag = tag.getList("Crafts", Tag.TAG_COMPOUND); + for (int i = 0; i < craftsTag.size(); i++) { + batch.allInputs.add(readKeyCounters(registries, craftsTag.getCompound(i))); + } + return batch; + } + + Set getComponentInputKeys() { + if (allInputs.isEmpty()) { + return Set.of(); + } + return AdvancedAlloyFurnacePatternPolicy.componentInputKeys(pattern, allInputs.get(0)); + } + } + + private static CompoundTag writeKeyCounters(HolderLookup.Provider registries, KeyCounter[] counters) { + ListTag list = new ListTag(); + for (GenericStack amount : CraftingAeAmountAccumulator.fromCounters(counters).segments()) { + list.add(GenericStack.writeTag(amount)); + } + CompoundTag tag = new CompoundTag(); + tag.put("Stacks", list); + return tag; + } + + private static KeyCounter[] readKeyCounters(HolderLookup.Provider registries, CompoundTag tag) { + ListTag list = tag.getList("Stacks", Tag.TAG_COMPOUND); + CraftingAeAmountAccumulator accumulator = new CraftingAeAmountAccumulator(); + for (int i = 0; i < list.size(); i++) { + GenericStack gs = GenericStack.readTag(list.getCompound(i)); + if (gs != null && gs.amount() > 0L) { + accumulator.add(gs); + } + } + List amounts = accumulator.segments(); + if (amounts.isEmpty()) { + return new KeyCounter[]{new KeyCounter()}; + } + KeyCounter[] counters = new KeyCounter[amounts.size()]; + for (int index = 0; index < amounts.size(); index++) { + GenericStack amount = amounts.get(index); + KeyCounter counter = new KeyCounter(); + counter.add(amount.what(), amount.amount()); + counters[index] = counter; + } + return counters; + } + + private record PatternKey(@Nullable AEItemKey definition) { + static PatternKey of(IPatternDetails pattern) { + if (pattern == null) return new PatternKey(null); + return new PatternKey(SmartDoublingPatterns.unwrap(pattern).getDefinition()); + } + } + + static long calculateTotalCrafts(long batchSize, long multiplier) { + return saturatingMultiply(Math.max(0L, batchSize), Math.max(1L, multiplier)); + } + + private static long multiplyExactPositive(long amount, long multiplier) { + long safeAmount = Math.max(1L, amount); + long safeMultiplier = Math.max(1L, multiplier); + if (safeAmount > Long.MAX_VALUE / safeMultiplier) { + throw new IllegalStateException("Batch partition failed to keep operations within long range"); + } + return safeAmount * safeMultiplier; + } + + private static long saturatingMultiply(long amount, long multiplier) { + if (amount <= 0L || multiplier <= 0L) { + return 0L; + } + return amount > Long.MAX_VALUE / multiplier ? Long.MAX_VALUE : amount * multiplier; + } + + private static long saturatingAdd(long left, long right) { + if (right > 0L && left > Long.MAX_VALUE - right) { + return Long.MAX_VALUE; + } + return left + right; + } + + private record PatternExecutionKey(PatternKey pattern, Set componentInputKeys) { + private PatternExecutionKey { + componentInputKeys = componentInputKeys == null ? Set.of() : Set.copyOf(componentInputKeys); + } + + static PatternExecutionKey of(IPatternDetails pattern, Set componentInputKeys) { + return new PatternExecutionKey(PatternKey.of(pattern), componentInputKeys); + } + } + + /** Pending batches keep their per-push multiplier separate from active-task identity. */ + private record PendingPatternExecutionKey( + PatternKey pattern, long operationsPerPush, Set componentInputKeys) { + private PendingPatternExecutionKey { + operationsPerPush = Math.max(1L, operationsPerPush); + componentInputKeys = componentInputKeys == null ? Set.of() : Set.copyOf(componentInputKeys); + } + + static PendingPatternExecutionKey of( + IPatternDetails pattern, long operationsPerPush, Set componentInputKeys) { + return new PendingPatternExecutionKey( + PatternKey.of(pattern), operationsPerPush, componentInputKeys); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/AdvancedAlloyFurnacePatternPolicy.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/AdvancedAlloyFurnacePatternPolicy.java new file mode 100644 index 00000000..22812468 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/AdvancedAlloyFurnacePatternPolicy.java @@ -0,0 +1,160 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import appeng.api.stacks.KeyCounter; +import com.sorrowmist.useless.content.recipe.RecipeOutputConstraint; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** Reads the local slot-level component matching policy. */ +final class AdvancedAlloyFurnacePatternPolicy { + private AdvancedAlloyFurnacePatternPolicy() { + } + + static List outputConstraints(@Nullable IPatternDetails pattern) { + if (pattern == null) { + return List.of(); + } + + IPatternDetails original = SmartDoublingPatterns.unwrap(pattern); + DynamicComponentPattern dynamic = dynamicDetails(original); + List outputs = java.util.Arrays.asList(original.getOutputs()); + List result = new ArrayList<>(outputs.size()); + for (int slot = 0; slot < outputs.size(); slot++) { + GenericStack output = outputs.get(slot); + if (output == null || output.what() == null) { + continue; + } + boolean idOnly = dynamic != null + && dynamic.isItemIdOutput(slot) + && output.what() instanceof AEItemKey; + result.add(idOnly + ? RecipeOutputConstraint.itemId(output.what()) + : RecipeOutputConstraint.exact(output.what())); + } + return List.copyOf(result); + } + + static boolean usesRecipeOutputs(@Nullable IPatternDetails pattern) { + IPatternDetails original = SmartDoublingPatterns.unwrap(pattern); + if (original instanceof OmniversalPatternDetails) { + return true; + } + + DynamicComponentPattern dynamic = dynamicDetails(original); + if (dynamic == null) { + return false; + } + return dynamic.usesDynamicOutputs(); + } + + static Set componentInputKeys( + @Nullable IPatternDetails pattern, @Nullable KeyCounter[] inputHolder) { + IPatternDetails original = SmartDoublingPatterns.unwrap(pattern); + DynamicComponentPattern dynamic = dynamicDetails(original); + if (dynamic == null || inputHolder == null) { + return Set.of(); + } + + Set result = new LinkedHashSet<>(); + IPatternDetails.IInput[] inputs = original.getInputs(); + if (inputHolder.length == inputs.length) { + for (int slot = 0; slot < inputs.length; slot++) { + if (!dynamic.isItemIdInput(slot) || inputHolder[slot] == null) { + continue; + } + for (var entry : inputHolder[slot]) { + if (entry.getLongValue() > 0) { + result.add(entry.getKey()); + } + } + } + return Set.copyOf(result); + } + + Set relaxedItems = relaxedItems(original, dynamic); + for (KeyCounter counter : inputHolder) { + if (counter == null) continue; + for (var entry : counter) { + if (entry.getLongValue() > 0 + && entry.getKey() instanceof AEItemKey itemKey + && relaxedItems.contains(itemKey.getItem())) { + result.add(itemKey); + } + } + } + return Set.copyOf(result); + } + + static Set componentInputKeys( + @Nullable IPatternDetails pattern, List actualInputs) { + return componentInputKeys(pattern, actualInputs, List.of()); + } + + static Set componentInputKeys( + @Nullable IPatternDetails pattern, + List actualInputs, + List actualKeyInputs) { + IPatternDetails original = SmartDoublingPatterns.unwrap(pattern); + DynamicComponentPattern dynamic = dynamicDetails(original); + if (dynamic == null || (actualInputs.isEmpty() && actualKeyInputs.isEmpty())) { + return Set.of(); + } + + Set relaxedItems = relaxedItems(original, dynamic); + + Set result = new LinkedHashSet<>(); + for (ItemStack stack : actualInputs) { + if (stack.isEmpty() || !relaxedItems.contains(stack.getItem())) { + continue; + } + AEItemKey key = AEItemKey.of(stack); + if (key != null) { + result.add(key); + } + } + for (GenericStack stack : actualKeyInputs) { + if (stack.what() instanceof AEItemKey itemKey + && relaxedItems.contains(itemKey.getItem())) { + result.add(itemKey); + } + } + return Set.copyOf(result); + } + + static Set componentInputKeysFromGenericStacks( + @Nullable IPatternDetails pattern, List actualInputs) { + return componentInputKeys(pattern, List.of(), actualInputs); + } + + private static Set relaxedItems( + IPatternDetails pattern, DynamicComponentPattern dynamic) { + Set result = new LinkedHashSet<>(); + IPatternDetails.IInput[] inputs = pattern.getInputs(); + for (int slot = 0; slot < inputs.length; slot++) { + if (!dynamic.isItemIdInput(slot)) { + continue; + } + for (GenericStack possible : inputs[slot].getPossibleInputs()) { + if (possible != null && possible.what() instanceof AEItemKey itemKey) { + result.add(itemKey.getItem()); + } + } + } + return result; + } + + @Nullable + private static DynamicComponentPattern dynamicDetails(@Nullable IPatternDetails pattern) { + return pattern instanceof DynamicComponentPattern dynamic ? dynamic : null; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/AdvancedAlloyFurnacePatternResolver.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/AdvancedAlloyFurnacePatternResolver.java new file mode 100644 index 00000000..95714491 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/AdvancedAlloyFurnacePatternResolver.java @@ -0,0 +1,401 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.crafting.PatternDetailsHelper; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import appeng.crafting.pattern.AEProcessingPattern; +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.content.recipe.adapters.draconicevolution.DraconicFusionRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.enderio.SoulBindingRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.core.component.OmniversalPatternData; +import com.sorrowmist.useless.core.component.UComponents; +import com.sorrowmist.useless.init.ModItems; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraftforge.fml.ModList; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.lang.reflect.Method; + +/** Builds provider-local component-aware views of otherwise unchanged AE patterns. */ +public final class AdvancedAlloyFurnacePatternResolver { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final String DRACONIC_EVOLUTION_MOD_ID = "draconicevolution"; + private static final String ENDER_IO_MOD_ID = "enderio"; + private static final String OCCULTISM_MOD_ID = "occultism"; + private static final String MALUM_MOD_ID = "malum"; + private static final String NEOVITAE_MOD_ID = "neovitae"; + private static final String NEOVITAE_DYNAMIC_SUPPORT = + "com.sorrowmist.useless.compat.neovitae.NeoVitaeDynamicPatternSupport"; + + private AdvancedAlloyFurnacePatternResolver() { + } + + @Nullable + public static IPatternDetails decode(ItemStack stack, Level level) { + if (stack == null || stack.isEmpty() || level == null) { + return null; + } + + AEItemKey definition = AEItemKey.of(stack); + if (definition != null && SmartDoublingPatterns.definitionOperations(definition) != null) { + return SmartDoublingPatterns.restore(definition, level); + } + + /* + * AE2's generic decoder deliberately catches every exception thrown by + * an EncodedPatternItem and returns null. That is useful for ordinary + * invalid patterns, but it makes a recipe-bound omniversal pattern look + * exactly like an empty slot. Decode our item explicitly so the + * binding can be validated and failures can be diagnosed, while still + * leaving all other AE2 pattern types on the normal path. + */ + if (stack.is(ModItems.OMNIVERSAL_PATTERN.get())) { + OmniversalPatternData data = definition == null + ? null + : com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(definition, UComponents.OMNIVERSAL_PATTERN_DATA.get()); + try { + return OmniversalPatternDetails.decode(definition, level); + } catch (RuntimeException exception) { + RateLimitedLogger.warn(LOGGER, "Ignoring invalid omniversal pattern (recipe={}, fingerprint={})", + data == null ? "" : data.recipeId(), + data == null ? "" : data.recipeFingerprint(), + exception); + return null; + } + } + + IPatternDetails decoded = PatternDetailsHelper.decodePattern(stack, level); + return decoded == null ? null : resolve(decoded, level); + } + + public static IPatternDetails resolve(IPatternDetails pattern, Level level) { + return resolve(pattern, level, null); + } + + public static IPatternDetails resolve( + IPatternDetails pattern, Level level, @Nullable String sourceId) { + if (pattern instanceof DynamicComponentPattern + || !(pattern instanceof AEProcessingPattern processingPattern)) { + return pattern; + } + + try { + String normalizedSource = sourceId == null || sourceId.isBlank() + ? null : RecipeSourceIds.normalize(sourceId); + if (RecipeSourceIds.UNKNOWN.equals(normalizedSource)) normalizedSource = null; + if ((normalizedSource == null || normalizedSource.equals(RecipeSourceIds.DRACONIC_EVOLUTION)) + && ModList.get().isLoaded(DRACONIC_EVOLUTION_MOD_ID)) { + IPatternDetails resolved = resolveDynamicDraconicPattern(processingPattern, level); + if (resolved != processingPattern) { + return resolved; + } + } + if ((normalizedSource == null || normalizedSource.equals(RecipeSourceIds.OCCULTISM)) + && ModList.get().isLoaded(OCCULTISM_MOD_ID)) { + IPatternDetails resolved = resolveDynamicOccultismPattern(processingPattern, level); + if (resolved != processingPattern) { + return resolved; + } + } + if ((normalizedSource == null || normalizedSource.equals(RecipeSourceIds.MALUM)) + && ModList.get().isLoaded(MALUM_MOD_ID)) { + IPatternDetails resolved = resolveDynamicMalumPattern(processingPattern, level); + if (resolved != processingPattern) { + return resolved; + } + } + if ((normalizedSource == null || normalizedSource.equals(RecipeSourceIds.ENDER_IO)) + && ModList.get().isLoaded(ENDER_IO_MOD_ID)) { + IPatternDetails resolved = resolveDynamicSoulBindingPattern(processingPattern, level, normalizedSource); + if (resolved != processingPattern) { + return resolved; + } + } + if ((normalizedSource == null || normalizedSource.equals(RecipeSourceIds.NEOVITAE)) + && ModList.get().isLoaded(NEOVITAE_MOD_ID)) { + IPatternDetails resolved = resolveDynamicNeoVitaePattern(processingPattern, level); + if (resolved != processingPattern) { + return resolved; + } + } + return processingPattern; + } catch (RuntimeException exception) { + RateLimitedLogger.warn(LOGGER, "Failed to create a component-aware AE view for pattern {}", + pattern.getDefinition(), exception); + return pattern; + } + } + + private static IPatternDetails resolveDynamicDraconicPattern( + AEProcessingPattern pattern, Level level) { + PatternStackView view = PatternStackView.fromPattern(pattern); + if (view == null || view.inputs().isEmpty() || view.outputs().isEmpty()) { + return pattern; + } + + var dynamicProfile = DraconicFusionRecipeAdapter.findDynamicPatternProfileLong(level, view); + if (dynamicProfile.isEmpty()) { + return pattern; + } + + var profile = dynamicProfile.get(); + AEProcessingPattern executionPattern = profile.canonicalInputs().isEmpty() + ? pattern + : withCanonicalInputs(pattern, profile.canonicalInputs()); + return new DynamicComponentPatternDetails( + executionPattern, + profile.idOnlyInputSlots(), + profile.idOnlyOutputSlots(), + level.registryAccess()); + } + + private static IPatternDetails resolveDynamicOccultismPattern( + AEProcessingPattern pattern, Level level) { + PatternStackView view = PatternStackView.fromPattern(pattern); + if (view == null || view.inputs().isEmpty() || view.outputs().isEmpty()) { + return pattern; + } + java.util.Optional profile = com.sorrowmist.useless.compat.forge1201.OptionalIntegration.call("occultism", + "com.sorrowmist.useless.content.recipe.adapters.occultism.OccultismRitualRecipeAdapter","findDynamicPatternProfileLong",java.util.Optional.class, + java.util.Optional.empty(),new Class[]{Level.class,PatternStackView.class},level,view); + if (profile.isEmpty()) { + return pattern; + } + return new DynamicComponentPatternDetails( + pattern, + optionalSlots(profile.get(),"idOnlyInputSlots"), + optionalSlots(profile.get(),"idOnlyOutputSlots"), + level.registryAccess()); + } + + private static IPatternDetails resolveDynamicMalumPattern( + AEProcessingPattern pattern, Level level) { + PatternStackView view = PatternStackView.fromPattern(pattern); + if (view == null || view.inputs().isEmpty() || view.outputs().isEmpty()) { + return pattern; + } + java.util.Optional profile = com.sorrowmist.useless.compat.forge1201.OptionalIntegration.call("malum", + "com.sorrowmist.useless.content.recipe.adapters.malum.SpiritInfusionRecipeAdapter","findDynamicPatternProfileLong",java.util.Optional.class, + java.util.Optional.empty(),new Class[]{Level.class,PatternStackView.class},level,view); + if (profile.isEmpty()) { + return pattern; + } + return new DynamicComponentPatternDetails( + pattern, + optionalSlots(profile.get(),"idOnlyInputSlots"), + optionalSlots(profile.get(),"idOnlyOutputSlots"), + level.registryAccess()); + } + + @SuppressWarnings("unchecked") + private static java.util.Set optionalSlots(Object profile,String accessor) { + try { return (java.util.Set)profile.getClass().getMethod(accessor).invoke(profile); } + catch(ReflectiveOperationException error) { throw new IllegalStateException("Invalid optional pattern profile",error); } + } + + private static IPatternDetails resolveDynamicSoulBindingPattern( + AEProcessingPattern pattern, Level level, @Nullable String sourceId) { + PatternStackView view = PatternStackView.fromPattern(pattern); + if (view == null || view.inputs().isEmpty() || view.outputs().isEmpty()) { + return pattern; + } + var profile = SoulBindingRecipeAdapter.findDynamicPatternProfileLong( + sourceId == null ? RecipeSourceIds.ENDER_IO : sourceId, level, view); + if (profile.isEmpty()) { + return pattern; + } + return new DynamicComponentPatternDetails( + pattern, + optionalSlots(profile.get(),"idOnlyInputSlots"), + optionalSlots(profile.get(),"idOnlyOutputSlots"), + profile.get().inputMatchers(), + level.registryAccess()); + } + + private static IPatternDetails resolveDynamicNeoVitaePattern( + AEProcessingPattern pattern, Level level) { + PatternStackView view = PatternStackView.fromPattern(pattern); + if (view == null || view.inputs().isEmpty() || view.outputs().isEmpty()) { + return pattern; + } + Optional profile = findNeoVitaeDynamicPatternProfile( + level, view); + if (profile.isEmpty()) return pattern; + + DynamicPatternProfile dynamic = profile.get(); + return new DynamicComponentPatternDetails( + pattern, + dynamic.idOnlyInputSlots(), + dynamic.idOnlyOutputSlots(), + dynamic.inputMatchers(), + level.registryAccess()); + } + + static Optional findNeoVitaeDynamicPatternProfile( + Level level, PatternStackView view) { + try { + Class support = Class.forName(NEOVITAE_DYNAMIC_SUPPORT); + Method method = support.getMethod( + "findDynamicPatternProfileLong", Level.class, PatternStackView.class); + Object value = method.invoke(null, level, view); + if (value instanceof Optional optional && optional.isPresent() + && optional.get() instanceof DynamicPatternProfile profile) { + return Optional.of(profile); + } + } catch (NoSuchMethodException exception) { + if (view.inputs().stream().allMatch(stack -> stack.amount() <= Integer.MAX_VALUE) + && view.outputs().stream().allMatch(stack -> stack.amount() <= Integer.MAX_VALUE)) { + try { + Class support = Class.forName(NEOVITAE_DYNAMIC_SUPPORT); + Method method = support.getMethod( + "findDynamicPatternProfile", Level.class, List.class, List.class); + Object value = method.invoke(null, level, + view.inputRepresentatives(), view.outputRepresentatives()); + if (value instanceof Optional optional && optional.isPresent() + && optional.get() instanceof DynamicPatternProfile profile) { + return Optional.of(profile); + } + } catch (ReflectiveOperationException | RuntimeException fallbackException) { + } + } + } catch (ClassNotFoundException | LinkageError ignored) { + // Neo Vitae is optional and has no support class when it is absent. + } catch (ReflectiveOperationException | RuntimeException exception) { + } + return Optional.empty(); + } + + static List itemInputs(IPatternDetails pattern) { + List result = new ArrayList<>(); + for (IPatternDetails.IInput input : pattern.getInputs()) { + AEItemKey itemKey = firstItemKey(input.getPossibleInputs()); + long amount = input.getMultiplier(); + if (itemKey == null || amount <= 0 || amount > Integer.MAX_VALUE) { + return List.of(); + } + result.add(itemKey.toStack((int) amount)); + } + return result; + } + + static List itemOutputs(IPatternDetails pattern) { + List result = new ArrayList<>(); + for (GenericStack output : pattern.getOutputs()) { + if (!(output.what() instanceof AEItemKey itemKey) + || output.amount() <= 0 || output.amount() > Integer.MAX_VALUE) { + return List.of(); + } + result.add(itemKey.toStack((int) output.amount())); + } + return result; + } + + @Nullable + private static AEItemKey firstItemKey(GenericStack[] possibleInputs) { + if (possibleInputs == null) { + return null; + } + for (GenericStack possibleInput : possibleInputs) { + if (possibleInput != null && possibleInput.what() instanceof AEItemKey itemKey) { + return itemKey; + } + } + return null; + } + + static AEProcessingPattern withCanonicalInputs( + AEProcessingPattern source, Map canonicalInputs) { + return new CanonicalInputProcessingPattern(source, canonicalInputs); + } + + private static final class CanonicalInputProcessingPattern extends AEProcessingPattern { + private final IPatternDetails.IInput[] inputs; + + private CanonicalInputProcessingPattern( + AEProcessingPattern source, Map canonicalInputs) { + super(source.getDefinition()); + IPatternDetails.IInput[] sourceInputs = source.getInputs(); + this.inputs = new IPatternDetails.IInput[sourceInputs.length]; + for (int slot = 0; slot < sourceInputs.length; slot++) { + this.inputs[slot] = sourceInputs[slot]; + } + for (Map.Entry entry : canonicalInputs.entrySet()) { + int slot = entry.getKey(); + if (slot < 0 || slot >= inputs.length) { + throw new IllegalArgumentException("Canonical input slot is outside the processing pattern: " + slot); + } + inputs[slot] = prependCanonicalInput(inputs[slot], entry.getValue()); + } + } + + @Override + public IPatternDetails.IInput[] getInputs() { + return inputs.clone(); + } + } + + static IPatternDetails.IInput prependCanonicalInput( + IPatternDetails.IInput source, ItemStack canonicalStack) { + if (source == null || canonicalStack == null || canonicalStack.isEmpty()) { + throw new IllegalArgumentException("Canonical processing input must be non-empty"); + } + + GenericStack[] original = source.getPossibleInputs(); + long candidateAmount = original.length > 0 && original[0] != null + ? original[0].amount() + : 1L; + if (candidateAmount <= 0) { + candidateAmount = 1L; + } + + AEItemKey canonicalKey = AEItemKey.of(canonicalStack.copyWithCount(1)); + if (canonicalKey == null) { + throw new IllegalArgumentException("Canonical processing input did not produce an AE item key"); + } + GenericStack canonical = new GenericStack(canonicalKey, candidateAmount); + + List possibleInputs = new ArrayList<>(original.length + 1); + possibleInputs.add(canonical); + for (GenericStack possible : original) { + if (possible != null && !possible.equals(canonical)) { + possibleInputs.add(possible); + } + } + return new CanonicalInput(source, possibleInputs.toArray(GenericStack[]::new)); + } + + private record CanonicalInput( + IPatternDetails.IInput source, GenericStack[] possibleInputs) implements IPatternDetails.IInput { + @Override + public GenericStack[] getPossibleInputs() { + return possibleInputs; + } + + @Override + public long getMultiplier() { + return source.getMultiplier(); + } + + @Override + public boolean isValid(AEKey input, Level level) { + return source.isValid(input, level); + } + + @Override + @Nullable + public AEKey getRemainingKey(AEKey template) { + return source.getRemainingKey(template); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/AlloyFurnaceAeHost.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/AlloyFurnaceAeHost.java new file mode 100644 index 00000000..c42c7faf --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/AlloyFurnaceAeHost.java @@ -0,0 +1,36 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.networking.IManagedGridNode; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Nullable; + +public interface AlloyFurnaceAeHost extends CraftingTaskContext { + @Nullable + IManagedGridNode getMainNode(); + + int getMaxAETaskCount(); + + Iterable getPatternStacks(); + + /** Called after a pending pattern snapshot has been rebuilt on the server thread. */ + default void onPatternsRebuilt() { + } + + /** + * 解析一次 ME 网络写入目标,供一次「产物回网」刷写 pass 内复用;网络不可达返回 {@code null}。 + * + *

不提供返回 null 的 default 实现:那样会将「未实现」表现为静默不回网 + * (产物始终留在队列中),因此刻意声明为抽象方法,由编译器强制两个宿主方块实体实现。

+ */ + @Nullable + CraftingAeOutputTarget resolveAeOutputTarget(); + + default boolean canPublishPatterns() { + return true; + } + + default boolean acceptsPattern(IPatternDetails pattern) { + return pattern != null; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/AlloyFurnaceBigIntegerCrafting.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/AlloyFurnaceBigIntegerCrafting.java new file mode 100644 index 00000000..12f945ba --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/AlloyFurnaceBigIntegerCrafting.java @@ -0,0 +1,483 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import appeng.api.stacks.KeyCounter; +import com.sorrowmist.useless.api.crafting.bigint.cpu.AlloyFurnaceBigIntegerCpuAdapters; +import com.sorrowmist.useless.core.config.ConfigManager; +import com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerOutput; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.catalyst.ResolvedCatalystEffect; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; + +/** + * 万象样板 BigInteger 批次的共享数学引擎。 + * + *

原先这套「窗口 / 分段 / 上限」的算术只存在于 Data Energistics 的适配器里,现在抽出来给 + * 两处共用:DE 适配器(保持既有行为)与对外公开的 bigint API({@code api.crafting.bigint})。 + * 抽出来的好处是两条入口对同一台机器算出的容量、上限与能量口径完全一致,不会各自漂移。

+ * + *

语义前提:bigint 批次是「折叠」语义 —— 一次解析/装配得到单份产出,再整体 ×count, + * 因此这里的上限都是「在不让产物分段列表爆炸的前提下,本批最多能接多少份」。

+ * + *

只对万象样板生效:本类的方法都要求宿主 + * {@link CraftingTaskContext#supportsBigIntegerRecipeBatches()} 为 true(目前只有多方块核心)。 + * 合成样板的 bigint 路径不经过这里,它直接由「一次装配 + BigInteger 放大」完成。

+ */ +public final class AlloyFurnaceBigIntegerCrafting { + private static final BigInteger MAX_LONG = BigInteger.valueOf(Long.MAX_VALUE); + /** 合成样板未绑定配方时的基础每 tick 能耗,与 {@code CraftingTask} 的配方缺失分支一致。 */ + private static final long CRAFTING_PATTERN_ENERGY_PER_TICK = 200L; + /** 催化剂未改变处理时间时的基础处理时间(ticks),与 {@code CraftingTaskContext} 的缺省分支一致。 */ + private static final long CRAFTING_PATTERN_BASE_PROCESS_TIME = 200L; + + /** + * 合成样板的单份能耗:基础每 tick 能耗 × 当前催化剂下的处理时间。 + * + *

合成样板不绑定合金炉配方,其能耗口径取自 {@code CraftingTask#refreshCatalystLayout} 的 + * 配方缺失分支(每 tick 能耗 200 FE)与 {@code CraftingTaskContext#getTaskProcessTime} + * (配方缺失时取催化剂效果的处理时间)。二者相乘即单份能耗,使合成样板与万象样板在同一台 + * 机器上遵循同一套催化剂计费规则;若两处口径发生偏离,同一台机器上的两类样板会出现计费不一致。

+ * + * @param effect 当前催化剂效果;{@code null} 表示无法解析,此时按基础处理时间计 + */ + public static long craftingPatternUnitEnergy(@Nullable ResolvedCatalystEffect effect) { + long processTime = effect == null + ? CRAFTING_PATTERN_BASE_PROCESS_TIME + : Math.max(1, effect.processTime()); + return CRAFTING_PATTERN_ENERGY_PER_TICK * processTime; + } + + private AlloyFurnaceBigIntegerCrafting() { + } + + /** 宿主是否具备「把万象样板折叠成 BigInteger 批次」的能力。 */ + public static boolean supports(@Nullable CraftingTaskContext context) { + return context != null && context.supportsBigIntegerRecipeBatches(); + } + + // ==================== 单份产出 ==================== + + /** + * 单次「样板推送」对应的实际产出(主产物 + 流体 + 隐藏键产出,按键合并)。 + * + *

与 {@code CraftingTask#generatePendingOutputs} 的口径一致:一律取 配方 的产出, + * 而不是样板声明的模板键 —— 样板声明的 id-only 产物槽只是模板,真正落进网络的是配方产出。

+ * + *

同键合并时按 long 饱和:仅当同一键同时出现在单份产出的 {@code outputs()} 与 + * {@code keyOutputs()} 时才会叠加;实际配方不会越过 long,此处仅作饱和保护以避免错误账目。

+ * + * @param manualOperations 一次样板推送代表多少次基础配方操作(手动放大样板 > 1,普通为 1) + */ + public static @NotNull List unitOutputs(@NotNull AdvancedAlloyFurnaceRecipe recipe, + long manualOperations) { + long multiplier = Math.max(1L, manualOperations); + Object2ObjectLinkedOpenHashMap merged = new Object2ObjectLinkedOpenHashMap<>(); + for (ItemStack output : recipe.outputs()) { + mergeUnitOutput(merged, GenericStack.fromItemStack(output), multiplier); + } + for (FluidStack output : recipe.outputFluids()) { + mergeUnitOutput(merged, GenericStack.fromFluidStack(output), multiplier); + } + for (GenericStack output : recipe.keyOutputs()) { + mergeUnitOutput(merged, output, multiplier); + } + List result = new ArrayList<>(merged.size()); + for (var entry : merged.object2ObjectEntrySet()) { + long amount = entry.getValue(); + if (amount > 0L) { + result.add(new GenericStack(entry.getKey(), amount)); + } + } + return List.copyOf(result); + } + + /** 单次样板推送的单份产出(不折手动倍率)。 */ + public static @NotNull List unitOutputs(@NotNull AdvancedAlloyFurnaceRecipe recipe) { + return unitOutputs(recipe, 1L); + } + + private static void mergeUnitOutput(Object2ObjectLinkedOpenHashMap merged, + @Nullable GenericStack output, + long multiplier) { + if (output == null || output.what() == null || output.amount() <= 0L) { + return; + } + long scaled = output.amount() > Long.MAX_VALUE / multiplier + ? Long.MAX_VALUE : output.amount() * multiplier; + merged.merge(output.what(), scaled, AlloyFurnaceBigIntegerCrafting::saturatingAdd); + } + + // ==================== 对外回执用的产出 ==================== + + /** 样板声明的产物(模板键)× count,作为「计划产出」给 CPU 侧对照。 */ + public static @NotNull List plannedOutputs(@NotNull IPatternDetails pattern, + @NotNull BigInteger count) { + return scaleToBigIntegerOutputs(java.util.Arrays.asList(pattern.getOutputs()), count); + } + + /** 单份实际产出 × count —— 本批真正会产出的 BigInteger 数量。 */ + public static @NotNull List scaledOutputs(@NotNull List unitOutputs, + @NotNull BigInteger count) { + return scaleToBigIntegerOutputs(unitOutputs, count); + } + + private static @NotNull List scaleToBigIntegerOutputs( + @NotNull List outputs, @NotNull BigInteger count) { + List result = new ArrayList<>(outputs.size()); + for (GenericStack output : outputs) { + if (output == null || output.what() == null || output.amount() <= 0L) { + continue; + } + result.add(new AlloyFurnaceBigIntegerOutput( + output.what(), BigInteger.valueOf(output.amount()).multiply(count))); + } + return List.copyOf(result); + } + + // ==================== 机器身份 ==================== + + /** + * 物理机器身份:{@code 维度@x,y,z};{@code level} 为空时退回 {@code fallback}。 + * + *

这是**唯一**的身份公式:数据能源适配器与对外公开 API 都走这里,保证两条入口认的是 + * 同一台物理机器(DE 靠它避免超卖,公开 API 靠它做发现与去重)。

+ */ + public static @NotNull String machineIdentity(@NotNull String fallback, + @Nullable Level level, + @NotNull BlockPos pos) { + if (level == null) { + return fallback; + } + ResourceLocation dimension = level.dimension().location(); + return dimension + "@" + pos.getX() + "," + pos.getY() + "," + pos.getZ(); + } + + // ==================== 上限 ==================== + /** + * 「材料窗口」上限:本机线程数 N ⇒ 本批最多吃下 N 份「每种材料各 {@code Long.MAX}」的量。 + * + *

传入的 {@code prototype} 是单次推送的原型,所以逐键的数量就是「每次推送的消耗量」; + * 逐键取最小,每个材料键各自都不能越过它那份 long 窗口。

+ * + * @return 非负上限;原型里没有正数条目时返回整个窗口(等价于不设限) + */ + public static @NotNull BigInteger maximumWindowedCount(@NotNull KeyCounter @NotNull [] prototype, + int threads) { + BigInteger window = MAX_LONG.multiply(BigInteger.valueOf(Math.max(1, threads))); + BigInteger limit = null; + for (KeyCounter counter : prototype) { + if (counter == null) { + continue; + } + for (var entry : counter) { + long amount = entry.getLongValue(); + if (amount <= 0L) { + continue; + } + BigInteger allowed = window.divide(BigInteger.valueOf(amount)); + limit = limit == null ? allowed : limit.min(allowed); + } + } + return limit == null ? window : limit; + } + + /** + * 「产物分段」上限:由「分段预算」反推能安全承载的推送次数。 + * + *

产物按 {@code CraftingAeAmountAccumulator#segments()} 切成 ≤ long 的段入队,段数随 count + * 线性增长,所以上限 = {@code Long.MAX × 分段预算 / 单位产出量}。

+ * + * @param unitOutputs 单次推送的实际产出(见 {@link #unitOutputs(AdvancedAlloyFurnaceRecipe, long)}) + * @param segmentBudget 本批允许产生的分段数(由 AIMD 控制器给出,恒 ≥ 1) + */ + public static @NotNull BigInteger maximumSegmentedCount(@NotNull List unitOutputs, + long segmentBudget) { + BigInteger segments = BigInteger.valueOf(Math.max(1L, segmentBudget)); + BigInteger limit = null; + for (GenericStack output : unitOutputs) { + if (output == null || output.amount() <= 0L) { + continue; + } + BigInteger allowed = MAX_LONG.multiply(segments).divide(BigInteger.valueOf(output.amount())); + limit = limit == null ? allowed : limit.min(allowed); + } + return limit == null ? MAX_LONG.multiply(segments) : limit; + } + + /** + * 「能量」上限:按可用能量反推本批最多能收多少份。 + * + *

口径与 {@code AlloyFurnaceRecipeExecutor} 完全对齐:

+ *
    + *
  • 与并行相关({@code energyMultipliesWithParallel}):单份能耗 = {@code recipeEnergy / divisor}, + * 所以份数上限 = {@code 可用能量 × divisor / recipeEnergy};
  • + *
  • 与并行无关:整批只收一次固定能耗,只要有能量就不设限。
  • + *
+ * + *

有用线圈(tier 10)走的是第二条 ⇒ 本方法返回 {@code null}(无能量闸),因此它的 + * bigint 容量只受材料窗口与产物分段预算限制,能吃到完整规模。这个性质由两处共同保证: + * {@code CatalystEffectResolver} 把 {@code energyMultipliesWithParallel} 设为 + * {@code !isUsefulIngot()},而 {@code OmniversalCoilStats} 给 tier 10 的催化剂类型就是 + * {@link com.sorrowmist.useless.api.enums.CatalystType#USEFUL_INGOT}。 + * 改这两处任一处都会让有用线圈的能量变成按 count 翻倍,从而把 bigint 批次压回小规模。

+ * + * @return 上限;{@code null} 表示「不设限」,{@link BigInteger#ZERO} 表示完全没能量 + */ + public static @Nullable BigInteger maximumCountForEnergy(@NotNull CraftingTaskContext context, + @NotNull AdvancedAlloyFurnaceRecipe recipe) { + return maximumCountForEnergy( + Math.max(0L, recipe.energy()), + Math.max(0L, context.getEnergyManager().getEnergyStoredLong()), + context.resolveTaskEffect(recipe)); + } + + /** + * 「能量」上限的通用形式:基准能耗与催化剂效果由调用方给出。 + * + *

合成样板没有绑定配方,无法从配方取能耗,因此需要这个重载;它与配方重载共用同一套 + * 催化剂判定,两类样板的能量闸因此严格一致。

+ * + * @param recipeEnergy 单份基准能耗 + * @param available 当前可用能量 + * @return 上限;{@code null} 表示「不设限」,{@link BigInteger#ZERO} 表示完全没能量 + */ + public static @Nullable BigInteger maximumCountForEnergy(long recipeEnergy, + long available, + @Nullable ResolvedCatalystEffect effect) { + long normalizedEnergy = Math.max(0L, recipeEnergy); + if (normalizedEnergy <= 0L) { + return null; + } + long usableEnergy = Math.max(0L, available); + int divisor = divisorOf(effect); + if (multipliesWithParallel(effect)) { + return BigInteger.valueOf(usableEnergy) + .multiply(BigInteger.valueOf(divisor)) + .divide(BigInteger.valueOf(normalizedEnergy)); + } + return usableEnergy >= divideRoundUp(normalizedEnergy, divisor) ? null : BigInteger.ZERO; + } + + /** + * 本批实际要扣的总能量(BigInteger 版 {@code calculateTargetTotalEnergy})。 + * + *

正常路径下 {@code count} 已由 {@link #maximumCountForEnergy} 收窄,结果必然落在 + * long 范围内;此处仍做饱和保护,避免调用方传入任意 count 时抛出异常。

+ * + *

有用线圈(tier 10)不会因 count 翻倍:它的催化剂是有用锭, + * {@code energyMultipliesWithParallel == false} ⇒ 这里走「整批一次固定能耗」分支, + * 结果 = {@code ceil(recipeEnergy / 1024)},与 {@code count} 无关。所以即使一批发配 + * {@code 1e22} 份,也只收这么一点能量 —— 与长版 counted 路径同一口径。

+ * + * @param count 本批的推送次数,必须为正 + */ + public static long totalEnergy(@NotNull AdvancedAlloyFurnaceRecipe recipe, + @NotNull BigInteger count, + @Nullable ResolvedCatalystEffect effect) { + return totalEnergy(Math.max(0L, recipe.energy()), count, effect); + } + + /** + * 按「单份基准能耗」计费的重载:合成样板不绑定合金炉配方,其基准能耗由调用方给出。 + * + *

该重载与配方重载共用同一套催化剂口径,因此合成样板与万象样板在同一台机器上的 + * 「是否按并行放大」判定完全一致,不会各自漂移。

+ * + * @param recipeEnergy 单份基准能耗;合成样板传 {@link #craftingPatternUnitEnergy(ResolvedCatalystEffect)} + * @param count 本批的推送次数,必须为正 + */ + public static long totalEnergy(long recipeEnergy, @NotNull BigInteger count, + @Nullable ResolvedCatalystEffect effect) { + long normalizedEnergy = Math.max(0L, recipeEnergy); + if (normalizedEnergy <= 0L) { + return 0L; + } + int divisor = divisorOf(effect); + if (!multipliesWithParallel(effect)) { + return divideRoundUp(normalizedEnergy, divisor); + } + BigInteger total = BigInteger.valueOf(normalizedEnergy) + .multiply(count) + .add(BigInteger.valueOf(divisor - 1L)) + .divide(BigInteger.valueOf(divisor)); + return total.compareTo(MAX_LONG) >= 0 ? Long.MAX_VALUE : total.longValueExact(); + } + + /** + * 合成样板 bigint 批次的次数上限:材料窗口 ∩ 产物分段预算,最后叠加运行时降频。 + * + *

合成样板由本机在虚拟 3×3 工作台上装配一次再折叠,不收能量, + * 所以这里没有能量闸 —— 与长版 counted 路径的合成样板分支口径一致。

+ * + * @param prototype 单次装配的原型(不是 ×count 的整批材料) + * @param segmentBudget 本批允许产生的分段数(AIMD 控制器给出) + */ + /** + * 把「材料窗口」并入已有上限;配置开关开启时完全跳过这道闸(不留任何有限上限)。 + * + *

为什么材料窗口可以被跳过:它本质是一道配平闸("每个线程一个 {@code Long} 窗口"), + * 而不是物理限制 —— 折叠语义下机器只消费一份原型,整批材料由调用方的 BigInteger 账本扣除。

+ * + *

跳过它仍然安全:{@code limit} 里已经含「产物交付能力」这道物理闸 + * (AE2 存储接口单次只收 {@code long},每 tick 能写回多少是有限的), + * 所以批次规模不会失控,只是不再受线程数制约。

+ */ + public static @NotNull BigInteger applyMaterialWindow(@NotNull BigInteger limit, + @NotNull KeyCounter @NotNull [] prototype, + int threads) { + return ConfigManager.isFurnaceAeUnlimitedBigintParallelism() + ? limit + : limit.min(maximumWindowedCount(prototype, threads)); + } + + /** + * 把「产物交付能力」并入上限;**若已有 CPU 适配器整批接走产物,则跳过这道闸**。 + * + *

该限制的前提是「产物由本机逐段写回网络」。一旦有适配器整批接走,交付责任即转移至对方, + * 此时再按本机写回速率限制单批规模只会无谓地压小批次 —— 并会同时压制外部 CPU 的自适应窗口 + * (其窗口增长要求「一批能在 2 tick 内返回完」;批次被压小后, + * 按「返回量 × 2 ≥ 窗口」判定的增长条件即无法满足)。

+ */ + public static @NotNull BigInteger applyDeliveryLimit(@NotNull BigInteger limit, + @NotNull List outputs, + long segmentBudget) { + return AlloyFurnaceBigIntegerCpuAdapters.hasBulkOutputAdapter() + ? limit + : limit.min(maximumSegmentedCount(outputs, segmentBudget)); + } + + /** + * 本机对一批合成样板的份数上限。 + * + *

为什么把 {@code requested} 也传进来:两道闸(材料窗口 / 产物交付)都可以被配置或 + * 外部适配器解除,那时"上限"就等于调用方请求的量本身。用 {@code requested} 当基准, + * 就不需要任何"足够大的哨兵值"来表达"不受限"。

+ * + * @param requested 调用方请求的份数(上限不会超过它) + */ + public static @NotNull BigInteger maximumCraftingPatternCount(@Nullable CraftingTaskContext context, + @NotNull IPatternDetails pattern, + @NotNull KeyCounter @NotNull [] prototype, + int threads, + long segmentBudget, + @NotNull BigInteger requested) { + BigInteger limit = applyMaterialWindow(requested, prototype, threads); + limit = applyDeliveryLimit(limit, java.util.Arrays.asList(pattern.getOutputs()), segmentBudget); + // 无任务上下文时无法解析催化剂效果,此处不施加能量闸;实际扣电仍在提交侧执行, + // 提交侧拒收不会让 DE 的 exact 分支卡死(容量上报侧已在有能力解析时收窄)。 + if (context != null) { + BigInteger energyCap = maximumCraftingPatternCountForEnergy(context); + if (energyCap != null) { + limit = limit.min(energyCap); + } + } + return limit.signum() <= 0 ? BigInteger.ZERO : AlloyFurnaceTickBudget.applyScale(limit); + } + + /** + * 合成样板的「能量」上限。 + * + *

合成样板不绑定合金炉配方,因此单份基准能耗取 {@link #craftingPatternUnitEnergy(ResolvedCatalystEffect)}; + * 催化剂效果仍从机器解析(传入 {@code null} 配方),使合成样板与万象样板在同一台机器上 + * 遵循同一套「是否按并行放大」的判定。

+ * + *

该判定是「有用线圈不按 count 翻倍计费」的实现位置:有用线圈的 + * {@code energyMultipliesWithParallel} 为 {@code false},此处遂返回 {@code null} + * (不设限),其 bigint 容量只受材料窗口与产物分段预算约束。

+ * + * @return 上限;{@code null} 表示不设限,{@link BigInteger#ZERO} 表示完全没能量 + */ + public static @Nullable BigInteger maximumCraftingPatternCountForEnergy(@NotNull CraftingTaskContext context) { + ResolvedCatalystEffect effect = context.resolveTaskEffect(null); + return maximumCountForEnergy( + craftingPatternUnitEnergy(effect), + context.getEnergyManager().getEnergyStoredLong(), + effect); + } + + /** + * 万象样板 bigint 批次的综合上限:配方可用性 → 窗口 → 分段 → 能量,最后叠加运行时降频。 + * + *

返回 {@link BigInteger#ZERO} 表示「本机此刻一份都收不了」,调用方据此不要发布 + * machine identity(DE 的 exact 分支一旦接管就不再回落长版路径,发布容量却拒绝会卡死该 provider)。

+ * + * @param prototype 单次推送的原型(不是 ×count 的整批材料) + * @param threads 本机当前线程数(多方块跟随线圈线程) + * @param segmentBudget 本批允许产生的分段数(AIMD 控制器给出) + */ + /** + * 本机对一批万象样板的份数上限。 + * + * @param requested 调用方请求的份数(上限不会超过它);见 + * {@link #maximumCraftingPatternCount} 里关于"为什么不用哨兵"的说明 + */ + public static @NotNull BigInteger maximumCount(@NotNull CraftingTaskContext context, + @NotNull OmniversalPatternDetails pattern, + @NotNull KeyCounter @NotNull [] prototype, + int threads, + long segmentBudget, + @NotNull BigInteger requested) { + AdvancedAlloyFurnaceRecipe recipe = pattern.recipe(); + if (recipe == null || !context.isTaskRecipeAvailable(recipe)) { + return BigInteger.ZERO; + } + long manualOperations = SmartDoublingPatterns.manualOperationsPerPattern(recipe, pattern); + if (manualOperations <= 0L) { + // 与长版容量侧同一口径:无法证明「一次推送 = 整数次配方操作」时不接这批。 + return BigInteger.ZERO; + } + List unitOutputs = unitOutputs(recipe, manualOperations); + if (unitOutputs.isEmpty()) { + return BigInteger.ZERO; + } + BigInteger limit = applyMaterialWindow(requested, prototype, threads); + limit = applyDeliveryLimit(limit, unitOutputs, segmentBudget); + BigInteger energyCap = maximumCountForEnergy(context, recipe); + if (energyCap != null) { + limit = limit.min(energyCap); + } + if (limit.signum() <= 0) { + return BigInteger.ZERO; + } + // 动态降频:本机最近实测耗时超预算时,按比例收窄本批次的窗口预算。 + return AlloyFurnaceTickBudget.applyScale(limit); + } + + // ==================== 内部 ==================== + + /** + * {@code null} 催化剂效果按「与并行相关」处理:那是收费更多的一侧,宁可少收也不免费。 + */ + private static boolean multipliesWithParallel(@Nullable ResolvedCatalystEffect effect) { + return effect == null || effect.energyMultipliesWithParallel(); + } + + private static int divisorOf(@Nullable ResolvedCatalystEffect effect) { + return effect == null ? 1 : Math.max(1, effect.energyDivisor()); + } + + private static long divideRoundUp(long amount, int divisor) { + if (amount <= 0L) { + return 0L; + } + long normalizedDivisor = Math.max(1, divisor); + return 1L + (amount - 1L) / normalizedDivisor; + } + + private static long saturatingAdd(long left, long right) { + return right > 0L && left > Long.MAX_VALUE - right ? Long.MAX_VALUE : left + right; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/AlloyFurnaceTickBudget.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/AlloyFurnaceTickBudget.java new file mode 100644 index 00000000..8eae9e09 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/AlloyFurnaceTickBudget.java @@ -0,0 +1,232 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import com.sorrowmist.useless.core.config.ConfigManager; +import net.minecraft.server.MinecraftServer; +import net.minecraftforge.server.ServerLifecycleHooks; + +import java.math.BigInteger; + +/** + * 本机自己的「每 tick 时间预算」动态降频,思路仿数据能源的 30ms 提交预算 + * ({@code CraftingDispatchWindow}):量一下自己每服务器 tick 花在合金炉合成上的时间, + * 超预算就把后面批次的窗口预算按比例收窄,避免大数批次把服务端线程拖死。 + * + *

为什么需要它:bigint 批次本身是 O(1) 折叠,但「产物切段 + 回网插入」以及数据能源那侧的 + * 记账都与批次规模相关。线程数可以配到很大(配置者自负),所以必须有运行时反馈: + * 超了就自动少收一点,缓过来再放开。

+ * + *

粒度是全局的(所有机器跑在同一个服务端线程上),和 DE 的每网格预算同一个量级; + * 多台机器的耗时会累加、共同把降频压下去 —— 这正是想要的效果。

+ * + *

统计窗口用墙钟({@code System.nanoTime()})而不是游戏刻:调用方只需在自己花时间的地方 + * 报一笔 {@link #addWork(long)} 即可,不需要额外的 tick 管道,也不会因为漏调而失真。

+ */ +public final class AlloyFurnaceTickBudget { + /** 统计窗口:约 2–3 个服务器 tick 的墙钟时长。 */ + private static final long WINDOW_NANOS = 50_000_000L; + /** 全局预算相对「每台机器回网预算」的倍数:留一倍余量给准入 / 任务工作。 */ + private static final long GLOBAL_BUDGET_MULTIPLIER = 2L; + /** 全局预算下限:即使配置很小也不低于此值(保持原有量级)。 */ + private static final long MIN_GLOBAL_BUDGET_NANOS = 10_000_000L; + /** + * 一个满速服务器 tick 的时长(20 TPS)。统计窗口本来就按一个 tick 折算, + * 所以「每 tick 花多少」与「每窗口花多少」是同一个量。 + */ + private static final long NOMINAL_TICK_NANOS = 50_000_000L; + /** + * 整套机器 + 其它负载一起,允许用到的一个满速 tick 的比例。 + * + *

为什么必须有这条闸:本预算被两个方向同时用来定尺 —— 回网路径按它收窄 + * 「本 tick 愿意花的时间」,准入路径按 {@code perTick ÷ OUTPUT_BUDGET_SAFETY_DIVISOR} + * 收窄「单批规模」。于是:

+ * + *
    + *
  • 单批实际耗时 ≈ 回网预算 ÷ 2;
  • + *
  • 降频阈值 = 回网预算 × 2。
  • + *
+ * + *

两者恒差 4 倍 ⇒ 单机(乃至 4 台)满载时阈值在结构上无法被触及, + * {@link #scale()} 恒为 1.0,该限制实际不起作用。

+ * + *

实测印证:回网预算配置为 100ms 时,回网每 tick 稳定占用 50ms(等于整条 tick), + * 而 {@code throttleScale} 仍为 1.0,服务器降至 17.4 TPS。

+ * + *

未直接乘以固定 tick 时长:固定值会误判 —— 实测该服务器其它负载仅 + * ~7.5ms,固定 30ms 的限制会把回网预算压在 ×3.9 吞吐上,而实际仍可再提升 2.5 倍。 + * 因此按真实空闲时间计算,见 {@link #freeTickAllowanceNanos()}。

+ */ + private static final double MAX_TICK_FRACTION = 0.9D; + + /** + * 每窗口({@link #WINDOW_NANOS})愿意花在这套机器合成上的时间。 + * + *

为什么必须由配置推导、不能硬编码:它原来是写死的 10ms,而 + * {@link #scaledFlushBudget(long)} 又要乘上 {@link #scale()}(= {@code 预算 / 实测})—— + * 于是「把回网预算配置调大」会被这里直接卡回去:配置成 20ms 时回网自己每 tick 花 20ms, + * 折算到 50ms 窗口约 50ms ⇒ {@code scale = 10/50 = 0.2} ⇒ 实际只剩 4ms,配置文件形同虚设。

+ * + *

现在按配置推导:全局 = 每台机器回网预算 × {@link #GLOBAL_BUDGET_MULTIPLIER}, + * 下限 {@link #MIN_GLOBAL_BUDGET_NANOS},再夹到真实空闲 tick 时间 + * ({@link #freeTickAllowanceNanos()})。这样配置仍是主要真相来源,同时仍保留 + * 「多台机器总耗时共同把降频压下去」的全局保护,并且不允许「把回网预算配大」 + * 变成「允许抢整条 tick」—— 见 {@link #MAX_TICK_FRACTION}。

+ */ + private static long budgetNanos() { + long perMachine = ConfigManager.getAdvancedAlloyFurnaceAeOutputReturnBudgetMillis() * 1_000_000L; + long derived = perMachine > Long.MAX_VALUE / GLOBAL_BUDGET_MULTIPLIER + ? Long.MAX_VALUE : perMachine * GLOBAL_BUDGET_MULTIPLIER; + return Math.max(MIN_GLOBAL_BUDGET_NANOS, Math.min(derived, freeTickAllowanceNanos())); + } + + /** + * 按真实空闲 tick 时间算出本套机器还能吃多少。 + * + *

公式:{@code 目标占用(MAX_TICK_FRACTION × 一个满速 tick) − 其它负载耗时}。

+ * + *

需扣除「本机自身耗时」的原因:{@code getCurrentSmoothedTickTime()} 度量的是 + * 整个服务器每 tick 的工作时间(不含 tick 循环的 sleep),其中已包含本套机器 + * 自身的开销。若直接将其视为「其它负载」,会形成自反馈 —— 本机耗时越多、空闲越少、 + * 预算越紧,最终将自身压制至接近零。因此必须减去 {@link #spentMillis()} 才代表真正的「其它」负载。

+ * + *

两个量的平滑窗口不同(服务器约 100 tick,本机约 1 tick),因此该值为粗估; + * 外层还有 {@link #MIN_GLOBAL_BUDGET_NANOS} 下限与配置推导的上限作为边界, + * 估算偏差只会使吞吐略偏保守或略偏激进,不会失控。

+ * + *

服务器不可用(未启动 / 客户端)时退回 {@code MAX_TICK_FRACTION × 一个 tick}, + * 与旧行为一致。

+ */ + private static long freeTickAllowanceNanos() { + long target = (long) (NOMINAL_TICK_NANOS * MAX_TICK_FRACTION); + MinecraftServer server = ServerLifecycleHooks.getCurrentServer(); + if (server == null) { + return target; + } + long serverWork = (long) (server.getAverageTickTime() * 1_000_000.0F); + long ourWork = (long) (spentMillis() * 1_000_000.0D); + long otherWork = Math.max(0L, serverWork - ourWork); + return Math.max(0L, target - otherWork); + } + /** 平滑系数:新窗口实测占 1/4,兼顾响应与抗抖。 */ + private static final double SMOOTHING = 0.25D; + /** 收窄下限:高负载时仍保留一定吞吐量,避免完全停止导致数据能源进入「无容量 → 重新提交」循环。 */ + private static final double MIN_SCALE = 0.02D; + /** 定点缩放精度。 */ + private static final long SCALE_UNITS = 10_000L; + + private static long windowStartNanos; + private static long spentThisWindow; + private static double smoothedNanos; + + private AlloyFurnaceTickBudget() {} + + /** 记录一次本机工作耗时(纳秒)。 */ + public static synchronized void addWork(long nanos) { + rollWindow(); + if (nanos > 0L) { + spentThisWindow = spentThisWindow > Long.MAX_VALUE - nanos + ? Long.MAX_VALUE : spentThisWindow + nanos; + } + } + + /** + * @return 当前批次缩放系数:预算内为 {@code 1.0};超出按 {@code 预算 / 实测} 收窄(下限 {@link #MIN_SCALE}) + */ + public static synchronized double scale() { + rollWindow(); + long spent = Math.max(spentThisWindow, (long) smoothedNanos); + long budget = budgetNanos(); + if (spent <= budget) { + return 1.0D; + } + return Math.max(MIN_SCALE, (double) budget / (double) spent); + } + + /** 定点缩放:把 {@code value} 按 {@link #scale()} 收窄;至少保留 1。 */ + public static BigInteger applyScale(BigInteger value) { + double scale = scale(); + if (scale >= 1.0D) { + return value; + } + long units = Math.max(1L, (long) (scale * SCALE_UNITS)); + BigInteger scaled = value + .multiply(BigInteger.valueOf(units)) + .divide(BigInteger.valueOf(SCALE_UNITS)); + return scaled.signum() <= 0 ? BigInteger.ONE : scaled; + } + + /** + * 本窗口是否正在降频({@link #scale()} < 1.0)。 + * + *

对应数据能源 dispatch window 的 {@code BUDGET_EXHAUSTED} 语义:让调用方知道 + * 「容量变小是因为我忙,不是我的能力只有这么多」,从而可以主动延后非紧急批次。

+ * + *

降频时仍至少接受 1 份的原因({@link #applyScale} 的 {@code MIN_SCALE} 下限): + * 完全上报「无容量」会触发数据能源「无容量 → 重新提交」的空转,代价更高。 + * 所以降频只收窄批次规模,不彻底停手 —— 调用方要停手请自行判断 {@link #isThrottled()}。

+ */ + public static synchronized boolean isThrottled() { + return scale() < 1.0D; + } + + /** 产物回网窗口的下限:保证重载下仍能推进(够覆盖一次插入),避免队列被彻底饿死。 */ + private static final long MIN_FLUSH_BUDGET_NANOS = 250_000L; + + /** + * 把一次「产物回网」的时间预算按当前全局预算系数收窄。 + * + *

准入路径用 {@link #applyScale(BigInteger)} 收窄批次规模,回网路径收窄的是 + * 本 tick 愿意花的时间。两者共用同一个预算信号,但目的相反:

+ *
    + *
  • 准入收窄 ⇒ 减少接收新任务(保护本 tick);
  • + *
  • 回网收窄 ⇒ 减少投递已完成的产物(同样保护本 tick)。
  • + *
+ * + *

必须收窄的原因:回网预算是每台机器的。若固定为某个常量(例如 4ms), + * N 台机器最坏为 N×4ms/tick —— 10 台满载即占用大半个 tick。按全局系数收窄后总量自然收敛。

+ * + *

保留下限的原因:回网是幂等的,理论上可截断至零;但在持续重载下会使队列无法推进 + * (队列非空 ⇒ 背压持续压制准入 ⇒ 容量 0 ⇒ 整机停摆)。因此保留下限以保证每 tick 仍有推进。 + * 调用方另有一层保障:预算检查位于第一次插入之后,因此每 tick 至少会尝试一次插入。

+ * + * @param baseBudgetNanos 本机的基础回网预算(正数) + * @return 收窄后的预算,恒 ∈ [{@code min(MIN_FLUSH_BUDGET_NANOS, baseBudgetNanos)}, baseBudgetNanos] + */ + public static synchronized long scaledFlushBudget(long baseBudgetNanos) { + if (baseBudgetNanos <= 0L) { + return MIN_FLUSH_BUDGET_NANOS; + } + double scale = scale(); + long scaled = scale >= 1.0D ? baseBudgetNanos : (long) (baseBudgetNanos * scale); + // 下限只在「收窄」方向生效:不会把一个本来就小于下限的预算抬上去。 + long floor = Math.min(MIN_FLUSH_BUDGET_NANOS, baseBudgetNanos); + return Math.max(floor, Math.min(baseBudgetNanos, scaled)); + } + + /** @return 观测用:本窗口(或平滑值)的每 tick 实测耗时(毫秒) */ + public static synchronized double spentMillis() { + return Math.max(spentThisWindow, (long) smoothedNanos) / 1_000_000.0D; + } + + /** @return 观测用:每 tick 预算(毫秒,随配置推导) */ + public static double budgetMillis() { + return budgetNanos() / 1_000_000.0D; + } + + /** 窗口到期就把实测值并入平滑值并清零。 */ + private static void rollWindow() { + long now = System.nanoTime(); + if (windowStartNanos == 0L) { + windowStartNanos = now; + return; + } + if (now - windowStartNanos < WINDOW_NANOS) { + return; + } + double observedPerTick = spentThisWindow * ((double) WINDOW_NANOS / (now - windowStartNanos)); + smoothedNanos = smoothedNanos <= 0.0D + ? observedPerTick + : smoothedNanos * (1.0D - SMOOTHING) + observedPerTick * SMOOTHING; + windowStartNanos = now; + spentThisWindow = 0L; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/CraftingAeAmountAccumulator.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/CraftingAeAmountAccumulator.java new file mode 100644 index 00000000..6234d03d --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/CraftingAeAmountAccumulator.java @@ -0,0 +1,230 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import appeng.api.stacks.KeyCounter; +import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap; +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Aggregates exact AE amounts without materializing item or fluid stacks for numeric chunks. + */ +final class CraftingAeAmountAccumulator { + private static final BigInteger MAX_LONG = BigInteger.valueOf(Long.MAX_VALUE); + + private final Object2ObjectLinkedOpenHashMap amounts = + new Object2ObjectLinkedOpenHashMap<>(); + + static CraftingAeAmountAccumulator fromCounters(KeyCounter[] counters) { + CraftingAeAmountAccumulator accumulator = new CraftingAeAmountAccumulator(); + accumulator.add(counters); + return accumulator; + } + + void add(KeyCounter[] counters) { + if (counters == null) { + return; + } + for (KeyCounter counter : counters) { + if (counter == null) { + continue; + } + for (var entry : counter) { + add(entry.getKey(), entry.getLongValue()); + } + } + } + + static CraftingAeAmountAccumulator fromGenericStacks(Iterable stacks) { + CraftingAeAmountAccumulator accumulator = new CraftingAeAmountAccumulator(); + for (GenericStack stack : stacks) { + accumulator.add(stack); + } + return accumulator; + } + + void add(GenericStack stack) { + Objects.requireNonNull(stack, "Crafting AE amount"); + add(stack.what(), stack.amount()); + } + + void add(AEKey key, long amount) { + add(key, BigInteger.valueOf(amount)); + } + + void add(AEKey key, BigInteger amount) { + Objects.requireNonNull(key, "Crafting AE key"); + Objects.requireNonNull(amount, "Crafting AE amount"); + if (amount.signum() <= 0) { + throw new IllegalArgumentException("Crafting AE amounts must be positive"); + } + this.amounts.merge(key, amount, BigInteger::add); + } + + // ==================== 聚合与扣减(回网路径用) ==================== + + /** + * 合并另一本账:逐键相加,键不存在则新建。 + * + *

用于回网刷新前把整条队列按键聚合成一本账:同一个键只投递一次,而不是每条各投一次。 + * 另一本账的余额恒为正({@link #consume} 扣到 0 就移除键),所以直接走 + * {@link #add(AEKey, BigInteger)} 即可。

+ */ + void addAll(CraftingAeAmountAccumulator other) { + Objects.requireNonNull(other, "Crafting AE amount ledger"); + for (var entry : other.amounts.object2ObjectEntrySet()) { + add(entry.getKey(), entry.getValue()); + } + } + + /** + * 按键扣减余额(把「已投递量」记回账本)。 + * + *

与 {@link #add(AEKey, BigInteger)} 的严格正数校验相反:这里允许扣到 0, + * 因为分批回网时投递量本来就可能小于余额。扣到 0 的键会被移除,保证 {@link #isEmpty()} + * 与持久化都不会留下零余额键。键不存在、或 {@code amount <= 0} 时返回 + * {@link BigInteger#ZERO} 且不抛异常 —— 归因是逐键遍历,必然遇到「该条目没有这个键」。

+ * + * @return 实际扣减量,恒 ∈ [0, 该键当前余额] + */ + BigInteger consume(AEKey key, BigInteger amount) { + Objects.requireNonNull(key, "Crafting AE key"); + Objects.requireNonNull(amount, "Crafting AE amount"); + if (amount.signum() <= 0) { + return BigInteger.ZERO; + } + BigInteger current = this.amounts.get(key); + if (current == null) { + return BigInteger.ZERO; + } + BigInteger consumed = current.min(amount); + BigInteger remaining = current.subtract(consumed); + if (remaining.signum() <= 0) { + this.amounts.remove(key); + } else { + this.amounts.put(key, remaining); + } + return consumed; + } + + /** + * 按键余额的不可变快照(保持插入顺序)。 + * + *

调用方可以在遍历快照的同时安全地 {@link #consume} 修改账本本身 —— 快照里的 value + * 是不可变 {@link BigInteger},不受影响。

+ * + *

刻意不暴露内部 map:避免调用方绕过 {@link #add}/{@link #consume} 直接改值, + * 破坏「无零/负余额」这条不变量。

+ */ + List> snapshotEntries() { + List> snapshot = new ArrayList<>(this.amounts.size()); + for (var entry : this.amounts.object2ObjectEntrySet()) { + snapshot.add(Map.entry(entry.getKey(), entry.getValue())); + } + return snapshot; + } + + /** 读取单键余额;键不存在返回 {@link BigInteger#ZERO}。 */ + BigInteger amount(AEKey key) { + return this.amounts.getOrDefault(key, BigInteger.ZERO); + } + + /** + * 所有键余额之和(精确 BigInteger)。 + * + *

回网积压度量用它:入队加总量、交付减实际交付量,两者都是精确值 ⇒ 零漂移。

+ */ + BigInteger totalAmount() { + BigInteger total = BigInteger.ZERO; + for (BigInteger amount : this.amounts.values()) { + total = total.add(amount); + } + return total; + } + + /** 深拷贝。值是可变性为零的 {@link BigInteger},逐键 putAll 即可。 */ + CraftingAeAmountAccumulator copy() { + CraftingAeAmountAccumulator copy = new CraftingAeAmountAccumulator(); + copy.amounts.putAll(this.amounts); + return copy; + } + + boolean fitsWithinLongAfterMultiplying(long multiplier) { + if (multiplier <= 0L) { + throw new IllegalArgumentException("Crafting AE multiplier must be positive"); + } + BigInteger factor = BigInteger.valueOf(multiplier); + for (BigInteger amount : this.amounts.values()) { + if (amount.multiply(factor).compareTo(MAX_LONG) > 0) { + return false; + } + } + return true; + } + + boolean isEmpty() { + return this.amounts.isEmpty(); + } + + List segments() { + ArrayList result = new ArrayList<>(this.amounts.size()); + for (var entry : this.amounts.object2ObjectEntrySet()) { + BigInteger remaining = entry.getValue(); + while (remaining.compareTo(MAX_LONG) > 0) { + result.add(new GenericStack(entry.getKey(), Long.MAX_VALUE)); + remaining = remaining.subtract(MAX_LONG); + } + if (remaining.signum() > 0) { + result.add(new GenericStack(entry.getKey(), remaining.longValueExact())); + } + } + return result; + } + + // ==================== 持久化 ==================== + + /** + * 紧凑持久化:每个键写一条「键 + BigInteger 字节数组」。 + * + *

为什么不直接写 {@link #segments()}:大数产物按 {@code Long.MAX} 切段, + * 1e22 个物品就是上千条 NBT;而回网队列的深度等于本机线程数,整条队列叠起来能到 GB 级, + * 且每次区块存盘都要重写一遍。按键聚合后每个键只占一条,重载时再 {@link #segments()} 展开, + * 语义完全等价。

+ */ + ListTag writeCompactTag(HolderLookup.Provider registries) { + ListTag tag = new ListTag(); + for (var entry : this.amounts.object2ObjectEntrySet()) { + CompoundTag entryTag = new CompoundTag(); + entryTag.put("Key", entry.getKey().toTagGeneric()); + entryTag.putByteArray("Amount", entry.getValue().toByteArray()); + tag.add(entryTag); + } + return tag; + } + + /** 读取 {@link #writeCompactTag} 写出的紧凑格式。 */ + static CraftingAeAmountAccumulator readCompactTag(HolderLookup.Provider registries, ListTag tag) { + CraftingAeAmountAccumulator accumulator = new CraftingAeAmountAccumulator(); + for (int index = 0; index < tag.size(); index++) { + CompoundTag entryTag = tag.getCompound(index); + byte[] encoded = entryTag.getByteArray("Amount"); + if (encoded.length == 0) { + continue; + } + AEKey key = AEKey.fromTagGeneric(entryTag.getCompound("Key")); + BigInteger amount = new BigInteger(encoded); + if (key != null && amount.signum() > 0) { + accumulator.add(key, amount); + } + } + return accumulator; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/CraftingAeOutputTarget.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/CraftingAeOutputTarget.java new file mode 100644 index 00000000..c8b3be8c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/CraftingAeOutputTarget.java @@ -0,0 +1,63 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import appeng.api.stacks.AEKey; +import org.jetbrains.annotations.NotNull; + +/** + * 一次「产物回网」刷写 pass 内可复用的 ME 网络写入目标。 + * + *

需要它的原因:{@link CraftingTaskContext#tryOutputKeyToAE} 每次调用都需重新解析 + * 装配体 / 节点 / 网格 / 存储服务(多方块侧为 2 次方块实体查询 + 2 次分配)。低频调用时影响有限, + * 但可持续吞吐直接由「每 tick 可插入次数」决定(AE2 存储接口只收 {@code long}, + * 每个产物分段都需一次真实插入),而一 tick 可能插入数千次 —— 该解析成本会直接影响吞吐。

+ * + *

一次 pass 内所有键都写在同一个网格上,解析一次即可复用。

+ * + *

实现必须与 {@code tryOutputKeyToAE} 语义完全一致:先给 neoecoae 的输出认领一次机会, + * 剩余部分再以 {@code MODULATE} 写进 ME 存储。

+ */ +@FunctionalInterface +public interface CraftingAeOutputTarget { + /** + * 把一个键的若干量写回网络。 + * + * @return 实际写入量,恒 ∈ [0, amount] + */ + long insert(@NotNull AEKey key, long amount); + + /** + * 实现是否支持把「兼容层认领」提到逐段插入之前({@link #claim} + {@link #insertRaw})。 + * + *

默认 {@code false} ⇒ 调用方继续只用 {@link #insert},行为与拆分前完全一致。 + * 只有返回 {@code true} 时,调用方才可以用「先 {@link #claim} 一次整键总量、再逐段 + * {@link #insertRaw}」的组合替代逐段 {@link #insert} —— 可减少 N-1 次认领调用。

+ */ + default boolean supportsClaimHoisting() { + return false; + } + + /** + * 只做「兼容层认领」(neoecoae 的动态产物认领),不写 ME 存储。 + * + *

只应在 {@link #supportsClaimHoisting()} 为 true 时调用,且每个键在一次刷写 pass 里 + * 只调用一次,传入该键本趟的全部待投递量(不低于 {@code Long.MAX_VALUE} 即可)。 + * 认领量由兼容层自己的待满足需求封顶,与传入量无关,所以提前到循环外不改变结果。

+ * + * @return 被兼容层认领的量,恒 ∈ [0, totalAmount] + */ + default long claim(@NotNull AEKey key, long totalAmount) { + return 0L; + } + + /** + * 只写 ME 存储,不做兼容层认领。 + * + *

默认实现退回 {@link #insert}:对没有兼容层认领的实现({@link #claim} 恒返回 0) + * 语义完全相同。

+ * + * @return 实际写入量,恒 ∈ [0, amount] + */ + default long insertRaw(@NotNull AEKey key, long amount) { + return insert(key, amount); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/CraftingTask.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/CraftingTask.java new file mode 100644 index 00000000..e1484c39 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/CraftingTask.java @@ -0,0 +1,1393 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.stacks.AEFluidKey; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import appeng.api.stacks.KeyCounter; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.catalyst.ResolvedCatalystEffect; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.execution.AlloyFurnaceRecipeExecutor; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.io.FurnaceOutputPort; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalStackView; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.FurnaceChemicalStorage; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * 高级合金炉中的单个 AE 合成任务。 + * 负责 AE 请求的配方匹配、批次推进、进度同步、取消返还和产物输出。 + */ +public class CraftingTask { + private static final int PROGRESS_SYNC_INTERVAL = 5; + // 等待状态不变时的补发间隔:单方块炉打开界面时不会主动拉取进度,靠补发让新进入追踪范围的玩家看到等待原因 + private static final int WAITING_SYNC_INTERVAL = 20; + private static final BigInteger MAX_INT_AMOUNT = BigInteger.valueOf(Integer.MAX_VALUE); + private static final String STATUS_PROCESSING = + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.processing"; + private static final String STATUS_WAITING_RECIPE = + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_recipe"; + private static final String STATUS_WAITING_INVALID_PATTERN = + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_invalid_pattern"; + private static final String STATUS_WAITING_OUTPUT_MISMATCH = + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_output_mismatch"; + private static final String STATUS_WAITING_CONDITIONS = + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_conditions"; + private static final String STATUS_WAITING_ENERGY = + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_energy"; + private static final String WAITING_ENERGY_DETAIL = + "gui.useless_mod.advanced_alloy_furnace.ae_task_waiting_energy_detail"; + + private final int taskId; + private final IPatternDetails pattern; + private final CraftingTaskContext context; + private final List taskInputItems = new ArrayList<>(); + private final List taskInputFluids = new ArrayList<>(); + private final List taskInputKeys = new ArrayList<>(); + private Set componentInputKeys = Set.of(); + private final List queuedSubTasks = new ArrayList<>(); + // 待输出缓冲:产物先进缓冲,由 flushPendingOutputs 逐 tick 写出,写不下时保留重试,避免静默丢失 + private final List pendingOutputItems = new ArrayList<>(); + private final List pendingOutputFluids = new ArrayList<>(); + private final List pendingOutputKeys = new ArrayList<>(); + private boolean awaitingOutputFlush = false; + // 当前子任务已扣除的能量,用于完成结算(与本体 settleCompletionEnergy 语义对齐) + private long accumulatedEnergy = 0; + private long craftCount = 1L; + private long displayedCraftCount = 1L; + // A manually scaled AE pattern may represent multiple base recipe operations per push. + private long patternOperationsPerPush = 1L; + private boolean patternOperationsResolved = false; + private boolean cancelled = false; + private boolean processingComplete = false; + private boolean progressedLastTick = false; + private boolean initialized = false; + private int baseProcessTime = 200; + private long baseEnergyPerTick = 200L; + private long maxParallel = 1L; + private int batches = 1; + private long lastBatchSize = 1L; + private int processTime = 200; + private int progress = 0; + private int progressUpdateCounter = 0; + private int waitingSyncCounter = 0; + private boolean progressRegistered = false; + private AdvancedAlloyFurnaceAeManager.AETaskProgress taskProgressRef = null; + private String waitingStatusKey = STATUS_WAITING_CONDITIONS; + private String waitingDetail = ""; + // 缓存已解析的配方,避免 tick() 每帧重复查找(AE 任务的输入在批次内固定) + private AdvancedAlloyFurnaceRecipe cachedRecipe = null; + + public CraftingTask(int taskId, IPatternDetails pattern, KeyCounter[] inputHolder, long totalCrafts, + CraftingTaskContext context) { + this.taskId = taskId; + this.pattern = pattern; + this.context = context; + this.craftCount = Math.max(1L, totalCrafts); + this.displayedCraftCount = this.craftCount; + storeInputMaterials(inputHolder, this.taskInputItems, this.taskInputFluids, + this.taskInputKeys, context != null && context.supportsLongAeAmounts()); + this.componentInputKeys = AdvancedAlloyFurnacePatternPolicy.componentInputKeys(pattern, inputHolder); + } + + /** 从一个拆分出的子任务构造独立任务(用于空闲线程再分配) */ + private CraftingTask(int taskId, IPatternDetails pattern, CraftingTaskContext context, + CraftingSubTask subTask, long patternOperationsPerPush) { + this.taskId = taskId; + this.pattern = pattern; + this.context = context; + this.craftCount = Math.max(1L, subTask.craftCount); + this.displayedCraftCount = this.craftCount; + this.patternOperationsPerPush = Math.max(1L, patternOperationsPerPush); + this.patternOperationsResolved = true; + this.taskInputItems.addAll(subTask.items); + this.taskInputFluids.addAll(subTask.fluids); + this.taskInputKeys.addAll(subTask.keys); + this.componentInputKeys = resolveComponentInputKeys(); + } + + /** 用于从 NBT 恢复的空任务构造 */ + private CraftingTask(int taskId, IPatternDetails pattern, CraftingTaskContext context) { + this.taskId = taskId; + this.pattern = pattern; + this.context = context; + } + + + public boolean isProcessingComplete() { + return processingComplete; + } + + public boolean progressedLastTick() { + return progressedLastTick; + } + + public boolean isAwaitingOutputFlush() { + return awaitingOutputFlush; + } + + public IPatternDetails getPattern() { + return pattern; + } + + Set getComponentInputKeys() { + return componentInputKeys; + } + + /** + * 直接合并一批 AE 输入(跳过中间 KeyCounter 对象分配),追加为一个子任务 + */ + public boolean addMergedBatch(List allInputs, long operationsPerPush) { + if (processingComplete || cancelled) { + return false; + } + if (allInputs == null || allInputs.isEmpty()) { + return true; + } + + long craftsPerInput = multiplyWithinLong( + Math.max(1L, operationsPerPush), Math.max(1L, patternOperationsPerPush)); + long maximumCraftsPerSubTask = maximumCraftsPerSubTask(); + if (craftsPerInput <= 0L || craftsPerInput > maximumCraftsPerSubTask) { + return false; + } + + List additions = new ArrayList<>(allInputs.size()); + for (KeyCounter[] counters : allInputs) { + additions.add(createSubTask(counters, craftsPerInput)); + } + for (CraftingSubTask addition : additions) { + appendQueuedSubTask(addition, maximumCraftsPerSubTask); + } + updateDisplayedCraftCount(); + updateTaskProgressTotals(); + context.markChanged(); + context.sendAETaskProgressToClients(); + return true; + } + + public boolean canStartNow() { + return prepareTaskRecipe() != null; + } + + public int getTaskId() { + return this.taskId; + } + + /** 是否还有排队中的子任务可拆分到空闲线程 */ + public boolean hasQueuedSubTasks() { + return !this.processingComplete && !this.cancelled && !this.queuedSubTasks.isEmpty(); + } + + /** + * 拆出队尾的一个子任务,构造为独立任务在空闲线程运行。 + * 当前任务保留正在处理的批次不受影响。 + * + * @param newTaskId 新任务 ID + * @return 拆分出的独立任务,若无可拆分子任务则返回 null + */ + @Nullable + public CraftingTask splitLastSubTask(int newTaskId) { + if (!hasQueuedSubTasks()) { + return null; + } + CraftingSubTask subTask = queuedSubTasks.remove(queuedSubTasks.size()-1); + updateDisplayedCraftCount(); + updateTaskProgressTotals(); + context.markChanged(); + context.sendAETaskProgressToClients(); + return new CraftingTask(newTaskId, pattern, context, subTask, patternOperationsPerPush); + } + + public String getWaitingDetail() { + return this.waitingDetail; + } + + public String getWaitingStatusKey() { + return this.waitingStatusKey; + } + + /** 从 KeyCounter[] 创建单次子任务(仅构造时使用) */ + private CraftingSubTask createSubTask(KeyCounter[] counters, long crafts) { + List items = new ArrayList<>(); + List fluids = new ArrayList<>(); + List keys = new ArrayList<>(); + storeInputMaterials(counters, items, fluids, keys, context.supportsLongAeAmounts()); + return new CraftingSubTask(items, fluids, keys, crafts); + } + + private void appendQueuedSubTask(CraftingSubTask addition, long maximumCraftsPerSubTask) { + if (!queuedSubTasks.isEmpty()) { + CraftingSubTask last = queuedSubTasks.get(queuedSubTasks.size()-1); + if (last.craftCount <= maximumCraftsPerSubTask - addition.craftCount) { + queuedSubTasks.set(queuedSubTasks.size() - 1, + mergeSubTasks(last, addition, last.craftCount + addition.craftCount)); + return; + } + } + queuedSubTasks.add(addition); + } + + private CraftingSubTask mergeSubTasks( + CraftingSubTask first, + CraftingSubTask second, + long craftCount) { + CraftingAeAmountAccumulator accumulator = new CraftingAeAmountAccumulator(); + addMaterials(accumulator, first.items, first.fluids, first.keys); + addMaterials(accumulator, second.items, second.fluids, second.keys); + + List items = new ArrayList<>(); + List fluids = new ArrayList<>(); + List keys = new ArrayList<>(); + for (GenericStack amount : accumulator.segments()) { + storeNormalizedMaterial( + amount, items, fluids, keys, context.supportsLongAeAmounts()); + } + return new CraftingSubTask(items, fluids, keys, craftCount); + } + + private static void storeInputMaterials( + KeyCounter[] counters, List items, List fluids, + List keys, boolean keepLongAmounts) { + for (GenericStack amount : CraftingAeAmountAccumulator.fromCounters(counters).segments()) { + storeNormalizedMaterial(amount, items, fluids, keys, keepLongAmounts); + } + } + + private static void storeNormalizedMaterial( + GenericStack amount, + List items, + List fluids, + List keys, + boolean keepLongAmounts) { + if (!keepLongAmounts && fitsInSingleStack(amount)) { + if (amount.what() instanceof AEItemKey itemKey) { + items.add(itemKey.toStack((int) amount.amount())); + return; + } + if (amount.what() instanceof AEFluidKey fluidKey) { + fluids.add(fluidKey.toStack((int) amount.amount())); + return; + } + } + keys.add(new KeyAmount(amount.what(), amount.amount())); + } + + /** + * 只有确实能装进"一个合法栈"的数量才允许物化成 ItemStack / FluidStack。 + *

+ * 1.21 的物品单栈上限是 {@code Item.ABSOLUTE_MAX_STACK_SIZE}(99),AE2 的物品槽容量 + * ({@code InternalInventory#getSlotLimit}、{@code GenericSlotCapacities})也取同一个值; + * 而 {@code ItemStack.CODEC} 的 count 是 {@code ExtraCodecs.intRange(1, 99)}, + * {@code ItemStack#save} 又走 {@code getOrThrow()}。因此 count 超限的栈是越界状态: + * 一旦它被存档(例如掉在地上变成 ItemEntity)就会抛异常,物化成千上万个栈更是性能灾难。 + * 超限数量一律留在 AEKey + long 表示里,由 {@link FurnaceOutputPort#outputKeyWithRemainder} + * 按槽位容量分片。 + *

+ * FluidStack 的数量本身是 int,没有 99 级限制,只需防 int 溢出。 + */ + private static boolean fitsInSingleStack(GenericStack amount) { + if (amount.what() instanceof AEItemKey itemKey) { + return amount.amount() <= itemKey.getMaxStackSize(); + } + return amount.amount() <= Integer.MAX_VALUE; + } + + /** + * 使用本体模具与样板目标输出统一查找配方。 + * 结果缓存于 cachedRecipe,避免 tick() 每帧重复查找; + * 输入变化时(新子任务/新批次)由调用方通过 invalidateRecipeCache() 使其失效。 + */ + private AdvancedAlloyFurnaceRecipe findTaskRecipe() { + if (context.getLevel() == null) return null; + if (cachedRecipe != null) return cachedRecipe; + + cachedRecipe = context.resolveTaskRecipe( + pattern, taskInputItems, taskInputFluids, toGenericStacks(taskInputKeys), craftCount); + return cachedRecipe; + } + + @Nullable + private AdvancedAlloyFurnaceRecipe prepareTaskRecipe() { + AdvancedAlloyFurnaceRecipe recipe = findTaskRecipe(); + if (!isRecipeValid(recipe) || !resolvePatternOperations(recipe) + || !recipeOutputsFitPhysicalTask(recipe)) { + return null; + } + return recipe; + } + + private boolean recipeOutputsFitPhysicalTask(AdvancedAlloyFurnaceRecipe recipe) { + if (AdvancedAlloyFurnacePatternPolicy.usesRecipeOutputs(pattern)) { + CraftingAeAmountAccumulator outputs = new CraftingAeAmountAccumulator(); + for (ItemStack output : recipe.outputs()) { + GenericStack amount = GenericStack.fromItemStack(output); + if (amount != null) { + outputs.add(amount); + } + } + for (FluidStack output : recipe.outputFluids()) { + GenericStack amount = GenericStack.fromFluidStack(output); + if (amount != null) { + outputs.add(amount); + } + } + for (GenericStack output : recipe.keyOutputs()) { + if (output != null && output.what() != null && output.amount() > 0L) { + outputs.add(output); + } + } + return acceptPhysicalOutputs(outputs, craftCount); + } + + CraftingAeAmountAccumulator declaredOutputs = new CraftingAeAmountAccumulator(); + for (GenericStack output : pattern.getOutputs()) { + declaredOutputs.add(output); + } + if (!acceptPhysicalOutputs( + declaredOutputs, calculatePatternOutputRuns(craftCount, patternOperationsPerPush))) { + return false; + } + + CraftingAeAmountAccumulator hiddenOutputs = new CraftingAeAmountAccumulator(); + for (GenericStack output : recipe.keyOutputs()) { + boolean declared = java.util.Arrays.stream(pattern.getOutputs()) + .anyMatch(patternOutput -> output.what().equals(patternOutput.what())); + if (!declared) { + hiddenOutputs.add(output); + } + } + return acceptPhysicalOutputs(hiddenOutputs, craftCount); + } + + private boolean acceptPhysicalOutputs(CraftingAeAmountAccumulator outputs, long multiplier) { + if (outputs.fitsWithinLongAfterMultiplying(multiplier)) { + return true; + } + setWaiting(STATUS_WAITING_INVALID_PATTERN, ""); + return false; + } + + /** + * A normal AE processing pattern has no operation-count metadata. When a player manually + * scales its inputs and outputs, derive the scale from the declared outputs and charge the + * corresponding number of base recipe operations instead of treating the whole pattern as one. + */ + private boolean resolvePatternOperations(AdvancedAlloyFurnaceRecipe recipe) { + if (patternOperationsResolved) { + return true; + } + + ManualPatternOperationResolver.Resolution resolution = + ManualPatternOperationResolver.resolve(recipe, pattern); + if (!resolution.valid()) { + setWaiting(STATUS_WAITING_INVALID_PATTERN, ""); + return false; + } + + long resolvedCraftCount = multiplyWithinLong(craftCount, resolution.operationsPerPattern()); + if (resolvedCraftCount <= 0L) { + setWaiting(STATUS_WAITING_INVALID_PATTERN, ""); + return false; + } + if (!AlloyFurnaceRecipeManager.matchesInputsForOperations( + recipe, taskInputItems, taskInputFluids, + toGenericStacks(taskInputKeys), resolvedCraftCount)) { + setWaiting(STATUS_WAITING_INVALID_PATTERN, ""); + return false; + } + + patternOperationsPerPush = resolution.operationsPerPattern(); + patternOperationsResolved = true; + craftCount = resolvedCraftCount; + updateDisplayedCraftCount(); + return true; + } + + /** 使配方缓存失效(输入/模具/催化剂变化时调用) */ + private void invalidateRecipeCache() { + this.cachedRecipe = null; + } + + private String getProductName() { + if (pattern == null || (pattern.getOutputs().length == 0)) { + return "Unknown"; + } + + var output = pattern.getOutputs()[0]; + if (output.what() instanceof AEItemKey itemKey) { + return itemKey.getDisplayName().getString(); + } else if (output.what() instanceof AEFluidKey fluidKey) { + return fluidKey.getDisplayName().getString(); + } + + return output.what().getDisplayName().getString(); + } + + private boolean isRecipeValid(AdvancedAlloyFurnaceRecipe recipe) { + if (context.getLevel() == null || pattern == null) { + setWaiting(pattern == null ? STATUS_WAITING_INVALID_PATTERN : STATUS_WAITING_CONDITIONS, ""); + return false; + } + + if (recipe == null) { + setWaiting(STATUS_WAITING_RECIPE, ""); + return false; + } + + if ((pattern.getOutputs().length == 0)) { + setWaiting(STATUS_WAITING_INVALID_PATTERN, ""); + return false; + } + + CraftingTaskContext.TaskAvailability availability = context.getTaskAvailability(recipe); + if (!availability.available()) { + setWaiting(availability.statusKey(), availability.statusDetail()); + return false; + } + + if (!AlloyFurnaceRecipeManager.matchesOutputConstraints( + recipe, AdvancedAlloyFurnacePatternPolicy.outputConstraints(pattern))) { + setWaiting(STATUS_WAITING_OUTPUT_MISMATCH, ""); + return false; + } + + clearWaiting(); + return true; + } + + private void setWaiting(String statusKey, String detail) { + this.waitingStatusKey = statusKey == null || statusKey.isBlank() + ? STATUS_WAITING_CONDITIONS : statusKey; + this.waitingDetail = detail == null ? "" : detail; + } + + private void clearWaiting() { + this.waitingStatusKey = STATUS_PROCESSING; + this.waitingDetail = ""; + } + + private void returnMaterialsToAE() { + if (context.getLevel() == null || context.getLevel().isClientSide) return; + + List itemsToReturn = new ArrayList<>(taskInputItems); + List fluidsToReturn = new ArrayList<>(taskInputFluids); + List keysToReturn = new ArrayList<>(taskInputKeys); + List outputItemsToReturn = new ArrayList<>(pendingOutputItems); + List outputFluidsToReturn = new ArrayList<>(pendingOutputFluids); + List outputKeysToReturn = new ArrayList<>(pendingOutputKeys); + for (CraftingSubTask subTask : queuedSubTasks) { + itemsToReturn.addAll(subTask.items); + fluidsToReturn.addAll(subTask.fluids); + keysToReturn.addAll(subTask.keys); + } + // 已结算但尚未写出的产物同样不能丢,随取消一并返还 + pendingOutputItems.clear(); + pendingOutputFluids.clear(); + pendingOutputKeys.clear(); + awaitingOutputFlush = false; + taskInputItems.clear(); + taskInputFluids.clear(); + taskInputKeys.clear(); + queuedSubTasks.clear(); + + returnMaterials(context, itemsToReturn, fluidsToReturn, keysToReturn); + returnOutputMaterials(context, outputItemsToReturn, outputFluidsToReturn, outputKeysToReturn); + } + + /** 返还尚未转为任务的 AE 原始输入(待启动批次取消时使用) */ + public static void returnInputsToAE(List allInputs, CraftingTaskContext context) { + List items = new ArrayList<>(); + List fluids = new ArrayList<>(); + List keys = new ArrayList<>(); + CraftingAeAmountAccumulator accumulator = new CraftingAeAmountAccumulator(); + for (KeyCounter[] counters : allInputs) { + accumulator.add(counters); + } + for (GenericStack amount : accumulator.segments()) { + storeNormalizedMaterial( + amount, items, fluids, keys, context.supportsLongAeAmounts()); + } + returnMaterials(context, items, fluids, keys); + } + + /** + * 返还材料,与产物一致地尊重“产物返回AE”开关:开关开启时优先写回 AE 网络, + * 其次本地输入槽/流体槽。任何路径都不允许静默丢失 —— + * 物品的剩余部分交给 {@link CraftingTaskContext#handleUnreturnedItem}(多方块核心、被动样板舱 + * 与单方块炉会暂存成 key + 数量逐 tick 重试,只有未覆写的实现才会掉落); + * 流体最后尝试输出流体槽,仍有剩余则进暂存缓冲重试; + * key 类材料(如化学品)没有本地槽位可回退,无视开关直接尝试 AE,失败部分进暂存缓冲。 + */ + private static void returnMaterials(CraftingTaskContext context, List items, List fluids, List keys) { + Level level = context.getLevel(); + if (level == null || level.isClientSide) return; + if (items.isEmpty() && fluids.isEmpty() && keys.isEmpty()) return; + normalizeMaterials(items, fluids, keys, context.supportsLongAeAmounts()); + + FurnaceOutputPort.AeOutput port = context.createAeOutputPort(); + + for (KeyAmount keyToReturn : keys) { + GenericStack remainder; + if (keyToReturn.key instanceof AEItemKey || keyToReturn.key instanceof AEFluidKey) { + remainder = FurnaceOutputPort.outputKeyWithRemainder( + new GenericStack(keyToReturn.key, keyToReturn.amount), + port, + context.getItemHandler(), + context.getInputSlotsStart(), + context.getInputSlotsCount(), + context.getInputFluidTanks(), + context.getFluidTankCount(), + context.getInputChemicalStorage(), + context.getChemicalKeyProvider()); + } else { + long inserted = clampInserted( + context.tryOutputKeyToAE(keyToReturn.key, keyToReturn.amount), keyToReturn.amount); + long remaining = keyToReturn.amount - inserted; + if (remaining > 0L && context.getChemicalKeyProvider().isChemicalKey(keyToReturn.key)) { + remaining -= insertReturnedChemical(context, keyToReturn.key, remaining); + } + remainder = remaining > 0L ? new GenericStack(keyToReturn.key, remaining) : null; + } + if (remainder != null) { + context.stashUnreturnedInput(remainder.what(), remainder.amount()); + } + } + + for (ItemStack stack : items) { + returnItemStack(context, port, stack, + context.getInputSlotsStart(), context.getInputSlotsCount()); + } + + for (FluidStack fluidStack : fluids) { + FluidStack leftover = FurnaceOutputPort.outputFluidWithRemainder(fluidStack, port, + context.getInputFluidTanks(), context.getFluidTankCount()); + if (!leftover.isEmpty()) { + leftover = FurnaceOutputPort.outputFluidWithRemainder(leftover, port, + context.getOutputFluidTanks(), context.getFluidTankCount()); + } + if (!leftover.isEmpty()) { + context.handleUnreturnedFluid(leftover); + } + } + context.markChanged(); + } + + /** + * 返还一个物品堆:正常情况(数量已保证不超过单栈上限)走 AE → 本地槽 → 掉落三级回退; + * 万一拿到超限栈,则改走 AEKey + long 投影,剩余暂存成 key 由管理器逐 tick 重试, + * 避免越界栈进入本地槽或 {@code Containers#dropItemStack}(后者会按 10~30 拆成大量实体)。 + */ + private static void returnItemStack( + CraftingTaskContext context, FurnaceOutputPort.AeOutput port, + ItemStack stack, int slotsStart, int slotsCount) { + if (stack == null || stack.isEmpty()) { + return; + } + AEItemKey key = AEItemKey.of(stack); + if (key != null && stack.getCount() > key.getMaxStackSize()) { + GenericStack remainder = FurnaceOutputPort.outputKeyWithRemainder( + new GenericStack(key, stack.getCount()), port, + context.getItemHandler(), slotsStart, slotsCount, null, 0, null, null); + if (remainder != null) { + context.stashUnreturnedInput(remainder.what(), remainder.amount()); + } + return; + } + ItemStack leftover = FurnaceOutputPort.outputItemWithRemainder(stack, port, + context.getItemHandler(), slotsStart, slotsCount); + if (!leftover.isEmpty()) { + context.handleUnreturnedItem(leftover); + } + } + + /** Returns already-produced outputs without putting chemical products into input slots. */ + private static void returnOutputMaterials(CraftingTaskContext context, List items, + List fluids, List keys) { + Level level = context.getLevel(); + if (level == null || level.isClientSide) return; + if (items.isEmpty() && fluids.isEmpty() && keys.isEmpty()) return; + normalizeMaterials(items, fluids, keys, context.supportsLongAeAmounts()); + + FurnaceOutputPort.AeOutput port = context.createAeOutputPort(); + for (ItemStack stack : items) { + returnItemStack(context, port, stack, + context.getOutputSlotsStart(), context.getOutputSlotsCount()); + } + + for (FluidStack fluidStack : fluids) { + FluidStack leftover = FurnaceOutputPort.outputFluidWithRemainder(fluidStack, port, + context.getOutputFluidTanks(), context.getFluidTankCount()); + if (leftover.isEmpty()) continue; + + AEFluidKey key = AEFluidKey.of(leftover); + if (key != null) { + context.stashUnreturnedOutput(key, leftover.getAmount()); + } else { + context.handleUnreturnedFluid(leftover); + } + } + + for (KeyAmount keyToReturn : keys) { + GenericStack remainder = FurnaceOutputPort.outputKeyWithRemainder( + new GenericStack(keyToReturn.key, keyToReturn.amount), + port, + context.getItemHandler(), + context.getOutputSlotsStart(), + context.getOutputSlotsCount(), + context.getOutputFluidTanks(), + context.getFluidTankCount(), + context.getOutputChemicalStorage(), + context.getChemicalKeyProvider()); + if (remainder != null) { + context.stashUnreturnedOutput(remainder.what(), remainder.amount()); + } + } + context.markChanged(); + } + + private int getRecipeProcessTime(AdvancedAlloyFurnaceRecipe recipe) { + return context.getTaskProcessTime(recipe, context.resolveTaskEffect(recipe)); + } + + public boolean tick() { + progressedLastTick = false; + if (processingComplete) { + return true; + } + if (cancelled || context.getLevel() == null || context.getLevel().isClientSide) { + processingComplete = true; + return true; + } + if (!context.isTaskExecutionEnabled()) return false; + if (!initialized && !initialize()) { + return true; + } + + if (!awaitingOutputFlush) { + AdvancedAlloyFurnaceRecipe recipe = findTaskRecipe(); + ResolvedCatalystEffect resolvedCatalystEffect = getCatalystEffect(recipe); + // 下方会先把状态置回处理中、再视能量改回等待,故记下本 tick 前的状态,用来判断等待状态是否真的变化 + String previousStatusKey = taskProgressRef == null ? null : taskProgressRef.getStatusKey(); + String previousStatusDetail = taskProgressRef == null ? null : taskProgressRef.getStatusDetail(); + + if (progress == 0 && accumulatedEnergy == 0L && !isRecipeValid(recipe)) { + updateWaitingProgress(); + return false; + } else if (taskProgressRef != null && !STATUS_WAITING_ENERGY.equals(waitingStatusKey)) { + taskProgressRef.setStatus(STATUS_PROCESSING, ""); + } + + if (progress < processTime) { + ProgressEnergyStep energyStep = getProgressEnergyStep(recipe, resolvedCatalystEffect); + AlloyFurnaceRecipeExecutor.TickResult tickResult = + AlloyFurnaceRecipeExecutor.consumeProgressEnergy( + context.getEnergyManager(), energyStep.targetEnergy(), + energyStep.progress(), energyStep.duration(), accumulatedEnergy); + accumulatedEnergy += tickResult.energyConsumed(); + if (!tickResult.progressAdvanced()) { + if (tickResult.energyConsumed() > 0L) { + context.markChanged(); + } + setWaiting(STATUS_WAITING_ENERGY, WAITING_ENERGY_DETAIL); + if (taskProgressRef != null) { + boolean statusChanged = !waitingStatusKey.equals(previousStatusKey) + || !waitingDetail.equals(previousStatusDetail); + taskProgressRef.setStatus(waitingStatusKey, waitingDetail); + if (shouldSyncWaiting(statusChanged)) { + context.sendAETaskProgressToClients(); + } + } + return false; + } + + clearWaiting(); + progress++; + progressedLastTick = true; + if (energyStep.resetAfterAdvance() + && energyStep.progress() + 1 >= energyStep.duration()) { + accumulatedEnergy = 0L; + } + context.getTotalAEProgressAtomic().incrementAndGet(); + if (taskProgressRef != null) { + taskProgressRef.setProgress(progress); + } + context.markChanged(); + + progressUpdateCounter++; + if (progressUpdateCounter >= PROGRESS_SYNC_INTERVAL) { + context.sendAETaskProgressToClients(); + progressUpdateCounter = 0; + } + + if (progress < processTime) { + return false; + } + } + + generatePendingOutputs(craftCount); + taskInputItems.clear(); + taskInputFluids.clear(); + taskInputKeys.clear(); + awaitingOutputFlush = true; + } + + // 产物写不下时保留在缓冲中逐 tick 重试,避免静默丢失 + if (!flushPendingOutputs()) { + return false; + } + awaitingOutputFlush = false; + + displayedCraftCount -= craftCount; + updateTaskProgressTotals(); + if (startNextSubTask()) { + context.markChanged(); + context.sendAETaskProgressToClients(); + return false; + } + finishTask(); + return true; + } + + private ProgressEnergyStep getProgressEnergyStep( + @Nullable AdvancedAlloyFurnaceRecipe recipe, + ResolvedCatalystEffect resolvedCatalystEffect) { + long recipeEnergy = recipe != null + ? Math.max(0L, recipe.energy()) + : saturatingMultiply(baseEnergyPerTick, Math.max(1, baseProcessTime)); + long targetEnergy = AlloyFurnaceRecipeExecutor.calculateTargetTotalEnergy( + recipeEnergy, craftCount, resolvedCatalystEffect); + return new ProgressEnergyStep(targetEnergy, progress, processTime, false); + } + + private boolean initialize() { + AdvancedAlloyFurnaceRecipe recipe = prepareTaskRecipe(); + if (recipe == null) { + this.updateWaitingProgress(); + return false; + } + + refreshCatalystLayout(); + recalculateBatchLayout(); + + long totalOutputCount = calculateDisplayedOutputCount(); + taskProgressRef = new AdvancedAlloyFurnaceAeManager.AETaskProgress(getProductName(), processTime, displayedCraftCount, totalOutputCount); + clearWaiting(); + context.getAETaskProgressMap().put(taskId, taskProgressRef); + context.getTotalAEMaxProgressAtomic().addAndGet(processTime); + progressRegistered = true; + initialized = true; + context.markChanged(); + context.sendAETaskProgressToClients(); + return true; + } + + /** + * 登记等待中的任务进度。配方无效或未初始化时每 tick 都会调用, + * 等待状态不变时复用已登记的进度对象,不再重复分配、标脏和发包。 + */ + private void updateWaitingProgress() { + long totalOutputCount = calculateDisplayedOutputCount(); + boolean changed = !isRegisteredWaitingProgress(totalOutputCount); + if (changed) { + taskProgressRef = new AdvancedAlloyFurnaceAeManager.AETaskProgress(getProductName(), 0, 1, displayedCraftCount, totalOutputCount, + waitingStatusKey, waitingDetail); + context.getAETaskProgressMap().put(taskId, taskProgressRef); + context.markChanged(); + } + if (shouldSyncWaiting(changed)) { + context.sendAETaskProgressToClients(); + } + } + + /** 已登记的进度对象是否与本次要登记的等待进度完全一致。 */ + private boolean isRegisteredWaitingProgress(long totalOutputCount) { + AdvancedAlloyFurnaceAeManager.AETaskProgress current = taskProgressRef; + return current != null + && context.getAETaskProgressMap().get(taskId) == current + && current.getProgress() == 0 + && current.getMaxProgress() == 1 + && current.getCraftCount() == displayedCraftCount + && current.getTotalOutputCount() == totalOutputCount + && waitingStatusKey.equals(current.getStatusKey()) + && waitingDetail.equals(current.getStatusDetail()); + } + + /** + * 等待状态的进度同步节流:状态变化时立即发送,不变时每 {@link #WAITING_SYNC_INTERVAL} tick 补发一次。 + */ + private boolean shouldSyncWaiting(boolean changed) { + if (changed || ++waitingSyncCounter >= WAITING_SYNC_INTERVAL) { + waitingSyncCounter = 0; + return true; + } + return false; + } + + private long calculateDisplayedOutputCount() { + long outputCount = 1L; + if (pattern != null && (pattern.getOutputs().length != 0)) { + outputCount = Math.max(1L, pattern.getOutputs()[0].amount()); + } + return saturatingMultiply(outputCount, + calculatePatternOutputRuns(displayedCraftCount, patternOperationsPerPush)); + } + + static long calculatePatternOutputRuns(long operations, long operationsPerPattern) { + long safeOperations = Math.max(1L, operations); + long safePatternOperations = Math.max(1L, operationsPerPattern); + return Math.max(1L, safeOperations / safePatternOperations); + } + + private void recalculateBatchLayout() { + long parallel = Math.max(1L, maxParallel); + long batchesLong = 1L + (Math.max(1L, craftCount) - 1L) / parallel; + batches = (int) Math.min(batchesLong, Integer.MAX_VALUE); + long processTimeLong = saturatingMultiply(Math.max(1, baseProcessTime), batchesLong); + processTime = (int) Math.min(processTimeLong, Integer.MAX_VALUE); + lastBatchSize = 1L + (Math.max(1L, craftCount) - 1L) % parallel; + } + + private void updateDisplayedCraftCount() { + this.displayedCraftCount = this.craftCount; + for (CraftingSubTask subTask : queuedSubTasks) { + this.displayedCraftCount = saturatingAdd(this.displayedCraftCount, subTask.craftCount); + } + } + + private void updateTaskProgressTotals() { + if (taskProgressRef != null) { + taskProgressRef.updateCraftCount(displayedCraftCount); + taskProgressRef.setStatus("gui.useless_mod.advanced_alloy_furnace.ae_task_status.processing", ""); + } + } + + private boolean startNextSubTask() { + if (queuedSubTasks.isEmpty()) { + return false; + } + if (progressRegistered) { + context.getTotalAEProgressAtomic().addAndGet(-progress); + context.getTotalAEMaxProgressAtomic().addAndGet(-processTime); + } + CraftingSubTask subTask = queuedSubTasks.remove(0); + taskInputItems.clear(); + taskInputItems.addAll(subTask.items); + taskInputFluids.clear(); + taskInputFluids.addAll(subTask.fluids); + taskInputKeys.clear(); + taskInputKeys.addAll(subTask.keys); + componentInputKeys = resolveComponentInputKeys(); + invalidateRecipeCache(); // 输入已变更,配方缓存失效 + craftCount = subTask.craftCount; + progress = 0; + accumulatedEnergy = 0; + refreshCatalystLayout(); // 催化剂可能在执行期间被更换,按当前催化剂重算批次布局 + recalculateBatchLayout(); + if (progressRegistered) { + context.getTotalAEMaxProgressAtomic().addAndGet(processTime); + } + if (taskProgressRef != null) { + taskProgressRef.setProgress(0); + taskProgressRef.setMaxProgress(processTime); + } + return true; + } + + private ResolvedCatalystEffect getCatalystEffect(AdvancedAlloyFurnaceRecipe recipe) { + return context.resolveTaskEffect(recipe); + } + + /** 按当前催化剂重算时长、每 tick 能耗与并行上限(初始化及每个子任务启动时调用) */ + private void refreshCatalystLayout() { + AdvancedAlloyFurnaceRecipe recipe = findTaskRecipe(); + baseProcessTime = getRecipeProcessTime(recipe); + if (recipe != null && recipe.processTime() > 0) { + baseEnergyPerTick = Math.max(1L, recipe.energy() / Math.max(1, recipe.processTime())); + } else { + baseEnergyPerTick = 200L; + } + maxParallel = context.getTaskParallel(recipe, getCatalystEffect(recipe)); + } + + private void finishTask() { + processingComplete = true; + if (progressRegistered) { + context.getTotalAEProgressAtomic().addAndGet(-progress); + context.getTotalAEMaxProgressAtomic().addAndGet(-processTime); + progressRegistered = false; + } + context.getAETaskProgressMap().remove(taskId); + context.markChanged(); + context.sendAETaskProgressToClients(); + } + + /** + * 生成本批产物到待输出缓冲。超出 int 的数量保持为紧凑 AEKey,避免按数量创建栈对象。 + * 实际写出由 flushPendingOutputs 逐 tick 执行。 + */ + private void generatePendingOutputs(long craftCount) { + if (context.getLevel() == null || context.getLevel().isClientSide) return; + + AdvancedAlloyFurnaceRecipe recipe = findTaskRecipe(); + + if (recipe != null && AdvancedAlloyFurnacePatternPolicy.usesRecipeOutputs(pattern)) { + for (ItemStack output : recipe.outputs()) { + addPendingItemOrKey(output, exactProduct(output.getCount(), craftCount)); + } + for (FluidStack output : recipe.outputFluids()) { + addPendingFluidOrKey(output, exactProduct(output.getAmount(), craftCount)); + } + for (GenericStack output : recipe.keyOutputs()) { + if (output != null && output.what() != null && output.amount() > 0) { + addPendingKey(output.what(), exactProduct(output.amount(), craftCount)); + } + } + context.markChanged(); + return; + } + + long patternOutputRuns = calculatePatternOutputRuns(craftCount, patternOperationsPerPush); + for (var output : pattern.getOutputs()) { + BigInteger amount = exactProduct(output.amount(), patternOutputRuns); + if (output.what() instanceof AEItemKey itemKey) { + addPendingItemOrKey(itemKey.toStack(), amount); + } else if (output.what() instanceof AEFluidKey fluidKey) { + addPendingFluidOrKey(fluidKey.toStack(1), amount); + } else { + addPendingKey(output.what(), amount); + } + } + + if (recipe != null) { + for (GenericStack keyOutput : recipe.keyOutputs()) { + // 样板输出已包含该 key(走上面的 else 分支输出),跳过以避免重复产出 + boolean alreadyInPattern = false; + for (var patternOutput : pattern.getOutputs()) { + if (keyOutput.what().equals(patternOutput.what())) { + alreadyInPattern = true; + break; + } + } + if (!alreadyInPattern) { + addPendingKey(keyOutput.what(), exactProduct(keyOutput.amount(), craftCount)); + } + } + } + context.markChanged(); + } + + private long maximumCraftsPerSubTask() { + if (pattern == null) { + return Long.MAX_VALUE; + } + long safePatternPushes = SmartDoublingPatterns.maximumSafeMultiplier(pattern); + long resolved = multiplyWithinLong(safePatternPushes, Math.max(1L, patternOperationsPerPush)); + return resolved <= 0L ? Long.MAX_VALUE : resolved; + } + + private void addPendingItemOrKey(ItemStack template, BigInteger amount) { + AEItemKey key = AEItemKey.of(template); + if (key == null) { + throw new IllegalStateException("A recipe item output must have an AE item key"); + } + // 判据是"能否装进一个合法栈"(1.21 物品单栈上限 99),而不是 int 上限: + // 超过单栈上限的数量留在 key + 数量表示里,绝不物化成越界 ItemStack。 + if (context.supportsLongAeAmounts() + || amount.compareTo(BigInteger.valueOf(key.getMaxStackSize())) > 0) { + addPendingKey(key, amount); + } else { + pendingOutputItems.add(template.copyWithCount(amount.intValueExact())); + } + } + + private void addPendingFluidOrKey(FluidStack template, BigInteger amount) { + AEFluidKey key = AEFluidKey.of(template); + if (key == null) { + throw new IllegalStateException("A recipe fluid output must have an AE fluid key"); + } + if (context.supportsLongAeAmounts() || amount.compareTo(MAX_INT_AMOUNT) > 0) { + addPendingKey(key, amount); + } else { + pendingOutputFluids.add(new FluidStack(template,amount.intValueExact())); + } + } + + private void addPendingKey(AEKey key, BigInteger amount) { + CraftingAeAmountAccumulator accumulator = new CraftingAeAmountAccumulator(); + addKeyAmounts(accumulator, pendingOutputKeys); + accumulator.add(key, amount); + pendingOutputKeys.clear(); + pendingOutputKeys.addAll(toKeyAmounts(accumulator.segments())); + } + + private static BigInteger exactProduct(long amount, long multiplier) { + if (amount <= 0L || multiplier <= 0L) { + throw new IllegalArgumentException("Crafting output factors must be positive"); + } + return BigInteger.valueOf(amount).multiply(BigInteger.valueOf(multiplier)); + } + + /** + * 尝试把待输出缓冲写入 AE 网络/本地槽位,写不下的部分保留在缓冲中下 tick 重试。 + * + * @return 是否已全部写出 + */ + private boolean flushPendingOutputs() { + FurnaceOutputPort.AeOutput port = context.createAeOutputPort(); + + for (int i = 0; i < pendingOutputItems.size(); ) { + ItemStack leftover = FurnaceOutputPort.outputItemWithRemainder(pendingOutputItems.get(i), port, + context.getItemHandler(), context.getOutputSlotsStart(), context.getOutputSlotsCount()); + if (leftover.isEmpty()) { + pendingOutputItems.remove(i); + } else { + pendingOutputItems.set(i, leftover); + i++; + } + } + + for (int i = 0; i < pendingOutputFluids.size(); ) { + FluidStack leftover = FurnaceOutputPort.outputFluidWithRemainder(pendingOutputFluids.get(i), port, + context.getOutputFluidTanks(), context.getFluidTankCount()); + if (leftover.isEmpty()) { + pendingOutputFluids.remove(i); + } else { + pendingOutputFluids.set(i, leftover); + i++; + } + } + + for (int i = 0; i < pendingOutputKeys.size(); ) { + KeyAmount pending = pendingOutputKeys.get(i); + GenericStack remainder = FurnaceOutputPort.outputKeyWithRemainder( + new GenericStack(pending.key, pending.amount), port, + context.getItemHandler(), + context.getOutputSlotsStart(), + context.getOutputSlotsCount(), + context.getOutputFluidTanks(), + context.getFluidTankCount(), + context.getOutputChemicalStorage(), context.getChemicalKeyProvider()); + if (remainder == null || remainder.amount() <= 0L) { + pendingOutputKeys.remove(i); + } else { + pendingOutputKeys.set(i, new KeyAmount(pending.key, remainder.amount())); + i++; + } + } + + boolean flushed = pendingOutputItems.isEmpty() && pendingOutputFluids.isEmpty() && pendingOutputKeys.isEmpty(); + context.markChanged(); + return flushed; + } + + private static long insertReturnedChemical(CraftingTaskContext context, AEKey key, long amount) { + if (amount <= 0L || !context.getChemicalKeyProvider().isChemicalKey(key)) return 0L; + ChemicalStackView view = context.getChemicalKeyProvider().fromGenericStack(new GenericStack(key, amount)); + if (view == null || view.isEmpty()) return 0L; + + long inserted = insertChemical(context.getInputChemicalStorage(), view); + if (inserted < amount) { + inserted += insertChemical(context.getOutputChemicalStorage(), view.copyWithAmount(amount - inserted)); + } + return inserted; + } + + private static long insertChemical(FurnaceChemicalStorage storage, + ChemicalStackView view) { + if (storage == null || !storage.isAvailable() || view == null || view.isEmpty()) return 0L; + ChemicalStackView remainder = storage.insertChemical(view, false); + return Math.max(0L, view.amount() - remainder.amount()); + } + + private static long clampInserted(long inserted, long requested) { + return Math.max(0L, Math.min(requested, inserted)); + } + + public void cancel() { + if (processingComplete) { + return; + } + this.cancelled = true; + returnMaterialsToAE(); + finishTask(); + } + + // ==================== 持久化 ==================== + + /** + * 将任务序列化为 NBT。 + * 保存输入材料、子任务队列以及当前批次的运行进度,使重载后能从原进度继续。 + */ + public CompoundTag save(HolderLookup.Provider registries) { + CompoundTag tag = new CompoundTag(); + tag.putInt("DataVersion", 2); + tag.putInt("TaskId", taskId); + tag.put("Pattern", pattern.getDefinition().toTag()); + tag.putLong("CraftCount", craftCount); + tag.putLong("PatternOperationsPerPush", patternOperationsPerPush); + tag.putBoolean("PatternOperationsResolved", patternOperationsResolved); + tag.put("Inputs", writeStacks(registries, taskInputItems, taskInputFluids, taskInputKeys)); + + ListTag subTasksTag = new ListTag(); + for (CraftingSubTask subTask : queuedSubTasks) { + CompoundTag subTag = new CompoundTag(); + subTag.putLong("CraftCount", subTask.craftCount); + subTag.put("Inputs", writeStacks(registries, subTask.items, subTask.fluids, subTask.keys)); + subTasksTag.add(subTag); + } + tag.put("SubTasks", subTasksTag); + + // 保存已初始化后的运行进度,使重载后从原进度继续,而非归零重算 + tag.putBoolean("Initialized", initialized); + if (initialized) { + tag.putInt("Progress", progress); + tag.putInt("ProcessTime", processTime); + tag.putInt("BaseProcessTime", baseProcessTime); + tag.putLong("BaseEnergyPerTick", baseEnergyPerTick); + tag.putLong("MaxParallel", maxParallel); + tag.putInt("Batches", batches); + tag.putLong("LastBatchSize", lastBatchSize); + tag.putLong("AccumulatedEnergy", accumulatedEnergy); + tag.putBoolean("AwaitingOutputFlush", awaitingOutputFlush); + tag.put("PendingOutputs", writeStacks(registries, pendingOutputItems, pendingOutputFluids, pendingOutputKeys)); + } + return tag; + } + + /** + * 从 NBT 恢复任务。样板解码失败时返回 null。 + * 若任务在存档前已初始化,则恢复运行进度并重新注册进全局进度统计。 + */ + @Nullable + public static CraftingTask load(CompoundTag tag, Level level, CraftingTaskContext context, HolderLookup.Provider registries) { + AEItemKey definition = AEItemKey.fromTag(tag.getCompound("Pattern")); + if (definition == null) { + return null; + } + IPatternDetails pattern = AdvancedAlloyFurnacePatternResolver.decode(definition.toStack(), level); + if (pattern == null) { + return null; + } + + int taskId = tag.getInt("TaskId"); + CraftingTask task = new CraftingTask(taskId, pattern, context); + task.craftCount = Math.max(1L, tag.getLong("CraftCount")); + task.patternOperationsPerPush = Math.max(1L, tag.getLong("PatternOperationsPerPush")); + task.patternOperationsResolved = tag.getBoolean("PatternOperationsResolved"); + readStacks(registries, tag.getList("Inputs", Tag.TAG_COMPOUND), + task.taskInputItems, task.taskInputFluids, task.taskInputKeys, + context.supportsLongAeAmounts()); + task.componentInputKeys = task.resolveComponentInputKeys(); + + ListTag subTasksTag = tag.getList("SubTasks", Tag.TAG_COMPOUND); + for (int i = 0; i < subTasksTag.size(); i++) { + CompoundTag subTag = subTasksTag.getCompound(i); + List items = new ArrayList<>(); + List fluids = new ArrayList<>(); + List keys = new ArrayList<>(); + readStacks(registries, subTag.getList("Inputs", Tag.TAG_COMPOUND), + items, fluids, keys, context.supportsLongAeAmounts()); + task.queuedSubTasks.add(new CraftingSubTask( + items, fluids, keys, Math.max(1L, subTag.getLong("CraftCount")))); + } + task.updateDisplayedCraftCount(); + + if (tag.getBoolean("Initialized")) { + task.restoreProgress(tag, registries); + } + return task; + } + + /** Returns all resources owned by a task tag when its pattern can no longer be decoded. */ + public static void returnSavedMaterials(CompoundTag tag, CraftingTaskContext context, + HolderLookup.Provider registries) { + List items = new ArrayList<>(); + List fluids = new ArrayList<>(); + List keys = new ArrayList<>(); + List pendingOutputItems = new ArrayList<>(); + List pendingOutputFluids = new ArrayList<>(); + List pendingOutputKeys = new ArrayList<>(); + readStacks(registries, tag.getList("Inputs", Tag.TAG_COMPOUND), + items, fluids, keys, context.supportsLongAeAmounts()); + + ListTag subTasksTag = tag.getList("SubTasks", Tag.TAG_COMPOUND); + for (int index = 0; index < subTasksTag.size(); index++) { + CompoundTag subTask = subTasksTag.getCompound(index); + readStacks(registries, subTask.getList("Inputs", Tag.TAG_COMPOUND), + items, fluids, keys, context.supportsLongAeAmounts()); + } + readStacks(registries, tag.getList("PendingOutputs", Tag.TAG_COMPOUND), + pendingOutputItems, pendingOutputFluids, pendingOutputKeys, context.supportsLongAeAmounts()); + returnMaterials(context, items, fluids, keys); + returnOutputMaterials(context, pendingOutputItems, pendingOutputFluids, pendingOutputKeys); + } + + /** + * 恢复已初始化任务的运行进度,并将进度重新注册进全局进度统计与进度显示对象。 + * 全局 atomic 与 taskProgressRef 不随存档持久化,需要在加载时重建。 + */ + private void restoreProgress(CompoundTag tag, HolderLookup.Provider registries) { + this.baseProcessTime = Math.max(1, tag.getInt("BaseProcessTime")); + this.baseEnergyPerTick = Math.max(1L, tag.getLong("BaseEnergyPerTick")); + this.maxParallel = Math.max(1L, tag.getLong("MaxParallel")); + this.batches = Math.max(1, tag.getInt("Batches")); + this.lastBatchSize = Math.max(1L, tag.getLong("LastBatchSize")); + this.processTime = Math.max(1, tag.getInt("ProcessTime")); + this.progress = Math.max(0, Math.min(tag.getInt("Progress"), this.processTime)); + this.accumulatedEnergy = Math.max(0L, tag.getLong("AccumulatedEnergy")); + this.awaitingOutputFlush = tag.getBoolean("AwaitingOutputFlush"); + readStacks(registries, tag.getList("PendingOutputs", Tag.TAG_COMPOUND), + this.pendingOutputItems, this.pendingOutputFluids, this.pendingOutputKeys, + context.supportsLongAeAmounts()); + + long totalOutputCount = calculateDisplayedOutputCount(); + this.taskProgressRef = new AdvancedAlloyFurnaceAeManager.AETaskProgress(getProductName(), processTime, displayedCraftCount, totalOutputCount); + this.taskProgressRef.setProgress(progress); + context.getAETaskProgressMap().put(taskId, taskProgressRef); + context.getTotalAEProgressAtomic().addAndGet(progress); + context.getTotalAEMaxProgressAtomic().addAndGet(processTime); + this.progressRegistered = true; + this.initialized = true; + } + + private static ListTag writeStacks( + HolderLookup.Provider registries, + List items, + List fluids, + List keys) { + CraftingAeAmountAccumulator accumulator = new CraftingAeAmountAccumulator(); + addMaterials(accumulator, items, fluids, keys); + + ListTag list = new ListTag(); + for (GenericStack amount : accumulator.segments()) { + list.add(GenericStack.writeTag(amount)); + } + return list; + } + + private static void readStacks( + HolderLookup.Provider registries, ListTag list, List items, + List fluids, List keys, boolean keepLongAmounts) { + CraftingAeAmountAccumulator accumulator = new CraftingAeAmountAccumulator(); + for (int i = 0; i < list.size(); i++) { + GenericStack amount = GenericStack.readTag(list.getCompound(i)); + if (amount != null && amount.amount() > 0L) { + accumulator.add(amount); + } + } + for (GenericStack amount : accumulator.segments()) { + storeNormalizedMaterial(amount, items, fluids, keys, keepLongAmounts); + } + } + + // 辅助类用于存储输出数据 + private static long saturatingMultiply(long amount, long multiplier) { + if (amount <= 0L || multiplier <= 0L) { + return 0L; + } + return amount > Long.MAX_VALUE / multiplier ? Long.MAX_VALUE : amount * multiplier; + } + + private static long multiplyWithinLong(long amount, long multiplier) { + if (amount <= 0L || multiplier <= 0L || amount > Long.MAX_VALUE / multiplier) { + return 0L; + } + return amount * multiplier; + } + + static long saturatingAdd(long left, long right) { + if (right > 0L && left > Long.MAX_VALUE - right) { + return Long.MAX_VALUE; + } + return left + right; + } + + private record ProgressEnergyStep( + long targetEnergy, int progress, int duration, boolean resetAfterAdvance) { + } + + private record KeyAmount(AEKey key, long amount) { + } + + private record CraftingSubTask(List items, List fluids, List keys, long craftCount) { + } + + private static void addKeyAmounts( + CraftingAeAmountAccumulator accumulator, + List amounts) { + for (KeyAmount amount : amounts) { + accumulator.add(amount.key, amount.amount); + } + } + + private static void addMaterials( + CraftingAeAmountAccumulator accumulator, + List items, + List fluids, + List keys) { + for (ItemStack stack : items) { + GenericStack amount = GenericStack.fromItemStack(stack); + if (amount == null) { + throw new IllegalStateException("A task item input must have an AE representation"); + } + accumulator.add(amount); + } + for (FluidStack stack : fluids) { + GenericStack amount = GenericStack.fromFluidStack(stack); + if (amount == null) { + throw new IllegalStateException("A task fluid input must have an AE representation"); + } + accumulator.add(amount); + } + addKeyAmounts(accumulator, keys); + } + + private static void normalizeMaterials( + List items, + List fluids, + List keys, + boolean keepLongAmounts) { + CraftingAeAmountAccumulator accumulator = new CraftingAeAmountAccumulator(); + addMaterials(accumulator, items, fluids, keys); + items.clear(); + fluids.clear(); + keys.clear(); + for (GenericStack amount : accumulator.segments()) { + storeNormalizedMaterial(amount, items, fluids, keys, keepLongAmounts); + } + } + + private static List toKeyAmounts(List amounts) { + ArrayList result = new ArrayList<>(amounts.size()); + for (GenericStack amount : amounts) { + result.add(new KeyAmount(amount.what(), amount.amount())); + } + return result; + } + + private static List toGenericStacks(List amounts) { + CraftingAeAmountAccumulator accumulator = new CraftingAeAmountAccumulator(); + addKeyAmounts(accumulator, amounts); + return accumulator.segments(); + } + + private Set resolveComponentInputKeys() { + return AdvancedAlloyFurnacePatternPolicy.componentInputKeys( + pattern, taskInputItems, toGenericStacks(taskInputKeys)); + } + +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/CraftingTaskContext.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/CraftingTaskContext.java new file mode 100644 index 00000000..c3b13f1c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/CraftingTaskContext.java @@ -0,0 +1,302 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import appeng.api.stacks.AEKey; +import appeng.api.crafting.IPatternDetails; +import appeng.api.stacks.GenericStack; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.catalyst.CatalystEffectResolver; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.catalyst.ResolvedCatalystEffect; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.parallel.AlloyFurnaceParallelCalculator; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.core.config.AlloyFurnaceTierRules; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.io.FurnaceOutputPort; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalKeyProvider; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalKeyProviders; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalStackView; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.FurnaceChemicalStorage; +import com.sorrowmist.useless.energy.IEnergyManager; +import appeng.api.networking.IGrid; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.capability.templates.FluidTank; +import net.minecraftforge.items.ItemStackHandler; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; + +/** + * CraftingTask 的上下文接口,提供对 AdvancedAlloyFurnaceBlockEntity 必要成员的访问 + */ +public interface CraftingTaskContext { + + /** 「等待能量」状态文案键:容量上报与任务等待共用,避免两处各自硬编码。 */ + String STATUS_WAITING_ENERGY_KEY = + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_energy"; + + // 槽位常量 + int getInputSlotsStart(); + int getInputSlotsCount(); + int getOutputSlotsStart(); + int getOutputSlotsCount(); + int getCatalystSlot(); + int getMoldSlot(); + int getFluidTankCount(); + + // 基础访问 + Level getLevel(); + net.minecraft.core.BlockPos getBlockPos(); + ItemStackHandler getItemHandler(); + IEnergyManager getEnergyManager(); + + @Nullable + default IGrid getAeGrid() { + return null; + } + + default int getMachineTier() { + return AlloyFurnaceTierRules.NO_MACHINE_TIER; + } + + // 状态更新 + void markChanged(); + void sendAETaskProgressToClients(); + + // 催化剂相关 + int getCatalystMaxParallel(); + + // AE网络输出 + long tryOutputToAE(net.minecraft.world.item.ItemStack stack); + long tryOutputFluidToAE(FluidStack stack); + long tryOutputKeyToAE(AEKey key, long amount); + + default long tryOutputChemicalToAE(ChemicalStackView stack) { + if (stack == null || stack.isEmpty()) return 0L; + GenericStack generic = getChemicalKeyProvider().toGenericStack(stack); + return generic == null ? 0L : tryOutputKeyToAE(generic.what(), generic.amount()); + } + + default FurnaceChemicalStorage getInputChemicalStorage() { + return FurnaceChemicalStorage.DISABLED; + } + + default FurnaceChemicalStorage getOutputChemicalStorage() { + return FurnaceChemicalStorage.DISABLED; + } + + default ChemicalKeyProvider getChemicalKeyProvider() { + return ChemicalKeyProviders.get(); + } + + // 产物输出模式 + boolean isReturnOutputToAe(); + + /** 暂存未能返还的输入(AE 写入失败时防丢失,由管理器逐 tick 重试写回) */ + void stashUnreturnedInput(AEKey key, long amount); + + /** Stores a produced key that could not be returned to AE or an output slot. */ + default void stashUnreturnedOutput(AEKey key, long amount) { + stashUnreturnedInput(key, amount); + } + + /** + * 统一 AE 输出端口:受“产物返回AE”开关约束,开关关闭时不写入 AE 网络, + * 剩余部分由调用方回退到本地槽位。 + */ + default FurnaceOutputPort.AeOutput createAeOutputPort() { + return new FurnaceOutputPort.AeOutput() { + @Override + public long insertItem(ItemStack stack) { + if (!isReturnOutputToAe()) return 0; + return tryOutputToAE(stack); + } + + @Override + public long insertFluid(FluidStack stack) { + if (!isReturnOutputToAe()) return 0; + return tryOutputFluidToAE(stack); + } + + @Override + public long insertKey(AEKey key, long amount) { + if (!isReturnOutputToAe()) return 0; + return tryOutputKeyToAE(key, amount); + } + }; + } + + // 任务进度管理 + ConcurrentHashMap getAETaskProgressMap(); + AtomicInteger getTotalAEMaxProgressAtomic(); + AtomicInteger getTotalAEProgressAtomic(); + + // 锁和流体罐 + ReentrantLock getCraftingLock(); + FluidTank[] getInputFluidTanks(); + FluidTank[] getOutputFluidTanks(); + + default AdvancedAlloyFurnaceRecipe resolveTaskRecipe( + IPatternDetails pattern, List items, List fluids, + List keys, long operations) { + if (pattern != null) { + IPatternDetails original = SmartDoublingPatterns.unwrap(pattern); + if (original instanceof OmniversalPatternDetails omniversal) { + return omniversal.recipe(); + } + } + + ItemStack mold = getItemHandler().getStackInSlot(getMoldSlot()); + return AlloyFurnaceRecipeManager.getInstance().findRecipeForCraftingWithConstraints( + getLevel(), items, fluids, keys, mold, + AdvancedAlloyFurnacePatternPolicy.outputConstraints(pattern), operations, + getMachineTier()); + } + + default boolean isTaskRecipeAvailable(AdvancedAlloyFurnaceRecipe recipe) { + return getTaskAvailability(recipe).available(); + } + + default TaskAvailability getTaskAvailability(AdvancedAlloyFurnaceRecipe recipe) { + if (recipe == null) { + return TaskAvailability.unavailable( + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_recipe", ""); + } + int machineTier = getMachineTier(); + int requiredTier = AlloyFurnaceTierRules.requiredTier(recipe.id(), recipe.tier()); + if (machineTier != AlloyFurnaceTierRules.NO_MACHINE_TIER && machineTier < requiredTier) { + return TaskAvailability.unavailable( + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_tier", ""); + } + if (recipe.molds().isEmpty()) return TaskAvailability.ready(); + if (recipe.molds().size() > 1) { + return TaskAvailability.unavailable( + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_missing_mold", + describeRequiredMolds(recipe.molds())); + } + ItemStackHandler itemHandler = getItemHandler(); + int moldSlot = getMoldSlot(); + if (itemHandler == null || moldSlot < 0 || moldSlot >= itemHandler.getSlots()) { + return TaskAvailability.unavailable( + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_missing_mold", + describeRequiredMolds(recipe.molds())); + } + ItemStack mold = itemHandler.getStackInSlot(moldSlot); + return AdapterUtils.matchesMold(recipe.molds().get(0), mold) + ? TaskAvailability.ready() + : TaskAvailability.unavailable( + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_missing_mold", + describeRequiredMolds(recipe.molds())); + } + + static String describeRequiredMold(Ingredient ingredient) { + if (ingredient == null || ingredient.isEmpty()) return ""; + for (ItemStack candidate : ingredient.getItems()) { + if (!candidate.isEmpty()) return candidate.getHoverName().getString(); + } + return ""; + } + + static String describeRequiredMolds(List ingredients) { + if (ingredients == null || ingredients.isEmpty()) return ""; + List descriptions = new ArrayList<>(); + for (Ingredient ingredient : ingredients) { + String description = describeRequiredMold(ingredient); + if (!description.isEmpty()) descriptions.add(description); + } + return String.join(", ", descriptions); + } + + record TaskAvailability(boolean available, String statusKey, String statusDetail) { + public TaskAvailability { + statusKey = statusKey == null ? "" : statusKey; + statusDetail = statusDetail == null ? "" : statusDetail; + } + + public static TaskAvailability ready() { + return new TaskAvailability(true, "", ""); + } + + public static TaskAvailability unavailable(String statusKey, String statusDetail) { + return new TaskAvailability(false, statusKey, statusDetail); + } + } + + default ResolvedCatalystEffect resolveTaskEffect(AdvancedAlloyFurnaceRecipe recipe) { + int baseTime = recipe == null ? 200 : Math.max(1, recipe.processTime()); + return CatalystEffectResolver.resolve(recipe, getItemHandler().getStackInSlot(getCatalystSlot()), baseTime); + } + + default int getTaskProcessTime(AdvancedAlloyFurnaceRecipe recipe, ResolvedCatalystEffect effect) { + return effect == null ? Math.max(1, recipe == null ? 200 : recipe.processTime()) + : Math.max(1, effect.processTime()); + } + + default long getTaskParallel(AdvancedAlloyFurnaceRecipe recipe, ResolvedCatalystEffect effect) { + return recipe == null || effect == null ? 1 + : AlloyFurnaceParallelCalculator.calculateAeTaskParallel(recipe, effect); + } + + /** Maximum number of crafting-pattern operations one furnace thread may accept in one push. */ + default long getCraftingPatternCapacity() { + return 1L; + } + + /** Long-count AE contexts keep item and fluid amounts as AE keys instead of int-sized stacks. */ + default boolean supportsLongAeAmounts() { + return false; + } + + /** + * 多方块核心返回 {@code true}:本机支持把万象样板折叠成 BigInteger 批次,并按 count 一次性计费。 + * + *

单方块高级合金炉保持 {@code false} —— 它的万象样板仍走长版 counted 路径。这个能力位同时被 + * Data Energistics 适配器与对外公开 API 用来决定「是否给万象样板发布 machine identity」, + * 因此**不能**与 {@link #supportsLongAeAmounts()} 混用(后者单方块与多方块都是 true,无法区分)。

+ * + *

为什么需要它:DE 的 exact 分支一旦接管某个 provider 就不会再回落长版路径,所以必须能 + * 精确判断「本机是否真的实现了 bigint 语义」,否则会给没有 bigint 能力的机器发布 machine identity。

+ */ + default boolean supportsBigIntegerRecipeBatches() { + return false; + } + + /** + * 本 tick 允许单个批次产生的产物分段数(AIMD 控制器的输出)。 + * + *

调用方约定:把它当作「本批最多产生多少段」传进容量计算 + * ({@code AlloyFurnaceBigIntegerCrafting.maximumSegmentedCount})。

+ * + *

为什么由机器给出而不是调用方自己定:单批该多大取决于本机的交付能力 + * (回网每 tick 能插多少次)与调度侧的派发频率,两者都只有机器自己测得到。 + * 机器用 TCP 拥塞控制那套(慢启动 / 乘法减小 / 线性回升)探测出合适值, + * 收敛到「一批大约一两个 tick 交付完」—— 那是最平滑的形态。

+ * + *

实现方必须保证返回值 ≥ 1:容量不会因积压归零,调度侧才能持续派发而无需停顿重启。

+ */ + default long outputSegmentBudget() { + return 1L; + } + + default boolean isTaskExecutionEnabled() { + return true; + } + + default void handleUnreturnedItem(ItemStack stack) { + if (stack == null || stack.isEmpty() || getLevel() == null) return; + var pos = getBlockPos(); + net.minecraft.world.Containers.dropItemStack( + getLevel(), pos.getX() + 0.5D, pos.getY() + 1.0D, pos.getZ() + 0.5D, stack); + } + + default void handleUnreturnedFluid(FluidStack stack) { + if (stack == null || stack.isEmpty()) return; + var key = appeng.api.stacks.AEFluidKey.of(stack); + if (key != null) stashUnreturnedInput(key, stack.getAmount()); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/DynamicComponentPattern.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/DynamicComponentPattern.java new file mode 100644 index 00000000..c1e4265b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/DynamicComponentPattern.java @@ -0,0 +1,22 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import appeng.api.crafting.IPatternDetails; + +/** Marker and policy access used by the local CPU integration. */ +public interface DynamicComponentPattern extends IPatternDetails { + String dynamicPatternIdentity(); + + boolean isItemIdInput(int slot); + + default boolean isTagInput(int slot) { + return false; + } + + default boolean isFluidTagInput(int slot) { + return false; + } + + boolean isItemIdOutput(int slot); + + boolean usesDynamicOutputs(); +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/DynamicComponentPatternDetails.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/DynamicComponentPatternDetails.java new file mode 100644 index 00000000..2c290981 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/DynamicComponentPatternDetails.java @@ -0,0 +1,709 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import appeng.api.stacks.KeyCounter; +import appeng.crafting.pattern.AEProcessingPattern; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.mojang.serialization.JsonOps; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.tags.TagKey; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluid; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HexFormat; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; + +/** + * Local AE pattern view for recipes whose output may carry runtime components. + * Only the explicitly marked slots use item-id matching; all other slots retain + * the source AE2 semantics. + */ +public class DynamicComponentPatternDetails extends AEProcessingPattern implements DynamicComponentPattern { + private final AEItemKey definition; + private final AEProcessingPattern source; + private final IInput[] inputs; + private final List outputs; + private final boolean[] itemIdInputs; + private final Map>> tagInputTags; + private final Map>> fluidTagInputTags; + private final boolean[] itemIdOutputs; + private final boolean hasInputMatchers; + private final String identity; + + @FunctionalInterface + public interface InputMatcher { + boolean test(AEKey input); + } + + public DynamicComponentPatternDetails( + AEProcessingPattern source, + Iterable itemIdInputSlots, + Iterable itemIdOutputSlots, + HolderLookup.Provider registries) { + this(source, itemIdInputSlots, itemIdOutputSlots, Map.of(), Map.of(), Map.of(), Map.of(), registries); + } + + public DynamicComponentPatternDetails( + AEProcessingPattern source, + Iterable itemIdInputSlots, + Iterable itemIdOutputSlots, + Map inputMatchers, + HolderLookup.Provider registries) { + this(source, itemIdInputSlots, itemIdOutputSlots, Map.of(), Map.of(), inputMatchers, Map.of(), registries); + } + + public DynamicComponentPatternDetails( + AEProcessingPattern source, + Iterable itemIdInputSlots, + Iterable itemIdOutputSlots, + Map>> tagInputTags, + Map inputMatchers, + HolderLookup.Provider registries) { + this(source, itemIdInputSlots, itemIdOutputSlots, tagInputTags, Map.of(), inputMatchers, Map.of(), registries); + } + + public DynamicComponentPatternDetails( + AEProcessingPattern source, + Iterable itemIdInputSlots, + Iterable itemIdOutputSlots, + Map>> tagInputTags, + Map>> fluidTagInputTags, + Map inputMatchers, + Map fluidInputMatchers, + HolderLookup.Provider registries) { + super(source.getDefinition()); + this.source = Objects.requireNonNull(source, "source"); + this.definition = source.getDefinition(); + this.outputs = List.copyOf(Arrays.asList(source.getOutputs())); + + IInput[] sourceInputs = source.getInputs(); + this.inputs = new IInput[sourceInputs.length]; + this.itemIdInputs = new boolean[sourceInputs.length]; + this.itemIdOutputs = new boolean[this.outputs.size()]; + this.tagInputTags = normalizeTagInputTags(tagInputTags, sourceInputs.length); + this.fluidTagInputTags = normalizeFluidTagInputTags(fluidTagInputTags, sourceInputs.length); + InputMatcher[] matchers = new InputMatcher[sourceInputs.length]; + InputMatcher[] fluidMatchers = new InputMatcher[sourceInputs.length]; + if (inputMatchers != null) { + for (Map.Entry entry : inputMatchers.entrySet()) { + Integer slot = entry.getKey(); + if (slot == null || slot < 0 || slot >= matchers.length) { + throw new IllegalArgumentException( + "Input matcher slot is outside the processing pattern: " + slot); + } + matchers[slot] = Objects.requireNonNull(entry.getValue(), "input matcher"); + } + } + if (fluidInputMatchers != null) { + for (Map.Entry entry : fluidInputMatchers.entrySet()) { + Integer slot = entry.getKey(); + if (slot == null || slot < 0 || slot >= fluidMatchers.length) { + throw new IllegalArgumentException( + "Fluid input matcher slot is outside the processing pattern: " + slot); + } + fluidMatchers[slot] = Objects.requireNonNull(entry.getValue(), "fluid input matcher"); + } + } + for (int slot = 0; slot < sourceInputs.length; slot++) { + this.inputs[slot] = sourceInputs[slot]; + } + + markSlots(this.itemIdInputs, itemIdInputSlots, "input"); + markSlots(this.itemIdOutputs, itemIdOutputSlots, "output"); + for (int slot : this.tagInputTags.keySet()) { + this.itemIdInputs[slot] = true; + } + this.hasInputMatchers = Arrays.stream(matchers).anyMatch(Objects::nonNull) + || Arrays.stream(fluidMatchers).anyMatch(Objects::nonNull) + || !this.tagInputTags.isEmpty() + || !this.fluidTagInputTags.isEmpty(); + for (int slot = 0; slot < this.inputs.length; slot++) { + if (this.itemIdInputs[slot]) { + this.inputs[slot] = new ItemIdInput(this.inputs[slot], this.tagInputTags.get(slot), matchers[slot]); + } else if (this.fluidTagInputTags.containsKey(slot)) { + this.inputs[slot] = new FluidTagInput(this.inputs[slot], + this.fluidTagInputTags.get(slot), fluidMatchers[slot]); + } + } + + String mode = Arrays.toString(this.itemIdInputs) + "/" + this.tagInputTags + + "/" + this.fluidTagInputTags + "/" + Arrays.toString(this.itemIdOutputs); + this.identity = "useless_mod:dynamic_component|modes=" + mode + + "|definition_sha256=" + definitionFingerprint(definition, registries); + } + + @Override + public AEItemKey getDefinition() { + return definition; + } + + @Override + public IInput[] getInputs() { + return inputs.clone(); + } + + @Override + public GenericStack[] getOutputs() { + return outputs.toArray(GenericStack[]::new); + } + + @Override + public boolean supportsPushInputsToExternalInventory() { + return source.supportsPushInputsToExternalInventory(); + } + + @Override + public void pushInputsToExternalInventory(KeyCounter[] inputHolder, PatternInputSink inputSink) { + if (inputHolder == null) { + return; + } + + // Native sparse arrays may contain null slots; do not use List.copyOf here. + List sparseInputs = Arrays.asList(source.getSparseInputs()); + if (sparseInputs.size() == inputs.length && !hasInputMatchers) { + for (KeyCounter counter : inputHolder) { + if (counter == null) continue; + for (var entry : counter) { + inputSink.pushInput(entry.getKey(), entry.getLongValue()); + } + } + return; + } + + pushSparseInputs(sparseInputs, inputHolder, inputSink); + } + + private void pushSparseInputs(List sparseInputs, + KeyCounter[] inputHolder, + PatternInputSink inputSink) { + List demands = new ArrayList<>(); + IPatternDetails.IInput[] sourceInputs = source.getInputs(); + for (GenericStack sparseInput : sparseInputs) { + if (sparseInput == null) { + continue; + } + AEKey expectedKey = sparseInput.what(); + if (expectedKey == null || sparseInput.amount() <= 0L) { + throw new IllegalStateException("Processing pattern contains an invalid sparse input"); + } + + int sourceSlot = sourceInputSlot(sourceInputs, expectedKey); + IInput dynamicInput = sourceSlot >= 0 && sourceSlot < inputs.length + ? dynamicInput(inputs[sourceSlot]) : null; + demands.add(new PushDemand(expectedKey, sparseInput.amount(), dynamicInput)); + } + + List supplies = new ArrayList<>(); + KeyCounter availableInputs = new KeyCounter(); + for (KeyCounter counter : inputHolder) { + if (counter != null) { + availableInputs.addAll(counter); + } + } + for (var entry : availableInputs) { + if (entry.getKey() != null && entry.getLongValue() > 0L) { + supplies.add(new PushSupply(entry.getKey(), entry.getLongValue())); + } + } + + PushAllocation allocation = allocatePushInputs(supplies, demands); + if (allocation == null) { + throw new IllegalStateException("Selected inputs cannot satisfy the dynamic processing pattern"); + } + + for (int demandIndex = 0; demandIndex < demands.size(); demandIndex++) { + for (PushEdge allocationEdge : allocation.edges().get(demandIndex)) { + long amount = allocationEdge.initialCapacity() - allocationEdge.edge().capacity; + if (amount > 0L) { + inputSink.pushInput(allocationEdge.key(), amount); + } + } + } + } + + @Nullable + private static IInput dynamicInput(IInput input) { + return input instanceof ItemIdInput || input instanceof FluidTagInput ? input : null; + } + + private static int sourceInputSlot(IPatternDetails.IInput[] sourceInputs, AEKey expectedKey) { + for (int slot = 0; slot < sourceInputs.length; slot++) { + IPatternDetails.IInput input = sourceInputs[slot]; + if (input == null) continue; + for (GenericStack possible : input.getPossibleInputs()) { + if (possible != null && expectedKey.equals(possible.what())) { + return slot; + } + } + } + for (int slot = 0; slot < sourceInputs.length; slot++) { + IPatternDetails.IInput input = sourceInputs[slot]; + if (input != null && input.isValid(expectedKey, null)) { + return slot; + } + } + return -1; + } + + @Nullable + private static PushAllocation allocatePushInputs(List supplies, List demands) { + if (demands.isEmpty()) { + return new PushAllocation(List.of()); + } + if (supplies.isEmpty()) { + return null; + } + + long totalDemand = 0L; + for (PushDemand demand : demands) { + totalDemand = saturatingAdd(totalDemand, demand.amount()); + } + long totalSupply = 0L; + for (PushSupply supply : supplies) { + totalSupply = saturatingAdd(totalSupply, supply.amount()); + } + if (totalSupply < totalDemand) { + return null; + } + + int sourceNode = 0; + int supplyStart = 1; + int demandStart = supplyStart + supplies.size(); + int sinkNode = demandStart + demands.size(); + PushFlowNetwork network = new PushFlowNetwork(sinkNode + 1); + List> allocationEdges = new ArrayList<>(demands.size()); + for (int supplyIndex = 0; supplyIndex < supplies.size(); supplyIndex++) { + network.addEdge(sourceNode, supplyStart + supplyIndex, supplies.get(supplyIndex).amount()); + } + for (int demandIndex = 0; demandIndex < demands.size(); demandIndex++) { + PushDemand demand = demands.get(demandIndex); + List edges = new ArrayList<>(); + for (int supplyIndex = 0; supplyIndex < supplies.size(); supplyIndex++) { + PushSupply supply = supplies.get(supplyIndex); + if (demand.accepts(supply.key())) { + PushFlowEdge edge = network.addEdge( + supplyStart + supplyIndex, demandStart + demandIndex, totalDemand); + edges.add(new PushEdge(supply.key(), edge, totalDemand)); + } + } + allocationEdges.add(edges); + network.addEdge(demandStart + demandIndex, sinkNode, demand.amount()); + } + + if (network.maxFlow(sourceNode, sinkNode, totalDemand) != totalDemand) { + return null; + } + return new PushAllocation(allocationEdges); + } + + private static long saturatingAdd(long left, long right) { + if (right > 0L && left > Long.MAX_VALUE - right) return Long.MAX_VALUE; + return left + right; + } + + private record PushSupply(AEKey key, long amount) { + } + + private record PushDemand(AEKey expectedKey, long amount, @Nullable IInput dynamicInput) { + private boolean accepts(AEKey key) { + return dynamicInput != null ? dynamicInput.isValid(key, null) : expectedKey.equals(key); + } + } + + private record PushEdge(AEKey key, PushFlowEdge edge, long initialCapacity) { + } + + private record PushAllocation(List> edges) { + } + + private static final class PushFlowNetwork { + private final List> graph; + private final int[] levels; + private final int[] nextEdges; + + private PushFlowNetwork(int nodeCount) { + graph = new ArrayList<>(nodeCount); + for (int i = 0; i < nodeCount; i++) graph.add(new ArrayList<>()); + levels = new int[nodeCount]; + nextEdges = new int[nodeCount]; + } + + private PushFlowEdge addEdge(int from, int to, long capacity) { + PushFlowEdge forward = new PushFlowEdge(to, graph.get(to).size(), capacity); + PushFlowEdge reverse = new PushFlowEdge(from, graph.get(from).size(), 0L); + graph.get(from).add(forward); + graph.get(to).add(reverse); + return forward; + } + + private long maxFlow(int source, int sink, long limit) { + long flow = 0L; + while (flow < limit && buildLevels(source, sink)) { + Arrays.fill(nextEdges, 0); + long pushed; + while (flow < limit && (pushed = push(source, sink, limit - flow)) > 0L) { + flow += pushed; + } + } + return flow; + } + + private boolean buildLevels(int source, int sink) { + Arrays.fill(levels, -1); + levels[source] = 0; + ArrayDeque queue = new ArrayDeque<>(); + queue.add(source); + while (!queue.isEmpty()) { + int node = queue.removeFirst(); + for (PushFlowEdge edge : graph.get(node)) { + if (edge.capacity > 0L && levels[edge.to] < 0) { + levels[edge.to] = levels[node] + 1; + queue.addLast(edge.to); + } + } + } + return levels[sink] >= 0; + } + + private long push(int node, int sink, long amount) { + if (node == sink) return amount; + List edges = graph.get(node); + for (; nextEdges[node] < edges.size(); nextEdges[node]++) { + PushFlowEdge edge = edges.get(nextEdges[node]); + if (edge.capacity <= 0L || levels[edge.to] != levels[node] + 1) continue; + long pushed = push(edge.to, sink, Math.min(amount, edge.capacity)); + if (pushed <= 0L) continue; + edge.capacity -= pushed; + graph.get(edge.to).get(edge.reverseIndex).capacity += pushed; + return pushed; + } + return 0L; + } + } + + private static final class PushFlowEdge { + private final int to; + private final int reverseIndex; + private long capacity; + + private PushFlowEdge(int to, int reverseIndex, long capacity) { + this.to = to; + this.reverseIndex = reverseIndex; + this.capacity = capacity; + } + } + + @Override + public String dynamicPatternIdentity() { + return identity; + } + + @Override + public boolean isItemIdInput(int slot) { + return slot >= 0 && slot < itemIdInputs.length && itemIdInputs[slot]; + } + + @Override + public boolean isTagInput(int slot) { + return tagInputTags.containsKey(slot); + } + + @Override + public boolean isFluidTagInput(int slot) { + return fluidTagInputTags.containsKey(slot); + } + + @Override + public boolean isItemIdOutput(int slot) { + return slot >= 0 && slot < itemIdOutputs.length && itemIdOutputs[slot]; + } + + @Override + public boolean usesDynamicOutputs() { + for (boolean itemIdOutput : itemIdOutputs) { + if (itemIdOutput) { + return true; + } + } + return false; + } + + @Override + public boolean equals(Object obj) { + return this == obj + || obj instanceof DynamicComponentPatternDetails other + && definition.equals(other.definition) + && Arrays.equals(itemIdInputs, other.itemIdInputs) + && tagInputTags.equals(other.tagInputTags) + && fluidTagInputTags.equals(other.fluidTagInputTags) + && Arrays.equals(itemIdOutputs, other.itemIdOutputs); + } + + @Override + public int hashCode() { + int result = definition.hashCode(); + result = 31 * result + Arrays.hashCode(itemIdInputs); + result = 31 * result + tagInputTags.hashCode(); + result = 31 * result + fluidTagInputTags.hashCode(); + return 31 * result + Arrays.hashCode(itemIdOutputs); + } + + static String definitionFingerprint(AEItemKey definition, HolderLookup.Provider registries) { + Objects.requireNonNull(definition, "definition"); + Objects.requireNonNull(registries, "registries"); + JsonElement encoded = com.sorrowmist.useless.compat.forge1201.recipe.AE2Codecs.ITEM_KEY.encodeStart( + net.minecraft.resources.RegistryOps.create(JsonOps.INSTANCE, registries), definition).getOrThrow(false, message -> { throw new IllegalArgumentException(message); }); + byte[] canonical = canonicalize(encoded).toString().getBytes(StandardCharsets.UTF_8); + try { + return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(canonical)); + } catch (NoSuchAlgorithmException exception) { + throw new IllegalStateException("SHA-256 is unavailable", exception); + } + } + + private static void markSlots(boolean[] target, Iterable slots, String side) { + if (slots == null) { + return; + } + for (Integer slot : slots) { + if (slot == null || slot < 0 || slot >= target.length) { + throw new IllegalArgumentException("Dynamic " + side + " slot is outside the pattern: " + slot); + } + target[slot] = true; + } + } + + private static Map>> normalizeTagInputTags( + Map>> source, int inputCount) { + if (source == null || source.isEmpty()) return Map.of(); + Map>> result = new java.util.TreeMap<>(); + for (Map.Entry>> entry : source.entrySet()) { + Integer slot = entry.getKey(); + if (slot == null || slot < 0 || slot >= inputCount) { + throw new IllegalArgumentException("Tag input slot is outside the processing pattern: " + slot); + } + java.util.LinkedHashSet> tags = new java.util.LinkedHashSet<>(); + Iterable> values = entry.getValue(); + if (values != null) { + for (TagKey tag : values) { + if (tag != null) tags.add(tag); + } + } + if (!tags.isEmpty()) result.put(slot, List.copyOf(tags)); + } + return Map.copyOf(result); + } + + private static Map>> normalizeFluidTagInputTags( + Map>> source, int inputCount) { + if (source == null || source.isEmpty()) return Map.of(); + Map>> result = new java.util.TreeMap<>(); + for (Map.Entry>> entry : source.entrySet()) { + Integer slot = entry.getKey(); + if (slot == null || slot < 0 || slot >= inputCount) { + throw new IllegalArgumentException("Fluid tag input slot is outside the processing pattern: " + slot); + } + java.util.LinkedHashSet> tags = new java.util.LinkedHashSet<>(); + Iterable> values = entry.getValue(); + if (values != null) { + for (TagKey tag : values) { + if (tag != null) tags.add(tag); + } + } + if (!tags.isEmpty()) result.put(slot, List.copyOf(tags)); + } + return Map.copyOf(result); + } + + private static JsonElement canonicalize(JsonElement element) { + if (element.isJsonObject()) { + TreeMap sorted = new TreeMap<>(); + for (var entry : element.getAsJsonObject().entrySet()) { + sorted.put(entry.getKey(), entry.getValue()); + } + JsonObject result = new JsonObject(); + for (var entry : sorted.entrySet()) { + result.add(entry.getKey(), canonicalize(entry.getValue())); + } + return result; + } + if (element.isJsonArray()) { + JsonArray result = new JsonArray(); + for (JsonElement child : element.getAsJsonArray()) { + result.add(canonicalize(child)); + } + return result; + } + return element.deepCopy(); + } + + private static final class ItemIdInput implements IInput { + private final IInput source; + private final GenericStack[] possibleInputs; + private final List> tags; + @Nullable + private final InputMatcher matcher; + + private ItemIdInput(IInput source, List> tags, @Nullable InputMatcher matcher) { + this.source = Objects.requireNonNull(source, "source"); + this.possibleInputs = source.getPossibleInputs().clone(); + this.tags = tags == null ? List.of() : tags; + this.matcher = matcher; + } + + private boolean isTagInput() { + return !tags.isEmpty(); + } + + @Override + public GenericStack[] getPossibleInputs() { + return possibleInputs.clone(); + } + + @Override + public long getMultiplier() { + return source.getMultiplier(); + } + + @Override + public boolean isValid(AEKey input, Level level) { + if (!(input instanceof AEItemKey itemKey)) { + return false; + } + if (!tags.isEmpty() && tags.stream().noneMatch(tag -> isTagMember(tag, itemKey.getItem()))) { + return false; + } + if (!tags.isEmpty()) { + return matcher == null || matcher.test(input); + } + if (matcher != null) { + return matcher.test(input); + } + for (GenericStack possible : possibleInputs) { + if (possible != null && possible.what() instanceof AEItemKey possibleItem + && possibleItem.getItem() == itemKey.getItem()) { + return true; + } + } + return false; + } + + private boolean isValidWithoutLevel(AEKey input) { + return isValid(input, null); + } + + @Override + @Nullable + public AEKey getRemainingKey(AEKey template) { + if (!isValidWithoutLevel(template)) { + return null; + } + AEKey direct = source.getRemainingKey(template); + if (direct != null) { + return direct; + } + if (template instanceof AEItemKey itemKey) { + for (GenericStack possible : possibleInputs) { + if (possible != null && possible.what() instanceof AEItemKey possibleItem + && possibleItem.getItem() == itemKey.getItem()) { + AEKey remaining = source.getRemainingKey(possible.what()); + if (remaining != null) { + return remaining; + } + } + } + } + return null; + } + + private static boolean isTagMember(TagKey tag, Item item) { + for (var holder : BuiltInRegistries.ITEM.getTagOrEmpty(tag)) { + if (holder.value() == item) return true; + } + return false; + } + } + + private static final class FluidTagInput implements IInput { + private final IInput source; + private final GenericStack[] possibleInputs; + private final List> tags; + @Nullable + private final InputMatcher matcher; + + private FluidTagInput(IInput source, List> tags, @Nullable InputMatcher matcher) { + this.source = Objects.requireNonNull(source, "source"); + this.possibleInputs = source.getPossibleInputs().clone(); + this.tags = tags == null ? List.of() : tags; + this.matcher = matcher; + } + + @Override + public GenericStack[] getPossibleInputs() { + return possibleInputs.clone(); + } + + @Override + public long getMultiplier() { + return source.getMultiplier(); + } + + @Override + public boolean isValid(AEKey input, Level level) { + if (!(input instanceof appeng.api.stacks.AEFluidKey fluidKey)) return false; + FluidStack stack = fluidKey.toStack(1); + if (tags.stream().noneMatch(stack.getFluid()::is)) return false; + return matcher == null || matcher.test(input); + } + + private boolean isValidWithoutLevel(AEKey input) { + return isValid(input, null); + } + + @Override + @Nullable + public AEKey getRemainingKey(AEKey template) { + if (!isValidWithoutLevel(template)) return null; + AEKey direct = source.getRemainingKey(template); + if (direct != null) return direct; + AEKey uniform = null; + boolean found = false; + for (GenericStack possible : possibleInputs) { + if (possible == null || !(possible.what() instanceof appeng.api.stacks.AEFluidKey)) { + continue; + } + AEKey remaining = source.getRemainingKey(possible.what()); + if (remaining == null) { + // A null remainder is also a meaningful candidate result. It cannot be + // combined with a non-null remainder for a dynamic fluid slot. + return null; + } + if (!found) { + uniform = remaining; + found = true; + } else if (!uniform.equals(remaining)) { + return null; + } + } + return found ? uniform : null; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/DynamicPatternCpuStateManager.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/DynamicPatternCpuStateManager.java new file mode 100644 index 00000000..3906fd5f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/DynamicPatternCpuStateManager.java @@ -0,0 +1,661 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import appeng.api.config.Actionable; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.WeakHashMap; + +/** CPU-side state for outputs whose components are determined at execution time. */ +public final class DynamicPatternCpuStateManager { + public static final DynamicPatternCpuStateManager INSTANCE = new DynamicPatternCpuStateManager(); + public static final String NBT_KEY = "uselessModDynamicComponentState"; + private static final String ECO_CPU_LOGIC_CLASS = + "cn.dancingsnow.neoecoae.api.me.ECOCraftingCPULogic"; + + private final Map states = new WeakHashMap<>(); + private final Map ecoStates = new LinkedHashMap<>(); + private final Map> stagedEcoOwners = new HashMap<>(); + + private DynamicPatternCpuStateManager() { + } + + public synchronized boolean hasAnyPending(Object logic) { + Objects.requireNonNull(logic, "logic"); + CpuState state = states.get(logic); + return state != null && !state.isEmpty(); + } + + public synchronized boolean hasAmbiguousOutputRegistration( + Object logic, DynamicComponentPattern pattern) { + Objects.requireNonNull(logic, "logic"); + Objects.requireNonNull(pattern, "pattern"); + CpuState state = states.get(logic); + return state != null && state.hasAmbiguous(pattern); + } + + public synchronized void registerExpectedOutputs( + Object logic, + UUID craftingId, + DynamicComponentPattern pattern, + @Nullable AEKey finalOutputKey, + long pushedCopies) { + Objects.requireNonNull(logic, "logic"); + Objects.requireNonNull(craftingId, "craftingId"); + Objects.requireNonNull(pattern, "pattern"); + if (pushedCopies <= 0) { + throw new IllegalArgumentException("pushedCopies must be > 0"); + } + + CpuState state = states.get(logic); + if (state == null || !craftingId.equals(state.craftingId)) { + state = new CpuState(craftingId); + states.put(logic, state); + } + state.register(pattern, finalOutputKey, pushedCopies); + if (ECO_CPU_LOGIC_CLASS.equals(logic.getClass().getName())) { + stagedEcoOwners.computeIfAbsent(craftingId, ignored -> new LinkedHashSet<>()).add(logic); + } + } + + public synchronized ClaimResult claim( + Object logic, AEKey incoming, long amount, Actionable actionable) { + Objects.requireNonNull(logic, "logic"); + Objects.requireNonNull(incoming, "incoming"); + Objects.requireNonNull(actionable, "actionable"); + if (!(incoming instanceof AEItemKey itemKey) || amount <= 0) { + return ClaimResult.EMPTY; + } + + CpuState state = states.get(logic); + if (state == null) { + return ClaimResult.EMPTY; + } + ClaimResult result = state.claim(itemKey.getId(), amount, actionable == Actionable.MODULATE); + if (actionable == Actionable.MODULATE && state.isEmpty()) { + states.remove(logic); + } + return result; + } + + public synchronized void clear(Object logic) { + Objects.requireNonNull(logic, "logic"); + states.remove(logic); + stagedEcoOwners.values().removeIf(owners -> { + owners.remove(logic); + return owners.isEmpty(); + }); + } + + @Nullable + public synchronized CompoundTag writeToTag(Object logic, HolderLookup.Provider registries) { + Objects.requireNonNull(registries, "registries"); + return writeToTag(logic); + } + + /** AE2 15 serializes keys directly into NBT; its CPU has no registry argument. */ + @Nullable + public synchronized CompoundTag writeToTag(Object logic) { + Objects.requireNonNull(logic, "logic"); + CpuState state = states.get(logic); + return state == null || state.isEmpty() ? null : state.toTag(); + } + + public synchronized void readFromTag( + Object logic, UUID craftingId, CompoundTag tag, HolderLookup.Provider registries) { + Objects.requireNonNull(registries, "registries"); + readFromTag(logic, craftingId, tag); + } + + public synchronized void readFromTag(Object logic, UUID craftingId, CompoundTag tag) { + Objects.requireNonNull(logic, "logic"); + Objects.requireNonNull(craftingId, "craftingId"); + Objects.requireNonNull(tag, "tag"); + if (tag.isEmpty()) { + states.remove(logic); + return; + } + states.put(logic, CpuState.fromTag(craftingId, tag)); + } + + public synchronized long getRemainingForItem(Object logic, ResourceLocation itemId) { + CpuState state = states.get(logic); + return state == null ? 0 : state.remainingForItem(itemId); + } + + public synchronized List snapshotPending(Object logic) { + CpuState state = states.get(logic); + return state == null ? List.of() : state.snapshots(); + } + + /** Creates or refreshes the public-API state for one ECO job. */ + public synchronized void ensureEcoJob(UUID craftingId, Object cpu, Object grid) { + Objects.requireNonNull(craftingId, "craftingId"); + EcoState state = ecoStates.get(craftingId); + if (state == null) { + state = new EcoState(craftingId, cpu, grid, new CpuState(craftingId)); + ecoStates.put(craftingId, state); + } else { + state.cpu = cpu; + state.grid = grid; + } + } + + /** Restores one ECO job attachment without exposing ECO's CPU internals. */ + public synchronized void readEcoJob( + UUID craftingId, Object cpu, Object grid, CompoundTag tag, HolderLookup.Provider registries) { + Objects.requireNonNull(craftingId, "craftingId"); + Objects.requireNonNull(tag, "tag"); + Objects.requireNonNull(registries, "registries"); + ecoStates.put(craftingId, new EcoState( + craftingId, cpu, grid, CpuState.fromTag(craftingId, tag))); + } + + @Nullable + public synchronized CompoundTag writeEcoJob(UUID craftingId, HolderLookup.Provider registries) { + Objects.requireNonNull(craftingId, "craftingId"); + Objects.requireNonNull(registries, "registries"); + EcoState state = ecoStates.get(craftingId); + return state == null || state.pending.isEmpty() ? null : state.pending.toTag(); + } + + public synchronized void clearEcoJob(UUID craftingId) { + Objects.requireNonNull(craftingId, "craftingId"); + ecoStates.remove(craftingId); + stagedEcoOwners.remove(craftingId); + states.entrySet().removeIf(entry -> craftingId.equals(entry.getValue().craftingId)); + } + + /** + * Binds a successful ECO dispatch to its job attachment. The legacy staged state is consumed when + * P16's optional fast-path bridge called the compatibility methods below before this event arrived. + */ + public synchronized void bindEcoDispatch( + UUID craftingId, + Object cpu, + Object grid, + DynamicComponentPattern pattern, + @Nullable AEKey finalOutputKey, + long pushedCopies) { + Objects.requireNonNull(craftingId, "craftingId"); + Objects.requireNonNull(pattern, "pattern"); + if (pushedCopies <= 0L) { + throw new IllegalArgumentException("pushedCopies must be > 0"); + } + + EcoState state = ecoStates.get(craftingId); + if (state == null) { + CpuState staged = takeStagedState(craftingId); + state = new EcoState(craftingId, cpu, grid, staged == null ? new CpuState(craftingId) : staged); + state.wasStaged = staged != null; + ecoStates.put(craftingId, state); + } else { + state.cpu = cpu; + state.grid = grid; + CpuState staged = takeStagedState(craftingId); + if (staged != null) { + if (state.pending.isEmpty()) { + state.pending = staged; + } else { + state.pending.mergeFrom(staged); + } + state.wasStaged = true; + } + } + + if (!state.wasStaged) { + state.pending.register(pattern, finalOutputKey, pushedCopies); + } + state.wasStaged = false; + } + + public synchronized boolean hasEcoPendingOnGrid(Object grid) { + if (grid == null) { + return false; + } + for (EcoState state : ecoStates.values()) { + if (state.grid == grid && !state.pending.isEmpty()) { + return true; + } + } + return false; + } + + public synchronized List ecoCandidates(Object grid, ResourceLocation itemId) { + if (grid == null || itemId == null || ecoStates.isEmpty()) { + // 空表提前返回:本方法在产物回网路径上被高频调用(在改为逐键一次之前为每段一次), + // 无 ECO 任务时不应承担集合遍历与 List.copyOf 的分配开销。 + return List.of(); + } + List result = new ArrayList<>(); + for (EcoState state : ecoStates.values()) { + if (state.grid != grid) { + continue; + } + for (PendingSnapshot pending : state.pending.snapshots()) { + if (itemId.equals(pending.itemId()) && pending.remainingAmount() > 0L) { + result.add(new EcoCandidate( + state.craftingId, + state.cpu, + pending.exactExpectedKey(), + pending.remainingAmount())); + } + } + } + return List.copyOf(result); + } + + public synchronized ClaimResult claimEcoExact(UUID craftingId, AEKey expectedKey, long amount) { + Objects.requireNonNull(craftingId, "craftingId"); + Objects.requireNonNull(expectedKey, "expectedKey"); + if (amount <= 0L) { + return ClaimResult.EMPTY; + } + EcoState state = ecoStates.get(craftingId); + return state == null ? ClaimResult.EMPTY : state.pending.claimExact(expectedKey, amount, true); + } + + public record PendingSnapshot( + String patternIdentity, + int outputSlot, + ResourceLocation itemId, + AEKey exactExpectedKey, + long remainingAmount, + boolean routesToRequester, + long registeredOrder) { + } + + public record EcoCandidate( + UUID craftingId, + Object cpu, + AEKey exactExpectedKey, + long remainingAmount) { + } + + public record Claim( + long claimedAmount, + boolean routesToRequester, + AEKey exactExpectedKey) { + public Claim { + if (claimedAmount <= 0) { + throw new IllegalArgumentException("claimedAmount must be > 0"); + } + Objects.requireNonNull(exactExpectedKey, "exactExpectedKey"); + } + } + + public record ClaimResult(long claimedAmount, List claims) { + public static final ClaimResult EMPTY = new ClaimResult(0, List.of()); + + public ClaimResult { + if (claimedAmount < 0) { + throw new IllegalArgumentException("claimedAmount must be >= 0"); + } + claims = List.copyOf(Objects.requireNonNull(claims, "claims")); + } + + public boolean claimedAnything() { + return claimedAmount > 0; + } + + public long claimedForRequester() { + long result = 0; + for (Claim claim : claims) { + if (claim.routesToRequester()) { + result = saturatingAdd(result, claim.claimedAmount()); + } + } + return result; + } + + public long claimedForInventory() { + return claimedAmount - claimedForRequester(); + } + } + + private static final class CpuState { + private static final String TAG_NEXT_SEQUENCE = "NextSequence"; + private static final String TAG_PENDING = "Pending"; + private static final String TAG_PATTERN_IDENTITY = "PatternIdentity"; + private static final String TAG_OUTPUT_SLOT = "OutputSlot"; + private static final String TAG_ITEM_ID = "ItemId"; + private static final String TAG_EXACT_TEMPLATE = "ExactTemplate"; + private static final String TAG_REMAINING = "RemainingAmount"; + private static final String TAG_ROUTES_TO_REQUESTER = "RoutesToRequester"; + private static final String TAG_REGISTERED_ORDER = "RegisteredOrder"; + + private final UUID craftingId; + private final Map pendingByKey = new LinkedHashMap<>(); + private final Map> pendingByItem = new LinkedHashMap<>(); + private long nextSequence = 1L; + + private CpuState(UUID craftingId) { + this.craftingId = craftingId; + } + + private boolean isEmpty() { + return pendingByKey.isEmpty(); + } + + private boolean hasAmbiguous(DynamicComponentPattern pattern) { + Map incoming = new LinkedHashMap<>(); + for (int slot = 0; slot < pattern.getOutputs().length; slot++) { + if (!pattern.isItemIdOutput(slot)) { + continue; + } + GenericStack output = pattern.getOutputs()[slot]; + if (!(output.what() instanceof AEItemKey itemKey)) { + continue; + } + PendingKey key = new PendingKey(pattern.dynamicPatternIdentity(), slot); + PendingKey batchExisting = incoming.putIfAbsent(itemKey.getId(), key); + if (batchExisting != null && !batchExisting.equals(key)) { + return true; + } + for (PendingKey existing : pendingByItem.getOrDefault(itemKey.getId(), new LinkedHashSet<>())) { + if (!existing.equals(key)) { + return true; + } + } + } + return false; + } + + private void register( + DynamicComponentPattern pattern, @Nullable AEKey finalOutputKey, long pushedCopies) { + List outputs = java.util.Arrays.asList(pattern.getOutputs()); + for (int slot = 0; slot < outputs.size(); slot++) { + if (!pattern.isItemIdOutput(slot)) { + continue; + } + GenericStack output = outputs.get(slot); + if (!(output.what() instanceof AEItemKey itemKey) || output.amount() <= 0) { + continue; + } + long amount = saturatingMultiply(output.amount(), pushedCopies); + boolean routesToRequester = finalOutputKey instanceof AEItemKey finalItem + && finalItem.getItem() == itemKey.getItem(); + PendingKey key = new PendingKey(pattern.dynamicPatternIdentity(), slot); + PendingOutput existing = pendingByKey.get(key); + if (existing != null) { + if (!existing.itemId.equals(itemKey.getId()) + || !existing.exactExpectedKey.equals(output.what()) + || existing.routesToRequester != routesToRequester) { + throw new IllegalStateException("Dynamic pattern identity changed its output definition"); + } + existing.addExpected(amount); + continue; + } + + PendingOutput pending = new PendingOutput( + key, + itemKey.getId(), + output.what(), + amount, + routesToRequester, + nextSequence++); + pendingByKey.put(key, pending); + pendingByItem.computeIfAbsent(itemKey.getId(), ignored -> new LinkedHashSet<>()).add(key); + } + } + + private ClaimResult claim(ResourceLocation itemId, long amount, boolean mutate) { + LinkedHashSet keys = pendingByItem.get(itemId); + if (keys == null || keys.isEmpty()) { + return ClaimResult.EMPTY; + } + + long remaining = amount; + List claims = new ArrayList<>(); + List satisfied = new ArrayList<>(); + Collection ordered = keys.stream() + .map(pendingByKey::get) + .filter(Objects::nonNull) + .sorted(Comparator.comparingLong(output -> output.registeredOrder)) + .toList(); + for (PendingOutput pending : ordered) { + if (remaining <= 0) { + break; + } + long claimed = Math.min(pending.remainingAmount, remaining); + if (claimed <= 0) { + continue; + } + if (mutate) { + pending.remainingAmount -= claimed; + if (pending.remainingAmount <= 0) { + satisfied.add(pending); + } + } + claims.add(new Claim(claimed, pending.routesToRequester, pending.exactExpectedKey)); + remaining -= claimed; + } + if (mutate) { + for (PendingOutput pending : satisfied) { + remove(pending); + } + } + long claimed = amount - remaining; + return claimed <= 0 ? ClaimResult.EMPTY : new ClaimResult(claimed, claims); + } + + private void remove(PendingOutput pending) { + pendingByKey.remove(pending.key); + LinkedHashSet keys = pendingByItem.get(pending.itemId); + if (keys != null) { + keys.remove(pending.key); + if (keys.isEmpty()) { + pendingByItem.remove(pending.itemId); + } + } + } + + private long remainingForItem(ResourceLocation itemId) { + long result = 0; + for (PendingKey key : pendingByItem.getOrDefault(itemId, new LinkedHashSet<>())) { + PendingOutput pending = pendingByKey.get(key); + if (pending != null) { + result = saturatingAdd(result, pending.remainingAmount); + } + } + return result; + } + + private List snapshots() { + return pendingByKey.values().stream() + .map(pending -> new PendingSnapshot( + pending.key.patternIdentity, + pending.key.outputSlot, + pending.itemId, + pending.exactExpectedKey, + pending.remainingAmount, + pending.routesToRequester, + pending.registeredOrder)) + .toList(); + } + + private ClaimResult claimExact(AEKey expectedKey, long amount, boolean mutate) { + if (amount <= 0L) { + return ClaimResult.EMPTY; + } + PendingOutput selected = pendingByKey.values().stream() + .filter(pending -> pending.exactExpectedKey.equals(expectedKey) + && pending.remainingAmount > 0L) + .min(Comparator.comparingLong(pending -> pending.registeredOrder)) + .orElse(null); + if (selected == null) { + return ClaimResult.EMPTY; + } + long claimed = Math.min(amount, selected.remainingAmount); + if (mutate) { + selected.remainingAmount -= claimed; + if (selected.remainingAmount <= 0L) { + remove(selected); + } + } + return new ClaimResult(claimed, List.of(new Claim( + claimed, selected.routesToRequester, selected.exactExpectedKey))); + } + + private void mergeFrom(CpuState other) { + for (PendingOutput incoming : other.pendingByKey.values()) { + PendingOutput existing = pendingByKey.get(incoming.key); + if (existing == null) { + PendingOutput copy = new PendingOutput( + incoming.key, + incoming.itemId, + incoming.exactExpectedKey, + incoming.remainingAmount, + incoming.routesToRequester, + incoming.registeredOrder); + pendingByKey.put(copy.key, copy); + pendingByItem.computeIfAbsent(copy.itemId, ignored -> new LinkedHashSet<>()).add(copy.key); + } else { + existing.addExpected(incoming.remainingAmount); + } + nextSequence = Math.max(nextSequence, incoming.registeredOrder + 1L); + } + } + + private CompoundTag toTag() { + CompoundTag tag = new CompoundTag(); + tag.putLong(TAG_NEXT_SEQUENCE, nextSequence); + ListTag pendingList = new ListTag(); + for (PendingOutput pending : pendingByKey.values()) { + CompoundTag entry = new CompoundTag(); + entry.putString(TAG_PATTERN_IDENTITY, pending.key.patternIdentity); + entry.putInt(TAG_OUTPUT_SLOT, pending.key.outputSlot); + entry.putString(TAG_ITEM_ID, pending.itemId.toString()); + entry.put(TAG_EXACT_TEMPLATE, pending.exactExpectedKey.toTagGeneric()); + entry.putLong(TAG_REMAINING, pending.remainingAmount); + entry.putBoolean(TAG_ROUTES_TO_REQUESTER, pending.routesToRequester); + entry.putLong(TAG_REGISTERED_ORDER, pending.registeredOrder); + pendingList.add(entry); + } + tag.put(TAG_PENDING, pendingList); + return tag; + } + + private static CpuState fromTag( + UUID craftingId, CompoundTag tag) { + CpuState state = new CpuState(craftingId); + state.nextSequence = Math.max(1L, tag.getLong(TAG_NEXT_SEQUENCE)); + ListTag pendingList = tag.getList(TAG_PENDING, Tag.TAG_COMPOUND); + for (int index = 0; index < pendingList.size(); index++) { + CompoundTag entry = pendingList.getCompound(index); + if (!entry.contains(TAG_EXACT_TEMPLATE, Tag.TAG_COMPOUND)) { + continue; + } + AEKey exact = AEKey.fromTagGeneric(entry.getCompound(TAG_EXACT_TEMPLATE)); + ResourceLocation itemId = ResourceLocation.tryParse(entry.getString(TAG_ITEM_ID)); + long remaining = entry.getLong(TAG_REMAINING); + if (exact == null || itemId == null || remaining <= 0) { + continue; + } + PendingKey key = new PendingKey( + entry.getString(TAG_PATTERN_IDENTITY), entry.getInt(TAG_OUTPUT_SLOT)); + PendingOutput pending = new PendingOutput( + key, + itemId, + exact, + remaining, + entry.getBoolean(TAG_ROUTES_TO_REQUESTER), + Math.max(1L, entry.getLong(TAG_REGISTERED_ORDER))); + state.pendingByKey.put(key, pending); + state.pendingByItem.computeIfAbsent(itemId, ignored -> new LinkedHashSet<>()).add(key); + state.nextSequence = Math.max(state.nextSequence, pending.registeredOrder + 1); + } + return state; + } + + private record PendingKey(String patternIdentity, int outputSlot) { + } + + private static final class PendingOutput { + private final PendingKey key; + private final ResourceLocation itemId; + private final AEKey exactExpectedKey; + private final boolean routesToRequester; + private final long registeredOrder; + private long remainingAmount; + + private PendingOutput( + PendingKey key, + ResourceLocation itemId, + AEKey exactExpectedKey, + long remainingAmount, + boolean routesToRequester, + long registeredOrder) { + this.key = key; + this.itemId = itemId; + this.exactExpectedKey = exactExpectedKey; + this.remainingAmount = remainingAmount; + this.routesToRequester = routesToRequester; + this.registeredOrder = registeredOrder; + } + + private void addExpected(long amount) { + remainingAmount = saturatingAdd(remainingAmount, amount); + } + } + } + + private static final class EcoState { + private final UUID craftingId; + private Object cpu; + private Object grid; + private CpuState pending; + private boolean wasStaged; + + private EcoState(UUID craftingId, Object cpu, Object grid, CpuState pending) { + this.craftingId = craftingId; + this.cpu = cpu; + this.grid = grid; + this.pending = pending; + this.wasStaged = false; + } + } + + @Nullable + private CpuState takeStagedState(UUID craftingId) { + LinkedHashSet owners = stagedEcoOwners.remove(craftingId); + if (owners == null) { + return null; + } + for (Object owner : owners) { + CpuState state = states.remove(owner); + if (state != null) { + return state; + } + } + return null; + } + + private static long saturatingAdd(long first, long second) { + return second > 0 && first > Long.MAX_VALUE - second ? Long.MAX_VALUE : first + second; + } + + private static long saturatingMultiply(long first, long second) { + if (first <= 0 || second <= 0) { + return 0; + } + return first > Long.MAX_VALUE / second ? Long.MAX_VALUE : first * second; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/DynamicPatternExecution.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/DynamicPatternExecution.java new file mode 100644 index 00000000..22b4b0e9 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/DynamicPatternExecution.java @@ -0,0 +1,33 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import appeng.api.crafting.IPatternDetails; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +/** Resolves execution wrappers while preserving their effective copy count. */ +public final class DynamicPatternExecution { + private DynamicPatternExecution() { + } + + @Nullable + public static Resolved resolve(IPatternDetails details) { + Objects.requireNonNull(details, "details"); + SmartDoublingPatterns.Resolved resolved = SmartDoublingPatterns.resolve(details); + IPatternDetails current = resolved.pattern(); + long copies = resolved.operationsPerPush(); + + return current instanceof DynamicComponentPattern dynamic + ? new Resolved(dynamic, copies) + : null; + } + + public record Resolved(DynamicComponentPattern pattern, long copies) { + public Resolved { + Objects.requireNonNull(pattern, "pattern"); + if (copies <= 0) { + throw new IllegalArgumentException("copies must be positive"); + } + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/DynamicPatternInsertContext.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/DynamicPatternInsertContext.java new file mode 100644 index 00000000..8750368b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/DynamicPatternInsertContext.java @@ -0,0 +1,34 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import appeng.api.config.Actionable; +import appeng.api.stacks.AEKey; + +/** Captures the strict AE2 match before the local dynamic claim is attempted. */ +public final class DynamicPatternInsertContext { + private final AEKey key; + private final long requestedAmount; + private final Actionable actionable; + private long strictMatched; + + public DynamicPatternInsertContext(AEKey key, long requestedAmount, Actionable actionable) { + this.key = key; + this.requestedAmount = requestedAmount; + this.actionable = actionable; + } + + public long requestedAmount() { + return requestedAmount; + } + + public Actionable actionable() { + return actionable; + } + + public long strictMatched() { + return strictMatched; + } + + public void setStrictMatched(long strictMatched) { + this.strictMatched = strictMatched; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/DynamicPatternPlanning.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/DynamicPatternPlanning.java new file mode 100644 index 00000000..3fb470db --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/DynamicPatternPlanning.java @@ -0,0 +1,59 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.networking.crafting.ICraftingService; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.GenericStack; + +/** Selects a deterministic child output before AE falls back to fuzzy component matching. */ +public final class DynamicPatternPlanning { + private DynamicPatternPlanning() { + } + + public static AEKey preferDeclaredCraftableInput( + IPatternDetails pattern, + int slot, + AEKey encodedKey, + ICraftingService craftingService) { + if (!(pattern instanceof DynamicComponentPattern dynamic) + || !dynamic.isItemIdInput(slot) + || craftingService == null) { + return encodedKey; + } + + IPatternDetails.IInput[] inputs = pattern.getInputs(); + if (slot < 0 || slot >= inputs.length || inputs[slot] == null) { + return encodedKey; + } + + GenericStack[] candidates = inputs[slot].getPossibleInputs(); + if (candidates == null || candidates.length == 0 || candidates[0] == null) { + return encodedKey; + } + + long primaryAmount = candidates[0].amount(); + for (GenericStack candidate : candidates) { + if (candidate == null || candidate.what() == null || candidate.amount() != primaryAmount) { + continue; + } + if (!craftingService.getCraftingFor(candidate.what()).isEmpty()) { + return candidate.what(); + } + } + + for (GenericStack candidate : candidates) { + if (candidate == null || candidate.what() == null || candidate.amount() != primaryAmount + || !(candidate.what() instanceof AEItemKey expectedItem)) { + continue; + } + AEKey fuzzy = craftingService.getFuzzyCraftable(candidate.what(), craftable -> + craftable instanceof AEItemKey craftableItem + && craftableItem.getItem() == expectedItem.getItem()); + if (fuzzy != null) { + return fuzzy; + } + } + return encodedKey; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/DynamicPatternProfile.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/DynamicPatternProfile.java new file mode 100644 index 00000000..cea8023f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/DynamicPatternProfile.java @@ -0,0 +1,45 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import net.minecraft.world.item.ItemStack; + +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +/** Neutral dynamic-pattern description shared by optional recipe integrations. */ +public record DynamicPatternProfile( + Map canonicalInputs, + Set idOnlyInputSlots, + Set idOnlyOutputSlots, + Map inputMatchers) { + + public DynamicPatternProfile(Set idOnlyInputSlots, Set idOnlyOutputSlots) { + this(Map.of(), idOnlyInputSlots, idOnlyOutputSlots, Map.of()); + } + + public DynamicPatternProfile { + Map inputCopies = new LinkedHashMap<>(); + if (canonicalInputs != null) { + for (Map.Entry entry : canonicalInputs.entrySet()) { + if (entry.getKey() == null || entry.getKey() < 0 + || entry.getValue() == null || entry.getValue().isEmpty()) { + throw new IllegalArgumentException("Canonical pattern inputs must be non-empty and non-negative"); + } + inputCopies.put(entry.getKey(), entry.getValue().copyWithCount(1)); + } + } + canonicalInputs = Map.copyOf(inputCopies); + idOnlyInputSlots = Set.copyOf(new LinkedHashSet<>(idOnlyInputSlots == null + ? Set.of() : idOnlyInputSlots)); + idOnlyOutputSlots = Set.copyOf(new LinkedHashSet<>(idOnlyOutputSlots == null + ? Set.of() : idOnlyOutputSlots)); + inputMatchers = inputMatchers == null ? Map.of() : Map.copyOf(inputMatchers); + if (idOnlyInputSlots.stream().anyMatch(slot -> slot == null || slot < 0) + || idOnlyOutputSlots.stream().anyMatch(slot -> slot == null || slot < 0) + || !idOnlyInputSlots.containsAll(canonicalInputs.keySet()) + || inputMatchers.keySet().stream().anyMatch(slot -> slot == null || slot < 0)) { + throw new IllegalArgumentException("Dynamic pattern slots must be non-negative and consistent"); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/GridOutputTarget.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/GridOutputTarget.java new file mode 100644 index 00000000..38470bb4 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/GridOutputTarget.java @@ -0,0 +1,84 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import appeng.api.config.Actionable; +import appeng.api.networking.IGrid; +import appeng.api.networking.security.IActionSource; +import appeng.api.stacks.AEKey; +import appeng.api.storage.MEStorage; +import com.sorrowmist.useless.compat.forge1201.OptionalIntegration; +import net.minecraftforge.fml.ModList; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * {@link CraftingAeOutputTarget} 的默认实现:先给 neoecoae 的输出认领一次机会,剩余量写进 ME 存储。 + * + *

语义与原来两个宿主里的内联 lambda 完全一致({@code tryOutputKeyToAE} 走 {@link #insert}, + * 一条不多一条不少)。抽成类的唯一目的是让「兼容层认领」与「存储插入」两段能被分别调用 —— + * 见下面 {@link #claim} 的说明。

+ * + *

为什么要把认领拆出来

+ * + *

「产物回网」的可持续吞吐完全由「每 tick 可插入次数」决定,而每个分段的固定成本包含一次 + * {@link NeoEcoDynamicOutputCompat#claim}:它是一次 {@code synchronized} 调用加若干次集合遍历与分配, + * 在没有 ECO 候选时返回 0。此前该成本每个 {@code Long.MAX} 分段都要支付一次 + * —— 8ms 回网预算下约 6 千次/tick,100ms 下约 7 万次/tick,其中绝大多数为无效开销。

+ * + *

提到循环外为什么不改行为:认领量只取决于 ECO 任务自己的剩余需求 + * ({@code pending.remainingAmount()} 封顶),与传入的 {@code amount} 无关。 + * 所以「按整键总量认领一次」与「逐段各认领一次」得到的认领量完全相同。 + * 调用方只需保证传入量不低于该需求 —— {@link Long#MAX_VALUE} 恒满足。

+ */ +public final class GridOutputTarget implements CraftingAeOutputTarget { + + private final @Nullable IGrid grid; + private final MEStorage storage; + private final IActionSource source; + /** 构造时判定一次即可:模块是否加载在整个运行期不变。 */ + private final boolean ecoCompatLoaded; + + public GridOutputTarget(@Nullable IGrid grid, @NotNull MEStorage storage, @NotNull IActionSource source) { + this.grid = grid; + this.storage = storage; + this.source = source; + this.ecoCompatLoaded = ModList.get().isLoaded("neoecoae"); + } + + @Override + public boolean supportsClaimHoisting() { + return this.ecoCompatLoaded; + } + + @Override + public long claim(@NotNull AEKey key, long totalAmount) { + if (!this.ecoCompatLoaded || this.grid == null || key == null || totalAmount <= 0L) { + return 0L; + } + return OptionalIntegration.call("neoecoae", "com.sorrowmist.useless.compat.neoecoae.NeoEcoDynamicOutputCompat", + "claim", Long.class, 0L, new Class[]{IGrid.class, AEKey.class, long.class}, + this.grid, key, totalAmount); + } + + @Override + public long insertRaw(@NotNull AEKey key, long amount) { + if (key == null || amount <= 0L) { + return 0L; + } + return this.storage.insert(key, amount, Actionable.MODULATE, this.source); + } + + /** 认领 + 存储插入,与拆分前的内联实现逐字等价。 */ + @Override + public long insert(@NotNull AEKey key, long amount) { + if (key == null || amount <= 0L) { + return 0L; + } + long claimed = claim(key, amount); + long accepted = Math.min(amount, claimed); + long remaining = amount - accepted; + if (remaining <= 0L) { + return amount; + } + return accepted + insertRaw(key, remaining); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/ManualPatternOperationResolver.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/ManualPatternOperationResolver.java new file mode 100644 index 00000000..a0770790 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/ManualPatternOperationResolver.java @@ -0,0 +1,103 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.stacks.AEFluidKey; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.fluids.FluidStack; + +import java.util.List; + +/** Resolves how many base recipe operations one manually scaled AE pattern represents. */ +final class ManualPatternOperationResolver { + private ManualPatternOperationResolver() { + } + + static Resolution resolve(AdvancedAlloyFurnaceRecipe recipe, IPatternDetails pattern) { + if (recipe == null || pattern == null || (pattern.getOutputs().length == 0)) { + return Resolution.invalid(); + } + + // Smart-doubling already carries its operation count separately in the AE task pipeline. + if (SmartDoublingPatterns.operationsPerPush(pattern) > 1L) { + return Resolution.valid(1L); + } + + DynamicComponentPattern dynamic = pattern instanceof DynamicComponentPattern value ? value : null; + long multiplier = 0L; + List outputs = java.util.Arrays.asList(pattern.getOutputs()); + for (int slot = 0; slot < outputs.size(); slot++) { + GenericStack output = outputs.get(slot); + if (output == null || output.what() == null || output.amount() <= 0L) { + return Resolution.invalid(); + } + + boolean itemIdOnly = dynamic != null && dynamic.isItemIdOutput(slot); + long recipeAmount = matchingRecipeOutputAmount(recipe, output.what(), itemIdOnly); + if (recipeAmount <= 0L || output.amount() % recipeAmount != 0L) { + return Resolution.invalid(); + } + + long candidate = output.amount() / recipeAmount; + if (candidate <= 0L || multiplier != 0L && multiplier != candidate) { + return Resolution.invalid(); + } + multiplier = candidate; + } + + return multiplier <= 0L ? Resolution.invalid() : Resolution.valid(multiplier); + } + + private static long matchingRecipeOutputAmount( + AdvancedAlloyFurnaceRecipe recipe, AEKey expected, boolean itemIdOnly) { + long amount = 0L; + for (ItemStack output : recipe.outputs()) { + AEItemKey key = AEItemKey.of(output); + if (matches(expected, key, itemIdOnly)) { + amount = saturatingAdd(amount, output.getCount()); + } + } + for (FluidStack output : recipe.outputFluids()) { + AEFluidKey key = AEFluidKey.of(output); + if (matches(expected, key, false)) { + amount = saturatingAdd(amount, output.getAmount()); + } + } + for (GenericStack output : recipe.keyOutputs()) { + if (output != null && output.what() != null && output.amount() > 0L + && matches(expected, output.what(), itemIdOnly)) { + amount = saturatingAdd(amount, output.amount()); + } + } + return amount; + } + + private static boolean matches(AEKey expected, AEKey candidate, boolean itemIdOnly) { + if (expected == null || candidate == null) { + return false; + } + if (!itemIdOnly) { + return expected.equals(candidate); + } + return expected instanceof AEItemKey expectedItem + && candidate instanceof AEItemKey candidateItem + && expectedItem.getItem() == candidateItem.getItem(); + } + + private static long saturatingAdd(long left, long right) { + return right > 0L && left > Long.MAX_VALUE - right ? Long.MAX_VALUE : left + right; + } + + record Resolution(boolean valid, long operationsPerPattern) { + static Resolution valid(long operationsPerPattern) { + return new Resolution(true, Math.max(1L, operationsPerPattern)); + } + + static Resolution invalid() { + return new Resolution(false, 1L); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/OccultismBoundBookPatternDetails.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/OccultismBoundBookPatternDetails.java new file mode 100644 index 00000000..eeee62a4 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/OccultismBoundBookPatternDetails.java @@ -0,0 +1,78 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.stacks.AEItemKey; +import appeng.crafting.pattern.AECraftingPattern; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.Level; + +import java.util.Set; + +/** AE crafting pattern whose Occultism spirit name is generated when it is assembled. */ +public final class OccultismBoundBookPatternDetails extends AECraftingPattern + implements DynamicComponentPattern { + static final ResourceLocation RECIPE_ID = new ResourceLocation( + "occultism", "crafting/bound_book_of_binding"); + private static final Set BOUND_BOOK_IDS = Set.of( + occultismItem("book_of_binding_bound_foliot"), + occultismItem("book_of_binding_bound_djinni"), + occultismItem("book_of_binding_bound_afrit"), + occultismItem("book_of_binding_bound_marid")); + + private final String identity; + + private OccultismBoundBookPatternDetails(AEItemKey definition, Level level) { + super(definition, level); + this.identity = "useless_mod:occultism_bound_book|definition_sha256=" + + DynamicComponentPatternDetails.definitionFingerprint( + definition, level.registryAccess()); + } + + public static IPatternDetails wrap(IPatternDetails details, Level level) { + if (!(details instanceof AECraftingPattern crafting) + || details instanceof OccultismBoundBookPatternDetails + || level == null + || !isSupported(crafting)) { + return details; + } + return new OccultismBoundBookPatternDetails(crafting.getDefinition(), level); + } + + static boolean isSupported(AECraftingPattern pattern) { + var encoded = com.sorrowmist.useless.compat.forge1201.recipe.NativeCraftingPatternData.read(pattern.getDefinition()); + if (encoded == null || !RECIPE_ID.equals(encoded.recipeId()) + || pattern.getOutputs().length != 1 + || !(pattern.getOutputs()[0].what() instanceof AEItemKey output)) { + return false; + } + return isSupportedRecipeAndOutput(encoded.recipeId(), output.getId()); + } + + static boolean isSupportedRecipeAndOutput(ResourceLocation recipeId, ResourceLocation outputId) { + return RECIPE_ID.equals(recipeId) && BOUND_BOOK_IDS.contains(outputId); + } + + private static ResourceLocation occultismItem(String path) { + return new ResourceLocation("occultism", path); + } + + @Override + public String dynamicPatternIdentity() { + return identity; + } + + @Override + public boolean isItemIdInput(int slot) { + return false; + } + + @Override + public boolean isItemIdOutput(int slot) { + return slot == 0; + } + + @Override + public boolean usesDynamicOutputs() { + return true; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/OmniversalBigIntegerTarget.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/OmniversalBigIntegerTarget.java new file mode 100644 index 00000000..8ea9723e --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/OmniversalBigIntegerTarget.java @@ -0,0 +1,190 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.networking.IGrid; +import appeng.api.stacks.KeyCounter; +import appeng.blockentity.crafting.IMolecularAssemblerSupportedPattern; +import com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerApi; +import com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerBatch; +import com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerCapacity; +import com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerTarget; +import com.sorrowmist.useless.api.crafting.bigint.cpu.AlloyFurnaceBigIntegerCpuBinding; +import com.sorrowmist.useless.content.blockentities.multiblock.MultiblockAlloyFurnaceCoreBlockEntity; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.math.BigInteger; +import java.util.Objects; +import java.util.Set; + +/** + * {@link AlloyFurnaceBigIntegerTarget} 的多方块实现:一台万象合金炉站在「样板供应器」一侧的视图。 + * + *

支持两类样板(与机器实际能力一致):

+ *
    + *
  • 万象样板 —— 解析绑定配方一次 → 产物 ×count,按 {@code count × 单份能耗} 收能量, + * 容量受「配方可用性 / 材料窗口 / 产物分段 / 能量」四道闸限制;
  • + *
  • AE2 合成样板 —— 在虚拟 3×3 工作台上装配一次 → 产物 ×count,不收能量, + * 容量只受「材料窗口 / 产物分段」限制。
  • + *
+ * + *

这个类刻意只做转发与解包:容量算术在 {@link AlloyFurnaceBigIntegerCrafting}, + * 提交在 {@link AdvancedAlloyFurnaceAeManager#pushBigIntegerBatch}。这样对外公开的 API + * 与数据能源适配器共用同一套判定,不会出现「两条入口算出不同容量」的情况。

+ * + *

{@code machineIdentity} 用的是全模组唯一的身份公式( + * {@link AlloyFurnaceBigIntegerCrafting#machineIdentity}),所以 DE 与公开 API 认的是同一台物理机器。

+ */ +public final class OmniversalBigIntegerTarget implements AlloyFurnaceBigIntegerTarget { + private final MultiblockAlloyFurnaceCoreBlockEntity core; + private final String routeIdentity; + + public OmniversalBigIntegerTarget(@NotNull MultiblockAlloyFurnaceCoreBlockEntity core, + @NotNull String routeIdentity) { + this.core = Objects.requireNonNull(core, "Multiblock alloy furnace core must not be null"); + this.routeIdentity = Objects.requireNonNull(routeIdentity, "Route identity must not be null"); + } + + @Override + public @NotNull String routeIdentity() { + return this.routeIdentity; + } + + @Override + public @NotNull String machineIdentity() { + return AlloyFurnaceBigIntegerCrafting.machineIdentity( + "machine@" + this.core.getBlockPos().asLong(), + this.core.getLevel(), + this.core.getBlockPos()); + } + + @Override + public @Nullable IGrid grid() { + return this.core.getAeGrid(); + } + + @Override + public @NotNull Set acceptedPatternKinds() { + return AlloyFurnaceBigIntegerApi.acceptedPatternKinds(); + } + + @Override + public @NotNull AlloyFurnaceBigIntegerCapacity capacity(IPatternDetails pattern, + KeyCounter @NotNull [] prototype, + @NotNull BigInteger requested) { + if (requested == null || requested.signum() <= 0 || prototype == null || prototype.length == 0) { + return AlloyFurnaceBigIntegerCapacity.none(""); + } + // 单批分段预算由机器用 AIMD 控制器实测给出(见 CraftingTaskContext#outputSegmentBudget): + // 它把「一批大约多少 tick 交付完」收敛到最平滑的形态,而不是用固定值猜。 + long segmentBudget = this.core.outputSegmentBudget(); + IPatternDetails original = pattern == null ? null : SmartDoublingPatterns.unwrap(pattern); + // 含返还物的样板一律不接 bigint 批次:AE2 对这类输入只算 1 份,按倍率折叠会向机器索要 + // count 份催化剂而实际只有 1 份。这里「退让」(报无容量)而不是拒收 —— + // 上层会回落 AE2 原生单份路径,永远正确,只是慢。 + // 关卡从本机取(core.getLevel()),不从 AE2 网格取。 + if (!SmartDoublingPatterns.reusableInputSlots(original, this.core.getLevel()).isEmpty()) { + return AlloyFurnaceBigIntegerCapacity.none(""); + } + if (original instanceof OmniversalPatternDetails omniversal) { + BigInteger accepted = AlloyFurnaceBigIntegerCrafting.maximumCount( + this.core, omniversal, prototype, threads(), segmentBudget, requested); + if (accepted.signum() <= 0) { + // 报 0 时给出原因(档次不够 / 缺模具 / 没能量),供调用方提示玩家。 + AdvancedAlloyFurnaceRecipe recipe = omniversal.recipe(); + return AlloyFurnaceBigIntegerCapacity.none(recipe == null + ? "" : this.core.getTaskAvailability(recipe).statusKey()); + } + return AlloyFurnaceBigIntegerCapacity.of(requested.min(accepted)); + } + if (original instanceof IMolecularAssemblerSupportedPattern) { + // 合成样板:一次装配折叠任意份数;能量闸与万象样板共用同一套催化剂口径, + // 有用线圈(energyMultipliesWithParallel == false)下该闸不设限。 + BigInteger accepted = AlloyFurnaceBigIntegerCrafting.maximumCraftingPatternCount( + this.core, original, prototype, threads(), segmentBudget, requested); + return accepted.signum() <= 0 + ? AlloyFurnaceBigIntegerCapacity.none(CraftingTaskContext.STATUS_WAITING_ENERGY_KEY) + : AlloyFurnaceBigIntegerCapacity.of(requested.min(accepted)); + } + return AlloyFurnaceBigIntegerCapacity.none(""); + } + + @Override + public @Nullable AlloyFurnaceBigIntegerBatch admit(IPatternDetails pattern, + KeyCounter @NotNull [] prototype, + @NotNull BigInteger requested, + @Nullable AlloyFurnaceBigIntegerCpuBinding cpu) { + AlloyFurnaceBigIntegerCapacity capacity = capacity(pattern, prototype, requested); + if (!capacity.isAvailable()) { + return null; + } + // 完整检查(配方可用性 / 背压 / 能量)在 commit 时重新执行:期间机器状态可能变化, + // 最终以 commit 的返回值为准,与 DE 侧 BigIntegerCraftingAdmission 的语义一致。 + return new Batch(pattern, prototype, capacity.accepted(), cpu); + } + + /** 本机当前线程数(多方块跟随线圈线程)。 */ + private int threads() { + return this.core.getMaxAETaskCount(); + } + + /** + * 本机(准确说是整个服务端)最近是否因耗时超预算而正在降频。 + * + *

{@link AlloyFurnaceTickBudget} 的粒度是全局的:所有机器跑在同一个服务端线程上,多台机器的 + * 耗时会累加、共同把降频压下去。这与数据能源的「每网格预算」是同一个量级。

+ */ + @Override + public boolean isThrottled() { + return AlloyFurnaceTickBudget.isThrottled(); + } + + /** 一次性准入凭据:只保留到 commit 所需的引用,提交后立刻释放。 */ + private final class Batch implements AlloyFurnaceBigIntegerBatch { + private final IPatternDetails pattern; + private final KeyCounter @NotNull [] prototype; + private final BigInteger count; + @Nullable + private final AlloyFurnaceBigIntegerCpuBinding cpu; + private boolean committed; + private boolean transferred; + + private Batch(IPatternDetails pattern, + KeyCounter @NotNull [] prototype, + BigInteger count, + @Nullable AlloyFurnaceBigIntegerCpuBinding cpu) { + this.pattern = pattern; + this.prototype = prototype; + this.count = count; + this.cpu = cpu; + } + + @Override + public @NotNull BigInteger count() { + return this.count; + } + + @Override + public boolean commit(KeyCounter @NotNull [] prototype) { + if (this.committed) { + throw new IllegalStateException("BigInteger crafting batch has already been committed"); + } + if (prototype != this.prototype) { + throw new IllegalArgumentException( + "BigInteger crafting batch must be committed with its prepared prototype"); + } + this.committed = true; + boolean accepted = OmniversalBigIntegerTarget.this.core + .pushBigIntegerBatch(this.pattern, this.count, prototype, this.cpu); + this.transferred = accepted; + return accepted; + } + + @Override + public boolean hasTransferredInputOwnership() { + return this.transferred; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/OmniversalPatternDetails.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/OmniversalPatternDetails.java new file mode 100644 index 00000000..c0efd09f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/OmniversalPatternDetails.java @@ -0,0 +1,325 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import appeng.api.stacks.AEItemKey; +import appeng.crafting.pattern.AEProcessingPattern; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeCatalog; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.enderio.SoulBindingRecipeAdapter; +import com.sorrowmist.useless.core.component.OmniversalPatternData; +import com.sorrowmist.useless.core.component.UComponents; +import com.sorrowmist.useless.core.config.ConfigManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fml.ModList; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.WeakHashMap; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import org.jetbrains.annotations.Nullable; + +public final class OmniversalPatternDetails extends DynamicComponentPatternDetails { + private static final int DEFAULT_DECODE_CACHE_CAPACITY = 2048; + private static final Object DECODE_CACHE_LOCK = new Object(); + private static final Map DECODE_CACHES = new WeakHashMap<>(); + /** + * Widest set of id-only input slots each definition ever decoded to. Re-deriving the slots can + * only lose some of them, and a lost slot makes the pattern require the exact NBT its inputs + * were encoded to ignore. + */ + private static final Map> PROVEN_ITEM_ID_INPUT_SLOTS = new ConcurrentHashMap<>(); + + private final OmniversalPatternData data; + private final AdvancedAlloyFurnaceRecipe recipe; + + private OmniversalPatternDetails(Decoded decoded) { + super(decoded.source, decoded.itemIdInputSlots, + decoded.data.itemIdOutputSlots(), + tagInputTags(decoded.data), + fluidTagInputTags(decoded.data), + dynamicInputMatchers(decoded), + Map.of(), + decoded.level.registryAccess()); + this.data = decoded.data; + this.recipe = decoded.entry.recipe(); + } + + public static OmniversalPatternDetails decode(AEItemKey definition, Level level) { + if (definition == null || level == null) return null; + + LevelDecodeCache cache; + synchronized (DECODE_CACHE_LOCK) { + cache = DECODE_CACHES.computeIfAbsent(level, ignored -> new LevelDecodeCache()); + } + while (true) { + long generation = AlloyFurnaceRecipeCatalog.generation(); + cache.prepare(generation); + if (generation != AlloyFurnaceRecipeCatalog.generation()) continue; + + CachedDecode cached = cache.get(definition); + if (cached != null) { + cache.hits.incrementAndGet(); + if (generation != AlloyFurnaceRecipeCatalog.generation()) continue; + return cached.valueOrThrow(); + } + cache.misses.incrementAndGet(); + + CompletableFuture ownerFuture = new CompletableFuture<>(); + InFlightDecode owner = new InFlightDecode(generation, ownerFuture); + InFlightDecode existing = cache.inFlight.putIfAbsent(definition, owner); + if (existing != null) { + if (existing.generation != generation) { + cache.inFlight.remove(definition, existing); + continue; + } + try { + CachedDecode result = existing.future.join(); + if (generation != AlloyFurnaceRecipeCatalog.generation()) continue; + return result.valueOrThrow(); + } catch (CompletionException exception) { + if (generation != AlloyFurnaceRecipeCatalog.generation()) continue; + throw unwrap(exception); + } + } + + CachedDecode result; + try { + cache.actualDecodes.incrementAndGet(); + try { + result = CachedDecode.success(decodeUncached(definition, level)); + } catch (RuntimeException exception) { + result = CachedDecode.failure(exception); + OmniversalPatternWarnings.decodeFailed(definition, + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(definition, UComponents.OMNIVERSAL_PATTERN_DATA.get()), exception); + } + // Only successes are cached: a transient failure must not disable the pattern for + // the rest of the session. + if (result.details != null && generation == AlloyFurnaceRecipeCatalog.generation()) { + cache.putIfCurrent(definition, result, generation); + } + ownerFuture.complete(result); + } catch (Throwable throwable) { + ownerFuture.completeExceptionally(throwable); + if (throwable instanceof RuntimeException runtimeException) throw runtimeException; + if (throwable instanceof Error error) throw error; + throw new RuntimeException(throwable); + } finally { + cache.inFlight.remove(definition, owner); + } + + if (generation != AlloyFurnaceRecipeCatalog.generation()) continue; + return result.valueOrThrow(); + } + } + + private static RuntimeException unwrap(CompletionException exception) { + Throwable cause = exception.getCause(); + if (cause instanceof RuntimeException runtimeException) return runtimeException; + if (cause instanceof Error error) throw error; + return new RuntimeException(cause == null ? exception : cause); + } + + private static OmniversalPatternDetails decodeUncached(AEItemKey definition, Level level) { + OmniversalPatternData data = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(definition, UComponents.OMNIVERSAL_PATTERN_DATA.get()); + if (data == null || data.version() > OmniversalPatternData.CURRENT_VERSION) { + throw new IllegalArgumentException("Missing or unsupported omniversal pattern data"); + } + AEProcessingPattern source = new AEProcessingPattern(definition); + Optional resolved = + data.version() < OmniversalPatternData.TAG_INPUT_VERSION + ? AlloyFurnaceRecipeCatalog.resolveLegacyPattern( + level, data.sourceId(), data.identity(), source, data.version()) + : AlloyFurnaceRecipeCatalog.resolvePattern( + level, data.sourceId(), data.identity(), source); + AlloyFurnaceRecipeCatalog.Entry entry = resolved + .orElseThrow(() -> new IllegalArgumentException( + "The bound alloy-furnace recipe is missing or has changed: " + data.recipeId())); + List itemIdInputSlots = widenItemIdInputSlots(definition, + OmniversalPatternEncoding.resolveItemIdInputSlots( + entry.recipe(), source, data.itemIdInputSlots())); + return new OmniversalPatternDetails(new Decoded( + source, data, entry, level, itemIdInputSlots)); + } + + private static List widenItemIdInputSlots(AEItemKey definition, List derived) { + List proven = PROVEN_ITEM_ID_INPUT_SLOTS.compute(definition, (key, stored) -> { + if (stored == null) return derived; + if (stored.containsAll(derived)) return stored; + List widened = new ArrayList<>(stored); + for (Integer slot : derived) { + if (!widened.contains(slot)) { + widened.add(slot); + } + } + widened.sort(null); + return List.copyOf(widened); + }); + if (!proven.equals(derived)) { + OmniversalPatternWarnings.idOnlySlotsNarrowed(definition, proven, derived); + } + return proven; + } + + private static final class LevelDecodeCache { + private long generation = Long.MIN_VALUE; + private final Object lock = new Object(); + private final ConcurrentHashMap inFlight = + new ConcurrentHashMap<>(); + private final AtomicLong hits = new AtomicLong(); + private final AtomicLong misses = new AtomicLong(); + private final AtomicLong evictions = new AtomicLong(); + private final AtomicLong actualDecodes = new AtomicLong(); + private final LinkedHashMap entries = + new LinkedHashMap<>(64, 0.75F, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + boolean remove = size() > capacity(); + if (remove) evictions.incrementAndGet(); + return remove; + } + }; + + private void prepare(long currentGeneration) { + synchronized (lock) { + if (currentGeneration > generation) { + generation = currentGeneration; + entries.clear(); + inFlight.clear(); + } + } + } + + @Nullable + private CachedDecode get(AEItemKey definition) { + synchronized (lock) { + return entries.get(definition); + } + } + + private void putIfCurrent(AEItemKey definition, CachedDecode result, long expectedGeneration) { + synchronized (lock) { + if (generation == expectedGeneration) entries.put(definition, result); + } + } + + private int capacity() { + try { + return ConfigManager.getOmniversalDecodeCacheCapacity(); + } catch (RuntimeException ignored) { + return DEFAULT_DECODE_CACHE_CAPACITY; + } + } + } + + private record InFlightDecode(long generation, CompletableFuture future) { + } + + private static final class CachedDecode { + @Nullable + private final OmniversalPatternDetails details; + private final RuntimeException failure; + + private CachedDecode(@Nullable OmniversalPatternDetails details, + RuntimeException failure) { + this.details = details; + this.failure = failure; + } + + private static CachedDecode success(OmniversalPatternDetails details) { + return new CachedDecode(details, null); + } + + private static CachedDecode failure(RuntimeException failure) { + return new CachedDecode(null, failure); + } + + private OmniversalPatternDetails valueOrThrow() { + if (failure != null) throw failure; + if (details == null) throw new IllegalStateException("Cached omniversal pattern has no result"); + return details; + } + } + + public record DecodeCacheStats(long hits, long misses, long evictions, long actualDecodes) { + } + + public static DecodeCacheStats stats(Level level) { + if (level == null) return new DecodeCacheStats(0L, 0L, 0L, 0L); + synchronized (DECODE_CACHE_LOCK) { + LevelDecodeCache cache = DECODE_CACHES.get(level); + return cache == null ? new DecodeCacheStats(0L, 0L, 0L, 0L) + : new DecodeCacheStats(cache.hits.get(), cache.misses.get(), + cache.evictions.get(), cache.actualDecodes.get()); + } + } + + public OmniversalPatternData data() { + return data; + } + + public AdvancedAlloyFurnaceRecipe recipe() { + return recipe; + } + + private static java.util.Map + dynamicInputMatchers(Decoded decoded) { + PatternStackView view = PatternStackView.fromPattern(decoded.source); + if (view == null) return java.util.Map.of(); + String sourceId = RecipeSourceIds.normalize(decoded.data.sourceId()); + if (RecipeSourceIds.NEOVITAE.equals(sourceId)) { + if (!ModList.get().isLoaded("neovitae")) return java.util.Map.of(); + return AdvancedAlloyFurnacePatternResolver.findNeoVitaeDynamicPatternProfile( + decoded.level, view) + .map(DynamicPatternProfile::inputMatchers) + .orElseGet(java.util.Map::of); + } + if (!ModList.get().isLoaded("enderio")) { + return java.util.Map.of(); + } + return SoulBindingRecipeAdapter.findDynamicPatternProfileLong( + decoded.data.sourceId().isBlank() ? "enderio" : decoded.data.sourceId(), + decoded.level, + view) + .map(SoulBindingRecipeAdapter.DynamicPatternProfile::inputMatchers) + .orElseGet(java.util.Map::of); + } + + private static Map>> + tagInputTags(OmniversalPatternData data) { + if (data.version() < OmniversalPatternData.TAG_INPUT_VERSION) return Map.of(); + Map>> result = new LinkedHashMap<>(); + for (OmniversalPatternData.TagInputSlot slot : data.tagInputSlots()) { + result.computeIfAbsent(slot.slot(), ignored -> new java.util.ArrayList<>()).add(slot.tag()); + } + return result; + } + + private static Map>> + fluidTagInputTags(OmniversalPatternData data) { + if (data.version() < OmniversalPatternData.FLUID_TAG_INPUT_VERSION) return Map.of(); + Map>> result = + new LinkedHashMap<>(); + for (OmniversalPatternData.FluidTagInputSlot slot : data.fluidTagInputSlots()) { + result.computeIfAbsent(slot.slot(), ignored -> new java.util.ArrayList<>()).add(slot.tag()); + } + return result; + } + + @Override + public String dynamicPatternIdentity() { + return "useless_mod:omniversal|recipe=" + data.recipeId() + + "|fingerprint=" + data.recipeFingerprint() + + "|dynamic=" + super.dynamicPatternIdentity(); + } + + private record Decoded(AEProcessingPattern source, OmniversalPatternData data, + AlloyFurnaceRecipeCatalog.Entry entry, Level level, + List itemIdInputSlots) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/OmniversalPatternEncoding.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/OmniversalPatternEncoding.java new file mode 100644 index 00000000..80ae002d --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/OmniversalPatternEncoding.java @@ -0,0 +1,632 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.crafting.PatternDetailsHelper; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.GenericStack; +import appeng.crafting.pattern.AECraftingPattern; +import appeng.crafting.pattern.AEProcessingPattern; +import com.mojang.datafixers.util.Pair; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeCatalog; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import com.sorrowmist.useless.core.component.OmniversalPatternData; +import com.sorrowmist.useless.core.component.UComponents; +import com.sorrowmist.useless.init.ModItems; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.tags.TagKey; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluid; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalLong; +import java.util.Set; +import java.util.TreeSet; + +public final class OmniversalPatternEncoding { + private OmniversalPatternEncoding() { + } + + public static ItemStack encode(ItemStack sourcePattern, AlloyFurnaceRecipeCatalog.Entry entry, Level level) { + if (sourcePattern == null || sourcePattern.isEmpty() || entry == null || level == null) { + return ItemStack.EMPTY; + } + IPatternDetails decoded = PatternDetailsHelper.decodePattern(sourcePattern, level); + return encode(sourcePattern, decoded, entry, level); + } + + /** Encodes from the caller's already decoded pattern to avoid a second AE2 decode pass. */ + public static ItemStack encode( + ItemStack sourcePattern, IPatternDetails decoded, + AlloyFurnaceRecipeCatalog.Entry entry, Level level) { + if (sourcePattern == null || sourcePattern.isEmpty() || decoded == null + || entry == null || level == null) { + return ItemStack.EMPTY; + } + if (decoded instanceof OmniversalPatternDetails) { + return ItemStack.EMPTY; + } + + if (decoded instanceof AECraftingPattern) { + ItemStack processingPattern = createProcessingPattern(entry.recipe()); + if (processingPattern.isEmpty()) { + return ItemStack.EMPTY; + } + + IPatternDetails processingDetails = PatternDetailsHelper.decodePattern( + processingPattern, level); + if (!(processingDetails instanceof AEProcessingPattern processing)) { + return ItemStack.EMPTY; + } + return encodeProcessingPattern(processingPattern, processing, entry, level); + } + + if (!(decoded instanceof AEProcessingPattern processing)) { + return ItemStack.EMPTY; + } + + return encodeProcessingPattern(sourcePattern, processing, entry, level); + } + + /** + * 用重新解析出的配方身份重编码一张既有的万象样板。 + * + *

失效样板自身的 AE payload 仍然完好,失效的只是它绑定的配方身份。这里把该 payload + * 当作普通 {@link AEProcessingPattern} 传入 {@link #encode},从而自然绕过「拒绝二次包装 + * 万象样板」的守卫,而不必放宽那条守卫、影响常规调用方。

+ * + *

payload 无法解析或重编码失败时返回 {@link ItemStack#EMPTY},由调用方保留原栈。

+ */ + public static ItemStack reencodeExisting( + ItemStack existing, AlloyFurnaceRecipeCatalog.Entry entry, Level level) { + if (existing == null || existing.isEmpty() || entry == null || level == null) { + return ItemStack.EMPTY; + } + AEItemKey definition = AEItemKey.of(existing); + if (definition == null) { + return ItemStack.EMPTY; + } + AEProcessingPattern source; + try { + source = new AEProcessingPattern(definition); + } catch (RuntimeException exception) { + // 连 AE payload 都读不出来就没有可复用的输入,交给调用方计为无法修复。 + return ItemStack.EMPTY; + } + return encode(existing, source, entry, level); + } + + /** + * 以当前配方的输入输出为准重建一张万象样板。 + * + *

与 {@link #reencodeExisting} 不同,本方法完全不读取旧样板的 AE payload,而是用 + * {@code entry.recipe()} 当前的输入输出重新生成 processing payload。适用于绑定配方仍在、 + * 但其输入(或输出)已被改动,因而旧 payload 无法再与当前配方匹配的失效样板—— + * 这类样板用 {@link #reencodeExisting} 无法修复。

+ * + *

配方无法编码为 processing 形式或重建结果无法解码时返回 {@link ItemStack#EMPTY}, + * 由调用方保留原栈。

+ */ + public static ItemStack reencodeFromRecipe( + AlloyFurnaceRecipeCatalog.Entry entry, Level level) { + if (entry == null || level == null || entry.recipe() == null) { + return ItemStack.EMPTY; + } + ItemStack processingPattern = createProcessingPattern(entry.recipe()); + if (processingPattern.isEmpty()) { + return ItemStack.EMPTY; + } + IPatternDetails processingDetails = PatternDetailsHelper.decodePattern(processingPattern, level); + if (!(processingDetails instanceof AEProcessingPattern processing)) { + return ItemStack.EMPTY; + } + return encodeProcessingPattern(processingPattern, processing, entry, level); + } + + private static ItemStack encodeProcessingPattern( + ItemStack sourcePattern, AEProcessingPattern processing, + AlloyFurnaceRecipeCatalog.Entry entry, Level level) { + if (sourcePattern == null || processing == null || entry == null || level == null) { + return ItemStack.EMPTY; + } + + AdvancedAlloyFurnaceRecipe recipe = entry.recipe(); + OptionalLong scale = AlloyFurnaceRecipeCatalog.findPatternScale( + level, entry.sourceId(), recipe, processing); + if (scale.isEmpty()) { + return ItemStack.EMPTY; + } + List normalizedInputs = normalizeInputs(processing, scale.getAsLong()); + List normalizedOutputs = normalizeStacks(java.util.Arrays.asList(processing.getOutputs()), scale.getAsLong()); + if (normalizedInputs.isEmpty() || normalizedOutputs.isEmpty()) { + return ItemStack.EMPTY; + } + + // A manually multiplied processing pattern is accepted for conversion, but the + // omniversal result always represents one base recipe operation. + ItemStack normalizedSource = sourcePattern.copy(); + com.sorrowmist.useless.compat.forge1201.recipe.NativeProcessingPatterns.write(normalizedSource, normalizedInputs, normalizedOutputs); + if (!com.sorrowmist.useless.compat.forge1201.recipe.NativeProcessingPatterns.hasPayload(normalizedSource)) { + return ItemStack.EMPTY; + } + + AEItemKey normalizedDefinition = AEItemKey.of(normalizedSource); + if (normalizedDefinition == null) { + return ItemStack.EMPTY; + } + AEProcessingPattern normalizedPattern; + try { + normalizedPattern = new AEProcessingPattern(normalizedDefinition); + } catch (RuntimeException exception) { + return ItemStack.EMPTY; + } + + List dynamicInputs = new ArrayList<>(); + List dynamicOutputs = new ArrayList<>(); + IPatternDetails resolved = AdvancedAlloyFurnacePatternResolver.resolve( + normalizedPattern, level, entry.sourceId()); + if (resolved instanceof DynamicComponentPattern dynamic) { + for (int slot = 0; slot < resolved.getInputs().length; slot++) { + if (dynamic.isItemIdInput(slot)) dynamicInputs.add(slot); + } + for (int slot = 0; slot < resolved.getOutputs().length; slot++) { + if (dynamic.isItemIdOutput(slot)) dynamicOutputs.add(slot); + } + } + + dynamicInputs = resolveItemIdInputSlots(recipe, normalizedPattern, dynamicInputs); + Map>> tagInputs = + resolveTagInputSlots(recipe, normalizedPattern); + Map>> fluidTagInputs = + resolveFluidTagInputSlots(recipe, normalizedPattern); + List moldTagInputs = + resolveMoldTagInputSlots(recipe); + List encodedTagInputs = new ArrayList<>(); + for (Map.Entry>> tagInput : tagInputs.entrySet()) { + for (TagKey tag : tagInput.getValue()) { + encodedTagInputs.add(new OmniversalPatternData.TagInputSlot(tagInput.getKey(), tag)); + } + } + List encodedFluidTagInputs = new ArrayList<>(); + for (Map.Entry>> tagInput : fluidTagInputs.entrySet()) { + for (TagKey tag : tagInput.getValue()) { + encodedFluidTagInputs.add(new OmniversalPatternData.FluidTagInputSlot(tagInput.getKey(), tag)); + } + } + List displayMolds = new ArrayList<>(); + if (recipe.molds().size() > 1) { + for (Ingredient mold : recipe.molds()) { + ItemStack[] options = mold.getItems(); + if (options.length > 0) { + AEItemKey key = AEItemKey.of(options[0]); + if (key != null) displayMolds.add(key); + } + } + } + Optional displayMold = recipe.molds().size() == 1 + ? firstDisplayMold(recipe.molds().get(0)) : Optional.empty(); + OmniversalPatternData data = new OmniversalPatternData( + OmniversalPatternData.CURRENT_VERSION, + entry.identity().recipeId(), + entry.identity().fingerprint(), + entry.sourceId(), + !recipe.molds().isEmpty(), + displayMold, + displayMolds, + encodedTagInputs, + encodedFluidTagInputs, + moldTagInputs, + dynamicInputs, + dynamicOutputs); + + ItemStack result = new ItemStack(ModItems.OMNIVERSAL_PATTERN.get()); + com.sorrowmist.useless.compat.forge1201.recipe.NativeProcessingPatterns.copyTo(normalizedSource, result); + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(result, UComponents.OMNIVERSAL_PATTERN_DATA.get(), data); + return result; + } + + private static List normalizeInputs( + AEProcessingPattern source, long scale) { + if (source == null || scale <= 0L) return List.of(); + + List result = new ArrayList<>(); + for (IPatternDetails.IInput input : source.getInputs()) { + if (input == null || input.getMultiplier() <= 0L + || input.getMultiplier() % scale != 0L) { + return List.of(); + } + GenericStack[] possible = input.getPossibleInputs(); + if (possible == null || possible.length == 0 + || possible[0] == null || possible[0].what() == null) { + return List.of(); + } + result.add(new GenericStack( + possible[0].what(), input.getMultiplier() / scale)); + } + return List.copyOf(result); + } + + private static List normalizeStacks( + List source, long scale) { + if (source == null || source.isEmpty() || scale <= 0L) return List.of(); + + List result = new ArrayList<>(source.size()); + for (GenericStack stack : source) { + if (stack == null || stack.what() == null || stack.amount() <= 0L + || stack.amount() % scale != 0L) { + return List.of(); + } + result.add(new GenericStack(stack.what(), stack.amount() / scale)); + } + return List.copyOf(result); + } + + private static Optional firstDisplayMold(Ingredient mold) { + ItemStack[] options = mold.getItems(); + return options.length == 0 ? Optional.empty() : Optional.ofNullable(AEItemKey.of(options[0])); + } + + /** + * Adds the input slots whose source ingredient ignores item components. + * + *

AE2 processing patterns encode a concrete {@link AEItemKey}, even for a vanilla + * {@link Ingredient} such as {@code Ingredient.of(Items.GOAT_HORN)}. That accidentally + * makes the pattern require the exact horn instrument selected while encoding. The furnace + * recipe does not have that restriction, so its omniversal pattern must use item-id matching + * for those slots. Non-simple ingredients, including {@code DataComponentIngredient}, remain + * exact.

+ * + *

The source pattern may have condensed repeated inputs. Classifying its already-condensed + * slots avoids relying on recipe-input positions and keeps the stored slot indices aligned with + * AE2's {@link AEProcessingPattern#getInputs()}.

+ */ + static List resolveItemIdInputSlots( + AdvancedAlloyFurnaceRecipe recipe, + AEProcessingPattern source, + Iterable declaredSlots) { + TreeSet slots = new TreeSet<>(); + if (declaredSlots != null) { + for (Integer slot : declaredSlots) { + if (slot != null) { + slots.add(slot); + } + } + } + slots.addAll(componentAgnosticInputSlots(recipe, source)); + return List.copyOf(slots); + } + + static Map>> resolveTagInputSlots( + AdvancedAlloyFurnaceRecipe recipe, AEProcessingPattern source) { + if (recipe == null || source == null || recipe.inputs().isEmpty()) { + return Map.of(); + } + + Map>> result = new java.util.TreeMap<>(); + IPatternDetails.IInput[] inputs = source.getInputs(); + for (int slot = 0; slot < inputs.length; slot++) { + IPatternDetails.IInput input = inputs[slot]; + if (input == null) continue; + java.util.LinkedHashSet> tags = new java.util.LinkedHashSet<>(); + boolean matchedRequirement = false; + boolean hasNonTagRequirement = false; + for (GenericStack possible : input.getPossibleInputs()) { + if (possible == null || !(possible.what() instanceof AEItemKey itemKey)) { + hasNonTagRequirement = true; + continue; + } + boolean matchedPossible = false; + for (CountedIngredient requirement : recipe.inputs()) { + if (requirement == null || requirement.count() <= 0L) continue; + Ingredient ingredient = requirement.ingredient(); + if (ingredient == null || !ingredient.test(itemKey.toStack(1))) continue; + matchedPossible = true; + matchedRequirement = true; + Optional> tag = directItemTag(ingredient); + if (tag.isPresent()) { + tags.add(tag.get()); + } else { + // A concrete or component-sensitive requirement sharing this encoded + // item must keep the slot strict; otherwise the tag branch would widen + // the recipe beyond the original allocator semantics. + hasNonTagRequirement = true; + } + } + if (!matchedPossible) { + hasNonTagRequirement = true; + } + } + if (matchedRequirement && !hasNonTagRequirement && !tags.isEmpty()) { + result.put(slot, List.copyOf(tags)); + } + } + return Map.copyOf(result); + } + + static Map>> resolveFluidTagInputSlots( + AdvancedAlloyFurnaceRecipe recipe, AEProcessingPattern source) { + if (recipe == null || source == null || recipe.inputFluids().isEmpty()) return Map.of(); + + Map>> result = new java.util.TreeMap<>(); + IPatternDetails.IInput[] inputs = source.getInputs(); + for (int slot = 0; slot < inputs.length; slot++) { + IPatternDetails.IInput input = inputs[slot]; + if (input == null) continue; + java.util.LinkedHashSet> tags = new java.util.LinkedHashSet<>(); + boolean matchedRequirement = false; + boolean hasNonTagRequirement = false; + for (GenericStack possible : input.getPossibleInputs()) { + if (possible == null || !(possible.what() instanceof appeng.api.stacks.AEFluidKey fluidKey)) { + hasNonTagRequirement = true; + continue; + } + boolean matchedPossible = false; + FluidStack representative = fluidKey.toStack(1); + for (LongSizedFluidIngredient requirement : recipe.inputFluids()) { + if (requirement == null || requirement.ingredient() == null + || requirement.amount() <= 0 || !requirement.ingredient().test(representative)) continue; + matchedPossible = true; + matchedRequirement = true; + if (!collectTagOnlyFluidIngredients(requirement.ingredient(), tags)) { + hasNonTagRequirement = true; + } + } + if (!matchedPossible) hasNonTagRequirement = true; + } + if (matchedRequirement && !hasNonTagRequirement && !tags.isEmpty()) { + result.put(slot, List.copyOf(tags)); + } + } + return Map.copyOf(result); + } + + /** + * Keeps pure tag-backed mold requirements visible on the encoded pattern. Mold requirements are + * deliberately not added to the AE processing inputs: they are reusable machine-side tools, not + * consumed network materials. The bound recipe remains authoritative at execution time. + */ + public static List resolveMoldTagInputSlots( + AdvancedAlloyFurnaceRecipe recipe) { + if (recipe == null || recipe.molds().isEmpty()) return List.of(); + + List result = new ArrayList<>(); + for (int moldSlot = 0; moldSlot < recipe.molds().size(); moldSlot++) { + Ingredient mold = recipe.molds().get(moldSlot); + java.util.LinkedHashSet> tags = new java.util.LinkedHashSet<>(); + if (!collectTagOnlyItemIngredients(mold, tags)) continue; + for (TagKey tag : tags) { + result.add(new OmniversalPatternData.MoldTagInputSlot(moldSlot, tag)); + } + } + return List.copyOf(result); + } + + private static boolean collectTagOnlyItemIngredients( + Ingredient ingredient, java.util.Set> tags) { + if (ingredient == null || ingredient.isEmpty()) return false; + if (!ingredient.isVanilla()) { + return collectKnownCustomTagIngredient(ingredient, tags); + } + var nativeTags = com.sorrowmist.useless.compat.forge1201.recipe.NativeIngredients.itemTags(ingredient); + if (nativeTags.isEmpty()) return false; + tags.addAll(nativeTags.get()); + return true; + } + + /** + * Some compatibility mods expose a semantic category as a custom Ingredient so they can apply + * their own matching rules. The encoded pattern cannot serialize that custom predicate. An + * exact registered tag is faithful; when no exact tag exists, a contained category tag is used + * for display only while the original custom predicate remains authoritative at runtime. + */ + private static boolean collectKnownCustomTagIngredient( + Ingredient ingredient, java.util.Set> tags) { + ItemStack[] candidates; + try { + candidates = ingredient.getItems(); + } catch (RuntimeException exception) { + return false; + } + if (candidates == null || candidates.length == 0) return false; + Set candidateItems = Collections.newSetFromMap(new IdentityHashMap<>()); + for (ItemStack candidate : candidates) { + if (candidate == null || candidate.isEmpty() || candidate.getCount() <= 0 + || candidate.hasTag() + || !candidateItems.add(candidate.getItem()) + || !ingredient.test(new ItemStack(candidate.getItem()))) { + return false; + } + } + + List categoryTags = new ArrayList<>(); + boolean found = false; + for (Pair, ? extends Iterable>> tag : BuiltInRegistries.ITEM.getTags().toList()) { + Set tagItems = Collections.newSetFromMap(new IdentityHashMap<>()); + for (Holder holder : tag.getSecond()) { + tagItems.add(holder.value()); + } + if (tagItems.equals(candidateItems)) { + tags.add(tag.getFirst()); + found = true; + continue; + } + + // Some semantic ingredients enumerate a broad item category instead of using a + // registered tag. A registered category tag is still useful for display when every + // member belongs to the ingredient; the original custom Ingredient remains the actual + // mold predicate at runtime. Prefer the largest such category to avoid displaying an + // arbitrary one-item tag for a broad ingredient. + // getItems() is only a display representation for custom ingredients. In particular, + // HoeIngredient exposes one representative hoe even though its predicate accepts every + // hoe. Do not require the representative list to contain the whole registered tag; + // probe the tag members against the custom predicate instead. + if (tagItems.size() >= 2 + && tagItems.stream().anyMatch(candidateItems::contains)) { + boolean acceptsAllMembers = true; + for (Item item : tagItems) { + try { + if (!ingredient.test(new ItemStack(item))) { + acceptsAllMembers = false; + break; + } + } catch (RuntimeException exception) { + acceptsAllMembers = false; + break; + } + } + if (acceptsAllMembers) { + categoryTags.add(new ItemTagCandidate(tag.getFirst(), tagItems.size())); + } + } + } + if (found || categoryTags.isEmpty()) return found; + + categoryTags.sort(Comparator.comparingInt(ItemTagCandidate::size).reversed() + .thenComparing(candidate -> candidate.tag().location().toString())); + tags.add(categoryTags.get(0).tag()); + return true; + } + + private record ItemTagCandidate(TagKey tag, int size) { + } + + private static boolean collectTagOnlyFluidIngredients( + FluidIngredient ingredient, java.util.Set> tags) { + var nativeTags = ingredient.tagOnlyTags(); + if (nativeTags.isEmpty()) return false; + tags.addAll(nativeTags.get()); + return true; + } + + private static Optional> directItemTag(Ingredient ingredient) { + return com.sorrowmist.useless.compat.forge1201.recipe.NativeIngredients.directItemTag(ingredient); + } + + /** + * Finds encoded item slots that can be matched by item id without changing source-recipe + * semantics. A non-simple requirement that accepts the encoded stack makes the slot strict: + * this handles overlapping ordinary and component-sensitive requirements conservatively. + */ + static List componentAgnosticInputSlots( + AdvancedAlloyFurnaceRecipe recipe, AEProcessingPattern source) { + if (recipe == null || source == null || recipe.inputs().isEmpty()) { + return List.of(); + } + + List result = new ArrayList<>(); + IPatternDetails.IInput[] inputs = source.getInputs(); + for (int slot = 0; slot < inputs.length; slot++) { + IPatternDetails.IInput input = inputs[slot]; + if (input == null || !isComponentAgnosticRecipeInput(recipe.inputs(), input.getPossibleInputs())) { + continue; + } + result.add(slot); + } + return List.copyOf(result); + } + + private static boolean isComponentAgnosticRecipeInput( + List requirements, GenericStack[] possibleInputs) { + if (possibleInputs == null || possibleInputs.length == 0) { + return false; + } + + boolean foundItem = false; + for (GenericStack possible : possibleInputs) { + if (possible == null || !(possible.what() instanceof AEItemKey itemKey) + || !isComponentAgnosticRequirement(requirements, itemKey.toStack(1))) { + return false; + } + foundItem = true; + } + return foundItem; + } + + private static boolean isComponentAgnosticRequirement( + List requirements, ItemStack encodedStack) { + boolean matched = false; + for (CountedIngredient requirement : requirements) { + if (requirement == null || requirement.count() <= 0L) { + continue; + } + Ingredient ingredient = requirement.ingredient(); + if (ingredient == null || !ingredient.test(encodedStack)) { + continue; + } + if (!ingredient.isSimple()) { + return false; + } + matched = true; + } + return matched; + } + + public static ItemStack createProcessingPattern(AdvancedAlloyFurnaceRecipe recipe) { + if (recipe == null) return ItemStack.EMPTY; + List inputs = new ArrayList<>(); + for (CountedIngredient input : recipe.inputs()) { + if (input == null || input.count() <= 0 || input.ingredient() == null) return ItemStack.EMPTY; + ItemStack representative = AdapterUtils.itemRepresentative(input.ingredient()); + AEItemKey key = representative == null ? null : AEItemKey.of(representative); + if (key == null) return ItemStack.EMPTY; + inputs.add(new GenericStack(key, input.count())); + } + for (LongSizedFluidIngredient input : recipe.inputFluids()) { + if (input == null || input.ingredient() == null || input.ingredient().isEmpty() + || input.amount() <= 0) return ItemStack.EMPTY; + FluidStack[] candidates = input.getRepresentatives(); + FluidStack representative = candidates.length == 0 + ? AdapterUtils.fluidRepresentative(input.ingredient(), 1) + : candidates[0]; + if (representative == null || representative.isEmpty()) return ItemStack.EMPTY; + appeng.api.stacks.AEFluidKey key = appeng.api.stacks.AEFluidKey.of(representative); + if (key == null) return ItemStack.EMPTY; + GenericStack fluid = new GenericStack(key, input.amount()); + // AE processing patterns have one concrete key per slot. Keep one representative + // here; omniversal metadata restores the Tag/Compound semantics on decode. + inputs.add(fluid); + } + for (GenericStack input : recipe.keyInputs()) { + if (input == null || input.what() == null || input.amount() <= 0L) return ItemStack.EMPTY; + inputs.add(input); + } + + List outputs = new ArrayList<>(); + for (ItemStack output : recipe.outputs()) { + if (output == null || output.isEmpty() || output.getCount() <= 0) return ItemStack.EMPTY; + GenericStack converted = GenericStack.fromItemStack(output); + if (converted == null || converted.what() == null || converted.amount() <= 0L) { + return ItemStack.EMPTY; + } + outputs.add(converted); + } + for (FluidStack output : recipe.outputFluids()) { + if (output == null || output.isEmpty() || output.getAmount() <= 0) return ItemStack.EMPTY; + GenericStack converted = GenericStack.fromFluidStack(output); + if (converted == null || converted.what() == null || converted.amount() <= 0L) { + return ItemStack.EMPTY; + } + outputs.add(converted); + } + for (GenericStack output : recipe.keyOutputs()) { + if (output == null || output.what() == null || output.amount() <= 0L) return ItemStack.EMPTY; + outputs.add(output); + } + if (inputs.isEmpty() || outputs.isEmpty()) return ItemStack.EMPTY; + return PatternDetailsHelper.encodeProcessingPattern(inputs.toArray(GenericStack[]::new), outputs.toArray(GenericStack[]::new)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/OmniversalPatternRecipeSelector.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/OmniversalPatternRecipeSelector.java new file mode 100644 index 00000000..5ba90d60 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/OmniversalPatternRecipeSelector.java @@ -0,0 +1,236 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import appeng.api.crafting.IPatternDetails; +import appeng.crafting.pattern.AECraftingPattern; +import com.sorrowmist.useless.compat.forge1201.recipe.NativeCraftingPatternData; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeCatalog; +import com.sorrowmist.useless.content.recipe.MoldMatcher; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.inventory.CraftingContainer; +import net.minecraft.world.item.crafting.CraftingRecipe; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; + +import java.math.BigInteger; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** Selects the recipe represented by a processing pattern and a mold hub inventory. */ +public final class OmniversalPatternRecipeSelector { + private OmniversalPatternRecipeSelector() { + } + + /** + * Selects the most specific available recipe first, then the recipe with the highest output + * ratio. The recipe id is the final stable tie-breaker. + */ + public static Optional select( + Level level, IPatternDetails pattern, List availableMolds) { + return select(level, pattern, MoldMatcher.prepare(MoldMatcher.sparseSlots(availableMolds))); + } + + public static Optional select( + Level level, IPatternDetails pattern, Map availableMolds) { + return select(level, pattern, MoldMatcher.prepare(availableMolds)); + } + + public static Optional select( + Level level, IPatternDetails pattern, MoldMatcher.PreparedMolds availableMolds) { + if (level == null || pattern == null) return Optional.empty(); + + List candidates = + AlloyFurnaceRecipeCatalog.findPatternCandidates(level, pattern); + // Prefer a complete processing pattern. Only when no complete recipe matches do we accept + // a pattern whose secondary outputs were removed by the player. + if (candidates.isEmpty()) { + candidates = AlloyFurnaceRecipeCatalog.findPatternCandidates(level, pattern, true); + } + if (candidates.isEmpty()) return Optional.empty(); + + return selectCandidate(candidates, availableMolds); + } + + /** + * Selects the existing workbench conversion represented by an AE crafting pattern. + * + *

Crafting patterns retain the source workbench recipe id, while the alloy-furnace + * catalogue stores the corresponding converted recipe with the standard {@code _converted} + * suffix. Restricting this lookup to that exact id and the Minecraft source prevents a crafting + * pattern from being converted merely because another adapter happens to have the same input + * and output shape.

+ */ + public static Optional selectCrafting( + Level level, AECraftingPattern pattern, MoldMatcher.PreparedMolds availableMolds) { + if (level == null || pattern == null) return Optional.empty(); + + var encoded = com.sorrowmist.useless.compat.forge1201.recipe.NativeCraftingPatternData.read(pattern.getDefinition()); + if (encoded == null) return Optional.empty(); + + RecipeHolder sourceHolder = com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.byKey(level.getRecipeManager(), encoded.recipeId()).orElse(null); + if (!(sourceHolder != null && sourceHolder.value() instanceof CraftingRecipe sourceRecipe) + || !matchesSourcePattern(sourceRecipe, encoded, level)) { + return Optional.empty(); + } + + ResourceLocation convertedId; + try { + convertedId = AdapterUtils.convertedId(encoded.recipeId()); + } catch (RuntimeException exception) { + return Optional.empty(); + } + + List candidates = AlloyFurnaceRecipeCatalog + .entries(level, RecipeSourceIds.MINECRAFT).stream() + .filter(entry -> entry != null + && convertedId.equals(entry.identity().recipeId())) + .toList(); + if (candidates.isEmpty()) return Optional.empty(); + + return selectCandidate(candidates, availableMolds); + } + + /** Validates the actual encoded 3x3 grid before trusting its recipe id. */ + private static boolean matchesSourcePattern( + CraftingRecipe recipe, NativeCraftingPatternData encoded, Level level) { + List encodedInputs = encoded.inputs(); + if (encodedInputs == null || encodedInputs.size() > 9) return false; + + List grid = new java.util.ArrayList<>(9); + for (ItemStack input : encodedInputs) { + if (input == null) return false; + grid.add(input.copy()); + } + while (grid.size() < 9) grid.add(ItemStack.EMPTY); + + try { + CraftingContainer input = com.sorrowmist.useless.compat.forge1201.recipe.CraftingInputs.of(3, 3, grid); + if (!recipe.matches(input, level)) return false; + ItemStack assembled = recipe.assemble(input, level.registryAccess()); + return assembled != null && !assembled.isEmpty() + && ItemStack.isSameItemSameTags(assembled, encoded.result()); + } catch (RuntimeException exception) { + return false; + } + } + + private static Optional selectCandidate( + List candidates, + MoldMatcher.PreparedMolds availableMolds) { + if (candidates == null || candidates.isEmpty()) return Optional.empty(); + + // Prefer the most specific and most productive recipe. For example, a SAG Mill + grinding + // ball recipe must be checked before the plain SAG Mill recipe, otherwise the latter would + // consume the same input and output shape and silently lose the grinding-ball bonus. + MoldMatcher.PreparedMolds molds = availableMolds == null + ? MoldMatcher.prepare(Map.of()) : availableMolds; + // The mold map is already sparse, so each candidate is tested once instead of testing the + // same candidate once for every occupied hub slot. Only candidates that can actually use + // the current hub are scored, avoiding a full sort of unusable recipes. + CandidateScore best = null; + for (AlloyFurnaceRecipeCatalog.Entry candidate : candidates) { + AdvancedAlloyFurnaceRecipe recipe = candidate.recipe(); + if (recipe == null || !molds.matches(recipe.molds())) { + continue; + } + CandidateScore score = new CandidateScore(candidate); + if (best == null || compareCandidates(score, best) < 0) best = score; + } + + return best == null ? Optional.empty() : Optional.of(best.entry()); + } + + private static int compareCandidates(CandidateScore left, CandidateScore right) { + int moldComparison = Integer.compare(right.moldCount(), left.moldCount()); + if (moldComparison != 0) return moldComparison; + + int outputRatioComparison = compareOutputRatio( + right.inputAmount(), right.outputAmount(), + left.inputAmount(), left.outputAmount()); + if (outputRatioComparison != 0) return outputRatioComparison; + + return left.entry().identity().recipeId().toString() + .compareTo(right.entry().identity().recipeId().toString()); + } + + /** Compares total declared output amount per total declared input amount without overflow. */ + private static int compareOutputRatio( + BigInteger leftInputs, BigInteger leftOutputs, + BigInteger rightInputs, BigInteger rightOutputs) { + if (leftInputs.signum() <= 0 || rightInputs.signum() <= 0) { + return Integer.compare(leftInputs.signum(), rightInputs.signum()); + } + return leftOutputs.multiply(rightInputs).compareTo(rightOutputs.multiply(leftInputs)); + } + + private static BigInteger totalInputAmount(AdvancedAlloyFurnaceRecipe recipe) { + BigInteger result = BigInteger.ZERO; + for (var input : recipe.inputs()) { + if (input != null && input.count() > 0L) { + result = result.add(BigInteger.valueOf(input.count())); + } + } + for (var input : recipe.inputFluids()) { + if (input != null && input.amount() > 0L) { + result = result.add(BigInteger.valueOf(input.amount())); + } + } + for (var input : recipe.keyInputs()) { + if (input != null && input.amount() > 0L) { + result = result.add(BigInteger.valueOf(input.amount())); + } + } + return result; + } + + private static BigInteger totalOutputAmount(AdvancedAlloyFurnaceRecipe recipe) { + BigInteger result = BigInteger.ZERO; + for (ItemStack output : recipe.outputs()) { + if (output != null && !output.isEmpty() && output.getCount() > 0) { + result = result.add(BigInteger.valueOf(output.getCount())); + } + } + for (var output : recipe.outputFluids()) { + if (output != null && !output.isEmpty() && output.getAmount() > 0L) { + result = result.add(BigInteger.valueOf(output.getAmount())); + } + } + for (var output : recipe.keyOutputs()) { + if (output != null && output.what() != null && output.amount() > 0L) { + result = result.add(BigInteger.valueOf(output.amount())); + } + } + return result; + } + + private record CandidateScore( + AlloyFurnaceRecipeCatalog.Entry entry, + int moldCount, + BigInteger inputAmount, + BigInteger outputAmount) { + private CandidateScore(AlloyFurnaceRecipeCatalog.Entry entry) { + this(entry, + moldCount(entry.recipe()), + totalInputAmount(entry.recipe()), + totalOutputAmount(entry.recipe())); + } + + private static int moldCount(AdvancedAlloyFurnaceRecipe recipe) { + return recipe == null ? 0 : recipe.molds().size(); + } + + private static BigInteger totalInputAmount(AdvancedAlloyFurnaceRecipe recipe) { + if (recipe == null) return BigInteger.ZERO; + return OmniversalPatternRecipeSelector.totalInputAmount(recipe); + } + + private static BigInteger totalOutputAmount(AdvancedAlloyFurnaceRecipe recipe) { + if (recipe == null) return BigInteger.ZERO; + return OmniversalPatternRecipeSelector.totalOutputAmount(recipe); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/OmniversalPatternUploader.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/OmniversalPatternUploader.java new file mode 100644 index 00000000..b0ca23b8 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/OmniversalPatternUploader.java @@ -0,0 +1,254 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import appeng.api.networking.IGrid; +import appeng.api.networking.IGridNode; +import appeng.api.networking.security.IActionHost; +import appeng.api.stacks.AEItemKey; +import appeng.crafting.pattern.AEProcessingPattern; +import appeng.helpers.IPatternTerminalLogicHost; +import com.sorrowmist.useless.content.blockentities.multiblock.MePatternAssemblyBlockEntity; +import com.sorrowmist.useless.content.blockentities.multiblock.MultiblockAlloyFurnaceCoreBlockEntity; +import com.sorrowmist.useless.content.blockentities.multiblock.OmniversalMoldHubBlockEntity; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeCatalog; +import com.sorrowmist.useless.core.component.OmniversalPatternData; +import com.sorrowmist.useless.core.component.UComponents; +import net.minecraft.core.GlobalPos; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +/** + * 把编码终端刚产出的万象样板移交给同一张 AE 网格上的多方块合金炉样板总成。 + * + *

移交目标限定为 {@link MePatternAssemblyBlockEntity},而非同样实现 + * {@code PatternContainer} 的单方块高级合金炉。区分依据是该总成是多方块合金炉在网格上 + * 唯一挂节点的部件,只有其背后的多方块核心声明了 bigint 批次语义;万象样板在该处才能以 + * 超过 long 的次数派发。单方块高级合金炉的万象样板不受本类影响,仍由玩家自行放置。

+ * + *

目标发现方式与 AE2 样板访问终端一致:以 {@code getMachineClasses()} 过滤出 + * 目标宿主类,再按该类取已通电机器。写入前先以 {@code simulateAdd} 确认可容纳, + * 避免在多个总成上留下部分插入。

+ * + *

网格上存在多台总成时按模具择优:只写入背后合金炉的模具仓真正满足这张样板的那一台。 + * 判定沿用合金炉核心做 AE 任务可用性检查时的同一逻辑,避免两处对「模具齐不齐」 + * 产生不同理解。一台都不满足时退回首个可容纳者,使样板不至于滞留在编码终端。

+ * + *

写入前还要对网格上的全部总成查重:只要任一总成已存有输入、输出、模具与配方 + * 完全一致的万象样板,本次便不再写入。等价样板在总成内并存没有意义——它们会各自参与 + * 合成派发,使同一次请求被重复满足,同时白占一份样板槽位。

+ */ +public final class OmniversalPatternUploader { + + private OmniversalPatternUploader() { + } + + /** 一次移交尝试的结局。 */ + public enum Outcome { + /** 样板已写入某个总成。 */ + UPLOADED, + /** 网格上已存在等价样板,本次未写入。 */ + DUPLICATE, + /** 没有可接收的总成:网格上没有已成型总成,或全部总成已满。 */ + SKIPPED + } + + /** + * @param outcome 本次移交的结局 + * @param target 接收样板的总成位置,仅 {@link Outcome#UPLOADED} 时非空 + */ + public record Result(Outcome outcome, @Nullable GlobalPos target) { + } + + /** + * 尝试把样板移交给网格上的多方块合金炉样板总成。 + * + *

本方法只负责移交,不产生任何玩家可见效果;调用方依据结局决定是否清空终端槽位、 + * 是否退回空白样板,并按 {@link PatternUploadNotice} 提示发起编码的玩家。

+ * + * @param host 编码终端的逻辑宿主,用于解析所在网格 + * @param pattern 刚编码完成的万象样板 + * @return 移交结局;{@link Outcome#SKIPPED} 时样板应保留在终端内 + */ + public static Result upload(IPatternTerminalLogicHost host, ItemStack pattern) { + if (pattern.isEmpty()) { + return new Result(Outcome.SKIPPED, null); + } + IGrid grid = resolveGrid(host); + if (grid == null) { + return new Result(Outcome.SKIPPED, null); + } + List targets = collectAssemblies(grid); + if (targets.isEmpty()) { + return new Result(Outcome.SKIPPED, null); + } + if (containsEquivalent(targets, pattern)) { + return new Result(Outcome.DUPLICATE, null); + } + for (MePatternAssemblyBlockEntity target : preferMoldSatisfying(targets, pattern, host.getLevel())) { + if (target.getLevel() == null) { + continue; + } + var terminalInventory = target.getTerminalPatternInventory(); + if (!terminalInventory.simulateAdd(pattern).isEmpty()) { + continue; + } + if (terminalInventory.addItems(pattern).isEmpty()) { + GlobalPos targetPos = GlobalPos.of( + target.getLevel().dimension(), target.getBlockPos().immutable()); + return new Result(Outcome.UPLOADED, targetPos); + } + } + return new Result(Outcome.SKIPPED, null); + } + + /** + * 判定总成内是否已存有与待移交样板等价的万象样板。 + * + *

判等直接比较物品与全部组件:万象样板的输入输出存于 + * {@code AEComponents.ENCODED_PROCESSING_PATTERN},配方 id、语义指纹与模具存于 + * {@code UComponents.OMNIVERSAL_PATTERN_DATA},二者都是值语义的 record。因此 + * 「组件全等」恰好等价于「输入、输出、模具、配方全一致」,既不需要逐个字段比对, + * 也不会漏掉模具标签这类隐式差异。

+ * + *

只扫描总成自身发布的样板库存,即玩家在样板访问终端中能看到的那一份, + * 不涉及终端内尚未移交的样板。

+ */ + private static boolean containsEquivalent(List targets, ItemStack pattern) { + for (MePatternAssemblyBlockEntity target : targets) { + var inventory = target.getTerminalPatternInventory(); + for (int slot = 0; slot < inventory.size(); slot++) { + ItemStack stored = inventory.getStackInSlot(slot); + if (!stored.isEmpty() && ItemStack.isSameItemSameTags(stored, pattern)) { + return true; + } + } + } + return false; + } + + /** + * 把模具仓能满足这张样板的总成排到前面,其余保持原有排序跟随其后。 + * + *

样板不需要模具时直接返回原列表,不做任何反查:绝大多数样板属于这一类, + * 而反查需要解码配方,代价远高于一次列表复制。

+ * + *

一台总成都不满足时同样返回原列表。此时样板仍会写入首个可容纳的总成, + * 而不是滞留在编码终端:模具可能稍后被补上,且这条路径与引入择优之前的行为一致, + * 不会让原本可用的流程因为择优失败而回退。

+ */ + private static List preferMoldSatisfying( + List targets, ItemStack pattern, @Nullable Level level) { + List requiredMolds = resolveRequiredMolds(pattern, level); + if (requiredMolds.isEmpty()) { + return targets; + } + List satisfying = new ArrayList<>(targets.size()); + List rest = new ArrayList<>(targets.size()); + for (MePatternAssemblyBlockEntity target : targets) { + (moldSatisfied(target, requiredMolds) ? satisfying : rest).add(target); + } + if (satisfying.isEmpty()) { + return targets; + } + satisfying.addAll(rest); + return satisfying; + } + + /** + * 读出这张样板要求的模具。 + * + *

反查方式与 {@code OmniversalPatternDetails.decodeUncached} 一致:按元数据版本 + * 分别走旧版兼容路径与当前路径。版本低于标签输入的那批样板用的是表示敏感指纹, + * 必须走兼容路径才解析得出来。

+ * + *

任何一步失败都返回空列表,使调用方退回「不择优」的行为。这里刻意不抛异常: + * 一次上传不该因为配方目录尚未就绪或样板绑定已失效而中断。

+ */ + private static List resolveRequiredMolds(ItemStack pattern, @Nullable Level level) { + OmniversalPatternData data = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(pattern, UComponents.OMNIVERSAL_PATTERN_DATA.get()); + if (data == null || !data.requiresMold() || level == null) { + return List.of(); + } + AEItemKey definition = AEItemKey.of(pattern); + if (definition == null) { + return List.of(); + } + try { + AEProcessingPattern source = new AEProcessingPattern(definition); + Optional resolved = + data.version() < OmniversalPatternData.TAG_INPUT_VERSION + ? AlloyFurnaceRecipeCatalog.resolveLegacyPattern( + level, data.sourceId(), data.identity(), source, data.version()) + : AlloyFurnaceRecipeCatalog.resolvePattern( + level, data.sourceId(), data.identity(), source); + return resolved.map(entry -> entry.recipe().molds()).orElse(List.of()); + } catch (RuntimeException exception) { + return List.of(); + } + } + + /** + * 判定这台总成背后的合金炉,其模具仓能否满足给定的模具需求。 + * + *

判定直接委托给模具仓自身的 {@code containsMolds},即合金炉核心做 AE 任务 + * 可用性检查时用的同一个方法。两处共用一套判定,才不会出现「上传时认为模具齐全、 + * 派发时却报缺模具」这种自相矛盾的状态。

+ * + *

结构未成型、模具仓未连接或对应区块未加载时链路返回 {@code null}, + * 一律视为不满足,让样板落到真正可用的那台总成上。

+ */ + private static boolean moldSatisfied( + MePatternAssemblyBlockEntity target, List requiredMolds) { + MultiblockAlloyFurnaceCoreBlockEntity controller = target.getController(); + if (controller == null) { + return false; + } + OmniversalMoldHubBlockEntity hub = controller.getLinkedMoldHub(); + return hub != null && hub.containsMolds(requiredMolds); + } + + /** + * 解析编码终端所在的 AE 网格。 + * + *

{@link IPatternTerminalLogicHost} 不暴露网格节点,因此需要向下转型到 AE2 的 + * {@link IActionHost}。该接口是网格访问的统一入口:有线编码终端部件与无线终端宿主 + * 都实现了它,故两种终端在此处一视同仁。早期实现只识别 {@code AEBasePart},使无线 + * 终端因宿主并非部件而始终解析不到网格,样板自动上传对无线终端形同失效。

+ * + *

宿主未连接接入点、超出范围或未实现该接口时 {@code getActionableNode()} 返回 + * {@code null},此时本方法同样返回 {@code null},样板保留在终端内,编码流程本身不受影响。

+ */ + private static @Nullable IGrid resolveGrid(IPatternTerminalLogicHost host) { + if (host instanceof IActionHost actionHost) { + IGridNode node = actionHost.getActionableNode(); + return node == null ? null : node.getGrid(); + } + return null; + } + + /** + * 收集网格上已通电的多方块样板总成,并按终端排序值升序排列。 + * + *

排序使「首个可容纳目标」的选择在多次上传之间保持稳定:机器索引本身是无序集合, + * 不排序时同一网格上的多个总成会随节点遍历顺序变化而轮流成为目标。

+ */ + private static List collectAssemblies(IGrid grid) { + Set assemblies = new LinkedHashSet<>(); + for (Class machineClass : grid.getMachineClasses()) { + if (MePatternAssemblyBlockEntity.class.isAssignableFrom(machineClass)) { + assemblies.addAll(grid.getActiveMachines(MePatternAssemblyBlockEntity.class)); + } + } + List result = new ArrayList<>(assemblies); + result.sort(Comparator.comparingLong(MePatternAssemblyBlockEntity::getTerminalSortOrder)); + return result; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/OmniversalPatternWarnings.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/OmniversalPatternWarnings.java new file mode 100644 index 00000000..0805c96e --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/OmniversalPatternWarnings.java @@ -0,0 +1,35 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.core.component.OmniversalPatternData; +import com.sorrowmist.useless.utils.RateLimitedLogger; +import org.slf4j.Logger; + +import java.util.List; + +/** Reports actionable pattern failures without logging normal encoding and dispatch work. */ +public final class OmniversalPatternWarnings { + private static final Logger LOGGER = LogUtils.getLogger(); + + private OmniversalPatternWarnings() {} + + public static void wrapFailed(String kind, Object definition, Exception exception) { + RateLimitedLogger.warn(LOGGER, "Omniversal {} pattern wrapping failed for {}; keeping the plain pattern", + kind, definition, exception); + } + + public static void decodeFailed(Object definition, OmniversalPatternData data, Exception exception) { + RateLimitedLogger.warn(LOGGER, "Omniversal pattern decode failed for {} (recipe={}, fingerprint={})", + definition, data == null ? "" : data.recipeId(), + data == null ? "" : data.recipeFingerprint(), exception); + } + + public static void idOnlySlotsNarrowed(Object definition, List proven, List derived) { + RateLimitedLogger.warn(LOGGER, "Omniversal pattern {} decoded with fewer id-only input slots {} than proven {}; keeping the wider set", + definition, derived, proven); + } + + public static void notPublished(String owner, Object slot, String reason) { + RateLimitedLogger.warn(LOGGER, "Omniversal pattern not published by {} (slot {}): {}", owner, slot, reason); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/PassivePatternInputTransaction.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/PassivePatternInputTransaction.java new file mode 100644 index 00000000..79c77bd3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/PassivePatternInputTransaction.java @@ -0,0 +1,503 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import appeng.api.config.Actionable; +import appeng.api.config.FuzzyMode; +import appeng.api.crafting.IPatternDetails; +import appeng.api.networking.security.IActionSource; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.GenericStack; +import appeng.api.stacks.KeyCounter; +import appeng.api.storage.MEStorage; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Supplier; + +/** Plans and commits one passive pattern extraction without partially consuming network inputs. */ +public final class PassivePatternInputTransaction { + private PassivePatternInputTransaction() { + } + + public enum Failure { + NONE, + MISSING_INPUT, + AMOUNT_OVERFLOW, + STORAGE_CHANGED + } + + public record Result(Failure failure, KeyCounter[] inputs, @Nullable AEKey missingKey) { + public Result { + failure = Objects.requireNonNull(failure, "failure"); + inputs = inputs == null ? new KeyCounter[0] : inputs; + } + + public boolean successful() { + return failure == Failure.NONE; + } + } + + @FunctionalInterface + public interface UnreturnedInputSink { + void accept(AEKey key, long amount); + } + + public static Result extract(IPatternDetails pattern, long operations, @Nullable Level level, + MEStorage storage, Supplier cachedInventory, + IActionSource source, + UnreturnedInputSink unreturnedInputSink) { + return extractAll(List.of(pattern), operations, level, storage, cachedInventory, source, + unreturnedInputSink).get(0); + } + + /** Simulates and commits each pattern in order without enumerating the network for exact inputs. */ + public static List extractAll(List patterns, long operations, + @Nullable Level level, MEStorage storage, + Supplier cachedInventory, IActionSource source, + UnreturnedInputSink unreturnedInputSink) { + long[] perPatternOperations = new long[patterns.size()]; + Arrays.fill(perPatternOperations, operations); + return extractAll(patterns, perPatternOperations, level, storage, cachedInventory, source, + unreturnedInputSink); + } + + /** + * Simulates and commits each pattern in order without enumerating the network for exact inputs. + * {@code operations} carries one batch size per pattern, so callers can run every slot at its + * own configured multiplier within the same cycle. + */ + public static List extractAll(List patterns, + long[] operations, + @Nullable Level level, MEStorage storage, + Supplier cachedInventory, IActionSource source, + UnreturnedInputSink unreturnedInputSink) { + Objects.requireNonNull(patterns, "patterns"); + Objects.requireNonNull(operations, "operations"); + Objects.requireNonNull(storage, "storage"); + Objects.requireNonNull(cachedInventory, "cachedInventory"); + Objects.requireNonNull(source, "source"); + Objects.requireNonNull(unreturnedInputSink, "unreturnedInputSink"); + if (operations.length != patterns.size()) { + throw new IllegalArgumentException("Expected one operation count per pattern: " + + operations.length + " != " + patterns.size()); + } + + AvailableInputIndex inputIndex = new AvailableInputIndex(cachedInventory); + List results = new ArrayList<>(patterns.size()); + for (int index = 0; index < patterns.size(); index++) { + IPatternDetails pattern = Objects.requireNonNull(patterns.get(index), "pattern"); + PlannedExtraction entry = plan( + pattern, operations[index], level, storage, source, inputIndex); + if (entry.failure != Failure.NONE) { + results.add(new Result(entry.failure, entry.inputs, entry.missingKey)); + continue; + } + results.add(commit(storage, source, entry, unreturnedInputSink)); + } + return List.copyOf(results); + } + + private static Result commit(MEStorage storage, IActionSource source, + PlannedExtraction planned, + UnreturnedInputSink unreturnedInputSink) { + KeyCounter extracted = new KeyCounter(); + for (var entry : planned.consumed) { + long amount = storage.extract( + entry.getKey(), entry.getLongValue(), Actionable.MODULATE, source); + if (amount > 0) { + extracted.add(entry.getKey(), amount); + } + if (amount < entry.getLongValue()) { + rollback(storage, source, extracted, unreturnedInputSink); + return new Result(Failure.STORAGE_CHANGED, new KeyCounter[0], entry.getKey()); + } + } + return new Result(Failure.NONE, planned.inputs, null); + } + + private static PlannedExtraction plan( + IPatternDetails pattern, long operations, @Nullable Level level, + MEStorage storage, IActionSource source, AvailableInputIndex inputIndex) { + if (operations <= 0) { + return PlannedExtraction.failure(Failure.AMOUNT_OVERFLOW, null); + } + + IPatternDetails.IInput[] patternInputs = pattern.getInputs(); + DynamicComponentPattern dynamic = pattern instanceof DynamicComponentPattern value ? value : null; + List demands = new ArrayList<>(); + Map availableByKey = new LinkedHashMap<>(); + KeyCounter[] selectedInputs = new KeyCounter[patternInputs.length]; + + for (int slot = 0; slot < patternInputs.length; slot++) { + IPatternDetails.IInput input = patternInputs[slot]; + selectedInputs[slot] = new KeyCounter(); + if (input == null) { + continue; + } + + long required; + try { + required = Math.multiplyExact(input.getMultiplier(), operations); + } catch (ArithmeticException exception) { + return PlannedExtraction.failure(Failure.AMOUNT_OVERFLOW, firstPossibleKey(input)); + } + if (required < 0) { + return PlannedExtraction.failure(Failure.AMOUNT_OVERFLOW, firstPossibleKey(input)); + } + + if (required <= 0) { + continue; + } + + Set candidates = new LinkedHashSet<>(); + GenericStack[] possibleInputs = input.getPossibleInputs(); + if (possibleInputs != null) { + for (GenericStack possible : possibleInputs) { + if (possible == null || possible.what() == null) { + continue; + } + addCandidate(candidates, possible.what(), input, level); + } + } + + if (dynamic != null) { + if (dynamic.isFluidTagInput(slot)) { + for (AEKey candidate : inputIndex.allFluidVariants()) { + addCandidate(candidates, candidate, input, level); + } + } else if (dynamic.isItemIdInput(slot)) { + if (dynamic.isTagInput(slot)) { + for (AEKey candidate : inputIndex.allItemVariants()) { + addCandidate(candidates, candidate, input, level); + } + } else if (possibleInputs != null) { + for (GenericStack possible : possibleInputs) { + if (possible != null && possible.what() instanceof AEItemKey item) { + for (AEKey candidate : inputIndex.itemVariants(item)) { + addCandidate(candidates, candidate, input, level); + } + } + } + } + } + } + + for (AEKey candidate : candidates) { + availableByKey.computeIfAbsent(candidate, key -> Math.max(0L, + storage.extract(key, Long.MAX_VALUE, Actionable.SIMULATE, source))); + } + demands.add(new SlotDemand(slot, required, List.copyOf(candidates))); + } + + KeyAllocation allocation = allocate(demands, availableByKey, patternInputs.length); + if (allocation == null) { + for (SlotDemand demand : demands) { + if (demand.candidates().isEmpty() + || !hasAvailableCandidate(demand, availableByKey)) { + return PlannedExtraction.failure(Failure.MISSING_INPUT, + firstPossibleKey(patternInputs[demand.slot()])); + } + } + return PlannedExtraction.failure(Failure.MISSING_INPUT, + firstPossibleKey(patternInputs[demands.get(0).slot()])); + } + return new PlannedExtraction(Failure.NONE, allocation.selectedInputs(), allocation.consumed(), null); + } + + private static void addCandidate(Set candidates, @Nullable AEKey candidate, + IPatternDetails.IInput input, @Nullable Level level) { + if (candidate != null && input.isValid(candidate, level)) { + candidates.add(candidate); + } + } + + private static boolean hasAvailableCandidate(SlotDemand demand, Map availableByKey) { + for (AEKey candidate : demand.candidates()) { + if (availableByKey.getOrDefault(candidate, 0L) > 0L) { + return true; + } + } + return false; + } + + @Nullable + private static KeyAllocation allocate(List demands, + Map availableByKey, + int slotCount) { + if (demands.isEmpty()) { + return new KeyAllocation(emptyCounters(slotCount), new KeyCounter()); + } + + List supplies = new ArrayList<>(); + long totalDemand = 0L; + for (SlotDemand demand : demands) { + totalDemand = saturatingAdd(totalDemand, demand.amount()); + } + for (Map.Entry entry : availableByKey.entrySet()) { + long amount = entry.getValue() == null ? 0L : entry.getValue(); + if (amount > 0L) { + supplies.add(new KeySupply(entry.getKey(), amount)); + } + } + long totalSupply = 0L; + for (KeySupply supply : supplies) { + totalSupply = saturatingAdd(totalSupply, supply.amount()); + } + if (totalSupply < totalDemand) { + return null; + } + + Map supplyIndexes = new LinkedHashMap<>(); + for (int index = 0; index < supplies.size(); index++) { + supplyIndexes.put(supplies.get(index).key(), index); + } + + int source = 0; + int supplyStart = 1; + int demandStart = supplyStart + supplies.size(); + int sink = demandStart + demands.size(); + FlowNetwork network = new FlowNetwork(sink + 1); + for (int index = 0; index < supplies.size(); index++) { + network.addEdge(source, supplyStart + index, supplies.get(index).amount()); + } + + List> demandEdges = new ArrayList<>(demands.size()); + for (int demandIndex = 0; demandIndex < demands.size(); demandIndex++) { + SlotDemand demand = demands.get(demandIndex); + List edges = new ArrayList<>(); + for (AEKey candidate : demand.candidates()) { + Integer supplyIndex = supplyIndexes.get(candidate); + if (supplyIndex == null) continue; + FlowEdge edge = network.addEdge( + supplyStart + supplyIndex, demandStart + demandIndex, totalDemand); + edges.add(new AllocationEdge(candidate, edge, totalDemand)); + } + demandEdges.add(edges); + network.addEdge(demandStart + demandIndex, sink, demand.amount()); + } + + if (network.maxFlow(source, sink, totalDemand) != totalDemand) { + return null; + } + + KeyCounter[] selectedInputs = emptyCounters(slotCount); + KeyCounter consumed = new KeyCounter(); + for (int demandIndex = 0; demandIndex < demands.size(); demandIndex++) { + KeyCounter selected = selectedInputs[demands.get(demandIndex).slot()]; + for (AllocationEdge allocationEdge : demandEdges.get(demandIndex)) { + long amount = allocationEdge.initialCapacity() - allocationEdge.edge().capacity; + if (amount > 0L) { + selected.add(allocationEdge.key(), amount); + consumed.add(allocationEdge.key(), amount); + } + } + } + return new KeyAllocation(selectedInputs, consumed); + } + + private static KeyCounter[] emptyCounters(int size) { + KeyCounter[] counters = new KeyCounter[size]; + for (int index = 0; index < size; index++) { + counters[index] = new KeyCounter(); + } + return counters; + } + + private static long saturatingAdd(long left, long right) { + if (right > 0L && left > Long.MAX_VALUE - right) return Long.MAX_VALUE; + return left + right; + } + + private static void rollback(MEStorage storage, IActionSource source, KeyCounter extracted, + UnreturnedInputSink unreturnedInputSink) { + for (var entry : extracted) { + long inserted = storage.insert( + entry.getKey(), entry.getLongValue(), Actionable.MODULATE, source); + long remainder = entry.getLongValue() - Math.max(0L, inserted); + if (remainder > 0) { + unreturnedInputSink.accept(entry.getKey(), remainder); + } + } + } + + @Nullable + private static AEKey firstPossibleKey(IPatternDetails.IInput input) { + for (GenericStack possible : input.getPossibleInputs()) { + if (possible != null && possible.what() != null) { + return possible.what(); + } + } + return null; + } + + record PlannedExtraction(Failure failure, KeyCounter[] inputs, + KeyCounter consumed, @Nullable AEKey missingKey) { + static PlannedExtraction failure(Failure failure, @Nullable AEKey missingKey) { + return new PlannedExtraction(failure, new KeyCounter[0], new KeyCounter(), missingKey); + } + } + + private record SlotDemand(int slot, long amount, List candidates) { + } + + private record KeySupply(AEKey key, long amount) { + } + + private record AllocationEdge(AEKey key, FlowEdge edge, long initialCapacity) { + } + + private record KeyAllocation(KeyCounter[] selectedInputs, KeyCounter consumed) { + } + + private static final class FlowNetwork { + private final List> graph; + private final int[] levels; + private final int[] nextEdges; + + private FlowNetwork(int nodeCount) { + this.graph = new ArrayList<>(nodeCount); + for (int index = 0; index < nodeCount; index++) { + this.graph.add(new ArrayList<>()); + } + this.levels = new int[nodeCount]; + this.nextEdges = new int[nodeCount]; + } + + private FlowEdge addEdge(int from, int to, long capacity) { + FlowEdge forward = new FlowEdge(to, this.graph.get(to).size(), capacity); + FlowEdge reverse = new FlowEdge(from, this.graph.get(from).size(), 0L); + this.graph.get(from).add(forward); + this.graph.get(to).add(reverse); + return forward; + } + + private long maxFlow(int source, int sink, long limit) { + long flow = 0L; + while (flow < limit && buildLevels(source, sink)) { + Arrays.fill(this.nextEdges, 0); + long pushed; + while (flow < limit + && (pushed = push(source, sink, limit - flow)) > 0L) { + flow += pushed; + } + } + return flow; + } + + private boolean buildLevels(int source, int sink) { + Arrays.fill(this.levels, -1); + this.levels[source] = 0; + ArrayDeque queue = new ArrayDeque<>(); + queue.add(source); + while (!queue.isEmpty()) { + int node = queue.removeFirst(); + for (FlowEdge edge : this.graph.get(node)) { + if (edge.capacity > 0L && this.levels[edge.to] < 0) { + this.levels[edge.to] = this.levels[node] + 1; + queue.addLast(edge.to); + } + } + } + return this.levels[sink] >= 0; + } + + private long push(int node, int sink, long amount) { + if (node == sink) return amount; + List edges = this.graph.get(node); + for (; this.nextEdges[node] < edges.size(); this.nextEdges[node]++) { + FlowEdge edge = edges.get(this.nextEdges[node]); + if (edge.capacity <= 0L || this.levels[edge.to] != this.levels[node] + 1) { + continue; + } + long pushed = push(edge.to, sink, Math.min(amount, edge.capacity)); + if (pushed <= 0L) continue; + edge.capacity -= pushed; + this.graph.get(edge.to).get(edge.reverseIndex).capacity += pushed; + return pushed; + } + return 0L; + } + } + + private static final class FlowEdge { + private final int to; + private final int reverseIndex; + private long capacity; + + private FlowEdge(int to, int reverseIndex, long capacity) { + this.to = to; + this.reverseIndex = reverseIndex; + this.capacity = capacity; + } + } + + /** Lazily enumerates network keys only when a component-relaxed input needs variants. */ + private static final class AvailableInputIndex { + private final Supplier cachedInventorySupplier; + private final Map> itemVariants = new IdentityHashMap<>(); + private List allItemVariants; + private List allFluidVariants; + private KeyCounter cachedInventory; + + private AvailableInputIndex(Supplier cachedInventorySupplier) { + this.cachedInventorySupplier = cachedInventorySupplier; + } + + private KeyCounter cachedInventory() { + if (cachedInventory == null) { + cachedInventory = Objects.requireNonNull( + cachedInventorySupplier.get(), "cached inventory supplier returned null"); + } + return cachedInventory; + } + + private List itemVariants(AEItemKey template) { + return itemVariants.computeIfAbsent(template.getItem(), ignored -> { + List variants = new ArrayList<>(); + for (var entry : cachedInventory().findFuzzy(template, FuzzyMode.IGNORE_ALL)) { + if (entry.getLongValue() > 0L) { + variants.add(entry.getKey()); + } + } + return List.copyOf(variants); + }); + } + + private List allItemVariants() { + if (allItemVariants == null) { + List variants = new ArrayList<>(); + for (var entry : cachedInventory()) { + if (entry.getLongValue() > 0L && entry.getKey() instanceof AEItemKey) { + variants.add(entry.getKey()); + } + } + allItemVariants = List.copyOf(variants); + } + return allItemVariants; + } + + private List allFluidVariants() { + if (allFluidVariants == null) { + List variants = new ArrayList<>(); + for (var entry : cachedInventory()) { + if (entry.getLongValue() > 0L && entry.getKey() instanceof appeng.api.stacks.AEFluidKey) { + variants.add(entry.getKey()); + } + } + allFluidVariants = List.copyOf(variants); + } + return allFluidVariants; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/PatternStackView.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/PatternStackView.java new file mode 100644 index 00000000..b7e69517 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/PatternStackView.java @@ -0,0 +1,84 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.GenericStack; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** Item-only pattern view that keeps AE amounts as long values. */ +public record PatternStackView(List inputs, List outputs) { + public PatternStackView { + inputs = List.copyOf(inputs == null ? List.of() : inputs); + outputs = List.copyOf(outputs == null ? List.of() : outputs); + } + + @Nullable + public static PatternStackView fromPattern(IPatternDetails pattern) { + if (pattern == null) return null; + + List inputs = new ArrayList<>(); + for (IPatternDetails.IInput input : pattern.getInputs()) { + if (input == null || input.getMultiplier() <= 0L) return null; + AEItemKey key = firstItemKey(input.getPossibleInputs()); + if (key == null) return null; + inputs.add(new GenericStack(key, input.getMultiplier())); + } + + List outputs = new ArrayList<>(); + for (GenericStack output : pattern.getOutputs()) { + if (output == null || output.amount() <= 0L || !(output.what() instanceof AEItemKey)) { + return null; + } + outputs.add(output); + } + return new PatternStackView(inputs, outputs); + } + + public static PatternStackView fromLegacy(List inputs, List outputs) { + return new PatternStackView(toGeneric(inputs), toGeneric(outputs)); + } + + public List inputRepresentatives() { + return representatives(inputs); + } + + public List outputRepresentatives() { + return representatives(outputs); + } + + private static List toGeneric(List stacks) { + if (stacks == null) return List.of(); + List result = new ArrayList<>(stacks.size()); + for (ItemStack stack : stacks) { + if (stack == null || stack.isEmpty()) continue; + GenericStack generic = GenericStack.fromItemStack(stack); + if (generic != null) result.add(generic); + } + return result; + } + + private static List representatives(List stacks) { + List result = new ArrayList<>(stacks.size()); + for (GenericStack stack : stacks) { + if (stack != null && stack.what() instanceof AEItemKey itemKey && stack.amount() > 0L) { + result.add(itemKey.toStack(1)); + } + } + return List.copyOf(result); + } + + @Nullable + private static AEItemKey firstItemKey(GenericStack[] possibleInputs) { + if (possibleInputs == null) return null; + for (GenericStack possible : possibleInputs) { + if (possible != null && possible.what() instanceof AEItemKey itemKey) { + return itemKey; + } + } + return null; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/PatternUploadNotice.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/PatternUploadNotice.java new file mode 100644 index 00000000..55517d34 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/PatternUploadNotice.java @@ -0,0 +1,134 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import appeng.helpers.IPatternTerminalLogicHost; +import appeng.menu.me.items.PatternEncodingTermMenu; +import net.minecraft.ChatFormatting; +import net.minecraft.core.GlobalPos; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.HoverEvent; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +import java.util.Locale; + +/** + * 构造并发送万象样板移交结果对应的玩家提示。 + * + *

提示由坐标、维度与距离三段组成:坐标片段与维度片段各自携带 {@code SUGGEST_COMMAND} + * 点击事件,玩家点击后对应的传送命令被填入聊天框,由玩家自行确认执行。命令采用 + * {@code SUGGEST_COMMAND} 而非 {@code RUN_COMMAND},使玩家在传送前仍可核对目标与落点。

+ * + *

该形态与 ExtendedAE 的样板供应器定位提示一致:坐标片段提供当前维度内的直接传送, + * 维度片段以 {@code execute in} 包裹同一落点,使目标位于其它维度时仍可抵达。

+ * + *

维度取自接收样板的总成,而非编码终端。二者可能不在同一维度:跨维度 AE 网络下 + * 终端位于玩家所在维度,总成却在另一维度,沿用终端维度会给出无法抵达的错误落点。

+ */ +public final class PatternUploadNotice { + + private static final String MESSAGE_KEY = "gui.useless_mod.omniversal_pattern.uploaded"; + private static final String DUPLICATE_KEY = "gui.useless_mod.omniversal_pattern.duplicate"; + private static final String TOOLTIP_KEY = "gui.useless_mod.omniversal_pattern.uploaded.tooltip"; + + private PatternUploadNotice() { + } + + /** + * 反查发起本次编码的玩家。 + * + *

编码终端的逻辑对象不持有玩家引用,因此按「当前打开的容器菜单正以同一个逻辑实例 + * 为编码目标」反查玩家。比较的是逻辑实例而非宿主对象:有线终端部件与无线终端宿主的 + * 对象身份不同,但二者暴露的是同一个 {@code PatternEncodingLogic}。编码由玩家在编码 + * 终端界面内触发,触发时其菜单必然仍指向该逻辑,因此这一反查是可靠的。

+ * + *

客户端预览与无玩家在线的场景返回 {@code null}。调用方除发送提示外,也据此决定 + * 能否退还空白样板,因此该方法对外可见。

+ * + * @param logic 本次编码所经过的逻辑实例,即 mixin 所在对象 + * @param host 编码终端的逻辑宿主,用于取得所在服务端 + */ + public static @Nullable ServerPlayer resolvePlayer(Object logic, IPatternTerminalLogicHost host) { + Level level = host.getLevel(); + if (level == null || level.isClientSide) { + return null; + } + MinecraftServer server = level.getServer(); + if (server == null) { + return null; + } + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + if (player.containerMenu instanceof PatternEncodingTermMenu menu + && menu.getTarget() instanceof IPatternTerminalLogicHost targetHost + && targetHost.getLogic() == logic) { + return player; + } + } + return null; + } + + /** + * 告知玩家样板已移交,并给出可点击的传送目标。 + * + * @param player 发起本次编码的玩家,可能为空(无玩家在线) + * @param target 接收样板的总成位置及其所在维度 + */ + public static void notifyUploaded(@Nullable ServerPlayer player, GlobalPos target) { + if (player != null) { + player.displayClientMessage(uploaded(player, target), false); + } + } + + /** + * 告知玩家网络中已存在等价样板,本次未写入。 + * + * @param player 发起本次编码的玩家,可能为空(无玩家在线) + */ + public static void notifyDuplicate(@Nullable ServerPlayer player) { + if (player != null) { + player.displayClientMessage(Component.translatable(DUPLICATE_KEY), false); + } + } + + /** + * @param player 接收提示的玩家,用于计算与目标的距离 + * @param target 接收样板的总成位置及其所在维度 + * @return 含可点击坐标片段与维度片段的提示文本 + */ + public static Component uploaded(ServerPlayer player, GlobalPos target) { + var pos = target.pos(); + double landingX = pos.getX() + 0.5; + int landingY = pos.getY() + 1; + double landingZ = pos.getZ() + 0.5; + + Component coordinates = Component.literal("[" + pos.toShortString() + "]") + .withStyle(style -> style + .withColor(ChatFormatting.GREEN) + .withClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, + teleportCommand(landingX, landingY, landingZ))) + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.translatable(TOOLTIP_KEY)))); + + String dimensionId = target.dimension().location().toString(); + Component dimensionComponent = Component.literal(dimensionId) + .withStyle(style -> style + .withColor(ChatFormatting.AQUA) + .withClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, + String.format(Locale.ROOT, "/execute in %s run tp @s %.1f %d %.1f", + dimensionId, landingX, landingY, landingZ))) + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.translatable(TOOLTIP_KEY)))); + + int distance = (int) Math.sqrt(player.blockPosition().distSqr(pos)); + return Component.translatable(MESSAGE_KEY, coordinates, dimensionComponent, distance); + } + + /** + * 落点取方块正上方一格:样板总成嵌在多方块结构内部,直接传送到其自身坐标会卡进结构。 + */ + private static String teleportCommand(double x, int y, double z) { + return String.format(Locale.ROOT, "/tp @s %.1f %d %.1f", x, y, z); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/PendingOmniversalPatternHolder.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/PendingOmniversalPatternHolder.java new file mode 100644 index 00000000..03486470 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/PendingOmniversalPatternHolder.java @@ -0,0 +1,31 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeIdentity; +import org.jetbrains.annotations.Nullable; + +/** + * Duck interface mixed into AE2's {@code PatternEncodingLogic} so a pattern encoding terminal can + * remember which alloy-furnace recipe the player picked in JEI. + * + *

The recipe has to be recorded at click time rather than rediscovered at encode time: a JEI + * recipe carries its mold, while an encoded processing pattern does not, and several recipes can + * share the same inputs and outputs while differing only in their mold. Searching the catalog for a + * match therefore cannot tell which one the player meant; the click can. + * + * @see com.sorrowmist.useless.mixin.ae2.PatternEncodingLogicMixin + */ +public interface PendingOmniversalPatternHolder { + @Nullable + AlloyFurnaceRecipeIdentity uselessMod$getPendingOmniversalRecipe(); + + void uselessMod$setPendingOmniversalRecipe(@Nullable AlloyFurnaceRecipeIdentity identity); + + @Nullable + String uselessMod$getPendingOmniversalSourceId(); + + void uselessMod$setPendingOmniversalSourceId(@Nullable String sourceId); + + /** Re-checks a pattern that may have been encoded before the JEI selection packet arrived. */ + default void uselessMod$tryConvertPendingOmniversalPattern() { + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/ProcessingPatternRecipeHolder.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/ProcessingPatternRecipeHolder.java new file mode 100644 index 00000000..68808d48 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/ProcessingPatternRecipeHolder.java @@ -0,0 +1,12 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeIdentity; +import org.jetbrains.annotations.Nullable; + +/** Runtime extension point for recipe-aware processing-pattern converters. */ +public interface ProcessingPatternRecipeHolder { + @Nullable + AlloyFurnaceRecipeIdentity uselessMod$getRecipeIdentity(); + + void uselessMod$setRecipeIdentity(@Nullable AlloyFurnaceRecipeIdentity identity); +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/ReusablePatternInputs.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/ReusablePatternInputs.java new file mode 100644 index 00000000..33127185 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/ReusablePatternInputs.java @@ -0,0 +1,112 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 「可复用输入」判定:被消耗后仍会返还、且返还物仍能当同一个槽的输入的输入。 + * + *

典型例子:神秘农业的注魔水晶(4 精华 + 1 水晶 → 1 高阶精华,水晶不消耗)、可复用模具、 + * 原样返还的工具。

+ * + *

为什么整批只需要 1 份:AE2 的合成模拟里,只要某个输入的 + * {@link IPatternDetails.IInput#getRemainingKey} 非空,{@code CraftingTreeProcess#updateLimitQty} + * 就会置 {@code limitQty},于是 {@code CraftingTreeNode#request} 走 {@code times = 1} + * —— 一次只模拟一件,并把返还物塞回模拟库存供下一件复用。所以一份合成计划对这类输入 + * 永远只算 1 份,与实际下单份数无关。

+ * + *

判据与 AE2 的复用条件逐字对齐:AE2 在 {@code CraftingCpuHelper#getValidItemTemplates} + * 里用 {@code input.isValid(返还键, level)} 过滤可用模板,只有通过过滤的返还物才会被复用。 + * 因此这里要求声明的每一个候选都满足「有返还键,且该键仍是这个槽的合法输入」—— + * 只要有一个候选不满足,AE2 就可能抽到它,此时把倍率压成 1 就会少抽输入。 + * 反过来,{@code CraftingTreeProcess} 的复用判断也只认 {@code isValid},所以本判据与 + * 「计划需要几份」不可能对不上。

+ * + *

只对合成样板有意义:{@code AEProcessingPattern} 的返还键硬编码为 {@code null}, + * 万象样板也恒由处理样板构建,所以处理样板与万象样板在这里恒得空结果,行为完全不变。

+ */ +final class ReusablePatternInputs { + private ReusablePatternInputs() { + } + + /** + * @param pattern 待判定的样板;{@code null} 或没有输入时返回空 + * @param level 用于 {@link IPatternDetails.IInput#isValid} 的关卡;{@code null} 时退回 + * 「原样返还」这一最保守的充分条件(组件不同的一律不认) + * @return 槽位下标 → 该槽在整批里只需 1 份时 AE2 期望收回的返还键;无可复用槽时为空 map + */ + static Map reusableRemainders(@Nullable IPatternDetails pattern, + @Nullable Level level) { + if (pattern == null) { + return Map.of(); + } + IPatternDetails.IInput[] inputs = pattern.getInputs(); + if (inputs == null || inputs.length == 0) { + return Map.of(); + } + + Map result = new LinkedHashMap<>(); + for (int slot = 0; slot < inputs.length; slot++) { + AEKey remainder = reusableRemainder(inputs[slot], level); + if (remainder != null) { + result.put(slot, remainder); + } + } + if (result.isEmpty()) { + return Map.of(); + } + // 保持槽位顺序:账本条目顺序与返还物记录顺序都要可复现。 + return Collections.unmodifiableMap(new LinkedHashMap<>(result)); + } + + /** @return 该槽可复用时返回返还键(以第一个候选算出的为准),否则 {@code null} */ + private static @Nullable AEKey reusableRemainder(@Nullable IPatternDetails.IInput input, + @Nullable Level level) { + if (input == null) { + return null; + } + GenericStack[] candidates = input.getPossibleInputs(); + if (candidates == null || candidates.length == 0) { + return null; + } + + AEKey remainder = null; + for (GenericStack candidate : candidates) { + if (candidate == null || candidate.what() == null) { + return null; + } + AEKey key = candidate.what(); + AEKey remaining = input.getRemainingKey(key); + if (remaining == null || !isStillUsableAsInput(remaining, key, input, level)) { + return null; + } + if (remainder == null) { + remainder = remaining; + } + } + return remainder; + } + + /** + * 返还物是否仍能当这个槽的输入。 + * + *

没有 {@code Level} 时不能用 {@code isValid}(AE2 对组件不同的返还物要跑 + * {@code recipe.matches}),只能退到「键完全相等」——它只覆盖原样返还那一类 + * (模具、大师注魔水晶),带耐久受损的返还物会判为不可复用。宁可少修,不可错修。

+ */ + private static boolean isStillUsableAsInput(AEKey remaining, AEKey key, + IPatternDetails.IInput input, + @Nullable Level level) { + if (level == null) { + return remaining.equals(key); + } + return input.isValid(remaining, level); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/ScaledCraftingPattern.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/ScaledCraftingPattern.java new file mode 100644 index 00000000..1ea584d4 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/ScaledCraftingPattern.java @@ -0,0 +1,75 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.KeyCounter; +import appeng.blockentity.crafting.IMolecularAssemblerSupportedPattern; +import net.minecraft.core.NonNullList; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.inventory.CraftingContainer; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +/** + * 一次推送代表 N 次工作台合成的 AE2 合成样板包装。 + * + *

输入倍率与产物由 {@link ScaledProcessingPattern} 统一处理;本类只保留分子装配器需要的 + * 合成接口,因此它仍然可以作为 {@link ScaledProcessingPattern} 返回给 NeoECOAE。

+ */ +public final class ScaledCraftingPattern extends ScaledProcessingPattern + implements IMolecularAssemblerSupportedPattern { + + public ScaledCraftingPattern(IMolecularAssemblerSupportedPattern pattern, long operationsPerPush) { + this(pattern, operationsPerPush, null); + } + + /** + * @param level 判定「可复用输入」用的关卡,由本模组自己的机器提供(见 + * {@link ScaledProcessingPattern} 的说明);{@code null} 时退回保守判据 + */ + public ScaledCraftingPattern(IMolecularAssemblerSupportedPattern pattern, long operationsPerPush, + @Nullable Level level) { + super(pattern, operationsPerPush, level); + if (!(getOriginal() instanceof IMolecularAssemblerSupportedPattern)) { + throw new IllegalArgumentException("Scaled crafting pattern requires a crafting pattern base"); + } + } + + /** @return 未放大的原始合成样板 */ + public IMolecularAssemblerSupportedPattern getOriginalCraftingPattern() { + return originalCraftingPattern(); + } + + /** 只装配一次;倍率由接收方在放大产物时体现。 */ + @Override + public ItemStack assemble(net.minecraft.world.Container input, Level level) { + return originalCraftingPattern().assemble(input, level); + } + + @Override + public NonNullList getRemainingItems(CraftingContainer input) { + return originalCraftingPattern().getRemainingItems(input); + } + + @Override + public boolean isItemValid(int slot, AEItemKey key, Level level) { + return originalCraftingPattern().isItemValid(slot, key, level); + } + + @Override + public boolean isSlotEnabled(int slot) { + return originalCraftingPattern().isSlotEnabled(slot); + } + + /** + * 只按一份合成铺料:它会从计数器里各扣掉一份合成所需的量, + * 剩下的 (N-1) 份由接收方在执行时按倍率一并消化。 + */ + @Override + public void fillCraftingGrid(KeyCounter[] table, CraftingGridAccessor gridAccessor) { + originalCraftingPattern().fillCraftingGrid(table, gridAccessor); + } + + private IMolecularAssemblerSupportedPattern originalCraftingPattern() { + return (IMolecularAssemblerSupportedPattern) getOriginal(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/ScaledPattern.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/ScaledPattern.java new file mode 100644 index 00000000..2829c26a --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/ScaledPattern.java @@ -0,0 +1,20 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import appeng.api.crafting.IPatternDetails; + +/** + * “一次 AE 推送代表多份操作”的包装契约。 + * + *

处理样板与合成样板各有一个包装实现:处理样板放大的是机器要执行的配方次数, + * 合成样板放大的是工作台合成的次数。两者都必须让 AE2 从声明值里看出倍率 —— + * 输入倍率放大后 AE2 才会一次抽出 N 份材料,产物放大后 AE2 才会把 N 份预期产物写进 + * CPU 的 {@code waitingFor},从而与“每次推送只扣 1 次任务计数”的原版记账保持一致。

+ */ +public interface ScaledPattern { + + /** @return 被包装的原始样板 */ + IPatternDetails getOriginal(); + + /** @return 本次推送代表的原始操作次数,恒为正数 */ + long getOperationsPerPush(); +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/ScaledProcessingPattern.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/ScaledProcessingPattern.java new file mode 100644 index 00000000..e0ab6109 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/ScaledProcessingPattern.java @@ -0,0 +1,169 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import appeng.api.stacks.KeyCounter; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** One processing-pattern push representing multiple operations. */ +public class ScaledProcessingPattern implements IPatternDetails, ScaledPattern { + private final IPatternDetails original; + private final long operationsPerPush; + private final AEItemKey definition; + private final IInput[] inputs; + private final List outputs; + + /** + * 不带 {@link Level} 的重载:只认「原样返还」的可复用输入,带耐久受损的返还物(注魔水晶这类) + * 判不出来。会走这条路的调用方都应改成带 {@code level} 的那个。 + */ + public ScaledProcessingPattern(IPatternDetails pattern, long operationsPerPush) { + this(pattern, operationsPerPush, null); + } + + /** + * @param pattern 被包装的原始样板 + * @param operationsPerPush 本次推送代表的原始操作次数,恒为正 + * @param level 判定「可复用输入」用的关卡。由本模组自己的机器提供 + * (`AlloyFurnaceCountedCraftingAdapter#providerLevel` / + * `SmartDoublingPlanner` 从供应器方块实体取), + * 不从 AE2 网格上取 —— 那条路要在 `CraftingService` 上挂 mixin, + * 会静默顶掉 OmniSequence 的注入。{@code null} 时退回保守判据 + */ + public ScaledProcessingPattern(IPatternDetails pattern, long operationsPerPush, + @Nullable Level level) { + SmartDoublingPatterns.Resolved resolved = SmartDoublingPatterns.resolve(pattern); + this.original = resolved.pattern(); + this.operationsPerPush = SmartDoublingPatterns.multiplyExactPositive( + resolved.operationsPerPush(), operationsPerPush, "smart-doubling multiplier"); + if (this.operationsPerPush > SmartDoublingPatterns.maximumSafeMultiplier(this.original)) { + throw new IllegalArgumentException("smart-doubling multiplier would overflow a pattern amount"); + } + + this.definition = SmartDoublingPatterns.executionDefinition(this.original, this.operationsPerPush); + IInput[] originalInputs = this.original.getInputs(); + Set reusableSlots = + ReusablePatternInputs.reusableRemainders(this.original, level).keySet(); + this.inputs = new IInput[originalInputs.length]; + for (int index = 0; index < originalInputs.length; index++) { + this.inputs[index] = new ScaledInput(originalInputs[index], this.operationsPerPush, + reusableSlots.contains(index)); + } + + List scaledOutputs = new ArrayList<>(this.original.getOutputs().length); + for (GenericStack output : this.original.getOutputs()) { + if (output != null) { + scaledOutputs.add(new GenericStack( + output.what(), Math.multiplyExact(output.amount(), this.operationsPerPush))); + } + } + this.outputs = List.copyOf(scaledOutputs); + } + + @Override + public IPatternDetails getOriginal() { + return original; + } + + @Override + public long getOperationsPerPush() { + return operationsPerPush; + } + + @Override + public AEItemKey getDefinition() { + return definition; + } + + @Override + public IInput[] getInputs() { + return inputs.clone(); + } + + @Override + public GenericStack[] getOutputs() { + return outputs.toArray(GenericStack[]::new); + } + + @Override + public boolean supportsPushInputsToExternalInventory() { + return original.supportsPushInputsToExternalInventory(); + } + + @Override + public void pushInputsToExternalInventory(KeyCounter[] inputHolder, PatternInputSink inputSink) { + for (KeyCounter counter : inputHolder) { + if (counter == null) { + continue; + } + for (var input : counter) { + inputSink.pushInput(input.getKey(), input.getLongValue()); + } + } + } + + @Override + public boolean equals(Object object) { + if (object == this) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } + ScaledProcessingPattern other = (ScaledProcessingPattern) object; + return operationsPerPush == other.operationsPerPush + && original.equals(other.original); + } + + @Override + public int hashCode() { + return 31 * original.hashCode() + Long.hashCode(operationsPerPush); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[operationsPerPush=" + operationsPerPush + + ", original=" + original + ']'; + } + + private record ScaledInput(IInput original, long operationsPerPush, boolean reusable) implements IInput { + private ScaledInput { + Objects.requireNonNull(original, "original"); + } + + @Override + public GenericStack[] getPossibleInputs() { + return original.getPossibleInputs(); + } + + /** + * 可复用输入(注魔水晶这类)整批只需要 1 份:AE2 的合成计划对它只算 1 份 + * (见 {@link ReusablePatternInputs}),这里放大就抽不出材料、一次推送都发不出去。 + * 其余输入照旧 ×倍率,AE2 才会一次抽出 N 份、把 N 份预期产物写进 CPU 的 {@code waitingFor}。 + */ + @Override + public long getMultiplier() { + long base = original.getMultiplier(); + return reusable ? base : Math.multiplyExact(base, operationsPerPush); + } + + @Override + public boolean isValid(AEKey input, Level level) { + return original.isValid(input, level); + } + + @Override + @Nullable + public AEKey getRemainingKey(AEKey template) { + return original.getRemainingKey(template); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/SmartDoublingCalculationContext.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/SmartDoublingCalculationContext.java new file mode 100644 index 00000000..f3d226ca --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/SmartDoublingCalculationContext.java @@ -0,0 +1,8 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import appeng.api.networking.IGrid; + +/** Internal bridge exposing the grid retained by an AE crafting calculation. */ +public interface SmartDoublingCalculationContext { + IGrid uselessMod$getCraftingGrid(); +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/SmartDoublingPatterns.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/SmartDoublingPatterns.java new file mode 100644 index 00000000..d76fe5dc --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/SmartDoublingPatterns.java @@ -0,0 +1,239 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.networking.IGrid; +import appeng.api.networking.IGridNode; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.GenericStack; +import appeng.blockentity.crafting.IMolecularAssemblerSupportedPattern; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.core.component.UComponents; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; +import java.util.Set; + +/** Creates, restores and resolves this mod's smart-doubling pattern wrappers. */ +public final class SmartDoublingPatterns { + + private SmartDoublingPatterns() { + } + + /** + * 把样板包装成“一次推送代表多份操作”。 + * + *

合成样板必须保留 {@code IMolecularAssemblerSupportedPattern} 身份(接收方要靠它判断 + * “这是要自己装配的合成样板”),所以它走专门的包装类;其余样板沿用处理样板包装。

+ */ + public static ScaledProcessingPattern scale(IPatternDetails pattern, long operationsPerPush) { + return scale(pattern, operationsPerPush, null); + } + + /** + * 带 {@link Level} 的包装:只有拿到关卡才能认出「带耐久受损的返还物」这类可复用输入 + * (注魔水晶),并把它们的倍率钉在 1 份上。见 {@link ReusablePatternInputs}。 + * + *

关卡由本模组自己的机器提供(供应器方块实体的 {@code getLevel()}), + * 不从 AE2 网格取 —— 见 {@code SmartDoublingPlanner#levelOfProviders} 的说明。

+ * + * @param level 可为 {@code null},此时退回保守判据(只认原样返还) + */ + public static ScaledProcessingPattern scale(IPatternDetails pattern, long operationsPerPush, + @Nullable Level level) { + // 两个包装类都会在构造时解析已有倍率,所以这里直接把入参交给对应包装, + // 嵌套调用时的乘法语义与处理样板保持一致。 + if (pattern instanceof IMolecularAssemblerSupportedPattern crafting) { + return new ScaledCraftingPattern(crafting, operationsPerPush, level); + } + return new ScaledProcessingPattern(pattern, operationsPerPush, level); + } + + /** + * 从网格取判定可复用输入所需的关卡。 + * + *

规划入口({@code CraftingService#submitJob})跑在服务端线程上,取关卡是安全的; + * 取不到时返回 {@code null},判定会退回保守判据 —— 只少认一类,不会误认。

+ */ + public static @Nullable Level levelOf(@Nullable IGrid grid) { + try { + IGridNode pivot = grid == null ? null : grid.getPivot(); + return pivot == null ? null : pivot.getLevel(); + } catch (RuntimeException exception) { + return null; + } + } + + /** + * 样板里「整批只需要 1 份、倍率不能放大」的输入槽位(注魔水晶这类可复用催化剂)。 + * + *

判定它的目的只有一个:把这类样板挡在批量之外。它的返还物仍能当同一个槽的输入, + * 而 AE2 的合成计划对这类输入只算 1 份({@code CraftingTreeProcess#updateLimitQty} 遇到 + * 非空 {@code getRemainingKey} 就置 {@code limitQty},{@code times = 1})。任何按倍率放大输入的 + * 通路(本模组的样板包装、数据能源 counted 派发的原型放大、bigint 批次)都必须跳过这类样板, + * 否则会向机器索要 {@code count} 份催化剂而实际只有 1 份 ⇒ 一次推送都发不出去。

+ * + *

判定见 {@link ReusablePatternInputs}。

+ */ + public static Set reusableInputSlots(@Nullable IPatternDetails pattern, + @Nullable Level level) { + return ReusablePatternInputs.reusableRemainders(pattern, level).keySet(); + } + + public static IPatternDetails unwrap(IPatternDetails pattern) { + return resolve(pattern).pattern(); + } + + /** + * 判断一个样板能否被放大。 + * + *

这里只做结构健全性检查:样板确实声明了可用的输入。同键返还(模具、可复用催化剂、 + * 带耐久返还的工具)不再被排除 —— 接收方会在铺完一份之后逐键比对“剩下的材料是不是 + * 这一份消耗量的整齐倍数”,只有确认整批每一份输入完全相同才折叠成一次装配 ×N; + * 一批里混着不同变体时会退回逐份装配,所以同键返还参与批量同样是安全的。

+ * + *

注意「可复用输入」不在本方法的职责内:注魔水晶这类「返还物仍能当输入用」的槽, + * AE2 的合成计划对它只算 1 份,倍率不能放大 —— 那件事由 {@link ReusablePatternInputs} + * 在 {@link #scale} 里按槽位处理,本方法仍只看声明结构,因此不碰 {@code Level}: + * 规划可能跑在计算线程上。

+ */ + public static boolean canScale(IPatternDetails pattern) { + IPatternDetails base = unwrap(pattern); + if (!(base instanceof IMolecularAssemblerSupportedPattern)) { + return true; + } + IPatternDetails.IInput[] inputs = base.getInputs(); + if (inputs == null || inputs.length == 0) { + return false; + } + for (IPatternDetails.IInput input : inputs) { + if (input == null) { + return false; + } + GenericStack[] possibleInputs = input.getPossibleInputs(); + if (possibleInputs == null || possibleInputs.length == 0) { + return false; + } + for (GenericStack candidate : possibleInputs) { + if (candidate == null || candidate.what() == null) { + return false; + } + } + } + return true; + } + + public static long operationsPerPush(IPatternDetails pattern) { + return resolve(pattern).operationsPerPush(); + } + + /** + * Resolves the output-derived manual multiplier carried by the unwrapped processing pattern. + * Zero means the pattern cannot be proven to represent an exact number of recipe operations. + */ + public static long manualOperationsPerPattern( + AdvancedAlloyFurnaceRecipe recipe, + IPatternDetails pattern) { + ManualPatternOperationResolver.Resolution resolution = + ManualPatternOperationResolver.resolve(recipe, unwrap(pattern)); + return resolution.valid() ? resolution.operationsPerPattern() : 0L; + } + + public static Resolved resolve(IPatternDetails pattern) { + Objects.requireNonNull(pattern, "pattern"); + IPatternDetails current = pattern; + long operations = 1L; + while (current instanceof ScaledPattern scaled) { + operations = multiplyExactPositive( + operations, scaled.getOperationsPerPush(), "nested smart-doubling multiplier"); + current = scaled.getOriginal(); + } + return new Resolved(current, operations); + } + + public static long maximumSafeMultiplier(IPatternDetails pattern) { + IPatternDetails original = unwrap(pattern); + long maximum = Long.MAX_VALUE; + for (IPatternDetails.IInput input : original.getInputs()) { + if (input != null && input.getMultiplier() > 0L) { + maximum = Math.min(maximum, Long.MAX_VALUE / input.getMultiplier()); + GenericStack[] possibleInputs = input.getPossibleInputs(); + if (possibleInputs != null) { + for (GenericStack possibleInput : possibleInputs) { + if (possibleInput != null && possibleInput.amount() > 0L) { + maximum = Math.min(maximum, + Long.MAX_VALUE / input.getMultiplier() / possibleInput.amount()); + } + } + } + } + } + for (GenericStack output : original.getOutputs()) { + if (output != null && output.amount() > 0L) { + maximum = Math.min(maximum, Long.MAX_VALUE / output.amount()); + } + } + return Math.max(1L, maximum); + } + + @Nullable + public static Long definitionOperations(AEItemKey definition) { + return definition == null ? null : com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(definition, UComponents.SMART_DOUBLING_OPERATIONS.get()); + } + + @Nullable + public static IPatternDetails restore(AEItemKey definition, Level level) { + Long operations = definitionOperations(definition); + if (operations == null) { + return null; + } + if (operations <= 0L || level == null) { + return null; + } + + ItemStack baseStack = definition.toStack(); + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.remove(baseStack, UComponents.SMART_DOUBLING_OPERATIONS.get()); + AEItemKey baseDefinition = AEItemKey.of(baseStack); + if (baseDefinition == null) { + return null; + } + + IPatternDetails decoded = AdvancedAlloyFurnacePatternResolver.decode( + baseDefinition.toStack(), level); + if (decoded == null) { + return null; + } + if (operations > maximumSafeMultiplier(decoded)) { + return null; + } + return scale(decoded, operations, level); + } + + static AEItemKey executionDefinition(IPatternDetails original, long operationsPerPush) { + ItemStack definition = original.getDefinition().toStack(); + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.remove(definition, UComponents.SMART_DOUBLING_OPERATIONS.get()); + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(definition, UComponents.SMART_DOUBLING_OPERATIONS.get(), operationsPerPush); + return Objects.requireNonNull(AEItemKey.of(definition), "scaled pattern definition"); + } + + static long multiplyExactPositive(long left, long right, String description) { + if (left <= 0L || right <= 0L) { + throw new IllegalArgumentException(description + " must be positive"); + } + try { + return Math.multiplyExact(left, right); + } catch (ArithmeticException exception) { + throw new IllegalArgumentException(description + " exceeds the long range", exception); + } + } + + public record Resolved(IPatternDetails pattern, long operationsPerPush) { + public Resolved { + Objects.requireNonNull(pattern, "pattern"); + if (operationsPerPush <= 0L) { + throw new IllegalArgumentException("operationsPerPush must be positive"); + } + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/SmartDoublingPlanner.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/SmartDoublingPlanner.java new file mode 100644 index 00000000..271dd0c4 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/SmartDoublingPlanner.java @@ -0,0 +1,133 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.networking.crafting.ICraftingProvider; +import com.sorrowmist.useless.api.crafting.SmartDoublingCraftingProvider; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import org.jetbrains.annotations.Nullable; + +import java.util.LinkedHashMap; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +/** Pure planner that partitions AE operations across eligible in-mod providers. */ +public final class SmartDoublingPlanner { + private SmartDoublingPlanner() { + } + + /** + * 把 AE 的操作数按「可放大的供应器数量」切分,并把每批包装成「一次推送代表多份操作」。 + * + *

判定「可复用输入」所需的关卡从供应器本身取(见 {@link #levelOfProviders}), + * 不从 AE2 网格取。

+ */ + public static Map rewrite( + Map crafts, + Function> providerLookup) { + Map rewritten = new LinkedHashMap<>(); + for (var entry : crafts.entrySet()) { + IPatternDetails pattern = entry.getKey(); + long totalOperations = entry.getValue(); + if (totalOperations <= 1L || pattern instanceof ScaledPattern + || !SmartDoublingPatterns.canScale(pattern)) { + merge(rewritten, pattern, totalOperations); + continue; + } + + Iterable providers = providerLookup.apply(pattern); + long providerCount = countEligibleProviders(providers, totalOperations); + if (providerCount == 0L) { + merge(rewritten, pattern, totalOperations); + continue; + } + Level level = levelOfProviders(providers); + + long maximumMultiplier = SmartDoublingPatterns.maximumSafeMultiplier(pattern); + long batchCount = Math.max( + Math.min(totalOperations, providerCount), + ceilDivPositive(totalOperations, maximumMultiplier)); + long baseMultiplier = totalOperations / batchCount; + long remainder = totalOperations % batchCount; + + if (remainder > 0L) { + merge(rewritten, + SmartDoublingPatterns.scale(pattern, baseMultiplier + 1L, level), remainder); + } + long baseBatchCount = batchCount - remainder; + if (baseBatchCount > 0L) { + merge(rewritten, SmartDoublingPatterns.scale(pattern, baseMultiplier, level), baseBatchCount); + } + } + return rewritten; + } + + /** + * 从候选供应器里取关卡。 + * + *

为什么不从 AE2 网格取:那需要在 {@code CraftingService} 上挂一个 mixin 去读它的私有 + * {@code grid} 字段;而该类上已经挂着 OmniSequence 的 {@code OmniCraftingServiceMixin}, + * 再加一个会静默顶掉对方的注入 —— 实测后果是 AppliedEnhancements 的 AELIS 精确规划器 + * 不再参与,计划从真实量级退化成 long 饱和值,任务卡在 0 进度,且日志里没有任何错误。

+ * + *

本模组的机器都是方块实体,从供应器自己取关卡既够用又安全;而且计划期与执行期取到的是 + * 同一台机器的关卡,天然不会出现「两处判定不一致」。

+ */ + private static @Nullable Level levelOfProviders(@Nullable Iterable providers) { + if (providers == null) { + return null; + } + for (ICraftingProvider provider : providers) { + if (provider instanceof BlockEntity blockEntity && blockEntity.getLevel() != null) { + return blockEntity.getLevel(); + } + } + return null; + } + + public static List eligibleProviders( + Iterable providers) { + if (providers == null) { + return List.of(); + } + List eligible = new ArrayList<>(); + for (ICraftingProvider provider : providers) { + if (provider instanceof SmartDoublingCraftingProvider) { + eligible.add(provider); + } + } + return List.copyOf(eligible); + } + + private static long countEligibleProviders( + Iterable providers, long maximumNeeded) { + if (providers == null) { + return 0L; + } + long count = 0L; + for (ICraftingProvider provider : providers) { + if (provider instanceof SmartDoublingCraftingProvider) { + count++; + if (count >= maximumNeeded) { + break; + } + } + } + return count; + } + + private static long ceilDivPositive(long dividend, long divisor) { + return dividend / divisor + (dividend % divisor == 0L ? 0L : 1L); + } + + private static void merge( + Map target, IPatternDetails pattern, long amount) { + target.merge(pattern, amount, SmartDoublingPlanner::saturatingAdd); + } + + private static long saturatingAdd(long left, long right) { + return right > 0L && left > Long.MAX_VALUE - right ? Long.MAX_VALUE : left + right; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/SmartDoublingPlans.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/SmartDoublingPlans.java new file mode 100644 index 00000000..b727fd13 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/ae/SmartDoublingPlans.java @@ -0,0 +1,62 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.networking.crafting.ICraftingPlan; +import appeng.api.networking.crafting.ICraftingProvider; +import appeng.crafting.CraftingPlan; +import com.sorrowmist.useless.compat.forge1201.OptionalIntegration; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +/** Applies smart-doubling to plans produced outside AE2's standard planner. */ +public final class SmartDoublingPlans { + private SmartDoublingPlans() { + } + + /** + * 改写提交给 CPU 的计划,把可放大的样板换成「一次推送代表多份操作」的包装。 + * + *

判定「可复用输入」所需的关卡由 {@link SmartDoublingPlanner#rewrite} 从供应器本身取 + * (也就是本模组自己的机器),所以这里不需要关卡参数 —— 也就避免了为此在 + * {@code CraftingService} 上挂 mixin(那会静默顶掉 OmniSequence 的注入, + * 详见 {@link SmartDoublingPlanner#rewrite} 的说明)。

+ * + *

注意:一旦这里改写了样板就会构造新的 {@link CraftingPlan}, + * 从而丢掉 AppliedEnhancements 的 AELIS 精确(BigInteger)计划 —— 两者互斥。

+ */ + public static ICraftingPlan rewriteForSubmission( + ICraftingPlan plan, + Function> providerLookup) { + Objects.requireNonNull(plan, "plan"); + Objects.requireNonNull(providerLookup, "providerLookup"); + + // ECO's schedule is tied to this exact confirmed task/material vector. Rebuilding the plan + // here would detach its dependency order and startup seeds before the CPU receives it. + if (OptionalIntegration.call("neoecoae", "com.sorrowmist.useless.compat.neoecoae.NeoEcoPlanningCompat", + "shouldPreserveSubmissionPlan", Boolean.class, false, new Class[]{ICraftingPlan.class}, plan)) { + return plan; + } + + Map rewritten = SmartDoublingPlanner.rewrite( + plan.patternTimes(), providerLookup); + if (rewritten.equals(plan.patternTimes())) { + return plan; + } + + Map immutablePatterns = Collections.unmodifiableMap( + new LinkedHashMap<>(rewritten)); + return new CraftingPlan( + plan.finalOutput(), + plan.bytes(), + plan.simulation(), + plan.multiplePaths(), + plan.usedItems(), + plan.emittedItems(), + plan.missingItems(), + immutablePatterns); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/catalyst/CatalystEffectResolver.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/catalyst/CatalystEffectResolver.java new file mode 100644 index 00000000..3d57b36d --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/catalyst/CatalystEffectResolver.java @@ -0,0 +1,64 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.catalyst; + +import com.sorrowmist.useless.api.enums.CatalystType; +import com.sorrowmist.useless.core.config.ConfigManager; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import net.minecraft.world.item.ItemStack; + +/** + * 高级合金炉催化剂效果解析器。 + * 负责把催化剂类型、配方输出和基础处理时间转换为运行时效果对象。 + */ +public final class CatalystEffectResolver { + private CatalystEffectResolver() { + } + + /** + * 根据配方、催化剂物品和基础处理时间解析最终催化剂效果。 + */ + public static ResolvedCatalystEffect resolve(AdvancedAlloyFurnaceRecipe recipe, ItemStack catalystStack, int baseProcessTime) { + return resolveForType(recipe, CatalystType.fromStack(catalystStack), baseProcessTime); + } + + /** Resolves the shared catalyst rules for non-item providers such as multiblock coils. */ + public static ResolvedCatalystEffect resolveForType( + AdvancedAlloyFurnaceRecipe recipe, CatalystType catalystType, int baseProcessTime) { + CatalystType resolvedType = catalystType == null ? CatalystType.NONE : catalystType; + int targetTier = findTargetUselessIngotTier(recipe); + boolean uselessIngotRecipe = targetTier > 0; + int catalystParallel = resolvedType.getNormalRecipeParallel(); + int recipeParallel = uselessIngotRecipe + ? Math.max(CatalystParallelManager.calculateParallelForUselessIngotRecipe(targetTier), + catalystParallel) + : catalystParallel; + int energyDivisor = resolvedType.isUsefulIngot() + ? 1 + : ConfigManager.getAdvancedAlloyFurnaceCatalystEnergyDivisor(resolvedType.getTier()); + return new ResolvedCatalystEffect( + resolvedType, + Math.max(1, catalystParallel), + Math.max(1, recipeParallel), + resolvedType.calculateProcessTime(baseProcessTime), + !resolvedType.isUsefulIngot(), + energyDivisor, + uselessIngotRecipe, + targetTier + ); + } + + /** + * 从配方输出中识别目标无用锭等级。 + */ + public static int findTargetUselessIngotTier(AdvancedAlloyFurnaceRecipe recipe) { + if (recipe == null) { + return 0; + } + for (ItemStack output : recipe.outputs()) { + CatalystType outputType = CatalystType.fromStack(output); + if (outputType.isUselessIngotTier()) { + return outputType.getTier(); + } + } + return 0; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/catalyst/CatalystParallelManager.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/catalyst/CatalystParallelManager.java new file mode 100644 index 00000000..186dff1b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/catalyst/CatalystParallelManager.java @@ -0,0 +1,102 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.catalyst; + +import com.sorrowmist.useless.api.enums.CatalystType; +import net.minecraft.world.item.ItemStack; + +/** + * 催化剂并行数管理器 + * 统一管理所有催化剂的并行数计算逻辑 + */ +public class CatalystParallelManager { + + public static final int BASE_PARALLEL = 2; + + /** + * 获取催化剂的等级 + * + * @param stack 催化剂物品堆 + * @return 等级,如果不是催化剂则返回0 + */ + public static int getCatalystTier(ItemStack stack) { + return CatalystType.fromStack(stack).getTier(); + } + + + /** + * 判断物品是否为有用锭 + * + * @param stack 物品堆 + * @return 是否为有用锭 + */ + public static boolean isUsefulIngot(ItemStack stack) { + return CatalystType.fromStack(stack).isUsefulIngot(); + } + + /** + * 计算普通配方的并行数 + * 普通配方使用催化剂时,并行数 = 3^催化剂等级 + * + * @param catalystStack 催化剂物品堆 + * @return 并行数,无催化剂返回1 + */ + public static int calculateParallelForNormalRecipe(ItemStack catalystStack) { + return CatalystType.fromStack(catalystStack).getNormalRecipeParallel(); + } + + /** + * 计算无用锭配方的并行数 + * 无用锭配方的并行数 = 2^目标等级 + * + * @param targetTier 目标无用锭等级(配方输出) + * @return 并行数,无效目标等级返回1 + */ + public static int calculateParallelForUselessIngotRecipe(int targetTier) { + if (targetTier < 1 || targetTier > 9) return 1; + return (int) Math.pow(BASE_PARALLEL, targetTier); + } + + /** + * 获取配方的目标无用锭等级 + * 通过检查配方输出物品判断 + * + * @param outputStack 配方输出物品 + * @return 等级(1-9),如果不是无用锭则返回0 + */ + public static int getTargetUselessIngotTier(ItemStack outputStack) { + CatalystType type = CatalystType.fromStack(outputStack); + return type.isUselessIngotTier() ? type.getTier() : 0; + } + + /** + * 获取催化剂的显示名称 + * + * @param stack 催化剂物品堆 + * @return 显示名称 + */ + public static String getCatalystDisplayName(ItemStack stack) { + return CatalystType.fromStack(stack).getDisplayName(); + } + + /** + * 检查物品是否为有效的催化剂 + * + * @param stack 物品堆 + * @return 是否为有效催化剂 + */ + public static boolean isValidCatalyst(ItemStack stack) { + return CatalystType.fromStack(stack).isValidCatalyst(); + } + + /** + * 计算催化剂对处理时间的加成 + * 一阶催化剂减少10%时间(90%),二阶减少20%(80%),以此类推 + * 有用锭固定为1 tick + * + * @param baseTime 基础处理时间 + * @param catalystStack 催化剂物品堆 + * @return 加成后的处理时间(向上取整,最小为1) + */ + public static int calculateProcessTimeWithCatalyst(int baseTime, ItemStack catalystStack) { + return CatalystType.fromStack(catalystStack).calculateProcessTime(baseTime); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/catalyst/ResolvedCatalystEffect.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/catalyst/ResolvedCatalystEffect.java new file mode 100644 index 00000000..68d276e2 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/catalyst/ResolvedCatalystEffect.java @@ -0,0 +1,19 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.catalyst; + +import com.sorrowmist.useless.api.enums.CatalystType; + +/** + * 催化剂在某个具体配方上下文中的运行结果。 + * 该对象用于把催化剂静态信息和配方目标无用锭等级整合成统一执行参数。 + */ +public record ResolvedCatalystEffect( + CatalystType catalystType, + long catalystParallel, + long recipeParallel, + int processTime, + boolean energyMultipliesWithParallel, + int energyDivisor, + boolean uselessIngotRecipe, + int targetUselessIngotTier +) { +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/chemical/ChemicalCompatProvider.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/chemical/ChemicalCompatProvider.java new file mode 100644 index 00000000..c5b40255 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/chemical/ChemicalCompatProvider.java @@ -0,0 +1,75 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; + +/** Optional chemical integration owned by a compatibility module. */ +public interface ChemicalCompatProvider { + ChemicalCompatProvider NONE = new ChemicalCompatProvider() { + @Override + public FurnaceChemicalStorage createStorage(long capacity, Runnable onChanged) { + return FurnaceChemicalStorage.DISABLED; + } + + @Override + public @Nullable ChemicalHandlerView getAdjacentHandler(Level level, BlockPos pos, BlockState state, + BlockEntity entity, @Nullable Direction side) { + return null; + } + + @Override + public boolean isAvailable() { + return false; + } + }; + + FurnaceChemicalStorage createStorage(long capacity, Runnable onChanged); + + @Nullable + ChemicalHandlerView getAdjacentHandler(Level level, BlockPos pos, BlockState state, + BlockEntity entity, @Nullable Direction side); + + /** + * 读「物品里装的化学品」——无线物流用它把化学品罐当成过滤标记物。 + * + *

没装任何化学品(或本环境不支持化学品)时返回 {@code null}。

+ */ + @Nullable + default ChemicalStackView chemicalInItem(ItemStack stack) { + return null; + } + + /** + * JEI 拖来的化学品原料 → 一个能当过滤标记的容器物品(装满该化学品的储罐)。 + * + *

参数是 JEI 的原始原料对象,只有化学品集成自己认得它;转换不了时返回空栈。

+ */ + default ItemStack markerForChemical(Object chemicalIngredient) { + return ItemStack.EMPTY; + } + + /** + * 把一个 {@link ChemicalStackView#typeKey() typeKey} 翻成注册表 id,供过滤器的通配符匹配用。 + * + *

{@code typeKey()} 是个不透明对象(Mekanism 那边是它自己的 {@code Chemical}), + * 常驻代码拿不到显示名或 id,所以只能由集成自己翻译。返回 {@code null} 表示 + * 「这一端给不出 id」:此时过滤器里的通配符对化学品恒不匹配——宁可「什么都不搬」 + * (可观察、玩家能改),也不要因为拿不到 id 就当「不限制」而把整张网络灌出去。

+ * + *

注意化学品没有 {@code #tag} 形式:Mekanism 的化学品注册表没有跨模组的 + * 通用 TagKey,界面会明确提示这一点。

+ */ + @Nullable + default net.minecraft.resources.ResourceLocation chemicalIdOf(Object typeKey) { + return null; + } + + default boolean isAvailable() { + return true; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/chemical/ChemicalCompatProviders.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/chemical/ChemicalCompatProviders.java new file mode 100644 index 00000000..97d0d81d --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/chemical/ChemicalCompatProviders.java @@ -0,0 +1,17 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical; + +/** Process-wide optional chemical integration selected during capability setup. */ +public final class ChemicalCompatProviders { + private static volatile ChemicalCompatProvider provider = ChemicalCompatProvider.NONE; + + private ChemicalCompatProviders() { + } + + public static ChemicalCompatProvider get() { + return provider; + } + + public static void register(ChemicalCompatProvider provider) { + ChemicalCompatProviders.provider = provider == null ? ChemicalCompatProvider.NONE : provider; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/chemical/ChemicalHandlerView.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/chemical/ChemicalHandlerView.java new file mode 100644 index 00000000..ed977a90 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/chemical/ChemicalHandlerView.java @@ -0,0 +1,10 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical; + +/** Core view of a neighboring chemical capability used by automatic IO. */ +public interface ChemicalHandlerView { + ChemicalStackView insertChemical(ChemicalStackView stack, boolean simulate); + + ChemicalStackView extractChemical(long amount, boolean simulate); + + ChemicalStackView extractChemical(ChemicalStackView stack, long amount, boolean simulate); +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/chemical/ChemicalKeyProvider.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/chemical/ChemicalKeyProvider.java new file mode 100644 index 00000000..7e7b58a4 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/chemical/ChemicalKeyProvider.java @@ -0,0 +1,33 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical; + +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import org.jetbrains.annotations.Nullable; + +/** Converts optional chemical views to and from the AE key representation. */ +public interface ChemicalKeyProvider { + ChemicalKeyProvider NONE = new ChemicalKeyProvider() { + @Override + public @Nullable GenericStack toGenericStack(ChemicalStackView stack) { + return null; + } + + @Override + public @Nullable ChemicalStackView fromGenericStack(GenericStack stack) { + return null; + } + + @Override + public boolean isChemicalKey(AEKey key) { + return false; + } + }; + + @Nullable + GenericStack toGenericStack(ChemicalStackView stack); + + @Nullable + ChemicalStackView fromGenericStack(GenericStack stack); + + boolean isChemicalKey(@Nullable AEKey key); +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/chemical/ChemicalKeyProviders.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/chemical/ChemicalKeyProviders.java new file mode 100644 index 00000000..913c1ece --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/chemical/ChemicalKeyProviders.java @@ -0,0 +1,17 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical; + +/** Process-wide provider selected by the optional AppMek integration. */ +public final class ChemicalKeyProviders { + private static volatile ChemicalKeyProvider provider = ChemicalKeyProvider.NONE; + + private ChemicalKeyProviders() { + } + + public static ChemicalKeyProvider get() { + return provider; + } + + public static void register(ChemicalKeyProvider provider) { + ChemicalKeyProviders.provider = provider == null ? ChemicalKeyProvider.NONE : provider; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/chemical/ChemicalStackView.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/chemical/ChemicalStackView.java new file mode 100644 index 00000000..ba438861 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/chemical/ChemicalStackView.java @@ -0,0 +1,81 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical; + +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.Nullable; + +/** + * A read-only, Mekanism-independent view of a chemical stack. + * + *

The value returned by {@link #typeKey()} is deliberately opaque. The + * optional integration owns its concrete type and is the only code that may + * interpret it.

+ */ +public interface ChemicalStackView { + ChemicalStackView EMPTY = new ChemicalStackView() { + @Override + public Object typeKey() { + return EmptyType.INSTANCE; + } + + @Override + public long amount() { + return 0L; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public ChemicalStackView copyWithAmount(long amount) { + return this; + } + + @Override + public Component displayName() { + return Component.empty(); + } + + @Override + public @Nullable ResourceLocation icon() { + return null; + } + + @Override + public int tintColor() { + return 0xFFFFFFFF; + } + }; + + Object typeKey(); + + long amount(); + + boolean isEmpty(); + + ChemicalStackView copyWithAmount(long amount); + + Component displayName(); + + /** Optional client-side icon. */ + @Nullable + default ResourceLocation icon() { + return null; + } + + /** ARGB tint used when drawing {@link #icon()}. */ + default int tintColor() { + return 0xFFFFFFFF; + } + + default boolean isSameType(@Nullable ChemicalStackView other) { + return other != null && !this.isEmpty() && !other.isEmpty() + && this.typeKey().equals(other.typeKey()); + } + + enum EmptyType { + INSTANCE + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/chemical/FurnaceChemicalStorage.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/chemical/FurnaceChemicalStorage.java new file mode 100644 index 00000000..63d36bab --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/chemical/FurnaceChemicalStorage.java @@ -0,0 +1,121 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical; + +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; + +/** + * Core chemical storage contract used by the alloy furnace. + * + *

Amounts are always {@code long}. Implementations may be backed by a + * Mekanism handler, while the furnace and its recipe code only see this + * contract.

+ */ +public interface FurnaceChemicalStorage { + FurnaceChemicalStorage DISABLED = new FurnaceChemicalStorage() { + @Override + public int size() { + return 0; + } + + @Override + public long capacity(int slot) { + return 0L; + } + + @Override + public ChemicalStackView getStackInSlot(int slot) { + return ChemicalStackView.EMPTY; + } + + @Override + public ChemicalStackView insertChemical(int slot, ChemicalStackView stack, boolean simulate) { + return stack == null ? ChemicalStackView.EMPTY : stack; + } + + @Override + public ChemicalStackView extractChemical(int slot, long amount, boolean simulate) { + return ChemicalStackView.EMPTY; + } + + @Override + public void setStackInSlot(int slot, ChemicalStackView stack) { + } + + @Override + public boolean isAvailable() { + return false; + } + }; + + int size(); + + long capacity(int slot); + + ChemicalStackView getStackInSlot(int slot); + + ChemicalStackView insertChemical(int slot, ChemicalStackView stack, boolean simulate); + + ChemicalStackView extractChemical(int slot, long amount, boolean simulate); + + void setStackInSlot(int slot, ChemicalStackView stack); + + default boolean isAvailable() { + return size() > 0; + } + + default void setCapacity(long capacity) { + } + + default ChemicalStackView insertChemical(ChemicalStackView stack, boolean simulate) { + if (stack == null || stack.isEmpty() || !isAvailable()) { + return stack == null ? ChemicalStackView.EMPTY : stack; + } + + ChemicalStackView remaining = stack; + for (int pass = 0; pass < 2 && !remaining.isEmpty(); pass++) { + for (int slot = 0; slot < size() && !remaining.isEmpty(); slot++) { + ChemicalStackView current = getStackInSlot(slot); + if ((pass == 0 && !current.isSameType(remaining)) + || (pass == 1 && !current.isEmpty())) { + continue; + } + remaining = insertChemical(slot, remaining, simulate); + } + } + return remaining; + } + + default ChemicalStackView extractChemical(ChemicalStackView type, long amount, boolean simulate) { + if (type == null || type.isEmpty() || amount <= 0L) { + return ChemicalStackView.EMPTY; + } + long remaining = amount; + ChemicalStackView result = ChemicalStackView.EMPTY; + for (int slot = 0; slot < size() && remaining > 0L; slot++) { + ChemicalStackView current = getStackInSlot(slot); + if (!current.isSameType(type)) continue; + ChemicalStackView extracted = extractChemical(slot, remaining, simulate); + if (extracted.isEmpty()) continue; + if (result.isEmpty()) { + result = extracted; + } else if (result.isSameType(extracted)) { + result = result.copyWithAmount(saturatingAdd(result.amount(), extracted.amount())); + } + remaining -= extracted.amount(); + } + return result; + } + + /** Persist the storage without exposing the optional stack type to core code. */ + default void save(CompoundTag tag, String prefix, HolderLookup.Provider registries) { + } + + /** Restore the storage without exposing the optional stack type to core code. */ + default void load(CompoundTag tag, String prefix, HolderLookup.Provider registries) { + } + + static long saturatingAdd(long left, long right) { + if (right > 0L && left > Long.MAX_VALUE - right) return Long.MAX_VALUE; + return left + right; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/execution/AlloyFurnaceRecipeExecutor.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/execution/AlloyFurnaceRecipeExecutor.java new file mode 100644 index 00000000..bc914e56 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/execution/AlloyFurnaceRecipeExecutor.java @@ -0,0 +1,200 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.execution; + +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.catalyst.ResolvedCatalystEffect; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.energy.IEnergyManager; + +import java.math.BigInteger; + +/** Shared long-energy accounting for local and AE alloy-furnace execution. */ +public final class AlloyFurnaceRecipeExecutor { + private AlloyFurnaceRecipeExecutor() { + } + + public record TickResult(boolean progressAdvanced, long energyConsumed) { + } + + public record CompletionEnergyResult(int actualParallel, long additionalEnergyConsumed) { + } + + public static long calculateBaseEnergyPerTick(AdvancedAlloyFurnaceRecipe recipe) { + long energy = Math.max(0L, recipe.energy()); + return energy == 0L ? 0L : Math.max(1L, energy / Math.max(1, recipe.processTime())); + } + + public static long calculateTickEnergy(long baseEnergyPerTick, int parallel, + ResolvedCatalystEffect resolvedCatalystEffect) { + if (!resolvedCatalystEffect.energyMultipliesWithParallel()) { + return divideRoundUp(Math.max(0L, baseEnergyPerTick), + resolvedCatalystEffect.energyDivisor()); + } + return multiplyThenDivideRoundUp( + Math.max(0L, baseEnergyPerTick), Math.max(0, parallel), + resolvedCatalystEffect.energyDivisor()); + } + + public static TickResult consumeTickEnergy(IEnergyManager energyManager, long baseEnergyPerTick, int parallel, + ResolvedCatalystEffect resolvedCatalystEffect) { + long energyRequired = calculateTickEnergy(baseEnergyPerTick, parallel, resolvedCatalystEffect); + if (energyRequired == 0L) { + return new TickResult(true, 0L); + } + if (!energyManager.tryConsumeEnergy(energyRequired)) { + return new TickResult(false, 0L); + } + return new TickResult(true, energyRequired); + } + + /** + * Charges the next progress step. Partial payments are retained so a step can cost more than the buffer capacity. + */ + public static TickResult consumeProgressEnergy(IEnergyManager energyManager, long targetTotalEnergy, + int currentProgress, int maxProgress, + long accumulatedEnergy) { + long normalizedTarget = Math.max(0L, targetTotalEnergy); + if (normalizedTarget == 0L) { + return new TickResult(true, 0L); + } + + long threshold = energyAtProgress( + normalizedTarget, Math.min(Math.max(0, currentProgress) + 1, Math.max(1, maxProgress)), maxProgress); + long required = threshold - Math.max(0L, accumulatedEnergy); + if (required <= 0L) { + return new TickResult(true, 0L); + } + + long consumable = Math.min(required, energyManager.getEnergyStoredLong()); + if (consumable <= 0L || !energyManager.tryConsumeEnergy(consumable)) { + return new TickResult(false, 0L); + } + return new TickResult(accumulatedEnergy + consumable >= threshold, consumable); + } + + /** Returns the cumulative energy that must be paid at a given progress position. */ + static long energyAtProgress(long totalEnergy, int progress, int maxProgress) { + if (totalEnergy <= 0L || progress <= 0) { + return 0L; + } + int steps = Math.max(1, maxProgress); + if (progress >= steps) { + return totalEnergy; + } + + long quotient = totalEnergy / steps; + long remainder = totalEnergy % steps; + long whole = quotient * progress; + long fractional = (remainder * progress + steps - 1L) / steps; + return whole + fractional; + } + + public static long calculateTargetTotalEnergy(long recipeEnergy, long targetParallel, + ResolvedCatalystEffect resolvedCatalystEffect) { + long normalizedEnergy = Math.max(0L, recipeEnergy); + return resolvedCatalystEffect.energyMultipliesWithParallel() + ? multiplyThenDivideRoundUp( + normalizedEnergy, Math.max(0, targetParallel), + resolvedCatalystEffect.energyDivisor()) + : divideRoundUp(normalizedEnergy, resolvedCatalystEffect.energyDivisor()); + } + + public static CompletionEnergyResult settleCompletionEnergy( + IEnergyManager energyManager, long recipeEnergy, int targetParallel, long accumulatedEnergy, + ResolvedCatalystEffect resolvedCatalystEffect) { + long targetTotalEnergy = calculateTargetTotalEnergy(recipeEnergy, targetParallel, resolvedCatalystEffect); + long additionalEnergyNeeded = targetTotalEnergy - Math.max(0L, accumulatedEnergy); + if (additionalEnergyNeeded <= 0L) { + return new CompletionEnergyResult(targetParallel, 0L); + } + + if (energyManager.tryConsumeEnergy(additionalEnergyNeeded)) { + return new CompletionEnergyResult(targetParallel, additionalEnergyNeeded); + } + + if (recipeEnergy <= 0L) { + return new CompletionEnergyResult(targetParallel, 0L); + } + + long normalizedAccumulated = Math.max(0L, accumulatedEnergy); + long totalAvailableEnergy = saturatingAdd( + normalizedAccumulated, energyManager.getEnergyStoredLong()); + int actualParallel; + if (resolvedCatalystEffect.energyMultipliesWithParallel()) { + long parallelLong = maximumAffordableParallel( + totalAvailableEnergy, recipeEnergy, resolvedCatalystEffect.energyDivisor()); + actualParallel = (int) Math.min(Integer.MAX_VALUE, parallelLong); + actualParallel = Math.max(0, Math.min(actualParallel, targetParallel)); + } else { + actualParallel = totalAvailableEnergy >= targetTotalEnergy ? targetParallel : 0; + } + + long actualTargetEnergy = calculateTargetTotalEnergy( + recipeEnergy, actualParallel, resolvedCatalystEffect); + long energyToConsume = Math.max(0L, actualTargetEnergy - normalizedAccumulated); + if (energyToConsume > 0L && !energyManager.tryConsumeEnergy(energyToConsume)) { + return new CompletionEnergyResult(0, 0L); + } + return new CompletionEnergyResult(actualParallel, energyToConsume); + } + + private static long saturatingMultiply(long amount, long multiplier) { + if (amount <= 0L || multiplier <= 0L) { + return 0L; + } + return amount > Long.MAX_VALUE / multiplier ? Long.MAX_VALUE : amount * multiplier; + } + + private static long multiplyThenDivideRoundUp(long amount, long multiplier, int divisor) { + if (amount <= 0L || multiplier <= 0L) { + return 0L; + } + long normalizedDivisor = Math.max(1, divisor); + long quotient = amount / normalizedDivisor; + long remainder = amount % normalizedDivisor; + long whole = saturatingMultiply(quotient, multiplier); + if (whole == Long.MAX_VALUE || remainder == 0L) { + return whole; + } + long fractional = multiplyThenDivideRoundUpExact( + remainder, multiplier, normalizedDivisor); + return saturatingAdd(whole, fractional); + } + + private static long multiplyThenDivideRoundUpExact(long amount, long multiplier, long divisor) { + if (amount <= Long.MAX_VALUE / multiplier) { + return divideRoundUp(amount * multiplier, divisor); + } + BigInteger result = BigInteger.valueOf(amount) + .multiply(BigInteger.valueOf(multiplier)) + .add(BigInteger.valueOf(divisor - 1L)) + .divide(BigInteger.valueOf(divisor)); + return result.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0 + ? Long.MAX_VALUE + : result.longValue(); + } + + private static long divideRoundUp(long amount, long divisor) { + if (amount <= 0L) { + return 0L; + } + return 1L + (amount - 1L) / Math.max(1L, divisor); + } + + private static long maximumAffordableParallel(long availableEnergy, long recipeEnergy, int divisor) { + if (availableEnergy <= 0L || recipeEnergy <= 0L) { + return 0L; + } + BigInteger affordable = BigInteger.valueOf(availableEnergy) + .multiply(BigInteger.valueOf(Math.max(1, divisor))) + .divide(BigInteger.valueOf(recipeEnergy)); + return affordable.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0 + ? Long.MAX_VALUE + : affordable.longValue(); + } + + private static long saturatingAdd(long left, long right) { + if (right > 0L && left > Long.MAX_VALUE - right) { + return Long.MAX_VALUE; + } + return left + right; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/io/FurnaceAutoIoController.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/io/FurnaceAutoIoController.java new file mode 100644 index 00000000..7ddc0c35 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/io/FurnaceAutoIoController.java @@ -0,0 +1,503 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.io; + +import com.sorrowmist.useless.api.enums.FurnaceFace; +import com.sorrowmist.useless.api.enums.FurnaceFaceMode; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalHandlerView; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalStackView; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.FurnaceChemicalStorage; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraftforge.common.capabilities.ForgeCapabilities; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.capability.IFluidHandler; +import net.minecraftforge.fluids.capability.templates.FluidTank; +import net.minecraftforge.items.IItemHandler; +import net.minecraftforge.items.ItemStackHandler; + +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.CATALYST_SLOT; +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.FLUID_TANK_COUNT; +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.INPUT_SLOTS_START; +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.MOLD_SLOT; +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.OUTPUT_SLOTS_COUNT; +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.OUTPUT_SLOTS_START; + +/** + * 高级合金炉自动输入输出控制器。 + *

+ * 负责与周围容器及 AE 网络之间的物品和流体自动搬运, + * 从方块实体中剥离,降低其复杂度。 + */ +public final class FurnaceAutoIoController { + + private static final Direction[] ALL_DIRECTIONS = Direction.values(); + // 自动 IO 连续空转时扫描间隔的上限(tick) + private static final int MAX_IDLE_BACKOFF_TICKS = 20; + private static final int[] MATERIAL_INPUT_SLOT_CANDIDATES; + private static final int[] CATALYST_INPUT_SLOT_CANDIDATES = {CATALYST_SLOT}; + private static final int[] MOLD_INPUT_SLOT_CANDIDATES = {MOLD_SLOT}; + private static final int[] EMPTY_INPUT_SLOT_CANDIDATES = {}; + + static { + MATERIAL_INPUT_SLOT_CANDIDATES = new int[]{ + INPUT_SLOTS_START, INPUT_SLOTS_START + 1, INPUT_SLOTS_START + 2, + INPUT_SLOTS_START + 3, INPUT_SLOTS_START + 4, INPUT_SLOTS_START + 5, + INPUT_SLOTS_START + 6, INPUT_SLOTS_START + 7, INPUT_SLOTS_START + 8, + }; + } + + /** + * 自动 IO 控制器所需的上下文回调。 + */ + public interface Context extends FurnaceFaceAccessor { + boolean isAutoInputEnabled(); + + boolean isAutoOutputEnabled(); + + long tryOutputToAE(ItemStack stack); + + long tryOutputFluidToAE(FluidStack stack); + + default long tryOutputChemicalToAE(ChemicalStackView stack) { + return 0L; + } + + default void handleUnreturnedChemical(ChemicalStackView stack) { + } + + default ChemicalHandlerView getAdjacentChemicalHandler(Level level, BlockPos pos, + net.minecraft.world.level.block.state.BlockState state, + BlockEntity entity, Direction side) { + return null; + } + + void markChanged(); + + /** + * 本机物品/流体/化学品存储的修订号,任意内容变化都必须递增。 + * 自动 IO 闲置退避期间用它判断本机是否产出了新东西或腾出了空间,变了就立即重新扫描。 + */ + long getIoStateRevision(); + } + + private final Context context; + private final ItemStackHandler itemHandler; + private final FluidTank[] inputFluidTanks; + private final FluidTank[] outputFluidTanks; + private final FurnaceChemicalStorage inputChemicalStorage; + private final FurnaceChemicalStorage outputChemicalStorage; + private final BlockPos worldPosition; + // 连续无搬运的扫描轮数,用于逐级拉长扫描间隔 + private int idleRounds = 0; + // 距下一次扫描还需跳过的 tick 数 + private int idleWaitTicks = 0; + // 上一轮扫描后看到的内容修订号,用于判断退避期间本机内容是否变化 + private long observedRevision; + + public FurnaceAutoIoController(Context context, ItemStackHandler itemHandler, + FluidTank[] inputFluidTanks, FluidTank[] outputFluidTanks, + BlockPos worldPosition) { + this(context, itemHandler, inputFluidTanks, outputFluidTanks, + FurnaceChemicalStorage.DISABLED, FurnaceChemicalStorage.DISABLED, worldPosition); + } + + public FurnaceAutoIoController(Context context, ItemStackHandler itemHandler, + FluidTank[] inputFluidTanks, FluidTank[] outputFluidTanks, + FurnaceChemicalStorage inputChemicalStorage, + FurnaceChemicalStorage outputChemicalStorage, + BlockPos worldPosition) { + this.context = context; + this.itemHandler = itemHandler; + this.inputFluidTanks = inputFluidTanks; + this.outputFluidTanks = outputFluidTanks; + this.inputChemicalStorage = inputChemicalStorage; + this.outputChemicalStorage = outputChemicalStorage; + this.worldPosition = worldPosition; + } + + /** + * 自动输入输出物品和流体到周围的容器。 + *

+ * 一整轮扫描没有任何搬运时,扫描间隔按 1、2、4、8、16 tick 逐级拉长,封顶 + * {@link #MAX_IDLE_BACKOFF_TICKS},避免机器闲置(输出槽满、AE 网络满、周围容器已空)时 + * 逐 tick 反复查询能力、遍历相邻容器槽位并向 AE 重复提交必然失败的插入。 + * 本机内容或 IO 配置一旦变化立即恢复逐 tick 扫描:前者由修订号体现,后者由 {@link #wake()} 唤醒; + * 周围容器与 AE 网络的变化无从得知,最迟一个封顶间隔后被发现。 + */ + public void tick(Level level) { + if (level.isClientSide) return; + + if (this.idleWaitTicks > 0) { + if (this.context.getIoStateRevision() == this.observedRevision) { + this.idleWaitTicks--; + return; + } + this.idleWaitTicks = 0; + } + + boolean moved = false; + if (this.context.isAutoInputEnabled()) { + moved = this.autoInputFromSurroundings(level); + } + + if (this.context.isAutoOutputEnabled()) { + moved |= this.autoOutputToSurroundings(level); + } + + this.observedRevision = this.context.getIoStateRevision(); + if (moved) { + this.idleRounds = 0; + } else { + this.idleRounds = Math.min(this.idleRounds + 1, MAX_IDLE_BACKOFF_TICKS); + this.idleWaitTicks = idleScanInterval(this.idleRounds) - 1; + } + } + + /** 连续空转 {@code idleRounds} 轮后的扫描间隔:1、2、4、8、16,之后封顶。 */ + private static int idleScanInterval(int idleRounds) { + return Math.min(MAX_IDLE_BACKOFF_TICKS, 1 << Math.min(idleRounds - 1, 5)); + } + + /** + * 立即恢复逐 tick 自动 IO(面模式或自动输入输出开关被改动时调用)。 + * 这类配置变化不改变本机内容,不会体现在修订号里,只能显式唤醒。 + */ + public void wake() { + this.idleRounds = 0; + this.idleWaitTicks = 0; + } + + /** + * 从周围容器自动输入物品和流体到机器。 + * 仅从开启了对应面模式的面进行输入。 + * + * @return 本轮是否至少搬运了一份物品、流体或化学品 + */ + private boolean autoInputFromSurroundings(Level level) { + boolean movedAny = false; + Direction facing = this.context.getFacing(); + for (Direction dir : Direction.values()) { + FurnaceFace face = FurnaceFace.fromDirection(dir, facing); + if (face == null) continue; + FurnaceFaceMode mode = this.context.getFaceMode(face); + if (!mode.allowsAny()) continue; + + BlockPos srcPos = this.worldPosition.relative(dir); + BlockEntity srcEntity = level.getBlockEntity(srcPos); + if (srcEntity == null) continue; + + // 输入物品 + if (mode.allowsMaterialInput() || mode.allowsCatalystInput() || mode.allowsMoldInput()) { + IItemHandler srcHandler = com.sorrowmist.useless.compat.forge1201.capability.CapabilityAccess.get(level, + ForgeCapabilities.ITEM_HANDLER, srcPos, srcEntity.getBlockState(), srcEntity, dir.getOpposite()); + if (srcHandler != null) { + IItemHandler selfHandler = new FurnaceSidedItemHandler(this.itemHandler, dir, this.context); + int[] targetSlots = getAutoInputSlotCandidates(mode); + for (int srcSlot = 0; srcSlot < srcHandler.getSlots(); srcSlot++) { + ItemStack extracted = srcHandler.extractItem(srcSlot, Integer.MAX_VALUE, true); + if (extracted.isEmpty()) continue; + + ItemStack remaining = extracted; + for (int machineSlot : targetSlots) { + remaining = selfHandler.insertItem(machineSlot, remaining, false); + if (remaining.isEmpty()) break; + } + + int moved = extracted.getCount() - remaining.getCount(); + if (moved > 0) { + srcHandler.extractItem(srcSlot, moved, false); + movedAny = true; + this.context.markChanged(); + } + } + } + } + + // 输入流体 + if (mode.allowsMaterialInput()) { + IFluidHandler srcFluidHandler = com.sorrowmist.useless.compat.forge1201.capability.CapabilityAccess.get(level, + ForgeCapabilities.FLUID_HANDLER, srcPos, srcEntity.getBlockState(), srcEntity, dir.getOpposite()); + if (srcFluidHandler != null) { + IFluidHandler selfFluidHandler = new FurnaceSidedFluidHandler(this.inputFluidTanks, this.outputFluidTanks, dir, this.context); + FluidStack drained = srcFluidHandler.drain(Integer.MAX_VALUE, IFluidHandler.FluidAction.SIMULATE); + if (!drained.isEmpty()) { + int filled = selfFluidHandler.fill(drained, IFluidHandler.FluidAction.EXECUTE); + if (filled > 0) { + srcFluidHandler.drain(filled, IFluidHandler.FluidAction.EXECUTE); + movedAny = true; + this.context.markChanged(); + } + } + } + } + + // 输入化学品。提取和容量计算都使用 long,且只写入本机输入化学品槽。 + if (mode.allowsMaterialInput() && this.inputChemicalStorage.isAvailable()) { + ChemicalHandlerView sourceChemicalHandler = this.context.getAdjacentChemicalHandler( + level, srcPos, srcEntity.getBlockState(), srcEntity, dir.getOpposite()); + if (sourceChemicalHandler != null) { + ChemicalStackView extracted = sourceChemicalHandler.extractChemical(Long.MAX_VALUE, true); + if (extracted != null && !extracted.isEmpty()) { + ChemicalStackView remainder = this.inputChemicalStorage.insertChemical(extracted, true); + long moved = acceptedAmount(extracted, remainder); + if (moved > 0L) { + ChemicalStackView actual = sourceChemicalHandler.extractChemical(extracted, moved, false); + if (actual != null && !actual.isEmpty()) { + ChemicalStackView localRemainder = this.inputChemicalStorage.insertChemical(actual, false); + if (acceptedAmount(actual, localRemainder) > 0L) { + movedAny = true; + this.context.markChanged(); + } + restoreChemicalRemainder(sourceChemicalHandler, actual, localRemainder); + } + } + } + } + } + } + return movedAny; + } + + private static int[] getAutoInputSlotCandidates(FurnaceFaceMode mode) { + if (mode.allowsMaterialInput()) return MATERIAL_INPUT_SLOT_CANDIDATES; + if (mode.allowsCatalystInput()) return CATALYST_INPUT_SLOT_CANDIDATES; + if (mode.allowsMoldInput()) return MOLD_INPUT_SLOT_CANDIDATES; + return EMPTY_INPUT_SLOT_CANDIDATES; + } + + /** + * 自动输出物品和流体到周围容器和 AE 网络。 + * 仅输出到开启了"原材料输出"模式的面。 + * + * @return 本轮是否至少搬运了一份物品、流体或化学品 + */ + private boolean autoOutputToSurroundings(Level level) { + boolean movedAny = false; + + // AE网络输出不受面模式控制 + for (int slot = OUTPUT_SLOTS_START; slot < OUTPUT_SLOTS_START + OUTPUT_SLOTS_COUNT; slot++) { + ItemStack stack = this.itemHandler.getStackInSlot(slot); + if (stack.isEmpty()) continue; + long inserted = this.context.tryOutputToAE(stack); + if (inserted > 0) { + // 必须走 setStackInSlot:直接 shrink 不触发变更回调,状态修订号与外部库存快照都不会更新, + // 熔炉会以为输出槽仍满而推迟配方搜索 + this.itemHandler.setStackInSlot(slot, remainingAfterInsert(stack, inserted)); + movedAny = true; + this.context.markChanged(); + } + } + + for (int i = 0; i < FLUID_TANK_COUNT; i++) { + FluidStack fluid = this.outputFluidTanks[i].getFluid(); + if (fluid.isEmpty()) continue; + long inserted = this.context.tryOutputFluidToAE(fluid); + if (inserted > 0) { + this.outputFluidTanks[i].drain((int) inserted, IFluidHandler.FluidAction.EXECUTE); + movedAny = true; + } + } + + for (int i = 0; i < this.outputChemicalStorage.size(); i++) { + ChemicalStackView chemical = this.outputChemicalStorage.getStackInSlot(i); + if (chemical.isEmpty()) continue; + long inserted = clampAmount(this.context.tryOutputChemicalToAE(chemical), chemical.amount()); + if (inserted > 0L) { + this.outputChemicalStorage.extractChemical(i, inserted, false); + movedAny = true; + } + } + + Direction facing = this.context.getFacing(); + + // 输出物品到周围容器(仅允许输出模式的面) + for (int slot = OUTPUT_SLOTS_START; slot < OUTPUT_SLOTS_START + OUTPUT_SLOTS_COUNT; slot++) { + ItemStack stack = this.itemHandler.getStackInSlot(slot); + if (stack.isEmpty()) continue; + for (Direction dir : ALL_DIRECTIONS) { + FurnaceFace face = FurnaceFace.fromDirection(dir, facing); + if (face == null) continue; + if (!this.context.getFaceMode(face).allowsMaterialOutput()) continue; + if (this.tryOutputItemToDirection(level, slot, dir)) { + movedAny = true; + this.context.markChanged(); + stack = this.itemHandler.getStackInSlot(slot); + if (stack.isEmpty()) break; + } + } + } + + // 输出流体到周围容器(仅允许输出模式的面) + for (int tankIndex = 0; tankIndex < FLUID_TANK_COUNT; tankIndex++) { + FluidStack fluid = this.outputFluidTanks[tankIndex].getFluid(); + if (fluid.isEmpty()) continue; + for (Direction dir : ALL_DIRECTIONS) { + FurnaceFace face = FurnaceFace.fromDirection(dir, facing); + if (face == null) continue; + if (!this.context.getFaceMode(face).allowsMaterialOutput()) continue; + int filled = this.tryOutputFluidToDirection(level, tankIndex, dir); + if (filled > 0) { + movedAny = true; + this.context.markChanged(); + fluid = this.outputFluidTanks[tankIndex].getFluid(); + if (fluid.isEmpty()) break; + } + } + } + + // 输出化学品到周围容器(仅允许输出模式的面)。 + for (int tankIndex = 0; tankIndex < this.outputChemicalStorage.size(); tankIndex++) { + ChemicalStackView chemical = this.outputChemicalStorage.getStackInSlot(tankIndex); + if (chemical.isEmpty()) continue; + for (Direction dir : ALL_DIRECTIONS) { + FurnaceFace face = FurnaceFace.fromDirection(dir, facing); + if (face == null || !this.context.getFaceMode(face).allowsMaterialOutput()) continue; + long moved = this.tryOutputChemicalToDirection(level, tankIndex, dir); + if (moved > 0L) { + movedAny = true; + this.context.markChanged(); + chemical = this.outputChemicalStorage.getStackInSlot(tankIndex); + if (chemical.isEmpty()) break; + } + } + } + + return movedAny; + } + + /** 扣除已写入 AE 的部分后的剩余栈;全部写入时返回空栈。 */ + private static ItemStack remainingAfterInsert(ItemStack stack, long inserted) { + long remaining = stack.getCount() - Math.min(stack.getCount(), Math.max(0L, inserted)); + return remaining <= 0L ? ItemStack.EMPTY : stack.copyWithCount((int) remaining); + } + + /** + * 尝试向指定方向输出物品。 + * + * @return 是否成功输出至少一部分物品 + */ + private boolean tryOutputItemToDirection(Level level, int slot, Direction direction) { + BlockPos targetPos = this.worldPosition.relative(direction); + BlockEntity targetEntity = level.getBlockEntity(targetPos); + + if (targetEntity == null) return false; + + IItemHandler targetHandler = com.sorrowmist.useless.compat.forge1201.capability.CapabilityAccess.get(level, + ForgeCapabilities.ITEM_HANDLER, + targetPos, + targetEntity.getBlockState(), + targetEntity, + direction.getOpposite() + ); + + if (targetHandler == null) return false; + + ItemStack stack = this.itemHandler.getStackInSlot(slot); + if (stack.isEmpty()) return false; + + for (int targetSlot = 0; targetSlot < targetHandler.getSlots(); targetSlot++) { + ItemStack remaining = targetHandler.insertItem(targetSlot, stack, false); + if (remaining.getCount() != stack.getCount()) { + this.itemHandler.setStackInSlot(slot, remaining); + this.context.markChanged(); + if (remaining.isEmpty()) { + return true; + } else { + stack = remaining; + } + } + } + + return false; + } + + /** + * 尝试向指定方向输出流体。 + * + * @return 成功输出的流体量 + */ + private int tryOutputFluidToDirection(Level level, int tankIndex, Direction direction) { + BlockPos targetPos = this.worldPosition.relative(direction); + BlockEntity targetEntity = level.getBlockEntity(targetPos); + + if (targetEntity == null) return 0; + + IFluidHandler targetHandler = com.sorrowmist.useless.compat.forge1201.capability.CapabilityAccess.get(level, + ForgeCapabilities.FLUID_HANDLER, + targetPos, + targetEntity.getBlockState(), + targetEntity, + direction.getOpposite() + ); + + if (targetHandler == null) return 0; + + FluidStack fluid = this.outputFluidTanks[tankIndex].getFluid(); + if (fluid.isEmpty()) return 0; + + int filled = targetHandler.fill(fluid, IFluidHandler.FluidAction.EXECUTE); + if (filled > 0) { + this.outputFluidTanks[tankIndex].drain(filled, IFluidHandler.FluidAction.EXECUTE); + this.context.markChanged(); + } + + return filled; + } + + private long tryOutputChemicalToDirection(Level level, int tankIndex, Direction direction) { + BlockPos targetPos = this.worldPosition.relative(direction); + BlockEntity targetEntity = level.getBlockEntity(targetPos); + if (targetEntity == null) return 0L; + + ChemicalHandlerView targetHandler = this.context.getAdjacentChemicalHandler( + level, targetPos, targetEntity.getBlockState(), targetEntity, direction.getOpposite()); + if (targetHandler == null) return 0L; + + ChemicalStackView chemical = this.outputChemicalStorage.getStackInSlot(tankIndex); + if (chemical.isEmpty()) return 0L; + ChemicalStackView remainder = targetHandler.insertChemical(chemical, true); + long moved = acceptedAmount(chemical, remainder); + if (moved <= 0L) return 0L; + + ChemicalStackView accepted = targetHandler.insertChemical(chemical.copyWithAmount(moved), false); + long actualMoved = acceptedAmount(chemical.copyWithAmount(moved), accepted); + if (actualMoved > 0L) { + this.outputChemicalStorage.extractChemical(tankIndex, actualMoved, false); + } + return actualMoved; + } + + private void restoreChemicalRemainder(ChemicalHandlerView source, ChemicalStackView extracted, + ChemicalStackView remainder) { + ChemicalStackView remaining = normalizeRemainder(extracted, remainder); + if (remaining.isEmpty()) return; + + remaining = source.insertChemical(remaining, false); + if (remaining == null || remaining.isEmpty()) return; + remaining = this.inputChemicalStorage.insertChemical(remaining, false); + if (remaining == null || remaining.isEmpty()) return; + remaining = this.outputChemicalStorage.insertChemical(remaining, false); + if (remaining != null && !remaining.isEmpty()) { + this.context.handleUnreturnedChemical(remaining); + } + } + + private static long acceptedAmount(ChemicalStackView requested, ChemicalStackView remainder) { + if (requested == null || requested.isEmpty() || remainder == null) return 0L; + long remaining = clampAmount(remainder.amount(), requested.amount()); + return requested.amount() - remaining; + } + + private static ChemicalStackView normalizeRemainder(ChemicalStackView requested, ChemicalStackView remainder) { + if (requested == null || requested.isEmpty()) return ChemicalStackView.EMPTY; + if (remainder == null) return requested; + long amount = clampAmount(remainder.amount(), requested.amount()); + return amount <= 0L ? ChemicalStackView.EMPTY : requested.copyWithAmount(amount); + } + + private static long clampAmount(long amount, long maximum) { + return Math.max(0L, Math.min(Math.max(0L, maximum), amount)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/io/FurnaceCombinedFluidTankHandler.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/io/FurnaceCombinedFluidTankHandler.java new file mode 100644 index 00000000..5f7f5d68 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/io/FurnaceCombinedFluidTankHandler.java @@ -0,0 +1,98 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.io; + +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.capability.IFluidHandler; +import net.minecraftforge.fluids.capability.templates.FluidTank; +import org.jetbrains.annotations.NotNull; + +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.FLUID_TANK_COUNT; + +/** + * 复合流体处理器:同时暴露输入槽与输出槽。 + * 填充仅作用于输入槽,抽取优先从输出槽进行。 + */ +public record FurnaceCombinedFluidTankHandler(FluidTank[] inputTanks, FluidTank[] outputTanks) implements IFluidHandler { + @Override + public int getTanks() { + return FLUID_TANK_COUNT * 2; + } + + @Override + public @NotNull FluidStack getFluidInTank(int tank) { + if (tank < FLUID_TANK_COUNT) { + return this.inputTanks[tank].getFluid(); + } else if (tank < FLUID_TANK_COUNT * 2) { + return this.outputTanks[tank - FLUID_TANK_COUNT].getFluid(); + } + return FluidStack.EMPTY; + } + + @Override + public int getTankCapacity(int tank) { + if (tank < FLUID_TANK_COUNT) { + return this.inputTanks[tank].getCapacity(); + } else if (tank < FLUID_TANK_COUNT * 2) { + return this.outputTanks[tank - FLUID_TANK_COUNT].getCapacity(); + } + return 0; + } + + @Override + public boolean isFluidValid(int tank, @NotNull FluidStack stack) { + if (tank < FLUID_TANK_COUNT) { + return this.inputTanks[tank].isFluidValid(stack); + } else if (tank < FLUID_TANK_COUNT * 2) { + return this.outputTanks[tank - FLUID_TANK_COUNT].isFluidValid(stack); + } + return false; + } + + @Override + public int fill(@NotNull FluidStack resource, @NotNull FluidAction action) { + return fillInput(inputTanks, resource, action); + } + + /** + * 静态辅助方法,用于 SidedFluidHandler 复用。 + */ + public static int fillInput(FluidTank[] inputTanks, FluidStack resource, FluidAction action) { + for (FluidTank tank : inputTanks) { + if (tank.isFluidValid(resource)) { + if (tank.getFluid().isEmpty() + || com.sorrowmist.useless.compat.forge1201.fluid.FluidStacks.same(tank.getFluid(), resource)) { + int filled = tank.fill(resource, action); + if (filled > 0) return filled; + } + } + } + return 0; + } + + @Override + public @NotNull FluidStack drain(@NotNull FluidStack resource, @NotNull FluidAction action) { + for (int i = 0; i < FLUID_TANK_COUNT; i++) { + FluidStack drained = this.outputTanks[i].drain(resource, action); + if (!drained.isEmpty()) return drained; + } + for (int i = 0; i < FLUID_TANK_COUNT; i++) { + FluidStack drained = this.inputTanks[i].drain(resource, action); + if (!drained.isEmpty()) return drained; + } + return FluidStack.EMPTY; + } + + @Override + public @NotNull FluidStack drain(int maxDrain, @NotNull FluidAction action) { + for (int i = 0; i < FLUID_TANK_COUNT; i++) { + if (!this.outputTanks[i].getFluid().isEmpty()) { + return this.outputTanks[i].drain(maxDrain, action); + } + } + for (int i = 0; i < FLUID_TANK_COUNT; i++) { + if (!this.inputTanks[i].getFluid().isEmpty()) { + return this.inputTanks[i].drain(maxDrain, action); + } + } + return FluidStack.EMPTY; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/io/FurnaceFaceAccessor.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/io/FurnaceFaceAccessor.java new file mode 100644 index 00000000..65407db6 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/io/FurnaceFaceAccessor.java @@ -0,0 +1,23 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.io; + +import com.sorrowmist.useless.api.enums.FurnaceFace; +import com.sorrowmist.useless.api.enums.FurnaceFaceMode; +import net.minecraft.core.Direction; + +/** + * 方向感知处理器所需的最小面配置访问接口。 + *

+ * 用于解耦 io 包中的方向感知处理器与方块实体,避免包间循环依赖。 + */ +public interface FurnaceFaceAccessor { + + /** + * 获取方块的水平朝向。 + */ + Direction getFacing(); + + /** + * 获取指定逻辑面的输入输出模式。 + */ + FurnaceFaceMode getFaceMode(FurnaceFace face); +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/io/FurnaceFluidTankHandler.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/io/FurnaceFluidTankHandler.java new file mode 100644 index 00000000..94cc6e66 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/io/FurnaceFluidTankHandler.java @@ -0,0 +1,69 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.io; + +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.capability.IFluidHandler; +import net.minecraftforge.fluids.capability.templates.FluidTank; +import org.jetbrains.annotations.NotNull; + +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.FLUID_TANK_COUNT; + +/** + * 单向流体处理器:包装一组流体槽,可选择是否允许填充。 + */ +public record FurnaceFluidTankHandler(FluidTank[] tanks, boolean allowFill) implements IFluidHandler { + + @Override + public int getTanks() { + return FLUID_TANK_COUNT; + } + + @Override + public @NotNull FluidStack getFluidInTank(int tank) { + return this.tanks[tank].getFluid(); + } + + @Override + public int getTankCapacity(int tank) { + return this.tanks[tank].getCapacity(); + } + + @Override + public boolean isFluidValid(int tank, @NotNull FluidStack stack) { + return this.tanks[tank].isFluidValid(stack); + } + + @Override + public int fill(@NotNull FluidStack resource, @NotNull FluidAction action) { + if (!this.allowFill) return 0; + for (int i = 0; i < FLUID_TANK_COUNT; i++) { + FluidTank tank = this.tanks[i]; + if (tank.isFluidValid(resource)) { + if (tank.getFluid().isEmpty() + || com.sorrowmist.useless.compat.forge1201.fluid.FluidStacks.same(tank.getFluid(), resource)) { + int filled = tank.fill(resource, action); + if (filled > 0) return filled; + } + } + } + return 0; + } + + @Override + public @NotNull FluidStack drain(@NotNull FluidStack resource, @NotNull FluidAction action) { + for (int i = 0; i < FLUID_TANK_COUNT; i++) { + FluidStack drained = this.tanks[i].drain(resource, action); + if (!drained.isEmpty()) return drained; + } + return FluidStack.EMPTY; + } + + @Override + public @NotNull FluidStack drain(int maxDrain, @NotNull FluidAction action) { + for (int i = 0; i < FLUID_TANK_COUNT; i++) { + if (!this.tanks[i].getFluid().isEmpty()) { + return this.tanks[i].drain(maxDrain, action); + } + } + return FluidStack.EMPTY; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/io/FurnaceInputPort.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/io/FurnaceInputPort.java new file mode 100644 index 00000000..bf1b901b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/io/FurnaceInputPort.java @@ -0,0 +1,261 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.io; + +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalKeyProvider; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.FurnaceChemicalStorage; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.FluidIngredientAllocator; +import com.sorrowmist.useless.content.recipe.ItemIngredientAllocator; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.capability.IFluidHandler; +import net.minecraftforge.fluids.capability.templates.FluidTank; +import net.minecraftforge.items.ItemStackHandler; + +import java.util.ArrayList; +import java.util.List; + +/** + * 高级合金炉统一输入端口。 + * 负责按配方并行数从本地物品输入槽和流体输入槽中消耗材料。 + */ +public final class FurnaceInputPort { + private FurnaceInputPort() { + } + + /** + * 按指定并行数消耗配方所需的物品和流体输入。 + */ + public static boolean consumeRecipeInputs(AdvancedAlloyFurnaceRecipe recipe, int parallel, ItemStackHandler itemHandler, int inputSlotsStart, int inputSlotsCount, FluidTank[] inputFluidTanks, int fluidTankCount) { + return consumeRecipeInputs(recipe, parallel, itemHandler, inputSlotsStart, inputSlotsCount, + inputFluidTanks, fluidTankCount, FurnaceChemicalStorage.DISABLED, + ChemicalKeyProvider.NONE); + } + + public static boolean consumeRecipeInputs(AdvancedAlloyFurnaceRecipe recipe, int parallel, + ItemStackHandler itemHandler, int inputSlotsStart, + int inputSlotsCount, FluidTank[] inputFluidTanks, + int fluidTankCount, FurnaceChemicalStorage inputChemicalStorage, + ChemicalKeyProvider chemicalKeyProvider) { + PreparedInputs prepared = prepareInputs( + recipe, parallel, itemHandler, inputSlotsStart, inputSlotsCount, inputFluidTanks, + fluidTankCount, inputChemicalStorage, chemicalKeyProvider); + if (prepared == null) return false; + + for (int i = 0; i < prepared.itemAllocation().inputCount(); i++) { + long consumed = prepared.itemAllocation().consumedFromInput(i); + if (consumed <= 0) continue; + + int slot = inputSlotsStart + i; + ItemStack remaining = itemHandler.getStackInSlot(slot).copy(); + remaining.shrink((int) consumed); + itemHandler.setStackInSlot(slot, remaining); + } + + for (int i = 0; i < prepared.fluidConsumedByTank().length; i++) { + long remaining = prepared.fluidConsumedByTank()[i]; + while (remaining > 0) { + int chunk = (int) Math.min(remaining, Integer.MAX_VALUE); + FluidStack drained = inputFluidTanks[i].drain(chunk, IFluidHandler.FluidAction.EXECUTE); + if (drained.getAmount() != chunk) { + throw new IllegalStateException("Validated alloy furnace fluid input changed during consumption"); + } + remaining -= chunk; + } + } + + for (int i = 0; i < prepared.chemicalConsumedByTank().length; i++) { + long amount = prepared.chemicalConsumedByTank()[i]; + if (amount <= 0L) continue; + var extracted = inputChemicalStorage.extractChemical(i, amount, false); + if (extracted.isEmpty() || extracted.amount() != amount) { + throw new IllegalStateException("Validated alloy furnace chemical input changed during consumption"); + } + } + return true; + } + + public static boolean canConsumeRecipeInputs(AdvancedAlloyFurnaceRecipe recipe, int parallel, ItemStackHandler itemHandler, int inputSlotsStart, int inputSlotsCount, FluidTank[] inputFluidTanks, int fluidTankCount) { + return canConsumeRecipeInputs(recipe, parallel, itemHandler, inputSlotsStart, inputSlotsCount, + inputFluidTanks, fluidTankCount, FurnaceChemicalStorage.DISABLED, + ChemicalKeyProvider.NONE); + } + + public static boolean canConsumeRecipeInputs(AdvancedAlloyFurnaceRecipe recipe, int parallel, + ItemStackHandler itemHandler, int inputSlotsStart, + int inputSlotsCount, FluidTank[] inputFluidTanks, + int fluidTankCount, FurnaceChemicalStorage inputChemicalStorage, + ChemicalKeyProvider chemicalKeyProvider) { + return prepareInputs( + recipe, parallel, itemHandler, inputSlotsStart, inputSlotsCount, inputFluidTanks, + fluidTankCount, inputChemicalStorage, chemicalKeyProvider) != null; + } + + public static int calculateMaterialParallel(AdvancedAlloyFurnaceRecipe recipe, ItemStackHandler itemHandler, int inputSlotsStart, int inputSlotsCount, FluidTank[] inputFluidTanks, int fluidTankCount) { + return calculateMaterialParallel(recipe, itemHandler, inputSlotsStart, inputSlotsCount, + inputFluidTanks, fluidTankCount, FurnaceChemicalStorage.DISABLED, + ChemicalKeyProvider.NONE); + } + + public static int calculateMaterialParallel(AdvancedAlloyFurnaceRecipe recipe, + ItemStackHandler itemHandler, int inputSlotsStart, + int inputSlotsCount, FluidTank[] inputFluidTanks, + int fluidTankCount, FurnaceChemicalStorage inputChemicalStorage, + ChemicalKeyProvider chemicalKeyProvider) { + if (recipe == null) return 0; + + List itemInputs = snapshotItems(itemHandler, inputSlotsStart, inputSlotsCount); + int itemParallel = ItemIngredientAllocator.maxOperations(recipe.inputs(), itemInputs); + int fluidParallel = maxFluidOperations(recipe.inputFluids(), inputFluidTanks, fluidTankCount); + int chemicalParallel = maxChemicalOperations(recipe.keyInputs(), inputChemicalStorage, chemicalKeyProvider); + boolean hasItemDemand = recipe.inputs().stream().anyMatch(input -> input != null && input.count() > 0); + boolean hasFluidDemand = recipe.inputFluids().stream().anyMatch(input -> input != null && input.amount() > 0); + boolean hasChemicalDemand = hasChemicalDemand(recipe.keyInputs(), chemicalKeyProvider); + if (!hasItemDemand && !hasFluidDemand && !hasChemicalDemand) return 1; + return Math.min(itemParallel, Math.min(fluidParallel, chemicalParallel)); + } + + private static PreparedInputs prepareInputs(AdvancedAlloyFurnaceRecipe recipe, long operations, ItemStackHandler itemHandler, int inputSlotsStart, int inputSlotsCount, FluidTank[] inputFluidTanks, int fluidTankCount) { + return prepareInputs(recipe, operations, itemHandler, inputSlotsStart, inputSlotsCount, + inputFluidTanks, fluidTankCount, FurnaceChemicalStorage.DISABLED, + ChemicalKeyProvider.NONE); + } + + private static PreparedInputs prepareInputs(AdvancedAlloyFurnaceRecipe recipe, long operations, + ItemStackHandler itemHandler, int inputSlotsStart, + int inputSlotsCount, FluidTank[] inputFluidTanks, + int fluidTankCount, FurnaceChemicalStorage inputChemicalStorage, + ChemicalKeyProvider chemicalKeyProvider) { + if (recipe == null || operations <= 0 || itemHandler == null || inputFluidTanks == null) return null; + + List itemInputs = snapshotItems(itemHandler, inputSlotsStart, inputSlotsCount); + ItemIngredientAllocator.Allocation itemAllocation = ItemIngredientAllocator.allocate( + recipe.inputs(), itemInputs, operations); + if (itemAllocation == null) return null; + + long[] fluidAllocation = allocateFluids(recipe.inputFluids(), operations, inputFluidTanks, fluidTankCount); + if (fluidAllocation == null) return null; + long[] chemicalAllocation = allocateChemicals(recipe.keyInputs(), operations, + inputChemicalStorage, chemicalKeyProvider); + if (chemicalAllocation == null) return null; + return new PreparedInputs(itemAllocation, fluidAllocation, chemicalAllocation); + } + + private static List snapshotItems(ItemStackHandler itemHandler, int inputSlotsStart, int inputSlotsCount) { + int count = Math.max(0, Math.min(inputSlotsCount, itemHandler.getSlots() - inputSlotsStart)); + List inputs = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + inputs.add(itemHandler.getStackInSlot(inputSlotsStart + i)); + } + return inputs; + } + + private static long[] allocateFluids(List requirements, long operations, FluidTank[] tanks, int fluidTankCount) { + FluidIngredientAllocator.Allocation allocation = FluidIngredientAllocator.allocateTanksLong( + requirements, tanks, fluidTankCount, operations); + if (allocation == null) return null; + long[] consumed = new long[allocation.supplyCount()]; + for (int i = 0; i < consumed.length; i++) { + consumed[i] = allocation.consumedFromSupply(i); + } + return consumed; + } + + private static int maxFluidOperations(List requirements, FluidTank[] tanks, int fluidTankCount) { + return FluidIngredientAllocator.maxTankOperationsLong(requirements, tanks, fluidTankCount); + } + + private static long[] allocateChemicals(List requirements, long operations, + FurnaceChemicalStorage storage, + ChemicalKeyProvider provider) { + int count = storage == null ? 0 : storage.size(); + long[] consumedByTank = new long[count]; + for (ChemicalDemand demand : mergeChemicalDemands(requirements)) { + // keyInputs also carries non-chemical AE materials used by other integrations. + // Those are supplied by the AE task path and do not belong in local chemical tanks. + if (provider == null || !provider.isChemicalKey(demand.key())) continue; + long remaining = saturatingMultiply(demand.amount(), operations); + for (int i = 0; i < count && remaining > 0L; i++) { + GenericStack available = provider.toGenericStack(storage.getStackInSlot(i)); + if (available == null || !demand.key().equals(available.what())) continue; + long free = available.amount() - consumedByTank[i]; + if (free <= 0L) continue; + long consumed = Math.min(remaining, free); + consumedByTank[i] += consumed; + remaining -= consumed; + } + if (remaining > 0L) return null; + } + return consumedByTank; + } + + private static int maxChemicalOperations(List requirements, + FurnaceChemicalStorage storage, + ChemicalKeyProvider provider) { + List demands = mergeChemicalDemands(requirements).stream() + .filter(demand -> provider != null && provider.isChemicalKey(demand.key())) + .toList(); + if (demands.isEmpty()) return Integer.MAX_VALUE; + if (storage == null || provider == null) return 0; + + int maximum = Integer.MAX_VALUE; + for (ChemicalDemand demand : demands) { + long availableAmount = 0L; + for (int i = 0; i < storage.size(); i++) { + GenericStack available = provider.toGenericStack(storage.getStackInSlot(i)); + if (available != null && demand.key().equals(available.what())) { + availableAmount = saturatingAdd(availableAmount, available.amount()); + } + } + long operations = availableAmount / demand.amount(); + maximum = Math.min(maximum, operations > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) operations); + } + return maximum; + } + + private static boolean hasChemicalDemand(List requirements, + ChemicalKeyProvider provider) { + if (requirements == null || provider == null) return false; + return requirements.stream().anyMatch(input -> input != null && input.what() != null + && input.amount() > 0L && provider.isChemicalKey(input.what())); + } + + private static List mergeChemicalDemands(List requirements) { + List demands = new ArrayList<>(); + if (requirements == null) return demands; + for (GenericStack requirement : requirements) { + if (requirement == null || requirement.what() == null || requirement.amount() <= 0L) continue; + boolean merged = false; + for (int i = 0; i < demands.size(); i++) { + ChemicalDemand existing = demands.get(i); + if (existing.key().equals(requirement.what())) { + demands.set(i, new ChemicalDemand(existing.key(), saturatingAdd(existing.amount(), requirement.amount()))); + merged = true; + break; + } + } + if (!merged) demands.add(new ChemicalDemand(requirement.what(), requirement.amount())); + } + return demands; + } + + private static long saturatingAdd(long left, long right) { + if (right > 0 && left > Long.MAX_VALUE - right) return Long.MAX_VALUE; + return left + right; + } + + private static long saturatingMultiply(long amount, long multiplier) { + if (amount <= 0 || multiplier <= 0) return 0; + if (amount > Long.MAX_VALUE / multiplier) return Long.MAX_VALUE; + return amount * multiplier; + } + + private record ChemicalDemand(AEKey key, long amount) { + } + + private record PreparedInputs(ItemIngredientAllocator.Allocation itemAllocation, + long[] fluidConsumedByTank, long[] chemicalConsumedByTank) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/io/FurnaceOutputPort.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/io/FurnaceOutputPort.java new file mode 100644 index 00000000..0f5dba96 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/io/FurnaceOutputPort.java @@ -0,0 +1,351 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.io; + +import appeng.api.stacks.AEFluidKey; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalKeyProvider; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.FurnaceChemicalStorage; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.capability.IFluidHandler; +import net.minecraftforge.fluids.capability.templates.FluidTank; +import net.minecraftforge.items.ItemStackHandler; +import org.jetbrains.annotations.Nullable; + +import java.util.function.BiConsumer; + +/** + * 高级合金炉统一输出端口。 + * 负责把产物优先写入 AE 网络,剩余部分再回退到本地物品槽或流体槽。 + */ +public final class FurnaceOutputPort { + private FurnaceOutputPort() { + } + + /** + * AE 输出适配接口,用于把方块实体或 AE 任务上下文适配为统一输出入口。 + */ + public interface AeOutput { + long insertItem(ItemStack stack); + + long insertFluid(FluidStack stack); + + long insertKey(AEKey key, long amount); + } + + /** + * 按配方批量输出物品、流体和 AEKey 产物。 + */ + public static void outputRecipe(AdvancedAlloyFurnaceRecipe recipe, int parallel, AeOutput aeOutput, ItemStackHandler itemHandler, int outputSlotsStart, int outputSlotsCount, FluidTank[] outputFluidTanks, int fluidTankCount) { + outputRecipe(recipe, parallel, aeOutput, itemHandler, outputSlotsStart, outputSlotsCount, + outputFluidTanks, fluidTankCount, FurnaceChemicalStorage.DISABLED, + ChemicalKeyProvider.NONE, null); + } + + public static void outputRecipe(AdvancedAlloyFurnaceRecipe recipe, int parallel, AeOutput aeOutput, + ItemStackHandler itemHandler, int outputSlotsStart, int outputSlotsCount, + FluidTank[] outputFluidTanks, int fluidTankCount, + FurnaceChemicalStorage outputChemicalStorage, + ChemicalKeyProvider chemicalKeyProvider) { + outputRecipe(recipe, parallel, aeOutput, itemHandler, outputSlotsStart, outputSlotsCount, + outputFluidTanks, fluidTankCount, outputChemicalStorage, chemicalKeyProvider, null); + } + + /** Same as the normal recipe output path, retaining key material that fits nowhere locally. */ + public static void outputRecipe(AdvancedAlloyFurnaceRecipe recipe, int parallel, AeOutput aeOutput, + ItemStackHandler itemHandler, int outputSlotsStart, int outputSlotsCount, + FluidTank[] outputFluidTanks, int fluidTankCount, + FurnaceChemicalStorage outputChemicalStorage, + ChemicalKeyProvider chemicalKeyProvider, + BiConsumer onKeyRemainder) { + for (ItemStack output : recipe.outputs()) { + long totalCountLong = (long) output.getCount() * parallel; + if (totalCountLong <= 0L) { + continue; + } + AEItemKey outputKey = AEItemKey.of(output); + if (outputKey != null && totalCountLong > outputKey.getMaxStackSize()) { + // 超过单栈上限的数量不允许物化成越界 ItemStack(1.21 上限 99, + // ItemStack.CODEC 的 count 是 intRange(1, 99) 且 save() 走 getOrThrow), + // 一律以 AEKey + long 投影,剩余交给 onKeyRemainder。 + GenericStack remainder = outputKeyWithRemainder( + new GenericStack(outputKey, totalCountLong), aeOutput, + itemHandler, outputSlotsStart, outputSlotsCount, null, 0, + outputChemicalStorage, chemicalKeyProvider); + if (remainder != null && onKeyRemainder != null) { + onKeyRemainder.accept(remainder.what(), remainder.amount()); + } + continue; + } + ItemStack toOutput = output.copy(); + toOutput.setCount((int) totalCountLong); + outputItem(toOutput, aeOutput, itemHandler, outputSlotsStart, outputSlotsCount); + } + + for (FluidStack outputFluid : recipe.outputFluids()) { + long totalAmountLong = (long) outputFluid.getAmount() * parallel; + if (totalAmountLong <= 0L) { + continue; + } + AEFluidKey outputFluidKey = AEFluidKey.of(outputFluid); + if (outputFluidKey != null && totalAmountLong > Integer.MAX_VALUE) { + // FluidStack 的数量是 int:超过 int 的部分不再截断丢弃,改以 AEKey + long 投影。 + GenericStack remainder = outputKeyWithRemainder( + new GenericStack(outputFluidKey, totalAmountLong), aeOutput, + null, 0, 0, outputFluidTanks, fluidTankCount, + outputChemicalStorage, chemicalKeyProvider); + if (remainder != null && onKeyRemainder != null) { + onKeyRemainder.accept(remainder.what(), remainder.amount()); + } + continue; + } + FluidStack toOutput = outputFluid.copy(); + toOutput.setAmount((int) totalAmountLong); + outputFluid(toOutput, aeOutput, outputFluidTanks, fluidTankCount); + } + + for (var keyOutput : recipe.keyOutputs()) { + if (keyOutput == null || keyOutput.what() == null || keyOutput.amount() <= 0L) continue; + GenericStack remainder = outputKeyWithRemainder( + new GenericStack(keyOutput.what(), saturatingMultiply(keyOutput.amount(), parallel)), + aeOutput, outputChemicalStorage, chemicalKeyProvider); + if (remainder != null && onKeyRemainder != null) { + onKeyRemainder.accept(remainder.what(), remainder.amount()); + } + } + } + + /** + * 输出单个物品堆,AE 写入失败的剩余部分回退到本地槽位。 + */ + public static void outputItem(ItemStack stack, AeOutput aeOutput, ItemStackHandler itemHandler, int slotsStart, int slotsCount) { + outputItemWithRemainder(stack, aeOutput, itemHandler, slotsStart, slotsCount); + } + + /** + * 输出单个物品堆,AE 写入失败的剩余部分回退到本地槽位。 + * + * @return 本地槽位也放不下的剩余部分(全部放入则为 EMPTY),由调用方决定去向 + */ + public static ItemStack outputItemWithRemainder(ItemStack stack, AeOutput aeOutput, ItemStackHandler itemHandler, int slotsStart, int slotsCount) { + if (stack.isEmpty()) { + return ItemStack.EMPTY; + } + long inserted = aeOutput.insertItem(stack); + int remainingCount = (int) Math.max(0, stack.getCount() - inserted); + if (remainingCount <= 0) { + return ItemStack.EMPTY; + } + ItemStack remainingStack = stack.copy(); + remainingStack.setCount(remainingCount); + int leftover = insertItemIntoSlots(remainingStack, itemHandler, slotsStart, slotsCount); + if (leftover <= 0) { + return ItemStack.EMPTY; + } + ItemStack leftoverStack = stack.copy(); + leftoverStack.setCount(leftover); + return leftoverStack; + } + + /** + * 输出单个流体栈,AE 写入失败的剩余部分回退到本地流体槽。 + */ + public static void outputFluid(FluidStack stack, AeOutput aeOutput, FluidTank[] tanks, int tankCount) { + outputFluidWithRemainder(stack, aeOutput, tanks, tankCount); + } + + /** + * 输出单个流体栈,AE 写入失败的剩余部分回退到本地流体槽。 + * + * @return 流体槽也装不下的剩余部分(全部装入则为 EMPTY),由调用方决定去向 + */ + public static FluidStack outputFluidWithRemainder(FluidStack stack, AeOutput aeOutput, FluidTank[] tanks, int tankCount) { + if (stack.isEmpty()) { + return FluidStack.EMPTY; + } + long inserted = aeOutput.insertFluid(stack); + int remainingAmount = (int) Math.max(0, stack.getAmount() - inserted); + if (remainingAmount <= 0) { + return FluidStack.EMPTY; + } + FluidStack remainingFluid = stack.copy(); + remainingFluid.setAmount(remainingAmount); + return fillFluidTanks(remainingFluid, tanks, tankCount); + } + + /** + * 输出 AEKey 类型产物。 + */ + public static void outputKey(AEKey key, long amount, AeOutput aeOutput) { + aeOutput.insertKey(key, amount); + } + + /** + * Outputs an AE key, then falls back to the local chemical slots when the + * registered provider recognizes it as a chemical key. + * + * @return the portion that could not be written to either destination + */ + public static GenericStack outputKeyWithRemainder(GenericStack stack, AeOutput aeOutput, + FurnaceChemicalStorage outputChemicalStorage, + ChemicalKeyProvider chemicalKeyProvider) { + return outputExactKey( + stack, aeOutput, null, 0, 0, null, 0, + outputChemicalStorage, chemicalKeyProvider); + } + + /** + * Outputs one exact AE key while projecting only the amount that can fit into the finite local destinations. + * The retained remainder stays compact regardless of its numeric size. + */ + public static GenericStack outputKeyWithRemainder( + GenericStack stack, + AeOutput aeOutput, + ItemStackHandler itemHandler, + int slotsStart, + int slotsCount, + FluidTank[] fluidTanks, + int fluidTankCount, + FurnaceChemicalStorage outputChemicalStorage, + ChemicalKeyProvider chemicalKeyProvider) { + return outputExactKey( + stack, aeOutput, itemHandler, slotsStart, slotsCount, fluidTanks, fluidTankCount, + outputChemicalStorage, chemicalKeyProvider); + } + + private static GenericStack outputExactKey( + GenericStack stack, + AeOutput aeOutput, + @Nullable ItemStackHandler itemHandler, + int slotsStart, + int slotsCount, + FluidTank @Nullable [] fluidTanks, + int fluidTankCount, + FurnaceChemicalStorage outputChemicalStorage, + ChemicalKeyProvider chemicalKeyProvider) { + if (stack == null || stack.what() == null || stack.amount() <= 0L) return null; + + long requested = stack.amount(); + long inserted = clampInserted(aeOutput.insertKey(stack.what(), requested), requested); + long remaining = requested - inserted; + if (remaining <= 0L) return null; + + if (stack.what() instanceof AEItemKey itemKey && itemHandler != null) { + remaining = insertItemKeyIntoSlots(itemKey, remaining, itemHandler, slotsStart, slotsCount); + } else if (stack.what() instanceof AEFluidKey fluidKey && fluidTanks != null) { + remaining = fillFluidKeyIntoTanks(fluidKey, remaining, fluidTanks, fluidTankCount); + } else if (chemicalKeyProvider != null && chemicalKeyProvider.isChemicalKey(stack.what())) { + var view = chemicalKeyProvider.fromGenericStack(new GenericStack(stack.what(), remaining)); + if (view != null && outputChemicalStorage != null) { + var remainder = outputChemicalStorage.insertChemical(view, false); + long local = Math.max(0L, remaining - remainder.amount()); + remaining -= local; + } + } + return remaining <= 0L ? null : new GenericStack(stack.what(), remaining); + } + + private static long insertItemKeyIntoSlots( + AEItemKey key, + long amount, + ItemStackHandler itemHandler, + int slotsStart, + int slotsCount) { + long remaining = amount; + int slotsEnd = Math.min(itemHandler.getSlots(), Math.addExact(slotsStart, slotsCount)); + for (int slot = slotsStart; slot < slotsEnd && remaining > 0L; slot++) { + ItemStack slotStack = itemHandler.getStackInSlot(slot); + if (slotStack.isEmpty() || !key.equals(AEItemKey.of(slotStack))) { + continue; + } + int space = itemHandler.getSlotLimit(slot) - slotStack.getCount(); + if (space <= 0) { + continue; + } + int inserted = (int) Math.min(remaining, space); + slotStack.grow(inserted); + remaining -= inserted; + } + for (int slot = slotsStart; slot < slotsEnd && remaining > 0L; slot++) { + if (!itemHandler.getStackInSlot(slot).isEmpty()) { + continue; + } + int slotLimit = itemHandler.getSlotLimit(slot); + if (slotLimit <= 0) { + continue; + } + int inserted = (int) Math.min(remaining, slotLimit); + itemHandler.setStackInSlot(slot, key.toStack(inserted)); + remaining -= inserted; + } + return remaining; + } + + private static long fillFluidKeyIntoTanks( + AEFluidKey key, + long amount, + FluidTank[] tanks, + int tankCount) { + long remaining = amount; + int tanksEnd = Math.min(tanks.length, tankCount); + for (int index = 0; index < tanksEnd && remaining > 0L; index++) { + int offered = (int) Math.min(remaining, Integer.MAX_VALUE); + int inserted = tanks[index].fill(key.toStack(offered), IFluidHandler.FluidAction.EXECUTE); + if (inserted > 0) { + remaining -= inserted; + } + } + return remaining; + } + + private static long clampInserted(long inserted, long requested) { + return Math.max(0L, Math.min(requested, inserted)); + } + + private static long saturatingMultiply(long amount, long multiplier) { + if (amount <= 0L || multiplier <= 0L) return 0L; + return amount > Long.MAX_VALUE / multiplier ? Long.MAX_VALUE : amount * multiplier; + } + + /** @return 槽位放不下的剩余数量 */ + private static int insertItemIntoSlots(ItemStack stack, ItemStackHandler itemHandler, int slotsStart, int slotsCount) { + int remainingCount = stack.getCount(); + for (int i = slotsStart; i < slotsStart + slotsCount && remainingCount > 0; i++) { + ItemStack slotStack = itemHandler.getStackInSlot(i); + if (ItemStack.isSameItemSameTags(slotStack, stack)) { + int space = itemHandler.getSlotLimit(i) - slotStack.getCount(); + int toAdd = Math.min(space, remainingCount); + slotStack.grow(toAdd); + remainingCount -= toAdd; + } + } + + for (int i = slotsStart; i < slotsStart + slotsCount && remainingCount > 0; i++) { + ItemStack slotStack = itemHandler.getStackInSlot(i); + if (slotStack.isEmpty()) { + ItemStack insertedStack = stack.copy(); + int toAdd = Math.min(itemHandler.getSlotLimit(i), remainingCount); + insertedStack.setCount(toAdd); + itemHandler.setStackInSlot(i, insertedStack); + remainingCount -= toAdd; + } + } + return remainingCount; + } + + /** @return 流体槽装不下的剩余部分(全部装入则为 EMPTY) */ + private static FluidStack fillFluidTanks(FluidStack fluidStack, FluidTank[] tanks, int tankCount) { + FluidStack remainingFluid = fluidStack.copy(); + for (int i = 0; i < tankCount && !remainingFluid.isEmpty(); i++) { + FluidTank tank = tanks[i]; + FluidStack tankFluid = tank.getFluid(); + if (tankFluid.isEmpty() || com.sorrowmist.useless.compat.forge1201.fluid.FluidStacks.same(tankFluid, remainingFluid)) { + int filled = tank.fill(remainingFluid, IFluidHandler.FluidAction.EXECUTE); + remainingFluid.shrink(filled); + } + } + return remainingFluid; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/io/FurnaceSidedFluidHandler.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/io/FurnaceSidedFluidHandler.java new file mode 100644 index 00000000..484533a3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/io/FurnaceSidedFluidHandler.java @@ -0,0 +1,93 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.io; + +import com.sorrowmist.useless.api.enums.FurnaceFace; +import com.sorrowmist.useless.api.enums.FurnaceFaceMode; +import net.minecraft.core.Direction; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.capability.IFluidHandler; +import net.minecraftforge.fluids.capability.templates.FluidTank; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.FLUID_TANK_COUNT; + +/** + * 根据面模式限制的方向感知流体处理器。 + *

+ * 仅当对应面模式激活时才允许填充/抽取操作。 + */ +public record FurnaceSidedFluidHandler(FluidTank[] inputTanks, FluidTank[] outputTanks, + Direction side, FurnaceFaceAccessor owner) implements IFluidHandler { + + @Nullable + private FurnaceFaceMode getMode() { + FurnaceFace face = FurnaceFace.fromDirection(side, owner.getFacing()); + if (face == null) return FurnaceFaceMode.DISABLED; + return owner.getFaceMode(face); + } + + @Override + public int getTanks() { + return FLUID_TANK_COUNT * 2; + } + + @Override + public @NotNull FluidStack getFluidInTank(int tank) { + if (tank < FLUID_TANK_COUNT) { + return inputTanks[tank].getFluid(); + } else if (tank < FLUID_TANK_COUNT * 2) { + return outputTanks[tank - FLUID_TANK_COUNT].getFluid(); + } + return FluidStack.EMPTY; + } + + @Override + public int getTankCapacity(int tank) { + if (tank < FLUID_TANK_COUNT) { + return inputTanks[tank].getCapacity(); + } else if (tank < FLUID_TANK_COUNT * 2) { + return outputTanks[tank - FLUID_TANK_COUNT].getCapacity(); + } + return 0; + } + + @Override + public boolean isFluidValid(int tank, @NotNull FluidStack stack) { + if (tank < FLUID_TANK_COUNT) { + return inputTanks[tank].isFluidValid(stack); + } else if (tank < FLUID_TANK_COUNT * 2) { + return outputTanks[tank - FLUID_TANK_COUNT].isFluidValid(stack); + } + return false; + } + + @Override + public int fill(@NotNull FluidStack resource, @NotNull FluidAction action) { + FurnaceFaceMode mode = getMode(); + if (mode == null || !mode.allowsMaterialInput()) return 0; + return FurnaceCombinedFluidTankHandler.fillInput(inputTanks, resource, action); + } + + @Override + public @NotNull FluidStack drain(@NotNull FluidStack resource, @NotNull FluidAction action) { + FurnaceFaceMode mode = getMode(); + if (mode == null || !mode.allowsMaterialOutput()) return FluidStack.EMPTY; + for (FluidTank tank : outputTanks) { + FluidStack drained = tank.drain(resource, action); + if (!drained.isEmpty()) return drained; + } + return FluidStack.EMPTY; + } + + @Override + public @NotNull FluidStack drain(int maxDrain, @NotNull FluidAction action) { + FurnaceFaceMode mode = getMode(); + if (mode == null || !mode.allowsMaterialOutput()) return FluidStack.EMPTY; + for (FluidTank tank : outputTanks) { + if (!tank.getFluid().isEmpty()) { + return tank.drain(maxDrain, action); + } + } + return FluidStack.EMPTY; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/io/FurnaceSidedItemHandler.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/io/FurnaceSidedItemHandler.java new file mode 100644 index 00000000..9bbc709c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/io/FurnaceSidedItemHandler.java @@ -0,0 +1,114 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.io; + +import com.sorrowmist.useless.api.enums.FurnaceFace; +import com.sorrowmist.useless.api.enums.FurnaceFaceMode; +import com.sorrowmist.useless.init.ModTags; +import net.minecraft.core.Direction; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.items.IItemHandler; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.CATALYST_SLOT; +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.INPUT_SLOTS_COUNT; +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.INPUT_SLOTS_START; +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.MOLD_SLOT; +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.OUTPUT_SLOTS_COUNT; +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.OUTPUT_SLOTS_START; + +/** + * 根据面模式限制的方向感知物品处理器。 + *

+ * 仅当对应面模式激活时才允许特定类型的插入/抽取操作。 + */ +public record FurnaceSidedItemHandler(IItemHandler baseHandler, @Nullable Direction side, + FurnaceFaceAccessor owner) implements IItemHandler { + + @Nullable + private FurnaceFaceMode getMode() { + if (side == null) return null; // 无限制 + FurnaceFace face = FurnaceFace.fromDirection(side, owner.getFacing()); + if (face == null) return FurnaceFaceMode.DISABLED; + return owner.getFaceMode(face); + } + + @Override + public int getSlots() { + return baseHandler.getSlots(); + } + + @Override + public @NotNull ItemStack getStackInSlot(int slot) { + return baseHandler.getStackInSlot(slot); + } + + @Override + public @NotNull ItemStack insertItem(int slot, @NotNull ItemStack stack, boolean simulate) { + FurnaceFaceMode mode = getMode(); + if (mode == null) { + // 无面向时普通行为 + return baseHandler.insertItem(slot, stack, simulate); + } + if (!mode.allowsAny()) return stack; // 完全禁止 + + // 催化剂优先路由 + if (stack.is(ModTags.CATALYSTS) && mode.allowsCatalystInput() && slot != CATALYST_SLOT) { + ItemStack catalystSlotStack = baseHandler.getStackInSlot(CATALYST_SLOT); + if (catalystSlotStack.isEmpty() || + (ItemStack.isSameItemSameTags(catalystSlotStack, stack) && + catalystSlotStack.getCount() < baseHandler.getSlotLimit(CATALYST_SLOT))) { + return baseHandler.insertItem(CATALYST_SLOT, stack, simulate); + } + } + + // 模具优先路由 + if (stack.is(ModTags.MOLDS) && mode.allowsMoldInput() && slot != MOLD_SLOT) { + ItemStack moldSlotStack = baseHandler.getStackInSlot(MOLD_SLOT); + if (moldSlotStack.isEmpty()) { + return baseHandler.insertItem(MOLD_SLOT, stack, simulate); + } + } + + boolean isInputSlot = slot >= INPUT_SLOTS_START && slot < INPUT_SLOTS_START + INPUT_SLOTS_COUNT; + boolean isCatalystSlot = slot == CATALYST_SLOT; + boolean isMoldSlot = slot == MOLD_SLOT; + + // 仅允许输入到对应类型的槽位 + if (isInputSlot && mode.allowsMaterialInput()) { + return baseHandler.insertItem(slot, stack, simulate); + } + if (isCatalystSlot && mode.allowsCatalystInput()) { + return baseHandler.insertItem(slot, stack, simulate); + } + if (isMoldSlot && mode.allowsMoldInput()) { + return baseHandler.insertItem(slot, stack, simulate); + } + + return stack; // 不允许插入到此槽位 + } + + @Override + public @NotNull ItemStack extractItem(int slot, int amount, boolean simulate) { + FurnaceFaceMode mode = getMode(); + if (mode == null) { + return baseHandler.extractItem(slot, amount, simulate); + } + if (!mode.allowsMaterialOutput()) return ItemStack.EMPTY; + + boolean isOutputSlot = slot >= OUTPUT_SLOTS_START && slot < OUTPUT_SLOTS_START + OUTPUT_SLOTS_COUNT; + if (isOutputSlot) { + return baseHandler.extractItem(slot, amount, simulate); + } + return ItemStack.EMPTY; + } + + @Override + public int getSlotLimit(int slot) { + return baseHandler.getSlotLimit(slot); + } + + @Override + public boolean isItemValid(int slot, @NotNull ItemStack stack) { + return baseHandler.isItemValid(slot, stack); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/layout/AdvancedAlloyFurnaceLayout.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/layout/AdvancedAlloyFurnaceLayout.java new file mode 100644 index 00000000..7766aaf5 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/layout/AdvancedAlloyFurnaceLayout.java @@ -0,0 +1,24 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout; + +/** + * 高级合金炉的槽位和流体槽布局定义。 + * 该类作为机器布局的单一数据源,避免槽位编号散落在不同模块中。 + */ +public final class AdvancedAlloyFurnaceLayout { + public static final int INPUT_SLOTS_START = 0; + public static final int INPUT_SLOTS_COUNT = 9; + public static final int OUTPUT_SLOTS_START = 9; + public static final int OUTPUT_SLOTS_COUNT = 9; + public static final int CATALYST_SLOT = 18; + public static final int MOLD_SLOT = 19; + public static final int PATTERN_SLOTS_START = 20; + public static final int PATTERN_SLOTS_COUNT = 108; + public static final int PATTERN_SLOTS_END = PATTERN_SLOTS_START + PATTERN_SLOTS_COUNT - 1; + public static final int TOTAL_SLOTS = 128; + public static final int FLUID_TANK_COUNT = 6; + public static final int CHEMICAL_TANK_COUNT = 3; + public static final int CHEMICAL_TANK_START = FLUID_TANK_COUNT; + + private AdvancedAlloyFurnaceLayout() { + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/parallel/AlloyFurnaceParallelCalculator.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/parallel/AlloyFurnaceParallelCalculator.java new file mode 100644 index 00000000..4d415117 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/parallel/AlloyFurnaceParallelCalculator.java @@ -0,0 +1,51 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.parallel; + +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.catalyst.ResolvedCatalystEffect; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; + +import java.math.BigInteger; + +/** Calculates parallel limits without requiring a full recipe energy payment in the buffer. */ +public final class AlloyFurnaceParallelCalculator { + private AlloyFurnaceParallelCalculator() { + } + + /** Limits parallelism only when its exact long energy target would overflow. */ + public static long calculateEnergyParallel( + AdvancedAlloyFurnaceRecipe recipe, ResolvedCatalystEffect resolvedCatalystEffect) { + if (!resolvedCatalystEffect.energyMultipliesWithParallel()) { + return Long.MAX_VALUE; + } + long recipeEnergy = recipe.energy(); + if (recipeEnergy <= 0L) { + return Long.MAX_VALUE; + } + BigInteger maximum = BigInteger.valueOf(Long.MAX_VALUE) + .multiply(BigInteger.valueOf(Math.max(1, resolvedCatalystEffect.energyDivisor()))) + .divide(BigInteger.valueOf(recipeEnergy)); + return maximum.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0 + ? Long.MAX_VALUE + : Math.max(1L, maximum.longValue()); + } + + public static int calculateStartableParallel( + long energyParallel, long catalystParallel, int materialParallel, int outputParallel) { + long limitedCatalystParallel = catalystParallel == Long.MAX_VALUE ? energyParallel : catalystParallel; + long parallel = Math.min( + Math.min(energyParallel, limitedCatalystParallel), + (long) Math.min(materialParallel, outputParallel)); + return (int) Math.max(0L, Math.min(Integer.MAX_VALUE, parallel)); + } + + public static int calculateCompletionTargetParallel( + int initialParallel, long catalystParallel, int materialParallel, int outputParallel) { + long parallel = Math.min((long) Math.min(materialParallel, outputParallel), catalystParallel); + return (int) Math.max(initialParallel, Math.min(Integer.MAX_VALUE, parallel)); + } + + public static long calculateAeTaskParallel( + AdvancedAlloyFurnaceRecipe recipe, ResolvedCatalystEffect resolvedCatalystEffect) { + long catalystParallel = Math.max(1L, resolvedCatalystEffect.recipeParallel()); + return Math.min(catalystParallel, calculateEnergyParallel(recipe, resolvedCatalystEffect)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/recipe/AlloyFurnaceRecipeCalculator.java b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/recipe/AlloyFurnaceRecipeCalculator.java new file mode 100644 index 00000000..596bc3ae --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/machines/advanced_alloy_furnace/recipe/AlloyFurnaceRecipeCalculator.java @@ -0,0 +1,316 @@ +package com.sorrowmist.useless.content.machines.advanced_alloy_furnace.recipe; + +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.catalyst.CatalystEffectResolver; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.catalyst.ResolvedCatalystEffect; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.io.FurnaceInputPort; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalKeyProvider; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalKeyProviders; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.FurnaceChemicalStorage; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.parallel.AlloyFurnaceParallelCalculator; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.core.config.AlloyFurnaceTierRules; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.capability.templates.FluidTank; +import net.minecraftforge.items.ItemStackHandler; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.CATALYST_SLOT; +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.FLUID_TANK_COUNT; +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.INPUT_SLOTS_COUNT; +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.INPUT_SLOTS_START; +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.MOLD_SLOT; +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.OUTPUT_SLOTS_COUNT; +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.OUTPUT_SLOTS_START; + +/** + * 高级合金炉配方匹配与并行数计算器。 + *

+ * 承担纯粹的只读计算职责:配方匹配、可处理性校验、并行数计算。 + * 不持有任何机器运行状态(进度、缓存等),这些仍由方块实体管理。 + */ +public final class AlloyFurnaceRecipeCalculator { + + private final ItemStackHandler itemHandler; + private final FluidTank[] inputFluidTanks; + private final FluidTank[] outputFluidTanks; + private final FurnaceChemicalStorage inputChemicalStorage; + private final FurnaceChemicalStorage outputChemicalStorage; + private final ChemicalKeyProvider chemicalKeyProvider; + public AlloyFurnaceRecipeCalculator(ItemStackHandler itemHandler, FluidTank[] inputFluidTanks, + FluidTank[] outputFluidTanks) { + this(itemHandler, inputFluidTanks, outputFluidTanks, + FurnaceChemicalStorage.DISABLED, FurnaceChemicalStorage.DISABLED, + ChemicalKeyProvider.NONE); + } + + public AlloyFurnaceRecipeCalculator(ItemStackHandler itemHandler, FluidTank[] inputFluidTanks, + FluidTank[] outputFluidTanks, + FurnaceChemicalStorage inputChemicalStorage, + FurnaceChemicalStorage outputChemicalStorage, + ChemicalKeyProvider chemicalKeyProvider) { + this.itemHandler = itemHandler; + this.inputFluidTanks = inputFluidTanks; + this.outputFluidTanks = outputFluidTanks; + this.inputChemicalStorage = inputChemicalStorage == null ? FurnaceChemicalStorage.DISABLED : inputChemicalStorage; + this.outputChemicalStorage = outputChemicalStorage == null ? FurnaceChemicalStorage.DISABLED : outputChemicalStorage; + this.chemicalKeyProvider = chemicalKeyProvider == null ? ChemicalKeyProvider.NONE : chemicalKeyProvider; + } + + /** + * 查找匹配的配方(统一匹配,支持物品+流体+模具优先级)。 + *

+ * 每次都通过统一管理器取得当前最具体配方;管理器的不可变输入键负责缓存, + * 避免复用旧配方导致后来变得可用的更具体配方长期饥饿。 + * + * @param level 世界 + * @return 匹配的配方,如果没有则返回空 + */ + public Optional findMatchingRecipe(@Nullable Level level) { + return findMatchingRecipe(level, AlloyFurnaceTierRules.NO_MACHINE_TIER); + } + + public Optional findMatchingRecipe( + @Nullable Level level, int machineTier) { + if (level == null) return Optional.empty(); + + // 构建输入列表(用于 AlloyFurnaceRecipeManager 查找) + List currentInputs = new ArrayList<>(); + for (int i = INPUT_SLOTS_START; i < INPUT_SLOTS_START + INPUT_SLOTS_COUNT; i++) { + ItemStack stack = this.itemHandler.getStackInSlot(i); + if (!stack.isEmpty()) { + currentInputs.add(stack); + } + } + + List currentFluids = new ArrayList<>(); + for (int i = 0; i < FLUID_TANK_COUNT; i++) { + FluidStack fluid = this.inputFluidTanks[i].getFluid(); + if (!fluid.isEmpty()) { + currentFluids.add(fluid.copy()); + } + } + + List currentChemicals = new ArrayList<>(); + ChemicalKeyProvider provider = currentChemicalKeyProvider(); + for (int i = 0; i < this.inputChemicalStorage.size(); i++) { + var generic = provider.toGenericStack(this.inputChemicalStorage.getStackInSlot(i)); + if (generic != null && generic.amount() > 0L) currentChemicals.add(generic); + } + + if (currentInputs.isEmpty() && currentFluids.isEmpty() && currentChemicals.isEmpty()) return Optional.empty(); + + ItemStack moldStack = this.itemHandler.getStackInSlot(MOLD_SLOT); + + AdvancedAlloyFurnaceRecipe bestRecipe = AlloyFurnaceRecipeManager.getInstance().findRecipe( + level, currentInputs, currentFluids, currentChemicals, moldStack, machineTier + ); + + if (bestRecipe != null && canProcessRecipe(bestRecipe, machineTier)) { + return Optional.of(bestRecipe); + } + + return Optional.empty(); + } + + /** + * 检查配方是否可处理(直接遍历slot,不构建中间列表)。 + *

+ * 模具检查提前,便于快速失败。 + */ + public boolean canProcessRecipe(AdvancedAlloyFurnaceRecipe recipe) { + return this.canProcessRecipe(recipe, AlloyFurnaceTierRules.NO_MACHINE_TIER); + } + + public boolean canProcessRecipe(AdvancedAlloyFurnaceRecipe recipe, int machineTier) { + return this.canConsumeRecipeInputs(recipe, 1, machineTier); + } + + /** + * 检查是否有足够的输入材料支持指定的并行数。 + * + * @param recipe 配方 + * @param parallel 并行数 + * @return 如果有足够的材料返回true + */ + public boolean canConsumeRecipeInputs(AdvancedAlloyFurnaceRecipe recipe, int parallel) { + return canConsumeRecipeInputs(recipe, parallel, AlloyFurnaceTierRules.NO_MACHINE_TIER); + } + + public boolean canConsumeRecipeInputs( + AdvancedAlloyFurnaceRecipe recipe, int parallel, int machineTier) { + if (recipe == null || parallel <= 0) return false; + if (!AlloyFurnaceTierRules.allows(recipe.id(), recipe.tier(), machineTier)) return false; + if (recipe.molds().size() > 1) return false; + if (!recipe.mold().isEmpty()) { + ItemStack moldStack = this.itemHandler.getStackInSlot(MOLD_SLOT); + if (!AdapterUtils.matchesMold(recipe.mold(), moldStack)) return false; + } + return FurnaceInputPort.canConsumeRecipeInputs( + recipe, parallel, this.itemHandler, INPUT_SLOTS_START, INPUT_SLOTS_COUNT, + this.inputFluidTanks, FLUID_TANK_COUNT, + this.inputChemicalStorage, currentChemicalKeyProvider()); + } + + /** + * 检查是否有足够的输入材料支持至少一次配方 + * (用于开始新配方前的检查)。 + */ + public boolean canConsumeRecipeInputs(AdvancedAlloyFurnaceRecipe recipe) { + return canConsumeRecipeInputs(recipe, 1, AlloyFurnaceTierRules.NO_MACHINE_TIER); + } + + /** + * 计算实际可用的并行数。 + * 按照以下顺序计算,避免数据溢出: + * 1. 通过配方及能量上限,计算当前配方理论允许的最大并行 + * 2. 通过催化剂获取当前催化剂允许的并行量 + * 3. 通过输入物品,匹配配方实际能运行的并行量 + * 4. 通过输出空间,计算能容纳的并行量 + *

+ * 所有计算都遵循"先除再乘"原则,避免溢出。 + */ + public int calculateActualParallel(AdvancedAlloyFurnaceRecipe recipe) { + ItemStack catalystStack = this.itemHandler.getStackInSlot(CATALYST_SLOT); + ResolvedCatalystEffect resolvedCatalystEffect = CatalystEffectResolver.resolve(recipe, catalystStack, recipe.processTime()); + long energyParallel = AlloyFurnaceParallelCalculator.calculateEnergyParallel(recipe, resolvedCatalystEffect); + long catalystParallel = resolvedCatalystEffect.recipeParallel(); + int materialParallel = this.calculateMaterialParallel(recipe); + int outputParallel = this.calculateOutputParallel(recipe); + return AlloyFurnaceParallelCalculator.calculateStartableParallel(energyParallel, catalystParallel, materialParallel, outputParallel); + } + + /** + * 计算催化剂允许的并行数。 + * 优先使用传入的缓存催化剂效果,避免重复解析。 + * + * @param recipe 配方 + * @param cachedCatalystEffect 已缓存的催化剂效果(可为 null) + */ + public int calculateCatalystParallel(AdvancedAlloyFurnaceRecipe recipe, + @Nullable ResolvedCatalystEffect cachedCatalystEffect) { + if (cachedCatalystEffect != null) { + return clampParallelToInt(cachedCatalystEffect.recipeParallel()); + } + ItemStack catalystStack = this.itemHandler.getStackInSlot(CATALYST_SLOT); + return clampParallelToInt( + CatalystEffectResolver.resolve(recipe, catalystStack, recipe.processTime()).recipeParallel()); + } + + private static int clampParallelToInt(long parallel) { + return (int) Math.max(0L, Math.min(Integer.MAX_VALUE, parallel)); + } + + /** + * 计算输入材料允许的并行数。 + * 对于每种材料: 可用数量 / 配方需求数量 = 该材料允许的并行数,取所有材料的最小值。 + */ + public int calculateMaterialParallel(AdvancedAlloyFurnaceRecipe recipe) { + return FurnaceInputPort.calculateMaterialParallel( + recipe, this.itemHandler, INPUT_SLOTS_START, INPUT_SLOTS_COUNT, + this.inputFluidTanks, FLUID_TANK_COUNT, + this.inputChemicalStorage, currentChemicalKeyProvider()); + } + + /** + * 计算输出空间允许的并行数。 + * 对于每种输出: 可用空间 / 单次产出数量 = 该输出允许的并行数,取所有输出的最小值。 + */ + public int calculateOutputParallel(AdvancedAlloyFurnaceRecipe recipe) { + int maxParallel = Integer.MAX_VALUE; + + // 计算物品输出空间限制 + for (ItemStack output : recipe.outputs()) { + long totalSpace = 0; + int outputCount = output.getCount(); + + if (outputCount <= 0) continue; + + for (int i = OUTPUT_SLOTS_START; i < OUTPUT_SLOTS_START + OUTPUT_SLOTS_COUNT; i++) { + ItemStack slotStack = this.itemHandler.getStackInSlot(i); + int slotLimit = this.itemHandler.getSlotLimit(i); + + if (slotStack.isEmpty()) { + totalSpace += slotLimit; + } else if (ItemStack.isSameItemSameTags(slotStack, output)) { + totalSpace += (long) slotLimit - slotStack.getCount(); + } + } + + // 先除: 可用空间 / 单次产出数量 = 该输出允许的并行数 + long parallelLong = totalSpace / outputCount; + int possibleParallel = parallelLong > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) parallelLong; + maxParallel = Math.min(maxParallel, possibleParallel); + + if (maxParallel <= 0) return 0; + } + + // 计算流体输出空间限制 + for (FluidStack outputFluid : recipe.outputFluids()) { + long totalSpace = 0; + int fluidAmount = outputFluid.getAmount(); + + if (fluidAmount <= 0) continue; + + for (int i = 0; i < FLUID_TANK_COUNT; i++) { + FluidStack tankFluid = this.outputFluidTanks[i].getFluid(); + int tankCapacity = this.outputFluidTanks[i].getCapacity(); + + if (tankFluid.isEmpty()) { + totalSpace += tankCapacity; + } else if (com.sorrowmist.useless.compat.forge1201.fluid.FluidStacks.same(tankFluid, outputFluid)) { + totalSpace += (long) tankCapacity - tankFluid.getAmount(); + } + } + + // 先除: 可用空间 / 单次产出数量 = 该流体允许的并行数 + long parallelLong = totalSpace / fluidAmount; + int possibleParallel = parallelLong > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) parallelLong; + maxParallel = Math.min(maxParallel, possibleParallel); + + if (maxParallel <= 0) return 0; + } + + ChemicalKeyProvider provider = currentChemicalKeyProvider(); + for (var outputChemical : recipe.keyOutputs()) { + if (outputChemical == null || outputChemical.what() == null || outputChemical.amount() <= 0L + || !provider.isChemicalKey(outputChemical.what())) { + continue; + } + var view = provider.fromGenericStack(outputChemical); + if (view == null) return 0; + long totalSpace = 0L; + for (int i = 0; i < this.outputChemicalStorage.size(); i++) { + var stored = this.outputChemicalStorage.getStackInSlot(i); + if (stored.isEmpty()) { + totalSpace = saturatingAdd(totalSpace, this.outputChemicalStorage.capacity(i)); + } else if (stored.isSameType(view)) { + long free = this.outputChemicalStorage.capacity(i) - stored.amount(); + if (free > 0L) totalSpace = saturatingAdd(totalSpace, free); + } + } + long possible = totalSpace / outputChemical.amount(); + maxParallel = Math.min(maxParallel, + possible > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) possible); + if (maxParallel <= 0) return 0; + } + + return maxParallel; + } + + private ChemicalKeyProvider currentChemicalKeyProvider() { + ChemicalKeyProvider registered = ChemicalKeyProviders.get(); + return registered == ChemicalKeyProvider.NONE ? this.chemicalKeyProvider : registered; + } + + private static long saturatingAdd(long left, long right) { + return right > 0L && left > Long.MAX_VALUE - right ? Long.MAX_VALUE : left + right; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/menus/AdvancedAlloyFurnaceMenu.java b/src/main/java/com/sorrowmist/useless/content/menus/AdvancedAlloyFurnaceMenu.java new file mode 100644 index 00000000..acdb54b1 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/menus/AdvancedAlloyFurnaceMenu.java @@ -0,0 +1,443 @@ +package com.sorrowmist.useless.content.menus; + +import appeng.client.gui.Icon; +import appeng.menu.AEBaseMenu; +import appeng.menu.SlotSemantics; +import com.sorrowmist.useless.content.blockentities.AdvancedAlloyFurnaceBlockEntity; +import com.sorrowmist.useless.content.blockentities.AdvancedAlloyFurnaceData; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.AdvancedAlloyFurnaceAeManager; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalStackView; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout; +import com.sorrowmist.useless.init.ModMenuType; +import com.sorrowmist.useless.inventory.slot.PatternSlotItemHandler; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.SimpleContainerData; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraftforge.fluids.capability.templates.FluidTank; +import net.minecraftforge.items.IItemHandler; +import net.minecraftforge.items.ItemStackHandler; +import net.minecraftforge.items.SlotItemHandler; +import org.jetbrains.annotations.NotNull; + +/** + * 支持高堆叠数量的槽位处理器 + * 允许槽位堆叠数量达到 Integer.MAX_VALUE(约21亿) + */ +class HighStackSlotItemHandler extends SlotItemHandler { + + public HighStackSlotItemHandler(IItemHandler itemHandler, int index, int xPosition, int yPosition) { + super(itemHandler, index, xPosition, yPosition); + } + + @Override + public int getMaxStackSize() { + return Integer.MAX_VALUE; + } + + @Override + public int getMaxStackSize(@NotNull ItemStack stack) { + return Integer.MAX_VALUE; + } +} + +public class AdvancedAlloyFurnaceMenu extends AEBaseMenu { + + // 物品输入槽:起点75,16,横向排布9个,每个16*16,横向间隔2像素 + private static final int INPUT_SLOTS_X = 75; + private static final int INPUT_SLOTS_Y = 16; + private static final int SLOT_SIZE = 16; + private static final int SLOT_SPACING = 2; + + // 物品输出槽:起点75,93,横向排布9个,每个16*16,横向间隔2像素 + private static final int OUTPUT_SLOTS_X = 75; + private static final int OUTPUT_SLOTS_Y = 93; + + // 催化剂槽:起点78,154,16*16 + private static final int CATALYST_SLOT_X = 78; + private static final int CATALYST_SLOT_Y = 154; + + // 模具槽:起点128,154,16*16 + private static final int MOLD_SLOT_X = 128; + private static final int MOLD_SLOT_Y = 154; + + // 样板槽:起点8,5,每个16*16,横向3个纵向9个,间隔2像素 + private static final int PATTERN_SLOTS_X = 8; + private static final int PATTERN_SLOTS_Y = 5; + private static final int PATTERN_SLOTS_COLS = 3; + private static final int PATTERN_SLOTS_ROWS = 9; + + // 玩家背包位置:起点75,178 + private static final int PLAYER_INVENTORY_X = 75; + private static final int PLAYER_INVENTORY_Y = 178; + private static final int PLAYER_HOTBAR_Y = 236; + + // 样板每页27个(3x9) + private static final int PATTERN_SLOTS_PER_PAGE = 27; + private int patternPage = 0; + + private static final int MACHINE_INPUT_START = AdvancedAlloyFurnaceLayout.INPUT_SLOTS_START; + private static final int MACHINE_INPUT_END = MACHINE_INPUT_START + AdvancedAlloyFurnaceLayout.INPUT_SLOTS_COUNT - 1; + private static final int MACHINE_OUTPUT_START = AdvancedAlloyFurnaceLayout.OUTPUT_SLOTS_START; + private static final int MACHINE_OUTPUT_END = MACHINE_OUTPUT_START + AdvancedAlloyFurnaceLayout.OUTPUT_SLOTS_COUNT - 1; + private static final int CATALYST_SLOT = AdvancedAlloyFurnaceLayout.CATALYST_SLOT; + private static final int MOLD_SLOT = AdvancedAlloyFurnaceLayout.MOLD_SLOT; + private static final int PATTERN_SLOTS_START = AdvancedAlloyFurnaceLayout.PATTERN_SLOTS_START; + private static final int PATTERN_SLOTS_END = AdvancedAlloyFurnaceLayout.PATTERN_SLOTS_END; + private static final int PLAYER_INVENTORY_START = AdvancedAlloyFurnaceLayout.TOTAL_SLOTS; + private static final int PLAYER_INVENTORY_END = PLAYER_INVENTORY_START + 26; + private static final int HOTBAR_START = PLAYER_INVENTORY_END + 1; + private static final int HOTBAR_END = HOTBAR_START + 8; + + private final AdvancedAlloyFurnaceBlockEntity blockEntity; + private final ContainerData data; + + public AdvancedAlloyFurnaceMenu(int containerId, Inventory inv, FriendlyByteBuf buf) { + this(containerId, inv, buf != null ? buf.readBlockPos() : BlockPos.ZERO); + } + + private AdvancedAlloyFurnaceMenu(int containerId, Inventory inv, BlockPos pos) { + this(containerId, inv, inv.player.level().getBlockEntity(pos)); + } + + private AdvancedAlloyFurnaceMenu(int containerId, Inventory inv, BlockEntity entity) { + this(containerId, inv, (AdvancedAlloyFurnaceBlockEntity) entity, getContainerData(entity)); + } + + public AdvancedAlloyFurnaceMenu(int containerId, Inventory inv, AdvancedAlloyFurnaceBlockEntity entity) { + this(containerId, inv, entity, entity.getData()); + } + + public AdvancedAlloyFurnaceMenu(int containerId, Inventory inv, AdvancedAlloyFurnaceBlockEntity entity, + ContainerData data) { + super(ModMenuType.ADVANCED_ALLOY_FURNACE_MENU.get(), containerId, inv, entity); + this.data = data; + this.blockEntity = entity; + this.addDataSlots(data); + + IItemHandler itemHandler = entity != null ? entity.getItemHandler() : new ItemStackHandler(AdvancedAlloyFurnaceLayout.TOTAL_SLOTS); + this.addMachineSlots(itemHandler); + + this.createPlayerInventorySlots(inv, PLAYER_INVENTORY_X, PLAYER_INVENTORY_Y, PLAYER_HOTBAR_Y); + } + + /** + * 添加机器槽位(输入、输出、催化剂、模具) + * + * @param itemHandler 物品处理器 + */ + private void addMachineSlots(IItemHandler itemHandler) { + // 添加9个输入槽位(横向排布) + for (int col = 0; col < 9; col++) { + int slotIndex = col; + int x = INPUT_SLOTS_X + col * (SLOT_SIZE + SLOT_SPACING); + int y = INPUT_SLOTS_Y; + this.addSlot(new HighStackSlotItemHandler(itemHandler, slotIndex, x, y), + SlotSemantics.MACHINE_INPUT); + } + + // 添加9个输出槽位(横向排布),不允许放入物品 + for (int col = 0; col < 9; col++) { + int slotIndex = 9 + col; + int x = OUTPUT_SLOTS_X + col * (SLOT_SIZE + SLOT_SPACING); + int y = OUTPUT_SLOTS_Y; + this.addSlot(new HighStackSlotItemHandler(itemHandler, slotIndex, x, y) { + @Override + public boolean mayPlace(@NotNull ItemStack stack) { + return false; + } + }, SlotSemantics.MACHINE_OUTPUT); + } + + // 添加催化剂槽位 + this.addSlot(new HighStackSlotItemHandler(itemHandler, CATALYST_SLOT, CATALYST_SLOT_X, CATALYST_SLOT_Y), + SlotSemantics.CONFIG); + + // 添加模具槽位 + this.addSlot(new HighStackSlotItemHandler(itemHandler, MOLD_SLOT, MOLD_SLOT_X, MOLD_SLOT_Y) { + @Override + public int getMaxStackSize() { + return 1; + } + }, SlotSemantics.CONFIG); + + // 添加108个样板槽位(4页 × 3x9),使用自定义的PatternSlotItemHandler实现背景图案 + for (int page = 0; page < 4; page++) { + for (int row = 0; row < PATTERN_SLOTS_ROWS; row++) { + for (int col = 0; col < PATTERN_SLOTS_COLS; col++) { + int slotIndex = PATTERN_SLOTS_START + page * PATTERN_SLOTS_PER_PAGE + row * PATTERN_SLOTS_COLS + col; + int x = PATTERN_SLOTS_X + col * (SLOT_SIZE + SLOT_SPACING); + int y = PATTERN_SLOTS_Y + row * (SLOT_SIZE + SLOT_SPACING); + PatternSlotItemHandler patternSlot = new PatternSlotItemHandler(itemHandler, slotIndex, x, y); + patternSlot.setIcon(Icon.BACKGROUND_BLANK_PATTERN); + patternSlot.setActive(page == 0); + this.addSlot(patternSlot, SlotSemantics.ENCODED_PATTERN); + } + } + } + } + + /** + * 获取容器数据 + * + * @param entity 方块实体 + * @return 容器数据 + */ + private static ContainerData getContainerData(BlockEntity entity) { + if (entity instanceof AdvancedAlloyFurnaceBlockEntity furnace) { + return furnace.getData(); + } + return new SimpleContainerData(AdvancedAlloyFurnaceData.DATA_COUNT); + } + + @Override + public @NotNull ItemStack quickMoveStack(@NotNull Player player, int index) { + if (this.blockEntity == null) { + return ItemStack.EMPTY; + } + + ItemStack itemstack = ItemStack.EMPTY; + Slot slot = this.slots.get(index); + + if (!slot.hasItem()) { + return itemstack; + } + + ItemStack stackInSlot = slot.getItem(); + itemstack = stackInSlot.copy(); + + if (index >= MACHINE_INPUT_START && index <= PATTERN_SLOTS_END) { + if (!this.moveItemStackTo(stackInSlot, PLAYER_INVENTORY_START, HOTBAR_END + 1, true)) { + return ItemStack.EMPTY; + } + slot.onQuickCraft(stackInSlot, itemstack); + } else if (index >= PLAYER_INVENTORY_START && index <= HOTBAR_END) { + if (!this.tryMoveToMachine(stackInSlot)) { + if (!this.moveWithinPlayerInventory(stackInSlot, index)) { + return ItemStack.EMPTY; + } + } + } + + if (stackInSlot.isEmpty()) { + slot.set(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + + if (stackInSlot.getCount() == itemstack.getCount()) { + return ItemStack.EMPTY; + } + + slot.onTake(player, stackInSlot); + return itemstack; + } + + /** + * 尝试将物品移动到机器槽位 + *

+ * 优先顺序:样板槽 -> 输入槽 -> 催化剂槽 -> 模具槽 + * 样板槽优先,且只能放入样板 + * + * @param stack 物品堆 + * @return 是否成功移动 + */ + private boolean tryMoveToMachine(ItemStack stack) { + // 优先检查是否是样板,如果是则优先放入样板槽 + if (appeng.api.crafting.PatternDetailsHelper.isEncodedPattern(stack)) { + if (this.moveItemStackTo(stack, PATTERN_SLOTS_START, PATTERN_SLOTS_END + 1, false)) { + return true; + } + } + if (this.moveItemStackTo(stack, MACHINE_INPUT_START, MACHINE_INPUT_END + 1, false)) { + return true; + } + if (this.moveItemStackTo(stack, CATALYST_SLOT, CATALYST_SLOT + 1, false)) { + return true; + } + if (this.moveItemStackTo(stack, MOLD_SLOT, MOLD_SLOT + 1, false)) { + return true; + } + // 如果不是样板,也尝试放入样板槽(但样板槽的mayPlace会阻止非样板放入) + return this.moveItemStackTo(stack, PATTERN_SLOTS_START, PATTERN_SLOTS_END + 1, false); + } + + /** + * 在玩家背包内部移动物品 + *

+ * 在背包和快捷栏之间切换 + * + * @param stack 物品堆 + * @param index 当前槽位索引 + * @return 是否成功移动 + */ + private boolean moveWithinPlayerInventory(ItemStack stack, int index) { + if (index >= PLAYER_INVENTORY_START && index <= PLAYER_INVENTORY_END) { + return this.moveItemStackTo(stack, HOTBAR_START, HOTBAR_END + 1, false); + } else if (index >= HOTBAR_START && index <= HOTBAR_END) { + return this.moveItemStackTo(stack, PLAYER_INVENTORY_START, PLAYER_INVENTORY_END + 1, false); + } + return false; + } + + @Override + public boolean stillValid(@NotNull Player player) { + return true; + } + + public AdvancedAlloyFurnaceBlockEntity getBlockEntity() { + return this.blockEntity; + } + + public long getEnergy() { + return AdvancedAlloyFurnaceData.joinBits( + this.data.get(AdvancedAlloyFurnaceData.DATA_ENERGY_STORED_LOW), + this.data.get(AdvancedAlloyFurnaceData.DATA_ENERGY_STORED_HIGH)); + } + + public long getMaxEnergy() { + return AdvancedAlloyFurnaceData.joinBits( + this.data.get(AdvancedAlloyFurnaceData.DATA_ENERGY_CAPACITY_LOW), + this.data.get(AdvancedAlloyFurnaceData.DATA_ENERGY_CAPACITY_HIGH)); + } + + public int getProgress() { + return this.data.get(AdvancedAlloyFurnaceData.DATA_PROGRESS); + } + + public int getMaxProgress() { + return this.data.get(AdvancedAlloyFurnaceData.DATA_MAX_PROGRESS); + } + + public int getCurrentParallel() { + return this.data.get(AdvancedAlloyFurnaceData.DATA_CURRENT_PARALLEL); + } + + public int getCatalystMaxParallel() { + return this.blockEntity != null ? this.blockEntity.getCatalystMaxParallel() : 1; + } + + public boolean hasMold() { + return this.data.get(AdvancedAlloyFurnaceData.DATA_HAS_MOLD) > 0; + } + + public int getFurnaceTier() { + return this.data.get(AdvancedAlloyFurnaceData.DATA_FURNACE_TIER); + } + + public boolean isReturnOutputToAe() { + return this.data.get(AdvancedAlloyFurnaceData.DATA_RETURN_OUTPUT_TO_AE) > 0; + } + + public FluidTank getInputFluidTank(int index) { + return this.blockEntity != null ? this.blockEntity.getInputFluidTank(index) : new FluidTank(0); + } + + public FluidTank getOutputFluidTank(int index) { + return this.blockEntity != null ? this.blockEntity.getOutputFluidTank(index) : new FluidTank(0); + } + + public ChemicalStackView getInputChemical(int index) { + return this.blockEntity != null ? this.blockEntity.getInputChemical(index) : ChemicalStackView.EMPTY; + } + + public ChemicalStackView getOutputChemical(int index) { + return this.blockEntity != null ? this.blockEntity.getOutputChemical(index) : ChemicalStackView.EMPTY; + } + + public long getChemicalTankCapacity() { + return this.blockEntity != null ? this.blockEntity.getChemicalTankCapacity() : 0L; + } + + public boolean hasChemicalSupport() { + return this.blockEntity != null && this.blockEntity.hasChemicalSupport(); + } + + // AE网络合成任务状态 + public int getActiveAETaskCount() { + return this.data.get(AdvancedAlloyFurnaceData.DATA_AE_ACTIVE_TASKS); + } + + public int getTotalAEProgress() { + return this.data.get(AdvancedAlloyFurnaceData.DATA_AE_TOTAL_PROGRESS); + } + + public int getTotalAEMaxProgress() { + return this.data.get(AdvancedAlloyFurnaceData.DATA_AE_TOTAL_MAX_PROGRESS); + } + + // 获取所有AE任务进度信息 + public java.util.Collection getAETaskProgressList() { + if (this.blockEntity != null) { + return this.blockEntity.getAETaskProgressList(); + } + return java.util.Collections.emptyList(); + } + + private void createPlayerInventorySlots(Inventory playerInventory, int inventoryX, int inventoryY, int hotbarY) { + for (int row = 0; row < 3; row++) { + for (int col = 0; col < 9; col++) { + int slotIndex = row * 9 + col; + int x = inventoryX + col * (SLOT_SIZE + SLOT_SPACING); + int y = inventoryY + row * (SLOT_SIZE + SLOT_SPACING); + this.addSlot(new Slot(playerInventory, slotIndex + 9, x, y), SlotSemantics.PLAYER_INVENTORY); + } + } + + for (int col = 0; col < 9; col++) { + int x = inventoryX + col * (SLOT_SIZE + SLOT_SPACING); + this.addSlot(new Slot(playerInventory, col, x, hotbarY), SlotSemantics.PLAYER_HOTBAR); + } + } + + public int getPatternPage() { + return this.patternPage; + } + + public int getMaxPatternPage() { + return (int) Math.ceil((double) AdvancedAlloyFurnaceLayout.PATTERN_SLOTS_COUNT / PATTERN_SLOTS_PER_PAGE) - 1; + } + + public void setPatternPage(int page) { + this.patternPage = Math.max(0, Math.min(page, this.getMaxPatternPage())); + this.updatePatternSlotActivity(); + } + + public void nextPatternPage() { + if (this.patternPage < this.getMaxPatternPage()) { + this.patternPage++; + this.updatePatternSlotActivity(); + } + } + + public void prevPatternPage() { + if (this.patternPage > 0) { + this.patternPage--; + this.updatePatternSlotActivity(); + } + } + + private void updatePatternSlotActivity() { + int currentPage = this.patternPage; + int slotsPerPage = PATTERN_SLOTS_PER_PAGE; + int base = currentPage * slotsPerPage; + int end = Math.min(base + slotsPerPage, AdvancedAlloyFurnaceLayout.PATTERN_SLOTS_COUNT); + + for (Slot slot : this.slots) { + if (slot instanceof com.sorrowmist.useless.inventory.slot.PatternSlotItemHandler patternSlot) { + int slotIndex = slot.getSlotIndex(); + int relativeIndex = slotIndex - AdvancedAlloyFurnaceLayout.PATTERN_SLOTS_START; + patternSlot.setActive(relativeIndex >= base && relativeIndex < end); + } + } + } + + public int getPatternSlotsPerPage() { + return PATTERN_SLOTS_PER_PAGE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/menus/ChainGroupMenu.java b/src/main/java/com/sorrowmist/useless/content/menus/ChainGroupMenu.java new file mode 100644 index 00000000..ec23a980 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/menus/ChainGroupMenu.java @@ -0,0 +1,80 @@ +package com.sorrowmist.useless.content.menus; + +import com.sorrowmist.useless.init.ModMenuType; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; + +/** + * 连锁等价组界面的容器。 + * + *

这个菜单不承载任何数据:等价组存在玩家的 {@code BeefToolLayout} 里, + * 编辑通过 {@code BeefToolLayoutUpdatePacket} 走服务端权威的校验与落库, + * 菜单只在客户端本地 new 出来,从不经 {@code player.openMenu} 下发。

+ * + *

它存在的唯一理由,是让界面能继承 + * {@link net.minecraft.client.gui.screens.inventory.AbstractContainerScreen}: + * JEI 与 EMI 的原料侧栏默认只画在容器界面旁边(JEI 内置的 handler 只覆盖 + * {@code AbstractContainerScreen}),普通 {@code Screen} 拿不到侧栏,也就没法拖拽。

+ * + *

但「是容器屏」对 EMI 还不够。EMI 的 + * {@code dev.emi.emi.screen.EmiScreenBase#of(Screen)} 在 + * {@code instanceof AbstractContainerScreen} 分支里还有一句 + * {@code if (menu.slots.isEmpty()) return EMPTY;} —— 返回 {@code EMPTY} 会让 + * {@code EmiScreenManager#recalculate()} 首句 {@code if (base.isEmpty()) return;} 直接退出, + * 侧栏的 {@code ScreenSpace} 一个都不建,EMI 侧栏整块不显示(JEI 不看槽位,所以不受影响)。 + * 因此这里塞了一个屏幕外的惰性槽位把 {@code slots} 撑成非空:坐标在屏幕外, + * {@code getSlotUnderMouse} 的 {@code isHovering} 永远不命中, + * {@code AbstractContainerScreen#renderSlots} 又会被 {@code isActive()} 挡掉, + * 所以它既不显示也不可交互,纯粹是给 EMI 看的「这是一个真容器」的标记。

+ */ +public final class ChainGroupMenu extends AbstractContainerMenu { + + /** + * 惰性占位槽的坐标。放到任何 GUI 缩放都够不到的地方:面板最宽也就几百像素, + * 而 {@code getSlotUnderMouse} 用 {@code isHovering(slot.x, slot.y, 16, 16, ...)} 判定, + * 屏幕外的坐标永远不命中,所以这个槽位不可能被点到、也不可能收到拖拽。 + */ + private static final int OFFSCREEN_SLOT_X = -10000; + private static final int OFFSCREEN_SLOT_Y = -10000; + + public ChainGroupMenu(int containerId, Inventory inventory) { + super(ModMenuType.CHAIN_GROUP_MENU.get(), containerId); + // 见类注释:EMI 要求 slots 非空才给侧栏。这个槽位不渲染、不可交互, + // 也不参与任何数据流(quickMoveStack 恒返回空)。 + addSlot(new Slot(inventory, 0, OFFSCREEN_SLOT_X, OFFSCREEN_SLOT_Y) { + @Override + public boolean isActive() { + return false; + } + + @Override + public boolean mayPlace(ItemStack stack) { + return false; + } + + @Override + public boolean mayPickup(Player player) { + return false; + } + }); + } + + /** {@code IForgeMenuType.create} 要求的工厂形态;本菜单不下发,参数直接忽略。 */ + public ChainGroupMenu(int containerId, Inventory inventory, FriendlyByteBuf data) { + this(containerId, inventory); + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + return ItemStack.EMPTY; + } + + @Override + public boolean stillValid(Player player) { + return true; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/menus/ContainerPatternConverter.java b/src/main/java/com/sorrowmist/useless/content/menus/ContainerPatternConverter.java new file mode 100644 index 00000000..1567422f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/menus/ContainerPatternConverter.java @@ -0,0 +1,126 @@ +package com.sorrowmist.useless.content.menus; + +import appeng.menu.AEBaseMenu; +import appeng.menu.SlotSemantics; +import appeng.menu.MenuOpener; +import appeng.menu.locator.MenuLocator; +import appeng.menu.locator.MenuLocators; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraftforge.common.extensions.IForgeMenuType; +import net.minecraftforge.network.NetworkHooks; +import appeng.client.gui.Icon; +import com.sorrowmist.useless.inventory.slot.PatternSlotItemHandler; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.inventory.Slot; +import net.minecraftforge.items.IItemHandler; + +public final class ContainerPatternConverter extends AEBaseMenu { + private static final int SLOT_SIZE = 18; + private static final int PATTERN_X = 8; + private static final int PATTERN_Y = 30; + private static final int PLAYER_INVENTORY_X = 8; + private static final int PLAYER_INVENTORY_Y = 97; + private static final int PLAYER_HOTBAR_X = 8; + private static final int PLAYER_HOTBAR_Y = 155; + + // AE2 15's builder queues registration in the AE2 namespace. Own this type here; + // ModMenuType registers it once in our namespace, while AE2 owns locator handling. + public static final MenuType TYPE = createType(); + + private static MenuType createType() { + MenuType type = IForgeMenuType.create(ContainerPatternConverter::fromNetwork); + MenuOpener.addOpener(type, ContainerPatternConverter::open); + return type; + } + + private static ContainerPatternConverter fromNetwork(int id, Inventory inventory, FriendlyByteBuf data) { + MenuLocator locator = MenuLocators.readFromPacket(data); + HostPatternConverter host = locator.locate(inventory.player, HostPatternConverter.class); + if (host == null) throw new IllegalStateException("Pattern converter item is no longer in its menu slot"); + ContainerPatternConverter menu = new ContainerPatternConverter(id, inventory, host); + menu.setLocator(locator); + menu.setReturnedFromSubScreen(data.readBoolean()); + return menu; + } + + private static boolean open(Player player, MenuLocator locator, boolean returning) { + if (!(player instanceof ServerPlayer serverPlayer)) return false; + HostPatternConverter host = locator.locate(player, HostPatternConverter.class); + if (host == null) return false; + NetworkHooks.openScreen(serverPlayer, new SimpleMenuProvider((id, inventory, ignored) -> { + ContainerPatternConverter menu = new ContainerPatternConverter(id, inventory, host); + menu.setLocator(locator); + return menu; + }, Component.translatable("item.useless_mod.omniversal_pattern_converter")), data -> { + MenuLocators.writeToPacket(data, locator); + data.writeBoolean(returning); + }); + return true; + } + + public static final int CONVERT_BUTTON = 0; + private final HostPatternConverter host; + + public ContainerPatternConverter(int containerId, Inventory playerInventory, HostPatternConverter host) { + super(TYPE, containerId, playerInventory, host); + this.host = host; + + IItemHandler patternHandler = host.getPatternInventory().toItemHandler(); + for (int slot = 0; slot < HostPatternConverter.PATTERN_SLOTS; slot++) { + int column = slot % 9; + int row = slot / 9; + PatternSlotItemHandler patternSlot = new PatternSlotItemHandler( + patternHandler, slot, + PATTERN_X + column * SLOT_SIZE, + PATTERN_Y + row * SLOT_SIZE); + patternSlot.setIcon(Icon.BACKGROUND_BLANK_PATTERN); + this.addSlot(patternSlot, SlotSemantics.ENCODED_PATTERN); + } + + addPlayerInventorySlots(playerInventory); + } + + private void addPlayerInventorySlots(Inventory playerInventory) { + for (int row = 0; row < 3; row++) { + for (int column = 0; column < 9; column++) { + int slot = column + row * 9; + addSlot(new Slot(playerInventory, slot + 9, + PLAYER_INVENTORY_X + column * SLOT_SIZE, + PLAYER_INVENTORY_Y + row * SLOT_SIZE), SlotSemantics.PLAYER_INVENTORY); + } + } + + for (int column = 0; column < 9; column++) { + addSlot(new Slot(playerInventory, column, + PLAYER_HOTBAR_X + column * SLOT_SIZE, + PLAYER_HOTBAR_Y), SlotSemantics.PLAYER_HOTBAR); + } + } + + @Override + public boolean clickMenuButton(Player player, int id) { + if (id != CONVERT_BUTTON) return false; + if (player.level().isClientSide()) return true; + + HostPatternConverter.ConversionResult result = host.convertPatterns(); + player.displayClientMessage(createResultMessage(result), true); + broadcastChanges(); + return true; + } + + private static Component createResultMessage(HostPatternConverter.ConversionResult result) { + return switch (result.failure()) { + case UNBOUND -> Component.translatable("message.useless_mod.pattern_converter.unbound"); + case TARGET_UNAVAILABLE -> Component.translatable("message.useless_mod.pattern_converter.target_unavailable"); + case NO_PATTERNS -> Component.translatable("message.useless_mod.pattern_converter.no_patterns"); + case INVALID_CONTEXT -> Component.translatable("message.useless_mod.pattern_converter.invalid_context"); + case NONE -> Component.translatable( + "message.useless_mod.pattern_converter.result", result.converted(), result.skipped()); + }; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/menus/DimensionConfigMenu.java b/src/main/java/com/sorrowmist/useless/content/menus/DimensionConfigMenu.java new file mode 100644 index 00000000..e39e4e5c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/menus/DimensionConfigMenu.java @@ -0,0 +1,587 @@ +package com.sorrowmist.useless.content.menus; + +import com.sorrowmist.useless.init.ModMenuType; +import com.sorrowmist.useless.network.DimensionConfigGhostSlotPacket; +import com.sorrowmist.useless.world.dimension.DimensionGenerationConfig; +import com.sorrowmist.useless.world.dimension.UselessDimensionConfigManager; +import com.sorrowmist.useless.world.dimension.UselessDimensions; +import com.sorrowmist.useless.world.teleport.AbstractDimensionTeleporter; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.util.Mth; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ClickType; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Optional; +import java.util.UUID; + +public final class DimensionConfigMenu extends AbstractContainerMenu { + private static final int PLAYER_INVENTORY_X = 16; + private static final int PLAYER_INVENTORY_Y = 254; + private static final int PLAYER_HOTBAR_Y = 312; + + public static final int BORDER_SLOT = 0; + public static final int FILL_SLOT = 1; + public static final int CENTER_SLOT = 2; + public static final int BOUNDARY_A_SLOT = 3; + public static final int BOUNDARY_B_SLOT = 4; + public static final int ROAD_A_SLOT = 5; + public static final int ROAD_B_SLOT = 6; + public static final int ROAD_C_SLOT = 7; + public static final int CENTER_MARKER_SLOT = 8; + public static final int GHOST_SLOT_COUNT = 9; + /** 道路宽度下限:为零时道路布局不生成,道路与边界槽位也会随之隐藏。 */ + public static final int MIN_ROAD_WIDTH = 1; + /** 道路宽度上限,与 {@link DimensionGenerationConfig.Features#normalized()} 的钳制范围一致。 */ + public static final int MAX_ROAD_WIDTH = 16; + + private final UUID playerId; + private final ResourceKey targetDimension; + private final boolean canTeleport; + private final boolean firstSetup; + private final ResourceKey sourceDimension; + private final BlockPos sourcePos; + @Nullable + private final AbstractDimensionTeleporter teleporter; + private final GhostSlot[] ghostSlots = new GhostSlot[GHOST_SLOT_COUNT]; + /** 打开菜单时该维度的原始配置,用于多联模式下保留边界与道路方块设置。 */ + private DimensionGenerationConfig initialConfig; + /** + * 菜单打开时该维度配置的只读快照。 + * + *

多联模式下边界与道路槽位被隐藏,其方块值无法从槽位读取,必须以该维度的 + * 原始配置兜底。{@link #initialConfig} 会在每次提交或导入后被重写为当前编辑 + * 结果,无法承担该职责,因此单独保留一份不随编辑变化的快照。 + */ + private final DimensionGenerationConfig preservedConfig; + + private int platformLayers; + private int platformStartY; + private int boundaryIntervalX; + private int boundaryIntervalZ; + private int roadWidth; + private DimensionGenerationConfig.Mode mode; + private boolean centerMarkerEnabled; + private boolean generateBedrock; + private boolean bedrockAtBottom; + + public DimensionConfigMenu(int containerId, Inventory inventory, FriendlyByteBuf buffer) { + this(containerId, inventory, Context.read(buffer, inventory.player)); + } + + public DimensionConfigMenu(int containerId, Inventory inventory, Context context) { + super(ModMenuType.DIMENSION_CONFIG_MENU.get(), containerId); + this.playerId = inventory.player.getUUID(); + this.targetDimension = context.targetDimension(); + this.canTeleport = context.canTeleport(); + this.firstSetup = context.firstSetup(); + this.sourceDimension = context.sourceDimension(); + this.sourcePos = context.sourcePos().immutable(); + this.teleporter = context.teleporter(); + + DimensionGenerationConfig config = context.initialConfig().normalized(); + this.initialConfig = config; + this.preservedConfig = config; + this.platformLayers = config.platformLayers(); + this.platformStartY = config.platformStartY(); + this.boundaryIntervalX = config.boundaryIntervalX(); + this.boundaryIntervalZ = config.boundaryIntervalZ(); + this.roadWidth = config.roadWidth(); + this.mode = config.mode(); + normalizeRoadWidth(); + this.centerMarkerEnabled = config.centerMarkerEnabled(); + this.generateBedrock = config.generateBedrock(); + this.bedrockAtBottom = config.bedrockAtBottom(); + + ghostSlots[BORDER_SLOT] = new GhostSlot(config.borderBlockId(), 16, 34); + ghostSlots[FILL_SLOT] = new GhostSlot(config.fillBlockId(), 16, 52); + ghostSlots[CENTER_SLOT] = new GhostSlot(config.centerBlockId(), 16, 70); + ghostSlots[BOUNDARY_A_SLOT] = new GhostSlot(config.boundaryBlockAId(), 16, 100); + ghostSlots[BOUNDARY_B_SLOT] = new GhostSlot(config.boundaryBlockBId(), 16, 118); + ghostSlots[ROAD_A_SLOT] = new GhostSlot(config.roadBlockAId(), 16, 148); + ghostSlots[ROAD_B_SLOT] = new GhostSlot(config.roadBlockBId(), 16, 165); + ghostSlots[ROAD_C_SLOT] = new GhostSlot(config.roadBlockCId(), 16, 182); + ghostSlots[CENTER_MARKER_SLOT] = new GhostSlot(config.centerMarkerBlockId(), 16, 212); + for (GhostSlot slot : ghostSlots) addSlot(slot); + updateSlotVisibility(); + addPlayerInventory(inventory); + } + + private void addPlayerInventory(Inventory inventory) { + for (int row = 0; row < 3; row++) { + for (int column = 0; column < 9; column++) { + addSlot(new Slot(inventory, column + row * 9 + 9, + PLAYER_INVENTORY_X + column * 18, PLAYER_INVENTORY_Y + row * 18)); + } + } + for (int column = 0; column < 9; column++) { + addSlot(new Slot(inventory, column, PLAYER_INVENTORY_X + column * 18, + PLAYER_HOTBAR_Y)); + } + } + + public static void openForTeleport(ServerPlayer player, AbstractDimensionTeleporter teleporter, + BlockPos sourcePos) { + ResourceKey target = teleporter.targetDimensionFor(player.level().dimension()); + open(player, new Context(target, teleporter, + player.level().dimension(), sourcePos, + true, !UselessDimensionConfigManager.isConfigured(player.server, target), + UselessDimensionConfigManager.get(player.server, target))); + } + + public static void openForEdit(ServerPlayer player, AbstractDimensionTeleporter teleporter, + BlockPos sourcePos) { + // 以传送方块自身所属的维度为准,而不是玩家当前所在的维度: + // 玩家可以在任一维度里放置并编辑属于其它维度的传送方块, + // 若按玩家所在维度取目标,界面会显示并写入错误的维度配置。 + ResourceKey target = teleporter.dimensionKey(); + open(player, new Context(target, teleporter, + player.level().dimension(), sourcePos, true, + !UselessDimensionConfigManager.isConfigured(player.server, target), + UselessDimensionConfigManager.get(player.server, target))); + } + + private static void open(ServerPlayer player, Context context) { + if (player.containerMenu instanceof DimensionConfigMenu) return; + MenuProvider provider = new MenuProvider() { + @Override + public @NotNull Component getDisplayName() { + return Component.translatable("menu.useless_mod.dimension_config"); + } + + @Override + public AbstractContainerMenu createMenu(int containerId, Inventory inventory, Player player) { + return new DimensionConfigMenu(containerId, inventory, context); + } + }; + net.minecraftforge.network.NetworkHooks.openScreen(player, provider, buffer -> context.write(buffer)); + } + + public ResourceKey getTargetDimension() { + return targetDimension; + } + + public boolean canTeleport() { + return canTeleport; + } + + public int getPlatformLayers() { + return platformLayers; + } + + public int getPlatformStartY() { + return platformStartY; + } + + public int getBoundaryIntervalX() { + return boundaryIntervalX; + } + + public int getBoundaryIntervalZ() { + return boundaryIntervalZ; + } + + public int getRoadWidth() { + return roadWidth; + } + + public DimensionGenerationConfig.Mode getMode() { + return mode; + } + + /** 是否处于多联模式:此时边界与道路配置整体隐藏。 */ + public boolean isMultiMode() { + return mode == DimensionGenerationConfig.Mode.MULTI; + } + + /** 模式按钮:在马路与多联之间循环。 */ + public void cycleMode() { + mode = mode.next(); + normalizeRoadWidth(); + updateSlotVisibility(); + } + + /** + * 马路模式下道路宽度至少为 1。 + * + *

边界与道路槽位的可见性依赖 {@link #isRoadFeatureEnabled()},而该判定包含 + * 道路宽度大于零这一条件。若宽度为零,切换到马路模式后这些槽位仍整体隐藏, + * 玩家必须先手动把宽度改为正值才能看到其方块预设。因此在进入马路模式时 + * 直接把宽度提升到最小值,使槽位随模式切换一同出现。 + */ + private void normalizeRoadWidth() { + if (mode == DimensionGenerationConfig.Mode.ROAD && roadWidth < 1) { + roadWidth = 1; + } + } + + /** 按当前模式刷新幽灵槽位的显示状态:多联模式隐藏边界与道路相关槽位。 */ + private void updateSlotVisibility() { + for (int index = 0; index < GHOST_SLOT_COUNT; index++) { + ghostSlots[index].setHidden(!isGhostSlotActive(index)); + } + } + + public boolean isCenterMarkerEnabled() { + return centerMarkerEnabled; + } + + public boolean isGenerateBedrock() { + return generateBedrock; + } + + public boolean isBedrockAtBottom() { + return bedrockAtBottom; + } + + public void setPlatformLayers(int value) { + platformLayers = value; + } + + public void setPlatformStartY(int value) { + platformStartY = value; + } + + public void setBoundaryIntervalX(int value) { + boundaryIntervalX = value; + } + + public void setBoundaryIntervalZ(int value) { + boundaryIntervalZ = value; + } + + public void setRoadWidth(int value) { + roadWidth = Mth.clamp(value, MIN_ROAD_WIDTH, MAX_ROAD_WIDTH); + } + + /** 边界间隔配置在马路模式下表示道路间隔,在多联模式下表示合并尺寸,因此始终显示。 */ + public boolean isBoundaryIntervalVisible() { + return true; + } + + /** 边界交替方块仅在马路模式使用;多联模式的合并组内部不再显示边框。 */ + public boolean isBoundarySlotVisible() { + return mode != DimensionGenerationConfig.Mode.MULTI; + } + + /** 道路宽度属于道路布局,仅马路模式可配置。 */ + public boolean isRoadWidthVisible() { + return mode == DimensionGenerationConfig.Mode.ROAD; + } + + /** 仅马路模式需要道路主体与中心标线配置。 */ + public boolean isRoadPatternSelected() { + return mode == DimensionGenerationConfig.Mode.ROAD; + } + + /** 中心标记仅马路模式可配置;多联模式不需要。 */ + public boolean isCenterMarkerVisible() { + return mode != DimensionGenerationConfig.Mode.MULTI; + } + + public void toggleCenterMarker() { + centerMarkerEnabled = !centerMarkerEnabled; + } + + public void toggleGenerateBedrock() { + generateBedrock = !generateBedrock; + } + + public void toggleBedrockAtBottom() { + bedrockAtBottom = !bedrockAtBottom; + } + + public GhostSlot getGhostSlot(int index) { + return index >= 0 && index < GHOST_SLOT_COUNT ? ghostSlots[index] : null; + } + + public boolean isGhostSlotActive(int index) { + if (index < 0 || index >= GHOST_SLOT_COUNT) return false; + return switch (index) { + case BOUNDARY_A_SLOT, BOUNDARY_B_SLOT, ROAD_A_SLOT -> isRoadFeatureEnabled(); + case ROAD_B_SLOT, ROAD_C_SLOT -> isRoadFeatureEnabled() + && isRoadPatternSelected(); + case CENTER_MARKER_SLOT -> isCenterMarkerVisible(); + default -> true; + }; + } + + private boolean isRoadFeatureEnabled() { + // 多联模式不使用道路布局,相关槽位整体隐藏。 + if (mode == DimensionGenerationConfig.Mode.MULTI) return false; + return DimensionGenerationConfig.areBoundaryAndRoadFeaturesEnabled( + boundaryIntervalX, boundaryIntervalZ, roadWidth); + } + + public boolean isCompleteConfiguration() { + return createConfiguration().map(config -> config.isValid() && config.hasAllowedBlockIds()).orElse(false); + } + + public Optional createConfiguration() { + ResourceLocation[] blocks = new ResourceLocation[GHOST_SLOT_COUNT]; + // 基础方块槽位在界面中始终可见,正常情况下必定有值;仅当槽位为空或 + // 引用了已失效的方块时才回落到该维度的原始配置,避免整份配置被判为 + // 不完整而让预览与保存一并失效。 + for (int i = BORDER_SLOT; i <= CENTER_SLOT; i++) { + blocks[i] = DimensionGenerationConfig.blockId(ghostSlots[i].getItem()); + if (blocks[i] == null) { + blocks[i] = switch (i) { + case BORDER_SLOT -> preservedConfig.borderBlockId(); + case FILL_SLOT -> preservedConfig.fillBlockId(); + default -> preservedConfig.centerBlockId(); + }; + } + } + // 多联模式下这些槽位被隐藏,槽位可能为空或已被导入预设改写, + // 必须以该维度打开菜单时的原始配置回落,否则保存多联模式会把该维度 + // 已有的边界与道路方块覆盖成全局默认值,或覆盖成预设里的值。 + // 该回落基准不能使用 initialConfig:它在每次提交与导入后都会被改写。 + DimensionGenerationConfig fallback = isMultiMode() + ? preservedConfig : DimensionGenerationConfig.defaults(); + blocks[BOUNDARY_A_SLOT] = optionalBlock(BOUNDARY_A_SLOT, fallback.boundaryBlockAId()); + blocks[BOUNDARY_B_SLOT] = optionalBlock(BOUNDARY_B_SLOT, fallback.boundaryBlockBId()); + blocks[ROAD_A_SLOT] = optionalBlock(ROAD_A_SLOT, fallback.roadBlockAId()); + blocks[ROAD_B_SLOT] = optionalBlock(ROAD_B_SLOT, fallback.roadBlockBId()); + blocks[ROAD_C_SLOT] = optionalBlock(ROAD_C_SLOT, fallback.roadBlockCId()); + blocks[CENTER_MARKER_SLOT] = optionalBlock(CENTER_MARKER_SLOT, fallback.centerMarkerBlockId()); + DimensionGenerationConfig.Features features = new DimensionGenerationConfig.Features( + blocks[BOUNDARY_A_SLOT], + blocks[BOUNDARY_B_SLOT], + boundaryIntervalX, + boundaryIntervalZ, + roadWidth, + mode, + blocks[ROAD_A_SLOT], + blocks[ROAD_B_SLOT], + blocks[ROAD_C_SLOT], + centerMarkerEnabled, + blocks[CENTER_MARKER_SLOT]); + return Optional.of(new DimensionGenerationConfig( + blocks[BORDER_SLOT], blocks[FILL_SLOT], blocks[CENTER_SLOT], + platformLayers, platformStartY, generateBedrock, bedrockAtBottom, features)); + } + + private ResourceLocation optionalBlock(int slot, ResourceLocation fallback) { + ResourceLocation id = DimensionGenerationConfig.blockId(ghostSlots[slot].getItem()); + return id == null ? fallback : id; + } + + public void setGhostSlotFromClient(int index, ItemStack stack) { + if (!isGhostSlotActive(index)) return; + ResourceLocation id = DimensionGenerationConfig.blockId(stack); + if (id == null || !DimensionGenerationConfig.isValidBlockId(id)) return; + GhostSlot slot = getGhostSlot(index); + if (slot == null) return; + slot.setBlockId(id); + PacketDistributor.sendToServer(new DimensionConfigGhostSlotPacket(containerId, index, id)); + } + + public boolean setGhostBlockId(int index, @Nullable ResourceLocation id) { + if (!isGhostSlotActive(index)) return false; + GhostSlot slot = getGhostSlot(index); + if (slot == null) return false; + if (id != null && !DimensionGenerationConfig.isAllowedBlockId(id)) return false; + slot.setBlockId(id); + return true; + } + + public void submit(ServerPlayer player, DimensionGenerationConfig requested, boolean teleportAfterSave) { + if (!requested.isValid() || !requested.hasAllowedBlockIds() + || !UselessDimensions.isUselessDimension(targetDimension) + || !player.getUUID().equals(playerId) + || !stillValid(player)) { + player.displayClientMessage(Component.translatable("gui.useless_mod.dimension_config.invalid"), true); + return; + } + + DimensionGenerationConfig applied; + try { + applied = UselessDimensionConfigManager.save( + player.server, targetDimension, requested, firstSetup); + } catch (IllegalArgumentException exception) { + player.displayClientMessage(Component.translatable("gui.useless_mod.dimension_config.invalid"), true); + return; + } + copyFrom(applied); + + if (teleportAfterSave && canTeleport() && sourceIsStillValid(player)) { + player.closeContainer(); + teleporter.teleportAfterConfiguration(player, sourceDimension, sourcePos); + return; + } + player.closeContainer(); + } + + /** + * 套用导入的预设到当前编辑状态,但不提交给服务端: + * 玩家仍需点击「应用」才会写入世界配置。 + */ + public void applyPreset(DimensionGenerationConfig config) { + copyFrom(config); + } + + private void copyFrom(DimensionGenerationConfig config) { + initialConfig = config; + platformLayers = config.platformLayers(); + platformStartY = config.platformStartY(); + boundaryIntervalX = config.boundaryIntervalX(); + boundaryIntervalZ = config.boundaryIntervalZ(); + roadWidth = config.roadWidth(); + mode = config.mode(); + normalizeRoadWidth(); + updateSlotVisibility(); + centerMarkerEnabled = config.centerMarkerEnabled(); + generateBedrock = config.generateBedrock(); + bedrockAtBottom = config.bedrockAtBottom(); + ghostSlots[BORDER_SLOT].setBlockId(config.borderBlockId()); + ghostSlots[FILL_SLOT].setBlockId(config.fillBlockId()); + ghostSlots[CENTER_SLOT].setBlockId(config.centerBlockId()); + ghostSlots[BOUNDARY_A_SLOT].setBlockId(config.boundaryBlockAId()); + ghostSlots[BOUNDARY_B_SLOT].setBlockId(config.boundaryBlockBId()); + ghostSlots[ROAD_A_SLOT].setBlockId(config.roadBlockAId()); + ghostSlots[ROAD_B_SLOT].setBlockId(config.roadBlockBId()); + ghostSlots[ROAD_C_SLOT].setBlockId(config.roadBlockCId()); + ghostSlots[CENTER_MARKER_SLOT].setBlockId(config.centerMarkerBlockId()); + } + + private boolean sourceIsStillValid(ServerPlayer player) { + return teleporter != null + && player.level().dimension().equals(sourceDimension) + && player.level().getBlockState(sourcePos).is(teleporter.getTeleportBlockForValidation()); + } + + @Override + public void clicked(int slotId, int button, @NotNull ClickType clickType, @NotNull Player player) { + if (slotId >= 0 && slotId < GHOST_SLOT_COUNT) { + ItemStack carried = getCarried(); + setGhostBlockId(slotId, + !carried.isEmpty() && carried.getItem() instanceof BlockItem + ? DimensionGenerationConfig.blockId(carried) : null); + return; + } + super.clicked(slotId, button, clickType, player); + } + + @Override + public @NotNull ItemStack quickMoveStack(@NotNull Player player, int index) { + return ItemStack.EMPTY; + } + + @Override + public boolean stillValid(@NotNull Player player) { + return player.getUUID().equals(playerId) + && player.level().dimension().equals(sourceDimension); + } + + public record Context(ResourceKey targetDimension, + @Nullable AbstractDimensionTeleporter teleporter, + ResourceKey sourceDimension, + BlockPos sourcePos, + boolean canTeleport, + boolean firstSetup, + DimensionGenerationConfig initialConfig) { + public void write(FriendlyByteBuf buffer) { + buffer.writeResourceLocation(targetDimension.location()); + buffer.writeBoolean(canTeleport && teleporter != null); + buffer.writeBoolean(firstSetup); + initialConfig.write(buffer); + } + + private static Context read(FriendlyByteBuf buffer, Player player) { + ResourceKey target = ResourceKey.create(Registries.DIMENSION, buffer.readResourceLocation()); + boolean canTeleport = buffer.readBoolean(); + boolean firstSetup = buffer.readBoolean(); + return new Context(target, null, player.level().dimension(), player.blockPosition(), + canTeleport, firstSetup, DimensionGenerationConfig.read(buffer)); + } + } + + public static final class GhostSlot extends Slot { + private ItemStack stack; + /** 隐藏的槽位既不渲染底板也不响应点击,用于多联模式下整体移除边界与道路配置。 */ + private boolean hidden; + + public boolean isHidden() { + return hidden; + } + + public void setHidden(boolean hidden) { + this.hidden = hidden; + } + + GhostSlot(ResourceLocation blockId, int x, int y) { + super(new SimpleContainer(1), 0, x, y); + this.stack = stackFor(blockId); + } + + private static ItemStack stackFor(@Nullable ResourceLocation id) { + if (id == null || !DimensionGenerationConfig.isValidBlockId(id)) return ItemStack.EMPTY; + return new ItemStack(net.minecraft.core.registries.BuiltInRegistries.BLOCK.get(id).asItem()); + } + + private void setBlockId(@Nullable ResourceLocation id) { + stack = stackFor(id); + } + + @Override + public ItemStack getItem() { + return stack; + } + + @Override + public boolean hasItem() { + return !stack.isEmpty(); + } + + @Override + public void set(ItemStack stack) { + setBlockId(DimensionGenerationConfig.blockId(stack)); + } + + @Override + public ItemStack remove(int amount) { + return ItemStack.EMPTY; + } + + @Override + public boolean mayPlace(ItemStack stack) { + return stack.getItem() instanceof BlockItem; + } + + @Override + public boolean mayPickup(Player player) { + return false; + } + + @Override + public boolean allowModification(Player player) { + return false; + } + + public boolean isFake() { + return true; + } + + /** 隐藏后既不可渲染也不可交互,等同于该槽位不存在。 */ + @Override + public boolean isActive() { + return !hidden; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/menus/HostPatternConverter.java b/src/main/java/com/sorrowmist/useless/content/menus/HostPatternConverter.java new file mode 100644 index 00000000..15c98c07 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/menus/HostPatternConverter.java @@ -0,0 +1,181 @@ +package com.sorrowmist.useless.content.menus; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.crafting.PatternDetailsHelper; +import appeng.crafting.pattern.AECraftingPattern; +import appeng.crafting.pattern.AEProcessingPattern; +import appeng.api.implementations.menuobjects.ItemMenuHost; +import appeng.api.inventories.InternalInventory; +import appeng.util.inv.AppEngInternalInventory; +import appeng.util.inv.InternalInventoryHost; +import com.sorrowmist.useless.content.blockentities.AdvancedAlloyFurnaceBlockEntity; +import com.sorrowmist.useless.content.blockentities.multiblock.MultiblockAlloyFurnaceCoreBlockEntity; +import com.sorrowmist.useless.content.blockentities.multiblock.OmniversalMoldHubBlockEntity; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.OmniversalPatternEncoding; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.OmniversalPatternRecipeSelector; +import com.sorrowmist.useless.content.recipe.MoldMatcher; +import com.sorrowmist.useless.core.component.PatternConverterData; +import com.sorrowmist.useless.core.component.UComponents; +import net.minecraft.core.GlobalPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.BlockEntity; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import static com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout.MOLD_SLOT; + +/** Server-side state and conversion logic for the converter item menu. */ +public final class HostPatternConverter extends ItemMenuHost + implements InternalInventoryHost { + public static final int PATTERN_SLOTS = 27; + + private final AppEngInternalInventory patternInventory = new AppEngInternalInventory(this, PATTERN_SLOTS); + + public HostPatternConverter( + Player player, int slot, ItemStack stack) { + super(player, slot, stack); + PatternConverterData data = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(getItemStack(), UComponents.PATTERN_CONVERTER_DATA.get()); + if (data != null) { + patternInventory.readFromNBT(data.data(), "Patterns"); + } + } + + public AppEngInternalInventory getPatternInventory() { + return patternInventory; + } + + @Override + public void saveChanges() { + saveInventory(); + } + + @Override + public void onChangeInventory(InternalInventory inventory, int slot) { + saveInventory(); + } + + private void saveInventory() { + if (isClientSide()) return; + getPlayer().getInventory().setChanged(); + CompoundTag data = new CompoundTag(); + patternInventory.writeToNBT(data, "Patterns"); + if (data.isEmpty()) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.remove(getItemStack(), UComponents.PATTERN_CONVERTER_DATA.get()); + } else { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(getItemStack(), UComponents.PATTERN_CONVERTER_DATA.get(), new PatternConverterData(data)); + } + } + + public ConversionResult convertPatterns() { + if (!(getPlayer() instanceof ServerPlayer player)) { + return new ConversionResult(0, 0, false, Failure.INVALID_CONTEXT); + } + + ConversionTarget target = findTarget(player); + if (target == null) { + return new ConversionResult(0, 0, false, com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(getItemStack(), UComponents.PATTERN_CONVERTER_LINK_TARGET.get()) == null + ? Failure.UNBOUND : Failure.TARGET_UNAVAILABLE); + } + + MoldMatcher.PreparedMolds preparedMolds = MoldMatcher.prepare(target.molds()); + int converted = 0; + int skipped = 0; + boolean hasPatterns = false; + for (int slot = 0; slot < patternInventory.size(); slot++) { + ItemStack source = patternInventory.getStackInSlot(slot); + if (source.isEmpty()) continue; + hasPatterns = true; + + try { + IPatternDetails details = PatternDetailsHelper.decodePattern(source, target.level()); + if (!(details instanceof AEProcessingPattern) + && !(details instanceof AECraftingPattern)) { + skipped++; + continue; + } + + var entry = details instanceof AECraftingPattern crafting + ? OmniversalPatternRecipeSelector.selectCrafting( + target.level(), crafting, preparedMolds) + : OmniversalPatternRecipeSelector.select( + target.level(), details, preparedMolds); + if (entry.isEmpty()) { + skipped++; + continue; + } + + ItemStack convertedPattern = OmniversalPatternEncoding.encode( + source, details, entry.get(), target.level()); + if (convertedPattern.isEmpty()) { + skipped++; + continue; + } + + patternInventory.setItemDirect(slot, convertedPattern); + converted++; + } catch (RuntimeException ignored) { + skipped++; + } + } + + if (!hasPatterns) { + return new ConversionResult(0, 0, false, Failure.NO_PATTERNS); + } + return new ConversionResult(converted, skipped, true, Failure.NONE); + } + + private static Map getAvailableMolds(OmniversalMoldHubBlockEntity hub) { + var molds = hub.getMolds(); + int activeSlots = molds.getActiveSlots(); + Map result = new LinkedHashMap<>(); + for (int slot = 0; slot < activeSlots; slot++) { + ItemStack mold = molds.getStackInSlot(slot); + if (!mold.isEmpty()) result.put(slot, mold.copy()); + } + return Collections.unmodifiableMap(result); + } + + private static Map getAvailableMolds(AdvancedAlloyFurnaceBlockEntity furnace) { + ItemStack mold = furnace.getItemHandler().getStackInSlot(MOLD_SLOT); + return mold.isEmpty() ? Map.of() : Map.of(0, mold.copy()); + } + + private ConversionTarget findTarget(ServerPlayer player) { + GlobalPos link = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(getItemStack(), UComponents.PATTERN_CONVERTER_LINK_TARGET.get()); + if (link == null) return null; + ServerLevel level = player.getServer() == null ? null : player.getServer().getLevel(link.dimension()); + if (level == null || !level.isLoaded(link.pos())) return null; + BlockEntity blockEntity = level.getBlockEntity(link.pos()); + if (blockEntity instanceof OmniversalMoldHubBlockEntity hub) { + return new ConversionTarget(level, getAvailableMolds(hub)); + } + if (blockEntity instanceof AdvancedAlloyFurnaceBlockEntity furnace) { + return new ConversionTarget(level, getAvailableMolds(furnace)); + } + if (blockEntity instanceof MultiblockAlloyFurnaceCoreBlockEntity core) { + OmniversalMoldHubBlockEntity hub = core.getLinkedMoldHub(); + if (hub != null) return new ConversionTarget(level, getAvailableMolds(hub)); + } + return null; + } + + public record ConversionResult(int converted, int skipped, boolean hasPatterns, Failure failure) { + } + + public enum Failure { + NONE, + UNBOUND, + TARGET_UNAVAILABLE, + NO_PATTERNS, + INVALID_CONTEXT + } + + private record ConversionTarget(ServerLevel level, Map molds) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/menus/MePatternAssemblyMenu.java b/src/main/java/com/sorrowmist/useless/content/menus/MePatternAssemblyMenu.java new file mode 100644 index 00000000..742e1adc --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/menus/MePatternAssemblyMenu.java @@ -0,0 +1,35 @@ +package com.sorrowmist.useless.content.menus; + +import com.sorrowmist.useless.content.blockentities.PagedMenuPageMemory; +import com.sorrowmist.useless.content.blockentities.RecoverableItemStackHandler; +import com.sorrowmist.useless.content.blockentities.multiblock.MePatternAssemblyBlockEntity; +import com.sorrowmist.useless.core.config.ConfigManager; +import com.sorrowmist.useless.init.ModMenuType; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.entity.player.Inventory; +import org.jetbrains.annotations.Nullable; + +public final class MePatternAssemblyMenu extends PagedRecoverableMenu { + public MePatternAssemblyMenu(int containerId, Inventory inventory, FriendlyByteBuf buffer) { + this(containerId, inventory, buffer.readBlockPos()); + } + + public MePatternAssemblyMenu(int containerId, Inventory inventory, BlockPos pos) { + super(ModMenuType.ME_PATTERN_ASSEMBLY_MENU.get(), containerId, inventory, + handler(inventory, pos), pos, pageMemory(inventory, pos)); + } + + @Nullable + private static PagedMenuPageMemory pageMemory(Inventory inventory, BlockPos pos) { + return inventory.player.level().getBlockEntity(pos) instanceof MePatternAssemblyBlockEntity assembly + ? assembly.getPageMemory() : null; + } + + private static RecoverableItemStackHandler handler(Inventory inventory, BlockPos pos) { + if (inventory.player.level().getBlockEntity(pos) instanceof MePatternAssemblyBlockEntity assembly) { + return assembly.getPatterns(); + } + return new RecoverableItemStackHandler(ConfigManager::getOmniversalPatternSlots, stack -> false, () -> {}); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/menus/MultiblockAlloyFurnaceMenu.java b/src/main/java/com/sorrowmist/useless/content/menus/MultiblockAlloyFurnaceMenu.java new file mode 100644 index 00000000..18861ccb --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/menus/MultiblockAlloyFurnaceMenu.java @@ -0,0 +1,104 @@ +package com.sorrowmist.useless.content.menus; + +import com.sorrowmist.useless.content.blockentities.multiblock.MultiblockAlloyFurnaceCoreBlockEntity; +import com.sorrowmist.useless.init.ModMenuType; +import com.sorrowmist.useless.network.AETaskProgressPacket; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.SimpleContainerData; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public final class MultiblockAlloyFurnaceMenu extends AbstractContainerMenu { + private final BlockPos blockPos; + private final ContainerData data; + @Nullable + private final MultiblockAlloyFurnaceCoreBlockEntity core; + private final TaskProgressSnapshot taskProgress = new TaskProgressSnapshot(); + + public MultiblockAlloyFurnaceMenu(int containerId, Inventory inventory, FriendlyByteBuf buffer) { + this(containerId, inventory, buffer.readBlockPos()); + } + + public MultiblockAlloyFurnaceMenu(int containerId, Inventory inventory, BlockPos pos) { + super(ModMenuType.MULTIBLOCK_ALLOY_FURNACE_MENU.get(), containerId); + blockPos = pos.immutable(); + core = inventory.player.level().getBlockEntity(pos) instanceof MultiblockAlloyFurnaceCoreBlockEntity value + ? value : null; + // The server's live ContainerData is read-only (its set method is a + // no-op). A client menu must always expose a writable buffer so the + // container sync packets can populate it, even when the client-side + // block entity is already present. + boolean clientSide = inventory.player.level().isClientSide; + data = createMenuData(clientSide, + !clientSide && core != null ? core.getMenuData() : null); + addDataSlots(data); + for (int row = 0; row < 3; row++) { + for (int column = 0; column < 9; column++) { + addSlot(new Slot(inventory, column + row * 9 + 9, 8 + column * 18, 140 + row * 18)); + } + } + for (int column = 0; column < 9; column++) { + addSlot(new Slot(inventory, column, 8 + column * 18, 198)); + } + } + + public BlockPos getBlockPos() { return blockPos; } + public boolean isFormed() { return data.get(0) != 0; } + public int getCoilTier() { return data.get(1); } + public long getEnergy() { return join(data.get(2), data.get(3)); } + public long getCapacity() { return join(data.get(4), data.get(5)); } + public int getActiveTasks() { return data.get(6); } + public int getRedstoneMode() { return data.get(7); } + public int getMaxTasks() { return data.get(8); } + public long getAutomaticEnergyLimit() { return join(data.get(9), data.get(10)); } + @Nullable public MultiblockAlloyFurnaceCoreBlockEntity getCore() { return core; } + + public List getTaskProgress() { + return taskProgress.get(); + } + + public void updateTaskProgress(List tasks) { + taskProgress.update(tasks); + } + + static ContainerData createMenuData(boolean clientSide, @Nullable ContainerData serverData) { + return !clientSide && serverData != null + ? serverData + : new SimpleContainerData(MultiblockAlloyFurnaceCoreBlockEntity.MENU_DATA_COUNT); + } + + static long join(int low, int high) { + return Integer.toUnsignedLong(low) | (long) high << 32; + } + + static final class TaskProgressSnapshot { + private volatile List tasks = List.of(); + + List get() { + return tasks; + } + + void update(List updatedTasks) { + tasks = List.copyOf(updatedTasks); + } + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + return ItemStack.EMPTY; + } + + @Override + public boolean stillValid(Player player) { + return player.level().getBlockEntity(blockPos) instanceof MultiblockAlloyFurnaceCoreBlockEntity + && player.distanceToSqr(blockPos.getX() + 0.5D, blockPos.getY() + 0.5D, blockPos.getZ() + 0.5D) <= 64.0D; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/menus/OmniversalMoldHubMenu.java b/src/main/java/com/sorrowmist/useless/content/menus/OmniversalMoldHubMenu.java new file mode 100644 index 00000000..c3cc4d8e --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/menus/OmniversalMoldHubMenu.java @@ -0,0 +1,35 @@ +package com.sorrowmist.useless.content.menus; + +import com.sorrowmist.useless.content.blockentities.PagedMenuPageMemory; +import com.sorrowmist.useless.content.blockentities.RecoverableItemStackHandler; +import com.sorrowmist.useless.content.blockentities.multiblock.OmniversalMoldHubBlockEntity; +import com.sorrowmist.useless.core.config.ConfigManager; +import com.sorrowmist.useless.init.ModMenuType; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.entity.player.Inventory; +import org.jetbrains.annotations.Nullable; + +public final class OmniversalMoldHubMenu extends PagedRecoverableMenu { + public OmniversalMoldHubMenu(int containerId, Inventory inventory, FriendlyByteBuf buffer) { + this(containerId, inventory, buffer.readBlockPos()); + } + + public OmniversalMoldHubMenu(int containerId, Inventory inventory, BlockPos pos) { + super(ModMenuType.OMNIVERSAL_MOLD_HUB_MENU.get(), containerId, inventory, + handler(inventory, pos), pos, pageMemory(inventory, pos)); + } + + @Nullable + private static PagedMenuPageMemory pageMemory(Inventory inventory, BlockPos pos) { + return inventory.player.level().getBlockEntity(pos) instanceof OmniversalMoldHubBlockEntity hub + ? hub.getPageMemory() : null; + } + + private static RecoverableItemStackHandler handler(Inventory inventory, BlockPos pos) { + if (inventory.player.level().getBlockEntity(pos) instanceof OmniversalMoldHubBlockEntity hub) { + return hub.getMolds(); + } + return new RecoverableItemStackHandler(ConfigManager::getOmniversalMoldSlots, stack -> false, () -> {}); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/menus/OreGeneratorMenu.java b/src/main/java/com/sorrowmist/useless/content/menus/OreGeneratorMenu.java new file mode 100644 index 00000000..c744a67b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/menus/OreGeneratorMenu.java @@ -0,0 +1,92 @@ +package com.sorrowmist.useless.content.menus; + +import com.sorrowmist.useless.content.blockentities.OreGeneratorBlockEntity; +import com.sorrowmist.useless.content.blockentities.OreGeneratorSampleHandler; +import com.sorrowmist.useless.content.blockentities.RecoverableItemStackHandler; +import com.sorrowmist.useless.core.config.ConfigManager; +import com.sorrowmist.useless.init.ModMenuType; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.SimpleContainerData; +import net.minecraft.world.inventory.Slot; + +public final class OreGeneratorMenu extends PagedRecoverableMenu { + private final BlockPos blockPos; + private final OreGeneratorBlockEntity generator; + private final ContainerData data; + + public OreGeneratorMenu(int containerId, Inventory inventory, FriendlyByteBuf buffer) { + this(containerId, inventory, buffer.readBlockPos()); + } + + public OreGeneratorMenu(int containerId, Inventory inventory, BlockPos pos) { + super(ModMenuType.ORE_GENERATOR_MENU.get(), containerId, inventory, + handler(inventory, pos), pos, 8, 22, 44, 254, 44, 312); + blockPos = pos.immutable(); + generator = inventory.player.level().getBlockEntity(pos) instanceof OreGeneratorBlockEntity value + ? value : null; + data = generator == null || inventory.player.level().isClientSide + ? new SimpleContainerData(OreGeneratorBlockEntity.MENU_DATA_COUNT) + : generator.getMenuData(); + addDataSlots(data); + } + + private static RecoverableItemStackHandler handler(Inventory inventory, BlockPos pos) { + if (inventory.player.level().getBlockEntity(pos) instanceof OreGeneratorBlockEntity generator) { + return generator.getSamples(); + } + return new OreGeneratorSampleHandler(ConfigManager::getOreGeneratorSlots, + stack -> false, () -> { }); + } + + public BlockPos getBlockPos() { + return blockPos; + } + + public OreGeneratorBlockEntity getGenerator() { + return generator; + } + + public long getOutputRate() { + return join(data.get(0), data.get(1)); + } + + public int getCountdownTicks() { + return Math.max(0, data.get(2)); + } + + public int getActiveSlots() { + return Math.max(0, data.get(3)); + } + + public int getConfiguredSlots() { + return Math.max(1, data.get(4)); + } + + public boolean isOutputToAe() { + return data.get(5) > 0; + } + + public boolean isAeOnline() { + return data.get(6) > 0; + } + + public int getSampleSlotIndex(Slot slot) { + int menuSlot = slots.indexOf(slot); + return menuSlot >= 0 && menuSlot < SLOTS_PER_PAGE + ? getPage() * SLOTS_PER_PAGE + menuSlot : -1; + } + + private static long join(int low, int high) { + return Integer.toUnsignedLong(low) | (long) high << 32; + } + + @Override + public boolean stillValid(net.minecraft.world.entity.player.Player player) { + return generator != null && player.level().getBlockEntity(blockPos) == generator + && player.distanceToSqr(blockPos.getX() + 0.5D, blockPos.getY() + 0.5D, + blockPos.getZ() + 0.5D) <= 64.0D; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/menus/PagedRecoverableMenu.java b/src/main/java/com/sorrowmist/useless/content/menus/PagedRecoverableMenu.java new file mode 100644 index 00000000..6f978669 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/menus/PagedRecoverableMenu.java @@ -0,0 +1,373 @@ +package com.sorrowmist.useless.content.menus; + +import com.sorrowmist.useless.content.blockentities.PagedMenuPageMemory; +import com.sorrowmist.useless.content.blockentities.RecoverableItemStackHandler; +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraftforge.items.IItemHandler; +import net.minecraftforge.items.IItemHandlerModifiable; +import net.minecraftforge.items.ItemStackHandler; +import net.minecraftforge.items.SlotItemHandler; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; +import java.util.function.IntSupplier; + +public class PagedRecoverableMenu extends AbstractContainerMenu { + /** Ten rows keep a full 4096-slot inventory practical while retaining paging. */ + public static final int SLOTS_PER_PAGE = 90; + public static final int PREVIOUS_PAGE = 0; + public static final int NEXT_PAGE = 1; + + private final RecoverableItemStackHandler inventory; + private final BlockPos blockPos; + private final Player player; + private final boolean clientSide; + @Nullable + private final PagedMenuPageMemory pageMemory; + private int page; + private int syncedPageCount = 1; + private int syncedActivePageCount = 1; + private final ContainerData pageData = new ContainerData() { + @Override + public int get(int index) { + return switch (index) { + case 0 -> getPage(); + case 1 -> clientSide ? syncedPageCount : calculatePageCount(); + case 2 -> clientSide ? syncedActivePageCount : calculateActivePageCount(); + default -> 0; + }; + } + + @Override + public void set(int index, int value) { + if (index == 0) page = Math.max(0, value); + else if (index == 1) syncedPageCount = Math.max(1, value); + else if (index == 2) syncedActivePageCount = Math.max(1, value); + } + + @Override + public int getCount() { + return 3; + } + }; + + protected PagedRecoverableMenu(MenuType type, int containerId, Inventory playerInventory, + RecoverableItemStackHandler inventory, BlockPos blockPos) { + this(type, containerId, playerInventory, inventory, blockPos, + 8, 32, 8, 225, 8, 283, null); + } + + protected PagedRecoverableMenu(MenuType type, int containerId, Inventory playerInventory, + RecoverableItemStackHandler inventory, BlockPos blockPos, + @Nullable PagedMenuPageMemory pageMemory) { + this(type, containerId, playerInventory, inventory, blockPos, + 8, 32, 8, 225, 8, 283, pageMemory); + } + + protected PagedRecoverableMenu(MenuType type, int containerId, Inventory playerInventory, + RecoverableItemStackHandler inventory, BlockPos blockPos, + int storageX, int storageY, + int playerInventoryX, int playerInventoryY, + int hotbarX, int hotbarY) { + this(type, containerId, playerInventory, inventory, blockPos, + storageX, storageY, playerInventoryX, playerInventoryY, hotbarX, hotbarY, null); + } + + protected PagedRecoverableMenu(MenuType type, int containerId, Inventory playerInventory, + RecoverableItemStackHandler inventory, BlockPos blockPos, + int storageX, int storageY, + int playerInventoryX, int playerInventoryY, + int hotbarX, int hotbarY, + @Nullable PagedMenuPageMemory pageMemory) { + super(type, containerId); + this.inventory = inventory; + this.blockPos = blockPos.immutable(); + this.player = playerInventory.player; + this.clientSide = player.level().isClientSide; + this.pageMemory = pageMemory; + int rememberedPage = pageMemory == null || clientSide + ? 0 + : pageMemory.get(player.getUUID()); + this.page = rememberedPage; + if (!clientSide) { + this.page = getPage(); + if (this.page != rememberedPage) { + rememberPage(); + } + } + IItemHandler pageView = new PageView(inventory, () -> page, clientSide); + for (int row = 0; row < 10; row++) { + for (int column = 0; column < 9; column++) { + addSlot(new SlotItemHandler(pageView, column + row * 9, + storageX + column * 18, storageY + row * 18)); + } + } + for (int row = 0; row < 3; row++) { + for (int column = 0; column < 9; column++) { + addSlot(new Slot(playerInventory, column + row * 9 + 9, + playerInventoryX + column * 18, playerInventoryY + row * 18)); + } + } + for (int column = 0; column < 9; column++) { + addSlot(new Slot(playerInventory, column, hotbarX + column * 18, hotbarY)); + } + addDataSlots(pageData); + } + + public BlockPos getBlockPos() { + return blockPos; + } + + public int getPage() { + return Math.min(page, Math.max(0, getPageCount() - 1)); + } + + public int getPageCount() { + return clientSide ? syncedPageCount : calculatePageCount(); + } + + private int calculatePageCount() { + int highest = inventory.getActiveSlots() - 1; + for (int slot = inventory.getSlots() - 1; slot >= inventory.getActiveSlots(); slot--) { + if (!inventory.getStackInSlot(slot).isEmpty()) { + highest = slot; + break; + } + } + return Math.max(1, highest / SLOTS_PER_PAGE + 1); + } + + public int getActivePageCount() { + return clientSide ? syncedActivePageCount : calculateActivePageCount(); + } + + /** + * 后备库存中的活跃槽位数,即非恢复槽位数量。 + * + *

搜索只应覆盖可正常回填的活跃槽位;超出该数量的槽位是只读恢复区,客户端据此截断 + * 参与过滤的槽位范围。

+ */ + public int getActiveSlotCount() { + return inventory.getActiveSlots(); + } + + /** + * 当前页内指定菜单槽位是否属于只读恢复区。 + * + *

恢复槽位超出后备库存的活跃槽位数量,只允许取出,放入会被拒绝。界面据此选用未启用样式 + * 绘制底板,使玩家在交互前即可分辨。参数以菜单槽位序号计,样板存储区之外的槽位 + * (玩家背包与快捷栏)返回 {@code false}。

+ */ + public boolean isRecoverySlot(int menuSlot) { + if (menuSlot < 0 || menuSlot >= SLOTS_PER_PAGE) return false; + return getPage() * SLOTS_PER_PAGE + menuSlot >= getActiveSlotCount(); + } + + private int calculateActivePageCount() { + return Math.max(1, (inventory.getActiveSlots() + SLOTS_PER_PAGE - 1) / SLOTS_PER_PAGE); + } + + public boolean isRecoveryPage() { + return getPage() >= getActivePageCount(); + } + + /** + * 当前是否处于搜索高亮状态。 + * + *

服务端只把当前页内容写进同步槽位,因此搜索在客户端完成,菜单只记录该状态供屏幕查询。 + * 槽位本身始终绑定服务端同步的原槽位,高亮不改变排布,交互不受影响。

+ */ + private boolean searchActive; + + /** 设置当前是否处于搜索高亮状态。 */ + public void setSearchActive(boolean active) { + searchActive = active; + } + + /** 当前是否处于搜索高亮状态。 */ + public boolean isFiltered() { + return searchActive; + } + + @Override + public boolean clickMenuButton(Player player, int id) { + if (id != PREVIOUS_PAGE && id != NEXT_PAGE) return false; + int count = getPageCount(); + page = Math.floorMod(getPage() + (id == NEXT_PAGE ? 1 : -1), count); + rememberPage(); + broadcastChanges(); + return true; + } + + @Override + public void broadcastChanges() { + // A configuration or coil-tier reduction can remove pages while a + // viewer has one selected. Keep the server-side page in range before + // slot synchronization maps the page view onto backing storage. + if (!clientSide) { + int clampedPage = getPage(); + if (page != clampedPage) { + page = clampedPage; + rememberPage(); + } + } + super.broadcastChanges(); + } + + private void rememberPage() { + if (!clientSide && pageMemory != null) { + pageMemory.set(player.getUUID(), getPage()); + } + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + if (index < 0 || index >= slots.size()) return ItemStack.EMPTY; + Slot slot = slots.get(index); + if (!slot.hasItem()) return ItemStack.EMPTY; + ItemStack source = slot.getItem(); + ItemStack copy = source.copy(); + if (index < SLOTS_PER_PAGE) { + if (!moveItemStackTo(source, SLOTS_PER_PAGE, slots.size(), true)) return ItemStack.EMPTY; + } else { + boolean moved = clientSide + ? !isRecoveryPage() && moveItemStackTo(source, 0, SLOTS_PER_PAGE, false) + : !isRecoveryPage() + && insertIntoActiveSlots(inventory, source, getPage() * SLOTS_PER_PAGE); + if (!moved) return ItemStack.EMPTY; + } + if (source.isEmpty()) slot.set(ItemStack.EMPTY); else slot.setChanged(); + if (source.getCount() == copy.getCount()) return ItemStack.EMPTY; + slot.onTake(player, source); + return copy; + } + + static boolean insertIntoActiveSlots( + RecoverableItemStackHandler inventory, ItemStack stack, int firstSlot) { + if (stack.isEmpty()) return false; + int activeSlots = inventory.getActiveSlots(); + if (activeSlots <= 0) return false; + + ItemStack remaining = stack.copy(); + int first = Math.floorMod(firstSlot, activeSlots); + // Match existing stacks before claiming empty slots, like vanilla quick-move. + final ItemStack[] remainingHolder = {remaining}; + inventory.withChangeBatch(() -> { + int firstPass = stack.getMaxStackSize() <= 1 ? 1 : 0; + for (int pass = firstPass; pass < 2 && !remainingHolder[0].isEmpty(); pass++) { + boolean emptySlots = pass == 1; + for (int offset = 0; offset < activeSlots && !remainingHolder[0].isEmpty(); offset++) { + int slot = (first + offset) % activeSlots; + ItemStack existing = inventory.getStackInSlot(slot); + if (existing.isEmpty() != emptySlots) continue; + // An occupied slot containing another stack cannot accept this item. Avoid + // invoking the handler validator for every unrelated occupied slot. + if (!existing.isEmpty() + && !ItemStack.isSameItemSameTags(existing, remainingHolder[0])) { + continue; + } + remainingHolder[0] = inventory.insertItem(slot, remainingHolder[0], false); + } + } + }); + remaining = remainingHolder[0]; + + int inserted = stack.getCount() - remaining.getCount(); + if (inserted <= 0) return false; + stack.shrink(inserted); + return true; + } + + @Override + public boolean stillValid(Player player) { + BlockEntity blockEntity = player.level().getBlockEntity(blockPos); + return blockEntity != null && blockEntity.getBlockPos().equals(blockPos) + && player.distanceToSqr(blockPos.getX() + 0.5D, blockPos.getY() + 0.5D, blockPos.getZ() + 0.5D) <= 64.0D; + } + + /** + * SlotItemHandler writes initial/container updates through + * IItemHandlerModifiable. On the server this view follows the selected + * backing page; on the client it is a fixed 90-slot packet buffer. The + * latter is deliberately independent of {@link #page}, because page-data + * and slot-data packets are allowed to arrive in either order. + */ + static final class PageView implements IItemHandlerModifiable { + private final RecoverableItemStackHandler inventory; + private final IntSupplier page; + @Nullable + private final ItemStackHandler clientPage; + + PageView(RecoverableItemStackHandler inventory, IntSupplier page, boolean clientSide) { + this.inventory = Objects.requireNonNull(inventory, "inventory"); + this.page = Objects.requireNonNull(page, "page"); + this.clientPage = clientSide ? new ItemStackHandler(SLOTS_PER_PAGE) : null; + } + + private int actual(int slot) { + return page.getAsInt() * SLOTS_PER_PAGE + slot; + } + + @Override + public int getSlots() { return SLOTS_PER_PAGE; } + + @Override + public ItemStack getStackInSlot(int slot) { + if (clientPage != null) { + return clientPage.getStackInSlot(slot); + } + int actual = actual(slot); + return actual < inventory.getSlots() ? inventory.getStackInSlot(actual) : ItemStack.EMPTY; + } + + @Override + public ItemStack insertItem(int slot, ItemStack stack, boolean simulate) { + if (clientPage != null) { + return clientPage.insertItem(slot, stack, simulate); + } + int actual = actual(slot); + return actual < inventory.getSlots() ? inventory.insertItem(actual, stack, simulate) : stack; + } + + @Override + public ItemStack extractItem(int slot, int amount, boolean simulate) { + if (clientPage != null) { + return clientPage.extractItem(slot, amount, simulate); + } + int actual = actual(slot); + return actual < inventory.getSlots() ? inventory.extractItem(actual, amount, simulate) : ItemStack.EMPTY; + } + + @Override + public int getSlotLimit(int slot) { + int actual = actual(slot); + return actual < inventory.getSlots() ? inventory.getSlotLimit(actual) : 0; + } + + @Override + public boolean isItemValid(int slot, ItemStack stack) { + int actual = actual(slot); + return actual < inventory.getSlots() && inventory.isItemValid(actual, stack); + } + + @Override + public void setStackInSlot(int slot, ItemStack stack) { + if (clientPage != null) { + clientPage.setStackInSlot(slot, stack); + return; + } + int actual = actual(slot); + if (actual < inventory.getSlots()) { + inventory.setStackInSlot(actual, stack); + } + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/menus/PassiveCraftingHatchMenu.java b/src/main/java/com/sorrowmist/useless/content/menus/PassiveCraftingHatchMenu.java new file mode 100644 index 00000000..67c7fd8c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/menus/PassiveCraftingHatchMenu.java @@ -0,0 +1,163 @@ +package com.sorrowmist.useless.content.menus; + +import com.sorrowmist.useless.content.blockentities.PagedMenuPageMemory; +import com.sorrowmist.useless.content.blockentities.RecoverableItemStackHandler; +import com.sorrowmist.useless.content.blockentities.multiblock.PassiveCraftingHatchBlockEntity; +import com.sorrowmist.useless.init.ModMenuType; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.SimpleContainerData; +import net.minecraft.world.inventory.Slot; +import org.jetbrains.annotations.Nullable; + +/** Paginated passive-pattern inventory with the hatch's interval controls. */ +public final class PassiveCraftingHatchMenu extends PagedRecoverableMenu { + private final @Nullable PassiveCraftingHatchBlockEntity hatch; + private final ContainerData data; + private final PassiveCraftingHatchBlockEntity.SlotStatus[] slotStatuses = + new PassiveCraftingHatchBlockEntity.SlotStatus[PagedRecoverableMenu.SLOTS_PER_PAGE]; + + public PassiveCraftingHatchMenu(int containerId, Inventory inventory, FriendlyByteBuf buffer) { + this(containerId, inventory, buffer.readBlockPos()); + } + + public PassiveCraftingHatchMenu(int containerId, Inventory inventory, BlockPos pos) { + super(ModMenuType.PASSIVE_CRAFTING_HATCH_MENU.get(), containerId, inventory, + handler(inventory, pos), pos, 8, 22, 44, 254, 44, 312, + pageMemory(inventory, pos)); + hatch = inventory.player.level().getBlockEntity(pos) + instanceof PassiveCraftingHatchBlockEntity found ? found : null; + ContainerData liveData = hatch == null + ? new SimpleContainerData(PassiveCraftingHatchBlockEntity.MENU_DATA_COUNT) + : hatch.getMenuData(); + data = createMenuData(inventory.player.level().isClientSide, liveData); + addDataSlots(data); + for (int slot = 0; slot < slotStatuses.length; slot++) { + slotStatuses[slot] = emptyStatus(slot); + } + } + + private static RecoverableItemStackHandler handler(Inventory inventory, BlockPos pos) { + if (inventory.player.level().getBlockEntity(pos) instanceof PassiveCraftingHatchBlockEntity hatch) { + return hatch.getPatterns(); + } + return new RecoverableItemStackHandler(PassiveCraftingHatchBlockEntity.MAX_PATTERN_SLOTS, 0, + () -> 0, stack -> false, () -> { }); + } + + @Nullable + private static PagedMenuPageMemory pageMemory(Inventory inventory, BlockPos pos) { + return inventory.player.level().getBlockEntity(pos) instanceof PassiveCraftingHatchBlockEntity hatch + ? hatch.getPageMemory() : null; + } + + static ContainerData createMenuData(boolean clientSide, ContainerData liveData) { + return clientSide + ? new SimpleContainerData(PassiveCraftingHatchBlockEntity.MENU_DATA_COUNT) + : liveData; + } + + @Nullable + public PassiveCraftingHatchBlockEntity getHatch() { + return hatch; + } + + public boolean isFormed() { + return data.get(0) != 0; + } + + public int getCoilTier() { + return data.get(1); + } + + public int getActivePatternSlots() { + return Math.max(0, Math.min(PassiveCraftingHatchBlockEntity.MAX_PATTERN_SLOTS, data.get(2))); + } + + public int getConfiguredPatternSlots() { + return Math.max(1, Math.min(PassiveCraftingHatchBlockEntity.MAX_PATTERN_SLOTS, data.get(9))); + } + + public int getIntervalTicks() { + return data.get(3); + } + + public int getCountdownTicks() { + return data.get(4); + } + + public long getMultiplier() { + return join(data.get(5), data.get(6)); + } + + /** {@code 0} means the slot follows {@link #getMultiplier()}; any other value is an override. */ + public long getSlotMultiplierOverride(int slot) { + return slot < 0 ? 0L : getSlotStatus(slot).multiplier(); + } + + public boolean hasOwnSlotMultiplier(int slot) { + return getSlotMultiplierOverride(slot) > 0L; + } + + /** Batch size the screen should display for a slot while the server snapshot is in flight. */ + public long getSlotMultiplier(int slot) { + long override = getSlotMultiplierOverride(slot); + return override > 0L ? override : Math.max(1L, getMultiplier()); + } + + public long getMaxMultiplier() { + return Math.max(1L, join(data.get(7), data.get(8))); + } + + static long join(int low, int high) { + return Integer.toUnsignedLong(low) | (long) high << 32; + } + + public int getPatternSlotIndex(Slot slot) { + int menuSlot = slots.indexOf(slot); + return menuSlot >= 0 && menuSlot < SLOTS_PER_PAGE + ? getPage() * SLOTS_PER_PAGE + menuSlot : -1; + } + + public PassiveCraftingHatchBlockEntity.SlotStatus getSlotStatus(int slot) { + int relative = Math.floorMod(slot, PagedRecoverableMenu.SLOTS_PER_PAGE); + if (slot < 0 || relative >= slotStatuses.length) { + return emptyStatus(slot); + } + PassiveCraftingHatchBlockEntity.SlotStatus status = slotStatuses[relative]; + return status.slot() == slot ? status : emptyStatus(slot); + } + + public void updateSlotStatuses(Iterable statuses) { + for (PassiveCraftingHatchBlockEntity.SlotStatus status : statuses) { + int relative = Math.floorMod(status.slot(), PagedRecoverableMenu.SLOTS_PER_PAGE); + if (relative >= 0 && relative < slotStatuses.length) { + slotStatuses[relative] = status; + } + } + } + + @Override + public boolean clickMenuButton(Player player, int id) { + boolean changed = super.clickMenuButton(player, id); + if (changed && hatch != null) { + hatch.requestStatusSync(); + } + return changed; + } + + @Override + public boolean stillValid(Player player) { + return hatch != null && player.level().getBlockEntity(getBlockPos()) == hatch + && player.distanceToSqr(getBlockPos().getX() + 0.5D, getBlockPos().getY() + 0.5D, + getBlockPos().getZ() + 0.5D) <= 64.0D; + } + + private static PassiveCraftingHatchBlockEntity.SlotStatus emptyStatus(int slot) { + return new PassiveCraftingHatchBlockEntity.SlotStatus( + slot, PassiveCraftingHatchBlockEntity.SlotState.EMPTY, 0, 0, ""); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/menus/StaffLinkMenu.java b/src/main/java/com/sorrowmist/useless/content/menus/StaffLinkMenu.java new file mode 100644 index 00000000..469c8f05 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/menus/StaffLinkMenu.java @@ -0,0 +1,602 @@ +package com.sorrowmist.useless.content.menus; + +import com.sorrowmist.useless.content.stafflink.LinkFlow; +import com.sorrowmist.useless.content.stafflink.LinkFilterCondition; +import com.sorrowmist.useless.content.stafflink.LinkFilterSlot; +import com.sorrowmist.useless.content.stafflink.LinkMedium; +import com.sorrowmist.useless.content.stafflink.ResourceFamily; +import com.sorrowmist.useless.content.stafflink.StaffLinkRoute; +import com.sorrowmist.useless.init.ModMenuType; +import com.sorrowmist.useless.network.StaffLinkConfigurePacket; +import com.sorrowmist.useless.network.StaffLinkCyclePacket; +import com.sorrowmist.useless.network.StaffLinkDetachPacket; +import com.sorrowmist.useless.network.StaffLinkNetworkPacket; +import com.sorrowmist.useless.network.StaffLinkRenamePacket; +import com.sorrowmist.useless.network.StaffLinkReorderPacket; +import com.sorrowmist.useless.world.stafflink.StaffLinkManager; +import com.sorrowmist.useless.world.stafflink.StaffLinkNetwork; +import net.minecraft.core.GlobalPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * 无线物流的配置界面容器。 + * + *

它背后没有方块实体:网络状态存在服务端的 {@code StaffLinkSavedData} 里,客户端拿到的是 + * {@link #receiveSync} 下发的一份快照。界面上所有编辑都发成命令包,服务端校验后回发新快照, + * 因此服务端永远是唯一权威。

+ * + *

真实槽位只有玩家背包。过滤器槽与锚点列表都是纯绘制控件(由 {@code StaffLinkScreen} + * 负责绘制与命中检测),不走原版的槽位点击管线——它们只是「编辑某个字段的入口」, + * 不需要参与物品搬运。

+ */ +public final class StaffLinkMenu extends AbstractContainerMenu { + private static final int PLAYER_INVENTORY_X = 44; + /** + * 背包第一行的 y。 + * + *

过滤槽不画在主界面上(改成覆盖层里的面板),所以面板高度维持原值, + * 背包位置也就不必下移。

+ */ + private static final int PLAYER_INVENTORY_Y = 254; + private static final int PLAYER_HOTBAR_Y = 312; + + private UUID networkId; + private final boolean clientSide; + + @Nullable + private StaffLinkNetwork snapshot; + /** 当前网络在归属者列表里的位置(由同步包带来,客户端没有那份列表)。 */ + private int networkIndex; + private int networkCount = 1; + private List filterMirror = emptyFilter(); + + @Nullable + private GlobalPos selectedAnchor; + private int selectedRoute; + + /** + * 多选出来的锚点(批量编辑用)。 + * + *

它和 {@link #selectedAnchor} 是两套东西:单选锚点决定右边配置区显示谁的配置, + * 多选集合决定「一次改动要写到哪些容器上」。集合为空时只改单选那一个。

+ */ + private final List multiSelection = new ArrayList<>(); + + /** 服务端构造:直接绑定到存档里的那张网络。 */ + public StaffLinkMenu(int containerId, Inventory inventory, UUID networkId) { + super(ModMenuType.STAFF_LINK_MENU.get(), containerId); + this.networkId = networkId; + this.clientSide = inventory.player.level().isClientSide(); + MinecraftServer server = inventory.player.level().getServer(); + this.snapshot = server == null ? null : StaffLinkManager.networkById(server, networkId); + addPlayerInventory(inventory); + } + + /** 客户端构造:网络 ID 随开界面的上下文一起下发,快照由同步包补上。 */ + public StaffLinkMenu(int containerId, Inventory inventory, FriendlyByteBuf buffer) { + this(containerId, inventory, buffer.readUUID()); + } + + private void addPlayerInventory(Inventory inventory) { + for (int row = 0; row < 3; row++) { + for (int column = 0; column < 9; column++) { + addSlot(new Slot(inventory, column + row * 9 + 9, + PLAYER_INVENTORY_X + column * 18, PLAYER_INVENTORY_Y + row * 18)); + } + } + for (int column = 0; column < 9; column++) { + addSlot(new Slot(inventory, column, PLAYER_INVENTORY_X + column * 18, PLAYER_HOTBAR_Y)); + } + } + + private static List emptyFilter() { + List filter = new ArrayList<>(StaffLinkRoute.FILTER_LIMIT); + for (int i = 0; i < StaffLinkRoute.FILTER_LIMIT; i++) { + filter.add(LinkFilterSlot.EMPTY); + } + return filter; + } + + // ------------------------------------------------------------------ 状态 + + public UUID getNetworkId() { + return networkId; + } + + /** + * 服务端专用:把界面绑定的网络换成另一张。 + * + *

玩家在界面里切网络({@code <}/{@code >}、新建、解散)改的是归属者(玩家或队伍) + * 名下的「当前网络」,而后续所有编辑包都靠服务端菜单里的这个 ID 寻址。不同步它, + * 切到 B 之后的编辑仍会打到 A 上——表现就是「在 B 里点一下,界面跳回 A」。

+ */ + public void setNetworkId(@Nullable UUID networkId) { + if (networkId != null && !networkId.equals(this.networkId)) { + this.networkId = networkId; + this.selectedAnchor = null; + this.snapshot = null; + this.filterMirror = emptyFilter(); + } + } + + @Nullable + public StaffLinkNetwork getSnapshot() { + return snapshot; + } + + /** 已绑定的锚点列表(快照为空时为空表)。 */ + public List getAnchors() { + return snapshot == null ? List.of() : snapshot.anchors(); + } + + @Nullable + public GlobalPos getSelectedAnchor() { + return selectedAnchor; + } + + public int getSelectedRoute() { + return selectedRoute; + } + + /** 当前选中锚点 + 线路对应的配置;未选中或该线路还没有配置时返回 {@code null}。 */ + @Nullable + public StaffLinkRoute getSelectedConfig() { + if (snapshot == null || selectedAnchor == null) { + return null; + } + return snapshot.routeAt(selectedAnchor, selectedRoute); + } + + /** 任意「锚点 × 线路」的配置;列表里给每个容器标出流向用。 */ + @Nullable + public StaffLinkRoute getConfig(GlobalPos anchor, int route) { + return snapshot == null ? null : snapshot.routeAt(anchor, route); + } + + public boolean isAnchorBound(GlobalPos anchor) { + return snapshot != null && snapshot.isBound(anchor); + } + + /** 过滤器显示用的镜像(长度固定为 {@link StaffLinkRoute#FILTER_LIMIT})。 */ + public List getFilterMirror() { + return filterMirror; + } + + /** 当前选中线路的资源类型;没有选中配置时返回 {@code null}。 */ + @Nullable + public LinkMedium getSelectedMedium() { + StaffLinkRoute config = getSelectedConfig(); + return config == null ? null : config.medium(); + } + + public boolean isFilterActive() { + StaffLinkRoute config = getSelectedConfig(); + return config != null && config.filterApplies(); + } + + /** 客户端收到服务端快照。 */ + public void receiveSync(StaffLinkNetwork network, int index, int count) { + // 服务端可能刚切到另一张网络(新建 / 解散),界面跟着走。 + this.networkId = network.id(); + this.snapshot = network; + this.networkIndex = Math.max(0, index); + this.networkCount = Math.max(1, count); + if (selectedAnchor != null && !network.isBound(selectedAnchor)) { + // 选中的锚点已经不在新快照里了。走 setSelection 而不是直接清字段:它顺手把过滤器 + // 镜像一起刷新了。 + setSelection(null, selectedRoute); + } else { + refreshFilterMirror(); + } + } + + /** 当前网络在该归属者网络列表里的下标(0 起)。 */ + public int getNetworkIndex() { + return networkIndex; + } + + /** 该归属者名下共有几张网络。 */ + public int getNetworkCount() { + return networkCount; + } + + /** + * 界面切换选中锚点 / 线路。 + * + *

纯客户端状态:右边配置区显示谁的配置,以及过滤槽镜像的是谁,都由它决定。 + * 服务端不需要知道——搬运引擎按存档里的网络配置自己跑,与玩家在看哪个容器无关。

+ */ + public void setSelection(@Nullable GlobalPos anchor, int route) { + int clamped = Math.max(0, Math.min(route, StaffLinkNetwork.ROUTE_COUNT - 1)); + this.selectedAnchor = anchor; + this.selectedRoute = clamped; + refreshFilterMirror(); + } + + /** 为某个还没有配置的「锚点 × 线路」生成一条默认配置。 */ + public StaffLinkRoute defaultRouteFor(GlobalPos anchor, int route) { + LinkMedium medium = LinkMedium.ITEM; + if (snapshot != null) { + for (StaffLinkRoute existing : snapshot.routes()) { + if (existing.anchor().equals(anchor)) { + medium = existing.medium(); + break; + } + } + } + LinkFlow flow = snapshot != null && snapshot.hasReleaseRoute() ? LinkFlow.ABSORB : LinkFlow.RELEASE; + // 应力线路的两个数值是「目标转速 / 旋转方向」,语义与别的介质不同,初值也要跟着换; + // 沿用 16 / 5 的话,那个 5 会被服务端当成非法方向收敛成 1,玩家会看到配置被悄悄改掉。 + boolean stress = medium.family() == ResourceFamily.STRESS; + // 气压线路的「数量」是「目标气压(毫巴)」:默认 2 bar,同样不能沿用 16(那是 0.016 bar)。 + boolean pressure = medium.family() == ResourceFamily.PRESSURE; + // 新线路默认关闭,由玩家显式打开。 + return new StaffLinkRoute(anchor, route, false, flow, medium, + stress ? StaffLinkRoute.STRESS_DEFAULT_RPM + : pressure ? StaffLinkRoute.PRESSURE_DEFAULT_MBAR : 16, + stress ? StaffLinkRoute.STRESS_CLOCKWISE : 5, + null, 0, List.of()); + } + + /** 当前网络名(没起过名时为空串)。 */ + public String getNetworkName() { + return snapshot == null ? "" : snapshot.name(); + } + + /** 给当前网络改名。 */ + public void renameNetwork(String name) { + String safe = name == null ? "" : name; + if (snapshot != null) { + snapshot.setName(safe); + } + if (clientSide) { + PacketDistributor.sendToServer( + new StaffLinkNetworkPacket(StaffLinkNetworkPacket.Action.RENAME, safe)); + } + } + + /** 新建一张网络并切过去;服务端会回发新网络的快照。 */ + public void createNetwork() { + if (clientSide) { + PacketDistributor.sendToServer( + new StaffLinkNetworkPacket(StaffLinkNetworkPacket.Action.NEW, "")); + } + } + + /** 切换当前网络(界面上的 < / > 按钮);服务端会回发新网络的快照。 */ + public void cycleNetwork(int delta) { + if (clientSide) { + PacketDistributor.sendToServer(new StaffLinkCyclePacket(delta)); + } + } + + /** 解散当前网络;服务端会切到相邻的一张并回发快照,一张不剩时会关掉界面。 */ + public void dissolveNetwork() { + if (clientSide) { + PacketDistributor.sendToServer( + new StaffLinkNetworkPacket(StaffLinkNetworkPacket.Action.DISSOLVE, "")); + } + } + + /** + * 写入/覆盖配置,并提交给服务端。 + * + *

多选为空时只写 {@code route} 自己那一条;多选非空时写到所有被选锚点的同号线路上。 + * 具体写法见 {@link #applyRouteTo}。

+ */ + public void applyRoute(StaffLinkRoute route) { + applyRouteTo(getEditTargets(), route); + } + + /** + * 把一条配置写到给定的一批锚点上。 + * + *

目标已有该线路配置时走 {@link #patch},没有时用 {@link #copyFor} 整条补上。

+ * + *

语义是整条覆盖:{@code patch(base, edited)} 逐字段取「与 edited 不同就采用 edited」, + * 而相等的字段取谁都一样,所以结果恒等于 {@code edited} 的字段——即目标的介质、方向、 + * 过滤器等都会被刷成 {@code edited} 的值。批量改「流量」时,各机原本不同的介质/方向 + * 会一起被抹平,这是有意的:批量编辑的期望就是「让这批机器一致」。

+ * + *

本方法不读取也不修改 {@link #multiSelection},所以「应用到全部」可以传一份 + * 临时目标列表进来,不会改变玩家当前的多选状态。

+ */ + public void applyRouteTo(List targets, StaffLinkRoute route) { + for (GlobalPos target : targets) { + StaffLinkRoute existing = snapshot == null ? null : snapshot.routeAt(target, route.route()); + StaffLinkRoute written = existing == null + ? copyFor(target, route) + : patch(existing, route); + if (snapshot != null) { + snapshot.putRoute(written); + } + if (clientSide) { + PacketDistributor.sendToServer(new StaffLinkConfigurePacket( + target, written.route(), written)); + } + } + refreshFilterMirror(); + } + + /** 把一条配置原样搬到另一个锚点上(用于给还没有配置的目标补默认值)。 */ + private static StaffLinkRoute copyFor(GlobalPos anchor, StaffLinkRoute route) { + return new StaffLinkRoute(anchor, route.route(), route.enabled(), route.flow(), route.medium(), + route.amount(), route.interval(), route.side(), route.weight(), + route.filter()); + } + + /** + * 逐字段取「与 {@code edited} 不同的那个值」。 + * + *

注意:相等的字段取 {@code base} 还是 {@code edited} 结果一样,所以本方法实际上 + * 等价于整条覆盖(只保留 {@code base} 的锚点与线路号)。保留这个写法是为了 + * 与 {@link #copyFor} 共用同一个「逐字段构造」的形状,别指望它能实现字段级 diff。

+ */ + private static StaffLinkRoute patch(StaffLinkRoute base, StaffLinkRoute edited) { + return new StaffLinkRoute( + base.anchor(), base.route(), + base.enabled() != edited.enabled() ? edited.enabled() : base.enabled(), + base.flow() != edited.flow() ? edited.flow() : base.flow(), + base.medium() != edited.medium() ? edited.medium() : base.medium(), + base.amount() != edited.amount() ? edited.amount() : base.amount(), + base.interval() != edited.interval() ? edited.interval() : base.interval(), + base.side() != edited.side() ? edited.side() : base.side(), + base.weight() != edited.weight() ? edited.weight() : base.weight(), + base.filter().equals(edited.filter()) ? base.filter() : edited.filter()); + } + + /** + * 只写一个锚点的配置,不参与批量。 + * + *

给「刚选中一台还没配过的机器、顺手补一条默认配置」用。这种补默认值的动作必须 + * 限定在单选那一台上:否则多选期间点一下别的机器,就会把一堆默认值糊到所有选中的机器上。

+ */ + public void applyRouteSingle(GlobalPos anchor, StaffLinkRoute route) { + if (snapshot != null) { + snapshot.putRoute(route); + } + if (clientSide) { + PacketDistributor.sendToServer(new StaffLinkConfigurePacket(anchor, route.route(), route)); + } + } + + // ------------------------------------------------------------------ 多选 + + /** 当前多选出来的锚点(只读视图)。 */ + public List getMultiSelection() { + return List.copyOf(multiSelection); + } + + public boolean isMultiSelected(GlobalPos anchor) { + return multiSelection.contains(anchor); + } + + /** 点一下加/减一个锚点;被减掉的正好是单选那个时,把单选让给集合里的下一个。 */ + public void toggleMultiSelection(GlobalPos anchor) { + if (!multiSelection.remove(anchor)) { + multiSelection.add(anchor); + } + if (selectedAnchor != null && !multiSelection.isEmpty() + && !multiSelection.contains(selectedAnchor)) { + selectedAnchor = multiSelection.get(0); + refreshFilterMirror(); + } + } + + public void clearMultiSelection() { + multiSelection.clear(); + } + + /** + * 直接把批量选择集合替换成给定的一组锚点(「全选」/ Shift 范围选择用)。 + * + *

去重;若当前单选锚点不在新集合里(或还没有单选),把单选让给集合的第一个, + * 保证配置区显示的永远是集合内的机器。

+ */ + public void setMultiSelection(List anchors) { + multiSelection.clear(); + for (GlobalPos anchor : anchors) { + if (anchor != null && !multiSelection.contains(anchor)) { + multiSelection.add(anchor); + } + } + if (!multiSelection.isEmpty() + && (selectedAnchor == null || !multiSelection.contains(selectedAnchor))) { + selectedAnchor = multiSelection.get(0); + refreshFilterMirror(); + } + } + + /** 解绑时把它从多选里一并摘掉,免得集合里留着一个不存在的锚点。 */ + private void forgetMultiSelection(GlobalPos anchor) { + multiSelection.remove(anchor); + } + + /** + * 一次编辑要落到哪些锚点上。 + * + *

多选为空 = 只改单选那个;多选非空 = 改所有被选的。多选里如果没包含单选锚点, + * 单选那个不参与批量(界面上的配置区显示的是它,但改动只发给被选中的容器)。

+ */ + public List getEditTargets() { + if (!multiSelection.isEmpty()) { + return List.copyOf(multiSelection); + } + return selectedAnchor == null ? List.of() : List.of(selectedAnchor); + } + + /** 改一个过滤器槽;没有选中配置或该线路不用过滤器时忽略。 */ + public void setFilterSlot(int index, LinkFilterSlot slot) { + StaffLinkRoute config = getSelectedConfig(); + if (config == null || !config.filterApplies()) { + return; + } + if (index < 0 || index >= StaffLinkRoute.FILTER_LIMIT) { + return; + } + List filter = new ArrayList<>(config.filter()); + filter.set(index, slot == null ? LinkFilterSlot.EMPTY : slot); + applyRoute(config.withFilter(filter)); + } + + /** + * 改一个过滤器槽的「白名单 / 黑名单」方向,标记与条件不动。 + * + *

黑名单:这一格标记命中的资源不搬,其余照搬。

+ */ + public void setFilterSlotExclude(int index, boolean exclude) { + editFilterSlot(index, slot -> slot.withExclude(exclude)); + } + + /** + * 改一个过滤器槽的一条控制条件。 + * + * @param input {@code true} = 输入端条件(在目标容器上测量);{@code false} = 输出端条件(在源容器上测量) + */ + public void setFilterSlotCondition(int index, boolean input, LinkFilterCondition condition) { + editFilterSlot(index, slot -> input + ? slot.withConditions(slot.outCond(), condition) + : slot.withConditions(condition, slot.inCond())); + } + + /** + * 改一条控制条件的「控制材料 B」。 + * + *

传一格的标记:物品 / 流体直接当 B;空标记 = 清掉 B(回到「测 A 自身」)。方向与数值保留。 + * 模式({@code #tag})不能当控制材料,忽略。

+ */ + public void setFilterSlotConditionControl(int index, boolean input, LinkFilterSlot marker) { + editFilterSlot(index, slot -> { + LinkFilterCondition current = input ? slot.inCond() : slot.outCond(); + LinkFilterCondition updated = applyControl(current, marker); + return input + ? slot.withConditions(slot.outCond(), updated) + : slot.withConditions(updated, slot.inCond()); + }); + } + + /** 通用:改一个过滤格;没有选中配置或该线路不用过滤器时忽略。 */ + private void editFilterSlot(int index, java.util.function.UnaryOperator change) { + StaffLinkRoute config = getSelectedConfig(); + if (config == null || !config.filterApplies()) { + return; + } + if (index < 0 || index >= StaffLinkRoute.FILTER_LIMIT) { + return; + } + List filter = new ArrayList<>(config.filter()); + LinkFilterSlot current = index < filter.size() ? filter.get(index) : LinkFilterSlot.EMPTY; + filter.set(index, change.apply(current)); + applyRoute(config.withFilter(filter)); + } + + /** 把控制材料 B 换成一格标记里的物品 / 流体;标记为空 ⇒ 清掉 B。 */ + private static LinkFilterCondition applyControl(LinkFilterCondition cond, LinkFilterSlot marker) { + if (marker == null || marker.isEmpty()) { + return cond.withoutControl(); + } + if (marker.isFluid()) { + return cond.withControl(marker.fluid()); + } + if (marker.isItem()) { + return cond.withControl(marker.item()); + } + // 模式不支持当控制材料:保持原样。 + return cond; + } + + /** 该锚点的自定义名;没起过名时返回 {@code null},界面回落到方块本名。 */ + @Nullable + public String getAnchorName(GlobalPos anchor) { + return snapshot == null ? null : snapshot.anchorName(anchor); + } + + /** 给锚点改名(空串表示恢复方块本名)。 */ + public void renameAnchor(GlobalPos anchor, String name) { + if (snapshot != null) { + snapshot.setAnchorName(anchor, name); + } + if (clientSide) { + PacketDistributor.sendToServer(new StaffLinkRenamePacket(anchor, name == null ? "" : name)); + } + } + + /** 解绑一个锚点。 */ + public void detach(GlobalPos anchor) { + if (snapshot != null) { + snapshot.detach(anchor); + } + if (anchor.equals(selectedAnchor)) { + // 走 setSelection 而不是直接改字段:它顺手把过滤器镜像一起刷新了。 + setSelection(null, selectedRoute); + } else { + refreshFilterMirror(); + } + if (clientSide) { + PacketDistributor.sendToServer(new StaffLinkDetachPacket(anchor)); + } + } + + /** + * 客户端:把某个锚点挪到列表里的指定位置。 + * + *

本地不先改:服务端改完会回发整网快照,以它为准,避免两边次序短暂不一致。

+ * + * @param targetIndex 目标位置(在完整锚点列表里的下标) + */ + public void moveAnchorTo(GlobalPos anchor, int targetIndex) { + if (clientSide) { + PacketDistributor.sendToServer(new StaffLinkReorderPacket(anchor, targetIndex)); + } + } + + private void refreshFilterMirror() { + StaffLinkRoute config = getSelectedConfig(); + if (config == null) { + filterMirror = emptyFilter(); + return; + } + List filter = new ArrayList<>(StaffLinkRoute.FILTER_LIMIT); + List source = config.filter(); + for (int index = 0; index < StaffLinkRoute.FILTER_LIMIT; index++) { + filter.add(index < source.size() ? source.get(index) : LinkFilterSlot.EMPTY); + } + filterMirror = List.copyOf(filter); + } + + // ------------------------------------------------------------------ 交互 + + @Override + public @NotNull ItemStack quickMoveStack(@NotNull Player player, int index) { + return ItemStack.EMPTY; + } + + @Override + public boolean stillValid(@NotNull Player player) { + if (!player.isAlive()) { + return false; + } + MinecraftServer server = player.level().getServer(); + if (server == null) { + // 客户端不做权威判定,交给服务端关闭。 + return true; + } + // 网络必须还在,而且必须挂在这名玩家(或他队伍)名下;否则界面该关掉。 + return player instanceof ServerPlayer serverPlayer + && StaffLinkManager.canAccess(server, serverPlayer, networkId); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/multiblock/OmniversalCoilStats.java b/src/main/java/com/sorrowmist/useless/content/multiblock/OmniversalCoilStats.java new file mode 100644 index 00000000..2ec192d1 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/multiblock/OmniversalCoilStats.java @@ -0,0 +1,109 @@ +package com.sorrowmist.useless.content.multiblock; + +import com.sorrowmist.useless.api.enums.CatalystType; +import com.sorrowmist.useless.content.blockentities.AdvancedAlloyFurnaceBlockEntity; +import com.sorrowmist.useless.content.blocks.multiblock.UselessCoilBlock; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.catalyst.CatalystEffectResolver; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.catalyst.ResolvedCatalystEffect; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.core.config.ConfigManager; + +/** Provides coil properties: catalyst behavior apart from processing time plus independent threads. */ +public record OmniversalCoilStats( + int tier, + CatalystType catalystType, + long singleTaskParallel, + int energyDivisor, + int threads, + long energyCapacity, + long maxReceive +) { + private static final OmniversalCoilStats[] BY_TIER = createStats(); + + public static OmniversalCoilStats forTier(int tier) { + if (tier < UselessCoilBlock.MIN_TIER || tier > UselessCoilBlock.MAX_TIER) { + throw new IllegalArgumentException("Unsupported omniversal coil tier: " + tier); + } + return BY_TIER[tier - UselessCoilBlock.MIN_TIER]; + } + + @Override + public long singleTaskParallel() { + return tier == UselessCoilBlock.USEFUL_TIER + ? Long.MAX_VALUE + : ConfigManager.getOmniversalCoilSingleTaskParallel(tier); + } + + @Override + public int energyDivisor() { + return tier == UselessCoilBlock.USEFUL_TIER + ? 1 << tier + : ConfigManager.getOmniversalCoilEnergyDivisor(tier); + } + + @Override + public int threads() { + return tier == UselessCoilBlock.USEFUL_TIER + ? ConfigManager.getOmniversalUsefulTierThreads() + : ConfigManager.getOmniversalCoilThreads(tier); + } + + public int processTime(int baseTime) { + if (tier == UselessCoilBlock.USEFUL_TIER) { + return 1; + } + long normalizedBaseTime = Math.max(1, baseTime); + double multiplier = ConfigManager.getOmniversalCoilTimeMultiplier(tier); + return Math.max(1, (int) Math.ceil(normalizedBaseTime * multiplier)); + } + + /** + * 解析本档次线圈在某个配方上的运行参数。 + * + *

不变式:有用线圈({@link UselessCoilBlock#USEFUL_TIER})的 + * {@code energyMultipliesWithParallel} 必须保持 {@code false}。

+ * + *

它来自 {@code CatalystEffectResolver} 的 {@code !isUsefulIngot()},而本档次(tier 10)的 + * {@code catalystType} 正是 {@link CatalystType#USEFUL_INGOT},于是整批只收一次固定能耗 + * ({@code ceil(recipeEnergy / 1024)}),与合成次数无关。

+ * + *

这条性质是 bigint(原生大数)批次能在有用线圈上吃到完整规模的前提:能量不随 count 放大, + * 容量就不会被能量闸压小。反过来,若把这里改成按并行计费,大数批次会被 + * {@code 可用能量 / 单份能耗} 卡回小规模。改本方法或改催化剂解析前请先确认这一点。

+ */ + public ResolvedCatalystEffect resolveEffect(AdvancedAlloyFurnaceRecipe recipe) { + int baseTime = recipe == null ? 200 : Math.max(1, recipe.processTime()); + ResolvedCatalystEffect catalystEffect = + CatalystEffectResolver.resolveForType(recipe, catalystType, baseTime); + return new ResolvedCatalystEffect( + catalystEffect.catalystType(), + singleTaskParallel(), + singleTaskParallel(), + processTime(baseTime), + catalystEffect.energyMultipliesWithParallel(), + energyDivisor(), + catalystEffect.uselessIngotRecipe(), + catalystEffect.targetUselessIngotTier()); + } + + private static OmniversalCoilStats[] createStats() { + OmniversalCoilStats[] stats = new OmniversalCoilStats[UselessCoilBlock.MAX_TIER]; + for (int tier = UselessCoilBlock.MIN_TIER; tier <= UselessCoilBlock.MAX_TIER; tier++) { + CatalystType catalystType = tier == UselessCoilBlock.USEFUL_TIER + ? CatalystType.USEFUL_INGOT + : CatalystType.uselessIngotTier(tier); + long singleTaskParallel = tier == UselessCoilBlock.USEFUL_TIER + ? Long.MAX_VALUE + : 1L << (tier * 2); + stats[tier - UselessCoilBlock.MIN_TIER] = new OmniversalCoilStats( + tier, + catalystType, + singleTaskParallel, + 1 << tier, + tier + 1, + AdvancedAlloyFurnaceBlockEntity.calculateEnergyCapacity(tier), + AdvancedAlloyFurnaceBlockEntity.calculateEnergyReceive(tier)); + } + return stats; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/multiblock/OmniversalFurnaceAutoBuilder.java b/src/main/java/com/sorrowmist/useless/content/multiblock/OmniversalFurnaceAutoBuilder.java new file mode 100644 index 00000000..3c80c6b7 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/multiblock/OmniversalFurnaceAutoBuilder.java @@ -0,0 +1,348 @@ +package com.sorrowmist.useless.content.multiblock; + +import appeng.api.config.Actionable; +import com.sorrowmist.useless.compat.AE2Compat; +import com.sorrowmist.useless.content.blockentities.multiblock.MePatternAssemblyBlockEntity; +import com.sorrowmist.useless.content.blockentities.multiblock.MultiblockAlloyFurnaceCoreBlockEntity; +import com.sorrowmist.useless.content.blockentities.multiblock.PassiveCraftingHatchBlockEntity; +import com.sorrowmist.useless.content.blocks.multiblock.MultiblockAlloyFurnaceCoreBlock; +import com.sorrowmist.useless.content.blocks.multiblock.DirectionalMultiblockPartBlock; +import com.sorrowmist.useless.content.blocks.multiblock.OmniversalAlloyFurnaceStructure; +import com.sorrowmist.useless.content.blocks.multiblock.UselessCoilBlock; +import com.sorrowmist.useless.init.ModBlocks; +import com.sorrowmist.useless.init.ModTags; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public final class OmniversalFurnaceAutoBuilder { + private OmniversalFurnaceAutoBuilder() { + } + + public record Result(boolean success, Component message) { + } + + public static Result build(ServerPlayer player, ItemStack tool, BlockPos corePos) { + ServerLevel level = player.serverLevel(); + BlockState coreState = level.getBlockState(corePos); + if (!coreState.is(ModBlocks.MULTIBLOCK_ALLOY_FURNACE_CORE.get())) { + return failure("gui.useless_mod.multiblock_builder.invalid_core"); + } + Direction facing = coreState.getValue(MultiblockAlloyFurnaceCoreBlock.FACING); + + int existingTier = 0; + for (OmniversalAlloyFurnaceStructure.Entry entry : OmniversalAlloyFurnaceStructure.entries()) { + if (entry.part() != OmniversalAlloyFurnaceStructure.Part.COIL) continue; + Block block = level.getBlockState(entry.worldPos(corePos, facing)).getBlock(); + if (block instanceof UselessCoilBlock coil) { + if (existingTier == 0) existingTier = coil.tier(); + else if (existingTier != coil.tier()) { + return failure("gui.useless_mod.multiblock_builder.mixed_coils"); + } + } + } + + int coilTier = existingTier; + if (coilTier == 0) coilTier = coilTier(player.getOffhandItem()); + if (coilTier == 0 && player.getAbilities().instabuild) coilTier = UselessCoilBlock.MAX_TIER; + if (coilTier == 0) { + for (int candidate = UselessCoilBlock.MAX_TIER; + candidate >= UselessCoilBlock.MIN_TIER; candidate--) { + int missing = missingCoils(level, corePos, facing, candidate); + ItemStack coil = new ItemStack(ModBlocks.USELESS_COILS.get(candidate).get(), missing); + long available = AE2Compat.tryExtractFromLinkedGrid(tool, player, coil, Actionable.SIMULATE) + + countInventory(player, coil.getItem()); + if (available >= missing) { + coilTier = candidate; + break; + } + } + } + if (coilTier == 0) return failure("gui.useless_mod.multiblock_builder.missing_coils"); + + FunctionalLayout functionalLayout = resolveFunctionalLayout(level, corePos, facing); + if (functionalLayout == null) { + return failure("gui.useless_mod.multiblock_builder.invalid_functional_parts"); + } + + List placements = new ArrayList<>(); + Map requirements = new LinkedHashMap<>(); + for (OmniversalAlloyFurnaceStructure.Entry entry : OmniversalAlloyFurnaceStructure.entries()) { + if (entry.part() == OmniversalAlloyFurnaceStructure.Part.CORE) continue; + BlockPos pos = entry.worldPos(corePos, facing); + if (!level.getWorldBorder().isWithinBounds(pos) || level.isOutsideBuildHeight(pos) + || !level.mayInteract(player, pos) || !player.mayUseItemAt(pos, Direction.UP, tool)) { + return failure("gui.useless_mod.multiblock_builder.no_permission"); + } + BlockState current = level.getBlockState(pos); + OmniversalAlloyFurnaceStructure.FunctionalPart functionalPart = + functionalLayout.parts().get(pos); + BlockState expected = expectedState(entry.part(), coilTier, functionalPart, facing); + if (matches(entry.part(), current, coilTier, functionalPart)) continue; + if (level.getBlockEntity(pos) != null || !canReplaceForBuild(current, functionalPart)) { + return failure("gui.useless_mod.multiblock_builder.blocked", pos.toShortString()); + } + placements.add(new Placement(pos.immutable(), current, expected)); + if (!expected.isAir()) { + requirements.merge(expected.getBlock().asItem(), 1, Integer::sum); + } + } + + if (!player.getAbilities().instabuild && !hasMaterials(player, tool, requirements)) { + return failure("gui.useless_mod.multiblock_builder.missing_materials"); + } + + MaterialTransaction materials = new MaterialTransaction(player, tool); + if (!player.getAbilities().instabuild && !materials.commit(requirements)) { + materials.rollback(); + return failure("gui.useless_mod.multiblock_builder.commit_failed"); + } + + List completed = new ArrayList<>(); + for (Placement placement : placements) { + if (!level.setBlock(placement.pos, placement.expected, 3)) { + for (int index = completed.size() - 1; index >= 0; index--) { + Placement previous = completed.get(index); + level.setBlock(previous.pos, previous.original, 3); + } + materials.rollback(); + return failure("gui.useless_mod.multiblock_builder.commit_failed"); + } + completed.add(placement); + } + + if (level.getBlockEntity(corePos) instanceof MultiblockAlloyFurnaceCoreBlockEntity core) { + core.requestStructureValidation(); + core.validateStructure(); + } + return new Result(true, Component.translatable( + "gui.useless_mod.multiblock_builder.success", coilTier)); + } + + private static FunctionalLayout resolveFunctionalLayout( + ServerLevel level, BlockPos corePos, Direction facing) { + Map parts = new LinkedHashMap<>(); + for (OmniversalAlloyFurnaceStructure.Entry entry : OmniversalAlloyFurnaceStructure.entries()) { + if (entry.part() != OmniversalAlloyFurnaceStructure.Part.CASING) continue; + BlockPos pos = entry.worldPos(corePos, facing); + OmniversalAlloyFurnaceStructure.FunctionalPart functionalPart = + OmniversalAlloyFurnaceStructure.functionalPart(level.getBlockState(pos)); + if (functionalPart == null) continue; + if (parts.containsValue(functionalPart)) return null; + if (isExclusivePartClaimedByAnotherCore(level, corePos, pos, functionalPart)) return null; + parts.put(pos.immutable(), functionalPart); + } + + if (!parts.containsValue(OmniversalAlloyFurnaceStructure.FunctionalPart.PATTERN_ASSEMBLY)) { + BlockPos pos = chooseFunctionalPosition(level, corePos, facing, parts, + OmniversalAlloyFurnaceStructure.PREFERRED_PATTERN_ASSEMBLY_POS); + if (pos == null) return null; + parts.put(pos, OmniversalAlloyFurnaceStructure.FunctionalPart.PATTERN_ASSEMBLY); + } + if (!parts.containsValue(OmniversalAlloyFurnaceStructure.FunctionalPart.MOLD_HUB)) { + BlockPos pos = chooseFunctionalPosition(level, corePos, facing, parts, + OmniversalAlloyFurnaceStructure.PREFERRED_MOLD_HUB_POS); + if (pos == null) return null; + parts.put(pos, OmniversalAlloyFurnaceStructure.FunctionalPart.MOLD_HUB); + } + return new FunctionalLayout(Map.copyOf(parts)); + } + + private static boolean isExclusivePartClaimedByAnotherCore( + ServerLevel level, BlockPos corePos, BlockPos partPos, + OmniversalAlloyFurnaceStructure.FunctionalPart functionalPart) { + return switch (functionalPart) { + case PATTERN_ASSEMBLY -> level.getBlockEntity(partPos) + instanceof MePatternAssemblyBlockEntity assembly + && assembly.isClaimedByOtherController(corePos); + case PASSIVE_HATCH -> level.getBlockEntity(partPos) + instanceof PassiveCraftingHatchBlockEntity hatch + && hatch.isClaimedByOtherController(corePos); + case MOLD_HUB -> false; + }; + } + + private static BlockPos chooseFunctionalPosition( + ServerLevel level, BlockPos corePos, Direction facing, + Map assigned, + BlockPos preferredLocalPos) { + for (OmniversalAlloyFurnaceStructure.Entry entry : OmniversalAlloyFurnaceStructure.entries()) { + if (entry.part() != OmniversalAlloyFurnaceStructure.Part.CASING + || !entry.localPos().equals(preferredLocalPos)) continue; + BlockPos pos = entry.worldPos(corePos, facing); + if (!assigned.containsKey(pos) && isAvailableFunctionalPosition(level, pos)) { + return pos.immutable(); + } + } + for (OmniversalAlloyFurnaceStructure.Entry entry : OmniversalAlloyFurnaceStructure.entries()) { + if (entry.part() != OmniversalAlloyFurnaceStructure.Part.CASING) continue; + BlockPos pos = entry.worldPos(corePos, facing); + if (!assigned.containsKey(pos) && isAvailableFunctionalPosition(level, pos)) { + return pos.immutable(); + } + } + return null; + } + + private static boolean isAvailableFunctionalPosition(ServerLevel level, BlockPos pos) { + if (level.getBlockEntity(pos) != null) return false; + BlockState state = level.getBlockState(pos); + return state.canBeReplaced() || state.isAir() || state.is(ModTags.OMNIVERSAL_FURNACE_CASINGS); + } + + private static boolean hasMaterials(ServerPlayer player, ItemStack tool, Map requirements) { + for (Map.Entry entry : requirements.entrySet()) { + ItemStack requested = new ItemStack(entry.getKey(), entry.getValue()); + long network = AE2Compat.tryExtractFromLinkedGrid(tool, player, requested, Actionable.SIMULATE); + if (network + countInventory(player, entry.getKey()) < entry.getValue()) return false; + } + return true; + } + + private static int missingCoils(ServerLevel level, BlockPos corePos, Direction facing, int tier) { + int missing = 0; + for (var entry : OmniversalAlloyFurnaceStructure.entries()) { + if (entry.part() == OmniversalAlloyFurnaceStructure.Part.COIL + && !matches(entry.part(), level.getBlockState(entry.worldPos(corePos, facing)), tier, null)) { + missing++; + } + } + return missing; + } + + private static int coilTier(ItemStack stack) { + return stack.getItem() instanceof BlockItem blockItem + && blockItem.getBlock() instanceof UselessCoilBlock coil ? coil.tier() : 0; + } + + private static boolean matches(OmniversalAlloyFurnaceStructure.Part part, BlockState state, int coilTier, + OmniversalAlloyFurnaceStructure.FunctionalPart functionalPart) { + if (functionalPart != null) { + return OmniversalAlloyFurnaceStructure.functionalPart(state) == functionalPart; + } + return switch (part) { + case CORE -> state.is(ModBlocks.MULTIBLOCK_ALLOY_FURNACE_CORE.get()); + case CASING -> state.is(ModTags.OMNIVERSAL_FURNACE_CASINGS) + && OmniversalAlloyFurnaceStructure.functionalPart(state) == null; + case COIL -> state.getBlock() instanceof UselessCoilBlock coil && coil.tier() == coilTier; + case AIR -> state.isAir(); + }; + } + + private static boolean canReplaceForBuild(BlockState current, + OmniversalAlloyFurnaceStructure.FunctionalPart functionalPart) { + return current.canBeReplaced() || current.isAir() + || functionalPart != null && current.is(ModTags.OMNIVERSAL_FURNACE_CASINGS); + } + + static BlockState expectedState(OmniversalAlloyFurnaceStructure.Part part, int coilTier, + OmniversalAlloyFurnaceStructure.FunctionalPart functionalPart, + Direction facing) { + if (functionalPart != null) { + return (switch (functionalPart) { + case PATTERN_ASSEMBLY -> ModBlocks.ME_PATTERN_ASSEMBLY.get().defaultBlockState(); + case MOLD_HUB -> ModBlocks.OMNIVERSAL_MOLD_HUB.get().defaultBlockState(); + case PASSIVE_HATCH -> ModBlocks.PASSIVE_CRAFTING_HATCH.get().defaultBlockState(); + }).setValue(DirectionalMultiblockPartBlock.FACING, facing); + } + return switch (part) { + case CASING -> ModBlocks.OMNIVERSAL_FURNACE_CASING.get().defaultBlockState(); + case COIL -> ModBlocks.USELESS_COILS.get(coilTier).get().defaultBlockState(); + case AIR -> Blocks.AIR.defaultBlockState(); + case CORE -> ModBlocks.MULTIBLOCK_ALLOY_FURNACE_CORE.get().defaultBlockState(); + }; + } + + private static int countInventory(ServerPlayer player, Item item) { + int count = 0; + for (int slot = 0; slot < player.getInventory().getContainerSize(); slot++) { + ItemStack stack = player.getInventory().getItem(slot); + if (stack.is(item)) count += stack.getCount(); + } + return count; + } + + private static Result failure(String key, Object... arguments) { + return new Result(false, Component.translatable(key, arguments)); + } + + private record Placement(BlockPos pos, BlockState original, BlockState expected) { + } + + private record FunctionalLayout( + Map parts) { + } + + private static final class MaterialTransaction { + private final ServerPlayer player; + private final ItemStack tool; + private final Map fromNetwork = new LinkedHashMap<>(); + private final List fromInventory = new ArrayList<>(); + + private MaterialTransaction(ServerPlayer player, ItemStack tool) { + this.player = player; + this.tool = tool; + } + + private boolean commit(Map requirements) { + for (Map.Entry requirement : requirements.entrySet()) { + int needed = requirement.getValue(); + ItemStack requested = new ItemStack(requirement.getKey(), needed); + int network = (int) Math.min(needed, + AE2Compat.tryExtractFromLinkedGrid(tool, player, requested, Actionable.MODULATE)); + if (network > 0) fromNetwork.merge(requirement.getKey(), network, Integer::sum); + needed -= network; + for (int slot = 0; slot < player.getInventory().getContainerSize() && needed > 0; slot++) { + ItemStack stack = player.getInventory().getItem(slot); + if (!stack.is(requirement.getKey())) continue; + int removed = Math.min(needed, stack.getCount()); + stack.shrink(removed); + fromInventory.add(new InventoryRemoval(slot, requirement.getKey(), removed)); + needed -= removed; + } + if (needed > 0) return false; + } + player.getInventory().setChanged(); + return true; + } + + private void rollback() { + for (Map.Entry entry : fromNetwork.entrySet()) { + ItemStack stack = new ItemStack(entry.getKey(), entry.getValue()); + long inserted = AE2Compat.tryInsertIntoLinkedGrid(tool, player, stack, Actionable.MODULATE); + int remaining = entry.getValue() - (int) inserted; + if (remaining > 0) player.getInventory().placeItemBackInInventory( + new ItemStack(entry.getKey(), remaining)); + } + for (InventoryRemoval removal : fromInventory) { + ItemStack current = player.getInventory().getItem(removal.slot); + if (current.isEmpty()) { + player.getInventory().setItem(removal.slot, new ItemStack(removal.item, removal.count)); + } else if (current.is(removal.item)) { + current.grow(removal.count); + } else { + player.getInventory().placeItemBackInInventory(new ItemStack(removal.item, removal.count)); + } + } + fromNetwork.clear(); + fromInventory.clear(); + player.getInventory().setChanged(); + } + } + + private record InventoryRemoval(int slot, Item item, int count) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/AdapterUtils.java b/src/main/java/com/sorrowmist/useless/content/recipe/AdapterUtils.java new file mode 100644 index 00000000..608620e0 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/AdapterUtils.java @@ -0,0 +1,612 @@ +package com.sorrowmist.useless.content.recipe; + +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import com.sorrowmist.useless.core.component.RitualBlueprintPentacles; +import com.sorrowmist.useless.core.component.UComponents; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.alchemy.PotionBrewing; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.material.Fluid; +import com.sorrowmist.useless.compat.forge1201.recipe.DataComponentIngredient; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.DataComponentFluidIngredient; +import com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import org.jetbrains.annotations.Nullable; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.ConcurrentModificationException; +import java.util.HexFormat; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * 配方适配器工具类 + *

+ * 提供适配器实现中常用的静态工具方法,消除重复代码。 + */ +public class AdapterUtils { + + /** 默认基础处理时间(ticks) */ + public static final int DEFAULT_PROCESS_TIME = 20; + /** 默认基础能量消耗(FE) */ + public static final int DEFAULT_ENERGY = 2000; + + public static final int MEKANISM_BASE_TICKS_REQUIRED = 200; + public static final int MEKANISM_ENRICHMENT_CHAMBER_TICKS_REQUIRED = MEKANISM_BASE_TICKS_REQUIRED; + public static final int MEKANISM_METALLURGIC_INFUSER_TICKS_REQUIRED = MEKANISM_BASE_TICKS_REQUIRED; + public static final int MEKANISM_ENRICHMENT_CHAMBER_ENERGY_PER_TICK = 50; + public static final int MEKANISM_METALLURGIC_INFUSER_ENERGY_PER_TICK = 50; + + /** + * 收集全量物品注册表中可作为酿造试剂的物品,用于收窄后续遍历范围。 + *

+ * hasPotionMix 与 hasContainerMix 的通过者必然满足 isPotionIngredient 或 + * isContainerIngredient,而 isIngredient 为二者与自定义酿造配方的并集, + * 因此以 isIngredient 预筛不会排除任何可产出配方的试剂。 + * + * @param brewing 当前等级的药水酿造实例 + * @return 预筛后的试剂候选列表 + */ + public static List reagentCandidates(PotionBrewing brewing) { + List candidates = new ArrayList<>(); + for (Item item : BuiltInRegistries.ITEM) { + if (item == null) { + continue; + } + if (isIngredientWithRetry(brewing, item)) { + candidates.add(item); + } + } + return List.copyOf(candidates); + } + + /** 单个试剂的最大重试次数。 */ + private static final int REAGENT_PROBE_ATTEMPTS = 32; + + /** + * 判定试剂是否被当前酿造实例接受,并在并发注册窗口内重试。 + * + *

PotionBrewing 的查询方法直接迭代其内部列表,主线程注册混合配方与后台线程查询重叠时 + * 会抛出 ConcurrentModificationException。此处若与第三方异常一并无视,候选列表会静默缩水, + * 调用方无法察觉产出缺失;因此对并发修改单独重试。其余异常仍按第三方异常配方忽略, + * 不得中断其余试剂的收集。

+ * + * @return 该试剂是否被接受;重试耗尽或抛出其他异常时返回 false + */ + private static boolean isIngredientWithRetry(PotionBrewing brewing, Item item) { + for (int attempt = 0; ; attempt++) { + try { + return brewing.isIngredient(item.getDefaultInstance()); + } catch (ConcurrentModificationException exception) { + if (attempt >= REAGENT_PROBE_ATTEMPTS - 1) { + return false; + } + try { + Thread.sleep(1L); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + return false; + } + } catch (RuntimeException ignored) { + return false; + } + } + } + + /** AE 能量到 FE 的转换系数 */ + public static final int AE_TO_FE_CONVERSION = 2; + /** ExtendedAE 设备能量乘数 */ + public static final int EXTENDEDAE_ENERGY_MULTIPLIER = 10; + /** AE2CS 配方最低能量消耗(FE) */ + public static final int AE2CS_MIN_ENERGY = DEFAULT_ENERGY / 4; + /** AdvancedAE 反应仓默认处理时间(ticks) */ + public static final int ADVANCEDAE_REACTION_CHAMBER_PROCESS_TIME = 100; + /** Industrial Foregoing 溶解成型机基础能耗 (FE/tick) */ + public static final int IF_BASE_ENERGY_PER_TICK = 90; + /** Industrial Foregoing 能量倍率 */ + public static final int IF_ENERGY_MULTIPLIER = 4; + + /** + * 生成转换后的配方 ID(追加 _converted 后缀) + * + * @param originalId 原始配方 ID + * @return 转换后的配方 ID + */ + public static ResourceLocation convertedId(ResourceLocation originalId) { + return new ResourceLocation( + originalId.getNamespace(), + originalId.getPath() + "_converted" + ); + } + + /** + * 将 ItemStack 转换为 Ingredient 模具 + * + * @param moldItem 模具物品 + * @return 可用于配方模具字段的 Ingredient,空输入返回 Ingredient.EMPTY + */ + public static Ingredient toMoldIngredient(@Nullable ItemStack moldItem) { + if (moldItem == null || moldItem.isEmpty()) return Ingredient.EMPTY; + return Ingredient.of(moldItem); + } + + /** + * Returns one concrete item only for APIs that require an {@link ItemStack} key. + * + *

The returned stack is a temporary encoding representative. The original ingredient must + * remain the recipe value so direct item tags keep their matching semantics.

+ */ + @Nullable + public static ItemStack itemRepresentative(@Nullable Ingredient ingredient) { + if (ingredient == null) return null; + + ItemStack[] displayed; + try { + displayed = ingredient.getItems(); + } catch (RuntimeException exception) { + displayed = new ItemStack[0]; + } + for (ItemStack stack : displayed) { + if (stack != null && !stack.isEmpty()) { + return stack.copyWithCount(1); + } + } + + Optional> directTag = directItemTag(ingredient); + if (directTag.isPresent()) { + // Ingredient may have cached an empty representation before datapack tags were bound. + // Read the current registry tag as a last-resort representative without changing the + // ingredient or serializing any of its members into recipe data. + for (Holder holder : BuiltInRegistries.ITEM.getTagOrEmpty(directTag.get())) { + ItemStack stack = holder.value().getDefaultInstance(); + if (!stack.isEmpty()) return stack.copyWithCount(1); + } + } + + // Some custom ingredients intentionally expose no display stacks. Probe registered + // defaults as a last-resort encoding representative while retaining the original + // ingredient for the omniversal matcher. + for (Item item : BuiltInRegistries.ITEM) { + ItemStack stack = item.getDefaultInstance(); + if (stack.isEmpty()) continue; + try { + if (ingredient.test(stack)) return stack.copyWithCount(1); + } catch (RuntimeException ignored) { + // A custom ingredient may not support probing arbitrary items. + } + } + return null; + } + + /** Returns one concrete fluid only for APIs that require a {@link FluidStack}. */ + @Nullable + public static FluidStack fluidRepresentative( + @Nullable FluidIngredient ingredient, int amount) { + if (ingredient == null || ingredient.isEmpty() || amount <= 0) return null; + + FluidStack[] displayed; + try { + displayed = ingredient.getStacks(); + } catch (RuntimeException exception) { + displayed = new FluidStack[0]; + } + for (FluidStack stack : displayed) { + if (stack != null && !stack.isEmpty()) { + return new net.minecraftforge.fluids.FluidStack(stack, amount); + } + } + + for (Fluid fluid : BuiltInRegistries.FLUID) { + FluidStack stack = new FluidStack(fluid, amount); + try { + if (ingredient.test(stack)) return stack; + } catch (RuntimeException ignored) { + // A custom ingredient may not support probing arbitrary fluids. + } + } + return null; + } + + private static Optional> directItemTag(Ingredient ingredient) { + return com.sorrowmist.useless.compat.forge1201.recipe.NativeIngredients.directItemTag(ingredient); + } + + /** Matches normal molds exactly and ritual blueprints by required pentacle inclusion. */ + public static boolean matchesMold(@Nullable Ingredient requiredMold, @Nullable ItemStack actualMold) { + if (requiredMold == null || requiredMold.isEmpty()) return true; + if (actualMold == null || actualMold.isEmpty()) return false; + if (requiredMold.test(actualMold)) return true; + + RitualBlueprintPentacles actualPentacles = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(actualMold, UComponents.RITUAL_BLUEPRINT_PENTACLE.get()); + if (actualPentacles == null || actualPentacles.isEmpty()) return false; + for (ItemStack representative : requiredMold.getItems()) { + RitualBlueprintPentacles requiredPentacles = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(representative, + UComponents.RITUAL_BLUEPRINT_PENTACLE.get()); + if (requiredPentacles != null && !requiredPentacles.isEmpty() + && ItemStack.isSameItem(representative, actualMold) + && actualPentacles.containsAll(requiredPentacles)) { + return true; + } + } + return false; + } + + /** + * 将 AE2CS 原配方能量转换为合金炉能量消耗。 + * + * @param originalEnergy 原配方 energy_cost + * @return 转换后的 FE 消耗 + */ + public static int ae2csEnergyCost(int originalEnergy) { + return Math.max(originalEnergy * AE_TO_FE_CONVERSION, AE2CS_MIN_ENERGY); + } + + public static int mekanismEnrichmentChamberEnergyCost(long operations) { + return mekanismEnergyCost(MEKANISM_ENRICHMENT_CHAMBER_ENERGY_PER_TICK, MEKANISM_ENRICHMENT_CHAMBER_TICKS_REQUIRED, operations); + } + + public static int mekanismEnrichmentChamberProcessTime(long operations) { + return mekanismProcessTime(MEKANISM_ENRICHMENT_CHAMBER_TICKS_REQUIRED, operations); + } + + public static int mekanismMetallurgicInfuserEnergyCost(long operations) { + return mekanismEnergyCost(MEKANISM_METALLURGIC_INFUSER_ENERGY_PER_TICK, MEKANISM_METALLURGIC_INFUSER_TICKS_REQUIRED, operations); + } + + public static int mekanismMetallurgicInfuserProcessTime(long operations) { + return mekanismProcessTime(MEKANISM_METALLURGIC_INFUSER_TICKS_REQUIRED, operations); + } + + public static int mekanismEnergyCost(long energyPerTick, long operations) { + return mekanismEnergyCost(energyPerTick, MEKANISM_BASE_TICKS_REQUIRED, operations); + } + + public static int mekanismEnergyCost(long energyPerTick, long ticksRequired, long operations) { + return safeInt(energyPerTick * ticksRequired * operations); + } + + public static int mekanismProcessTime(long operations) { + return mekanismProcessTime(MEKANISM_BASE_TICKS_REQUIRED, operations); + } + + public static int mekanismProcessTime(long ticksRequired, long operations) { + return safeInt(ticksRequired * operations); + } + + public static int safeInt(long value) { + if (value > Integer.MAX_VALUE) return Integer.MAX_VALUE; + return (int) Math.max(0, value); + } + + public static long gcd(long a, long b) { + while (b != 0) { + long temp = b; + b = a % b; + a = temp; + } + return a; + } + + /** + * 将 ItemStack 列表按 Ingredient 合并,返回合并后的计数 Map + *

+ * 用于 AlloyFurnaceRecipeManager 层统一合并输入。相同物品会合并到已有 Ingredient 项中,数量为 ItemStack count 总和。 + * + * @param inputs 原始物品输入列表 + * @return 合并后的 Ingredient 到数量映射 + */ + public static Map mergeInputs(List inputs) { + Map merged = new LinkedHashMap<>(); + if (inputs == null) return merged; + for (ItemStack stack : inputs) { + if (stack == null || stack.isEmpty()) continue; + boolean found = false; + for (Map.Entry entry : merged.entrySet()) { + ItemStack[] representatives = entry.getKey().getItems(); + if (representatives.length == 1 + && ItemStack.isSameItemSameTags(representatives[0], stack)) { + merged.put(entry.getKey(), entry.getValue() + stack.getCount()); + found = true; + break; + } + } + if (!found) { + // Vanilla Ingredient.of(ItemStack) ignores components in equals(), so it cannot + // safely be used as a map key for two component-distinct stacks of the same item. + merged.put(DataComponentIngredient.of(true, stack.copyWithCount(1)), (long) stack.getCount()); + } + } + return merged; + } + + /** + * 将 FluidStack 列表按流体类型合并,返回合并后的计数 Map + *

+ * 用于 AlloyFurnaceRecipeManager 层统一合并流体输入。流体类型与组件相同的 FluidStack 会合并数量。 + * + * @param fluidInputs 原始流体输入列表 + * @return 合并后的 FluidStack 到数量映射 + */ + public static Map mergeFluids(List fluidInputs) { + if (fluidInputs == null || fluidInputs.isEmpty()) return Map.of(); + Map merged = new LinkedHashMap<>(); + for (FluidStack stack : fluidInputs) { + if (stack.isEmpty()) continue; + boolean found = false; + for (Map.Entry entry : merged.entrySet()) { + if (com.sorrowmist.useless.compat.forge1201.fluid.FluidStacks.same(entry.getKey(), stack)) { + merged.put(entry.getKey(), entry.getValue() + stack.getAmount()); + found = true; + break; + } + } + if (!found) { + merged.put(stack.copy(), (long) stack.getAmount()); + } + } + return merged; + } + + /** Converts a concrete fluid stack without dropping its component predicate. */ + public static SizedFluidIngredient toSizedFluidIngredient(@Nullable FluidStack stack) { + if (stack == null || stack.isEmpty() || stack.getAmount() <= 0) return null; + FluidIngredient ingredient = !stack.hasTag() + ? FluidIngredient.single(stack) + : DataComponentFluidIngredient.of(true, stack); + return new SizedFluidIngredient(ingredient, stack.getAmount()); + } + + /** Expands an aggregated fluid map into non-mutating supplies for the common allocator. */ + public static List fluidSupplies(@Nullable Map mergedFluids) { + if (mergedFluids == null || mergedFluids.isEmpty()) return List.of(); + List supplies = new java.util.ArrayList<>(mergedFluids.size()); + for (Map.Entry entry : mergedFluids.entrySet()) { + FluidStack stack = entry.getKey(); + long amount = entry.getValue() == null ? 0L : entry.getValue(); + if (stack == null || stack.isEmpty() || amount <= 0L) continue; + FluidStack copy = stack.copy(); + copy.setAmount(amount > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) amount); + supplies.add(copy); + } + return supplies; + } + + public static boolean matchesFluidIngredients( + @Nullable Map mergedFluids, + List requirements) { + if (requirements == null || requirements.isEmpty()) return true; + return FluidIngredientAllocator.matches(requirements, mergedFluids, 1L); + } + + public static boolean matchesLongFluidIngredients( + @Nullable Map mergedFluids, + List requirements) { + if (requirements == null || requirements.isEmpty()) return true; + return FluidIngredientAllocator.matchesLong(requirements, mergedFluids, 1L); + } + + /** + * 将 GenericStack 列表按 AEKey 合并,返回合并后的数量 Map。 + * + * @param keyInputs 原始 AEKey 输入列表 + * @return 合并后的 AEKey 到数量映射 + */ + public static Map mergeKeys(List keyInputs) { + if (keyInputs == null || keyInputs.isEmpty()) return Map.of(); + Map merged = new LinkedHashMap<>(); + for (GenericStack stack : keyInputs) { + if (stack == null || stack.what() == null || stack.amount() <= 0) continue; + merged.merge(stack.what(), stack.amount(), Long::sum); + } + return merged; + } + + /** + * 检查已合并 AEKey 输入是否满足配方需求。 + * + * @param mergedKeys 已合并的 AEKey 输入 + * @param requiredKeys 配方需求 AEKey 列表 + * @return 是否满足全部 AEKey 需求 + */ + public static boolean matchesKeyRequirements(Map mergedKeys, List requiredKeys) { + if (requiredKeys == null || requiredKeys.isEmpty()) return true; + if (mergedKeys == null || mergedKeys.isEmpty()) return false; + for (GenericStack required : requiredKeys) { + if (required == null || required.what() == null || required.amount() <= 0) continue; + long found = mergedKeys.getOrDefault(required.what(), 0L); + if (found < required.amount()) return false; + } + return true; + } + + /** + * 检查合并后的输入是否满足配方需求 + *

+ * 适用于多输入配方。每个需求 Ingredient 都会在 mergedInputs 中统计可匹配的总数量。 + * + * @param mergedInputs 已合并的实际输入 + * @param requiredCounts 配方需求 Ingredient 到数量映射 + * @return 实际输入是否满足全部需求 + */ + public static boolean matchesRequired(Map mergedInputs, Map requiredCounts) { + return ItemIngredientAllocator.matches(mergedInputs, requiredCounts); + } + + /** + * 检查已合并输入中是否存在至少 1 个匹配指定 Ingredient 的物品。 + * + * @param mergedInputs 已合并的实际输入 + * @param required 需要匹配的 Ingredient + * @return 是否存在匹配输入 + */ + public static boolean hasMatchingIngredient(Map mergedInputs, Ingredient required) { + return countMatchingIngredient(mergedInputs, required) > 0; + } + + /** + * 检查已合并输入中是否存在足够数量的指定 Ingredient。 + * + * @param mergedInputs 已合并的实际输入 + * @param required 需要匹配的 Ingredient + * @param count 需求数量 + * @return 是否存在足够数量的匹配输入 + */ + public static boolean hasMatchingIngredient(Map mergedInputs, Ingredient required, long count) { + return countMatchingIngredient(mergedInputs, required) >= count; + } + + /** + * 统计已合并输入中可匹配指定 Ingredient 的总数量。 + *

+ * 同时使用 areIngredientsEqual 和 ingredientMatches:前者判断两个 Ingredient 的可选项集合是否完全等价,后者判断输入 Ingredient 的代表物品是否能被需求 Ingredient 接受。 + * 这样可以覆盖“输入被 mergeInputs 转成 Ingredient.of(具体 ItemStack),需求是 tag 或多候选 Ingredient”的场景。 + * + * @param mergedInputs 已合并的实际输入 + * @param required 需要匹配的 Ingredient + * @return 匹配到的总数量 + */ + public static long countMatchingIngredient(Map mergedInputs, Ingredient required) { + long found = 0; + for (Map.Entry input : mergedInputs.entrySet()) { + if (areIngredientsEqual(required, input.getKey()) || ingredientMatches(required, input.getKey())) { + found += input.getValue(); + } + } + return found; + } + + /** + * 判断输入 Ingredient 的任意代表物品是否能满足需求 Ingredient。 + * + * @param required 配方需求 Ingredient + * @param input 实际输入 Ingredient + * @return 输入 Ingredient 是否能被需求 Ingredient 接受 + */ + private static boolean ingredientMatches(Ingredient required, Ingredient input) { + if (required == null || input == null) return false; + + ItemStack[] inputStacks = input.getItems(); + if (inputStacks.length == 0) return false; + + for (ItemStack stack : inputStacks) { + if (required.test(stack)) { + return true; + } + } + return false; + } + + /** + * 合并相同 Ingredient 的计数(用 areIngredientsEqual 判断相同) + * + * @param map 需要写入的 Ingredient 计数 Map + * @param ingredient 要合并的 Ingredient + * @param count 要增加的数量 + */ + public static void mergeIngredient(Map map, Ingredient ingredient, long count) { + for (Map.Entry entry : map.entrySet()) { + if (areIngredientsEqual(entry.getKey(), ingredient)) { + map.put(entry.getKey(), entry.getValue() + count); + return; + } + } + map.put(ingredient, count); + } + + public static boolean isIngredientEmpty(@Nullable Ingredient ingredient) { + return ingredient == null || ingredient.isEmpty(); + } + + public static List mergeIngredients(List itemInputs) { + if (itemInputs == null || itemInputs.isEmpty()) return List.of(); + Map ingredientCounts = new LinkedHashMap<>(); + for (Ingredient ingredient : itemInputs) { + if (isIngredientEmpty(ingredient)) continue; + mergeIngredient(ingredientCounts, ingredient, 1L); + } + return ingredientCounts.entrySet().stream() + .map(entry -> new CountedIngredient(entry.getKey(), entry.getValue())) + .toList(); + } + + /** + * 检查输入物品列表是否满足所需的 Ingredient 计数 + * + * @param inputs 原始物品输入列表 + * @param requiredCounts 配方需求 Ingredient 到数量映射 + * @return 输入是否满足全部需求 + */ + public static boolean matchesCountedIngredients(List inputs, Map requiredCounts) { + List requirements = requiredCounts.entrySet().stream() + .map(entry -> new CountedIngredient(entry.getKey(), entry.getValue())) + .toList(); + return ItemIngredientAllocator.matches(requirements, inputs, 1L); + } + + /** + * 计算文本的 SHA-256 摘要并截断为定长片段,供构造长度受限的稳定配方 id 使用。 + * + *

配方 id 必须只由内容特征派生:客户端与服务端各自构建一次配方目录,任何依赖遍历 + * 顺序的组成部分都会让同一个 id 在两端指向不同配方,表现为万象样板在服务端无法解析。 + * 内容特征拼接过长或含资源路径不允许的字符时,用摘要替代。

+ * + * @param value 参与摘要的文本 + * @return 32 个十六进制字符的摘要片段 + */ + public static String stableHash(String value) { + try { + byte[] hash = MessageDigest.getInstance("SHA-256") + .digest(value.getBytes(StandardCharsets.UTF_8)); + return HexFormat.of().formatHex(hash).substring(0, 32); + } catch (NoSuchAlgorithmException exception) { + throw new IllegalStateException("SHA-256 is unavailable", exception); + } + } + + /** + * 比较两个 Ingredient 是否代表相同的材料 + *

+ * 比较时忽略候选物品顺序,但要求候选数量一致,且每个候选物品和组件都能在另一方找到。 + * + * @param a 第一个 Ingredient + * @param b 第二个 Ingredient + * @return 两个 Ingredient 的候选物品集合是否等价 + */ + public static boolean areIngredientsEqual(Ingredient a, Ingredient b) { + if (a == b) return true; + if (a == null || b == null) return false; + if (a.equals(b)) return true; + + // Custom ingredient item lists are display hints, not semantic identities. + if (!a.isVanilla() || !b.isVanilla()) return false; + + ItemStack[] stacksA = a.getItems(); + ItemStack[] stacksB = b.getItems(); + + if (stacksA.length == 0 || stacksA.length != stacksB.length) return false; + + for (ItemStack stackA : stacksA) { + boolean found = false; + for (ItemStack stackB : stacksB) { + if (ItemStack.isSameItem(stackA, stackB) && ItemStack.isSameItemSameTags(stackA, stackB)) { + found = true; + break; + } + } + if (!found) return false; + } + return true; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/AdvancedAlloyFurnaceRecipe.java b/src/main/java/com/sorrowmist/useless/content/recipe/AdvancedAlloyFurnaceRecipe.java new file mode 100644 index 00000000..9b8681b3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/AdvancedAlloyFurnaceRecipe.java @@ -0,0 +1,367 @@ +package com.sorrowmist.useless.content.recipe; + +import net.minecraft.world.Container; +import appeng.api.stacks.GenericStack; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; + + +import net.minecraft.core.NonNullList; + +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.ByteBufCodecs; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.*; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public record AdvancedAlloyFurnaceRecipe( + ResourceLocation id, + List inputs, + List inputFluids, + List keyInputs, + List outputs, + List outputFluids, + List keyOutputs, + long energy, + int processTime, + Ingredient catalyst, + int catalystUses, + List molds, + AlloyFurnaceMode mode, + int tier +) implements Recipe { + + /** A negative tier means that the recipe uses the ID-based configuration rule. */ + public static final int NO_EXPLICIT_TIER = -1; + public static final int MIN_TIER = 0; + public static final int MAX_TIER = 10; + + private static final int SIZED_FLUID_NETWORK_VERSION = -1; + private static final int LONG_SIZED_FLUID_NETWORK_VERSION = -2; + private static final Codec> INPUT_FLUIDS_CODEC = Codec.either( + LongSizedFluidIngredient.CODEC.codec().listOf(), com.sorrowmist.useless.compat.forge1201.recipe.RecipeCodecs.FLUID_STACK.listOf()) + .xmap(either -> either.map( + list -> list, + list -> list.stream().map(LongSizedFluidIngredient::from).toList()), + list -> com.mojang.datafixers.util.Either.left(list)); + + public AdvancedAlloyFurnaceRecipe { + if (tier < NO_EXPLICIT_TIER || tier > MAX_TIER) { + throw new IllegalArgumentException("Alloy-furnace recipe tier must be between -1 and " + MAX_TIER); + } + List normalizedMolds = new ArrayList<>(); + if (molds != null) { + for (Ingredient mold : molds) { + if (mold != null && !mold.isEmpty()) { + normalizedMolds.add(mold); + } + } + } + molds = List.copyOf(normalizedMolds); + inputFluids = normalizeFluidIngredients(inputFluids); + } + + // 网络同步 StreamCodec + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, r) -> { + com.sorrowmist.useless.compat.forge1201.network.PortCodecs.RESOURCE_LOCATION.encode(buf, r.id()); + CountedIngredient.STREAM_CODEC.apply(ByteBufCodecs.list()).encode(buf, r.inputs()); + boolean fitsLegacyFormat = r.inputFluids().stream().allMatch(LongSizedFluidIngredient::fitsInt); + buf.writeVarInt(fitsLegacyFormat + ? SIZED_FLUID_NETWORK_VERSION : LONG_SIZED_FLUID_NETWORK_VERSION); + ByteBufCodecs.VAR_INT.encode(buf, r.inputFluids().size()); + for (LongSizedFluidIngredient inputFluid : r.inputFluids()) { + if (fitsLegacyFormat) { + SizedFluidIngredient.STREAM_CODEC.encode(buf, inputFluid.toSizedFluidIngredient()); + } else { + LongSizedFluidIngredient.STREAM_CODEC.encode(buf, inputFluid); + } + } + com.sorrowmist.useless.compat.forge1201.recipe.AE2Codecs.GENERIC_STACK_STREAM.apply(ByteBufCodecs.list()).encode(buf, r.keyInputs()); + com.sorrowmist.useless.compat.forge1201.network.PortCodecs.ITEM_STACK.apply(ByteBufCodecs.list()).encode(buf, r.outputs()); + com.sorrowmist.useless.compat.forge1201.network.PortCodecs.FLUID_STACK.apply(ByteBufCodecs.list()).encode(buf, r.outputFluids()); + com.sorrowmist.useless.compat.forge1201.recipe.AE2Codecs.GENERIC_STACK_STREAM.apply(ByteBufCodecs.list()).encode(buf, r.keyOutputs()); + ByteBufCodecs.VAR_LONG.encode(buf, r.energy()); + ByteBufCodecs.VAR_INT.encode(buf, r.processTime()); + CountedIngredient.INGREDIENT_STREAM_CODEC.encode(buf, r.catalyst()); + ByteBufCodecs.VAR_INT.encode(buf, r.catalystUses()); + CountedIngredient.INGREDIENT_STREAM_CODEC.apply(ByteBufCodecs.list()).encode(buf, r.molds()); + ByteBufCodecs.STRING_UTF8.encode(buf, r.mode().getSerializedName()); + ByteBufCodecs.VAR_INT.encode(buf, r.tier()); + }, + buf -> { + ResourceLocation id = com.sorrowmist.useless.compat.forge1201.network.PortCodecs.RESOURCE_LOCATION.decode(buf); + List inputs = CountedIngredient.STREAM_CODEC.apply(ByteBufCodecs.list()).decode(buf); + int fluidFormat = buf.readVarInt(); + List inputFluids = new ArrayList<>(); + if (fluidFormat == SIZED_FLUID_NETWORK_VERSION) { + int size = ByteBufCodecs.VAR_INT.decode(buf); + if (size < 0 || size > 65536) { + throw new IllegalArgumentException("Negative sized fluid ingredient list length: " + size); + } + for (int i = 0; i < size; i++) { + inputFluids.add(LongSizedFluidIngredient.from( + SizedFluidIngredient.STREAM_CODEC.decode(buf))); + } + } else if (fluidFormat == LONG_SIZED_FLUID_NETWORK_VERSION) { + int size = ByteBufCodecs.VAR_INT.decode(buf); + if (size < 0 || size > 65536) { + throw new IllegalArgumentException("Negative long sized fluid ingredient list length: " + size); + } + for (int i = 0; i < size; i++) { + inputFluids.add(LongSizedFluidIngredient.STREAM_CODEC.decode(buf)); + } + } else { + if (fluidFormat < 0 || fluidFormat > 65536) { + throw new IllegalArgumentException("Unknown alloy-furnace fluid input format: " + fluidFormat); + } + for (int i = 0; i < fluidFormat; i++) { + inputFluids.add(LongSizedFluidIngredient.from(com.sorrowmist.useless.compat.forge1201.network.PortCodecs.FLUID_STACK.decode(buf))); + } + } + List keyInputs = com.sorrowmist.useless.compat.forge1201.recipe.AE2Codecs.GENERIC_STACK_STREAM.apply(ByteBufCodecs.list()).decode(buf); + List outputs = com.sorrowmist.useless.compat.forge1201.network.PortCodecs.ITEM_STACK.apply(ByteBufCodecs.list()).decode(buf); + List outputFluids = com.sorrowmist.useless.compat.forge1201.network.PortCodecs.FLUID_STACK.apply(ByteBufCodecs.list()).decode(buf); + List keyOutputs = com.sorrowmist.useless.compat.forge1201.recipe.AE2Codecs.GENERIC_STACK_STREAM.apply(ByteBufCodecs.list()).decode(buf); + long energy = ByteBufCodecs.VAR_LONG.decode(buf); + int processTime = ByteBufCodecs.VAR_INT.decode(buf); + Ingredient catalyst = CountedIngredient.INGREDIENT_STREAM_CODEC.decode(buf); + int catalystUses = ByteBufCodecs.VAR_INT.decode(buf); + List molds = CountedIngredient.INGREDIENT_STREAM_CODEC.apply(ByteBufCodecs.list()).decode(buf); + String modeStr = ByteBufCodecs.STRING_UTF8.decode(buf); + AlloyFurnaceMode mode = AlloyFurnaceMode.fromString(modeStr); + int tier = ByteBufCodecs.VAR_INT.decode(buf); + + return new AdvancedAlloyFurnaceRecipe( + id, inputs, inputFluids, keyInputs, outputs, outputFluids, keyOutputs, + energy, processTime, catalyst, catalystUses, molds, mode, tier + ); + } + ); + // Keep the legacy id/count shape while retaining persistent item components in JSON. + private static final Codec ITEM_STACK_CODEC = com.sorrowmist.useless.compat.forge1201.recipe.RecipeCodecs.ITEM_STACK; + public static final RecipeType TYPE = new RecipeType<>() { + @Override public String toString() { return "useless_mod:advanced_alloy_furnace"; } + }; + + // 主 Codec(JSON / datapack) + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec( + instance -> instance.group( + ResourceLocation.CODEC.fieldOf("id").forGetter(AdvancedAlloyFurnaceRecipe::id), + CountedIngredient.CODEC.codec().listOf().fieldOf("ingredients") + .forGetter(AdvancedAlloyFurnaceRecipe::inputs), + INPUT_FLUIDS_CODEC.optionalFieldOf("input_fluids", List.of()) + .forGetter(AdvancedAlloyFurnaceRecipe::inputFluids), + com.sorrowmist.useless.compat.forge1201.recipe.AE2Codecs.GENERIC_STACK.listOf().optionalFieldOf("key_inputs", List.of()) + .forGetter(AdvancedAlloyFurnaceRecipe::keyInputs), + ITEM_STACK_CODEC.listOf().optionalFieldOf("outputs", List.of()) + .forGetter(AdvancedAlloyFurnaceRecipe::outputs), + com.sorrowmist.useless.compat.forge1201.recipe.RecipeCodecs.FLUID_STACK.listOf().optionalFieldOf("output_fluids", List.of()) + .forGetter(AdvancedAlloyFurnaceRecipe::outputFluids), + com.sorrowmist.useless.compat.forge1201.recipe.AE2Codecs.GENERIC_STACK.listOf().optionalFieldOf("key_outputs", List.of()) + .forGetter(AdvancedAlloyFurnaceRecipe::keyOutputs), + Codec.LONG.optionalFieldOf("energy", 2000L).forGetter(AdvancedAlloyFurnaceRecipe::energy), + Codec.INT.optionalFieldOf("process_time", 200).forGetter(AdvancedAlloyFurnaceRecipe::processTime), + com.sorrowmist.useless.compat.forge1201.recipe.RecipeCodecs.INGREDIENT.optionalFieldOf("catalyst", Ingredient.EMPTY) + .forGetter(AdvancedAlloyFurnaceRecipe::catalyst), + Codec.INT.optionalFieldOf("catalyst_uses", 0).forGetter(AdvancedAlloyFurnaceRecipe::catalystUses), + com.sorrowmist.useless.compat.forge1201.recipe.RecipeCodecs.INGREDIENT.optionalFieldOf("mold") + .forGetter(AdvancedAlloyFurnaceRecipe::legacyMold), + com.sorrowmist.useless.compat.forge1201.recipe.RecipeCodecs.INGREDIENT.listOf().optionalFieldOf("molds") + .forGetter(AdvancedAlloyFurnaceRecipe::multipleMolds), + AlloyFurnaceMode.CODEC.optionalFieldOf("mode", AlloyFurnaceMode.NORMAL) + .forGetter(AdvancedAlloyFurnaceRecipe::mode), + Codec.intRange(NO_EXPLICIT_TIER, MAX_TIER) + .optionalFieldOf("tier", NO_EXPLICIT_TIER) + .forGetter(AdvancedAlloyFurnaceRecipe::tier) + ).apply(instance, AdvancedAlloyFurnaceRecipe::fromCodec)); + + private static Optional legacyMold(AdvancedAlloyFurnaceRecipe recipe) { + return recipe.molds.size() == 1 ? Optional.of(recipe.molds.get(0)) : Optional.empty(); + } + + private static Optional> multipleMolds(AdvancedAlloyFurnaceRecipe recipe) { + return recipe.molds.size() > 1 ? Optional.of(recipe.molds) : Optional.empty(); + } + + private static AdvancedAlloyFurnaceRecipe fromCodec( + ResourceLocation id, + List inputs, + List inputFluids, + List keyInputs, + List outputs, + List outputFluids, + List keyOutputs, + long energy, + int processTime, + Ingredient catalyst, + int catalystUses, + Optional legacyMold, + Optional> multipleMolds, + AlloyFurnaceMode mode, + int tier) { + if (legacyMold.isPresent() && multipleMolds.isPresent()) { + throw new IllegalArgumentException("Advanced alloy-furnace recipe cannot define both mold and molds"); + } + List molds = multipleMolds.orElseGet(() -> + legacyMold.map(List::of).orElseGet(List::of)); + return new AdvancedAlloyFurnaceRecipe( + id, inputs, inputFluids, keyInputs, outputs, outputFluids, keyOutputs, + energy, processTime, catalyst, catalystUses, molds, mode, tier); + } + + public AdvancedAlloyFurnaceRecipe(ResourceLocation id, + List inputs, + Iterable inputFluids, + List keyInputs, + List outputs, + List outputFluids, + List keyOutputs, + long energy, + int processTime, + Ingredient catalyst, + int catalystUses, + List molds, + AlloyFurnaceMode mode) { + this(id, inputs, convertLegacyFluids(inputFluids), keyInputs, outputs, outputFluids, keyOutputs, + energy, processTime, catalyst, catalystUses, molds, mode, NO_EXPLICIT_TIER); + } + + public AdvancedAlloyFurnaceRecipe(ResourceLocation id, + List inputs, + Iterable inputFluids, + List outputs, + List outputFluids, + long energy, + int processTime, + Ingredient catalyst, + int catalystUses, + Ingredient mold, + AlloyFurnaceMode mode) { + this(id, inputs, convertLegacyFluids(inputFluids), List.of(), outputs, outputFluids, List.of(), energy, processTime, catalyst, catalystUses, + mold == null ? List.of() : List.of(mold), mode, NO_EXPLICIT_TIER); + } + + public AdvancedAlloyFurnaceRecipe(ResourceLocation id, + List inputs, + Iterable inputFluids, + List keyInputs, + List outputs, + List outputFluids, + List keyOutputs, + long energy, + int processTime, + Ingredient catalyst, + int catalystUses, + Ingredient mold, + AlloyFurnaceMode mode) { + this(id, inputs, convertLegacyFluids(inputFluids), keyInputs, outputs, outputFluids, keyOutputs, + energy, processTime, catalyst, catalystUses, + mold == null ? List.of() : List.of(mold), mode, NO_EXPLICIT_TIER); + } + + private static List convertLegacyFluids(Iterable fluids) { + if (fluids == null) return List.of(); + List result = new ArrayList<>(); + for (Object value : fluids) { + if (value instanceof LongSizedFluidIngredient ingredient + && ingredient.ingredient() != null + && !ingredient.ingredient().isEmpty() + && ingredient.amount() > 0) { + result.add(ingredient); + } else if (value instanceof SizedFluidIngredient ingredient + && ingredient.ingredient() != null + && !ingredient.ingredient().isEmpty() + && ingredient.amount() > 0) { + result.add(LongSizedFluidIngredient.from(ingredient)); + } else if (value instanceof FluidStack fluid + && !fluid.isEmpty() && fluid.getAmount() > 0) { + result.add(LongSizedFluidIngredient.from(fluid)); + } + } + return result; + } + + private static List normalizeFluidIngredients(List fluids) { + if (fluids == null || fluids.isEmpty()) return List.of(); + List normalized = new ArrayList<>(); + for (LongSizedFluidIngredient fluid : fluids) { + if (fluid == null || fluid.ingredient() == null || fluid.ingredient().isEmpty() + || fluid.amount() <= 0) continue; + int existing = -1; + for (int i = 0; i < normalized.size(); i++) { + if (normalized.get(i).ingredient().equals(fluid.ingredient())) { + existing = i; + break; + } + } + if (existing < 0) { + normalized.add(fluid); + } else { + long amount; + try { + amount = Math.addExact(normalized.get(existing).amount(), fluid.amount()); + } catch (ArithmeticException exception) { + throw new IllegalArgumentException("Fluid ingredient amount exceeds long range", exception); + } + normalized.set(existing, new LongSizedFluidIngredient(fluid.ingredient(), amount)); + } + } + return List.copyOf(normalized); + } + + /** Compatibility accessor for the historical single-mold API. */ + public Ingredient mold() { + return this.molds.isEmpty() ? Ingredient.EMPTY : this.molds.get(0); + } + + // ────────────── 以下是 Recipe 接口的占位实现 ────────────── + + @Override public ResourceLocation getId() { return id; } + + @Override public boolean matches(Container recipeInput, Level level) { + return false; // 机器自己匹配,不走原版系统 + } + + @Override public ItemStack assemble(Container recipeInput, net.minecraft.core.RegistryAccess provider) { + return this.outputs.isEmpty() ? ItemStack.EMPTY : this.outputs.get(0).copy(); + } + + @Override + public boolean canCraftInDimensions(int width, int height) { + return true; + } + + @Override + public ItemStack getResultItem(net.minecraft.core.RegistryAccess registries) { + return this.assemble(null, registries); + } + + @Override + public NonNullList getIngredients() { + NonNullList list = NonNullList.create(); + this.inputs.forEach(ci -> list.add(ci.ingredient())); + return list; + } + + @Override + public @NotNull RecipeSerializer getSerializer() { + return AdvancedAlloyFurnaceRecipeSerializer.INSTANCE; // 替换成你实际的注册对象 + } + + @Override + public RecipeType getType() { + return TYPE; // 替换成你实际的注册对象 + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/AdvancedAlloyFurnaceRecipeBuilder.java b/src/main/java/com/sorrowmist/useless/content/recipe/AdvancedAlloyFurnaceRecipeBuilder.java new file mode 100644 index 00000000..4ace00c2 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/AdvancedAlloyFurnaceRecipeBuilder.java @@ -0,0 +1,197 @@ +package com.sorrowmist.useless.content.recipe; + +import appeng.api.stacks.GenericStack; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import net.minecraft.data.recipes.FinishedRecipe; +import java.util.function.Consumer; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.ItemLike; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class AdvancedAlloyFurnaceRecipeBuilder { + + private final List inputs = new ArrayList<>(); + private final List inputFluids = new ArrayList<>(); + private final List keyInputs = new ArrayList<>(); + private final List outputs = new ArrayList<>(); + private final List outputFluids = new ArrayList<>(); + private final List keyOutputs = new ArrayList<>(); + private long energy = 2000L; + private int processTime = 200; + private Ingredient catalyst = Ingredient.EMPTY; + private int catalystUses = 0; + private final List molds = new ArrayList<>(); + private AlloyFurnaceMode mode = AlloyFurnaceMode.NORMAL; + private int tier = AdvancedAlloyFurnaceRecipe.NO_EXPLICIT_TIER; + + public static AdvancedAlloyFurnaceRecipeBuilder create() { + return new AdvancedAlloyFurnaceRecipeBuilder(); + } + + public AdvancedAlloyFurnaceRecipeBuilder input(Ingredient ingredient, long count) { + this.inputs.add(new CountedIngredient(Objects.requireNonNull(ingredient, "ingredient"), count)); + return this; + } + + public AdvancedAlloyFurnaceRecipeBuilder input(ItemLike item, long count) { + return input(Ingredient.of(Objects.requireNonNull(item, "item")), count); + } + + public AdvancedAlloyFurnaceRecipeBuilder fluidInput(FluidStack fluid) { + SizedFluidIngredient converted = fluid == null || fluid.isEmpty() ? null : SizedFluidIngredient.of(fluid); + if (converted != null) this.inputFluids.add(LongSizedFluidIngredient.from(converted)); + return this; + } + + public AdvancedAlloyFurnaceRecipeBuilder fluidInput(SizedFluidIngredient fluid) { + if (fluid != null) { + this.inputFluids.add(LongSizedFluidIngredient.from(fluid)); + } + return this; + } + + public AdvancedAlloyFurnaceRecipeBuilder fluidInput(LongSizedFluidIngredient fluid) { + if (fluid != null) { + this.inputFluids.add(fluid); + } + return this; + } + + public AdvancedAlloyFurnaceRecipeBuilder keyInput(GenericStack keyInput) { + if (keyInput != null) { + this.keyInputs.add(keyInput); + } + return this; + } + + public AdvancedAlloyFurnaceRecipeBuilder output(ItemLike item, int count) { + this.outputs.add(new ItemStack(Objects.requireNonNull(item, "item"), count)); + return this; + } + + public AdvancedAlloyFurnaceRecipeBuilder output(ItemStack output) { + if (output != null && !output.isEmpty()) { + this.outputs.add(output.copy()); + } + return this; + } + + public AdvancedAlloyFurnaceRecipeBuilder fluidOutput(FluidStack output) { + if (output != null && !output.isEmpty()) { + this.outputFluids.add(output.copy()); + } + return this; + } + + public AdvancedAlloyFurnaceRecipeBuilder keyOutput(GenericStack keyOutput) { + if (keyOutput != null) { + this.keyOutputs.add(keyOutput); + } + return this; + } + + public AdvancedAlloyFurnaceRecipeBuilder energy(long energy) { + this.energy = energy; + return this; + } + + public AdvancedAlloyFurnaceRecipeBuilder processTime(int ticks) { + this.processTime = ticks; + return this; + } + + public AdvancedAlloyFurnaceRecipeBuilder catalyst(Ingredient catalyst, int uses) { + this.catalyst = Objects.requireNonNull(catalyst, "catalyst"); + this.catalystUses = uses; + return this; + } + + public AdvancedAlloyFurnaceRecipeBuilder mold(Ingredient mold) { + this.molds.clear(); + if (mold != null) { + this.molds.add(mold); + } + return this; + } + + /** Replaces the complete set of independent mold requirements. */ + public AdvancedAlloyFurnaceRecipeBuilder molds(List molds) { + this.molds.clear(); + if (molds != null) { + this.molds.addAll(molds); + } + return this; + } + + /** Replaces the complete set of independent mold requirements. */ + public AdvancedAlloyFurnaceRecipeBuilder molds(Ingredient... molds) { + this.molds.clear(); + if (molds != null) { + for (Ingredient mold : molds) { + this.molds.add(mold); + } + } + return this; + } + + /** Adds one independent mold requirement. */ + public AdvancedAlloyFurnaceRecipeBuilder addMold(Ingredient mold) { + if (mold != null) { + this.molds.add(mold); + } + return this; + } + + public AdvancedAlloyFurnaceRecipeBuilder mode(AlloyFurnaceMode mode) { + this.mode = Objects.requireNonNull(mode, "mode"); + return this; + } + + public AdvancedAlloyFurnaceRecipeBuilder tier(int tier) { + this.tier = tier; + return this; + } + + /** Builds an in-memory recipe for use by an adapter conversion method. */ + public AdvancedAlloyFurnaceRecipe build(ResourceLocation id) { + Objects.requireNonNull(id, "id"); + return new AdvancedAlloyFurnaceRecipe( + id, + List.copyOf(this.inputs), + List.copyOf(this.inputFluids), + List.copyOf(this.keyInputs), + this.outputs.stream().map(ItemStack::copy).toList(), + this.outputFluids.stream().map(FluidStack::copy).toList(), + List.copyOf(this.keyOutputs), + this.energy, + this.processTime, + this.catalyst, + this.catalystUses, + List.copyOf(this.molds), + this.mode, + this.tier + ); + } + + public void save(Consumer output, ResourceLocation id) { + AdvancedAlloyFurnaceRecipe recipe=build(id); + output.accept(new FinishedRecipe() { + @Override public void serializeRecipeData(com.google.gson.JsonObject target) { + var json=AdvancedAlloyFurnaceRecipe.CODEC.codec().encodeStart(com.mojang.serialization.JsonOps.INSTANCE,recipe) + .getOrThrow(false,message -> { throw new IllegalArgumentException(message); }).getAsJsonObject(); + json.entrySet().forEach(entry -> target.add(entry.getKey(),entry.getValue())); + } + @Override public ResourceLocation getId() { return id; } + @Override public net.minecraft.world.item.crafting.RecipeSerializer getType() { return recipe.getSerializer(); } + @Override public com.google.gson.JsonObject serializeAdvancement() { return null; } + @Override public ResourceLocation getAdvancementId() { return null; } + }); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/AdvancedAlloyFurnaceRecipeSerializer.java b/src/main/java/com/sorrowmist/useless/content/recipe/AdvancedAlloyFurnaceRecipeSerializer.java new file mode 100644 index 00000000..31d2b595 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/AdvancedAlloyFurnaceRecipeSerializer.java @@ -0,0 +1,26 @@ +package com.sorrowmist.useless.content.recipe; + +import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; +import com.mojang.serialization.JsonOps; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.crafting.RecipeSerializer; + +public final class AdvancedAlloyFurnaceRecipeSerializer implements RecipeSerializer { + public static final AdvancedAlloyFurnaceRecipeSerializer INSTANCE = new AdvancedAlloyFurnaceRecipeSerializer(); + @Override public AdvancedAlloyFurnaceRecipe fromJson(ResourceLocation id,JsonObject input) { + // The manager/resource path is authoritative. KubeJS may omit or replace a JSON id. + JsonObject json=input.deepCopy(); json.addProperty("id",id.toString()); + return AdvancedAlloyFurnaceRecipe.CODEC.codec().parse(JsonOps.INSTANCE,json) + .getOrThrow(false,message -> { throw new JsonSyntaxException(id+": "+message); }); + } + @Override public AdvancedAlloyFurnaceRecipe fromNetwork(ResourceLocation id,FriendlyByteBuf buffer) { + AdvancedAlloyFurnaceRecipe recipe=AdvancedAlloyFurnaceRecipe.STREAM_CODEC.decode(buffer); + if(!id.equals(recipe.id())) throw new IllegalArgumentException("Recipe packet ID does not match manager ID: "+id); + return recipe; + } + @Override public void toNetwork(FriendlyByteBuf buffer,AdvancedAlloyFurnaceRecipe recipe) { + AdvancedAlloyFurnaceRecipe.STREAM_CODEC.encode(buffer,recipe); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/AlloyFurnaceRecipeCatalog.java b/src/main/java/com/sorrowmist/useless/content/recipe/AlloyFurnaceRecipeCatalog.java new file mode 100644 index 00000000..de3795b1 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/AlloyFurnaceRecipeCatalog.java @@ -0,0 +1,1150 @@ +package com.sorrowmist.useless.content.recipe; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.stacks.AEFluidKey; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.AdvancedAlloyFurnacePatternResolver; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.DynamicComponentPattern; +import com.sorrowmist.useless.core.component.OmniversalPatternData; +import com.sorrowmist.useless.content.recipe.adapters.RecipeAdapterCompatRegistry; +import com.sorrowmist.useless.init.ModRecipeTypes; +import com.mojang.logging.LogUtils; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.OptionalLong; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import org.slf4j.Logger; + +/** Server/client recipe directory used by JEI, the encoder and pattern validation. */ +public final class AlloyFurnaceRecipeCatalog { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final Map CACHE = java.util.Collections.synchronizedMap(new WeakHashMap<>()); + private static final Map BUILD_LOCKS = java.util.Collections.synchronizedMap(new WeakHashMap<>()); + /** + * Client and integrated-server levels live in the same JVM but have different recipe managers. + * Their cache generations must therefore be independent: a client recipe update must not make + * the server catalog look stale while a pattern is being encoded. + */ + private static final Map GENERATIONS = new WeakHashMap<>(); + private static final AtomicInteger CURRENT_RECIPE_COUNT = new AtomicInteger(); + private static final AtomicLong GENERATION = new AtomicLong(); + + private AlloyFurnaceRecipeCatalog() { + } + + public record Entry(AlloyFurnaceRecipeIdentity identity, AdvancedAlloyFurnaceRecipe recipe, String sourceId) { + public Entry { + sourceId = RecipeSourceIds.normalize(sourceId); + } + + public Entry(AlloyFurnaceRecipeIdentity identity, AdvancedAlloyFurnaceRecipe recipe) { + this(identity, recipe, RecipeSourceIds.UNKNOWN); + } + } + + public static List entries(Level level) { + if (level == null) return List.of(); + return snapshot(level).entries; + } + + public static List entries(Level level, String sourceId) { + if (level == null) return List.of(); + String normalizedSource = RecipeSourceIds.normalize(sourceId); + return snapshot(level).bySource.getOrDefault(normalizedSource, List.of()); + } + + /** Builds and publishes the immutable current-generation snapshot synchronously. */ + public static void prewarm(Level level) { + if (level != null) snapshot(level); + } + + /** + * 返回当前世代快照是否已构建完成、可供查询。 + * + *

供调用方判断「此刻能否安全消费目录」——例如客户端要在目录就绪后再刷新 JEI 展示, + * 以及延迟任务要在目录就绪后再解码样板,避免在构建完成前读到空列表。

+ */ + public static boolean isReady(Level level) { + return level != null && snapshotIfReady(level) != null; + } + + public static List recipes(Level level) { + return entries(level).stream().map(Entry::recipe).toList(); + } + + /** Returns the number of unique recipes in the most recently built catalog snapshot. */ + public static int currentRecipeCount() { + return CURRENT_RECIPE_COUNT.get(); + } + + /** + * Returns whether the stack is used as a mold by any recipe currently + * exposed through the shared alloy-furnace recipe directory. + */ + public static boolean isKnownMold(Level level, ItemStack stack) { + if (level == null || stack == null || stack.isEmpty()) return false; + + Snapshot snapshot = snapshot(level); + AEItemKey key = AEItemKey.of(stack); + if (key == null) return false; + Boolean cached = snapshot.knownMoldCache.get(key); + if (cached != null) return cached; + + boolean result = isKnownMoldInEntries(stack, snapshot.entries); + snapshot.knownMoldCache.put(key, result); + return result; + } + + private static boolean isKnownMoldInEntries(ItemStack stack, List entries) { + for (Entry entry : entries) { + AdvancedAlloyFurnaceRecipe recipe = entry.recipe(); + if (recipe == null) continue; + for (var mold : recipe.molds()) { + if (mold != null && !mold.isEmpty() && AdapterUtils.matchesMold(mold, stack)) { + return true; + } + } + } + return false; + } + + static boolean isKnownMold(ItemStack stack, Iterable recipes) { + if (stack == null || stack.isEmpty() || recipes == null) return false; + for (AdvancedAlloyFurnaceRecipe recipe : recipes) { + if (recipe == null) continue; + for (var mold : recipe.molds()) { + if (mold != null && !mold.isEmpty() && AdapterUtils.matchesMold(mold, stack)) { + return true; + } + } + } + return false; + } + + public static Optional resolve(Level level, AlloyFurnaceRecipeIdentity identity) { + if (level == null || identity == null) return Optional.empty(); + Snapshot snapshot = snapshotIfReady(level); + if (snapshot == null) return Optional.empty(); + Entry resolved = snapshot.byIdentity.get(identity); + return Optional.ofNullable(resolved); + } + + /** Resolves by source first, retaining the all-source path for old metadata and unknown sources. */ + public static Optional resolve( + Level level, String sourceId, AlloyFurnaceRecipeIdentity identity) { + if (level == null || identity == null) return Optional.empty(); + String normalizedSource = RecipeSourceIds.normalize(sourceId); + if (RecipeSourceIds.UNKNOWN.equals(normalizedSource)) return resolve(level, identity); + Snapshot snapshot = snapshotIfReady(level); + if (snapshot == null) return Optional.empty(); + Entry resolved = snapshot.bySourceAndRecipeId + .getOrDefault(new SourceRecipeKey(normalizedSource, identity.recipeId()), List.of()) + .stream() + .filter(entry -> entry.identity().equals(identity)) + .findFirst() + .orElse(null); + return resolved == null ? resolve(level, identity) : Optional.of(resolved); + } + + /** + * Resolves a pattern binding when the stored fingerprint differs across the client/server + * boundary but the recipe id and encoded processing contents identify exactly one current recipe. + * The exact identity remains the first choice; the fallback is deliberately unique-only so two + * recipes sharing a pattern cannot silently acquire the wrong mold. + */ + public static Optional resolvePattern( + Level level, AlloyFurnaceRecipeIdentity identity, IPatternDetails pattern) { + if (level == null || identity == null || pattern == null) return Optional.empty(); + + Optional exact = resolve(level, identity); + if (exact.isPresent()) return exact; + + Snapshot snapshot = snapshotIfReady(level); + if (snapshot == null) return Optional.empty(); + Set componentAgnosticOutputs = componentAgnosticOutputSlots(pattern, level, null); + List candidates = snapshot.byRecipeId + .getOrDefault(identity.recipeId(), List.of()) + .stream() + .filter(entry -> matchesPattern( + entry.recipe(), pattern, componentAgnosticOutputs, true)) + .toList(); + if (candidates.size() != 1) { + // A pattern encoded on the other side of a client/server boundary can carry the same + // item output with a different component patch (or a legacy encoder can omit a + // secondary output). The recipe id is already bound by the JEI selection, so use a + // component-insensitive shape check only when the normal exact candidate search found + // none. This still refuses ambiguous ids and never turns an unrelated pattern into a + // recipe binding. + if (!candidates.isEmpty()) { + return Optional.empty(); + } + List shapeCandidates = snapshot.byRecipeId + .getOrDefault(identity.recipeId(), List.of()) + .stream() + .filter(entry -> ignoresItemOutputComponents(entry.recipe(), componentAgnosticOutputs) + && matchesPatternShape(entry.recipe(), pattern)) + .toList(); + candidates = shapeCandidates; + } + if (candidates.size() != 1) return Optional.empty(); + + Entry compatible = candidates.get(0); + snapshot.compatibilityAliases.putIfAbsent(identity, compatible); + return Optional.of(compatible); + } + + public static Optional resolvePattern( + Level level, String sourceId, AlloyFurnaceRecipeIdentity identity, IPatternDetails pattern) { + if (level == null || identity == null || pattern == null) return Optional.empty(); + String normalizedSource = RecipeSourceIds.normalize(sourceId); + if (RecipeSourceIds.UNKNOWN.equals(normalizedSource)) { + return resolvePattern(level, identity, pattern); + } + Optional exact = resolve(level, normalizedSource, identity); + if (exact.isPresent()) return exact; + Snapshot snapshot = snapshotIfReady(level); + if (snapshot == null) return Optional.empty(); + Set componentAgnosticOutputs = componentAgnosticOutputSlots(pattern, level, normalizedSource); + List candidates = snapshot.bySourceAndRecipeId + .getOrDefault(new SourceRecipeKey(normalizedSource, identity.recipeId()), List.of()) + .stream() + .filter(entry -> matchesPattern( + entry.recipe(), pattern, componentAgnosticOutputs, true)) + .toList(); + if (candidates.size() == 1) return Optional.of(candidates.get(0)); + return resolvePattern(level, identity, pattern); + } + + /** + * Version-one patterns used representation-sensitive fingerprints. Accept + * them only when their recipe id and encoded processing contents identify + * exactly one current recipe, then cache that old identity as an alias. + */ + public static Optional resolveLegacyPattern( + Level level, AlloyFurnaceRecipeIdentity legacyIdentity, IPatternDetails pattern) { + return resolveLegacyPattern(level, legacyIdentity, pattern, 1); + } + + public static Optional resolveLegacyPattern( + Level level, AlloyFurnaceRecipeIdentity legacyIdentity, IPatternDetails pattern, int version) { + if (level == null || legacyIdentity == null || pattern == null) return Optional.empty(); + Snapshot snapshot = snapshotIfReady(level); + if (snapshot == null) return Optional.empty(); + Entry cached = snapshot.byIdentity.get(legacyIdentity); + if (cached != null) return Optional.of(cached); + cached = snapshot.compatibilityAliases.get(legacyIdentity); + if (cached != null) return Optional.of(cached); + if (snapshot.legacyMisses.contains(legacyIdentity)) return Optional.empty(); + + Entry match = null; + for (Entry entry : snapshot.byRecipeId.getOrDefault(legacyIdentity.recipeId(), List.of())) { + boolean validLegacyIdentity = level.isClientSide + ? matchesPattern(entry.recipe, pattern) + : legacyFingerprint(entry.recipe, level, version) + .equals(legacyIdentity.fingerprint()); + if (!validLegacyIdentity) continue; + if (match != null) { + snapshot.legacyMisses.add(legacyIdentity); + return Optional.empty(); + } + match = entry; + } + if (match == null) { + snapshot.legacyMisses.add(legacyIdentity); + return Optional.empty(); + } + Entry existing = snapshot.compatibilityAliases.putIfAbsent(legacyIdentity, match); + return Optional.of(existing == null ? match : existing); + } + + public static Optional resolveLegacyPattern( + Level level, String sourceId, AlloyFurnaceRecipeIdentity legacyIdentity, + IPatternDetails pattern, int version) { + if (level == null || legacyIdentity == null || pattern == null) return Optional.empty(); + String normalizedSource = RecipeSourceIds.normalize(sourceId); + if (RecipeSourceIds.UNKNOWN.equals(normalizedSource)) { + return resolveLegacyPattern(level, legacyIdentity, pattern, version); + } + Snapshot snapshot = snapshotIfReady(level); + if (snapshot == null) return Optional.empty(); + Entry match = null; + for (Entry entry : snapshot.bySourceAndRecipeId + .getOrDefault(new SourceRecipeKey(normalizedSource, legacyIdentity.recipeId()), List.of())) { + boolean valid = level.isClientSide + ? matchesPattern(entry.recipe(), pattern) + : legacyFingerprint(entry.recipe(), level, version).equals(legacyIdentity.fingerprint()); + if (!valid) continue; + if (match != null) return resolveLegacyPattern(level, legacyIdentity, pattern, version); + match = entry; + } + return match == null + ? resolveLegacyPattern(level, legacyIdentity, pattern, version) + : Optional.of(match); + } + + private static String legacyFingerprint( + AdvancedAlloyFurnaceRecipe recipe, Level level, int version) { + return version >= OmniversalPatternData.SEMANTIC_FINGERPRINT_VERSION + ? AlloyFurnaceRecipeFingerprint.createLegacySemantic(recipe, level.registryAccess()) + : AlloyFurnaceRecipeFingerprint.createLegacy(recipe, level.registryAccess()); + } + + public static List findPatternCandidates(Level level, IPatternDetails pattern) { + return findPatternCandidates(level, pattern, false); + } + + /** + * Finds recipes represented by a processing pattern. + * + *

When {@code allowMissingOutputs} is true, the pattern may omit any recipe outputs, but it + * must still contain at least one valid output. This mode is intended for the omniversal + * pattern converter; the strict mode remains the default for normal recipe lookup. + */ + public static List findPatternCandidates( + Level level, IPatternDetails pattern, boolean allowMissingOutputs) { + if (level == null || pattern == null) return List.of(); + PatternContents contents = PatternContents.read(pattern); + if (contents == null) return List.of(); + // Resolved once per lookup: identifying dynamic slots scans every fusion recipe, and the + // answer depends only on the pattern, not on the candidate being tested. + Set componentAgnosticOutputs = componentAgnosticOutputSlots(pattern, level); + return entries(level).stream() + .filter(entry -> findPatternScale( + entry.recipe, pattern, contents, + componentAgnosticOutputs, allowMissingOutputs).isPresent()) + .sorted(Comparator.comparing(entry -> entry.identity.recipeId().toString())) + .toList(); + } + + /** + * Resolves an encoded processing pattern to a single recipe that has a reusable mold. This is + * used only as a conservative fallback when the JEI selection packet and AE2's encoding update + * arrive in the opposite order. Recipes without molds are intentionally excluded because a + * plain processing pattern is already the correct representation for them. + */ + public static Optional findUniqueMoldPatternCandidate(Level level, IPatternDetails pattern) { + List candidates = findPatternCandidates(level, pattern).stream() + .filter(entry -> entry.recipe() != null && !entry.recipe().molds().isEmpty()) + .toList(); + return candidates.size() == 1 ? Optional.of(candidates.get(0)) : Optional.empty(); + } + + /** + * Whether the encoded contents of {@code pattern} describe {@code recipe}. + * + *

Used when the recipe is already known — the player picked it in JEI — so the only remaining + * question is whether the slots AE2 filled actually correspond to it. Unlike + * {@link #findPatternCandidates}, this never searches, so a recipe that shares its inputs and + * outputs with another one is still accepted: ambiguity was resolved by the player's click. + */ + public static boolean matchesRecipe(Level level, AdvancedAlloyFurnaceRecipe recipe, IPatternDetails pattern) { + return matchesRecipe(level, null, recipe, pattern); + } + + public static boolean matchesRecipe( + Level level, String sourceId, AdvancedAlloyFurnaceRecipe recipe, IPatternDetails pattern) { + if (level == null || recipe == null || pattern == null) return false; + return matchesPattern(recipe, pattern, + componentAgnosticOutputSlots(pattern, level, sourceId), true); + } + + /** + * Returns the positive integer number of base recipe operations represented by a pattern. + * The same proportional matcher used for recipe resolution is used here so callers can + * normalize a manually multiplied pattern without accepting a non-proportional edit. + */ + public static OptionalLong findPatternScale( + Level level, AdvancedAlloyFurnaceRecipe recipe, IPatternDetails pattern) { + return findPatternScale(level, null, recipe, pattern); + } + + public static OptionalLong findPatternScale( + Level level, String sourceId, AdvancedAlloyFurnaceRecipe recipe, IPatternDetails pattern) { + if (level == null || recipe == null || pattern == null) return OptionalLong.empty(); + return findPatternScale(recipe, pattern, + componentAgnosticOutputSlots(pattern, level, sourceId), true); + } + + /** + * Output slots whose components must be ignored when matching a pattern against a recipe. + * + *

Draconic Evolution fusion recipes copy components from the catalyst onto the result + * ({@code IFusionDataTransfer}), so the encoded pattern carries components that the catalogued + * recipe — built from the recipe's static result — cannot have. Those slots are exactly the ones + * the resolver already marks as item-id-only, so reuse that decision instead of relaxing every + * output (which would collapse recipes differing only by an output component, e.g. Productive + * Bees honeycomb {@code bee_type}). + */ + private static Set componentAgnosticOutputSlots(IPatternDetails pattern, Level level) { + return componentAgnosticOutputSlots(pattern, level, null); + } + + private static Set componentAgnosticOutputSlots( + IPatternDetails pattern, Level level, String sourceId) { + IPatternDetails resolved = AdvancedAlloyFurnacePatternResolver.resolve(pattern, level, sourceId); + if (!(resolved instanceof DynamicComponentPattern dynamic)) { + + return Set.of(); + } + + Set slots = new LinkedHashSet<>(); + List outputs = java.util.Arrays.asList(resolved.getOutputs()); + for (int slot = 0; slot < outputs.size(); slot++) { + if (dynamic.isItemIdOutput(slot)) slots.add(slot); + } + + return slots; + } + + /** Invalidates every recipe-manager catalog, used when no level-specific owner is available. */ + public static void invalidate() { + synchronized (CACHE) { + CACHE.clear(); + GENERATIONS.replaceAll((ignored, generation) -> generation + 1L); + } + CURRENT_RECIPE_COUNT.set(0); + GENERATION.incrementAndGet(); + } + + /** Invalidates only the catalog owned by {@code level}. */ + public static void invalidate(Level level) { + if (level == null) { + invalidate(); + return; + } + + Object cacheKey = level.getRecipeManager(); + synchronized (CACHE) { + GENERATIONS.put(cacheKey, currentGenerationLocked(cacheKey) + 1L); + CACHE.remove(cacheKey); + } + CURRENT_RECIPE_COUNT.set(0); + GENERATION.incrementAndGet(); + } + + /** Returns the global invalidation count retained for diagnostics and compatibility. */ + public static long generation() { + return GENERATION.get(); + } + + private static Snapshot snapshot(Level level) { + Object cacheKey = level.getRecipeManager(); + while (true) { + long generation; + Snapshot cached; + synchronized (CACHE) { + generation = currentGenerationLocked(cacheKey); + cached = CACHE.get(cacheKey); + } + if (cached != null && cached.generation == generation) { + return cached; + } + + Object buildLock = buildLock(cacheKey); + synchronized (buildLock) { + synchronized (CACHE) { + generation = currentGenerationLocked(cacheKey); + cached = CACHE.get(cacheKey); + if (cached != null && cached.generation == generation) { + return cached; + } + } + Snapshot built = build(level, generation); + synchronized (CACHE) { + if (currentGenerationLocked(cacheKey) != generation) continue; + cached = CACHE.get(cacheKey); + if (cached != null && cached.generation == generation) { + return cached; + } + CACHE.put(cacheKey, built); + return built; + } + } + } + } + + /** + * Returns the current catalog without doing work. Pattern decoding can run on the server + * thread, so a cache miss here must not turn into a synchronous scan of every adapter recipe. + */ + private static Snapshot snapshotIfReady(Level level) { + if (level == null) return null; + Object cacheKey = level.getRecipeManager(); + synchronized (CACHE) { + long generation = currentGenerationLocked(cacheKey); + Snapshot cached = CACHE.get(cacheKey); + return cached != null && cached.generation == generation ? cached : null; + } + } + + private static long currentGenerationLocked(Object cacheKey) { + return GENERATIONS.computeIfAbsent(cacheKey, ignored -> 0L); + } + + private static Object buildLock(Object cacheKey) { + synchronized (BUILD_LOCKS) { + return BUILD_LOCKS.computeIfAbsent(cacheKey, ignored -> new Object()); + } + } + + private static Snapshot build(Level level, long generation) { + long startedAt = System.nanoTime(); + List recipes = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(level.getRecipeManager(), ModRecipeTypes.ADVANCED_ALLOY_FURNACE_TYPE.get())) { + recipes.add(new CollectedRecipe(holder.value(), RecipeSourceIds.CORE)); + } + // 先把 adapter 分成「合成配方」与「转换配方」两类。转换类只在这里登记,稍后对全表做单次遍历, + // 避免每个 adapter 都把整张配方表扫一遍(adapter 数量 × 全服配方数)。 + List converting = new ArrayList<>(); + for (com.sorrowmist.useless.api.recipe.IRecipeAdapter adapter : AlloyFurnaceRecipeManager.getInstance().getRegisteredAdapters()) { + if (adapter.getClass().getPackageName().contains(".ae.ae2lt")) continue; + String sourceId = AlloyFurnaceRecipeManager.getInstance().getAdapterSourceId(adapter); + if (adapter.getClass().getName().equals("com.sorrowmist.useless.content.recipe.adapters.mysticalagriculture.SeedEssenceRecipeAdapter") + || adapter.getClass().getName().equals("com.sorrowmist.useless.content.recipe.adapters.ae.ae2cs.CrystalGrowthRecipeAdapter")) { + try { + for (Object value:(List)adapter.getClass().getMethod("getAllRecipes").invoke(adapter)) + recipes.add(new CollectedRecipe((AdvancedAlloyFurnaceRecipe)value,sourceId)); + } catch (ReflectiveOperationException error) { + throw new IllegalStateException("Cannot enumerate installed synthetic adapter "+adapter.getClass().getName(),error); + } + continue; + } + collectGenerated(adapter, sourceId, level, recipes); + converting.add(new AdapterSource(adapter, sourceId)); + } + + collectConverted(converting, com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getRecipes(level.getRecipeManager()), level, recipes); + if (RecipeAdapterCompatRegistry.isLoaded(RecipeAdapterCompatRegistry.AE2LT)) { + List optional=com.sorrowmist.useless.compat.forge1201.OptionalIntegration.call("ae2lt", + "com.sorrowmist.useless.content.recipe.adapters.ae.ae2lt.AELightningTechCompatLoader","getJeiRecipes", + List.class,List.of(),new Class[]{net.minecraft.world.item.crafting.RecipeManager.class,Level.class}, + level.getRecipeManager(),level); + optional.forEach(recipe -> recipes.add(new CollectedRecipe((AdvancedAlloyFurnaceRecipe)recipe,RecipeSourceIds.AE2LT))); + } + // 指纹计算是纯 CPU 密集操作(codec 整条编码 + 成分规范化 + canonicalize + SHA-256), + // 数万条配方时它占据了目录构建剩余耗时的绝大部分。这里并行计算指纹,再按原有先后 + // 顺序合并:putIfAbsent 的次序与串行版本完全一致,去重结果因此保持不变。 + // 每条配方各自创建序列化上下文(create 内部完成),故并行不存在共享可变状态。 + List fingerprinted = recipes.parallelStream() + .map(collected -> fingerprint(collected, level)) + .filter(Objects::nonNull) + .toList(); + Map unique = new LinkedHashMap<>(); + for (FingerprintedRecipe item : fingerprinted) { + unique.putIfAbsent(item.identity(), new Entry(item.identity(), item.recipe(), item.sourceId())); + } + List ordered = unique.values().stream() + .sorted(Comparator.comparing((Entry entry) -> entry.identity.recipeId().toString()) + .thenComparing(entry -> entry.identity.fingerprint())) + .toList(); + CURRENT_RECIPE_COUNT.set(ordered.size()); + Map> byRecipeId = new LinkedHashMap<>(); + for (Entry entry : ordered) { + byRecipeId.computeIfAbsent(entry.identity.recipeId(), ignored -> new ArrayList<>()).add(entry); + } + byRecipeId.replaceAll((ignored, entries) -> List.copyOf(entries)); + Map> bySource = new LinkedHashMap<>(); + Map> bySourceAndRecipeId = new LinkedHashMap<>(); + for (Entry entry : ordered) { + bySource.computeIfAbsent(entry.sourceId(), ignored -> new ArrayList<>()).add(entry); + bySourceAndRecipeId.computeIfAbsent( + new SourceRecipeKey(entry.sourceId(), entry.identity().recipeId()), + ignored -> new ArrayList<>()).add(entry); + } + bySource.replaceAll((ignored, entries) -> List.copyOf(entries)); + bySourceAndRecipeId.replaceAll((ignored, entries) -> List.copyOf(entries)); + Snapshot snapshot = new Snapshot(ordered, Map.copyOf(unique), Map.copyOf(byRecipeId), + Map.copyOf(bySource), Map.copyOf(bySourceAndRecipeId), generation, + new ConcurrentHashMap<>(), new ConcurrentHashMap<>(), + ConcurrentHashMap.newKeySet()); + LOGGER.info("Built alloy-furnace recipe catalog: generation={}, recipes={}, sources={}, elapsed={} ms", + generation, ordered.size(), bySource.size(), (System.nanoTime() - startedAt) / 1_000_000L); + return snapshot; + } + + /** 一个待做配方转换的 adapter 及其来源标识。 */ + private record AdapterSource(com.sorrowmist.useless.api.recipe.IRecipeAdapter adapter, String sourceId) { + } + + /** 已完成指纹计算的配方,用于在并行计算后按原顺序合并。 */ + private record FingerprintedRecipe( + AlloyFurnaceRecipeIdentity identity, AdvancedAlloyFurnaceRecipe recipe, String sourceId) { + } + + /** + * 为单条配方计算身份指纹。无法编码的配方返回 {@code null} 并只记一行日志:这是 + * 「可预期的坏配方」而不是故障,明细放 DEBUG 的配方字段 dump,不刷整条异常栈。 + * + *

本方法被并行调用,因此只读取入参、不触碰任何可变共享状态。

+ */ + private static FingerprintedRecipe fingerprint(CollectedRecipe collected, Level level) { + AdvancedAlloyFurnaceRecipe recipe = collected.recipe(); + if (recipe == null) return null; + try { + AlloyFurnaceRecipeIdentity identity = new AlloyFurnaceRecipeIdentity( + recipe.id(), AlloyFurnaceRecipeFingerprint.create(recipe, level.registryAccess())); + return new FingerprintedRecipe(identity, recipe, collected.sourceId()); + } catch (RuntimeException exception) { + RateLimitedLogger.warn(LOGGER, "Skipping alloy-furnace recipe with an unencodable identity: {} ({})", + recipe.id(), exception.getMessage()); + + return null; + } + } + + /** + * 单次遍历全部源配方,按配方类分派给对应的 adapter。 + * + *

这里刻意反过来组织循环:外层是配方、内层是按类索引的 adapter 列表。原先每个 adapter 都 + * 自己扫一遍 {@code holders},复杂度是 adapter 数 × 全服配方数;整合包里 adapter 有数十个、 + * 配方上万条时这一项会主导整个目录构建耗时。

+ */ + private static void collectConverted(List sources, Collection> holders, + Level level, List output) { + if (sources.isEmpty() || holders.isEmpty()) return; + + // 同一个具体配方类只需解析一次「哪些 adapter 能处理它」。配方类型数远小于配方条数, + // 因此类型判断的总次数从「配方条数 × adapter 数」降到「配方类型数 × adapter 数」。 + Map, List> resolvedByRecipeClass = new HashMap<>(); + for (RecipeHolder holder : holders) { + Object value = holder.value(); + if (value == null) continue; + List matched = resolvedByRecipeClass.computeIfAbsent( + value.getClass(), recipeClass -> resolveAdapters(sources, recipeClass)); + if (matched.isEmpty()) continue; + convertWith(matched, holder, level, output); + } + } + + /** + * 返回所有能处理 {@code recipeClass} 的 adapter。 + * + *

按 {@code sources} 的注册顺序返回,与原先「每个 adapter 各扫一遍全表」时的执行次序一致; + * 同一个配方被多个 adapter 命中时,转换结果的先后顺序因此保持不变。

+ */ + private static List resolveAdapters(List sources, Class recipeClass) { + List matched = new ArrayList<>(); + for (AdapterSource source : sources) { + Class declared = source.adapter().getRecipeClass(); + // adapter 声明的源配方类可能是抽象基类,实际配方是其子类,所以要按可赋值方向判断。 + if (declared != null && declared.isAssignableFrom(recipeClass)) { + matched.add(source); + } + } + return List.copyOf(matched); + } + + private static void convertWith(List sources, RecipeHolder holder, Level level, + List output) { + for (AdapterSource source : sources) { + RecipeConversionUtils.convertAll(source.adapter(), holder, level) + .forEach(recipe -> output.add(new CollectedRecipe(recipe, source.sourceId()))); + } + } + + private static void collectGenerated(com.sorrowmist.useless.api.recipe.IRecipeAdapter adapter, String sourceId, Level level, + List output) { + List> generated; + try { + generated = adapter.getGeneratedRecipes(level); + } catch (RuntimeException exception) { + RateLimitedLogger.warn(LOGGER, "Skipping generated recipes: adapter={}", adapter.getClass().getName(), exception); + return; + } + if (generated == null) return; + for (RecipeHolder holder : generated) { + RecipeConversionUtils.convertAll(adapter, holder, level) + .forEach(recipe -> output.add(new CollectedRecipe(recipe, sourceId))); + } + } + + // Package-private for regression testing of component-exact output matching. + static boolean matchesPattern(AdvancedAlloyFurnaceRecipe recipe, IPatternDetails pattern) { + return matchesPattern(recipe, pattern, Set.of(), false); + } + + private static boolean matchesPattern(AdvancedAlloyFurnaceRecipe recipe, IPatternDetails pattern, + Set componentAgnosticOutputs, + boolean allowMissingOutputs) { + return findPatternScale(recipe, pattern, componentAgnosticOutputs, allowMissingOutputs).isPresent(); + } + + private static OptionalLong findPatternScale( + AdvancedAlloyFurnaceRecipe recipe, IPatternDetails pattern, + Set componentAgnosticOutputs, boolean allowMissingOutputs) { + return findPatternScale(recipe, pattern, PatternContents.read(pattern), + componentAgnosticOutputs, allowMissingOutputs); + } + + private static OptionalLong findPatternScale( + AdvancedAlloyFurnaceRecipe recipe, IPatternDetails pattern, + PatternContents contents, Set componentAgnosticOutputs, + boolean allowMissingOutputs) { + if (contents == null) return OptionalLong.empty(); + + List expectedOutputs = new ArrayList<>(); + recipe.outputs().stream().map(GenericStack::fromItemStack).forEach(expectedOutputs::add); + recipe.outputFluids().stream().map(GenericStack::fromFluidStack).forEach(expectedOutputs::add); + expectedOutputs.addAll(recipe.keyOutputs()); + + // A manually multiplied AE processing pattern still represents the same recipe. Try only + // positive integer ratios derived from its outputs, then validate every input/output at + // that same ratio. This deliberately rejects division and arbitrary additions/removals. + for (long operations : candidatePatternScales( + java.util.Arrays.asList(pattern.getOutputs()), expectedOutputs, componentAgnosticOutputs)) { + if (!matchesScaledInputs(recipe, contents, operations)) continue; + + // Outputs match component-exactly by default, mirroring the server-side identity built + // by AlloyFurnaceRecipeFingerprint.encodeExactOutput. Relaxing item components + // everywhere would collapse recipes that differ only by an output component, so only + // resolver-marked item-id-only slots ignore components. + boolean outputsMatch = allowMissingOutputs + ? matchesGenericSubset( + java.util.Arrays.asList(pattern.getOutputs()), expectedOutputs, componentAgnosticOutputs, operations) + : componentAgnosticOutputs.isEmpty() + ? sameGenericScaled(java.util.Arrays.asList(pattern.getOutputs()), expectedOutputs, operations) + : sameGenericIgnoringSlotComponents( + java.util.Arrays.asList(pattern.getOutputs()), expectedOutputs, + componentAgnosticOutputs, operations); + if (outputsMatch) return OptionalLong.of(operations); + } + return OptionalLong.empty(); + } + + /** + * Compatibility matcher for a pattern made by a different recipe-directory representation. + * Inputs, fluids, keys, counts, and item identities remain strict; only item output components + * are ignored. The exact matcher above remains the first choice so recipes distinguished by + * output components cannot become ambiguous during normal resolution. + */ + private static boolean matchesPatternShape( + AdvancedAlloyFurnaceRecipe recipe, IPatternDetails pattern) { + PatternContents contents = PatternContents.read(pattern); + if (contents == null) return false; + + List expectedOutputs = new ArrayList<>(); + recipe.outputs().stream().map(GenericStack::fromItemStack).forEach(expectedOutputs::add); + recipe.outputFluids().stream().map(GenericStack::fromFluidStack).forEach(expectedOutputs::add); + expectedOutputs.addAll(recipe.keyOutputs()); + for (long operations : candidatePatternScales(java.util.Arrays.asList(pattern.getOutputs()), expectedOutputs, Set.of())) { + if (matchesScaledInputs(recipe, contents, operations) + && matchesGenericSubsetIgnoringItemComponents( + java.util.Arrays.asList(pattern.getOutputs()), expectedOutputs, operations)) { + return true; + } + } + return false; + } + + private static boolean matchesScaledInputs( + AdvancedAlloyFurnaceRecipe recipe, PatternContents contents, long operations) { + long requiredItemCount = multiplyAmount(sumItemRequirements(recipe.inputs()), operations); + long actualItemCount = sumGenericAmounts(contents.items); + return requiredItemCount >= 0L + && actualItemCount >= 0L + && requiredItemCount == actualItemCount + && ItemIngredientAllocator.matches(recipe.inputs(), List.of(), contents.items, operations) + && matchesFluidIngredients(contents.fluids, recipe.inputFluids(), operations) + && sameGenericScaled(contents.keys, recipe.keyInputs(), operations); + } + + private static List candidatePatternScales( + List actual, List expected, Set componentAgnosticSlots) { + if (actual == null || actual.isEmpty() || expected == null || expected.isEmpty()) { + return List.of(); + } + + LinkedHashSet candidates = new LinkedHashSet<>(); + for (int actualSlot = 0; actualSlot < actual.size(); actualSlot++) { + GenericStack actualStack = actual.get(actualSlot); + if (actualStack == null || actualStack.what() == null || actualStack.amount() <= 0L) continue; + + long matchingAmount = 0L; + for (GenericStack expectedStack : expected) { + if (expectedStack == null || expectedStack.what() == null || expectedStack.amount() <= 0L) { + continue; + } + boolean matches = componentAgnosticSlots.contains(actualSlot) + ? matchesItemId(actualStack.what(), expectedStack.what()) + : actualStack.what().equals(expectedStack.what()); + if (!matches) continue; + + if (actualStack.amount() % expectedStack.amount() == 0L) { + candidates.add(actualStack.amount() / expectedStack.amount()); + } + matchingAmount = addAmount(matchingAmount, expectedStack.amount()); + } + if (matchingAmount > 0L && actualStack.amount() % matchingAmount == 0L) { + candidates.add(actualStack.amount() / matchingAmount); + } + } + return candidates.stream().filter(scale -> scale > 0L).toList(); + } + + private static long sumItemRequirements(List requirements) { + long result = 0L; + if (requirements == null) return result; + for (CountedIngredient requirement : requirements) { + if (requirement == null || requirement.count() <= 0L) continue; + if (requirement.count() > Long.MAX_VALUE - result) return -1L; + result += requirement.count(); + } + return result; + } + + private static long sumGenericAmounts(List stacks) { + long result = 0L; + if (stacks == null) return result; + for (GenericStack stack : stacks) { + if (stack == null || stack.amount() <= 0L) return -1L; + if (stack.amount() > Long.MAX_VALUE - result) return -1L; + result += stack.amount(); + } + return result; + } + + private static long multiplyAmount(long amount, long multiplier) { + if (amount < 0L || multiplier <= 0L || amount > Long.MAX_VALUE / multiplier) return -1L; + return amount * multiplier; + } + + private static boolean hasComponentSensitiveItemOutputs(AdvancedAlloyFurnaceRecipe recipe) { + for (ItemStack output : recipe.outputs()) { + if (output != null && !output.isEmpty() && (output.hasTag() || output.serializeNBT().contains("ForgeCaps"))) { + return true; + } + } + return false; + } + + /** + * Whether the shape fallback may ignore this recipe's item output components. + * + *

A component-bearing output is only safe to compare by shape when every such output slot is + * already excluded from component comparison, i.e. the resolver marked it item-id-only. + * Blanket-refusing every component-bearing output also refused precisely the dynamic recipes that + * depend on this fallback - miner spirits, repair and upgrade variants all carry runtime generated + * components on their result - so their slot provenance is checked instead. + */ + private static boolean ignoresItemOutputComponents( + AdvancedAlloyFurnaceRecipe recipe, Set componentAgnosticOutputs) { + if (!hasComponentSensitiveItemOutputs(recipe)) return true; + if (componentAgnosticOutputs == null || componentAgnosticOutputs.isEmpty()) return false; + List outputs = recipe.outputs(); + for (int slot = 0; slot < outputs.size(); slot++) { + ItemStack output = outputs.get(slot); + if (output != null && !output.isEmpty() && (output.hasTag() || output.serializeNBT().contains("ForgeCaps")) + && !componentAgnosticOutputs.contains(slot)) { + return false; + } + } + return true; + } + + + private static boolean matchesGenericSubsetIgnoringItemComponents( + List actual, List expected, long operations) { + if (actual == null || actual.isEmpty() || expected == null || actual.size() > expected.size()) { + return false; + } + + boolean[] matched = new boolean[expected.size()]; + for (GenericStack actualStack : actual) { + if (actualStack == null || actualStack.what() == null || actualStack.amount() <= 0L) { + return false; + } + int matchedSlot = -1; + for (int expectedSlot = 0; expectedSlot < expected.size(); expectedSlot++) { + if (matched[expectedSlot]) continue; + GenericStack expectedStack = expected.get(expectedSlot); + long expectedAmount = expectedStack == null + ? -1L : multiplyAmount(expectedStack.amount(), operations); + if (expectedStack == null || expectedStack.what() == null + || expectedAmount < 0L || expectedAmount != actualStack.amount() + || !sameKeyIgnoringItemComponents(actualStack.what(), expectedStack.what())) { + continue; + } + matchedSlot = expectedSlot; + break; + } + if (matchedSlot < 0) return false; + matched[matchedSlot] = true; + } + // At least one output was validated above. The retained output may be any recipe output; + // this also supports a pattern that intentionally keeps only a secondary product as a + // converter fallback. + return true; + } + + private static boolean sameKeyIgnoringItemComponents(AEKey left, AEKey right) { + if (left instanceof AEItemKey leftItem && right instanceof AEItemKey rightItem) { + return leftItem.getItem() == rightItem.getItem(); + } + return left.equals(right); + } + + /** + * Matches the outputs retained by a known recipe-bound pattern. The encoder may omit any + * outputs, but every retained stack must still match one complete recipe output. + */ + private static boolean matchesGenericSubset( + List actual, List expected, + Set componentAgnosticSlots, long operations) { + if (actual == null || actual.isEmpty() || expected == null || actual.size() > expected.size()) { + return false; + } + + boolean[] matched = new boolean[expected.size()]; + for (int actualSlot = 0; actualSlot < actual.size(); actualSlot++) { + GenericStack actualOutput = actual.get(actualSlot); + if (actualOutput == null || actualOutput.what() == null || actualOutput.amount() <= 0L) { + return false; + } + + int matchedSlot = -1; + for (int expectedSlot = 0; expectedSlot < expected.size(); expectedSlot++) { + if (matched[expectedSlot]) { + continue; + } + GenericStack expectedOutput = expected.get(expectedSlot); + long expectedAmount = expectedOutput == null + ? -1L : multiplyAmount(expectedOutput.amount(), operations); + if (expectedOutput == null || expectedOutput.what() == null + || expectedAmount < 0L || expectedAmount != actualOutput.amount()) { + continue; + } + + boolean matches = componentAgnosticSlots.contains(actualSlot) + ? matchesItemId(actualOutput.what(), expectedOutput.what()) + : actualOutput.what().equals(expectedOutput.what()); + if (matches) { + matchedSlot = expectedSlot; + break; + } + } + if (matchedSlot < 0) { + return false; + } + matched[matchedSlot] = true; + } + // At least one output was validated above. Any recipe output can be the retained fallback + // when the original primary output was removed from the pattern. + return true; + } + + private static boolean matchesItemId(AEKey actual, AEKey expected) { + return actual instanceof AEItemKey actualItem + && expected instanceof AEItemKey expectedItem + && actualItem.getItem() == expectedItem.getItem(); + } + + /** + * Compares outputs slot-by-slot, ignoring item components in the given slots. Slot indices are + * positional, so this cannot use the order-insensitive multiset comparison. + */ + private static boolean sameGenericIgnoringSlotComponents( + List patternOutputs, List recipeOutputs, + Set ignoredSlots, long operations) { + if (patternOutputs.size() != recipeOutputs.size()) return false; + for (int slot = 0; slot < patternOutputs.size(); slot++) { + GenericStack left = patternOutputs.get(slot); + GenericStack right = recipeOutputs.get(slot); + long expectedAmount = right == null ? -1L : multiplyAmount(right.amount(), operations); + if (left == null || right == null || expectedAmount < 0L + || left.amount() != expectedAmount) return false; + if (!ignoredSlots.contains(slot)) { + if (!left.what().equals(right.what())) return false; + } else if (!(left.what() instanceof AEItemKey leftItem) + || !(right.what() instanceof AEItemKey rightItem) + || leftItem.getItem() != rightItem.getItem()) { + return false; + } + } + return true; + } + + private static boolean sameAmounts(List left, List right) { + Map leftMap = new LinkedHashMap<>(); + for (FluidStack stack : left) leftMap.merge(AEFluidKey.of(stack), (long) stack.getAmount(), Long::sum); + Map rightMap = new LinkedHashMap<>(); + for (FluidStack stack : right) rightMap.merge(AEFluidKey.of(stack), (long) stack.getAmount(), Long::sum); + return leftMap.equals(rightMap); + } + + private static boolean matchesFluidIngredients( + List actual, + List required, long operations) { + long actualAmount = 0L; + if (actual != null) { + for (PatternFluidInput input : actual) { + if (input != null && input.amount() > 0) { + actualAmount = addAmount(actualAmount, input.amount()); + } + } + } + long requiredAmount = 0L; + if (required != null) { + for (var ingredient : required) { + if (ingredient != null) requiredAmount = addAmount(requiredAmount, ingredient.amount()); + } + } + requiredAmount = multiplyAmount(requiredAmount, operations); + if (requiredAmount < 0L) return false; + if (actualAmount != requiredAmount) return false; + return matchesFluidCandidates(actual, required, operations, 0, new ArrayList<>()); + } + + private static boolean matchesFluidCandidates( + List actual, + List required, + long operations, int index, List selected) { + if (index >= actual.size()) { + return FluidIngredientAllocator.matchesLong(required, List.of(), selected, operations); + } + PatternFluidInput input = actual.get(index); + if (input == null || input.candidates().isEmpty()) return false; + for (GenericStack candidate : input.candidates()) { + if (candidate == null || candidate.what() == null || candidate.amount() <= 0L) continue; + selected.add(candidate); + if (matchesFluidCandidates(actual, required, operations, index + 1, selected)) return true; + selected.remove(selected.size()-1); + } + return false; + } + + private static long addAmount(long left, long right) { + return right > 0 && left > Long.MAX_VALUE - right ? Long.MAX_VALUE : left + right; + } + + private static boolean sameGeneric(List left, List right) { + Map leftMap = genericMap(left); + Map rightMap = genericMap(right); + return leftMap.equals(rightMap); + } + + private static boolean sameGenericScaled( + List actual, List expected, long operations) { + if (operations <= 0L) return false; + + Map actualMap = genericMap(actual); + Map expectedMap = new LinkedHashMap<>(); + if (expected != null) { + for (GenericStack stack : expected) { + if (stack == null || stack.what() == null || stack.amount() <= 0L) return false; + long amount = multiplyAmount(stack.amount(), operations); + if (amount < 0L) return false; + Long previous = expectedMap.putIfAbsent(stack.what(), amount); + if (previous != null) { + if (amount > Long.MAX_VALUE - previous) return false; + expectedMap.put(stack.what(), previous + amount); + } + } + } + return actualMap.equals(expectedMap); + } + + private static Map genericMap(List stacks) { + Map result = new LinkedHashMap<>(); + for (GenericStack stack : stacks) { + if (stack == null || stack.what() == null || stack.amount() <= 0) continue; + result.merge(stack.what(), stack.amount(), Long::sum); + } + return result; + } + + private record Snapshot( + List entries, + Map byIdentity, + Map> byRecipeId, + Map> bySource, + Map> bySourceAndRecipeId, + long generation, + Map knownMoldCache, + Map compatibilityAliases, + Set legacyMisses) { + } + + private record CollectedRecipe(AdvancedAlloyFurnaceRecipe recipe, String sourceId) { + private CollectedRecipe { + sourceId = RecipeSourceIds.normalize(sourceId); + } + } + + private record SourceRecipeKey(String sourceId, ResourceLocation recipeId) { + private SourceRecipeKey { + sourceId = RecipeSourceIds.normalize(sourceId); + } + } + + private record PatternContents(List items, List fluids, + List keys) { + private static PatternContents read(IPatternDetails pattern) { + List items = new ArrayList<>(); + List fluids = new ArrayList<>(); + List keys = new ArrayList<>(); + for (IPatternDetails.IInput input : pattern.getInputs()) { + if (input == null) return null; + GenericStack[] possible = input.getPossibleInputs(); + long amount = input.getMultiplier(); + if (possible.length == 0 || amount <= 0) return null; + + List fluidCandidates = new ArrayList<>(); + boolean hasNonFluidCandidate = false; + for (GenericStack candidate : possible) { + if (candidate == null || candidate.what() == null) continue; + if (candidate.what() instanceof AEFluidKey fluidKey) { + GenericStack fluid = new GenericStack(fluidKey, amount); + if (fluidCandidates.stream().noneMatch(existing -> + existing.what().equals(fluid.what()))) { + fluidCandidates.add(fluid); + } + } else { + hasNonFluidCandidate = true; + } + } + // A processing slot cannot be represented faithfully when its encoded candidates + // cross the item/fluid boundary. Keep the pattern invalid rather than silently + // discarding one side of the slot. + if (!fluidCandidates.isEmpty() && hasNonFluidCandidate) return null; + if (!fluidCandidates.isEmpty()) { + fluids.add(new PatternFluidInput(List.copyOf(fluidCandidates), amount)); + continue; + } + + GenericStack first = java.util.Arrays.stream(possible) + .filter(candidate -> candidate != null && candidate.what() != null) + .findFirst().orElse(null); + if (first == null) return null; + AEKey key = first.what(); + if (key instanceof AEItemKey itemKey) items.add(new GenericStack(itemKey, amount)); + else keys.add(new GenericStack(key, amount)); + } + return new PatternContents(items, fluids, keys); + } + } + + private record PatternFluidInput(List candidates, long amount) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/AlloyFurnaceRecipeFingerprint.java b/src/main/java/com/sorrowmist/useless/content/recipe/AlloyFurnaceRecipeFingerprint.java new file mode 100644 index 00000000..45848066 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/AlloyFurnaceRecipeFingerprint.java @@ -0,0 +1,533 @@ +package com.sorrowmist.useless.content.recipe; + +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.mojang.serialization.JsonOps; +import net.minecraft.core.HolderLookup; + + +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraftforge.fluids.FluidStack; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HexFormat; +import java.util.List; +import java.util.Objects; +import java.util.TreeMap; + +public final class AlloyFurnaceRecipeFingerprint { + private AlloyFurnaceRecipeFingerprint() { + } + + public static String create(AdvancedAlloyFurnaceRecipe recipe, HolderLookup.Provider registries) { + return create(recipe, registries, true); + } + + static String createLegacy(AdvancedAlloyFurnaceRecipe recipe, HolderLookup.Provider registries) { + return create(recipe, registries, false); + } + + static String createLegacySemantic(AdvancedAlloyFurnaceRecipe recipe, HolderLookup.Provider registries) { + return create(recipe, registries, true, false); + } + + private static String create( + AdvancedAlloyFurnaceRecipe recipe, + HolderLookup.Provider registries, + boolean normalizeIngredientSemantics) { + return create(recipe, registries, normalizeIngredientSemantics, true); + } + + private static String create( + AdvancedAlloyFurnaceRecipe recipe, + HolderLookup.Provider registries, + boolean normalizeIngredientSemantics, + boolean preserveTags) { + Objects.requireNonNull(recipe, "recipe"); + Objects.requireNonNull(registries, "registries"); + try { + return createEncoded(recipe, registries, normalizeIngredientSemantics, preserveTags); + } catch (RuntimeException ignored) { + // Keep the historical encoded fingerprint above untouched. This path is only for a + // runtime recipe that the historical codec cannot represent. + try { + return createStructuralFallback(recipe, registries, normalizeIngredientSemantics, preserveTags); + } catch (RuntimeException structuralFailure) { + // This is still deterministic: a runtime object address must never be part of a + // persisted pattern identity. + return createFinalFallback(recipe); + } + } + } + + private static String createEncoded( + AdvancedAlloyFurnaceRecipe recipe, + HolderLookup.Provider registries, + boolean normalizeIngredientSemantics, + boolean preserveTags) { + var context = net.minecraft.resources.RegistryOps.create(JsonOps.INSTANCE,registries); + // This port hashes native 1.20.1 NBT; 1.21 component patterns are not silently treated as equivalent. + JsonElement encoded = AdvancedAlloyFurnaceRecipe.CODEC.codec() + .encodeStart(context, recipe) + .getOrThrow(false, message -> { throw new IllegalArgumentException(message); }); + JsonObject recipeJson = encoded.getAsJsonObject(); + if (normalizeIngredientSemantics) { + normalizeIngredients(recipe, recipeJson, context, preserveTags); + } + if ((!normalizeIngredientSemantics || !preserveTags) + && !recipe.inputFluids().isEmpty() + && recipe.inputFluids().stream().allMatch(LongSizedFluidIngredient::fitsInt)) { + encodeLegacyFluidInputs(recipe, recipeJson, context); + } + JsonArray exactItemOutputs = new JsonArray(); + for (var output : recipe.outputs()) { + exactItemOutputs.add(encodeExactOutput(output, context)); + } + recipeJson.add("fingerprint_item_outputs", exactItemOutputs); + byte[] canonical = canonicalize(encoded).toString().getBytes(StandardCharsets.UTF_8); + try { + return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(canonical)); + } catch (NoSuchAlgorithmException exception) { + throw new IllegalStateException("SHA-256 is unavailable", exception); + } + } + + /** + * Builds an identity without invoking a codec. This is deliberately a last resort for + * malformed third-party data, so every individual field is guarded and a bad component cannot + * discard the whole recipe. + */ + private static String createStructuralFallback( + AdvancedAlloyFurnaceRecipe recipe, + HolderLookup.Provider registries, + boolean normalizeIngredientSemantics, + boolean preserveTags) { + StringBuilder value = new StringBuilder("alloy_furnace_fingerprint_fallback_v1|"); + append(value, "id", recipe.id()); + append(value, "inputs", recipe.inputs(), input -> safeCountedIngredient(input, registries, + normalizeIngredientSemantics, preserveTags)); + append(value, "input_fluids", recipe.inputFluids(), input -> safeFluidIngredient(input, registries)); + append(value, "key_inputs", recipe.keyInputs(), input -> safeGenericStack(input, registries)); + append(value, "outputs", recipe.outputs(), output -> safeItemStack(output, registries)); + append(value, "output_fluids", recipe.outputFluids(), fluid -> safeFluidStack(fluid, registries)); + append(value, "key_outputs", recipe.keyOutputs(), output -> safeGenericStack(output, registries)); + append(value, "energy", recipe.energy()); + append(value, "process_time", recipe.processTime()); + append(value, "catalyst", safeIngredient(recipe.catalyst(), registries, normalizeIngredientSemantics, + preserveTags)); + append(value, "catalyst_uses", recipe.catalystUses()); + append(value, "molds", recipe.molds(), mold -> safeIngredient(mold, registries, + normalizeIngredientSemantics, preserveTags)); + append(value, "mode", recipe.mode()); + append(value, "tier", recipe.tier()); + return digest(value.toString()); + } + + private static String safeCountedIngredient( + CountedIngredient input, + HolderLookup.Provider registries, + boolean normalizeIngredientSemantics, + boolean preserveTags) { + if (input == null) return "null"; + return safeIngredient(input.ingredient(), registries, normalizeIngredientSemantics, preserveTags) + + "#" + input.count(); + } + + private static String safeFluidIngredient( + LongSizedFluidIngredient input, + HolderLookup.Provider registries) { + if (input == null) return "null"; + return safeFluidIngredientValue(input.ingredient(), registries) + "#" + input.amount(); + } + + private static String safeIngredient( + Ingredient ingredient, + HolderLookup.Provider registries, + boolean normalizeIngredientSemantics, + boolean preserveTags) { + if (ingredient == null) return "empty"; + try { + if (ingredient.isEmpty()) return "empty"; + } catch (RuntimeException exception) { + return "ingredient_error:" + ingredient.getClass().getName() + + ":" + exception.getClass().getName(); + } + try { + if (normalizeIngredientSemantics && ingredient.isSimple() && !(preserveTags && hasDirectTag(ingredient))) { + String[] itemIds = Arrays.stream(ingredient.getItems()) + .filter(stack -> stack != null && !stack.isEmpty()) + .map(ItemStack::getItem) + .map(BuiltInRegistries.ITEM::getKey) + .filter(Objects::nonNull) + .map(Object::toString) + .distinct() + .sorted() + .toArray(String[]::new); + return String.join(",", itemIds); + } + } catch (RuntimeException ignored) { + // Continue with the codec or textual fallback. + } + try { + var context = net.minecraft.resources.RegistryOps.create(JsonOps.INSTANCE,registries); + var encoded = com.sorrowmist.useless.compat.forge1201.recipe.RecipeCodecs.INGREDIENT.encodeStart(context, ingredient); + if (encoded.result().isPresent()) return canonicalize(encoded.result().get()).toString(); + } catch (RuntimeException ignored) { + // Continue with the representative stack fallback. + } + try { + String items = Arrays.stream(ingredient.getItems()) + .map(stack -> safeItemStack(stack, registries)) + .sorted() + .reduce((left, right) -> left + "," + right) + .orElse(ingredient.getClass().getName()); + return ingredient.getClass().getName() + "[" + items + "]"; + } catch (RuntimeException ignored) { + return ingredient.getClass().getName(); + } + } + + private static String safeFluidIngredientValue( + com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient ingredient, + HolderLookup.Provider registries) { + if (ingredient == null) return "empty"; + try { + if (ingredient.isEmpty()) return "empty"; + } catch (RuntimeException exception) { + return "fluid_ingredient_error:" + ingredient.getClass().getName() + + ":" + exception.getClass().getName(); + } + try { + var context = net.minecraft.resources.RegistryOps.create(JsonOps.INSTANCE,registries); + var encoded = com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient.CODEC + .encodeStart(context, ingredient); + if (encoded.result().isPresent()) return canonicalize(encoded.result().get()).toString(); + } catch (RuntimeException ignored) { + // Use representatives below. + } + try { + return Arrays.stream(ingredient.getStacks()) + .map(stack -> safeFluidStack(stack, registries)) + .sorted() + .reduce((left, right) -> left + "," + right) + .orElse(ingredient.getClass().getName()); + } catch (RuntimeException ignored) { + return ingredient.getClass().getName(); + } + } + + /** + * 把 ItemStack 编码成只由数据内容决定的稳定字符串。 + * + *

编码结果交给 {@link #canonicalize(JsonElement)} 递归排序,因此组件映射、嵌套容器内容等 + * 任何深度的迭代顺序差异都会被消除。需要为物品栈生成跨进程一致的签名时(例如派生配方 id) + * 应复用本方法,而不要直接拼接组件映射的字符串表示。 + */ + public static String safeItemStack(ItemStack stack, HolderLookup.Provider registries) { + if (stack == null) return "null"; + String itemId; + try { + var key = BuiltInRegistries.ITEM.getKey(stack.getItem()); + itemId = key == null + ? "unregistered_item:" + stack.getItem().getClass().getName() + : key.toString(); + } catch (RuntimeException exception) { + itemId = "item_error:" + exception.getClass().getName(); + } + String components; + try { + // Include capability serialization and all foreign NBT; exclude the independent count. + var data=stack.copyWithCount(1).serializeNBT(); data.remove("Count"); + components=com.sorrowmist.useless.compat.forge1201.recipe.NbtCanonical.snbt(data); + } catch(RuntimeException exception) { + components=com.sorrowmist.useless.compat.forge1201.recipe.NbtCanonical.snbt(stack.getTag()); + } + String count; + try { + count = String.valueOf(stack.getCount()); + } catch (RuntimeException exception) { + count = "count_error:" + exception.getClass().getName(); + } + return itemId + "#" + count + "#" + components; + } + + /** + * 把 FluidStack 编码成只由数据内容决定的稳定字符串;与 {@link #safeItemStack(ItemStack, HolderLookup.Provider)} + * 同源,供派生配方 id 等需要跨进程一致的场景复用。 + */ + public static String safeFluidStack(FluidStack stack, HolderLookup.Provider registries) { + if (stack == null) return "null"; + String fluidId; + try { + var key = BuiltInRegistries.FLUID.getKey(stack.getFluid()); + fluidId = key == null + ? "unregistered_fluid:" + stack.getFluid().getClass().getName() + : key.toString(); + } catch (RuntimeException exception) { + fluidId = "fluid_error:" + exception.getClass().getName(); + } + String components=com.sorrowmist.useless.compat.forge1201.recipe.NbtCanonical.snbt(stack.getTag()); + String amount; + try { + amount = String.valueOf(stack.getAmount()); + } catch (RuntimeException exception) { + amount = "amount_error:" + exception.getClass().getName(); + } + return fluidId + "#" + amount + "#" + components; + } + + private static String safeGenericStack(GenericStack stack, HolderLookup.Provider registries) { + if (stack == null) return "null"; + try { + var context = net.minecraft.resources.RegistryOps.create(JsonOps.INSTANCE,registries); + var encoded = com.sorrowmist.useless.compat.forge1201.recipe.AE2Codecs.GENERIC_STACK.encodeStart(context, stack); + if (encoded.result().isPresent()) { + return canonicalize((JsonElement) encoded.result().get()).toString(); + } + } catch (RuntimeException exception) { + // Try the key codec separately so a broken amount or wrapper does not erase the key. + } + try { + var context = net.minecraft.resources.RegistryOps.create(JsonOps.INSTANCE,registries); + var encoded = com.sorrowmist.useless.compat.forge1201.recipe.AE2Codecs.AE_KEY.encodeStart(context, stack.what()); + if (encoded.result().isPresent()) { + return "key=" + canonicalize(encoded.result().get()) + ";amount=" + stack.amount(); + } + } catch (RuntimeException exception) { + // Use registry identity and scalar metadata as the final stable representation. + } + AEKey key = stack.what(); + return "key_type=" + (key == null ? "null" : key.getClass().getName()) + + ";key_id=" + safeKeyId(key) + + ";has_components=" + safeKeyHasComponents(key) + + ";amount=" + safeLong(() -> stack.amount()); + } + + private static String safeKeyId(AEKey key) { + if (key == null) return "null"; + try { + return safeText(key.getId()); + } catch (RuntimeException exception) { + return "id_error:" + exception.getClass().getName(); + } + } + + private static String safeKeyHasComponents(AEKey key) { + if (key == null) return "false"; + try { + return Boolean.toString(!key.equals(key.dropSecondary())); + } catch (RuntimeException exception) { + return "error:" + exception.getClass().getName(); + } + } + + private static String safeLong(java.util.function.LongSupplier supplier) { + try { + return Long.toString(supplier.getAsLong()); + } catch (RuntimeException exception) { + return "long_error:" + exception.getClass().getName(); + } + } + + private static void append(StringBuilder value, String name, T field) { + value.append(name).append('='); + appendToken(value, safeText(field)); + value.append(';'); + } + + private static void append( + StringBuilder value, String name, List fields, + java.util.function.Function encoder) { + value.append(name).append('['); + if (fields != null) { + int index = 0; + for (T field : fields) { + value.append(index++).append(':'); + try { + appendToken(value, encoder.apply(field)); + } catch (RuntimeException exception) { + appendToken(value, "field_error:" + exception.getClass().getName()); + } + value.append('|'); + } + } + value.append("];"); + } + + private static void appendToken(StringBuilder value, String token) { + String safe = token == null ? "null" : token; + value.append(safe.length()).append(':').append(safe); + } + + private static String safeText(Object value) { + try { + return String.valueOf(value); + } catch (RuntimeException exception) { + return "text_error:" + exception.getClass().getName(); + } + } + + private static String createFinalFallback(AdvancedAlloyFurnaceRecipe recipe) { + return digest("alloy_furnace_fingerprint_emergency_v1" + + "|class=" + recipe.getClass().getName() + + "|id=" + safeText(recipe.id()) + + "|inputs=" + safeListSize(recipe.inputs()) + + "|input_fluids=" + safeListSize(recipe.inputFluids()) + + "|key_inputs=" + safeListSize(recipe.keyInputs()) + + "|outputs=" + safeListSize(recipe.outputs()) + + "|output_fluids=" + safeListSize(recipe.outputFluids()) + + "|key_outputs=" + safeListSize(recipe.keyOutputs()) + + "|energy=" + safeText(recipe.energy()) + + "|process_time=" + safeText(recipe.processTime()) + + "|catalyst_uses=" + safeText(recipe.catalystUses()) + + "|mode=" + safeText(recipe.mode()) + + "|tier=" + safeText(recipe.tier())); + } + + private static String safeListSize(List values) { + try { + return values == null ? "null" : Integer.toString(values.size()); + } catch (RuntimeException exception) { + return "size_error:" + exception.getClass().getName(); + } + } + + private static String digest(String value) { + try { + return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256") + .digest(value.getBytes(StandardCharsets.UTF_8))); + } catch (NoSuchAlgorithmException exception) { + throw new IllegalStateException("SHA-256 is unavailable", exception); + } + } + + /** + * Ordinary simple ingredients are normalized to their item ids. Direct tag values retain + * their tag location so a tag's membership can change without changing the recipe identity. + * Non-simple custom ingredients retain their codec form because their behavior can depend on + * components or predicates that cannot be represented by item ids alone. + */ + private static void normalizeIngredients( + AdvancedAlloyFurnaceRecipe recipe, + JsonObject encoded, + com.mojang.serialization.DynamicOps context, + boolean preserveTags) { + JsonArray inputs = new JsonArray(); + for (CountedIngredient input : recipe.inputs()) { + JsonObject counted = new JsonObject(); + counted.add("ingredient", encodeSemanticIngredient(input.ingredient(), context, preserveTags)); + if (input.count() != 1L) { + counted.addProperty("count", input.count()); + } + inputs.add(counted); + } + encoded.add("ingredients", inputs); + + if (!recipe.catalyst().isEmpty()) { + encoded.add("catalyst", encodeSemanticIngredient(recipe.catalyst(), context, preserveTags)); + } + if (recipe.molds().size() == 1) { + // Preserve the historical single-mold fingerprint shape. + encoded.add("mold", encodeSemanticIngredient(recipe.molds().get(0), context, preserveTags)); + } else if (recipe.molds().size() > 1) { + List moldElements = new ArrayList<>(); + for (Ingredient mold : recipe.molds()) { + moldElements.add(encodeSemanticIngredient(mold, context, preserveTags)); + } + // Mold requirements are a set-like collection for identity purposes, but repeated + // entries remain significant. Sorting the encoded elements makes list order irrelevant. + moldElements.sort(java.util.Comparator.comparing(element -> canonicalize(element).toString())); + JsonArray molds = new JsonArray(); + moldElements.forEach(molds::add); + encoded.add("molds", molds); + } + } + + /** Restores the pre-Ingredient representation used by version 1-4 pattern fingerprints. */ + private static void encodeLegacyFluidInputs( + AdvancedAlloyFurnaceRecipe recipe, + JsonObject encoded, + com.mojang.serialization.DynamicOps context) { + JsonArray fluids = new JsonArray(); + for (LongSizedFluidIngredient ingredient : recipe.inputFluids()) { + if (ingredient == null || ingredient.ingredient() == null || ingredient.amount() <= 0) continue; + for (FluidStack stack : ingredient.toSizedFluidIngredient().getFluids()) { + if (stack == null || stack.isEmpty()) continue; + fluids.add(FluidStack.CODEC.encodeStart(context, stack).getOrThrow(false, message -> { throw new IllegalArgumentException(message); })); + } + } + encoded.add("input_fluids", fluids); + } + + private static JsonElement encodeSemanticIngredient( + Ingredient ingredient, + com.mojang.serialization.DynamicOps context, + boolean preserveTags) { + if (preserveTags && hasDirectTag(ingredient)) { + return com.sorrowmist.useless.compat.forge1201.recipe.RecipeCodecs.INGREDIENT.encodeStart(context, ingredient).getOrThrow(false, message -> { throw new IllegalArgumentException(message); }); + } + if (!ingredient.isSimple()) { + return com.sorrowmist.useless.compat.forge1201.recipe.RecipeCodecs.INGREDIENT.encodeStart(context, ingredient).getOrThrow(false, message -> { throw new IllegalArgumentException(message); }); + } + + String[] itemIds = Arrays.stream(ingredient.getItems()) + .filter(stack -> stack != null && !stack.isEmpty()) + .map(ItemStack::getItem) + .map(BuiltInRegistries.ITEM::getKey) + .filter(Objects::nonNull) + .map(Object::toString) + .distinct() + .sorted() + .toArray(String[]::new); + JsonArray values = new JsonArray(); + for (String itemId : itemIds) { + JsonObject value = new JsonObject(); + value.addProperty("item", itemId); + values.add(value); + } + return values.size() == 1 ? values.get(0) : values; + } + + private static boolean hasDirectTag(Ingredient ingredient) { + return com.sorrowmist.useless.compat.forge1201.recipe.NativeIngredients.hasTag(ingredient); + } + + /** + * Keep the historical ItemStack representation whenever it can encode the + * stack. Minecraft's ItemStack codec intentionally caps counts at 99; + * large machine outputs use a versioned fallback containing an exact + * single-item AE key and the independent count. + */ + private static JsonElement encodeExactOutput( + net.minecraft.world.item.ItemStack output, + com.mojang.serialization.DynamicOps context) { + return com.sorrowmist.useless.compat.forge1201.recipe.RecipeCodecs.ITEM_STACK.encodeStart(context,output) + .getOrThrow(false,message -> { throw new IllegalArgumentException(message); }); + } + + private static JsonElement canonicalize(JsonElement element) { + if (element.isJsonObject()) { + TreeMap sorted = new TreeMap<>(); + for (var entry : element.getAsJsonObject().entrySet()) sorted.put(entry.getKey(), entry.getValue()); + JsonObject result = new JsonObject(); + for (var entry : sorted.entrySet()) result.add(entry.getKey(), canonicalize(entry.getValue())); + return result; + } + if (element.isJsonArray()) { + JsonArray result = new JsonArray(); + for (JsonElement child : element.getAsJsonArray()) result.add(canonicalize(child)); + return result; + } + return element.deepCopy(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/AlloyFurnaceRecipeIdentity.java b/src/main/java/com/sorrowmist/useless/content/recipe/AlloyFurnaceRecipeIdentity.java new file mode 100644 index 00000000..6bab0b18 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/AlloyFurnaceRecipeIdentity.java @@ -0,0 +1,13 @@ +package com.sorrowmist.useless.content.recipe; + +import net.minecraft.resources.ResourceLocation; + +import java.util.Objects; + +public record AlloyFurnaceRecipeIdentity(ResourceLocation recipeId, String fingerprint) { + public AlloyFurnaceRecipeIdentity { + Objects.requireNonNull(recipeId, "recipeId"); + Objects.requireNonNull(fingerprint, "fingerprint"); + if (fingerprint.isBlank()) throw new IllegalArgumentException("Recipe fingerprint cannot be blank"); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/AlloyFurnaceRecipeManager.java b/src/main/java/com/sorrowmist/useless/content/recipe/AlloyFurnaceRecipeManager.java new file mode 100644 index 00000000..16ff3cdc --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/AlloyFurnaceRecipeManager.java @@ -0,0 +1,1326 @@ +package com.sorrowmist.useless.content.recipe; + +import appeng.api.stacks.AEFluidKey; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import com.sorrowmist.useless.init.ModRecipeTypes; +import com.sorrowmist.useless.core.config.AlloyFurnaceTierRules; +import it.unimi.dsi.fastutil.objects.Object2LongLinkedOpenHashMap; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.Recipe; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluid; +import com.sorrowmist.useless.compat.forge1201.recipe.DataComponentIngredient; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * 高级熔炉配方管理器 + *

+ * 统一管理高级熔炉的所有配方来源,包括: + * - 自定义的 AdvancedAlloyFurnaceRecipe + * - 原版熔炉配方(通过适配器转换) + * - 其他模组配方(通过适配器扩展) + *

+ * 提供高效的配方查找和缓存机制,支持按优先级匹配: + * 物品+流体+模具 > 物品+模具 > 流体+模具 > 物品+流体 > 物品 > 流体 + */ +public class AlloyFurnaceRecipeManager { + private static final AlloyFurnaceRecipeManager INSTANCE = new AlloyFurnaceRecipeManager(); + + /** 按模具物品直接查找 adapter(getMoldItem() != null 的注册到这里) */ + private final Map>> moldAdapterMap = new ConcurrentHashMap<>(); + /** 无固定模具的 adapter,需通过 matchesMold() 动态判断(如 SeedEssenceRecipeAdapter) */ + private final List> fallbackAdapters = new CopyOnWriteArrayList<>(); + private final List> allAdapters = new CopyOnWriteArrayList<>(); + private final Map, String> adapterSourceIds = new ConcurrentHashMap<>(); + + // access-order LinkedHashMap(LRU)。get() 会改动内部链表,非线程安全, + // 而 findRecipe 可能被 AE 合成任务和主线程同时调用,故用 synchronizedMap 保护。 + private final Map recipeCache = Collections.synchronizedMap( + new LinkedHashMap<>(16, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > MAX_CACHE_SIZE; + } + }); + private final Map>> adapterMatchCache = Collections.synchronizedMap( + new LinkedHashMap<>(16, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry>> eldest) { + return size() > MAX_ADAPTER_CACHE_SIZE; + } + }); + private final Map> conversionCache = + new ConcurrentHashMap<>(); + private final Map, Boolean> runtimeConversionCache = + new ConcurrentHashMap<>(); + + private static final int MAX_CACHE_SIZE = 500; + private static final int MAX_ADAPTER_CACHE_SIZE = 500; + + // ========== 预构建索引 ========== + + /** + * A client and an integrated server have different recipe managers in the same JVM. Keep one + * immutable index per manager so a lookup can never observe recipes from the other side or a + * previously closed world. + */ + private final Map recipeIndexes = + Collections.synchronizedMap(new WeakHashMap<>()); + + public static AlloyFurnaceRecipeManager getInstance() { + return INSTANCE; + } + + private AlloyFurnaceRecipeManager() { + } + + public void registerAdapter(com.sorrowmist.useless.api.recipe.IRecipeAdapter adapter) { + registerAdapter(adapter, adapter == null ? RecipeSourceIds.UNKNOWN : adapter.sourceId()); + } + + /** @deprecated Use the public API adapter type. */ + @Deprecated(forRemoval = false) + public void registerAdapter(IRecipeAdapter adapter) { + registerAdapter((com.sorrowmist.useless.api.recipe.IRecipeAdapter) adapter); + } + + public void registerAdapter(com.sorrowmist.useless.api.recipe.IRecipeAdapter adapter, String sourceId) { + if (adapter == null) return; + ItemStack moldItem = adapter.getMoldItem(); + + String normalizedSource = RecipeSourceIds.normalize(sourceId); + allAdapters.add(adapter); + adapterSourceIds.put(adapter, normalizedSource); + if (moldItem != null && !moldItem.isEmpty()) { + moldAdapterMap.computeIfAbsent(moldItem.getItem(), ignored -> new CopyOnWriteArrayList<>()) + .add(adapter); + } else { + fallbackAdapters.add(adapter); + } + clearCache(); + invalidateIndex(); + } + + /** @deprecated Use the public API adapter type. */ + @Deprecated(forRemoval = false) + public void registerAdapter(IRecipeAdapter adapter, String sourceId) { + registerAdapter((com.sorrowmist.useless.api.recipe.IRecipeAdapter) adapter, sourceId); + } + + public List> getRegisteredAdapters() { + return List.copyOf(allAdapters); + } + + public String getAdapterSourceId(com.sorrowmist.useless.api.recipe.IRecipeAdapter adapter) { + if (adapter == null) return RecipeSourceIds.UNKNOWN; + return adapterSourceIds.getOrDefault(adapter, RecipeSourceIds.normalize(adapter.sourceId())); + } + + /** @deprecated Use the public API adapter type. */ + @Deprecated(forRemoval = false) + public String getAdapterSourceId(IRecipeAdapter adapter) { + return getAdapterSourceId((com.sorrowmist.useless.api.recipe.IRecipeAdapter) adapter); + } + + public void buildIndex(Level level) { + if (level == null) return; + + synchronized (this) { + clearCache(); + rebuildIndex(level.getRecipeManager()); + } + } + + private RecipeIndex ensureIndex(Level level) { + RecipeManager recipeManager = level.getRecipeManager(); + RecipeIndex index = recipeIndexes.get(recipeManager); + if (index != null) return index; + + synchronized (this) { + index = recipeIndexes.get(recipeManager); + return index != null ? index : rebuildIndex(recipeManager); + } + } + + private RecipeIndex rebuildIndex(RecipeManager recipeManager) { + recipeIndexes.remove(recipeManager); + + RecipeIndexBuilder builder = new RecipeIndexBuilder(); + List> recipes = com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, ModRecipeTypes.ADVANCED_ALLOY_FURNACE_TYPE.get()); + for (RecipeHolder holder : recipes) { + builder.indexRecipe(holder.value()); + } + + // Publish only after every map/list has been frozen. + RecipeIndex index = builder.build(); + recipeIndexes.put(recipeManager, index); + return index; + } + + /** 按当前机器输入查找最具体的可运行配方。 */ + @Nullable + public AdvancedAlloyFurnaceRecipe findRecipe(Level level, List inputs, + List fluidInputs, @Nullable ItemStack mold) { + return findRecipe(level, inputs, fluidInputs, List.of(), mold); + } + + @Nullable + public AdvancedAlloyFurnaceRecipe findRecipe(Level level, List inputs, + List fluidInputs, List keyInputs, @Nullable ItemStack mold) { + return findRecipe(level, inputs, fluidInputs, keyInputs, mold, AlloyFurnaceTierRules.NO_MACHINE_TIER); + } + + @Nullable + public AdvancedAlloyFurnaceRecipe findRecipe(Level level, List inputs, + List fluidInputs, List keyInputs, + @Nullable ItemStack mold, int machineTier) { + return findRecipe(level, new RecipeLookupContext( + inputs, fluidInputs, keyInputs, mold, List.of(), 1L, machineTier)); + } + + /** + * 按 AE 样板目标输出查找配方。 + * + * @param expectedOutputs 样板声明的输出;只作为配方身份约束,不比较输出数量 + * @param operations 当前输入聚合了多少次样板操作 + */ + @Nullable + public AdvancedAlloyFurnaceRecipe findRecipeForCrafting(Level level, List inputs, + List fluidInputs, List keyInputs, + @Nullable ItemStack mold, List expectedOutputs, + long operations) { + return findRecipeForCraftingWithConstraints(level, inputs, fluidInputs, keyInputs, mold, + RecipeOutputConstraint.exact(expectedOutputs), operations); + } + + @Nullable + public AdvancedAlloyFurnaceRecipe findRecipeForCraftingWithConstraints( + Level level, List inputs, + List fluidInputs, List keyInputs, + @Nullable ItemStack mold, List expectedOutputs, + long operations) { + return findRecipeForCraftingWithConstraints( + level, inputs, fluidInputs, keyInputs, mold, expectedOutputs, operations, + AlloyFurnaceTierRules.NO_MACHINE_TIER); + } + + @Nullable + public AdvancedAlloyFurnaceRecipe findRecipeForCraftingWithConstraints( + Level level, List inputs, + List fluidInputs, List keyInputs, + @Nullable ItemStack mold, List expectedOutputs, + long operations, int machineTier) { + return findRecipe(level, new RecipeLookupContext( + inputs, fluidInputs, keyInputs, mold, expectedOutputs, operations, machineTier)); + } + + @Nullable + private AdvancedAlloyFurnaceRecipe findRecipe(Level level, RecipeLookupContext context) { + if (level == null) { + return null; + } + + boolean hasItems = !context.inputs().isEmpty(); + boolean hasFluids = !context.fluidInputs().isEmpty(); + boolean hasKeys = !context.keyInputs().isEmpty(); + boolean hasMold = context.mold() != null && !context.mold().isEmpty(); + + if (!hasItems && !hasFluids && !hasKeys && !hasMold) { + return null; + } + + RecipeCacheKey cacheKey = RecipeCacheKey.from(context); + LookupCacheKey lookupKey = new LookupCacheKey(level.getRecipeManager(), cacheKey); + + CachedLookup cached = recipeCache.get(lookupKey); + if (cached != null) { + return cached.recipe(); + } + + RecipeIndex index = ensureIndex(level); + List indexedCandidates = getCandidateRecipes(index, context); + RecipeAccumulator mergedCandidates = new RecipeAccumulator(indexedCandidates.size()); + mergedCandidates.addAll(indexedCandidates); + mergedCandidates.addAll(findAdaptedRecipes(level, context, cacheKey)); + + AdvancedAlloyFurnaceRecipe recipe = selectBestRecipe( + mergedCandidates.toList(), context, new LookupSnapshot(cacheKey.keys())); + if (recipe != null) { + cacheRecipe(lookupKey, recipe); + return recipe; + } + + cacheNegativeResult(lookupKey); + return null; + } + + /** + * 索引只用于缩小候选集,所有集合都取并集;交集筛选可能在某个候选数量不足时 + * 错误隐藏仍可运行的通用配方。 + */ + private List getCandidateRecipes( + RecipeIndex index, RecipeLookupContext context) { + RecipeAccumulator candidates = new RecipeAccumulator(); + + // These recipes cannot be safely narrowed by a concrete item key. Keep them in every + // lookup so custom ingredients and empty-display ingredients retain their semantics. + candidates.addAll(index.nonSimpleIngredientRecipes()); + candidates.addAll(index.unindexedRecipes()); + + for (ItemStack input : context.inputs()) { + if (input == null || input.isEmpty()) continue; + List recipes = index.inputItemIndex().get(input.getItem()); + if (recipes != null) candidates.addAll(recipes); + } + + if (!context.fluidInputs().isEmpty()) { + for (FluidStack input : context.fluidInputs()) { + if (input == null || input.isEmpty()) continue; + List recipes = index.inputFluidIndex().get(input.getFluid()); + if (recipes != null) candidates.addAll(recipes); + } + candidates.addAll(index.fluidFallbackRecipes()); + } + if (!context.keyInputs().isEmpty()) { + for (GenericStack input : context.keyInputs()) { + if (input.what() instanceof AEItemKey itemKey) { + List itemRecipes = + index.inputItemIndex().get(itemKey.getItem()); + if (itemRecipes != null) candidates.addAll(itemRecipes); + } else if (input.what() instanceof AEFluidKey fluidKey) { + List fluidRecipes = + index.inputFluidIndex().get(fluidKey.getFluid()); + if (fluidRecipes != null) candidates.addAll(fluidRecipes); + candidates.addAll(index.fluidFallbackRecipes()); + } else { + List recipes = index.keyInputIndex().get(input.what()); + if (recipes != null) candidates.addAll(recipes); + } + } + } + if (context.mold() != null && !context.mold().isEmpty()) { + List recipes = index.moldIndex().get(context.mold().getItem()); + if (recipes != null) candidates.addAll(recipes); + } + + return candidates.toList(); + } + + @Nullable + private AdvancedAlloyFurnaceRecipe selectBestRecipe(Iterable candidates, + RecipeLookupContext context) { + return selectBestRecipe(candidates, context, LookupSnapshot.from(context)); + } + + @Nullable + private AdvancedAlloyFurnaceRecipe selectBestRecipe( + Iterable candidates, + RecipeLookupContext context, LookupSnapshot snapshot) { + AdvancedAlloyFurnaceRecipe best = null; + RecipeSpecificity bestSpecificity = null; + List equallySpecific = new ArrayList<>(); + for (AdvancedAlloyFurnaceRecipe recipe : candidates) { + // The ordinary furnace exposes one mold slot. Multiblock recipes are resolved from + // their bound Omniversal Pattern and are checked against the mold hub separately. + if (recipe.molds().size() > 1) continue; + if (!matchesLookup(recipe, context, snapshot)) continue; + if (best == null) { + best = recipe; + equallySpecific.add(recipe); + continue; + } + + RecipeSpecificity candidateSpecificity = RecipeSpecificity.from(recipe); + if (bestSpecificity == null) { + bestSpecificity = RecipeSpecificity.from(best); + } + int specificity = compareSpecificity(candidateSpecificity, bestSpecificity); + if (specificity > 0) { + best = recipe; + bestSpecificity = candidateSpecificity; + equallySpecific.clear(); + equallySpecific.add(recipe); + } else if (specificity == 0) { + equallySpecific.add(recipe); + if (compareRecipeId(recipe, best) < 0) { + best = recipe; + bestSpecificity = candidateSpecificity; + } + } + } + + if (best != null && isAmbiguousManualCraftingLookup(context, equallySpecific)) { + return null; + } + return best; + } + + /** 包级可见的纯选择入口,供回归测试和其他无世界上下文的调用者使用。 */ + @Nullable + static AdvancedAlloyFurnaceRecipe selectBestCandidate(Iterable candidates, + List inputs, List fluidInputs, + List keyInputs, @Nullable ItemStack mold, + List expectedOutputs, long operations) { + RecipeLookupContext context = new RecipeLookupContext( + inputs, fluidInputs, keyInputs, mold, + RecipeOutputConstraint.exact(expectedOutputs), operations, + AlloyFurnaceTierRules.NO_MACHINE_TIER); + return getInstance().selectBestRecipe(candidates, context); + } + + @Nullable + static AdvancedAlloyFurnaceRecipe selectBestCandidateWithConstraints( + Iterable candidates, + List inputs, List fluidInputs, + List keyInputs, @Nullable ItemStack mold, + List expectedOutputs, long operations) { + RecipeLookupContext context = new RecipeLookupContext( + inputs, fluidInputs, keyInputs, mold, expectedOutputs, operations, + AlloyFurnaceTierRules.NO_MACHINE_TIER); + return getInstance().selectBestRecipe(candidates, context); + } + + private boolean matchesLookup(AdvancedAlloyFurnaceRecipe recipe, + RecipeLookupContext context, LookupSnapshot snapshot) { + return AlloyFurnaceTierRules.allows(recipe.id(), recipe.tier(), context.machineTier()) + && matchesMold(recipe, context.mold()) + && matchesOutputConstraints(recipe, context.expectedOutputs()) + && matchesKeys(recipe, snapshot.keyInputs(), context.operations()) + && matchesItems(recipe, context.inputs(), context.keyInputs(), context.operations()) + && matchesFluids(recipe, context.fluidInputs(), context.keyInputs(), context.operations()); + } + + /** 按“模具专用、输入种类、各类数量、来源”比较具体度;并列项另按 ID 稳定选择。 */ + private int compareSpecificity(RecipeSpecificity candidateSpecificity, + RecipeSpecificity currentSpecificity) { + boolean candidateHasMold = candidateSpecificity.hasMold(); + boolean currentHasMold = currentSpecificity.hasMold(); + if (candidateHasMold != currentHasMold) return candidateHasMold ? 1 : -1; + + long candidateKinds = candidateSpecificity.inputKinds(); + long currentKinds = currentSpecificity.inputKinds(); + if (candidateKinds != currentKinds) return Long.compare(candidateKinds, currentKinds); + + long candidateItems = candidateSpecificity.itemAmount(); + long currentItems = currentSpecificity.itemAmount(); + if (candidateItems != currentItems) return Long.compare(candidateItems, currentItems); + + long candidateFluids = candidateSpecificity.fluidAmount(); + long currentFluids = currentSpecificity.fluidAmount(); + if (candidateFluids != currentFluids) return Long.compare(candidateFluids, currentFluids); + + long candidateKeys = candidateSpecificity.keyAmount(); + long currentKeys = currentSpecificity.keyAmount(); + if (candidateKeys != currentKeys) return Long.compare(candidateKeys, currentKeys); + + boolean candidateConverted = candidateSpecificity.converted(); + boolean currentConverted = currentSpecificity.converted(); + if (candidateConverted != currentConverted) return candidateConverted ? -1 : 1; + + return 0; + } + + private int compareRecipeId(AdvancedAlloyFurnaceRecipe left, AdvancedAlloyFurnaceRecipe right) { + return left.id().toString().compareTo(right.id().toString()); + } + + private boolean isAmbiguousManualCraftingLookup( + RecipeLookupContext context, List candidates) { + if (!context.expectedOutputs().isEmpty() + || context.mold() == null + || !context.mold().is(Items.CRAFTING_TABLE) + || candidates.size() < 2) { + return false; + } + + Map expected = outputSignature(candidates.get(0)); + for (int index = 1; index < candidates.size(); index++) { + if (!expected.equals(outputSignature(candidates.get(index)))) { + return true; + } + } + return false; + } + + private Map outputSignature(AdvancedAlloyFurnaceRecipe recipe) { + Map result = new LinkedHashMap<>(); + for (ItemStack output : recipe.outputs()) { + mergeOutput(result, GenericStack.fromItemStack(output)); + } + for (FluidStack output : recipe.outputFluids()) { + mergeOutput(result, GenericStack.fromFluidStack(output)); + } + for (GenericStack output : recipe.keyOutputs()) { + mergeOutput(result, output); + } + return result; + } + + private void mergeOutput(Map outputs, @Nullable GenericStack output) { + if (output == null || output.what() == null || output.amount() <= 0L) { + return; + } + outputs.merge(output.what(), output.amount(), AlloyFurnaceRecipeManager::saturatingAdd); + } + + private boolean matchesItems(AdvancedAlloyFurnaceRecipe recipe, List inputs, + List keyInputs, long operations) { + return ItemIngredientAllocator.matches(recipe.inputs(), inputs, keyInputs, operations); + } + + private boolean matchesFluids(AdvancedAlloyFurnaceRecipe recipe, List inputs, long operations) { + return FluidIngredientAllocator.matchesLong(recipe.inputFluids(), inputs, operations); + } + + private boolean matchesFluids(AdvancedAlloyFurnaceRecipe recipe, + List inputs, List keyInputs, + long operations) { + return FluidIngredientAllocator.matchesLong(recipe.inputFluids(), inputs, keyInputs, operations); + } + + private boolean matchesKeys(AdvancedAlloyFurnaceRecipe recipe, + Map inputs, long operations) { + return containsScaled(inputs, snapshotGenericStacks(recipe.keyInputs()), operations); + } + + private static boolean containsScaled(Map available, Map required, long operations) { + for (Map.Entry entry : required.entrySet()) { + long amount = saturatingMultiply(entry.getValue(), operations); + if (available.getOrDefault(entry.getKey(), 0L) < amount) return false; + } + return true; + } + + /** + * AE 输出约束只比较 AEKey(包含组件),不比较数量,以兼容已有倍量样板。 + */ + public static boolean matchesExpectedOutputs(AdvancedAlloyFurnaceRecipe recipe, List expectedOutputs) { + return matchesOutputConstraints(recipe, RecipeOutputConstraint.exact(expectedOutputs)); + } + + /** + * Checks whether the supplied AE materials can cover an exact number of executions of a + * recipe. Pattern output counts are deliberately not considered here: callers use this when + * validating a manually scaled processing pattern after its output multiplier was resolved. + */ + public static boolean matchesInputsForOperations( + AdvancedAlloyFurnaceRecipe recipe, List inputs, + List fluidInputs, List keyInputs, long operations) { + if (recipe == null || operations <= 0L) { + return false; + } + return ItemIngredientAllocator.matches(recipe.inputs(), inputs, keyInputs, operations) + && FluidIngredientAllocator.matchesLong(recipe.inputFluids(), fluidInputs, keyInputs, operations) + && containsScaled(snapshotNonStackKeyInputs(keyInputs), + snapshotGenericStacks(recipe.keyInputs()), operations); + } + + public static boolean matchesOutputConstraints( + AdvancedAlloyFurnaceRecipe recipe, List expectedOutputs) { + if (expectedOutputs == null || expectedOutputs.isEmpty()) return true; + + Set available = new LinkedHashSet<>(); + for (ItemStack output : recipe.outputs()) { + GenericStack stack = GenericStack.fromItemStack(output); + if (stack != null) available.add(stack.what()); + } + for (FluidStack output : recipe.outputFluids()) { + GenericStack stack = GenericStack.fromFluidStack(output); + if (stack != null) available.add(stack.what()); + } + for (GenericStack output : recipe.keyOutputs()) { + if (output != null && output.what() != null) available.add(output.what()); + } + + for (RecipeOutputConstraint expected : expectedOutputs) { + if (expected == null || available.stream().noneMatch(expected::matches)) return false; + } + return true; + } + + /** 收集所有可能匹配的外部配方,最终由统一匹配器过滤和排序。 */ + private List findAdaptedRecipes( + Level level, RecipeLookupContext context, RecipeCacheKey cacheKey) { + ItemStack mold = context.mold(); + List> exactAdapters = + mold == null || mold.isEmpty() ? null : moldAdapterMap.get(mold.getItem()); + if ((exactAdapters == null || exactAdapters.isEmpty()) && fallbackAdapters.isEmpty()) { + return List.of(); + } + + List candidates = new ArrayList<>(); + Map mergedInputs = mergeItemInputs(context.inputs(), context.keyInputs()); + Map mergedFluids = mergeFluidInputs( + context.fluidInputs(), context.keyInputs()); + Map mergedKeys = AdapterUtils.mergeKeys(context.keyInputs()); + + if (exactAdapters != null) { + for (com.sorrowmist.useless.api.recipe.IRecipeAdapter exactAdapter : exactAdapters) { + collectAdapterRecipes(exactAdapter, level, context.inputs(), mergedInputs, mergedFluids, + mergedKeys, mold, cacheKey, candidates); + } + } + + for (com.sorrowmist.useless.api.recipe.IRecipeAdapter adapter : fallbackAdapters) { + if (adapter.matchesMold(mold)) { + collectAdapterRecipes(adapter, level, context.inputs(), mergedInputs, mergedFluids, + mergedKeys, mold, cacheKey, candidates); + } + } + return candidates; + } + + @SuppressWarnings("unchecked") + private > void collectAdapterRecipes( + com.sorrowmist.useless.api.recipe.IRecipeAdapter adapter, Level level, + List actualInputs, + Map mergedInputs, Map mergedFluids, + Map mergedKeys, @Nullable ItemStack mold, + RecipeCacheKey cacheKey, + List candidates) { + com.sorrowmist.useless.api.recipe.IRecipeAdapter typedAdapter = + (com.sorrowmist.useless.api.recipe.IRecipeAdapter) adapter; + for (RecipeHolder holder : findAdapterMatches( + typedAdapter, level, mergedInputs, mergedFluids, mergedKeys, mold, actualInputs, cacheKey)) { + candidates.addAll(convertAdapterRecipes(typedAdapter, holder, level, actualInputs)); + } + } + + @SuppressWarnings("unchecked") + private > List> findAdapterMatches( + com.sorrowmist.useless.api.recipe.IRecipeAdapter adapter, Level level, + Map mergedInputs, Map mergedFluids, + Map mergedKeys, @Nullable ItemStack mold, List actualInputs, + RecipeCacheKey cacheKey) { + AdapterLookupKey lookupKey = AdapterLookupKey.of( + level.getRecipeManager(), adapter, cacheKey, actualInputs); + List> cached = adapterMatchCache.get(lookupKey); + if (cached != null) { + return (List>) (List) cached; + } + + List> matches = adapter.findMatchingRecipes( + level, mergedInputs, mergedFluids, mergedKeys, mold, actualInputs); + if (matches == null || matches.isEmpty()) { + adapterMatchCache.put(lookupKey, List.of()); + return List.of(); + } + + List> immutable = new ArrayList<>(matches.size()); + for (RecipeHolder match : matches) { + if (match != null) immutable.add(match); + } + List> result = List.copyOf(immutable); + adapterMatchCache.put(lookupKey, result); + return (List>) (List) result; + } + + private > List convertAdapterRecipes( + com.sorrowmist.useless.api.recipe.IRecipeAdapter adapter, + RecipeHolder holder, Level level, List actualInputs) { + if (usesRuntimeConversion(adapter)) { + return RecipeConversionUtils.convertAll(adapter, holder, level, actualInputs); + } + + AdapterConversionKey key = new AdapterConversionKey( + level.getRecipeManager(), adapter, holder.id()); + return conversionCache.computeIfAbsent(key, ignored -> { + List converted = RecipeConversionUtils.convertAll( + adapter, holder, level); + return converted == null || converted.isEmpty() ? List.of() : List.copyOf(converted); + }); + } + + private boolean usesRuntimeConversion( + com.sorrowmist.useless.api.recipe.IRecipeAdapter adapter) { + return runtimeConversionCache.computeIfAbsent(adapter, current -> { + try { + Method method = current.getClass().getMethod( + "convertAll", RecipeHolder.class, Level.class, List.class); + return method.getDeclaringClass() + != com.sorrowmist.useless.api.recipe.IRecipeAdapter.class; + } catch (NoSuchMethodException | SecurityException exception) { + // Be conservative for an adapter with an unusual bridge/default method. + return true; + } + }); + } + + private boolean matchesMold(AdvancedAlloyFurnaceRecipe recipe, @Nullable ItemStack mold) { + if (recipe.molds().size() > 1) return false; + Ingredient requiredMold = recipe.mold(); + + if (requiredMold == null || requiredMold.isEmpty()) { + return true; + } + + if (mold == null || mold.isEmpty()) { + return false; + } + + return AdapterUtils.matchesMold(requiredMold, mold); + } + + private void cacheRecipe(LookupCacheKey key, AdvancedAlloyFurnaceRecipe recipe) { + recipeCache.put(key, new CachedLookup(recipe)); + } + + private void cacheNegativeResult(LookupCacheKey key) { + recipeCache.put(key, new CachedLookup(null)); + } + + public void clearCache() { + recipeCache.clear(); + adapterMatchCache.clear(); + conversionCache.clear(); + } + + /** + * 强制重建索引(仅在配方注册表变更时调用,如数据包重载) + */ + public synchronized void invalidateIndex() { + recipeIndexes.clear(); + clearCache(); + AlloyFurnaceRecipeCatalog.invalidate(); + } + + /** Invalidates only the recipe index and catalog owned by the supplied level. */ + public synchronized void invalidateIndex(Level level) { + if (level == null) { + invalidateIndex(); + return; + } + recipeIndexes.remove(level.getRecipeManager()); + clearCache(); + AlloyFurnaceRecipeCatalog.invalidate(level); + } + + private record LookupSnapshot(Map keyInputs) { + private static LookupSnapshot from(RecipeLookupContext context) { + return new LookupSnapshot(snapshotNonStackKeyInputs(context.keyInputs())); + } + } + + private record RecipeSpecificity( + boolean hasMold, + long inputKinds, + long itemAmount, + long fluidAmount, + long keyAmount, + boolean converted + ) { + private static RecipeSpecificity from(AdvancedAlloyFurnaceRecipe recipe) { + long itemAmount = 0L; + for (CountedIngredient input : recipe.inputs()) { + if (input != null) { + itemAmount = saturatingAdd(itemAmount, Math.max(0L, input.count())); + } + } + + long fluidAmount = 0L; + for (LongSizedFluidIngredient input : recipe.inputFluids()) { + if (input != null) { + fluidAmount = saturatingAdd(fluidAmount, Math.max(0L, input.amount())); + } + } + + long keyAmount = 0L; + for (GenericStack input : recipe.keyInputs()) { + if (input != null) { + keyAmount = saturatingAdd(keyAmount, Math.max(0L, input.amount())); + } + } + + return new RecipeSpecificity( + !recipe.molds().isEmpty(), + (long) recipe.inputs().size() + + recipe.inputFluids().size() + + recipe.keyInputs().size(), + itemAmount, + fluidAmount, + keyAmount, + recipe.id().getPath().endsWith("_converted")); + } + } + + private record RecipeIndex( + Map> inputItemIndex, + Map> moldIndex, + Map> inputFluidIndex, + Map> keyInputIndex, + List fluidFallbackRecipes, + List nonSimpleIngredientRecipes, + List unindexedRecipes + ) { + } + + /** Preserves insertion order while deduplicating by identity, avoiding recipe record hashing. */ + private static final class RecipeAccumulator { + private final List recipes; + private final IdentityHashMap seen; + + private RecipeAccumulator() { + this(8); + } + + private RecipeAccumulator(int expectedSize) { + int capacity = Math.max(1, expectedSize); + this.recipes = new ArrayList<>(capacity); + this.seen = new IdentityHashMap<>(capacity); + } + + private boolean add(@Nullable AdvancedAlloyFurnaceRecipe recipe) { + if (recipe == null || seen.put(recipe, Boolean.TRUE) != null) return false; + recipes.add(recipe); + return true; + } + + private void addAll(Iterable additions) { + if (additions == null) return; + for (AdvancedAlloyFurnaceRecipe recipe : additions) add(recipe); + } + + private List toList() { + return List.copyOf(recipes); + } + } + + /** Builds with identity accumulators, then freezes the result for lock-free read access. */ + private static final class RecipeIndexBuilder { + private final Map inputItemIndex = new LinkedHashMap<>(); + private final Map moldIndex = new LinkedHashMap<>(); + private final Map inputFluidIndex = new LinkedHashMap<>(); + private final Map keyInputIndex = new LinkedHashMap<>(); + private final RecipeAccumulator fluidFallbackRecipes = new RecipeAccumulator(); + private final RecipeAccumulator nonSimpleIngredientRecipes = new RecipeAccumulator(); + private final RecipeAccumulator unindexedRecipes = new RecipeAccumulator(); + private final IdentityHashMap indexedRecipes = + new IdentityHashMap<>(); + + private void indexRecipe(AdvancedAlloyFurnaceRecipe recipe) { + if (recipe == null || indexedRecipes.put(recipe, Boolean.TRUE) != null) return; + + boolean hasSearchableRequirement = false; + boolean hasMeaningfulRequirement = false; + + for (CountedIngredient counted : recipe.inputs()) { + if (counted == null || counted.count() <= 0L) continue; + Ingredient ingredient = counted.ingredient(); + if (ingredient == null || ingredient.isEmpty()) continue; + + hasMeaningfulRequirement = true; + boolean represented = false; + try { + for (ItemStack stack : ingredient.getItems()) { + if (stack != null && !stack.isEmpty()) { + add(inputItemIndex, stack.getItem(), recipe); + represented = true; + } + } + } catch (RuntimeException ignored) { + // A custom ingredient may not expose display representatives. + } + if (!isSimple(ingredient)) { + nonSimpleIngredientRecipes.add(recipe); + } + if (!represented) { + unindexedRecipes.add(recipe); + } else { + hasSearchableRequirement = true; + } + } + + for (Ingredient mold : recipe.molds()) { + if (mold == null || mold.isEmpty()) continue; + + hasMeaningfulRequirement = true; + boolean represented = false; + try { + for (ItemStack stack : mold.getItems()) { + if (stack != null && !stack.isEmpty()) { + add(moldIndex, stack.getItem(), recipe); + represented = true; + } + } + } catch (RuntimeException ignored) { + // Fall back to the broad list for opaque mold ingredients. + } + if (!isSimple(mold)) { + nonSimpleIngredientRecipes.add(recipe); + } + if (!represented) { + unindexedRecipes.add(recipe); + } else { + hasSearchableRequirement = true; + } + } + + for (LongSizedFluidIngredient counted : recipe.inputFluids()) { + if (counted == null || counted.amount() <= 0L + || counted.ingredient() == null || counted.ingredient().isEmpty()) { + continue; + } + + hasMeaningfulRequirement = true; + boolean represented = false; + try { + for (FluidStack stack : counted.ingredient().getStacks()) { + if (stack != null && !stack.isEmpty()) { + add(inputFluidIndex, stack.getFluid(), recipe); + represented = true; + } + } + } catch (RuntimeException ignored) { + // Custom fluid ingredients are handled by the fallback list. + } + if (!isSimple(counted.ingredient()) || !represented) { + fluidFallbackRecipes.add(recipe); + } + hasSearchableRequirement = true; + } + + for (GenericStack input : recipe.keyInputs()) { + if (input == null || input.what() == null || input.amount() <= 0L) continue; + hasMeaningfulRequirement = true; + add(keyInputIndex, input.what(), recipe); + hasSearchableRequirement = true; + } + + // Recipes with no concrete lookup key (for example catalyst-only recipes) still need + // to be considered for every query. + if (!hasMeaningfulRequirement || !hasSearchableRequirement) { + unindexedRecipes.add(recipe); + } + } + + private RecipeIndex build() { + return new RecipeIndex( + freeze(inputItemIndex), + freeze(moldIndex), + freeze(inputFluidIndex), + freeze(keyInputIndex), + fluidFallbackRecipes.toList(), + nonSimpleIngredientRecipes.toList(), + unindexedRecipes.toList()); + } + + private static boolean isSimple(Ingredient ingredient) { + try { + return ingredient.isSimple(); + } catch (RuntimeException ignored) { + return false; + } + } + + private static boolean isSimple(FluidIngredient ingredient) { + try { + return ingredient.isSimple(); + } catch (RuntimeException ignored) { + return false; + } + } + + private static void add( + Map index, + K key, AdvancedAlloyFurnaceRecipe recipe) { + index.computeIfAbsent(key, ignored -> new RecipeAccumulator()).add(recipe); + } + + private static Map> freeze( + Map source) { + Map> result = new LinkedHashMap<>(source.size()); + for (Map.Entry entry : source.entrySet()) { + result.put(entry.getKey(), entry.getValue().toList()); + } + return Collections.unmodifiableMap(result); + } + } + + private record RecipeLookupContext( + List inputs, + List fluidInputs, + List keyInputs, + @Nullable ItemStack mold, + List expectedOutputs, + long operations, + int machineTier + ) { + private RecipeLookupContext { + inputs = inputs == null ? List.of() : inputs; + fluidInputs = fluidInputs == null ? List.of() : fluidInputs; + keyInputs = requireKeyInputs(keyInputs); + expectedOutputs = expectedOutputs == null ? List.of() : expectedOutputs; + operations = Math.max(1L, operations); + machineTier = machineTier < AlloyFurnaceTierRules.NO_MACHINE_TIER + ? AlloyFurnaceTierRules.NO_MACHINE_TIER + : Math.min(AlloyFurnaceTierRules.MAX_TIER, machineTier); + } + } + + private static List requireKeyInputs(@Nullable List inputs) { + if (inputs == null) { + return List.of(); + } + for (GenericStack input : inputs) { + Objects.requireNonNull(input, "Recipe key input"); + if (input.what() == null || input.amount() <= 0L) { + throw new IllegalArgumentException("Recipe key inputs must contain a key and positive amount"); + } + } + return inputs; + } + + /** Represents both a successful and a negative lookup without a second map. */ + private record CachedLookup(@Nullable AdvancedAlloyFurnaceRecipe recipe) { + } + + /** The public cache fingerprint remains order-independent; manager identity is internal. */ + private record LookupCacheKey(RecipeManager recipeManager, RecipeCacheKey lookup) { + } + + /** Static conversions are scoped to the recipe manager that supplied the holder. */ + private record AdapterConversionKey( + RecipeManager recipeManager, + com.sorrowmist.useless.api.recipe.IRecipeAdapter adapter, + ResourceLocation recipeId + ) { + } + + /** + * Adapter matching can inspect the concrete input list, so its key keeps stack order and + * components in addition to the aggregated lookup fingerprint. + */ + private record AdapterLookupKey( + RecipeManager recipeManager, + com.sorrowmist.useless.api.recipe.IRecipeAdapter adapter, + Map items, + Map fluids, + Map keys, + @Nullable AEKey mold, + List orderedInputs + ) { + private static AdapterLookupKey of( + RecipeManager recipeManager, + com.sorrowmist.useless.api.recipe.IRecipeAdapter adapter, + RecipeCacheKey lookup, + List actualInputs) { + return new AdapterLookupKey( + recipeManager, + adapter, + lookup.items(), + lookup.fluids(), + lookup.keys(), + lookup.mold(), + orderedItemSignature(actualInputs)); + } + } + + private record OrderedItemSignature(@Nullable AEKey key, int count) { + } + + /** + * 只保存不可变的 AEKey/数量快照,不持有调用方可变的 ItemStack。 + *

+ * 这里刻意不用 record:缓存命中路径每次都要拿这个键去哈希与判等,而三张 map 各自的 + * hashCode 都要遍历一遍。record 的 hashCode 每次重算,改成在构造时算一次存下来, + * 命中路径就只剩读一个 int。判等语义与字段全部不变,缓存命中率不受影响。 + */ + static final class RecipeCacheKey { + private final Map items; + private final Map fluids; + private final Map keys; + @Nullable + private final AEKey mold; + private final List expectedOutputs; + private final long operations; + private final int machineTier; + private final long tierRulesVersion; + /** 构造时算好的哈希值,供缓存查找反复读取。 */ + private final int cachedHash; + + private RecipeCacheKey( + Map items, + Map fluids, + Map keys, + @Nullable AEKey mold, + List expectedOutputs, + long operations, + int machineTier, + long tierRulesVersion + ) { + this.items = items; + this.fluids = fluids; + this.keys = keys; + this.mold = mold; + this.expectedOutputs = expectedOutputs; + this.operations = operations; + this.machineTier = machineTier; + this.tierRulesVersion = tierRulesVersion; + this.cachedHash = computeHash(); + } + + private int computeHash() { + int result = this.items.hashCode(); + result = 31 * result + this.fluids.hashCode(); + result = 31 * result + this.keys.hashCode(); + result = 31 * result + Objects.hashCode(this.mold); + result = 31 * result + this.expectedOutputs.hashCode(); + result = 31 * result + Long.hashCode(this.operations); + result = 31 * result + this.machineTier; + result = 31 * result + Long.hashCode(this.tierRulesVersion); + return result; + } + + Map items() { + return this.items; + } + + Map fluids() { + return this.fluids; + } + + Map keys() { + return this.keys; + } + + @Nullable + AEKey mold() { + return this.mold; + } + + @Override + public int hashCode() { + return this.cachedHash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof RecipeCacheKey other)) return false; + // 先比基本类型字段,再比集合,冲突时能少走几次 map 判等 + return this.operations == other.operations + && this.machineTier == other.machineTier + && this.tierRulesVersion == other.tierRulesVersion + && Objects.equals(this.mold, other.mold) + && this.items.equals(other.items) + && this.fluids.equals(other.fluids) + && this.keys.equals(other.keys) + && this.expectedOutputs.equals(other.expectedOutputs); + } + + private static RecipeCacheKey from(RecipeLookupContext context) { + LookupInputFingerprint fingerprint = LookupInputFingerprint.from(context); + AEItemKey moldKey = context.mold() == null || context.mold().isEmpty() + ? null + : AEItemKey.of(context.mold()); + return new RecipeCacheKey( + fingerprint.items(), + fingerprint.fluids(), + fingerprint.keys(), + moldKey, + List.copyOf(context.expectedOutputs()), + context.operations(), + context.machineTier(), + context.machineTier() == AlloyFurnaceTierRules.NO_MACHINE_TIER + ? 0L : AlloyFurnaceTierRules.configurationVersion() + ); + } + + static RecipeCacheKey create(List inputs, List fluidInputs, + List keyInputs, @Nullable ItemStack mold, + List expectedOutputs, long operations) { + return from(new RecipeLookupContext( + inputs, fluidInputs, keyInputs, mold, + RecipeOutputConstraint.exact(expectedOutputs), operations, + AlloyFurnaceTierRules.NO_MACHINE_TIER)); + } + + static RecipeCacheKey createWithConstraints( + List inputs, List fluidInputs, + List keyInputs, @Nullable ItemStack mold, + List expectedOutputs, long operations) { + return from(new RecipeLookupContext( + inputs, fluidInputs, keyInputs, mold, expectedOutputs, operations, + AlloyFurnaceTierRules.NO_MACHINE_TIER)); + } + } + + /** + * Builds all cache-key input maps in one pass over each supplied collection. + *

+ * 缓存命中也要先构建一次键,故只在有内容时才分配 map,构建完直接冻结,不再整份复制。 + */ + private record LookupInputFingerprint( + Map items, + Map fluids, + Map keys + ) { + private static LookupInputFingerprint from(RecipeLookupContext context) { + Map items = null; + for (ItemStack stack : context.inputs()) { + if (stack == null || stack.isEmpty()) continue; + AEItemKey key = AEItemKey.of(stack); + if (key != null) { + items = mergeAmount(items, key, stack.getCount()); + } + } + + Map fluids = null; + for (FluidStack stack : context.fluidInputs()) { + if (stack == null || stack.isEmpty()) continue; + AEFluidKey key = AEFluidKey.of(stack); + if (key != null) { + fluids = mergeAmount(fluids, key, stack.getAmount()); + } + } + + Map keys = null; + for (GenericStack stack : context.keyInputs()) { + if (stack.what() instanceof AEItemKey) { + items = mergeAmount(items, stack.what(), stack.amount()); + } else if (stack.what() instanceof AEFluidKey) { + fluids = mergeAmount(fluids, stack.what(), stack.amount()); + } else { + keys = mergeAmount(keys, stack.what(), stack.amount()); + } + } + + return new LookupInputFingerprint(frozen(items), frozen(fluids), frozen(keys)); + } + + private static Map mergeAmount( + @Nullable Map amounts, AEKey key, long amount) { + Map target = amounts == null ? new HashMap<>() : amounts; + target.merge(key, amount, AlloyFurnaceRecipeManager::saturatingAdd); + return target; + } + + private static Map frozen(@Nullable Map amounts) { + return amounts == null ? Map.of() : Collections.unmodifiableMap(amounts); + } + } + + private static List orderedItemSignature(List stacks) { + if (stacks == null || stacks.isEmpty()) return List.of(); + + List result = new ArrayList<>(stacks.size()); + for (ItemStack stack : stacks) { + if (stack == null || stack.isEmpty()) { + result.add(new OrderedItemSignature(null, 0)); + continue; + } + + GenericStack genericStack = GenericStack.fromItemStack(stack); + result.add(new OrderedItemSignature( + genericStack == null ? null : genericStack.what(), + stack.getCount())); + } + return List.copyOf(result); + } + + /** Gives item-based adapters the same long amounts for physical stacks and AE item keys. */ + private static Map mergeItemInputs( + List stacks, List keyInputs) { + Object2LongLinkedOpenHashMap amounts = new Object2LongLinkedOpenHashMap<>(); + for (ItemStack stack : stacks) { + if (stack == null || stack.isEmpty()) continue; + amounts.mergeLong(AEItemKey.of(stack), + stack.getCount(), AlloyFurnaceRecipeManager::saturatingAdd); + } + for (GenericStack stack : keyInputs) { + if (stack.what() instanceof AEItemKey itemKey) { + amounts.mergeLong(itemKey, + stack.amount(), AlloyFurnaceRecipeManager::saturatingAdd); + } + } + Object2LongLinkedOpenHashMap result = new Object2LongLinkedOpenHashMap<>(amounts.size()); + for (var entry : amounts.object2LongEntrySet()) { + result.put(DataComponentIngredient.of(true, entry.getKey().toStack(1)), entry.getLongValue()); + } + return result; + } + + private static Map mergeFluidInputs( + List stacks, List keyInputs) { + Map result = new LinkedHashMap<>(); + if (stacks != null) { + for (FluidStack stack : stacks) { + if (stack != null && !stack.isEmpty() && stack.getAmount() > 0) { + mergeFluidAmount(result, stack, stack.getAmount()); + } + } + } + for (GenericStack input : keyInputs) { + if (input.what() instanceof AEFluidKey fluidKey) { + mergeFluidAmount(result, fluidKey.toStack(1), input.amount()); + } + } + return result; + } + + private static void mergeFluidAmount(Map target, + FluidStack stack, long amount) { + for (Map.Entry entry : target.entrySet()) { + if (com.sorrowmist.useless.compat.forge1201.fluid.FluidStacks.same(entry.getKey(), stack)) { + entry.setValue(saturatingAdd(entry.getValue(), amount)); + return; + } + } + target.put(new net.minecraftforge.fluids.FluidStack(stack, 1), amount); + } + + private static Map snapshotNonStackKeyInputs(List stacks) { + Map result = new LinkedHashMap<>(); + for (GenericStack stack : stacks) { + if (stack.what() instanceof AEItemKey || stack.what() instanceof AEFluidKey) { + continue; + } + result.merge(stack.what(), stack.amount(), AlloyFurnaceRecipeManager::saturatingAdd); + } + return Map.copyOf(result); + } + + private static Map snapshotGenericStacks(List stacks) { + Map result = new LinkedHashMap<>(); + for (GenericStack stack : stacks) { + result.merge(stack.what(), stack.amount(), AlloyFurnaceRecipeManager::saturatingAdd); + } + return Map.copyOf(result); + } + + private static long saturatingAdd(long a, long b) { + if (a >= Long.MAX_VALUE - b) return Long.MAX_VALUE; + return a + b; + } + + private static long saturatingMultiply(long amount, long multiplier) { + if (amount <= 0 || multiplier <= 0) return 0; + if (amount > Long.MAX_VALUE / multiplier) return Long.MAX_VALUE; + return amount * multiplier; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/BeefUnbreakableRecipe.java b/src/main/java/com/sorrowmist/useless/content/recipe/BeefUnbreakableRecipe.java new file mode 100644 index 00000000..dc7e0cc5 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/BeefUnbreakableRecipe.java @@ -0,0 +1,91 @@ +package com.sorrowmist.useless.content.recipe; + +import com.sorrowmist.useless.content.items.EndlessBeafItem; + +import net.minecraft.world.item.ItemStack; + +import net.minecraft.world.item.crafting.CraftingBookCategory; +import net.minecraft.world.inventory.CraftingContainer; +import net.minecraft.world.item.crafting.CustomRecipe; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.level.Level; + +/** + * 造化杖 + 任意物品 → 该物品的「不可破坏」版本。 + *

+ * 一个 {@link CustomRecipe}(特殊配方,{@code isSpecial() == true},不占配方书): + *

    + *
  • 合成格里恰好有 1 个非造化杖物品,且至少 1 个造化杖变体;
  • + *
  • 该物品尚未带 {@code DataComponents.UNBREAKABLE}(已不可破坏的不再参与);
  • + *
  • 2×2 与 3×3 都可用({@link #canCraftInDimensions})。
  • + *
+ * 产出 = 该物品本体(保留 NBT / 组件),额外打上 {@code UNBREAKABLE}。 + *

+ * 消耗语义:目标物品被正常消耗;造化杖不消耗 —— 走 {@link EndlessBeafItem} 覆写的 + * {@code hasCraftingRemainingItem / getCraftingRemainingItem},由 {@code Recipe} 的默认 + * {@code getRemainingItems} 自动返还(本类刻意不覆写它)。 + */ +public class BeefUnbreakableRecipe extends CustomRecipe { + public BeefUnbreakableRecipe(net.minecraft.resources.ResourceLocation id, CraftingBookCategory category) { + super(id,category); + } + + @Override + public boolean matches(CraftingContainer input, Level level) { + return findTarget(input) != null; + } + + @Override + public ItemStack assemble(CraftingContainer input, net.minecraft.core.RegistryAccess registries) { + ItemStack target = findTarget(input); + if (target == null) { + return ItemStack.EMPTY; + } + ItemStack result = target.copyWithCount(1); + result.getOrCreateTag().putBoolean("Unbreakable",true); + return result; + } + + @Override + public boolean canCraftInDimensions(int width, int height) { + // 2×2 与 3×3 都支持 + return width * height >= 2; + } + + @Override + public RecipeSerializer getSerializer() { + return com.sorrowmist.useless.init.ModRecipeSerializers.BEEF_UNBREAKABLE_SERIALIZER.get(); + } + + /** + * 找出「唯一的目标物品」:格子内恰好一个非造化杖物品,且至少一个造化杖。 + * 不满足返回 null。 + */ + private static ItemStack findTarget(CraftingContainer input) { + ItemStack target = null; + boolean hasStaff = false; + for (int i = 0; i < input.getContainerSize(); i++) { + ItemStack stack = input.getItem(i); + if (stack.isEmpty()) { + continue; + } + if (stack.getItem() instanceof EndlessBeafItem) { + hasStaff = true; + continue; + } + // 第二个非杖物品 → 不是本配方的形状 + if (target != null) { + return null; + } + target = stack; + } + if (!hasStaff || target == null) { + return null; + } + // 已不可破坏的目标不再参与 + if (target.hasTag() && target.getTag().getBoolean("Unbreakable")) { + return null; + } + return target; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/CountedIngredient.java b/src/main/java/com/sorrowmist/useless/content/recipe/CountedIngredient.java new file mode 100644 index 00000000..cc4f5243 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/CountedIngredient.java @@ -0,0 +1,28 @@ +package com.sorrowmist.useless.content.recipe; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.ByteBufCodecs; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import net.minecraft.world.item.crafting.Ingredient; + +public record CountedIngredient( + Ingredient ingredient, + long count +) { + static final StreamCodec INGREDIENT_STREAM_CODEC = + com.sorrowmist.useless.compat.forge1201.recipe.RecipeCodecs.INGREDIENT_STREAM; + + public static final StreamCodec STREAM_CODEC = + StreamCodec.composite( + INGREDIENT_STREAM_CODEC, CountedIngredient::ingredient, + ByteBufCodecs.VAR_LONG, CountedIngredient::count, + CountedIngredient::new + ); + static final MapCodec CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group( + com.sorrowmist.useless.compat.forge1201.recipe.RecipeCodecs.INGREDIENT.fieldOf("ingredient").forGetter(CountedIngredient::ingredient), + Codec.LONG.optionalFieldOf("count", 1L).forGetter(CountedIngredient::count) + ).apply(instance, CountedIngredient::new)); +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/ExpectedOutputScaler.java b/src/main/java/com/sorrowmist/useless/content/recipe/ExpectedOutputScaler.java new file mode 100644 index 00000000..eaaf749c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/ExpectedOutputScaler.java @@ -0,0 +1,169 @@ +package com.sorrowmist.useless.content.recipe; + +import net.minecraft.world.item.ItemStack; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; + +/** Converts independent random item outputs into the smallest deterministic batch. */ +public final class ExpectedOutputScaler { + private static final int MAX_PROBABILITY_DENOMINATOR = 1000; + private static final double EXACT_FRACTION_EPSILON = 1.0E-9; + + private ExpectedOutputScaler() { + } + + public static Optional scale(List weightedOutputs) { + if (weightedOutputs == null || weightedOutputs.isEmpty()) { + return Optional.of(new ScaledOutputs(1, List.of())); + } + + List expectedOutputs = new ArrayList<>(); + long operations = 1; + + for (WeightedItemOutput weighted : weightedOutputs) { + if (weighted == null || weighted.stack() == null || weighted.stack().isEmpty()) { + continue; + } + if (weighted.min() < 0 || weighted.max() < weighted.min() || !Double.isFinite(weighted.chance())) { + return Optional.empty(); + } + + Fraction chance = approximateChance(weighted.chance()); + if (chance.numerator() == 0 || weighted.max() == 0) { + continue; + } + + final long numerator; + final long denominator; + try { + numerator = Math.multiplyExact(chance.numerator(), Math.addExact((long) weighted.min(), weighted.max())); + denominator = Math.multiplyExact(chance.denominator(), 2L); + } catch (ArithmeticException overflow) { + return Optional.empty(); + } + + long divisor = AdapterUtils.gcd(numerator, denominator); + long reducedNumerator = numerator / divisor; + long reducedDenominator = denominator / divisor; + expectedOutputs.add(new ExpectedItemOutput(weighted.stack().copyWithCount(1), reducedNumerator, reducedDenominator)); + + OptionalInt lcm = leastCommonMultiple(operations, reducedDenominator); + if (lcm.isEmpty()) { + return Optional.empty(); + } + operations = lcm.getAsInt(); + } + + List scaledOutputs = new ArrayList<>(); + for (ExpectedItemOutput expected : expectedOutputs) { + long factor = operations / expected.denominator(); + final long count; + try { + count = Math.multiplyExact(expected.numerator(), factor); + } catch (ArithmeticException overflow) { + return Optional.empty(); + } + if (count <= 0) { + continue; + } + if (!mergeOutput(scaledOutputs, expected.stack(), count)) { + return Optional.empty(); + } + } + + return Optional.of(new ScaledOutputs((int) operations, scaledOutputs)); + } + + public static OptionalInt multiplyToInt(long value, int multiplier) { + if (value < 0 || multiplier < 0) { + return OptionalInt.empty(); + } + try { + long result = Math.multiplyExact(value, (long) multiplier); + return result <= Integer.MAX_VALUE ? OptionalInt.of((int) result) : OptionalInt.empty(); + } catch (ArithmeticException overflow) { + return OptionalInt.empty(); + } + } + + private static Fraction approximateChance(double rawChance) { + double chance = Math.max(0.0, Math.min(1.0, rawChance)); + if (chance == 0.0) { + return new Fraction(0, 1); + } + if (chance == 1.0) { + return new Fraction(1, 1); + } + + long bestNumerator = 0; + long bestDenominator = 1; + double bestError = Double.MAX_VALUE; + for (int denominator = 1; denominator <= MAX_PROBABILITY_DENOMINATOR; denominator++) { + long numerator = Math.round(chance * denominator); + double error = Math.abs(chance - (double) numerator / denominator); + if (error < bestError) { + bestError = error; + bestNumerator = numerator; + bestDenominator = denominator; + if (error < EXACT_FRACTION_EPSILON) { + break; + } + } + } + + long divisor = AdapterUtils.gcd(bestNumerator, bestDenominator); + return new Fraction(bestNumerator / divisor, bestDenominator / divisor); + } + + private static OptionalInt leastCommonMultiple(long left, long right) { + if (left <= 0 || right <= 0) { + return OptionalInt.empty(); + } + long reducedRight = right / AdapterUtils.gcd(left, right); + try { + long result = Math.multiplyExact(left, reducedRight); + return result <= Integer.MAX_VALUE ? OptionalInt.of((int) result) : OptionalInt.empty(); + } catch (ArithmeticException overflow) { + return OptionalInt.empty(); + } + } + + private static boolean mergeOutput(List outputs, ItemStack stack, long amount) { + for (ItemStack existing : outputs) { + if (!ItemStack.isSameItemSameTags(existing, stack)) { + continue; + } + long merged = (long) existing.getCount() + amount; + if (merged > Integer.MAX_VALUE) { + return false; + } + existing.setCount((int) merged); + return true; + } + if (amount > Integer.MAX_VALUE) { + return false; + } + ItemStack output = stack.copy(); + output.setCount((int) amount); + outputs.add(output); + return true; + } + + public record WeightedItemOutput(ItemStack stack, int min, int max, double chance) { + } + + public record ScaledOutputs(int operations, List outputs) { + public ScaledOutputs { + outputs = outputs.stream().map(ItemStack::copy).toList(); + } + } + + private record ExpectedItemOutput(ItemStack stack, long numerator, long denominator) { + } + + private record Fraction(long numerator, long denominator) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/FluidIngredientAllocator.java b/src/main/java/com/sorrowmist/useless/content/recipe/FluidIngredientAllocator.java new file mode 100644 index 00000000..0bae9f73 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/FluidIngredientAllocator.java @@ -0,0 +1,484 @@ +package com.sorrowmist.useless.content.recipe; + +import appeng.api.stacks.AEFluidKey; +import appeng.api.stacks.GenericStack; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import net.minecraftforge.fluids.capability.templates.FluidTank; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * Allocates fluid supplies to sized fluid ingredients without reusing a tank amount. + * A max-flow network is used because tag/compound ingredients can overlap. + */ +public final class FluidIngredientAllocator { + private FluidIngredientAllocator() { + } + + public static boolean matches(List requirements, + List supplies, long operations) { + return allocate(requirements, supplies, operations) != null; + } + + public static boolean matchesLong(List requirements, + List supplies, long operations) { + return allocateLong(requirements, supplies, operations) != null; + } + + /** Matches ordinary fluid stacks plus AE fluid supplies whose amount may exceed an int. */ + public static boolean matches(List requirements, + List supplies, List keySupplies, + long operations) { + return allocate(requirements, supplies, keySupplies, operations) != null; + } + + public static boolean matchesLong(List requirements, + List supplies, List keySupplies, + long operations) { + return allocateLong(requirements, supplies, keySupplies, operations) != null; + } + + /** Matches already-aggregated fluid supplies without narrowing their long amounts to int. */ + public static boolean matches(List requirements, + Map supplies, long operations) { + return allocate(requirements, supplies, operations) != null; + } + + public static boolean matchesLong(List requirements, + Map supplies, long operations) { + return allocateLong(requirements, supplies, operations) != null; + } + + public static boolean matchesTanks(List requirements, + FluidTank[] tanks, int tankCount, long operations) { + return allocateTanks(requirements, tanks, tankCount, operations) != null; + } + + public static boolean matchesTanksLong(List requirements, + FluidTank[] tanks, int tankCount, long operations) { + return allocateTanksLong(requirements, tanks, tankCount, operations) != null; + } + + @Nullable + public static Allocation allocate(List requirements, + List supplies, long operations) { + return allocateLong(toLongRequirements(requirements), supplies, operations); + } + + @Nullable + public static Allocation allocateLong(List requirements, + List supplies, long operations) { + int resultSize = supplies == null ? 0 : supplies.size(); + List demands = demands(requirements, operations); + if (demands == null) return null; + if (demands.isEmpty()) { + return new Allocation(new long[resultSize]); + } + List normalizedSupplies = new ArrayList<>(); + if (supplies != null) { + for (int i = 0; i < supplies.size(); i++) { + FluidStack stack = supplies.get(i); + if (stack != null && !stack.isEmpty() && stack.getAmount() > 0) { + normalizedSupplies.add(new Supply(i, stack.getAmount(), stack.copy())); + } + } + } + return solve(normalizedSupplies, demands, resultSize); + } + + @Nullable + public static Allocation allocate(List requirements, + List supplies, List keySupplies, + long operations) { + return allocateLong(toLongRequirements(requirements), supplies, keySupplies, operations); + } + + @Nullable + public static Allocation allocateLong(List requirements, + List supplies, List keySupplies, + long operations) { + int inputSize = supplies == null ? 0 : supplies.size(); + List demands = demands(requirements, operations); + if (demands == null) return null; + if (demands.isEmpty()) { + return new Allocation(new long[inputSize + (keySupplies == null ? 0 : keySupplies.size())]); + } + List normalizedSupplies = new ArrayList<>(); + int resultSize = inputSize; + if (supplies != null) { + for (int i = 0; i < supplies.size(); i++) { + FluidStack stack = supplies.get(i); + if (stack != null && !stack.isEmpty() && stack.getAmount() > 0) { + normalizedSupplies.add(new Supply(i, stack.getAmount(), stack.copy())); + } + } + } + if (keySupplies != null) { + for (int i = 0; i < keySupplies.size(); i++) { + GenericStack generic = keySupplies.get(i); + if (generic == null || generic.amount() <= 0 || !(generic.what() instanceof AEFluidKey fluid)) { + continue; + } + normalizedSupplies.add(new Supply(resultSize + i, generic.amount(), fluid.toStack(1))); + } + resultSize += keySupplies.size(); + } + return solve(normalizedSupplies, demands, resultSize); + } + + @Nullable + public static Allocation allocate(List requirements, + Map supplies, long operations) { + return allocateLong(toLongRequirements(requirements), supplies, operations); + } + + @Nullable + public static Allocation allocateLong(List requirements, + Map supplies, long operations) { + int resultSize = supplies == null ? 0 : supplies.size(); + List demands = demands(requirements, operations); + if (demands == null) return null; + if (demands.isEmpty()) { + return new Allocation(new long[resultSize]); + } + List normalizedSupplies = new ArrayList<>(); + if (supplies != null) { + int index = 0; + for (Map.Entry entry : supplies.entrySet()) { + FluidStack stack = entry.getKey(); + long amount = entry.getValue() == null ? 0L : entry.getValue(); + if (stack != null && !stack.isEmpty() && amount > 0L) { + // FluidIngredient predicates inspect identity/components, not the amount. + // Keeping a one-unit representative lets the allocator carry the complete + // aggregated long amount without creating billions of FluidStack fragments. + normalizedSupplies.add(new Supply(index, amount, new net.minecraftforge.fluids.FluidStack(stack, 1))); + } + index++; + } + } + return solve(normalizedSupplies, demands, resultSize); + } + + @Nullable + public static Allocation allocateTanks(List requirements, + FluidTank[] tanks, int tankCount, long operations) { + return allocateTanksLong(toLongRequirements(requirements), tanks, tankCount, operations); + } + + @Nullable + public static Allocation allocateTanksLong(List requirements, + FluidTank[] tanks, int tankCount, long operations) { + int count = tanks == null ? 0 : Math.max(0, Math.min(tankCount, tanks.length)); + List demands = demands(requirements, operations); + if (demands == null) return null; + if (demands.isEmpty()) { + return new Allocation(new long[count]); + } + List supplies = new ArrayList<>(); + for (int i = 0; i < count; i++) { + FluidStack stack = tanks[i] == null ? FluidStack.EMPTY : tanks[i].getFluid(); + if (!stack.isEmpty() && stack.getAmount() > 0) { + supplies.add(new Supply(i, stack.getAmount(), stack.copy())); + } + } + return solve(supplies, demands, count); + } + + /** Returns the largest whole operation count that all requirements can support. */ + public static int maxOperations(List requirements, + List supplies) { + return maxOperationsLong(toLongRequirements(requirements), supplies); + } + + public static int maxOperationsLong(List requirements, + List supplies) { + if (requirements == null || requirements.isEmpty()) return Integer.MAX_VALUE; + if (requirements.size() == 1) { + LongSizedFluidIngredient requirement = requirements.get(0); + if (requirement == null || requirement.amount() <= 0) return Integer.MAX_VALUE; + if (requirement.ingredient() == null || requirement.ingredient().isEmpty()) return 0; + long available = 0L; + if (supplies != null) { + for (FluidStack stack : supplies) { + if (stack != null && !stack.isEmpty() && stack.getAmount() > 0 + && requirement.ingredient().test(stack)) { + available = saturatingAdd(available, stack.getAmount()); + } + } + } + return operationCount(available / requirement.amount()); + } + long totalAvailable = 0L; + if (supplies != null) { + for (FluidStack stack : supplies) { + if (stack != null && !stack.isEmpty()) totalAvailable = saturatingAdd(totalAvailable, stack.getAmount()); + } + } + long perOperation = 0L; + for (LongSizedFluidIngredient requirement : requirements) { + if (requirement != null && requirement.amount() > 0) { + perOperation = saturatingAdd(perOperation, requirement.amount()); + } + } + if (perOperation <= 0L) return Integer.MAX_VALUE; + int low = 0; + int high = operationCount(totalAvailable / perOperation); + while (low < high) { + int middle = low + (int) (((long) high - low + 1L) / 2L); + if (matchesLong(requirements, supplies, middle)) low = middle; + else high = middle - 1; + } + return low; + } + + public static int maxTankOperations(List requirements, + FluidTank[] tanks, int tankCount) { + return maxTankOperationsLong(toLongRequirements(requirements), tanks, tankCount); + } + + public static int maxTankOperationsLong(List requirements, + FluidTank[] tanks, int tankCount) { + if (requirements == null || requirements.isEmpty()) return Integer.MAX_VALUE; + int count = tanks == null ? 0 : Math.max(0, Math.min(tankCount, tanks.length)); + if (requirements.size() == 1) { + LongSizedFluidIngredient requirement = requirements.get(0); + if (requirement == null || requirement.amount() <= 0) return Integer.MAX_VALUE; + if (requirement.ingredient() == null || requirement.ingredient().isEmpty()) return 0; + long available = 0L; + for (int i = 0; i < count; i++) { + FluidStack stack = tanks[i] == null ? FluidStack.EMPTY : tanks[i].getFluid(); + if (!stack.isEmpty() && stack.getAmount() > 0 + && requirement.ingredient().test(stack)) { + available = saturatingAdd(available, stack.getAmount()); + } + } + return operationCount(available / requirement.amount()); + } + long totalAvailable = 0L; + for (int i = 0; i < count; i++) { + if (tanks[i] != null) totalAvailable = saturatingAdd(totalAvailable, tanks[i].getFluidAmount()); + } + long perOperation = 0L; + for (LongSizedFluidIngredient requirement : requirements) { + if (requirement != null && requirement.amount() > 0) { + perOperation = saturatingAdd(perOperation, requirement.amount()); + } + } + if (perOperation <= 0L) return Integer.MAX_VALUE; + int low = 0; + int high = operationCount(totalAvailable / perOperation); + while (low < high) { + int middle = low + (int) (((long) high - low + 1L) / 2L); + if (matchesTanksLong(requirements, tanks, tankCount, middle)) low = middle; + else high = middle - 1; + } + return low; + } + + private static int operationCount(long operations) { + return operations >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) Math.max(0L, operations); + } + + private static List demands(List requirements, long operations) { + List result = new ArrayList<>(); + if (requirements == null || operations <= 0L) return result; + for (LongSizedFluidIngredient requirement : requirements) { + if (requirement == null || requirement.amount() <= 0) continue; + if (requirement.ingredient() == null || requirement.ingredient().isEmpty()) { + return null; + } + try { result.add(new Demand(requirement.ingredient(), Math.multiplyExact(requirement.amount(),operations))); } + catch(ArithmeticException overflow) { return null; } + } + return result; + } + + @Nullable + private static Allocation solve(List supplies, List demands, int resultSize) { + if (demands == null) return null; + if (demands.isEmpty()) return new Allocation(new long[resultSize]); + if (supplies.isEmpty()) return null; + long totalDemand = 0L; + for (Demand demand : demands) { + try { totalDemand = Math.addExact(totalDemand,demand.amount()); } + catch(ArithmeticException overflow) { return null; } + } + long totalSupply = 0L; + for (Supply supply : supplies) totalSupply = saturatingAdd(totalSupply, supply.amount()); + if (totalSupply < totalDemand) return null; + + // A single demand has no competing allocation edges. Handle it directly; tag and + // component predicates remain fully respected by the same ingredient test. + if (demands.size() == 1) { + return solveSingleDemand(supplies, demands.get(0), resultSize); + } + + int source = 0; + int supplyStart = 1; + int demandStart = supplyStart + supplies.size(); + int sink = demandStart + demands.size(); + FlowNetwork network = new FlowNetwork(sink + 1); + List supplyEdges = new ArrayList<>(supplies.size()); + for (int i = 0; i < supplies.size(); i++) { + supplyEdges.add(network.addEdge(source, supplyStart + i, supplies.get(i).amount())); + } + for (int i = 0; i < supplies.size(); i++) { + Supply supply = supplies.get(i); + for (int j = 0; j < demands.size(); j++) { + com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient ingredient = demands.get(j).ingredient(); + if (ingredient != null && !ingredient.isEmpty() && ingredient.test(supply.stack())) { + network.addEdge(supplyStart + i, demandStart + j, totalDemand); + } + } + } + for (int i = 0; i < demands.size(); i++) { + network.addEdge(demandStart + i, sink, demands.get(i).amount()); + } + if (network.maxFlow(source, sink, totalDemand) != totalDemand) return null; + + long[] consumed = new long[resultSize]; + for (int i = 0; i < supplies.size(); i++) { + Supply supply = supplies.get(i); + consumed[supply.originalIndex()] = supply.amount() - supplyEdges.get(i).capacity; + } + return new Allocation(consumed); + } + + @Nullable + private static Allocation solveSingleDemand(List supplies, Demand demand, int resultSize) { + long remaining = demand.amount(); + long[] consumed = new long[resultSize]; + for (Supply supply : supplies) { + if (remaining <= 0L || !demand.ingredient().test(supply.stack())) continue; + long amount = Math.min(remaining, supply.amount()); + consumed[supply.originalIndex()] = amount; + remaining -= amount; + } + return remaining > 0L ? null : new Allocation(consumed); + } + + private static long saturatingAdd(long left, long right) { + if (right > 0 && left > Long.MAX_VALUE - right) return Long.MAX_VALUE; + return left + right; + } + + private static List toLongRequirements( + List requirements) { + if (requirements == null || requirements.isEmpty()) return List.of(); + List result = new ArrayList<>(requirements.size()); + for (SizedFluidIngredient requirement : requirements) { + if (requirement != null && requirement.amount() > 0) { + result.add(LongSizedFluidIngredient.from(requirement)); + } + } + return result; + } + + private static long saturatingMultiply(long left, long right) { + if (left <= 0 || right <= 0) return 0; + return left > Long.MAX_VALUE / right ? Long.MAX_VALUE : left * right; + } + + public static final class Allocation { + private final long[] consumedBySupply; + + private Allocation(long[] consumedBySupply) { + this.consumedBySupply = consumedBySupply; + } + + public int supplyCount() { + return consumedBySupply.length; + } + + public long consumedFromSupply(int index) { + return consumedBySupply[index]; + } + } + + private record Supply(int originalIndex, long amount, FluidStack stack) { + } + + private record Demand(com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient ingredient, long amount) { + } + + private static final class FlowNetwork { + private final List> graph; + private final int[] levels; + private final int[] nextEdges; + + private FlowNetwork(int nodeCount) { + graph = new ArrayList<>(nodeCount); + for (int i = 0; i < nodeCount; i++) graph.add(new ArrayList<>()); + levels = new int[nodeCount]; + nextEdges = new int[nodeCount]; + } + + private FlowEdge addEdge(int from, int to, long capacity) { + FlowEdge forward = new FlowEdge(to, graph.get(to).size(), capacity); + FlowEdge reverse = new FlowEdge(from, graph.get(from).size(), 0L); + graph.get(from).add(forward); + graph.get(to).add(reverse); + return forward; + } + + private long maxFlow(int source, int sink, long limit) { + long flow = 0L; + while (flow < limit && buildLevels(source, sink)) { + Arrays.fill(nextEdges, 0); + long pushed; + while (flow < limit && (pushed = push(source, sink, limit - flow)) > 0) flow += pushed; + } + return flow; + } + + private boolean buildLevels(int source, int sink) { + Arrays.fill(levels, -1); + levels[source] = 0; + ArrayDeque queue = new ArrayDeque<>(); + queue.add(source); + while (!queue.isEmpty()) { + int node = queue.removeFirst(); + for (FlowEdge edge : graph.get(node)) { + if (edge.capacity > 0 && levels[edge.to] < 0) { + levels[edge.to] = levels[node] + 1; + queue.addLast(edge.to); + } + } + } + return levels[sink] >= 0; + } + + private long push(int node, int sink, long amount) { + if (node == sink) return amount; + List edges = graph.get(node); + for (; nextEdges[node] < edges.size(); nextEdges[node]++) { + FlowEdge edge = edges.get(nextEdges[node]); + if (edge.capacity <= 0 || levels[edge.to] != levels[node] + 1) continue; + long pushed = push(edge.to, sink, Math.min(amount, edge.capacity)); + if (pushed <= 0) continue; + edge.capacity -= pushed; + graph.get(edge.to).get(edge.reverseIndex).capacity += pushed; + return pushed; + } + return 0L; + } + } + + private static final class FlowEdge { + private final int to; + private final int reverseIndex; + private long capacity; + + private FlowEdge(int to, int reverseIndex, long capacity) { + this.to = to; + this.reverseIndex = reverseIndex; + this.capacity = capacity; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/IRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/IRecipeAdapter.java new file mode 100644 index 00000000..b189bf1f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/IRecipeAdapter.java @@ -0,0 +1,16 @@ +package com.sorrowmist.useless.content.recipe; + +import net.minecraft.world.item.crafting.Recipe; + +/** + * @deprecated Implement {@link com.sorrowmist.useless.api.recipe.IRecipeAdapter} instead. This + * bridge keeps existing built-in and third-party adapters source-compatible. + */ +@Deprecated(forRemoval = false) +public interface IRecipeAdapter> + extends com.sorrowmist.useless.api.recipe.IRecipeAdapter { + @Override + default String sourceId() { + return RecipeSourceIds.fromAdapterClass(getClass()); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/ItemIngredientAllocator.java b/src/main/java/com/sorrowmist/useless/content/recipe/ItemIngredientAllocator.java new file mode 100644 index 00000000..6de52c2e --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/ItemIngredientAllocator.java @@ -0,0 +1,422 @@ +package com.sorrowmist.useless.content.recipe; + +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.GenericStack; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; + +/** + * Count-aware allocator for item ingredient requirements. + *

+ * A single input amount can only satisfy one requirement. This is important for + * overlapping tags and component-sensitive custom ingredients, where independent + * per-ingredient counting can otherwise use the same stack more than once. + */ +public final class ItemIngredientAllocator { + private ItemIngredientAllocator() { + } + + public static boolean matches(List requirements, List inputs, long operations) { + return allocate(requirements, inputs, operations) != null; + } + + /** Matches item supplies stored as AE keys without narrowing their long amounts to stack counts. */ + public static boolean matches(List requirements, List inputs, + List keyInputs, long operations) { + List safeInputs = inputs == null ? List.of() : inputs; + List safeKeyInputs = keyInputs == null ? List.of() : keyInputs; + List demands = createDemands(requirements, operations); + if (demands == null) return false; + if (demands.isEmpty()) return true; + + List supplies = new ArrayList<>(); + for (int i = 0; i < safeInputs.size(); i++) { + ItemStack stack = safeInputs.get(i); + if (stack == null || stack.isEmpty() || stack.getCount() <= 0) continue; + supplies.add(new Supply(i, stack.getCount(), + ingredient -> ingredient != null && ingredient.test(stack), true)); + } + for (int i = 0; i < safeKeyInputs.size(); i++) { + GenericStack input = safeKeyInputs.get(i); + if (input == null || input.amount() <= 0L || !(input.what() instanceof AEItemKey itemKey)) continue; + ItemStack representative = itemKey.toStack(1); + supplies.add(new Supply(safeInputs.size() + i, input.amount(), + ingredient -> ingredient != null && ingredient.test(representative), true)); + } + + return solve(supplies, demands, + safeInputs.size() + safeKeyInputs.size()) != null; + } + + /** + * Allocates concrete input stacks to all requirements. + * + * @return the amount consumed from each input list entry, or {@code null} when no valid allocation exists + */ + @Nullable + public static Allocation allocate(List requirements, List inputs, long operations) { + List safeInputs = inputs == null ? List.of() : inputs; + List demands = createDemands(requirements, operations); + if (demands == null) return null; + if (demands.isEmpty()) return new Allocation(new long[safeInputs.size()]); + + List supplies = new ArrayList<>(); + for (int i = 0; i < safeInputs.size(); i++) { + ItemStack stack = safeInputs.get(i); + if (stack == null || stack.isEmpty() || stack.getCount() <= 0) continue; + supplies.add(new Supply(i, stack.getCount(), + ingredient -> ingredient != null && ingredient.test(stack), true)); + } + + return solve(supplies, demands, safeInputs.size()); + } + + /** Matches adapter input maps without flattening overlapping ingredient requirements. */ + public static boolean matches(Map available, Map required) { + List demands = new ArrayList<>(); + if (required != null) { + for (Map.Entry entry : required.entrySet()) { + long amount = entry.getValue() == null ? 0L : entry.getValue(); + if (amount > 0) { + demands.add(new Demand(entry.getKey(), amount)); + } + } + } + if (demands == null) return false; + if (demands.isEmpty()) return true; + + List supplies = new ArrayList<>(); + if (available != null) { + int index = 0; + for (Map.Entry entry : available.entrySet()) { + Ingredient input = entry.getKey(); + long amount = entry.getValue() == null ? 0L : entry.getValue(); + if (input != null && amount > 0) { + supplies.add(new Supply(index, amount, + requirement -> ingredientCanSupply(input, requirement), + isExactIngredient(input))); + } + index++; + } + } + + return solve(supplies, demands, available == null ? 0 : available.size()) != null; + } + + /** Returns the largest whole-operation count supported by the supplied item stacks. */ + public static int maxOperations(List requirements, List inputs) { + if (requirements != null && requirements.size() == 1) { + CountedIngredient requirement = requirements.get(0); + if (requirement == null || requirement.count() <= 0) return Integer.MAX_VALUE; + if (requirement.ingredient() == null || requirement.ingredient().isEmpty()) return 0; + long available = 0L; + if (inputs != null) { + for (ItemStack stack : inputs) { + if (stack != null && !stack.isEmpty() && stack.getCount() > 0 + && requirement.ingredient().test(stack)) { + available = saturatingAdd(available, stack.getCount()); + } + } + } + long operations = available / requirement.count(); + return operations >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) operations; + } + long demandPerOperation = 0L; + if (requirements != null) { + for (CountedIngredient requirement : requirements) { + if (requirement != null && requirement.count() > 0) { + demandPerOperation = saturatingAdd(demandPerOperation, requirement.count()); + } + } + } + if (demandPerOperation == 0) return Integer.MAX_VALUE; + + long totalAvailable = 0L; + if (inputs != null) { + for (ItemStack stack : inputs) { + if (stack != null && !stack.isEmpty() && stack.getCount() > 0) { + totalAvailable = saturatingAdd(totalAvailable, stack.getCount()); + } + } + } + + long upperLong = totalAvailable / demandPerOperation; + int low = 0; + int high = upperLong > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) upperLong; + while (low < high) { + int middle = low + (int) (((long) high - low + 1L) / 2L); + if (matches(requirements, inputs, middle)) { + low = middle; + } else { + high = middle - 1; + } + } + return low; + } + + private static boolean ingredientCanSupply(Ingredient input, Ingredient requirement) { + if (requirement == null) return false; + if (input == requirement || input.equals(requirement)) return true; + for (ItemStack representative : input.getItems()) { + if (!representative.isEmpty() && requirement.test(representative)) { + return true; + } + } + return false; + } + + private static List createDemands(List requirements, long operations) { + List demands = new ArrayList<>(); + long multiplier = Math.max(0L, operations); + if (requirements == null) return demands; + + for (CountedIngredient requirement : requirements) { + if (requirement == null || requirement.count() <= 0) continue; + long amount; + try { amount = Math.multiplyExact(requirement.count(),multiplier); } + catch(ArithmeticException overflow) { return null; } + if (amount > 0) { + demands.add(new Demand(requirement.ingredient(), amount)); + } + } + return demands; + } + + @Nullable + private static Allocation solve(List supplies, List demands, int resultSize) { + if (demands == null) return null; + if (demands.isEmpty()) return new Allocation(new long[resultSize]); + if (supplies.isEmpty()) return null; + + long totalDemand = 0L; + for (Demand demand : demands) { + try { totalDemand = Math.addExact(totalDemand,demand.amount()); } + catch(ArithmeticException overflow) { return null; } + } + long totalSupply = 0L; + for (Supply supply : supplies) { + totalSupply = saturatingAdd(totalSupply, supply.amount()); + } + if (totalSupply < totalDemand) return null; + + // With one requirement there are no competing allocation edges, regardless of whether + // the ingredient is a tag or a custom predicate. + if (demands.size() == 1) { + return solveSingleDemand(supplies, demands.get(0), resultSize); + } + + // Most machine recipes use concrete item ingredients. Their matching relation is + // disjoint, so building a flow network would only add allocation overhead. Tags, + // overlapping ingredients and custom predicates continue through the general path. + if (isExactProblem(supplies, demands)) { + return solveExact(supplies, demands, resultSize); + } + + int source = 0; + int supplyStart = 1; + int demandStart = supplyStart + supplies.size(); + int sink = demandStart + demands.size(); + FlowNetwork network = new FlowNetwork(sink + 1); + List supplyEdges = new ArrayList<>(supplies.size()); + + for (int i = 0; i < supplies.size(); i++) { + supplyEdges.add(network.addEdge(source, supplyStart + i, supplies.get(i).amount())); + } + for (int i = 0; i < supplies.size(); i++) { + Supply supply = supplies.get(i); + for (int j = 0; j < demands.size(); j++) { + if (supply.matches().test(demands.get(j).ingredient())) { + network.addEdge(supplyStart + i, demandStart + j, totalDemand); + } + } + } + for (int i = 0; i < demands.size(); i++) { + network.addEdge(demandStart + i, sink, demands.get(i).amount()); + } + + if (network.maxFlow(source, sink, totalDemand) != totalDemand) return null; + + long[] consumed = new long[resultSize]; + for (int i = 0; i < supplies.size(); i++) { + Supply supply = supplies.get(i); + consumed[supply.originalIndex()] = supply.amount() - supplyEdges.get(i).capacity; + } + return new Allocation(consumed); + } + + @Nullable + private static Allocation solveSingleDemand(List supplies, Demand demand, int resultSize) { + long remaining = demand.amount(); + long[] consumed = new long[resultSize]; + for (Supply supply : supplies) { + if (remaining <= 0L || !supply.matches().test(demand.ingredient())) continue; + long amount = Math.min(remaining, supply.amount()); + int originalIndex = supply.originalIndex(); + consumed[originalIndex] = saturatingAdd(consumed[originalIndex], amount); + remaining -= amount; + } + return remaining > 0L ? null : new Allocation(consumed); + } + + private static boolean isExactProblem(List supplies, List demands) { + for (Supply supply : supplies) { + if (!supply.exact()) return false; + } + for (Demand demand : demands) { + if (!isExactIngredient(demand.ingredient())) return false; + } + return true; + } + + /** Greedy allocation is valid when every requirement and supply denotes one exact stack. */ + @Nullable + private static Allocation solveExact(List supplies, List demands, int resultSize) { + long[] remaining = new long[supplies.size()]; + for (int i = 0; i < supplies.size(); i++) { + remaining[i] = supplies.get(i).amount(); + } + + long[] consumed = new long[resultSize]; + for (Demand demand : demands) { + long needed = demand.amount(); + for (int i = 0; i < supplies.size() && needed > 0L; i++) { + Supply supply = supplies.get(i); + if (remaining[i] <= 0L || !supply.matches().test(demand.ingredient())) { + continue; + } + long amount = Math.min(needed, remaining[i]); + remaining[i] -= amount; + needed -= amount; + int originalIndex = supply.originalIndex(); + consumed[originalIndex] = saturatingAdd(consumed[originalIndex], amount); + } + if (needed > 0L) return null; + } + return new Allocation(consumed); + } + + private static boolean isExactIngredient(@Nullable Ingredient ingredient) { + try { return com.sorrowmist.useless.compat.forge1201.recipe.NativeIngredients.isSingleItem(ingredient); } + catch(RuntimeException exception) { return false; } + } + + private static long saturatingAdd(long a, long b) { + if (b > 0 && a > Long.MAX_VALUE - b) return Long.MAX_VALUE; + return a + b; + } + + private static long saturatingMultiply(long amount, long multiplier) { + if (amount <= 0 || multiplier <= 0) return 0; + if (amount > Long.MAX_VALUE / multiplier) return Long.MAX_VALUE; + return amount * multiplier; + } + + public static final class Allocation { + private final long[] consumedByInput; + + private Allocation(long[] consumedByInput) { + this.consumedByInput = consumedByInput; + } + + public int inputCount() { + return this.consumedByInput.length; + } + + public long consumedFromInput(int inputIndex) { + return this.consumedByInput[inputIndex]; + } + } + + private record Supply(int originalIndex, long amount, Predicate matches, boolean exact) { + } + + private record Demand(Ingredient ingredient, long amount) { + } + + private static final class FlowNetwork { + private final List> graph; + private final int[] levels; + private final int[] nextEdges; + + private FlowNetwork(int nodeCount) { + this.graph = new ArrayList<>(nodeCount); + for (int i = 0; i < nodeCount; i++) { + this.graph.add(new ArrayList<>()); + } + this.levels = new int[nodeCount]; + this.nextEdges = new int[nodeCount]; + } + + private FlowEdge addEdge(int from, int to, long capacity) { + FlowEdge forward = new FlowEdge(to, this.graph.get(to).size(), capacity); + FlowEdge reverse = new FlowEdge(from, this.graph.get(from).size(), 0L); + this.graph.get(from).add(forward); + this.graph.get(to).add(reverse); + return forward; + } + + private long maxFlow(int source, int sink, long limit) { + long flow = 0L; + while (flow < limit && this.buildLevels(source, sink)) { + Arrays.fill(this.nextEdges, 0); + long pushed; + while (flow < limit && (pushed = this.push(source, sink, limit - flow)) > 0) { + flow += pushed; + } + } + return flow; + } + + private boolean buildLevels(int source, int sink) { + Arrays.fill(this.levels, -1); + this.levels[source] = 0; + ArrayDeque queue = new ArrayDeque<>(); + queue.add(source); + while (!queue.isEmpty()) { + int node = queue.removeFirst(); + for (FlowEdge edge : this.graph.get(node)) { + if (edge.capacity > 0 && this.levels[edge.to] < 0) { + this.levels[edge.to] = this.levels[node] + 1; + queue.addLast(edge.to); + } + } + } + return this.levels[sink] >= 0; + } + + private long push(int node, int sink, long amount) { + if (node == sink) return amount; + List edges = this.graph.get(node); + for (; this.nextEdges[node] < edges.size(); this.nextEdges[node]++) { + FlowEdge edge = edges.get(this.nextEdges[node]); + if (edge.capacity <= 0 || this.levels[edge.to] != this.levels[node] + 1) continue; + long pushed = this.push(edge.to, sink, Math.min(amount, edge.capacity)); + if (pushed <= 0) continue; + edge.capacity -= pushed; + this.graph.get(edge.to).get(edge.reverseIndex).capacity += pushed; + return pushed; + } + return 0L; + } + } + + private static final class FlowEdge { + private final int to; + private final int reverseIndex; + private long capacity; + + private FlowEdge(int to, int reverseIndex, long capacity) { + this.to = to; + this.reverseIndex = reverseIndex; + this.capacity = capacity; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/LongSizedFluidIngredient.java b/src/main/java/com/sorrowmist/useless/content/recipe/LongSizedFluidIngredient.java new file mode 100644 index 00000000..8df07192 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/LongSizedFluidIngredient.java @@ -0,0 +1,75 @@ +package com.sorrowmist.useless.content.recipe; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.ByteBufCodecs; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; + +import java.util.Arrays; +import java.util.Objects; + +/** A fluid ingredient whose required amount is not limited to FluidStack's int count. */ +public record LongSizedFluidIngredient(FluidIngredient ingredient, long amount) { + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group( + FluidIngredient.CODEC.fieldOf("ingredient").forGetter(LongSizedFluidIngredient::ingredient), + Codec.LONG.fieldOf("amount").forGetter(LongSizedFluidIngredient::amount) + ).apply(instance, LongSizedFluidIngredient::new)); + + public static final StreamCodec STREAM_CODEC = + StreamCodec.composite( + FluidIngredient.STREAM_CODEC, LongSizedFluidIngredient::ingredient, + ByteBufCodecs.VAR_LONG, LongSizedFluidIngredient::amount, + LongSizedFluidIngredient::new + ); + + public LongSizedFluidIngredient { + Objects.requireNonNull(ingredient, "ingredient"); + if (amount <= 0L) { + throw new IllegalArgumentException("Fluid ingredient amount must be positive"); + } + } + + public static LongSizedFluidIngredient from(SizedFluidIngredient ingredient) { + Objects.requireNonNull(ingredient, "ingredient"); + return new LongSizedFluidIngredient(ingredient.ingredient(), ingredient.amount()); + } + + public static LongSizedFluidIngredient from(FluidStack stack) { + return new LongSizedFluidIngredient( + SizedFluidIngredient.of(Objects.requireNonNull(stack, "stack")).ingredient(), + stack.getAmount()); + } + + /** Returns fluid candidates using a representable display amount. */ + public FluidStack[] getFluids() { + int displayAmount = (int) Math.min(amount, Integer.MAX_VALUE); + return Arrays.stream(ingredient.getStacks()) + .filter(stack -> stack != null && !stack.isEmpty()) + .map(stack -> new net.minecraftforge.fluids.FluidStack(stack, displayAmount)) + .toArray(FluidStack[]::new); + } + + /** Returns unit representatives for predicates, tags, components, and AE key creation. */ + public FluidStack[] getRepresentatives() { + return Arrays.stream(ingredient.getStacks()) + .filter(stack -> stack != null && !stack.isEmpty()) + .map(stack -> new net.minecraftforge.fluids.FluidStack(stack, 1)) + .toArray(FluidStack[]::new); + } + + public boolean fitsInt() { + return amount <= Integer.MAX_VALUE; + } + + public SizedFluidIngredient toSizedFluidIngredient() { + if (!fitsInt()) { + throw new IllegalStateException("Fluid ingredient amount exceeds FluidStack's int range"); + } + return new SizedFluidIngredient(ingredient, (int) amount); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/MoldMatcher.java b/src/main/java/com/sorrowmist/useless/content/recipe/MoldMatcher.java new file mode 100644 index 00000000..267ecfb4 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/MoldMatcher.java @@ -0,0 +1,227 @@ +package com.sorrowmist.useless.content.recipe; + +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.crafting.Ingredient; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** Matches independent mold requirements against the mold slots of a hub. */ +public final class MoldMatcher { + private MoldMatcher() { + } + + public static boolean matches(List requirements, List available) { + return matches(requirements, sparseSlots(available)); + } + + /** Matches against a sparse, physical-slot-indexed mold map. */ + public static boolean matches( + List requirements, Map available) { + return prepare(available).matches(requirements); + } + + /** + * Builds a reusable matcher for one immutable mold-inventory snapshot. The prepared index is + * useful when many patterns are checked against the same hub: concrete ingredients only visit + * slots containing one of their possible items, and each ingredient's matching slots are + * cached for the lifetime of this matcher. + */ + public static PreparedMolds prepare(Map available) { + return new PreparedMolds(available); + } + + public static final class PreparedMolds { + private final Map available; + private final List occupiedSlots; + private final Map> slotsByItem; + private final Map> matchingSlotsCache = new HashMap<>(); + + private PreparedMolds(Map available) { + Map normalized = new LinkedHashMap<>(); + if (available != null) { + for (Map.Entry entry : available.entrySet()) { + Integer slot = entry.getKey(); + ItemStack mold = entry.getValue(); + if (slot != null && slot >= 0 && mold != null && !mold.isEmpty()) { + normalized.put(slot, mold.copy()); + } + } + } + this.available = Collections.unmodifiableMap(normalized); + this.occupiedSlots = List.copyOf(normalized.keySet()); + + Map> indexed = new HashMap<>(); + for (Map.Entry entry : normalized.entrySet()) { + indexed.computeIfAbsent(entry.getValue().getItem(), ignored -> new ArrayList<>()) + .add(entry.getKey()); + } + indexed.replaceAll((ignored, slots) -> List.copyOf(slots)); + this.slotsByItem = Map.copyOf(indexed); + } + + public boolean matches(List requirements) { + List normalized = normalizeRequirements(requirements); + if (normalized.isEmpty()) return true; + if (occupiedSlots.isEmpty() || normalized.size() > occupiedSlots.size()) return false; + + Map requirementBySlot = new HashMap<>(); + for (int requirement = 0; requirement < normalized.size(); requirement++) { + if (!augment(normalized, requirement, requirementBySlot, new HashSet<>())) { + return false; + } + } + return true; + } + + private boolean augment( + List requirements, int requirement, + Map requirementBySlot, Set visited) { + for (int slot : matchingSlots(requirements.get(requirement))) { + if (!visited.add(slot)) continue; + int previous = requirementBySlot.getOrDefault(slot, -1); + if (previous < 0 || augment(requirements, previous, requirementBySlot, visited)) { + requirementBySlot.put(slot, requirement); + return true; + } + } + return false; + } + + private List matchingSlots(Ingredient requirement) { + if (requirement == null || requirement.isEmpty()) return List.of(); + return matchingSlotsCache.computeIfAbsent(requirement, this::findMatchingSlots); + } + + private List findMatchingSlots(Ingredient requirement) { + // Custom ingredients can accept items that are not present in getItems(), so their + // predicate must retain the complete sparse-slot scan for correctness. + if (!requirement.isVanilla()) return scanAll(requirement); + + ItemStack[] representatives; + try { + representatives = requirement.getItems(); + } catch (RuntimeException ignored) { + return scanAll(requirement); + } + if (representatives == null || representatives.length == 0) { + return scanAll(requirement); + } + + Set possibleSlots = new LinkedHashSet<>(); + for (ItemStack representative : representatives) { + if (representative == null || representative.isEmpty()) continue; + possibleSlots.addAll(slotsByItem.getOrDefault( + representative.getItem(), List.of())); + } + + List result = new ArrayList<>(); + for (int slot : possibleSlots) { + ItemStack mold = available.get(slot); + if (AdapterUtils.matchesMold(requirement, mold)) result.add(slot); + } + return List.copyOf(result); + } + + private List scanAll(Ingredient requirement) { + List result = new ArrayList<>(); + for (int slot : occupiedSlots) { + if (AdapterUtils.matchesMold(requirement, available.get(slot))) result.add(slot); + } + return List.copyOf(result); + } + } + + /** + * Returns true when a complete assignment exists that uses {@code availableSlot} for one of + * the requirements. This is what makes the hub's physical slot order meaningful when several + * recipes have identical processing inputs and outputs. + */ + public static boolean matchesWithAnchor( + List requirements, List available, int availableSlot) { + return matchesWithAnchor(requirements, sparseSlots(available), availableSlot); + } + + /** + * Matches while reserving one physical mold slot for a requirement. The map is sparse, so empty + * slots never enter the assignment search. + */ + public static boolean matchesWithAnchor( + List requirements, Map available, int availableSlot) { + List normalized = normalizeRequirements(requirements); + if (normalized.isEmpty()) return false; + if (available == null || availableSlot < 0 || !available.containsKey(availableSlot) + || available.get(availableSlot) == null || available.get(availableSlot).isEmpty() + || normalized.size() > available.size()) { + return false; + } + + for (int anchoredRequirement = 0; anchoredRequirement < normalized.size(); anchoredRequirement++) { + if (!AdapterUtils.matchesMold(normalized.get(anchoredRequirement), available.get(availableSlot))) { + continue; + } + + Map requirementBySlot = new HashMap<>(); + requirementBySlot.put(availableSlot, anchoredRequirement); + boolean complete = true; + for (int requirement = 0; requirement < normalized.size(); requirement++) { + if (requirement == anchoredRequirement) continue; + Set visited = new HashSet<>(); + visited.add(availableSlot); + if (!augment(normalized, available, requirement, requirementBySlot, visited)) { + complete = false; + break; + } + } + if (complete) return true; + } + return false; + } + + private static boolean augment( + List requirements, Map available, + int requirement, Map requirementBySlot, Set visited) { + Ingredient needed = requirements.get(requirement); + for (Map.Entry entry : available.entrySet()) { + Integer slot = entry.getKey(); + ItemStack mold = entry.getValue(); + if (slot == null || visited.contains(slot) || mold == null || mold.isEmpty() + || !AdapterUtils.matchesMold(needed, mold)) continue; + visited.add(slot); + int previous = requirementBySlot.getOrDefault(slot, -1); + if (previous < 0 || augment(requirements, available, previous, requirementBySlot, visited)) { + requirementBySlot.put(slot, requirement); + return true; + } + } + return false; + } + + /** Creates an insertion-ordered map containing only non-empty physical mold slots. */ + public static Map sparseSlots(List available) { + if (available == null || available.isEmpty()) return Map.of(); + Map result = new LinkedHashMap<>(); + for (int slot = 0; slot < available.size(); slot++) { + ItemStack mold = available.get(slot); + if (mold != null && !mold.isEmpty()) result.put(slot, mold); + } + return Collections.unmodifiableMap(result); + } + + public static List normalizeRequirements(List requirements) { + if (requirements == null || requirements.isEmpty()) return List.of(); + List normalized = new ArrayList<>(requirements.size()); + for (Ingredient requirement : requirements) { + if (requirement != null && !requirement.isEmpty()) normalized.add(requirement); + } + return List.copyOf(normalized); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/RecipeConversionUtils.java b/src/main/java/com/sorrowmist/useless/content/recipe/RecipeConversionUtils.java new file mode 100644 index 00000000..12477857 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/RecipeConversionUtils.java @@ -0,0 +1,67 @@ +package com.sorrowmist.useless.content.recipe; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.mojang.logging.LogUtils; +import net.minecraft.world.item.ItemStack; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import org.slf4j.Logger; + +import java.util.List; + +/** Isolates failures from individual external-recipe conversions. */ +public final class RecipeConversionUtils { + private static final Logger LOGGER = LogUtils.getLogger(); + + private RecipeConversionUtils() { + } + + public static List convertAll( + com.sorrowmist.useless.api.recipe.IRecipeAdapter adapter, RecipeHolder holder, Level level) { + return convert(adapter, holder, level, null, false); + } + + /** @deprecated Use the public API adapter type. */ + @Deprecated(forRemoval = false) + public static List convertAll( + IRecipeAdapter adapter, RecipeHolder holder, Level level) { + return convertAll((com.sorrowmist.useless.api.recipe.IRecipeAdapter) adapter, holder, level); + } + + public static List convertAll( + com.sorrowmist.useless.api.recipe.IRecipeAdapter adapter, RecipeHolder holder, Level level, + List actualInputs) { + return convert(adapter, holder, level, actualInputs, true); + } + + /** @deprecated Use the public API adapter type. */ + @Deprecated(forRemoval = false) + public static List convertAll( + IRecipeAdapter adapter, RecipeHolder holder, Level level, + List actualInputs) { + return convertAll((com.sorrowmist.useless.api.recipe.IRecipeAdapter) adapter, + holder, level, actualInputs); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private static List convert( + com.sorrowmist.useless.api.recipe.IRecipeAdapter adapter, RecipeHolder holder, Level level, + List actualInputs, boolean useActualInputs) { + if (adapter == null || holder == null) { + return List.of(); + } + + try { + com.sorrowmist.useless.api.recipe.IRecipeAdapter rawAdapter = adapter; + List converted = useActualInputs + ? rawAdapter.convertAll((RecipeHolder) holder, level, actualInputs) + : rawAdapter.convertAll((RecipeHolder) holder, level); + return converted == null ? List.of() : converted; + } catch (RuntimeException exception) { + RateLimitedLogger.warn(LOGGER, "Skipping failed recipe conversion: adapter={}, recipe={}", + adapter.getClass().getName(), holder.id(), exception); + return List.of(); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/RecipeOutputConstraint.java b/src/main/java/com/sorrowmist/useless/content/recipe/RecipeOutputConstraint.java new file mode 100644 index 00000000..a8b05ad3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/RecipeOutputConstraint.java @@ -0,0 +1,64 @@ +package com.sorrowmist.useless.content.recipe; + +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Describes how an AE pattern output identifies a converted furnace recipe. + * Amounts are intentionally excluded because smart-doubling scaling is represented by the + * operation count rather than by a different recipe identity. + */ +public record RecipeOutputConstraint(AEKey key, MatchMode matchMode) { + + public RecipeOutputConstraint { + Objects.requireNonNull(key, "key"); + Objects.requireNonNull(matchMode, "matchMode"); + if (matchMode == MatchMode.ITEM_ID && !(key instanceof AEItemKey)) { + matchMode = MatchMode.EXACT; + } + } + + public static RecipeOutputConstraint exact(AEKey key) { + return new RecipeOutputConstraint(key, MatchMode.EXACT); + } + + public static RecipeOutputConstraint itemId(AEKey key) { + return new RecipeOutputConstraint(key, MatchMode.ITEM_ID); + } + + public static List exact(List stacks) { + if (stacks == null || stacks.isEmpty()) { + return List.of(); + } + + List result = new ArrayList<>(stacks.size()); + for (GenericStack stack : stacks) { + if (stack != null && stack.what() != null) { + result.add(exact(stack.what())); + } + } + return List.copyOf(result); + } + + public boolean matches(AEKey candidate) { + if (candidate == null) { + return false; + } + if (matchMode == MatchMode.EXACT) { + return key.equals(candidate); + } + return key instanceof AEItemKey expectedItem + && candidate instanceof AEItemKey candidateItem + && expectedItem.getItem() == candidateItem.getItem(); + } + + public enum MatchMode { + EXACT, + ITEM_ID + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/RecipeSourceIds.java b/src/main/java/com/sorrowmist/useless/content/recipe/RecipeSourceIds.java new file mode 100644 index 00000000..54de209e --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/RecipeSourceIds.java @@ -0,0 +1,138 @@ +package com.sorrowmist.useless.content.recipe; + +/** Stable identifiers used to partition the shared alloy-furnace recipe directory. */ +public final class RecipeSourceIds { + public static final String CORE = "core"; + public static final String UNKNOWN = "compatibility"; + public static final String MINECRAFT = "minecraft"; + /** 原版钓鱼战利品表转换来源。 */ + public static final String MINECRAFT_FISHING = "minecraft_fishing"; + public static final String FARMERS_DELIGHT = "farmersdelight"; + public static final String EXTRA_DELIGHT = "extradelight"; + public static final String CRABBERS_DELIGHT = "crabbersdelight"; + public static final String CASUALNESS_DELIGHT = "casualnessdelight"; + public static final String EXPANDED_DELIGHT = "expandeddelight"; + public static final String BREWIN_AND_CHEWIN = "brewinandchewin"; + public static final String NOMADS_DELIGHT = "nomads_delight"; + public static final String UBES_DELIGHT = "ubesdelight"; + public static final String BARBEQUES_DELIGHT = "barbequesdelight"; + public static final String YOUKAIS_FEASTS = "youkaisfeasts"; + public static final String YOUKAI_HOMECOMING = "youkaishomecoming"; + public static final String KALEIDOSCOPE_COOKERY = "kaleidoscope_cookery"; + public static final String KALEIDOSCOPE_GRILLING = "kaleidoscope_grilling"; + public static final String KALEIDOSCOPE_TAVERN = "kaleidoscope_tavern"; + public static final String EXTENDED_AE = "expatternprovider"; + public static final String ADVANCED_AE = "advanced_ae"; + public static final String MEKANISM = "mekanism"; + public static final String MEKANISM_GENERATORS = "mekanismgenerators"; + public static final String APP_MEK = "appmek"; + public static final String AE2 = "ae2"; + public static final String AE2CS = "ae2cs"; + public static final String INDUSTRIAL_FOREGOING = "industrialforegoing"; + public static final String ACTUALLY_ADDITIONS = "actuallyadditions"; + public static final String ARS_NOUVEAU = "ars_nouveau"; + public static final String MYSTICAL_AGRICULTURE = "mysticalagriculture"; + public static final String AE2LT = "ae2lt"; + public static final String DATA_ENERGISTICS = "data_energistics"; + public static final String PRODUCTIVE_BEES = "productivebees"; + public static final String DRACONIC_EVOLUTION = "draconicevolution"; + public static final String POWAH = "powah"; + public static final String EXTENDED_CRAFTING = "extendedcrafting"; + public static final String AVARITIA = "avaritia"; + public static final String NEO_ECO_AE = "neoecoae"; + public static final String NATURES_AURA = "naturesaura"; + public static final String FORBIDDEN_ARCANUS = "forbidden_arcanus"; + public static final String OCCULTISM = "occultism"; + public static final String SUMMONING_RITUALS = "summoningrituals"; + public static final String MALUM = "malum"; + public static final String ENDER_IO = "enderio"; + public static final String CREATE = "create"; + public static final String ORITECH = "oritech"; + public static final String NEOVITAE = "neovitae"; + public static final String UFO = "ufo"; + public static final String MODERN_INDUSTRIALIZATION = "modern_industrialization"; + public static final String IMMERSIVE_ENGINEERING = "immersiveengineering"; + public static final String PNEUMATICCRAFT = "pneumaticcraft"; + public static final String BIG_REACTORS = "bigreactors"; + public static final String JUSTDIRETHINGS = "justdirethings"; + public static final String JDTE = "jdte"; + public static final String IRONS_SPELLBOOKS = "irons_spellbooks"; + public static final String HOSTILE_NETWORKS = "hostilenetworks"; + public static final String APOTHIC_FLUX = "apothic_flux"; + /** Flux Networks 硬编码的通量粉尘转化机制。 */ + public static final String FLUX_NETWORKS = "fluxnetworks"; + /** 神化铁砧砸碎宝石的硬编码机制。 */ + public static final String APOTHEOSIS = "apotheosis"; + /** Lychee 数据驱动的方块压碎配方。 */ + public static final String LYCHEE = "lychee"; + /** Astral Sorcery 的星能注入与聚星缸配方。 */ + public static final String ASTRAL_SORCERY = "astralsorcery"; + /** Botany Pots 的作物生长配方。 */ + public static final String BOTANY_POTS = "botanypots"; + /** Ex Deorum 的桶堆肥、桶混合、流体转化与坩埚熔化配方。 */ + public static final String EX_DEORUM = "exdeorum"; + + private RecipeSourceIds() { + } + + public static String normalize(String sourceId) { + if (sourceId == null || sourceId.isBlank()) return UNKNOWN; + return sourceId.trim().toLowerCase(java.util.Locale.ROOT); + } + + /** Best-effort stable source for adapters that do not explicitly provide one. */ + public static String fromAdapterClass(Class adapterClass) { + if (adapterClass == null) return UNKNOWN; + String name = adapterClass.getPackageName(); + if (name.endsWith(".minecraft")) return MINECRAFT; + if (name.endsWith(".adapters.delight")) return FARMERS_DELIGHT; + if (name.endsWith(".farmersdelight")) return FARMERS_DELIGHT; + if (name.endsWith(".extradelight")) return EXTRA_DELIGHT; + if (name.endsWith(".crabbersdelight")) return CRABBERS_DELIGHT; + if (name.endsWith(".casualnessdelight")) return CASUALNESS_DELIGHT; + if (name.endsWith(".expandeddelight")) return EXPANDED_DELIGHT; + if (name.endsWith(".brewinandchewin")) return BREWIN_AND_CHEWIN; + if (name.endsWith(".nomadsdelight")) return NOMADS_DELIGHT; + if (name.endsWith(".ubesdelight")) return UBES_DELIGHT; + if (name.endsWith(".barbequesdelight")) return BARBEQUES_DELIGHT; + if (name.endsWith(".youkaishomecoming")) return YOUKAI_HOMECOMING; + if (name.endsWith(".extendedae")) return EXTENDED_AE; + if (name.endsWith(".advancedae")) return ADVANCED_AE; + if (name.endsWith(".ae2cs")) return AE2CS; + if (name.endsWith(".ae2lt")) return AE2LT; + if (name.endsWith(".dataenergistics")) return DATA_ENERGISTICS; + if (name.endsWith(".ae2")) return AE2; + if (name.endsWith(".industrialforegoing")) return INDUSTRIAL_FOREGOING; + if (name.endsWith(".actuallyadditions")) return ACTUALLY_ADDITIONS; + if (name.endsWith(".arsnouveau")) return ARS_NOUVEAU; + if (name.endsWith(".mysticalagriculture")) return MYSTICAL_AGRICULTURE; + if (name.endsWith(".productivebees")) return PRODUCTIVE_BEES; + if (name.endsWith(".draconicevolution")) return DRACONIC_EVOLUTION; + if (name.endsWith(".powah")) return POWAH; + if (name.endsWith(".astralsorcery")) return ASTRAL_SORCERY; + if (name.endsWith(".extendedcrafting")) return EXTENDED_CRAFTING; + if (name.endsWith(".avaritia")) return AVARITIA; + if (name.endsWith(".eco")) return NEO_ECO_AE; + if (name.endsWith(".naturesaura")) return NATURES_AURA; + if (name.endsWith(".forbiddenarcanus")) return FORBIDDEN_ARCANUS; + if (name.endsWith(".occultism")) return OCCULTISM; + if (name.endsWith(".summoningrituals")) return SUMMONING_RITUALS; + if (name.endsWith(".malum")) return MALUM; + if (name.endsWith(".enderio")) return ENDER_IO; + if (name.endsWith(".create")) return CREATE; + if (name.endsWith(".oritech")) return ORITECH; + if (name.endsWith(".neovitae")) return NEOVITAE; + if (name.endsWith(".ufo")) return UFO; + if (name.endsWith(".modernindustrialization")) return MODERN_INDUSTRIALIZATION; + if (name.endsWith(".immersiveengineering")) return IMMERSIVE_ENGINEERING; + if (name.endsWith(".pneumaticcraft")) return PNEUMATICCRAFT; + if (name.endsWith(".extremereactors")) return BIG_REACTORS; + if (name.endsWith(".justdirethings")) return JUSTDIRETHINGS; + if (name.endsWith(".ironsspellbooks")) return IRONS_SPELLBOOKS; + if (name.endsWith(".hostilenetworks")) return HOSTILE_NETWORKS; + if (name.endsWith(".apothicflux")) return APOTHIC_FLUX; + if (name.endsWith(".fluxnetworks")) return FLUX_NETWORKS; + if (name.endsWith(".mekanism") || name.endsWith(".generators")) return MEKANISM; + return UNKNOWN; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/RecipeAdapterCompatRegistry.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/RecipeAdapterCompatRegistry.java new file mode 100644 index 00000000..203163c9 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/RecipeAdapterCompatRegistry.java @@ -0,0 +1,584 @@ +package com.sorrowmist.useless.content.recipe.adapters; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.minecraft.BrewingRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.minecraft.CraftingRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.minecraft.FishingLootRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.minecraft.SmeltingRecipeAdapter; +import com.sorrowmist.useless.content.recipe.adapters.minecraft.VillagerTradeRecipeAdapter; +import com.sorrowmist.useless.core.config.ConfigManager; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; +import com.mojang.logging.LogUtils; +import org.slf4j.Logger; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public final class RecipeAdapterCompatRegistry { + private static final Logger LOGGER = LogUtils.getLogger(); + + public static final String EXTENDED_AE = RecipeSourceIds.EXTENDED_AE; + public static final String ADVANCED_AE = RecipeSourceIds.ADVANCED_AE; + public static final String MEKANISM = RecipeSourceIds.MEKANISM; + public static final String MEKANISM_GENERATORS = RecipeSourceIds.MEKANISM_GENERATORS; + public static final String APP_MEK = RecipeSourceIds.APP_MEK; + public static final String AE2 = RecipeSourceIds.AE2; + public static final String AE2CS = RecipeSourceIds.AE2CS; + public static final String FARMERS_DELIGHT = RecipeSourceIds.FARMERS_DELIGHT; + public static final String EXTRA_DELIGHT = RecipeSourceIds.EXTRA_DELIGHT; + public static final String CRABBERS_DELIGHT = RecipeSourceIds.CRABBERS_DELIGHT; + public static final String CASUALNESS_DELIGHT = RecipeSourceIds.CASUALNESS_DELIGHT; + public static final String EXPANDED_DELIGHT = RecipeSourceIds.EXPANDED_DELIGHT; + public static final String BREWIN_AND_CHEWIN = RecipeSourceIds.BREWIN_AND_CHEWIN; + public static final String NOMADS_DELIGHT = RecipeSourceIds.NOMADS_DELIGHT; + public static final String UBES_DELIGHT = RecipeSourceIds.UBES_DELIGHT; + public static final String BARBEQUES_DELIGHT = RecipeSourceIds.BARBEQUES_DELIGHT; + public static final String YOUKAIS_FEASTS = RecipeSourceIds.YOUKAIS_FEASTS; + public static final String YOUKAI_HOMECOMING = RecipeSourceIds.YOUKAI_HOMECOMING; + public static final String KALEIDOSCOPE_COOKERY = RecipeSourceIds.KALEIDOSCOPE_COOKERY; + public static final String KALEIDOSCOPE_GRILLING = RecipeSourceIds.KALEIDOSCOPE_GRILLING; + public static final String KALEIDOSCOPE_TAVERN = RecipeSourceIds.KALEIDOSCOPE_TAVERN; + public static final String INDUSTRIAL_FOREGOING = RecipeSourceIds.INDUSTRIAL_FOREGOING; + public static final String ACTUALLY_ADDITIONS = RecipeSourceIds.ACTUALLY_ADDITIONS; + public static final String ARS_NOUVEAU = RecipeSourceIds.ARS_NOUVEAU; + public static final String MYSTICAL_AGRICULTURE = RecipeSourceIds.MYSTICAL_AGRICULTURE; + public static final String AE2LT = RecipeSourceIds.AE2LT; + public static final String DATA_ENERGISTICS = RecipeSourceIds.DATA_ENERGISTICS; + public static final String PRODUCTIVE_BEES = RecipeSourceIds.PRODUCTIVE_BEES; + public static final String DRACONIC_EVOLUTION = RecipeSourceIds.DRACONIC_EVOLUTION; + public static final String POWAH = RecipeSourceIds.POWAH; + public static final String EXTENDED_CRAFTING = RecipeSourceIds.EXTENDED_CRAFTING; + public static final String AVARITIA = RecipeSourceIds.AVARITIA; + + /** + * Re-Avaritia 与 AvaritiaNeo 均声明 modId {@code avaritia},但包结构互不兼容:前者位于 + * {@code committee.nova.mods.avaritia},后者位于 {@code net.byAqua3.avaritia}。二者的显示名 + * 分别为 {@code Re-Avaritia} 与 {@code Avaritia},该差异不足以作为可靠判据,故改以 Re-Avaritia + * 独有的注册类作为判定依据:仅当该类可加载时才注册针对 Re-Avaritia 编写的适配器,否则适配器会在 + * 取模具物品时抛出 {@link NoClassDefFoundError}。 + */ + private static final String RE_AVARITIA_REGISTRY_CLASS = + "committee.nova.mods.avaritia.init.registry.ModBlocks"; + + public static final String NEO_ECO_AE = RecipeSourceIds.NEO_ECO_AE; + public static final String NATURES_AURA = RecipeSourceIds.NATURES_AURA; + public static final String FORBIDDEN_ARCANUS = RecipeSourceIds.FORBIDDEN_ARCANUS; + public static final String OCCULTISM = RecipeSourceIds.OCCULTISM; + public static final String SUMMONING_RITUALS = RecipeSourceIds.SUMMONING_RITUALS; + public static final String MALUM = RecipeSourceIds.MALUM; + public static final String ENDER_IO = RecipeSourceIds.ENDER_IO; + public static final String CREATE = RecipeSourceIds.CREATE; + public static final String ORITECH = RecipeSourceIds.ORITECH; + public static final String NEOVITAE = RecipeSourceIds.NEOVITAE; + public static final String UFO = RecipeSourceIds.UFO; + public static final String MODERN_INDUSTRIALIZATION = RecipeSourceIds.MODERN_INDUSTRIALIZATION; + public static final String IMMERSIVE_ENGINEERING = RecipeSourceIds.IMMERSIVE_ENGINEERING; + public static final String PNEUMATICCRAFT = RecipeSourceIds.PNEUMATICCRAFT; + public static final String BIG_REACTORS = RecipeSourceIds.BIG_REACTORS; + public static final String JUSTDIRETHINGS = RecipeSourceIds.JUSTDIRETHINGS; + public static final String JDTE = RecipeSourceIds.JDTE; + public static final String IRONS_SPELLBOOKS = RecipeSourceIds.IRONS_SPELLBOOKS; + public static final String HOSTILE_NETWORKS = RecipeSourceIds.HOSTILE_NETWORKS; + public static final String APOTHIC_FLUX = RecipeSourceIds.APOTHIC_FLUX; + public static final String FLUX_NETWORKS = RecipeSourceIds.FLUX_NETWORKS; + public static final String APOTHEOSIS = RecipeSourceIds.APOTHEOSIS; + public static final String LYCHEE = RecipeSourceIds.LYCHEE; + public static final String ASTRAL_SORCERY = RecipeSourceIds.ASTRAL_SORCERY; + public static final String BOTANY_POTS = RecipeSourceIds.BOTANY_POTS; + public static final String EX_DEORUM = RecipeSourceIds.EX_DEORUM; + + private static final List ENTRIES = List.of( + new CompatEntry(null, RecipeAdapterCompatRegistry::registerMinecraft), + new CompatEntry(FARMERS_DELIGHT, RecipeAdapterCompatRegistry::registerFarmersDelight), + new CompatEntry(EXTRA_DELIGHT, RecipeAdapterCompatRegistry::registerExtraDelight), + new CompatEntry(CRABBERS_DELIGHT, RecipeAdapterCompatRegistry::registerCrabbersDelight), + new CompatEntry(CASUALNESS_DELIGHT, RecipeAdapterCompatRegistry::registerCasualnessDelight), + new CompatEntry(EXPANDED_DELIGHT, RecipeAdapterCompatRegistry::registerExpandedDelight), + new CompatEntry(BREWIN_AND_CHEWIN, RecipeAdapterCompatRegistry::registerBrewinAndChewin), + new CompatEntry(NOMADS_DELIGHT, RecipeAdapterCompatRegistry::registerNomadsDelight), + new CompatEntry(UBES_DELIGHT, RecipeAdapterCompatRegistry::registerUbesDelight), + new CompatEntry(BARBEQUES_DELIGHT, RecipeAdapterCompatRegistry::registerBarbequesDelight), + new CompatEntry(YOUKAIS_FEASTS, RecipeAdapterCompatRegistry::registerYoukaisHomecoming), + new CompatEntry(KALEIDOSCOPE_COOKERY, RecipeAdapterCompatRegistry::registerKaleidoscopeCookery), + new CompatEntry(KALEIDOSCOPE_GRILLING, RecipeAdapterCompatRegistry::registerKaleidoscopeGrilling), + new CompatEntry(KALEIDOSCOPE_TAVERN, RecipeAdapterCompatRegistry::registerKaleidoscopeTavern), + new CompatEntry(EXTENDED_AE, RecipeAdapterCompatRegistry::registerExtendedAE), + new CompatEntry(ADVANCED_AE, RecipeAdapterCompatRegistry::registerAdvancedAE), + new CompatEntry(MEKANISM, RecipeAdapterCompatRegistry::registerMekanism), + new CompatEntry(MEKANISM_GENERATORS, RecipeAdapterCompatRegistry::registerMekanismGenerators), + new CompatEntry(AE2, RecipeAdapterCompatRegistry::registerAE2), + new CompatEntry(AE2CS, RecipeAdapterCompatRegistry::registerAECrystalScience), + new CompatEntry(INDUSTRIAL_FOREGOING, RecipeAdapterCompatRegistry::registerIndustrialForegoing), + new CompatEntry(ACTUALLY_ADDITIONS, RecipeAdapterCompatRegistry::registerActuallyAdditions), + new CompatEntry(ARS_NOUVEAU, RecipeAdapterCompatRegistry::registerArsNouveau), + new CompatEntry(MYSTICAL_AGRICULTURE, RecipeAdapterCompatRegistry::registerMysticalAgriculture), + new CompatEntry(AE2LT, RecipeAdapterCompatRegistry::registerAELightningTech), + new CompatEntry(DATA_ENERGISTICS, RecipeAdapterCompatRegistry::registerDataEnergistics), + new CompatEntry(PRODUCTIVE_BEES, RecipeAdapterCompatRegistry::registerProductiveBees), + new CompatEntry(DRACONIC_EVOLUTION, RecipeAdapterCompatRegistry::registerDraconicEvolution), + new CompatEntry(POWAH, RecipeAdapterCompatRegistry::registerPowah), + new CompatEntry(EXTENDED_CRAFTING, RecipeAdapterCompatRegistry::registerExtendedCrafting), + new CompatEntry(AVARITIA, RecipeAdapterCompatRegistry::registerAvaritia), + new CompatEntry(NEO_ECO_AE, RecipeAdapterCompatRegistry::registerNeoECOAE), + new CompatEntry(NATURES_AURA, RecipeAdapterCompatRegistry::registerNaturesAura), + new CompatEntry(FORBIDDEN_ARCANUS, RecipeAdapterCompatRegistry::registerForbiddenArcanus), + new CompatEntry(OCCULTISM, RecipeAdapterCompatRegistry::registerOccultism), + new CompatEntry(SUMMONING_RITUALS, RecipeAdapterCompatRegistry::registerSummoningRituals), + new CompatEntry(MALUM, RecipeAdapterCompatRegistry::registerMalum), + new CompatEntry(ENDER_IO, RecipeAdapterCompatRegistry::registerEnderIO), + new CompatEntry(CREATE, RecipeAdapterCompatRegistry::registerCreate), + new CompatEntry(ORITECH, RecipeAdapterCompatRegistry::registerOritech), + new CompatEntry(NEOVITAE, RecipeAdapterCompatRegistry::registerNeoVitae), + new CompatEntry(UFO, RecipeAdapterCompatRegistry::registerUfo), + new CompatEntry(MODERN_INDUSTRIALIZATION, RecipeAdapterCompatRegistry::registerModernIndustrialization), + new CompatEntry(IMMERSIVE_ENGINEERING, RecipeAdapterCompatRegistry::registerImmersiveEngineering), + new CompatEntry(PNEUMATICCRAFT, RecipeAdapterCompatRegistry::registerPneumaticCraft), + new CompatEntry(BIG_REACTORS, RecipeAdapterCompatRegistry::registerBigReactors), + new CompatEntry(JUSTDIRETHINGS, RecipeAdapterCompatRegistry::registerJustDireThings), + new CompatEntry(IRONS_SPELLBOOKS, RecipeAdapterCompatRegistry::registerIronsSpellbooks), + new CompatEntry(HOSTILE_NETWORKS, RecipeAdapterCompatRegistry::registerHostileNetworks), + new CompatEntry(APOTHIC_FLUX, RecipeAdapterCompatRegistry::registerApothicFlux), + new CompatEntry(FLUX_NETWORKS, RecipeAdapterCompatRegistry::registerFluxNetworks), + new CompatEntry(APOTHEOSIS, RecipeAdapterCompatRegistry::registerApotheosis), + new CompatEntry(LYCHEE, RecipeAdapterCompatRegistry::registerLychee), + new CompatEntry(ASTRAL_SORCERY, RecipeAdapterCompatRegistry::registerAstralSorcery), + new CompatEntry(BOTANY_POTS, RecipeAdapterCompatRegistry::registerBotanyPots), + new CompatEntry(EX_DEORUM, RecipeAdapterCompatRegistry::registerExDeorum) + ); + + private RecipeAdapterCompatRegistry() {} + + public static void init(FMLCommonSetupEvent event) { + for (CompatEntry entry : ENTRIES) { + initCompat(event, entry.modId(), entry.registerAction()); + } + } + + public static boolean isLoaded(String modId) { + return ModList.get().isLoaded(modId); + } + + /** + * 探测指定类能否被当前类加载器解析。传入 {@code initialize=false} 以避免触发目标类的静态初始化, + * 因此该探测本身不产生副作用;类缺失或其依赖不满足时返回 {@code false},不向调用方抛出异常。 + */ + private static boolean isClassAvailable(String className) { + if (className == null || className.isBlank()) { + return false; + } + try { + Class.forName(className, false, RecipeAdapterCompatRegistry.class.getClassLoader()); + return true; + } catch (ClassNotFoundException | LinkageError exception) { + return false; + } + } + + private static void initCompat(FMLCommonSetupEvent event, @Nullable String modId, Runnable registerAction) { + if (modId != null && (!isLoaded(modId) || !ConfigManager.isRecipeConversionEnabled(modId))) return; + + event.enqueueWork(() -> { + try { + registerAction.run(); + } catch (Exception e) { + RateLimitedLogger.error(LOGGER, "Failed to register recipe adapters for mod: {}", modId, e); + } + }); + } + + private static void registerByName(String className) { + try { + Class type=Class.forName(className,true,RecipeAdapterCompatRegistry.class.getClassLoader()); + register((IRecipeAdapter)type.getConstructor().newInstance()); + } catch (ReflectiveOperationException | LinkageError exception) { + throw new IllegalStateException("Installed mod recipe adapter unavailable: "+className,exception); + } + } + + private static void registerFactory(String className) { + try { + Class type=Class.forName(className,true,RecipeAdapterCompatRegistry.class.getClassLoader()); + var adapters=(java.util.List)type.getMethod("createAdapters").invoke(null); + for(Object adapter:adapters) register((IRecipeAdapter)adapter); + } catch (ReflectiveOperationException | LinkageError exception) { + throw new IllegalStateException("Installed mod adapter factory unavailable: "+className,exception); + } + } + + private static void register(IRecipeAdapter adapter) { + AlloyFurnaceRecipeManager.getInstance().registerAdapter(adapter); + } + + private static void registerMinecraft() { + if (ConfigManager.isCraftingRecipeConversionEnabled()) { + register(new CraftingRecipeAdapter()); + } + if (ConfigManager.isSmeltingRecipeConversionEnabled()) { + register(new SmeltingRecipeAdapter(RecipeType.SMELTING, Items.FURNACE)); + register(new SmeltingRecipeAdapter(RecipeType.BLASTING, Items.BLAST_FURNACE)); + register(new SmeltingRecipeAdapter(RecipeType.SMOKING, Items.SMOKER)); + } + if (ConfigManager.isBrewingRecipeConversionEnabled()) { + register(new BrewingRecipeAdapter()); + } + if (ConfigManager.isVillagerTradeRecipeConversionEnabled()) { + register(new VillagerTradeRecipeAdapter()); + } + if (ConfigManager.isFishingRecipeConversionEnabled()) { + register(new FishingLootRecipeAdapter()); + } + } + + private static void registerFarmersDelight() { + invokeOptionalLoader( + "com.sorrowmist.useless.compat.farmersdelight.FarmersDelightRecipeCompatLoader"); + } + + private static void registerExtraDelight() { + invokeOptionalLoader( + "com.sorrowmist.useless.compat.extradelight.ExtraDelightRecipeCompatLoader"); + } + + private static void registerCrabbersDelight() { + invokeOptionalLoader( + "com.sorrowmist.useless.compat.crabbersdelight.CrabbersDelightRecipeCompatLoader"); + } + + private static void registerCasualnessDelight() { + invokeOptionalLoader( + "com.sorrowmist.useless.compat.casualnessdelight.CasualnessDelightRecipeCompatLoader"); + } + + private static void registerExpandedDelight() { + invokeOptionalLoader( + "com.sorrowmist.useless.compat.expandeddelight.ExpandedDelightRecipeCompatLoader"); + } + + private static void registerBrewinAndChewin() { + invokeOptionalLoader( + "com.sorrowmist.useless.compat.brewinandchewin.BrewinAndChewinRecipeCompatLoader"); + } + + private static void registerNomadsDelight() { + invokeOptionalLoader( + "com.sorrowmist.useless.compat.nomadsdelight.NomadsDelightRecipeCompatLoader"); + } + + private static void registerUbesDelight() { + invokeOptionalLoader( + "com.sorrowmist.useless.compat.ubesdelight.UbesDelightRecipeCompatLoader"); + } + + private static void registerBarbequesDelight() { + invokeOptionalLoader( + "com.sorrowmist.useless.compat.barbequesdelight.BarbequesDelightRecipeCompatLoader"); + } + + private static void registerYoukaisHomecoming() { + invokeOptionalLoader( + "com.sorrowmist.useless.compat.youkaishomecoming.YoukaisHomecomingRecipeCompatLoader"); + } + + private static void registerKaleidoscopeCookery() { + invokeOptionalLoader( + "com.sorrowmist.useless.compat.kaleidoscope.cookery.KaleidoscopeCookeryRecipeCompatLoader"); + } + + private static void registerKaleidoscopeGrilling() { + invokeOptionalLoader( + "com.sorrowmist.useless.compat.kaleidoscope.grilling.KaleidoscopeGrillingRecipeCompatLoader"); + } + + private static void registerKaleidoscopeTavern() { + invokeOptionalLoader( + "com.sorrowmist.useless.compat.kaleidoscope.tavern.KaleidoscopeTavernRecipeCompatLoader"); + } + + private static void registerExtendedAE() { + registerByName("com.sorrowmist.useless.content.recipe.adapters.ae.extendedae.CircuitCutterRecipeAdapter"); + // Crystal assembler is a later-version machine, absent from ExtendedAE 1.4.6. + registerByName("com.sorrowmist.useless.content.recipe.adapters.ae.extendedae.CrystalFixerRecipeAdapter"); + } + + private static void registerAdvancedAE() { + registerByName("com.sorrowmist.useless.content.recipe.adapters.ae.advancedae.ReactionChamberRecipeAdapter"); + } + + private static void registerMekanism() { + invokeOptionalLoader("com.sorrowmist.useless.compat.mekanism.MekanismRecipeCompatLoader"); + } + + private static void registerMekanismGenerators() { + if (isLoaded(MEKANISM) && isLoaded(APP_MEK)) { + invokeOptionalLoader("com.sorrowmist.useless.compat.mekanismgenerators.MekanismGeneratorsCompatLoader"); + } + } + + private static void invokeOptionalLoader(String className) { + try { + Class loader = Class.forName(className, true, RecipeAdapterCompatRegistry.class.getClassLoader()); + loader.getMethod("register").invoke(null); + } catch (ReflectiveOperationException | LinkageError exception) { + RateLimitedLogger.error(LOGGER, "Failed to register optional recipe adapters from {}", className, exception); + } + } + + private static void registerAE2() { + registerByName("com.sorrowmist.useless.content.recipe.adapters.ae.ae2.ChargerRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.ae.ae2.InscriberRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.ae.ae2.WorldInteractionRecipeAdapter"); + } + + private static void registerAECrystalScience() { + registerByName("com.sorrowmist.useless.content.recipe.adapters.ae.ae2cs.CircuitEtcherRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.ae.ae2cs.CrystalAggregatorRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.ae.ae2cs.CrystalPulverizerRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.ae.ae2cs.CrystalGrowthRecipeAdapter"); + } + + private static void registerIndustrialForegoing() { + registerByName("com.sorrowmist.useless.content.recipe.adapters.industrialforegoing.BioReactorRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.industrialforegoing.DissolutionChamberRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.industrialforegoing.FermentationStationRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.industrialforegoing.FluidSievingMachineRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.industrialforegoing.WashingFactoryRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.industrialforegoing.LatexProcessingUnitRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.industrialforegoing.FluidExtractorRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.industrialforegoing.SlaughterFactoryRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.industrialforegoing.LaserDrillOreRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.industrialforegoing.LaserDrillFluidRecipeAdapter"); + if (isLoaded("ifeu")) { + registerByName("com.sorrowmist.useless.content.recipe.adapters.industrialforegoing.InfuserRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.industrialforegoing.PrecisionShapedRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.industrialforegoing.PrecisionShapelessRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.industrialforegoing.FluidShapedRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.industrialforegoing.FluidShapelessRecipeAdapter"); + } + } + + private static void registerActuallyAdditions() { + registerByName("com.sorrowmist.useless.content.recipe.adapters.actuallyadditions.LaserRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.actuallyadditions.EmpowererRecipeAdapter"); + } + + private static void registerArsNouveau() { + registerByName("com.sorrowmist.useless.content.recipe.adapters.arsnouveau.EnchantingApparatusRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.arsnouveau.ImbuementRecipeAdapter"); + } + + private static void registerMysticalAgriculture() { + registerByName("com.sorrowmist.useless.content.recipe.adapters.mysticalagriculture.InfusionRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.mysticalagriculture.AwakeningRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.mysticalagriculture.SeedEssenceRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.mysticalagriculture.SoulJarRecipeAdapter"); + if (isLoaded("omnia")) { + registerByName("com.sorrowmist.useless.content.recipe.adapters.mysticalagriculture.OmniaUniversalSeedRecipeAdapter"); + } + } + + private static void registerAELightningTech() { + registerFactory("com.sorrowmist.useless.content.recipe.adapters.ae.ae2lt.AELightningTechCompatLoader"); + } + + private static void registerDataEnergistics() { + registerByName("com.sorrowmist.useless.content.recipe.adapters.ae.dataenergistics.DataReassemblerRecipeAdapter"); + } + + private static void registerProductiveBees() { + registerByName("com.sorrowmist.useless.content.recipe.adapters.productivebees.BeeProduceRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.productivebees.CentrifugeRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.productivebees.BeeBreedingRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.productivebees.BeeConversionRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.productivebees.BeeFishingRecipeAdapter"); + } + + private static void registerDraconicEvolution() { + registerByName("com.sorrowmist.useless.content.recipe.adapters.draconicevolution.DraconicFusionRecipeAdapter"); + } + + private static void registerPowah() { + registerByName("com.sorrowmist.useless.content.recipe.adapters.powah.EnergizingRecipeAdapter"); + } + + private static void registerExtendedCrafting() { + registerByName("com.sorrowmist.useless.content.recipe.adapters.extendedcrafting.ExtendedCraftingTableRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.extendedcrafting.ExtendedCraftingCompressorRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.extendedcrafting.ExtendedCraftingCombinationRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.extendedcrafting.ExtendedCraftingEnderCrafterRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.extendedcrafting.ExtendedCraftingFluxCrafterRecipeAdapter"); + } + + private static void registerAvaritia() { + // 二级门禁:外层 CompatEntry 已确认 modId "avaritia" 存在,但 AvaritiaNeo 同样使用该 modId + // 且包结构完全不兼容。缺失该判定时,下列适配器会在取模具物品时抛出 NoClassDefFoundError, + // 该错误属于 Error 而非 Exception,会穿透 initCompat 的捕获并导致整局加载失败。 + if (!isClassAvailable(RE_AVARITIA_REGISTRY_CLASS)) { + RateLimitedLogger.warn(LOGGER, "Mod id {} is present but {} is not loadable; skipping Re-Avaritia recipe adapters. " + + "A non Re-Avaritia implementation of this mod id is likely installed.", + AVARITIA, RE_AVARITIA_REGISTRY_CLASS); + return; + } + registerByName("com.sorrowmist.useless.content.recipe.adapters.avaritia.ReAvaritiaSculkCraftingRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.avaritia.ReAvaritiaTableRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.avaritia.ReAvaritiaExtremeSmithingRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.avaritia.ReAvaritiaCompressorRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.avaritia.ReAvaritiaCollectorRecipeAdapter"); + } + + private static void registerNeoECOAE() { + registerByName("com.sorrowmist.useless.content.recipe.adapters.eco.IntegratedWorkingStationRecipeAdapter"); + } + + private static void registerNaturesAura() { + registerByName("com.sorrowmist.useless.content.recipe.adapters.naturesaura.TreeRitualRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.naturesaura.NatureAltarRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.naturesaura.AnimalSpawnerRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.naturesaura.OfferingRecipeAdapter"); + } + + private static void registerForbiddenArcanus() { + invokeOptionalLoader("com.sorrowmist.useless.compat.forbiddenarcanus.ForbiddenArcanusRecipeCompatLoader"); + } + + private static void registerOccultism() { + registerByName("com.sorrowmist.useless.content.recipe.adapters.occultism.OccultismRitualRecipeAdapter"); + } + + private static void registerSummoningRituals() { + registerByName("com.sorrowmist.useless.content.recipe.adapters.summoningrituals.SummoningRitualsAltarRecipeAdapter"); + } + + private static void registerMalum() { + registerByName("com.sorrowmist.useless.content.recipe.adapters.malum.SpiritFocusingRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.malum.SpiritInfusionRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.malum.VoidFavorRecipeAdapter"); + } + + private static void registerEnderIO() { + registerByName("com.sorrowmist.useless.content.recipe.adapters.enderio.EnchanterRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.enderio.FireCraftingRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.enderio.AlloySmeltingRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.enderio.SlicingRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.enderio.SagMillingRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.enderio.SoulVialRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.enderio.SoulBindingRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.enderio.TankRecipeAdapter"); + // EnderIO 6.2.13 has no Vat machine or fermenting recipe type. + } + + private static void registerCreate() { + invokeOptionalLoader("com.sorrowmist.useless.compat.create.CreateRecipeCompatLoader"); + } + + private static void registerOritech() { + invokeOptionalLoader("com.sorrowmist.useless.compat.oritech.OritechRecipeCompatLoader"); + } + + private static void registerNeoVitae() { + invokeOptionalLoader("com.sorrowmist.useless.compat.neovitae.NeoVitaeRecipeCompatLoader"); + } + + private static void registerUfo() { + invokeOptionalLoader("com.sorrowmist.useless.compat.ufo.UfoRecipeCompatLoader"); + } + + private static void registerModernIndustrialization() { + invokeOptionalLoader( + "com.sorrowmist.useless.compat.modernindustrialization.ModernIndustrializationRecipeCompatLoader"); + } + + private static void registerImmersiveEngineering() { + invokeOptionalLoader( + "com.sorrowmist.useless.compat.immersiveengineering.ImmersiveEngineeringRecipeCompatLoader"); + } + + private static void registerPneumaticCraft() { + invokeOptionalLoader( + "com.sorrowmist.useless.compat.pneumaticcraft.PneumaticCraftRecipeCompatLoader"); + } + + private static void registerBigReactors() { + invokeOptionalLoader( + "com.sorrowmist.useless.compat.extremereactors.ExtremeReactorsRecipeCompatLoader"); + } + + private static void registerJustDireThings() { + registerByName("com.sorrowmist.useless.content.recipe.adapters.justdirethings.GooSpreadRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.justdirethings.GooSpreadRecipeTagAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.justdirethings.FluidDropRecipeAdapter"); + // 附属开关需在此二次判定:外层 CompatEntry 只看本体(justdirethings)的开关, + // 本体开启时附属仍可能被玩家单独关闭,所以这里必须再查一次 JDTE 自己的开关。 + if (isLoaded(JDTE) && ConfigManager.isRecipeConversionEnabled(JDTE)) { + invokeOptionalLoader( + "com.sorrowmist.useless.content.recipe.adapters.justdirethings.jdte.JDTERecipeCompatLoader"); + } + } + + private static void registerIronsSpellbooks() { + invokeOptionalLoader( + "com.sorrowmist.useless.compat.ironsspellbooks.IronsSpellbooksRecipeCompatLoader"); + } + + private static void registerHostileNetworks() { + registerByName("com.sorrowmist.useless.content.recipe.adapters.hostilenetworks.HostileNetworksRecipeAdapter"); + } + + private static void registerApothicFlux() { + invokeOptionalLoader( + "com.sorrowmist.useless.compat.apothicflux.ApothicFluxRecipeCompatLoader"); + } + + private static void registerFluxNetworks() { + registerByName("com.sorrowmist.useless.content.recipe.adapters.fluxnetworks.FluxNetworksRecipeAdapter"); + } + + private static void registerApotheosis() { + invokeOptionalLoader( + "com.sorrowmist.useless.compat.apotheosis.ApotheosisRecipeCompatLoader"); + } + + private static void registerLychee() { + invokeOptionalLoader( + "com.sorrowmist.useless.compat.lychee.LycheeRecipeCompatLoader"); + } + + private static void registerAstralSorcery() { + // 植物魔法(Mystical Agriculture)存在同名适配器,此处必须使用全限定名以避免简名冲突。 + registerByName("com.sorrowmist.useless.content.recipe.adapters.astralsorcery.InfusionRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.astralsorcery.LightwellRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.astralsorcery.AltarRecipeAdapter"); + } + + private static void registerBotanyPots() { + registerByName("com.sorrowmist.useless.content.recipe.adapters.botanypots.BotanyPotsRecipeAdapter"); + } + + /** + * 注册 Ex Deorum 的桶、坩埚、筛子与锤子配方适配器。 + * + *

桶的四类配方(堆肥、物品混合、流体混合、流体转化)共用 {@code exdeorum:barrels} + * 物品标签作为模具,因此无需按石桶与木桶分别登记;坩埚与筛子的模具由材料注册表在运行时枚举。 + * 筛子适配同时覆盖普通筛子与重型筛子,其模具由配方限定的筛网决定。

+ * + *

锤子适配覆盖普通锤与压缩锤两类配方,模具按配方实际类型在转换阶段选定,固定为钻石锤与 + * 钻石压缩锤。该适配器以生成配方方式提供结果,必须在此登记,否则其配方不会进入合金炉配方目录。

+ */ + private static void registerExDeorum() { + registerByName("com.sorrowmist.useless.content.recipe.adapters.exdeorum.BarrelCompostRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.exdeorum.BarrelMixingRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.exdeorum.BarrelFluidMixingRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.exdeorum.FluidTransformationRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.exdeorum.CrucibleRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.exdeorum.SieveRecipeAdapter"); + registerByName("com.sorrowmist.useless.content.recipe.adapters.exdeorum.HammerRecipeAdapter"); + } + + private record CompatEntry(@Nullable String modId, Runnable registerAction) {} +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/actuallyadditions/EmpowererRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/actuallyadditions/EmpowererRecipeAdapter.java new file mode 100644 index 00000000..8f500721 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/actuallyadditions/EmpowererRecipeAdapter.java @@ -0,0 +1,141 @@ +package com.sorrowmist.useless.content.recipe.adapters.actuallyadditions; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import de.ellpeck.actuallyadditions.mod.blocks.ActuallyBlocks; +import de.ellpeck.actuallyadditions.mod.crafting.ActuallyRecipes; +import de.ellpeck.actuallyadditions.mod.crafting.EmpowererRecipe; +import net.minecraft.core.NonNullList; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Actually Additions 充能台配方适配器 + *

+ * 将充能台配方转换为合金炉配方 + * 充能台使用1个基础物品 + 4个辅助物品来产生输出 + */ +public class EmpowererRecipeAdapter implements IRecipeAdapter { + + @Override + public Class getRecipeClass() { + return EmpowererRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return new ItemStack(ActuallyBlocks.EMPOWERER.get()); + } + + @Override + @Nullable + public AdvancedAlloyFurnaceRecipe convert(RecipeHolder holder, Level level) { + if (holder == null) return null; + + EmpowererRecipe originalRecipe = holder.value(); + ResourceLocation originalId = holder.id(); + + // 获取所有输入并合并相同物品 + Map ingredientCounts = new LinkedHashMap<>(); + + // 基础输入 + Ingredient baseInput = originalRecipe.getInput(); + if (baseInput != null && baseInput.getItems().length > 0) { + AdapterUtils.mergeIngredient(ingredientCounts, baseInput, 1L); + } + + // 4个辅助物品(modifiers) + NonNullList modifiers = NonNullList.create(); + modifiers.add(originalRecipe.getStandOne()); + modifiers.add(originalRecipe.getStandTwo()); + modifiers.add(originalRecipe.getStandThree()); + modifiers.add(originalRecipe.getStandFour()); + + for (Ingredient modifier : modifiers) { + if (modifier != null && modifier.getItems().length > 0) { + AdapterUtils.mergeIngredient(ingredientCounts, modifier, 1L); + } + } + + if (ingredientCounts.isEmpty()) { + return null; + } + + // 转换为 CountedIngredient 列表 + List countedIngredients = new ArrayList<>(); + for (Map.Entry entry : ingredientCounts.entrySet()) { + countedIngredients.add(new CountedIngredient(entry.getKey(), entry.getValue())); + } + + // 获取输出物品 + ItemStack output = originalRecipe.getOutput(); + if (output.isEmpty()) { + return null; + } + + // 获取配方能量和处理时间 + int energyPerStand = originalRecipe.getEnergyPerStand(); + int totalEnergy = energyPerStand * 4; // 4个充能台 + int time = originalRecipe.getTime(); + + // 创建充能台模具要求 + Ingredient moldIngredient = Ingredient.of(new ItemStack(ActuallyBlocks.EMPOWERER.get())); + + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(originalId), + countedIngredients, + List.of(), // 无流体输入 + List.of(output), // 输出物品 + List.of(), // 无流体输出 + totalEnergy, + time, + Ingredient.EMPTY, // 无催化剂 + 0, + moldIngredient, // 充能台作为模具 + AlloyFurnaceMode.NORMAL + ); + } + + @Override + @Nullable + public List> findMatchingRecipes(Level level, Map mergedInputs, Map mergedFluids, @Nullable ItemStack mold) { + if (mergedInputs.isEmpty() || level == null) return List.of(); + + RecipeManager recipeManager = level.getRecipeManager(); + List> recipes = com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, ActuallyRecipes.Types.EMPOWERING.get()); + + return recipes.stream() + .filter(holder -> matchesRecipeInputs(holder.value(), mergedInputs)) + .toList(); + } + + private boolean matchesRecipeInputs(EmpowererRecipe recipe, Map mergedInputs) { + // 统计配方所需的物品(按 Ingredient 合并) + Map recipeCounts = new LinkedHashMap<>(); + Ingredient baseInput = recipe.getInput(); + if (baseInput != null && baseInput.getItems().length > 0) { + AdapterUtils.mergeIngredient(recipeCounts, baseInput, 1L); + } + AdapterUtils.mergeIngredient(recipeCounts, recipe.getStandOne(), 1L); + AdapterUtils.mergeIngredient(recipeCounts, recipe.getStandTwo(), 1L); + AdapterUtils.mergeIngredient(recipeCounts, recipe.getStandThree(), 1L); + AdapterUtils.mergeIngredient(recipeCounts, recipe.getStandFour(), 1L); + + return AdapterUtils.matchesRequired(mergedInputs, recipeCounts); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/actuallyadditions/LaserRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/actuallyadditions/LaserRecipeAdapter.java new file mode 100644 index 00000000..3c01907c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/actuallyadditions/LaserRecipeAdapter.java @@ -0,0 +1,101 @@ +package com.sorrowmist.useless.content.recipe.adapters.actuallyadditions; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import de.ellpeck.actuallyadditions.mod.blocks.ActuallyBlocks; +import de.ellpeck.actuallyadditions.mod.crafting.LaserRecipe; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Actually Additions 原子再构机配方适配器 + *

+ * 将原子再构机配方转换为高级熔炉配方 + * 原子再构机使用能量将输入物品转换为输出物品 + */ +public class LaserRecipeAdapter implements IRecipeAdapter { + + @Override + public Class getRecipeClass() { + return LaserRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return new ItemStack(ActuallyBlocks.ATOMIC_RECONSTRUCTOR.get()); + } + + @Override + @Nullable + public AdvancedAlloyFurnaceRecipe convert(RecipeHolder holder, Level level) { + if (holder == null) return null; + + LaserRecipe originalRecipe = holder.value(); + ResourceLocation originalId = holder.id(); + + // 获取输入材料 + List countedIngredients = new ArrayList<>(); + Ingredient input = originalRecipe.getInput(); + if (input != null && input.getItems().length > 0) { + countedIngredients.add(new CountedIngredient(input, 1L)); + } + + if (countedIngredients.isEmpty()) { + return null; + } + + // 获取输出物品 + ItemStack result = originalRecipe.getResultItem(level.registryAccess()); + if (result.isEmpty()) { + return null; + } + + // 获取配方能量消耗 + int energy = originalRecipe.getEnergy(); + + // 创建原子再构机模具要求 + Ingredient moldIngredient = Ingredient.of(new ItemStack(ActuallyBlocks.ATOMIC_RECONSTRUCTOR.get())); + + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(originalId), + countedIngredients, + List.of(), // 无流体输入 + List.of(result), // 输出物品 + List.of(), // 无流体输出 + energy, // 使用配方的能量消耗 + AdapterUtils.DEFAULT_PROCESS_TIME, + Ingredient.EMPTY, // 无催化剂 + 0, + moldIngredient, // 原子再构机作为模具 + AlloyFurnaceMode.NORMAL + ); + } + + @Override + @Nullable + public List> findMatchingRecipes(Level level, Map mergedInputs, Map mergedFluids, @Nullable ItemStack mold) { + if (mergedInputs.isEmpty() || level == null) return List.of(); + + RecipeManager recipeManager = level.getRecipeManager(); + List> recipes = com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, de.ellpeck.actuallyadditions.mod.crafting.ActuallyRecipes.Types.LASER.get()); + + return recipes.stream().filter(holder -> { + Ingredient input = holder.value().getInput(); + return input != null && AdapterUtils.hasMatchingIngredient(mergedInputs, input); + }).toList(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/advancedae/ReactionChamberRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/advancedae/ReactionChamberRecipeAdapter.java new file mode 100644 index 00000000..8f7e58c8 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/advancedae/ReactionChamberRecipeAdapter.java @@ -0,0 +1,69 @@ +package com.sorrowmist.useless.content.recipe.adapters.ae.advancedae; + +import appeng.api.stacks.AEFluidKey; +import appeng.api.stacks.AEItemKey; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess; +import com.sorrowmist.useless.content.recipe.*; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fluids.FluidStack; +import net.pedroksl.advanced_ae.common.definitions.AAEBlocks; +import net.pedroksl.advanced_ae.recipes.ReactionChamberRecipe; +import java.util.*; + +/** AdvancedAE 1.2 uses its own ingredient classes; its fluid predicate ignores NBT. */ +public class ReactionChamberRecipeAdapter implements IRecipeAdapter { + @Override public Class getRecipeClass() { return ReactionChamberRecipe.class; } + @Override public ItemStack getMoldItem() { return new ItemStack(AAEBlocks.REACTION_CHAMBER.block()); } + + @Override public AdvancedAlloyFurnaceRecipe convert(RecipeHolder holder,Level level) { + if (holder == null || holder.value() == null) return null; + var source=holder.value(); + List items=new ArrayList<>(); + if (source.getInputs() == null) return null; + for (var input:source.getInputs()) { + if (input == null || input.getAmount() <= 0 || AdapterUtils.isIngredientEmpty(input.getIngredient())) return null; + items.add(new CountedIngredient(input.getIngredient(),input.getAmount())); + } + List fluids=new ArrayList<>(); + var fluid=source.getFluid(); + // Zero-amount native fluid is the explicit no-fluid sentinel. + if (fluid != null && fluid.getAmount() != 0) { + if (fluid.getAmount() < 0 || fluid.getIngredient() == null || fluid.getIngredient() == Fluids.EMPTY) return null; + fluids.add(new SizedFluidIngredient(FluidIngredient.single(fluid.getIngredient()),fluid.getAmount())); + } + if (items.isEmpty() && fluids.isEmpty()) return null; + var output=source.output; + if (output == null || output.what() == null || output.amount() <= 0 || output.amount() > Integer.MAX_VALUE + || source.getEnergy() < 0) return null; + List outputs=new ArrayList<>(); + List outputFluids=new ArrayList<>(); + if (output.what() instanceof AEItemKey item) outputs.add(item.toStack((int)output.amount())); + else if (output.what() instanceof AEFluidKey liquid) outputFluids.add(liquid.toStack((int)output.amount())); + else return null; + return new AdvancedAlloyFurnaceRecipe(AdapterUtils.convertedId(holder.id()),items,fluids,outputs,outputFluids, + source.getEnergy(),AdapterUtils.ADVANCEDAE_REACTION_CHAMBER_PROCESS_TIME,Ingredient.EMPTY,0, + AdapterUtils.toMoldIngredient(getMoldItem()),AlloyFurnaceMode.NORMAL); + } + + public boolean matches(AdvancedAlloyFurnaceRecipe recipe,Map items,Map fluids) { + if (recipe == null) return false; + Map required=new LinkedHashMap<>(); + for (var input:recipe.inputs()) AdapterUtils.mergeIngredient(required,input.ingredient(),input.count()); + return AdapterUtils.matchesRequired(items,required) && FluidIngredientAllocator.matchesLong(recipe.inputFluids(),fluids,1L); + } + @Override public List> findMatchingRecipes(Level level,Map items, + Map fluids,ItemStack mold) { + if (level == null || !matchesMold(mold)) return List.of(); + List> result=new ArrayList<>(); + for (var holder:RecipeManagerAccess.getAllRecipesFor(level.getRecipeManager(),ReactionChamberRecipe.TYPE)) + if (matches(convert(holder,level),items,fluids)) result.add(holder); + return result; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2/ChargerRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2/ChargerRecipeAdapter.java new file mode 100644 index 00000000..fcd2f0c4 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2/ChargerRecipeAdapter.java @@ -0,0 +1,95 @@ +package com.sorrowmist.useless.content.recipe.adapters.ae.ae2; + +import appeng.core.definitions.AEBlocks; +import appeng.recipes.AERecipeTypes; +import appeng.recipes.handlers.ChargerRecipe; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Converts AE2 charger recipes into alloy-furnace recipes. */ +public final class ChargerRecipeAdapter implements IRecipeAdapter { + private static final long ENERGY = 3_200L; + private static final int PROCESS_TIME = 20; + + @Override + public Class getRecipeClass() { + return ChargerRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(AEBlocks.CHARGER.asItem()); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + + ChargerRecipe source = holder.value(); + Ingredient input = source.getIngredient(); + ItemStack output = source.getResultItem(); + if (AdapterUtils.isIngredientEmpty(input) || output == null || output.isEmpty()) { + return List.of(); + } + + AdvancedAlloyFurnaceRecipe converted = new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + List.of(new CountedIngredient(input, 1L)), + List.of(), + List.of(), + List.of(output.copy()), + List.of(), + List.of(), + ENERGY, + PROCESS_TIME, + Ingredient.EMPTY, + 0, + AdapterUtils.toMoldIngredient(getMoldItem()), + AlloyFurnaceMode.NORMAL + ); + return List.of(converted); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || mergedInputs == null || mergedInputs.isEmpty() + || !matchesMold(mold)) { + return List.of(); + } + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(level.getRecipeManager(), AERecipeTypes.CHARGER)) { + ChargerRecipe source = holder.value(); + Ingredient input = source.getIngredient(); + if (AdapterUtils.isIngredientEmpty(input) || source.getResultItem().isEmpty()) { + continue; + } + + Map required = new LinkedHashMap<>(); + AdapterUtils.mergeIngredient(required, input, 1L); + if (AdapterUtils.matchesRequired(mergedInputs, required)) { + matches.add(holder); + } + } + return matches; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2/InscriberRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2/InscriberRecipeAdapter.java new file mode 100644 index 00000000..6c76be77 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2/InscriberRecipeAdapter.java @@ -0,0 +1,199 @@ +package com.sorrowmist.useless.content.recipe.adapters.ae.ae2; + +import appeng.core.definitions.AEBlocks; +import appeng.recipes.AERecipeTypes; +import appeng.recipes.handlers.InscriberProcessType; +import appeng.recipes.handlers.InscriberRecipe; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * AE2 压印器配方适配器 + *

+ * 将压印器配方转换为高级合金熔炉配方 + *

+ * 处理逻辑: + * - 压印模板(顶部/底部)→ 模具(mold) + * - INSCRIBE 模式:模具不消耗(正常模具行为) + * - PRESS 模式:模具被消耗(需要特殊处理) + * - 中间物品 → 普通输入(总是被消耗) + *

+ * 这样可以正确处理所有AE系列mod(AE2、ExtendedAE、AdvancedAE、MEGA Cells等)的压印器配方 + */ +public class InscriberRecipeAdapter implements IRecipeAdapter { + + // AE2 压印器基础能量消耗 + private static final int AE2_ENERGY_PER_TICK = 10; + private static final int AE2_PROCESS_TICKS = 20; // 1秒 + private static final int TOTAL_ENERGY = AE2_ENERGY_PER_TICK * AE2_PROCESS_TICKS; // 200 + + @Override + public Class getRecipeClass() { + return InscriberRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return null; + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + if (mold == null || mold.isEmpty()) return false; + if (ItemStack.isSameItem(mold, new ItemStack(AEBlocks.INSCRIBER.asItem()))) return true; + return true; + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + List result = new ArrayList<>(); + + if (holder == null) return result; + + InscriberRecipe originalRecipe = holder.value(); + ResourceLocation originalId = holder.id(); + + // 获取输入 + Ingredient middleInput = originalRecipe.getMiddleInput(); + Ingredient topInput = originalRecipe.getTopOptional(); + Ingredient bottomInput = originalRecipe.getBottomOptional(); + ItemStack output = originalRecipe.getResultItem(); + InscriberProcessType processType = originalRecipe.getProcessType(); + + // 检查中间输入是否有效 + if (middleInput == null || isIngredientEmpty(middleInput) || output.isEmpty()) { + return result; + } + + // 构建输入列表 + List countedIngredients = new ArrayList<>(); + Ingredient moldIngredient; + + if (processType == InscriberProcessType.PRESS) { + // PRESS 模式:所有物品(顶部、底部、中间)都放入输入栏,以 ae2:inscriber 作为模具 + countedIngredients.add(new CountedIngredient(middleInput, 1)); + if (!isIngredientEmpty(topInput)) { + countedIngredients.add(new CountedIngredient(topInput, 1)); + } + if (!isIngredientEmpty(bottomInput)) { + countedIngredients.add(new CountedIngredient(bottomInput, 1)); + } + moldIngredient = Ingredient.of(AEBlocks.INSCRIBER.asItem()); + } else { + // INSCRIBE 模式:以顶部或底部物品作为模具(不消耗),中间物品作为输入 + countedIngredients.add(new CountedIngredient(middleInput, 1)); + if (!isIngredientEmpty(topInput)) { + moldIngredient = topInput; + } else if (!isIngredientEmpty(bottomInput)) { + moldIngredient = bottomInput; + } else { + moldIngredient = Ingredient.of(AEBlocks.INSCRIBER.asItem()); + } + } + + AdvancedAlloyFurnaceRecipe convertedRecipe = new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(originalId), + countedIngredients, + List.of(), + List.of(output.copy()), + List.of(), + TOTAL_ENERGY, + AE2_PROCESS_TICKS, + Ingredient.EMPTY, // 催化剂(AE2压印器配方不使用催化剂) + 0, + moldIngredient, // 模具 + AlloyFurnaceMode.NORMAL + ); + + result.add(convertedRecipe); + return result; + } + + /** + * 检查 Ingredient 是否为空 + *

+ * 使用 Ingredient.isEmpty() 而非 getItems().length, + * 因为 AE2 等模组可能使用自定义 Ingredient 类型, + * 其 getItems() 返回空数组但 isEmpty() 正确返回 false。 + */ + private boolean isIngredientEmpty(Ingredient ingredient) { + if (ingredient == null) return true; + return ingredient.isEmpty(); + } + + @Override + @Nullable + @SuppressWarnings("unchecked") + public List> findMatchingRecipes(Level level, Map mergedInputs, Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || mergedInputs.isEmpty()) { + return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + List> recipes = (List>) (List) com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + AERecipeTypes.INSCRIBER + ); + + List> matches = new java.util.ArrayList<>(); + + boolean hasMold = mold != null && !mold.isEmpty(); + + for (RecipeHolder holder : recipes) { + InscriberRecipe recipe = holder.value(); + + Ingredient middleInput = recipe.getMiddleInput(); + Ingredient topInput = recipe.getTopOptional(); + Ingredient bottomInput = recipe.getBottomOptional(); + InscriberProcessType processType = recipe.getProcessType(); + + if (middleInput == null || isIngredientEmpty(middleInput)) continue; + + boolean hasTop = !isIngredientEmpty(topInput); + boolean hasBottom = !isIngredientEmpty(bottomInput); + + if (processType == InscriberProcessType.PRESS) { + if (!hasMold || !ItemStack.isSameItem(mold, new ItemStack(AEBlocks.INSCRIBER.asItem()))) continue; + + Map requiredInputs = new LinkedHashMap<>(); + AdapterUtils.mergeIngredient(requiredInputs, middleInput, 1L); + if (hasTop) AdapterUtils.mergeIngredient(requiredInputs, topInput, 1L); + if (hasBottom) AdapterUtils.mergeIngredient(requiredInputs, bottomInput, 1L); + if (!AdapterUtils.matchesRequired(mergedInputs, requiredInputs)) continue; + + matches.add(holder); + } else { + boolean moldMatchesTop = hasTop && hasMold && topInput.test(mold); + boolean moldMatchesBottom = hasBottom && hasMold && bottomInput.test(mold); + + if ((hasTop || hasBottom) && (!hasMold || !moldMatchesTop && !moldMatchesBottom)) { + continue; + } + + Map requiredInputs = new LinkedHashMap<>(); + AdapterUtils.mergeIngredient(requiredInputs, middleInput, 1L); + if (!AdapterUtils.matchesRequired(mergedInputs, requiredInputs)) continue; + + matches.add(holder); + } + } + + return matches; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2/WorldInteractionRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2/WorldInteractionRecipeAdapter.java new file mode 100644 index 00000000..0b7a4d51 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2/WorldInteractionRecipeAdapter.java @@ -0,0 +1,135 @@ +package com.sorrowmist.useless.content.recipe.adapters.ae.ae2; + +import appeng.recipes.AERecipeTypes; +import appeng.recipes.transform.TransformCircumstance; +import appeng.recipes.transform.TransformRecipe; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Converts AE2 and addon in-world transform recipes using the corresponding environment mold. */ +public final class WorldInteractionRecipeAdapter implements IRecipeAdapter { + @Override + public Class getRecipeClass() { + return TransformRecipe.class; + } + + /** Each transform circumstance selects its own mold, so this adapter uses the fallback index. */ + @Override + @Nullable + public ItemStack getMoldItem() { + return null; + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + return mold != null && !mold.isEmpty() + && (mold.is(Items.WATER_BUCKET) + || mold.is(Items.LAVA_BUCKET) + || mold.is(Items.TNT)); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + + TransformRecipe source = holder.value(); + ItemStack mold = moldFor(source.circumstance); + ItemStack output = source.getResultItem(); + List inputs = AdapterUtils.mergeIngredients(source.getIngredients()); + if (mold.isEmpty() || output == null || output.isEmpty() || inputs.isEmpty()) { + return List.of(); + } + + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + inputs, + List.of(), + List.of(), + List.of(output.copy()), + List.of(), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + AdapterUtils.DEFAULT_PROCESS_TIME, + Ingredient.EMPTY, + 0, + List.of(AdapterUtils.toMoldIngredient(mold)), + AlloyFurnaceMode.NORMAL + )); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || mergedInputs == null || mergedInputs.isEmpty() + || !matchesMold(mold)) { + return List.of(); + } + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(level.getRecipeManager(), + AERecipeTypes.TRANSFORM)) { + TransformRecipe source = holder.value(); + ItemStack requiredMold = moldFor(source.circumstance); + if (requiredMold.isEmpty() || !mold.is(requiredMold.getItem())) { + continue; + } + + Map requirements = ingredientRequirements(source); + if (!requirements.isEmpty() + && AdapterUtils.matchesRequired(mergedInputs, requirements)) { + matches.add(holder); + } + } + return matches; + } + + private static Map ingredientRequirements(TransformRecipe recipe) { + Map requirements = new LinkedHashMap<>(); + if (recipe == null) { + return requirements; + } + for (Ingredient ingredient : recipe.getIngredients()) { + if (!AdapterUtils.isIngredientEmpty(ingredient)) { + AdapterUtils.mergeIngredient(requirements, ingredient, 1L); + } + } + return requirements; + } + + private static ItemStack moldFor(@Nullable TransformCircumstance circumstance) { + if (circumstance == null) { + return ItemStack.EMPTY; + } + if (circumstance.isExplosion()) { + return Items.TNT.getDefaultInstance(); + } + if (circumstance.isFluid(Fluids.WATER)) { + return Items.WATER_BUCKET.getDefaultInstance(); + } + if (circumstance.isFluid(Fluids.LAVA)) { + return Items.LAVA_BUCKET.getDefaultInstance(); + } + return ItemStack.EMPTY; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2cs/CircuitEtcherRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2cs/CircuitEtcherRecipeAdapter.java new file mode 100644 index 00000000..83a4dad2 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2cs/CircuitEtcherRecipeAdapter.java @@ -0,0 +1,157 @@ +package com.sorrowmist.useless.content.recipe.adapters.ae.ae2cs; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import io.github.lounode.ae2cs.common.init.AECSRecipeTypes; +import io.github.lounode.ae2cs.common.recipe.circuit_etcher.CircuitEtcherRecipe; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.common.crafting.SizedIngredient; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * AE2CS 电路蚀刻机配方适配器 + */ +public class CircuitEtcherRecipeAdapter implements IRecipeAdapter { + + @Override + public Class getRecipeClass() { + return CircuitEtcherRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return new ItemStack( + BuiltInRegistries.ITEM.get( + new ResourceLocation("ae2cs", "circuit_etcher"))); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + List result = new ArrayList<>(); + + if (holder == null) return result; + + CircuitEtcherRecipe recipe = holder.value(); + ResourceLocation originalId = holder.id(); + + ItemStack output = recipe.result(); + if (output.isEmpty()) return result; + + Map ingredientCounts = new LinkedHashMap<>(); + addSizedIngredient(ingredientCounts, recipe.inputA()); + addSizedIngredient(ingredientCounts, recipe.inputB()); + addSizedIngredient(ingredientCounts, recipe.inputC()); + + if (ingredientCounts.isEmpty()) return result; + + List countedIngredients = new ArrayList<>(); + for (Map.Entry entry : ingredientCounts.entrySet()) { + countedIngredients.add(new CountedIngredient(entry.getKey(), entry.getValue())); + } + + AdvancedAlloyFurnaceRecipe convertedRecipe = new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(originalId), + countedIngredients, List.of(), + List.of(output.copy()), List.of(), + AdapterUtils.ae2csEnergyCost(recipe.energyCost()), 100, + Ingredient.EMPTY, 0, + makeMold("circuit_etcher"), + AlloyFurnaceMode.NORMAL + ); + + result.add(convertedRecipe); + return result; + } + + @Override + @Nullable + public AdvancedAlloyFurnaceRecipe convert(RecipeHolder holder, Level level) { + List recipes = convertAll(holder, level); + return recipes.isEmpty() ? null : recipes.get(0); + } + + @Override + @Nullable + @SuppressWarnings("unchecked") + public List> findMatchingRecipes(Level level, Map mergedInputs, Map mergedFluids, @Nullable ItemStack mold) { + if (level == null) return List.of(); + if (!checkMold(mold, "circuit_etcher")) return List.of(); + + RecipeManager recipeManager = level.getRecipeManager(); + List> matches = new java.util.ArrayList<>(); + for (RecipeHolder holder : (List>) (List) + com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, AECSRecipeTypes.CIRCUIT_ETCHER.get())) { + CircuitEtcherRecipe recipe = holder.value(); + if (recipe.result().isEmpty()) continue; + + Map required = new LinkedHashMap<>(); + addSizedIngredient(required, recipe.inputA()); + addSizedIngredient(required, recipe.inputB()); + addSizedIngredient(required, recipe.inputC()); + if (required.isEmpty()) continue; + + if (AdapterUtils.matchesRequired(mergedInputs, required)) matches.add(holder); + } + return matches; + } + + static void addSizedIngredient(Map map, SizedIngredient si) { + if (si == null) return; + Ingredient ing = si.ingredient(); + long count = si.count(); + if (ing.isEmpty() || count <= 0) return; + for (Map.Entry entry : map.entrySet()) { + if (AdapterUtils.areIngredientsEqual(entry.getKey(), ing)) { + map.put(entry.getKey(), entry.getValue() + count); + return; + } + } + map.put(ing, count); + } + + static boolean matchesCounted(List inputs, Map requiredCounts) { + Map matched = new LinkedHashMap<>(); + for (ItemStack stack : inputs) { + if (stack.isEmpty()) continue; + int stackCount = stack.getCount(); + for (Map.Entry entry : requiredCounts.entrySet()) { + if (entry.getKey().test(stack)) { + matched.merge(entry.getKey(), (long) stackCount, Long::sum); + break; + } + } + } + for (Map.Entry entry : requiredCounts.entrySet()) { + if (matched.getOrDefault(entry.getKey(), 0L) < entry.getValue()) return false; + } + return true; + } + + static boolean checkMold(@Nullable ItemStack mold, String machineName) { + if (mold == null || mold.isEmpty()) return false; + ResourceLocation id = BuiltInRegistries.ITEM.getKey(mold.getItem()); + return "ae2cs".equals(id.getNamespace()) && machineName.equals(id.getPath()); + } + + static Ingredient makeMold(String machineName) { + return Ingredient.of(new ItemStack( + BuiltInRegistries.ITEM.get( + new ResourceLocation("ae2cs", machineName)))); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2cs/CrystalAggregatorRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2cs/CrystalAggregatorRecipeAdapter.java new file mode 100644 index 00000000..2355a585 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2cs/CrystalAggregatorRecipeAdapter.java @@ -0,0 +1,105 @@ +package com.sorrowmist.useless.content.recipe.adapters.ae.ae2cs; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import io.github.lounode.ae2cs.common.init.AECSRecipeTypes; +import io.github.lounode.ae2cs.common.recipe.crystal_aggregator.CrystalAggregatorRecipe; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * AE2CS 晶体聚合器配方适配器 + */ +public class CrystalAggregatorRecipeAdapter implements IRecipeAdapter { + + @Override + public Class getRecipeClass() { + return CrystalAggregatorRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return new ItemStack( + BuiltInRegistries.ITEM.get( + new ResourceLocation("ae2cs", "crystal_aggregator"))); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + List result = new ArrayList<>(); + + if (holder == null) return result; + + CrystalAggregatorRecipe recipe = holder.value(); + ResourceLocation originalId = holder.id(); + + ItemStack output = recipe.result(); + if (output.isEmpty()) return result; + + Map ingredientCounts = new LinkedHashMap<>(); + CircuitEtcherRecipeAdapter.addSizedIngredient(ingredientCounts, recipe.inputA()); + CircuitEtcherRecipeAdapter.addSizedIngredient(ingredientCounts, recipe.inputB()); + CircuitEtcherRecipeAdapter.addSizedIngredient(ingredientCounts, recipe.inputC()); + + if (ingredientCounts.isEmpty()) return result; + + List countedIngredients = new ArrayList<>(); + for (Map.Entry entry : ingredientCounts.entrySet()) { + countedIngredients.add(new CountedIngredient(entry.getKey(), entry.getValue())); + } + + AdvancedAlloyFurnaceRecipe convertedRecipe = new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(originalId), + countedIngredients, List.of(), + List.of(output.copy()), List.of(), + AdapterUtils.ae2csEnergyCost(recipe.energyCost()), 100, + Ingredient.EMPTY, 0, + CircuitEtcherRecipeAdapter.makeMold("crystal_aggregator"), + AlloyFurnaceMode.NORMAL + ); + + result.add(convertedRecipe); + return result; + } + + @Override + @Nullable + @SuppressWarnings("unchecked") + public List> findMatchingRecipes(Level level, Map mergedInputs, Map mergedFluids, @Nullable ItemStack mold) { + if (level == null) return List.of(); + if (!CircuitEtcherRecipeAdapter.checkMold(mold, "crystal_aggregator")) return List.of(); + + RecipeManager recipeManager = level.getRecipeManager(); + List> matches = new java.util.ArrayList<>(); + for (RecipeHolder holder : (List>) (List) + com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, AECSRecipeTypes.CRYSTAL_AGGREGATOR.get())) { + CrystalAggregatorRecipe recipe = holder.value(); + if (recipe.result().isEmpty()) continue; + + Map required = new LinkedHashMap<>(); + CircuitEtcherRecipeAdapter.addSizedIngredient(required, recipe.inputA()); + CircuitEtcherRecipeAdapter.addSizedIngredient(required, recipe.inputB()); + CircuitEtcherRecipeAdapter.addSizedIngredient(required, recipe.inputC()); + if (required.isEmpty()) continue; + + if (AdapterUtils.matchesRequired(mergedInputs, required)) matches.add(holder); + } + return matches; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2cs/CrystalGrowthRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2cs/CrystalGrowthRecipeAdapter.java new file mode 100644 index 00000000..0b264733 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2cs/CrystalGrowthRecipeAdapter.java @@ -0,0 +1,189 @@ +package com.sorrowmist.useless.content.recipe.adapters.ae.ae2cs; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import io.github.lounode.ae2cs.common.init.AECSItems; +import io.github.lounode.ae2cs.common.item.CrystalSeedItem; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.Recipe; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.Container; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.registry.DeferredItem; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * AE2CS 晶体催生仓种子→纯水晶配方适配器 + *

+ * 根据种子与纯水晶的对应关系自动生成配方: + * - 输入:水晶种子(消耗) + * - 输出:对应纯水晶 + *

+ * 每种种子生成两条同输入同输出的配方,仅模具不同: + * - 生长腔模具 ae2cs:crystal_growth_chamber:沿用历史配方 id,已编码的样板不受影响 + * - 水桶模具 minecraft:water_bucket:对应「种子浸水生长」的语义 + *

+ * 本适配器因此没有唯一模具,{@link #getMoldItem()} 返回 null 走 fallback 分支, + * 改由每条配方自带的 mold 字段在查找时精确区分(高级熔炉的模具槽只放一个模具)。 + */ +public class CrystalGrowthRecipeAdapter implements IRecipeAdapter { + + /** 晶体催生仓方块在 ae2cs 命名空间下的注册名 */ + private static final String GROWTH_CHAMBER_MOLD = "crystal_growth_chamber"; + + /** 种子物品 → 该种子的全部模具变体配方 */ + private final Map> recipeMap = new HashMap<>(); + private final List allRecipes = new ArrayList<>(); + + public CrystalGrowthRecipeAdapter() { + buildRecipes(); + } + + private void buildRecipes() { + List> seeds = AECSItems.getCrystalSeeds(); + + for (DeferredItem seedRef : seeds) { + CrystalSeedItem seedItem = seedRef.get(); + Item seed = seedRef.asItem(); + Item crystal = seedItem.getGrowTo(); + + if (crystal == null) continue; + + String seedPath = BuiltInRegistries.ITEM.getKey(seed).getPath(); + + int energy = Math.max(seedItem.getOvergrowTick() * 5, 1000); + int processTime = Math.max(seedItem.getOvergrowTick() / 10, 60); + + // 生长腔版本保留原始 id:配方身份含 id,改动会让既有万象样板失配 + AdvancedAlloyFurnaceRecipe chamberRecipe = createRecipe( + new ResourceLocation("ae2cs", "growth_" + seedPath), + seed, crystal, energy, processTime, + CircuitEtcherRecipeAdapter.makeMold(GROWTH_CHAMBER_MOLD)); + + AdvancedAlloyFurnaceRecipe waterBucketRecipe = createRecipe( + new ResourceLocation("ae2cs", "growth_" + seedPath + "_water_bucket"), + seed, crystal, energy, processTime, + AdapterUtils.toMoldIngredient(Items.WATER_BUCKET.getDefaultInstance())); + + recipeMap.put(seed, List.of(chamberRecipe, waterBucketRecipe)); + allRecipes.add(chamberRecipe); + allRecipes.add(waterBucketRecipe); + } + } + + /** 按同一输入输出、仅模具不同的方式构造一条生长配方 */ + private static AdvancedAlloyFurnaceRecipe createRecipe( + ResourceLocation id, Item seed, Item crystal, int energy, int processTime, + Ingredient mold) { + return new AdvancedAlloyFurnaceRecipe( + id, + List.of(new CountedIngredient(Ingredient.of(seed), 1)), + List.of(), + List.of(new ItemStack(crystal)), + List.of(), + energy, + processTime, + Ingredient.EMPTY, + 0, + mold, + AlloyFurnaceMode.NORMAL + ); + } + + public List getAllRecipes() { + return allRecipes; + } + + @Override + public Class getRecipeClass() { + return GrowthDummyRecipe.class; + } + + /** + * 没有唯一模具:生长腔与水桶各有一套配方,故返回 null 让管理器走 fallback 分支, + * 由 {@link #matchesMold} 与每条配方自带的模具字段共同筛选。 + */ + @Override + @Nullable + public ItemStack getMoldItem() { + return null; + } + + /** 只接受生长腔与水桶两种模具,其余模具直接跳过本适配器。 */ + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + if (mold == null || mold.isEmpty()) return false; + return CircuitEtcherRecipeAdapter.checkMold(mold, GROWTH_CHAMBER_MOLD) + || mold.is(Items.WATER_BUCKET); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + return List.of(holder.value().convertedRecipe); + } + + @Override + @Nullable + public AdvancedAlloyFurnaceRecipe convert(RecipeHolder holder, Level level) { + return holder.value().convertedRecipe; + } + + @Override + @Nullable + public List> findMatchingRecipes(Level level, Map mergedInputs, Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || mergedInputs.isEmpty()) return List.of(); + if (!matchesMold(mold)) return List.of(); + + List> matches = new java.util.ArrayList<>(); + for (Ingredient input : mergedInputs.keySet()) { + for (ItemStack stack : input.getItems()) { + Item item = stack.getItem(); + if (!(item instanceof CrystalSeedItem)) continue; + + // 同一颗种子有生长腔与水桶两种模具变体,只挑出与当前模具相符的那一条 + List variants = recipeMap.get(item); + if (variants == null) continue; + + for (AdvancedAlloyFurnaceRecipe recipe : variants) { + if (AdapterUtils.matchesMold(recipe.mold(), mold)) { + matches.add(new RecipeHolder<>(recipe.id(), new GrowthDummyRecipe(recipe))); + } + } + } + } + return matches; + } + + public static class GrowthDummyRecipe implements Recipe { + final AdvancedAlloyFurnaceRecipe convertedRecipe; + + GrowthDummyRecipe(AdvancedAlloyFurnaceRecipe r) { this.convertedRecipe = r; } + + @Override + public net.minecraft.resources.ResourceLocation getId() { return convertedRecipe.id(); } + + @Override public boolean matches(Container input, Level level) { return false; } + @Override public ItemStack assemble(Container input, net.minecraft.core.RegistryAccess registries) { return ItemStack.EMPTY; } + @Override public boolean canCraftInDimensions(int w, int h) { return false; } + @Override public ItemStack getResultItem(net.minecraft.core.RegistryAccess registries) { return ItemStack.EMPTY; } + @Override public RecipeSerializer getSerializer() { return null; } + @Override public RecipeType getType() { return null; } + } + +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2cs/CrystalPulverizerRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2cs/CrystalPulverizerRecipeAdapter.java new file mode 100644 index 00000000..a51624be --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2cs/CrystalPulverizerRecipeAdapter.java @@ -0,0 +1,108 @@ +package com.sorrowmist.useless.content.recipe.adapters.ae.ae2cs; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import io.github.lounode.ae2cs.common.init.AECSRecipeTypes; +import io.github.lounode.ae2cs.common.recipe.crystal_pulverizer.CrystalPulverizerRecipe; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * AE2CS 晶体粉碎机配方适配器 + */ +public class CrystalPulverizerRecipeAdapter implements IRecipeAdapter { + + @Override + public Class getRecipeClass() { + return CrystalPulverizerRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return new ItemStack( + BuiltInRegistries.ITEM.get( + new ResourceLocation("ae2cs", "crystal_pulverizer"))); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + List result = new ArrayList<>(); + + if (holder == null) return result; + + CrystalPulverizerRecipe recipe = holder.value(); + ResourceLocation originalId = holder.id(); + + ItemStack output = recipe.result(); + if (output.isEmpty()) return result; + + Map ingredientCounts = new LinkedHashMap<>(); + CircuitEtcherRecipeAdapter.addSizedIngredient(ingredientCounts, recipe.input()); + + if (ingredientCounts.isEmpty()) return result; + + List countedIngredients = new ArrayList<>(); + for (Map.Entry entry : ingredientCounts.entrySet()) { + countedIngredients.add(new CountedIngredient(entry.getKey(), entry.getValue())); + } + + AdvancedAlloyFurnaceRecipe convertedRecipe = new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(originalId), + countedIngredients, List.of(), + List.of(output.copy()), List.of(), + AdapterUtils.ae2csEnergyCost(recipe.energyCost()), 100, + Ingredient.EMPTY, 0, + CircuitEtcherRecipeAdapter.makeMold("crystal_pulverizer"), + AlloyFurnaceMode.NORMAL + ); + + result.add(convertedRecipe); + return result; + } + + @Override + @Nullable + public AdvancedAlloyFurnaceRecipe convert(RecipeHolder holder, Level level) { + List recipes = convertAll(holder, level); + return recipes.isEmpty() ? null : recipes.get(0); + } + + @Override + @Nullable + @SuppressWarnings("unchecked") + public List> findMatchingRecipes(Level level, Map mergedInputs, Map mergedFluids, @Nullable ItemStack mold) { + if (level == null) return List.of(); + if (!CircuitEtcherRecipeAdapter.checkMold(mold, "crystal_pulverizer")) return List.of(); + + RecipeManager recipeManager = level.getRecipeManager(); + List> matches = new java.util.ArrayList<>(); + for (RecipeHolder holder : (List>) (List) + com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, AECSRecipeTypes.CRYSTAL_PULVERIZER.get())) { + CrystalPulverizerRecipe recipe = holder.value(); + if (recipe.result().isEmpty()) continue; + + Map required = new LinkedHashMap<>(); + CircuitEtcherRecipeAdapter.addSizedIngredient(required, recipe.input()); + if (required.isEmpty()) continue; + + if (AdapterUtils.matchesRequired(mergedInputs, required)) matches.add(holder); + } + return matches; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2lt/AELightningIngredientHelper.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2lt/AELightningIngredientHelper.java new file mode 100644 index 00000000..df9928a5 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2lt/AELightningIngredientHelper.java @@ -0,0 +1,59 @@ +package com.sorrowmist.useless.content.recipe.adapters.ae.ae2lt; + +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import com.moakiee.ae2lt.me.key.LightningKey; +import com.moakiee.ae2lt.registry.ModItems; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import net.minecraft.world.item.crafting.Ingredient; + +import java.util.Map; + +final class AELightningIngredientHelper { + + private static final int EXTREME_TO_HIGH_RATIO = 4; + + private AELightningIngredientHelper() {} + + static GenericStack createLightningKeyInput(LightningKey.Tier tier, long amount) { + return new GenericStack(LightningKey.of(tier), Math.max(1, amount)); + } + + static boolean matchesLightning(Map mergedKeys, LightningKey.Tier tier, long amount) { + return mergedKeys.getOrDefault(LightningKey.of(tier), 0L) >= amount; + } + + static boolean matchesSimulationOrAssemblyLightning(Map mergedInputs, Map mergedKeys, LightningKey.Tier tier, long amount) { + if (amount <= 0) return false; + if (tier == LightningKey.Tier.HIGH_VOLTAGE) { + return mergedKeys.getOrDefault(LightningKey.HIGH_VOLTAGE, 0L) >= amount; + } + long extreme = mergedKeys.getOrDefault(LightningKey.EXTREME_HIGH_VOLTAGE, 0L); + if (extreme >= amount) { + return true; + } + return hasLightningCollapseMatrix(mergedInputs) + && mergedKeys.getOrDefault(LightningKey.HIGH_VOLTAGE, 0L) >= amount * EXTREME_TO_HIGH_RATIO; + } + + static boolean matchesOverloadLightning(Map mergedInputs, Map mergedKeys, LightningKey.Tier tier, long amount) { + if (amount <= 0) return false; + if (tier == LightningKey.Tier.HIGH_VOLTAGE) { + return mergedKeys.getOrDefault(LightningKey.HIGH_VOLTAGE, 0L) >= amount; + } + long extreme = mergedKeys.getOrDefault(LightningKey.EXTREME_HIGH_VOLTAGE, 0L); + if (extreme >= amount) { + return true; + } + if (!hasLightningCollapseMatrix(mergedInputs)) { + return false; + } + long remainingExtreme = amount - extreme; + long highVoltageNeeded = remainingExtreme * EXTREME_TO_HIGH_RATIO; + return highVoltageNeeded >= 0L && mergedKeys.getOrDefault(LightningKey.HIGH_VOLTAGE, 0L) >= highVoltageNeeded; + } + + private static boolean hasLightningCollapseMatrix(Map mergedInputs) { + return AdapterUtils.hasMatchingIngredient(mergedInputs, Ingredient.of(ModItems.LIGHTNING_COLLAPSE_MATRIX.get())); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2lt/AELightningTechCompatLoader.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2lt/AELightningTechCompatLoader.java new file mode 100644 index 00000000..3e66b879 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2lt/AELightningTechCompatLoader.java @@ -0,0 +1,73 @@ +package com.sorrowmist.useless.content.recipe.adapters.ae.ae2lt; + +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +/** Loads the optional AE2 Lightning Tech recipe bridge without linking it from core classes. */ +public final class AELightningTechCompatLoader { + private static final String PROVIDER_CLASS = + "com.sorrowmist.useless.content.recipe.adapters.ae.ae2lt.AELightningTechCompatProvider"; + + private AELightningTechCompatLoader() { + } + + public static List> createAdapters() { + Object result = invoke("createAdapters", new Class[0]); + if (!(result instanceof List rawAdapters)) { + throw new IllegalStateException("AE2 Lightning Tech compat provider returned invalid adapters"); + } + List> adapters = new ArrayList<>(rawAdapters.size()); + for (Object value : rawAdapters) { + if (!(value instanceof IRecipeAdapter adapter)) { + throw new IllegalStateException("AE2 Lightning Tech compat provider returned a non-adapter value"); + } + adapters.add(adapter); + } + return List.copyOf(adapters); + } + + public static List getJeiRecipes( + RecipeManager recipeManager, Level level) { + Object result = invoke( + "getJeiRecipes", + new Class[]{RecipeManager.class, Level.class}, + recipeManager, + level); + if (!(result instanceof List rawRecipes)) { + throw new IllegalStateException("AE2 Lightning Tech compat provider returned invalid JEI recipes"); + } + List recipes = new ArrayList<>(rawRecipes.size()); + for (Object value : rawRecipes) { + if (!(value instanceof AdvancedAlloyFurnaceRecipe recipe)) { + throw new IllegalStateException("AE2 Lightning Tech compat provider returned an invalid JEI recipe"); + } + recipes.add(recipe); + } + return List.copyOf(recipes); + } + + private static Object invoke(String methodName, Class[] parameterTypes, Object... arguments) { + try { + ClassLoader loader = AELightningTechCompatLoader.class.getClassLoader(); + Class provider = Class.forName(PROVIDER_CLASS, true, loader); + Method method = provider.getMethod(methodName, parameterTypes); + return method.invoke(null, arguments); + } catch (InvocationTargetException exception) { + Throwable cause = exception.getCause(); + throw new IllegalStateException( + "AE2 Lightning Tech compat provider failed in " + methodName, + cause == null ? exception : cause); + } catch (ReflectiveOperationException | LinkageError exception) { + throw new IllegalStateException( + "AE2 Lightning Tech compat provider could not be loaded for " + methodName, + exception); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2lt/AELightningTechCompatProvider.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2lt/AELightningTechCompatProvider.java new file mode 100644 index 00000000..dd1092e8 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2lt/AELightningTechCompatProvider.java @@ -0,0 +1,164 @@ +package com.sorrowmist.useless.content.recipe.adapters.ae.ae2lt; + +import com.moakiee.ae2lt.registry.ModItems; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeConversionUtils; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.Recipe; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.Container; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fluids.FluidStack; + +import java.util.ArrayList; +import java.util.List; + +import static com.moakiee.ae2lt.registry.ModRecipeTypes.CRYSTAL_CATALYZER_TYPE; +import static com.moakiee.ae2lt.registry.ModRecipeTypes.FIRMAMENT_CONVERSION_TYPE; +import static com.moakiee.ae2lt.registry.ModRecipeTypes.LIGHTNING_ASSEMBLY_TYPE; +import static com.moakiee.ae2lt.registry.ModRecipeTypes.LIGHTNING_SIMULATION_TYPE; +import static com.moakiee.ae2lt.registry.ModRecipeTypes.OVERLOAD_PROCESSING_TYPE; + +/** The only entry point that links optional AE2 Lightning Tech recipe types. */ +public final class AELightningTechCompatProvider { + private static final ResourceLocation ANNIHILATION_PLANE_ID = + new ResourceLocation("ae2", "annihilation_plane"); + private static final ResourceLocation OVERLOAD_TNT_ID = + new ResourceLocation("ae2lt", "overload_tnt"); + private static final int MATERIAL_WATER_AMOUNT = 1; + private static final long MATERIAL_ENERGY = 2000L; + private static final int MATERIAL_PROCESS_TIME = 200; + + private AELightningTechCompatProvider() { + } + + public static List> createAdapters() { + return List.of( + new LightningSimulationRecipeAdapter(), + new LightningAssemblyRecipeAdapter(), + new OverloadProcessingRecipeAdapter(), + new CrystalCatalyzerRecipeAdapter(), + new FirmamentConversionRecipeAdapter(), + new SteakLightningRecipeAdapter()); + } + + public static List getJeiRecipes( + RecipeManager recipeManager, Level level) { + List recipes = new ArrayList<>(); + addConvertedRecipes( + recipes, recipeManager, LIGHTNING_SIMULATION_TYPE.get(), + new LightningSimulationRecipeAdapter(), level); + addConvertedRecipes( + recipes, recipeManager, LIGHTNING_ASSEMBLY_TYPE.get(), + new LightningAssemblyRecipeAdapter(), level); + addConvertedRecipes( + recipes, recipeManager, OVERLOAD_PROCESSING_TYPE.get(), + new OverloadProcessingRecipeAdapter(), level); + addConvertedRecipes( + recipes, recipeManager, CRYSTAL_CATALYZER_TYPE.get(), + new CrystalCatalyzerRecipeAdapter(), level); + addConvertedRecipes( + recipes, recipeManager, FIRMAMENT_CONVERSION_TYPE.get(), + new FirmamentConversionRecipeAdapter(), level); + recipes.addAll(new SteakLightningRecipeAdapter().getAllRecipes()); + recipes.addAll(createMaterialRecipes()); + return recipes; + } + + private static List createMaterialRecipes() { + List recipes = new ArrayList<>(); + Item annihilationPlane = BuiltInRegistries.ITEM.getOptional(ANNIHILATION_PLANE_ID).orElse(null); + if (annihilationPlane != null && annihilationPlane != Items.AIR) { + List firmamentDustMolds = List.of( + Ingredient.of(Items.END_STONE), + Ingredient.of(annihilationPlane)); + List floatingMatterMolds = List.of( + Ingredient.of(Items.SHULKER_SPAWN_EGG), + Ingredient.of(annihilationPlane)); + + recipes.add(createMaterialRecipe( + new ResourceLocation( + "ae2lt", "firmament_dust_from_end_stone"), + new ItemStack(ModItems.FIRMAMENT_DUST.get()), + firmamentDustMolds)); + recipes.add(createMaterialRecipe( + new ResourceLocation( + "ae2lt", "floating_matter_from_shulker_spawn_egg"), + new ItemStack(ModItems.FLOATING_MATTER.get()), + floatingMatterMolds)); + } + + Item overloadTnt = BuiltInRegistries.ITEM.getOptional(OVERLOAD_TNT_ID).orElse(null); + if (overloadTnt != null && overloadTnt != Items.AIR) { + recipes.add(createItemRecipe( + new ResourceLocation( + "ae2lt", "research_note_from_lightning_collapse_matrix"), + List.of( + new CountedIngredient( + Ingredient.of(ModItems.LIGHTNING_COLLAPSE_MATRIX.get()), 1000L), + new CountedIngredient(Ingredient.of(overloadTnt), 1L)), + new ItemStack(ModItems.RESEARCH_NOTE.get()))); + } + + return List.copyOf(recipes); + } + + private static AdvancedAlloyFurnaceRecipe createMaterialRecipe( + ResourceLocation id, ItemStack output, List molds) { + return new AdvancedAlloyFurnaceRecipe( + id, + List.of(), + List.of(new FluidStack(Fluids.WATER, MATERIAL_WATER_AMOUNT)), + List.of(), + List.of(output), + List.of(), + List.of(), + MATERIAL_ENERGY, + MATERIAL_PROCESS_TIME, + Ingredient.EMPTY, + 0, + molds, + AlloyFurnaceMode.NORMAL); + } + + private static AdvancedAlloyFurnaceRecipe createItemRecipe( + ResourceLocation id, List inputs, + ItemStack output) { + return new AdvancedAlloyFurnaceRecipe( + id, + inputs, + List.of(), + List.of(), + List.of(output), + List.of(), + List.of(), + MATERIAL_ENERGY, + MATERIAL_PROCESS_TIME, + Ingredient.EMPTY, + 0, + List.of(), + AlloyFurnaceMode.NORMAL); + } + + private static > void addConvertedRecipes( + List convertedRecipes, + RecipeManager recipeManager, + RecipeType recipeType, + IRecipeAdapter adapter, + Level level) { + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, recipeType)) { + convertedRecipes.addAll(RecipeConversionUtils.convertAll(adapter, holder, level)); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2lt/CrystalCatalyzerRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2lt/CrystalCatalyzerRecipeAdapter.java new file mode 100644 index 00000000..f1536763 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2lt/CrystalCatalyzerRecipeAdapter.java @@ -0,0 +1,173 @@ +package com.sorrowmist.useless.content.recipe.adapters.ae.ae2lt; + +import appeng.api.stacks.AEKey; +import com.moakiee.ae2lt.machine.crystalcatalyzer.recipe.CrystalCatalyzerOutput; +import com.moakiee.ae2lt.machine.crystalcatalyzer.recipe.CrystalCatalyzerRecipe; +import com.moakiee.ae2lt.machine.crystalcatalyzer.recipe.Mode; +import com.moakiee.ae2lt.registry.ModRecipeTypes; +import com.moakiee.ae2lt.registry.ModBlocks; +import com.moakiee.ae2lt.registry.ModItems; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** Converts AE2 Lightning Tech 2.0 crystal-catalyzer recipes. */ +public class CrystalCatalyzerRecipeAdapter implements IRecipeAdapter { + + private static final int BASE_OUTPUT_MULTIPLIER = 256; + private static final int MATRIX_OUTPUT_MULTIPLIER = 1024; + private static final int BASE_PROCESS_TIME = 200; + private static final int WATER_PER_CYCLE = 1000; + + @Override + public Class getRecipeClass() { + return CrystalCatalyzerRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(ModBlocks.CRYSTAL_CATALYZER.get()); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder == null) return List.of(); + + CrystalCatalyzerRecipe recipe = holder.value(); + if (!supportsMode(recipe.mode())) return List.of(); + + Optional catalyst = recipe.catalyst(); + if (catalyst.isPresent() && recipe.catalystCount() <= 0) return List.of(); + + FluidStack waterInput = new FluidStack(Fluids.WATER, WATER_PER_CYCLE); + var keyInputs = List.of(AELightningIngredientHelper.createLightningKeyInput( + recipe.lightningTier(), (long) recipe.lightningCost())); + + List baseMolds = molds(catalyst, false); + ItemStack baseOutput = resolveOutput(recipe, BASE_OUTPUT_MULTIPLIER); + if (baseOutput.isEmpty()) return List.of(); + + ResourceLocation baseId = AdapterUtils.convertedId(holder.id()); + AdvancedAlloyFurnaceRecipe base = createRecipe( + baseId, baseOutput, waterInput, keyInputs, baseMolds, recipe); + List result = new ArrayList<>(); + result.add(base); + List matrixMolds = molds(catalyst, true); + ItemStack matrixOutput = resolveOutput(recipe, MATRIX_OUTPUT_MULTIPLIER); + if (matrixOutput.isEmpty()) return List.of(); + result.add(createRecipe( + new ResourceLocation( + baseId.getNamespace(), baseId.getPath() + "_with_collapse_matrix"), + matrixOutput, waterInput, keyInputs, matrixMolds, recipe)); + return List.copyOf(result); + } + + @Override + @Nullable + @SuppressWarnings("unchecked") + public List> findMatchingRecipes( + Level level, + Map mergedInputs, + Map mergedFluids, + Map mergedKeys, + @Nullable ItemStack mold) { + if (level == null || mold == null || mold.isEmpty() || !matchesMold(mold)) return List.of(); + + RecipeManager recipeManager = level.getRecipeManager(); + List> recipes = + (List>) (List) com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + ModRecipeTypes.CRYSTAL_CATALYZER_TYPE.get()); + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : recipes) { + CrystalCatalyzerRecipe recipe = holder.value(); + + if (!supportsMode(recipe.mode())) continue; + + Optional catalyst = recipe.catalyst(); + if (catalyst.isPresent() && recipe.catalystCount() <= 0) continue; + if (!matchesFluid(mergedFluids, WATER_PER_CYCLE)) continue; + if (!AELightningIngredientHelper.matchesLightning( + mergedKeys == null ? Map.of() : mergedKeys, + recipe.lightningTier(), (long) recipe.lightningCost())) continue; + if (resolveOutput(recipe, BASE_OUTPUT_MULTIPLIER).isEmpty()) continue; + + matches.add(holder); + } + return matches; + } + + private static boolean supportsMode(Mode mode) { + return mode == Mode.CRYSTAL || mode == Mode.DUST; + } + + private static AdvancedAlloyFurnaceRecipe createRecipe( + ResourceLocation id, + ItemStack output, + FluidStack waterInput, + List keyInputs, + List molds, + CrystalCatalyzerRecipe source) { + return new AdvancedAlloyFurnaceRecipe( + id, + List.of(), + List.of(waterInput.copy()), + keyInputs, + List.of(output), + List.of(), + List.of(), + Math.max(1, source.energyPerCycle()), + BASE_PROCESS_TIME, + Ingredient.EMPTY, + 0, + molds, + AlloyFurnaceMode.NORMAL); + } + + private static List molds(Optional catalyst, boolean withMatrix) { + List molds = new ArrayList<>(); + molds.add(AdapterUtils.toMoldIngredient(new ItemStack(ModBlocks.CRYSTAL_CATALYZER.get()))); + catalyst.ifPresent(molds::add); + // A source recipe without a catalyst already uses the crystal catalyzer as its + // only mold. Keep the matrix variant selectable without inventing a new mold + // requirement for that recipe shape. + if (withMatrix && catalyst.isPresent()) { + molds.add(AdapterUtils.toMoldIngredient( + new ItemStack(ModItems.LIGHTNING_COLLAPSE_MATRIX.get()))); + } + return List.copyOf(molds); + } + + private static ItemStack resolveOutput(CrystalCatalyzerRecipe recipe, int multiplier) { + CrystalCatalyzerOutput outputSpec = recipe.outputSpec(); + ItemStack resolved = outputSpec.resolve(); + if (resolved.isEmpty() || outputSpec.count() <= 0 + || outputSpec.count() > Integer.MAX_VALUE / multiplier) { + return ItemStack.EMPTY; + } + return resolved.copyWithCount(outputSpec.count() * multiplier); + } + + private static boolean matchesFluid(Map mergedFluids, int amount) { + if (mergedFluids == null || mergedFluids.isEmpty()) return false; + FluidStack required = new FluidStack(Fluids.WATER, amount); + return mergedFluids.entrySet().stream() + .anyMatch(entry -> entry.getValue() >= amount + && com.sorrowmist.useless.compat.forge1201.fluid.FluidStacks.same(entry.getKey(), required)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2lt/FirmamentConversionRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2lt/FirmamentConversionRecipeAdapter.java new file mode 100644 index 00000000..43e8cda3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2lt/FirmamentConversionRecipeAdapter.java @@ -0,0 +1,133 @@ +package com.sorrowmist.useless.content.recipe.adapters.ae.ae2lt; + +import appeng.api.stacks.AEKey; +import com.moakiee.ae2lt.machine.firmament.recipe.FirmamentConversionIngredient; +import com.moakiee.ae2lt.machine.firmament.recipe.FirmamentConversionRecipe; +import com.moakiee.ae2lt.registry.ModRecipeTypes; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class FirmamentConversionRecipeAdapter implements IRecipeAdapter { + + private static final int ENERGY_PER_TICK = 20; + + @Override + public Class getRecipeClass() { + return FirmamentConversionRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return new ItemStack( + BuiltInRegistries.ITEM.get( + new ResourceLocation("ae2lt", "firmament_conversion_core"))); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + List result = new ArrayList<>(); + + if (holder == null) return result; + + FirmamentConversionRecipe recipe = holder.value(); + ResourceLocation originalId = holder.id(); + List inputs = recipe.inputs(); + List outputs = recipe.getResultStacks(); + + if (inputs.isEmpty() || outputs.isEmpty()) { + return result; + } + + Map ingredientCounts = new LinkedHashMap<>(); + for (FirmamentConversionIngredient input : inputs) { + AdapterUtils.mergeIngredient(ingredientCounts, input.ingredient(), input.count()); + } + + List countedIngredients = new ArrayList<>(); + for (Map.Entry entry : ingredientCounts.entrySet()) { + countedIngredients.add(new CountedIngredient(entry.getKey(), entry.getValue())); + } + + Ingredient moldIngredient = Ingredient.of(new ItemStack( + BuiltInRegistries.ITEM.get( + new ResourceLocation("ae2lt", "firmament_conversion_core") + ) + )); + + AdvancedAlloyFurnaceRecipe convertedRecipe = new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(originalId), + countedIngredients, + List.of(), + List.of(), + outputs.stream().map(ItemStack::copy).toList(), + List.of(), + List.of(), + Math.max(1, recipe.processTime() * ENERGY_PER_TICK), + recipe.processTime(), + Ingredient.EMPTY, + 0, + moldIngredient, + AlloyFurnaceMode.NORMAL + ); + + result.add(convertedRecipe); + return result; + } + + @Override + @Nullable + @SuppressWarnings("unchecked") + public List> findMatchingRecipes(Level level, Map mergedInputs, Map mergedFluids, Map mergedKeys, @Nullable ItemStack mold) { + if (level == null || mergedInputs.isEmpty()) return List.of(); + + if (mold != null && !mold.isEmpty()) { + ResourceLocation moldId = BuiltInRegistries.ITEM.getKey(mold.getItem()); + if (!"ae2lt".equals(moldId.getNamespace()) || !"firmament_conversion_core".equals(moldId.getPath())) return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + List> recipes = new ArrayList<>(com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, ModRecipeTypes.FIRMAMENT_CONVERSION_TYPE.get())); + recipes.sort(Comparator + .>comparingInt(holder -> holder.value().priority()).reversed() + .thenComparing(holder -> holder.value().inputs().size(), Comparator.reverseOrder()) + .thenComparing(holder -> holder.value().totalInputCount(), Comparator.reverseOrder()) + .thenComparing(holder -> holder.id().toString())); + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : recipes) { + FirmamentConversionRecipe recipe = holder.value(); + List recipeInputs = recipe.inputs(); + List outputs = recipe.getResultStacks(); + if (recipeInputs.isEmpty() || outputs.isEmpty()) continue; + + Map requiredCounts = new LinkedHashMap<>(); + for (FirmamentConversionIngredient input : recipeInputs) { + AdapterUtils.mergeIngredient(requiredCounts, input.ingredient(), input.count()); + } + + if (AdapterUtils.matchesRequired(mergedInputs, requiredCounts)) { + matches.add(holder); + } + } + return matches; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2lt/LightningAssemblyRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2lt/LightningAssemblyRecipeAdapter.java new file mode 100644 index 00000000..aff02b4f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2lt/LightningAssemblyRecipeAdapter.java @@ -0,0 +1,140 @@ +package com.sorrowmist.useless.content.recipe.adapters.ae.ae2lt; + +import appeng.api.stacks.AEKey; +import com.moakiee.ae2lt.machine.lightningassembly.recipe.LightningAssemblyRecipe; +import com.moakiee.ae2lt.machine.lightningchamber.recipe.LightningSimulationIngredient; +import com.moakiee.ae2lt.registry.ModRecipeTypes; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * AE2 Lightning Tech 闪电装配室配方适配器 + *

+ * 将闪电装配室配方转换为高级合金熔炉配方 + *

+ * 处理逻辑: + * - 所有输入物品 → 普通输入(合并相同物品) + * - 产物 → 输出 + * - ae2lt:lightning_assembly_chamber → 模具(不消耗) + */ +public class LightningAssemblyRecipeAdapter implements IRecipeAdapter { + + private static final int BASE_PROCESS_TIME = 80; + + @Override + public Class getRecipeClass() { + return LightningAssemblyRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return new ItemStack( + BuiltInRegistries.ITEM.get( + new ResourceLocation("ae2lt", "lightning_assembly_chamber"))); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + List result = new ArrayList<>(); + + if (holder == null) return result; + + LightningAssemblyRecipe recipe = holder.value(); + ResourceLocation originalId = holder.id(); + + List inputs = recipe.inputs(); + ItemStack output = recipe.getResultStack(); + + if (output.isEmpty() || inputs.isEmpty()) { + return result; + } + + Map ingredientCounts = new LinkedHashMap<>(); + for (LightningSimulationIngredient input : inputs) { + AdapterUtils.mergeIngredient(ingredientCounts, input.ingredient(), input.count()); + } + + List countedIngredients = new ArrayList<>(); + for (Map.Entry entry : ingredientCounts.entrySet()) { + countedIngredients.add(new CountedIngredient(entry.getKey(), entry.getValue())); + } + var keyInputs = List.of(AELightningIngredientHelper.createLightningKeyInput(recipe.lightningTier(), recipe.lightningCost())); + + int processTime = BASE_PROCESS_TIME + inputs.size() * 15; + + Ingredient moldIngredient = Ingredient.of(new ItemStack( + BuiltInRegistries.ITEM.get( + new ResourceLocation("ae2lt", "lightning_assembly_chamber") + ) + )); + + AdvancedAlloyFurnaceRecipe convertedRecipe = new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(originalId), + countedIngredients, + List.of(), + keyInputs, + List.of(output.copy()), + List.of(), + List.of(), + 4000, + processTime, + Ingredient.EMPTY, + 0, + moldIngredient, + AlloyFurnaceMode.NORMAL + ); + + result.add(convertedRecipe); + return result; + } + + @Override + @Nullable + @SuppressWarnings("unchecked") + public List> findMatchingRecipes(Level level, Map mergedInputs, Map mergedFluids, Map mergedKeys, @Nullable ItemStack mold) { + if (level == null || mergedInputs.isEmpty()) return List.of(); + + if (mold != null && !mold.isEmpty()) { + ResourceLocation moldId = BuiltInRegistries.ITEM.getKey(mold.getItem()); + if (!"ae2lt".equals(moldId.getNamespace()) || !"lightning_assembly_chamber".equals(moldId.getPath())) return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + List> matches = new java.util.ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, ModRecipeTypes.LIGHTNING_ASSEMBLY_TYPE.get())) { + LightningAssemblyRecipe recipe = holder.value(); + List recipeInputs = recipe.inputs(); + ItemStack output = recipe.getResultStack(); + if (output.isEmpty() || recipeInputs.isEmpty()) continue; + + Map requiredCounts = new LinkedHashMap<>(); + for (LightningSimulationIngredient input : recipeInputs) { + AdapterUtils.mergeIngredient(requiredCounts, input.ingredient(), input.count()); + } + + if (AdapterUtils.matchesRequired(mergedInputs, requiredCounts) + && AELightningIngredientHelper.matchesSimulationOrAssemblyLightning(mergedInputs, mergedKeys, recipe.lightningTier(), recipe.lightningCost())) { + matches.add(holder); + } + } + return matches; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2lt/LightningSimulationRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2lt/LightningSimulationRecipeAdapter.java new file mode 100644 index 00000000..6c6ba940 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2lt/LightningSimulationRecipeAdapter.java @@ -0,0 +1,140 @@ +package com.sorrowmist.useless.content.recipe.adapters.ae.ae2lt; + +import appeng.api.stacks.AEKey; +import com.moakiee.ae2lt.machine.lightningchamber.recipe.LightningSimulationIngredient; +import com.moakiee.ae2lt.machine.lightningchamber.recipe.LightningSimulationRecipe; +import com.moakiee.ae2lt.registry.ModRecipeTypes; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * AE2 Lightning Tech 闪电模拟室配方适配器 + *

+ * 将闪电模拟室配方转换为高级合金熔炉配方 + *

+ * 处理逻辑: + * - 所有输入物品 → 普通输入(合并相同物品) + * - 产物 → 输出 + * - ae2lt:lightning_simulation_room → 模具(不消耗) + */ +public class LightningSimulationRecipeAdapter implements IRecipeAdapter { + + private static final int BASE_PROCESS_TIME = 60; + + @Override + public Class getRecipeClass() { + return LightningSimulationRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return new ItemStack( + BuiltInRegistries.ITEM.get( + new ResourceLocation("ae2lt", "lightning_simulation_room"))); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + List result = new ArrayList<>(); + + if (holder == null) return result; + + LightningSimulationRecipe recipe = holder.value(); + ResourceLocation originalId = holder.id(); + + List inputs = recipe.inputs(); + ItemStack output = recipe.getResultStack(); + + if (output.isEmpty() || inputs.isEmpty()) { + return result; + } + + Map ingredientCounts = new LinkedHashMap<>(); + for (LightningSimulationIngredient input : inputs) { + AdapterUtils.mergeIngredient(ingredientCounts, input.ingredient(), input.count()); + } + + List countedIngredients = new ArrayList<>(); + for (Map.Entry entry : ingredientCounts.entrySet()) { + countedIngredients.add(new CountedIngredient(entry.getKey(), entry.getValue())); + } + var keyInputs = List.of(AELightningIngredientHelper.createLightningKeyInput(recipe.lightningTier(), recipe.lightningCost())); + + int processTime = BASE_PROCESS_TIME + inputs.size() * 20; + + Ingredient moldIngredient = Ingredient.of(new ItemStack( + BuiltInRegistries.ITEM.get( + new ResourceLocation("ae2lt", "lightning_simulation_room") + ) + )); + + AdvancedAlloyFurnaceRecipe convertedRecipe = new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(originalId), + countedIngredients, + List.of(), + keyInputs, + List.of(output.copy()), + List.of(), + List.of(), + 2000, + processTime, + Ingredient.EMPTY, + 0, + moldIngredient, + AlloyFurnaceMode.NORMAL + ); + + result.add(convertedRecipe); + return result; + } + + @Override + @Nullable + @SuppressWarnings("unchecked") + public List> findMatchingRecipes(Level level, Map mergedInputs, Map mergedFluids, Map mergedKeys, @Nullable ItemStack mold) { + if (level == null || mergedInputs.isEmpty()) return List.of(); + + if (mold != null && !mold.isEmpty()) { + ResourceLocation moldId = BuiltInRegistries.ITEM.getKey(mold.getItem()); + if (!"ae2lt".equals(moldId.getNamespace()) || !"lightning_simulation_room".equals(moldId.getPath())) return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + List> matches = new java.util.ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, ModRecipeTypes.LIGHTNING_SIMULATION_TYPE.get())) { + LightningSimulationRecipe recipe = holder.value(); + List recipeInputs = recipe.inputs(); + ItemStack output = recipe.getResultStack(); + if (output.isEmpty() || recipeInputs.isEmpty()) continue; + + Map requiredCounts = new LinkedHashMap<>(); + for (LightningSimulationIngredient input : recipeInputs) { + AdapterUtils.mergeIngredient(requiredCounts, input.ingredient(), input.count()); + } + + if (AdapterUtils.matchesRequired(mergedInputs, requiredCounts) + && AELightningIngredientHelper.matchesSimulationOrAssemblyLightning(mergedInputs, mergedKeys, recipe.lightningTier(), recipe.lightningCost())) { + matches.add(holder); + } + } + return matches; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2lt/OverloadProcessingRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2lt/OverloadProcessingRecipeAdapter.java new file mode 100644 index 00000000..39765442 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2lt/OverloadProcessingRecipeAdapter.java @@ -0,0 +1,173 @@ +package com.sorrowmist.useless.content.recipe.adapters.ae.ae2lt; + +import appeng.api.stacks.AEKey; +import com.moakiee.ae2lt.machine.overloadfactory.recipe.OverloadProcessingIngredient; +import com.moakiee.ae2lt.machine.overloadfactory.recipe.OverloadProcessingRecipe; +import com.moakiee.ae2lt.registry.ModRecipeTypes; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * AE2 Lightning Tech 过载处理工厂配方适配器 + *

+ * 将过载处理工厂配方转换为高级合金熔炉配方 + *

+ * 处理逻辑: + * - 物品输入 → 普通输入(合并相同物品) + * - 流体输入 → 流体输入 + * - 物品产物 → 输出 + * - 流体产物 → 流体输出 + * - ae2lt:overload_processing_factory → 模具(不消耗) + */ +public class OverloadProcessingRecipeAdapter implements IRecipeAdapter { + + private static final int BASE_PROCESS_TIME = 100; + + @Override + public Class getRecipeClass() { + return OverloadProcessingRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return new ItemStack( + BuiltInRegistries.ITEM.get( + new ResourceLocation("ae2lt", "overload_processing_factory"))); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + List result = new ArrayList<>(); + + if (holder == null) return result; + + OverloadProcessingRecipe recipe = holder.value(); + ResourceLocation originalId = holder.id(); + + List itemInputs = recipe.itemInputs(); + FluidStack fluidInput = recipe.fluidInput(); + List itemResults = recipe.itemResults(); + FluidStack fluidResult = recipe.fluidResult(); + + boolean hasItemInputs = !itemInputs.isEmpty(); + boolean hasFluidInput = !fluidInput.isEmpty(); + boolean hasItemOutputs = !itemResults.isEmpty(); + boolean hasFluidOutput = !fluidResult.isEmpty(); + + // 只要有输入并且有输出就可以转换,不管是物品还是流体 + if (!hasItemInputs && !hasFluidInput) { + return result; + } + if (!hasItemOutputs && !hasFluidOutput) { + return result; + } + + Map ingredientCounts = new LinkedHashMap<>(); + for (OverloadProcessingIngredient input : itemInputs) { + AdapterUtils.mergeIngredient(ingredientCounts, input.ingredient(), input.count()); + } + + List countedIngredients = new ArrayList<>(); + for (Map.Entry entry : ingredientCounts.entrySet()) { + countedIngredients.add(new CountedIngredient(entry.getKey(), entry.getValue())); + } + var keyInputs = List.of(AELightningIngredientHelper.createLightningKeyInput(recipe.lightningTier(), recipe.lightningCost())); + + var sizedFluid = hasFluidInput ? AdapterUtils.toSizedFluidIngredient(fluidInput) : null; + List inputFluids = sizedFluid == null ? List.of() : List.of(sizedFluid); + List outputs = hasItemOutputs ? itemResults.stream().map(ItemStack::copy).toList() : List.of(); + List outputFluids = hasFluidOutput ? List.of(fluidResult.copy()) : List.of(); + + int processTime = BASE_PROCESS_TIME; + long energy = recipe.totalEnergy(); + int scaledEnergy = energy > Integer.MAX_VALUE ? 10000 : (int) energy; + + Ingredient moldIngredient = Ingredient.of(new ItemStack( + BuiltInRegistries.ITEM.get( + new ResourceLocation("ae2lt", "overload_processing_factory") + ) + )); + + AdvancedAlloyFurnaceRecipe convertedRecipe = new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(originalId), + countedIngredients, + inputFluids, + keyInputs, + outputs, + outputFluids, + List.of(), + scaledEnergy, + processTime, + Ingredient.EMPTY, + 0, + moldIngredient, + AlloyFurnaceMode.NORMAL + ); + + result.add(convertedRecipe); + return result; + } + + @Override + @Nullable + @SuppressWarnings("unchecked") + public List> findMatchingRecipes(Level level, Map mergedInputs, Map mergedFluids, Map mergedKeys, @Nullable ItemStack mold) { + if (level == null) return List.of(); + + if (mold != null && !mold.isEmpty()) { + ResourceLocation moldId = BuiltInRegistries.ITEM.getKey(mold.getItem()); + if (!"ae2lt".equals(moldId.getNamespace()) || !"overload_processing_factory".equals(moldId.getPath())) return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + List> matches = new java.util.ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, ModRecipeTypes.OVERLOAD_PROCESSING_TYPE.get())) { + OverloadProcessingRecipe recipe = holder.value(); + + List recipeItemInputs = recipe.itemInputs(); + FluidStack recipeFluidInput = recipe.fluidInput(); + + // 检查物品输入匹配 + boolean itemsMatch = true; + if (!recipeItemInputs.isEmpty()) { + Map requiredCounts = new LinkedHashMap<>(); + for (OverloadProcessingIngredient input : recipeItemInputs) { + AdapterUtils.mergeIngredient(requiredCounts, input.ingredient(), input.count()); + } + itemsMatch = AdapterUtils.matchesRequired(mergedInputs, requiredCounts); + } + + // 检查流体输入匹配(使用已合并的流体数据) + boolean fluidsMatch = recipeFluidInput.isEmpty() + || (AdapterUtils.toSizedFluidIngredient(recipeFluidInput) != null + && com.sorrowmist.useless.content.recipe.FluidIngredientAllocator.matches( + List.of(AdapterUtils.toSizedFluidIngredient(recipeFluidInput)), + mergedFluids, 1L)); + + if ((recipeItemInputs.isEmpty() || itemsMatch) + && (recipeFluidInput.isEmpty() || fluidsMatch) + && AELightningIngredientHelper.matchesOverloadLightning(mergedInputs, mergedKeys, recipe.lightningTier(), recipe.lightningCost())) { + matches.add(holder); + } + } + return matches; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2lt/SteakLightningRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2lt/SteakLightningRecipeAdapter.java new file mode 100644 index 00000000..8a3c6ef5 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/ae2lt/SteakLightningRecipeAdapter.java @@ -0,0 +1,158 @@ +package com.sorrowmist.useless.content.recipe.adapters.ae.ae2lt; + +import appeng.api.stacks.GenericStack; +import com.moakiee.ae2lt.me.key.LightningKey; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.items.BeefToolVariants; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.Recipe; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.Container; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.ItemLike; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Map; + +/** + * AE2 Lightning Tech 牛排工具闪电配方适配器 + *

+ * 使用牛排工具(EndlessBeafItem,任意变体/状态)作为模具,额外提供两个配方: + * - 输入 1mb 水 → 输出 1 个高压闪电到 AE 网络 + * - 输入 1mb 熔岩 → 输出 1 个极高压闪电到 AE 网络 + *

+ * 牛排工具没有固定单一物品,作为 fallback 适配器通过 {@link #matchesMold(ItemStack)} 动态判断。 + */ +public class SteakLightningRecipeAdapter implements IRecipeAdapter { + + private static final int ENERGY = 2000; + private static final int PROCESS_TIME = 100; + + private final AdvancedAlloyFurnaceRecipe waterRecipe; + private final AdvancedAlloyFurnaceRecipe lavaRecipe; + + public SteakLightningRecipeAdapter() { + Ingredient moldIngredient = createSteakMold(); + + this.waterRecipe = new AdvancedAlloyFurnaceRecipe( + new ResourceLocation("ae2lt", "steak_water_high_voltage_lightning"), + List.of(), + List.of(new FluidStack(Fluids.WATER, 1)), + List.of(), + List.of(), + List.of(), + List.of(new GenericStack(LightningKey.HIGH_VOLTAGE, 1)), + ENERGY, + PROCESS_TIME, + Ingredient.EMPTY, + 0, + moldIngredient, + AlloyFurnaceMode.NORMAL + ); + + this.lavaRecipe = new AdvancedAlloyFurnaceRecipe( + new ResourceLocation("ae2lt", "steak_lava_extreme_high_voltage_lightning"), + List.of(), + List.of(new FluidStack(Fluids.LAVA, 1)), + List.of(), + List.of(), + List.of(), + List.of(new GenericStack(LightningKey.EXTREME_HIGH_VOLTAGE, 1)), + ENERGY, + PROCESS_TIME, + Ingredient.EMPTY, + 0, + moldIngredient, + AlloyFurnaceMode.NORMAL + ); + } + + private static Ingredient createSteakMold() { + return Ingredient.of( + BeefToolVariants.allVariantItems().toArray(ItemLike[]::new)); + } + + public List getAllRecipes() { + return List.of(waterRecipe, lavaRecipe); + } + + @Override + public Class getRecipeClass() { + return SteakLightningDummyRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return null; // 牛排工具有多个变体,作为 fallback 适配器动态匹配 + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + return BeefToolVariants.isBeafTool(mold); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + return List.of(holder.value().convertedRecipe); + } + + @Override + @Nullable + public AdvancedAlloyFurnaceRecipe convert(RecipeHolder holder, Level level) { + return holder.value().convertedRecipe; + } + + @Override + @Nullable + public List> findMatchingRecipes(Level level, Map mergedInputs, Map mergedFluids, @Nullable ItemStack mold) { + if (level == null) return List.of(); + if (!matchesMold(mold)) return List.of(); + if (mergedFluids == null || mergedFluids.isEmpty()) return List.of(); + + List> matches = new java.util.ArrayList<>(); + if (hasFluid(mergedFluids, Fluids.WATER)) { + matches.add(new RecipeHolder<>(waterRecipe.id(), new SteakLightningDummyRecipe(waterRecipe))); + } + if (hasFluid(mergedFluids, Fluids.LAVA)) { + matches.add(new RecipeHolder<>(lavaRecipe.id(), new SteakLightningDummyRecipe(lavaRecipe))); + } + return matches; + } + + private static boolean hasFluid(Map mergedFluids, Fluid fluid) { + for (Map.Entry entry : mergedFluids.entrySet()) { + if (entry.getKey().getFluid().isSame(fluid) && entry.getValue() >= 1) { + return true; + } + } + return false; + } + + public static class SteakLightningDummyRecipe implements Recipe { + final AdvancedAlloyFurnaceRecipe convertedRecipe; + + SteakLightningDummyRecipe(AdvancedAlloyFurnaceRecipe r) { this.convertedRecipe = r; } + + @Override + public net.minecraft.resources.ResourceLocation getId() { return convertedRecipe.id(); } + + @Override public boolean matches(Container input, Level level) { return false; } + @Override public ItemStack assemble(Container input, net.minecraft.core.RegistryAccess registries) { return ItemStack.EMPTY; } + @Override public boolean canCraftInDimensions(int w, int h) { return false; } + @Override public ItemStack getResultItem(net.minecraft.core.RegistryAccess registries) { return ItemStack.EMPTY; } + @Override public RecipeSerializer getSerializer() { return null; } + @Override public RecipeType getType() { return null; } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/dataenergistics/DataReassemblerRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/dataenergistics/DataReassemblerRecipeAdapter.java new file mode 100644 index 00000000..eb691b66 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/dataenergistics/DataReassemblerRecipeAdapter.java @@ -0,0 +1,256 @@ +package com.sorrowmist.useless.content.recipe.adapters.ae.dataenergistics; + +import appeng.api.stacks.AEFluidKey; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import com.fish_dan_.data_energistics.recipe.reassembler.DataRipperReassemblerIngredient; +import com.fish_dan_.data_energistics.recipe.reassembler.DataRipperReassemblerRecipe; +import com.fish_dan_.data_energistics.registry.DERecipes; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.FluidIngredientAllocator; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * DataEnergistics 数据重组器配方适配器 + *

+ * 将数据重组器配方转换为高级合金熔炉配方 + *

+ * 处理逻辑: + * - 物品输入 → 普通输入(合并相同物品) + * - 流体输入 → 流体输入 + * - 物品产物 → 输出 + * - 流体产物 → 流体输出 + * - keyInput → AEKey 输入 + * - keyOutput → AEKey 输出 + * - data_energistics:data_reassembler → 模具(不消耗) + */ +public class DataReassemblerRecipeAdapter implements IRecipeAdapter { + + private static final int BASE_PROCESS_TIME = 200; + private static final int BASE_ENERGY = 2000; + + @Override + public Class getRecipeClass() { + return DataRipperReassemblerRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return new ItemStack( + BuiltInRegistries.ITEM.get( + new ResourceLocation("data_energistics", "data_reassembler"))); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + List result = new ArrayList<>(); + + if (holder == null) return result; + + DataRipperReassemblerRecipe recipe = holder.value(); + ResourceLocation originalId = holder.id(); + + // 获取输入 + List itemInputs = recipe.getItemInputs(); + List fluidInputs = recipe.getFluidInputs(); + GenericStack keyInput = recipe.getKeyInput(); + + // 获取输出 + List itemOutputs = recipe.getItemOutputs(); + List fluidOutputs = recipe.getFluidOutputs(); + GenericStack keyOutput = recipe.getKeyOutput(); + + boolean hasItemInputs = !itemInputs.isEmpty(); + boolean hasFluidInputs = !fluidInputs.isEmpty(); + boolean hasKeyInput = keyInput != null && keyInput.amount() > 0; + boolean hasItemOutputs = !itemOutputs.isEmpty(); + boolean hasFluidOutputs = !fluidOutputs.isEmpty(); + boolean hasKeyOutput = keyOutput != null && keyOutput.amount() > 0; + + // 只要有输入并且有输出就可以转换 + if (!hasItemInputs && !hasFluidInputs && !hasKeyInput) { + return result; + } + if (!hasItemOutputs && !hasFluidOutputs && !hasKeyOutput) { + return result; + } + + // 合并物品输入 + Map ingredientCounts = new LinkedHashMap<>(); + for (DataRipperReassemblerIngredient input : itemInputs) { + AdapterUtils.mergeIngredient(ingredientCounts, input.ingredient(), input.count()); + } + + List countedIngredients = new ArrayList<>(); + for (Map.Entry entry : ingredientCounts.entrySet()) { + countedIngredients.add(new CountedIngredient(entry.getKey(), entry.getValue())); + } + + List keyInputs = hasKeyInput ? List.of(keyInput) : List.of(); + + // 转换流体输入 + List inputFluids = new ArrayList<>(); + for (GenericStack fluidInput : fluidInputs) { + if (fluidInput.what() instanceof AEFluidKey fluidKey && fluidInput.amount() > 0) { + int amount = fluidInput.amount() > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) fluidInput.amount(); + SizedFluidIngredient ingredient = AdapterUtils.toSizedFluidIngredient(fluidKey.toStack(amount)); + if (ingredient != null) inputFluids.add(ingredient); + } + } + + // 转换物品输出 + List outputs = new ArrayList<>(); + for (ItemStack itemOutput : itemOutputs) { + if (!itemOutput.isEmpty()) { + outputs.add(itemOutput.copy()); + } + } + + List keyOutputs = hasKeyOutput ? List.of(keyOutput) : List.of(); + + // 转换流体输出 + List outputFluids = new ArrayList<>(); + for (GenericStack fluidOutput : fluidOutputs) { + if (fluidOutput.what() instanceof AEFluidKey fluidKey && fluidOutput.amount() > 0) { + int amount = fluidOutput.amount() > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) fluidOutput.amount(); + outputFluids.add(fluidKey.toStack(amount)); + } + } + + int processTime = recipe.getProcessTicks(); + int energy = BASE_ENERGY + (itemInputs.size() + fluidInputs.size()) * 200; + if (hasKeyInput) energy += 500; + if (hasKeyOutput) energy += 500; + + // 使用数据重组器本身作为模具 + Ingredient moldIngredient = Ingredient.of(new ItemStack( + BuiltInRegistries.ITEM.get( + new ResourceLocation("data_energistics", "data_reassembler") + ) + )); + + AdvancedAlloyFurnaceRecipe convertedRecipe = new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(originalId), + countedIngredients, + inputFluids, + keyInputs, + outputs, + outputFluids, + keyOutputs, + energy, + processTime, + Ingredient.EMPTY, + 0, + moldIngredient, + AlloyFurnaceMode.NORMAL + ); + + result.add(convertedRecipe); + return result; + } + + @Override + @Nullable + @SuppressWarnings("unchecked") + public List> findMatchingRecipes(Level level, Map mergedInputs, Map mergedFluids, Map mergedKeys, @Nullable ItemStack mold) { + if (level == null) return List.of(); + + // 检查模具是否匹配数据重组器 + if (mold != null && !mold.isEmpty()) { + ResourceLocation moldId = BuiltInRegistries.ITEM.getKey(mold.getItem()); + if (!"data_energistics".equals(moldId.getNamespace()) || !"data_reassembler".equals(moldId.getPath())) return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + List> recipes = (List>) (List) com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + DERecipes.DATA_RIPPER_REASSEMBLER_TYPE.get() + ); + + List> matches = new ArrayList<>(); + + for (RecipeHolder holder : recipes) { + DataRipperReassemblerRecipe recipe = holder.value(); + + List recipeItemInputs = recipe.getItemInputs(); + List recipeFluidInputs = recipe.getFluidInputs(); + GenericStack keyInput = recipe.getKeyInput(); + + // 构建 requiredCounts + Map requiredCounts = new LinkedHashMap<>(); + for (DataRipperReassemblerIngredient input : recipeItemInputs) { + AdapterUtils.mergeIngredient(requiredCounts, input.ingredient(), input.count()); + } + + boolean hasKeyInput = keyInput != null && keyInput.amount() > 0; + + // 检查物品输入匹配(包括 keyInput) + boolean itemsMatch = true; + if (!requiredCounts.isEmpty()) { + itemsMatch = AdapterUtils.matchesRequired(mergedInputs, requiredCounts); + } + + // 检查流体输入匹配 + boolean fluidsMatch = true; + if (!recipeFluidInputs.isEmpty()) { + fluidsMatch = matchesFluidInputs(mergedFluids, recipeFluidInputs); + } + + boolean keysMatch = !hasKeyInput || matchesKeyInput(mergedInputs, mergedFluids, mergedKeys, keyInput); + + if ((requiredCounts.isEmpty() || itemsMatch) && (recipeFluidInputs.isEmpty() || fluidsMatch) && keysMatch) { + matches.add(holder); + } + } + + return matches; + } + + private boolean matchesFluidInputs(Map mergedFluids, List requiredFluids) { + if (mergedFluids == null || mergedFluids.isEmpty()) { + return false; + } + + List requirements = new ArrayList<>(); + for (GenericStack required : requiredFluids) { + if (!(required.what() instanceof AEFluidKey requiredKey) || required.amount() <= 0) continue; + int amount = required.amount() > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) required.amount(); + SizedFluidIngredient ingredient = AdapterUtils.toSizedFluidIngredient(requiredKey.toStack(amount)); + if (ingredient != null) requirements.add(ingredient); + } + return FluidIngredientAllocator.matches(requirements, mergedFluids, 1L); + } + + private boolean matchesKeyInput(Map mergedInputs, Map mergedFluids, Map mergedKeys, GenericStack keyInput) { + if (keyInput.what() instanceof AEItemKey itemKey) { + return AdapterUtils.hasMatchingIngredient(mergedInputs, Ingredient.of(itemKey.toStack()), keyInput.amount()); + } + if (keyInput.what() instanceof AEFluidKey fluidKey) { + if (keyInput.amount() <= 0 || keyInput.amount() > Integer.MAX_VALUE) return false; + SizedFluidIngredient requirement = AdapterUtils.toSizedFluidIngredient( + fluidKey.toStack((int) keyInput.amount())); + return requirement != null && FluidIngredientAllocator.matches( + List.of(requirement), mergedFluids, 1L); + } + return mergedKeys.getOrDefault(keyInput.what(), 0L) >= keyInput.amount(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/extendedae/CircuitCutterRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/extendedae/CircuitCutterRecipeAdapter.java new file mode 100644 index 00000000..571adb20 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/extendedae/CircuitCutterRecipeAdapter.java @@ -0,0 +1,67 @@ +package com.sorrowmist.useless.content.recipe.adapters.ae.extendedae; + +import com.glodblock.github.extendedae.common.EPPItemAndBlock; +import com.glodblock.github.extendedae.recipe.CircuitCutterRecipe; +import com.glodblock.github.extendedae.recipe.util.IngredientStack; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess; +import com.sorrowmist.useless.content.recipe.*; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import java.util.*; + +/** Native 1.20.1 cutter inputs include the fluid cost (normally 100 mB water). */ +public class CircuitCutterRecipeAdapter implements IRecipeAdapter { + @Override public Class getRecipeClass() { return CircuitCutterRecipe.class; } + @Override public ItemStack getMoldItem() { return new ItemStack(EPPItemAndBlock.CIRCUIT_CUTTER); } + + static SizedFluidIngredient fluid(IngredientStack.Fluid source) { + if (source == null || source.getAmount() <= 0 + || !(source.getIngredient() instanceof com.glodblock.github.extendedae.recipe.util.FluidIngredient nativeIngredient)) + return null; + // Native EAE compares fluid families and ignores NBT. Keep tags structural across reloads. + var json=nativeIngredient.toJson().getAsJsonObject().deepCopy(); + json.remove("nbt"); + json.addProperty("type",json.has("tag") ? "useless_mod:same_fluid_tag" : "useless_mod:same_fluid"); + var ingredient=FluidIngredient.fromJson(json); + return ingredient.isEmpty() ? null : new SizedFluidIngredient(ingredient,source.getAmount()); + } + + @Override public AdvancedAlloyFurnaceRecipe convert(RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) return null; + var source=holder.value(); + var input=source.getInput(); + if (input == null || input.getAmount() <= 0 || !(input.getIngredient() instanceof Ingredient ingredient) + || AdapterUtils.isIngredientEmpty(ingredient) || source.output == null || source.output.isEmpty()) return null; + var fluid=fluid(source.getFluid()); + // A malformed mandatory input must never turn into a cheaper recipe. + if (fluid == null) return null; + return new AdvancedAlloyFurnaceRecipe(AdapterUtils.convertedId(holder.id()), + List.of(new CountedIngredient(ingredient,input.getAmount())),List.of(fluid), + List.of(source.output.copy()),List.of(), + (long)AdapterUtils.EXTENDEDAE_ENERGY_MULTIPLIER * AdapterUtils.DEFAULT_PROCESS_TIME * AdapterUtils.AE_TO_FE_CONVERSION, + AdapterUtils.DEFAULT_PROCESS_TIME,Ingredient.EMPTY,0,AdapterUtils.toMoldIngredient(getMoldItem()),AlloyFurnaceMode.NORMAL); + } + + public boolean matches(AdvancedAlloyFurnaceRecipe recipe, Map items, Map fluids) { + if (recipe == null) return false; + Map required=new LinkedHashMap<>(); + for (var input:recipe.inputs()) AdapterUtils.mergeIngredient(required,input.ingredient(),input.count()); + return AdapterUtils.matchesRequired(items,required) + && FluidIngredientAllocator.matchesLong(recipe.inputFluids(),fluids,1L); + } + + @Override public List> findMatchingRecipes(Level level,Map items, + Map fluids,ItemStack mold) { + if (level == null || !matchesMold(mold)) return List.of(); + List> matches=new ArrayList<>(); + for (var holder:RecipeManagerAccess.getAllRecipesFor(level.getRecipeManager(),CircuitCutterRecipe.TYPE)) + if (matches(convert(holder,level),items,fluids)) matches.add(holder); + return matches; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/extendedae/CrystalAssemblerRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/extendedae/CrystalAssemblerRecipeAdapter.java new file mode 100644 index 00000000..ffae31f6 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/extendedae/CrystalAssemblerRecipeAdapter.java @@ -0,0 +1,148 @@ +package com.sorrowmist.useless.content.recipe.adapters.ae.extendedae; + +import com.glodblock.github.extendedae.common.EAESingletons; +import com.glodblock.github.extendedae.recipe.CrystalAssemblerRecipe; +import com.glodblock.github.glodium.recipe.stack.IngredientStack; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.FluidIngredientAllocator; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * ExtendedAE 水晶装配器配方适配器 + */ +public class CrystalAssemblerRecipeAdapter implements IRecipeAdapter { + + @Override + public Class getRecipeClass() { + return CrystalAssemblerRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return new ItemStack(EAESingletons.CRYSTAL_ASSEMBLER); + } + + @Override + @Nullable + public AdvancedAlloyFurnaceRecipe convert(RecipeHolder holder, Level level) { + if (holder == null) return null; + + CrystalAssemblerRecipe originalRecipe = holder.value(); + ResourceLocation originalId = holder.id(); + + // 获取输入材料 + List countedIngredients = new ArrayList<>(); + List inputs = originalRecipe.getInputs(); + + for (IngredientStack.Item inputStack : inputs) { + if (inputStack != null && !inputStack.isEmpty()) { + Ingredient ingredient = inputStack.getIngredient(); + long count = inputStack.getAmount(); + if (ingredient != null && !ingredient.isEmpty()) { + countedIngredients.add(new CountedIngredient(ingredient, count)); + } + } + } + + if (countedIngredients.isEmpty()) { + return null; + } + + // 获取输出物品 + ItemStack result = originalRecipe.output; + List outputs = List.of(result.copy()); + + // 获取流体输入 + List inputFluids = new ArrayList<>(); + IngredientStack.Fluid fluidInput = originalRecipe.getFluid(); + if (fluidInput != null && !fluidInput.isEmpty()) { + // 从 IngredientStack.Fluid 提取 FluidStack + var fluidIngredient = fluidInput.getIngredient(); + long amount = fluidInput.getAmount(); + if (fluidIngredient != null && !fluidIngredient.isEmpty() && amount > 0 + && amount <= Integer.MAX_VALUE) { + inputFluids.add(new SizedFluidIngredient(fluidIngredient, (int) amount)); + } + } + + // 计算能量消耗:EAE使用AE单位,需要转换为FE(*2) + int energyCost = AdapterUtils.EXTENDEDAE_ENERGY_MULTIPLIER * AdapterUtils.DEFAULT_PROCESS_TIME * AdapterUtils.AE_TO_FE_CONVERSION; + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(originalId), + countedIngredients, + inputFluids, + outputs, + List.of(), // 无流体输出 + energyCost, + AdapterUtils.DEFAULT_PROCESS_TIME, + Ingredient.EMPTY, // 无催化剂 + 0, + // 水晶装配器作为模具 + Ingredient.of(new ItemStack(EAESingletons.CRYSTAL_ASSEMBLER)), + AlloyFurnaceMode.NORMAL + ); + } + + @Override + @Nullable + public List> findMatchingRecipes(Level level, Map mergedInputs, Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || mergedInputs.isEmpty()) { + return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + List> recipes = com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, CrystalAssemblerRecipe.TYPE); + + List> matches = new java.util.ArrayList<>(); + for (RecipeHolder holder : recipes) { + CrystalAssemblerRecipe recipe = holder.value(); + // 使用全类名 + List recipeInputs = recipe.getInputs(); + + Map requiredInputs = new LinkedHashMap<>(); + for (IngredientStack.Item inputStack : recipeInputs) { + if (inputStack == null || inputStack.isEmpty()) continue; + + Ingredient ingredient = inputStack.getIngredient(); + AdapterUtils.mergeIngredient(requiredInputs, ingredient, inputStack.getAmount()); + } + + boolean fluidMatch = matchesFluidInput(mergedFluids, recipe.getFluid()); + if (AdapterUtils.matchesRequired(mergedInputs, requiredInputs) + && !recipeInputs.isEmpty() && fluidMatch) { + matches.add(holder); + } + } + return matches; + } + + private boolean matchesFluidInput(Map mergedFluids, IngredientStack.Fluid fluidInput) { + if (fluidInput == null || fluidInput.isEmpty()) return true; + var ingredient = fluidInput.getIngredient(); + long amount = fluidInput.getAmount(); + if (ingredient == null || ingredient.isEmpty() || amount <= 0 || amount > Integer.MAX_VALUE) { + return false; + } + return FluidIngredientAllocator.matches( + List.of(new SizedFluidIngredient(ingredient, (int) amount)), + mergedFluids, 1L); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/extendedae/CrystalFixerRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/extendedae/CrystalFixerRecipeAdapter.java new file mode 100644 index 00000000..faf8760a --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/extendedae/CrystalFixerRecipeAdapter.java @@ -0,0 +1,63 @@ +package com.sorrowmist.useless.content.recipe.adapters.ae.extendedae; + +import appeng.core.definitions.AEBlocks; +import appeng.core.definitions.AEItems; +import com.glodblock.github.extendedae.common.EPPItemAndBlock; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import com.sorrowmist.useless.content.recipe.*; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import java.util.*; + +/** Native 1.4 has a hardcoded machine, not CrystalFixerRecipe/RecipeType. */ +public class CrystalFixerRecipeAdapter implements IRecipeAdapter { + @Override public Class getRecipeClass() { return CrystalFixingSyntheticRecipe.class; } + @Override public ItemStack getMoldItem() { return new ItemStack(EPPItemAndBlock.CRYSTAL_FIXER); } + + /** E[n] = (5 + E[n-1] + ... + E[n-4])/4 for native nextInt(5) progress. + * Each work call consumes charged quartz with probability 1/10. Round up for + * this deterministic furnace conversion so integer recipes cannot underpay. + */ + static int fuelPerRepair() { + double[] calls=new double[101]; + for (int n=1;n<=100;n++) { + double total=5; + for(int step=1;step<=4;step++) total+=calls[Math.max(0,n-step)]; + calls[n]=total/4; + } + return (int)Math.ceil(calls[100]/10); + } + + @Override public List> getGeneratedRecipes(Level level) { + return List.of(repair("quartz_block",AEBlocks.QUARTZ_BLOCK.stack(),AEBlocks.DAMAGED_BUDDING_QUARTZ.stack()), + repair("damaged",AEBlocks.DAMAGED_BUDDING_QUARTZ.stack(),AEBlocks.CHIPPED_BUDDING_QUARTZ.stack()), + repair("chipped",AEBlocks.CHIPPED_BUDDING_QUARTZ.stack(),AEBlocks.FLAWED_BUDDING_QUARTZ.stack())); + } + private RecipeHolder repair(String stage,ItemStack input,ItemStack output) { + var id=AdapterUtils.convertedId(new ResourceLocation("expatternprovider","crystal_fixer/"+stage)); + var recipe=new AdvancedAlloyFurnaceRecipe(id,List.of(new CountedIngredient(Ingredient.of(input),1), + new CountedIngredient(Ingredient.of(AEItems.CERTUS_QUARTZ_CRYSTAL_CHARGED.stack()),fuelPerRepair())), + List.of(),List.of(output),List.of(), + 50L * 102 * AdapterUtils.AE_TO_FE_CONVERSION,102,Ingredient.EMPTY,0, + AdapterUtils.toMoldIngredient(getMoldItem()),AlloyFurnaceMode.NORMAL); + return new RecipeHolder<>(id,new CrystalFixingSyntheticRecipe(recipe)); + } + @Override public AdvancedAlloyFurnaceRecipe convert(RecipeHolder holder,Level level) { + return holder == null ? null : holder.value().convertedRecipe(); + } + @Override public List> findMatchingRecipes(Level level,Map items, + Map fluids,ItemStack mold) { + if(level == null || !matchesMold(mold)) return List.of(); + List> result=new ArrayList<>(); + for(var holder:getGeneratedRecipes(level)) { + Map required=new LinkedHashMap<>(); + for(var input:holder.value().convertedRecipe().inputs()) AdapterUtils.mergeIngredient(required,input.ingredient(),input.count()); + if(AdapterUtils.matchesRequired(items,required)) result.add(holder); + } + return result; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/extendedae/CrystalFixingSyntheticRecipe.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/extendedae/CrystalFixingSyntheticRecipe.java new file mode 100644 index 00000000..3a4a7364 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ae/extendedae/CrystalFixingSyntheticRecipe.java @@ -0,0 +1,55 @@ +package com.sorrowmist.useless.content.recipe.adapters.ae.extendedae; + +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.Container; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; + +/** Recipe-manager payload for ExtendedAE 1.4's hardcoded crystal repairs. */ +public final class CrystalFixingSyntheticRecipe implements Recipe { + private final AdvancedAlloyFurnaceRecipe convertedRecipe; + + public CrystalFixingSyntheticRecipe(AdvancedAlloyFurnaceRecipe convertedRecipe) { + this.convertedRecipe = convertedRecipe; + } + + public AdvancedAlloyFurnaceRecipe convertedRecipe() { + return this.convertedRecipe; + } + + @Override + public net.minecraft.resources.ResourceLocation getId() { return convertedRecipe.id(); } + + @Override + public boolean matches(Container input, Level level) { + return this.convertedRecipe != null && this.convertedRecipe.matches(input, level); + } + + @Override + public ItemStack assemble(Container input, net.minecraft.core.RegistryAccess registries) { + return this.convertedRecipe == null ? ItemStack.EMPTY : this.convertedRecipe.assemble(input, registries); + } + + @Override + public boolean canCraftInDimensions(int width, int height) { + return this.convertedRecipe != null && this.convertedRecipe.canCraftInDimensions(width, height); + } + + @Override + public ItemStack getResultItem(net.minecraft.core.RegistryAccess registries) { + return this.convertedRecipe == null ? ItemStack.EMPTY : this.convertedRecipe.getResultItem(registries); + } + + @Override + public RecipeSerializer getSerializer() { + return this.convertedRecipe == null ? null : this.convertedRecipe.getSerializer(); + } + + @Override + public RecipeType getType() { + return this.convertedRecipe == null ? null : this.convertedRecipe.getType(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/apotheosis/ApotheosisAnvilSmashingRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/apotheosis/ApotheosisAnvilSmashingRecipeAdapter.java new file mode 100644 index 00000000..1e2286d5 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/apotheosis/ApotheosisAnvilSmashingRecipeAdapter.java @@ -0,0 +1,185 @@ +package com.sorrowmist.useless.content.recipe.adapters.apotheosis; + +import appeng.api.stacks.AEKey; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.api.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.Recipe; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.Container; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * 将神化的铁砧砸碎宝石机制转换为高级合金炉配方。 + * + *

神化并未把该机制实现为数据驱动配方,而是在 {@code AdventureEvents#gemSmashing} 中监听 + * 铁砧落地事件,扫描落点方块范围内的掉落物实体,把宝石原地替换为等量的宝石粉。因此本适配器 + * 以合成配方的形式表达这条固定转换,模具统一为铁砧。

+ * + *

产出语义与原始机制保持 1:1:输入若干宝石产出等量宝石粉,不做数量放大。

+ */ +public final class ApotheosisAnvilSmashingRecipeAdapter + implements IRecipeAdapter { + + /** 神化宝石的物品 id。 */ + private static final ResourceLocation GEM_ID = + new ResourceLocation("apotheosis", "gem"); + /** 神化宝石粉的物品 id。 */ + private static final ResourceLocation GEM_DUST_ID = + new ResourceLocation("apotheosis", "gem_dust"); + /** 合成配方使用的稳定标识。 */ + private static final ResourceLocation SYNTHETIC_ID = + new ResourceLocation("apotheosis", "anvil_gem_smashing_converted"); + + @Override + public String sourceId() { + return RecipeSourceIds.APOTHEOSIS; + } + + @Override + public Class getRecipeClass() { + return AnvilSmashingRecipe.class; + } + + /** 模具统一为铁砧,与原始机制的触发方块一致。 */ + @Override + public ItemStack getMoldItem() { + return new ItemStack(Items.ANVIL); + } + + @Override + public List> getGeneratedRecipes(Level level) { + Item gemDust = BuiltInRegistries.ITEM.getOptional(GEM_DUST_ID).orElse(null); + if (gemDust == null) { + return List.of(); + } + + AdvancedAlloyFurnaceRecipe converted = new AdvancedAlloyFurnaceRecipe( + SYNTHETIC_ID, + List.of(new CountedIngredient(ApotheosisGemIngredient.of(), 1L)), + List.of(), + List.of(), + List.of(new ItemStack(gemDust)), + List.of(), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + AdapterUtils.DEFAULT_PROCESS_TIME, + Ingredient.EMPTY, + 0, + AdapterUtils.toMoldIngredient(getMoldItem()), + AlloyFurnaceMode.NORMAL); + return List.of(new RecipeHolder<>(SYNTHETIC_ID, new AnvilSmashingRecipe(converted))); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + AdvancedAlloyFurnaceRecipe converted = holder.value().convertedRecipe(); + return converted == null ? List.of() : List.of(converted); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + Map mergedKeys, @Nullable ItemStack mold, List actualInputs) { + if (level == null || !matchesMold(mold)) { + return List.of(); + } + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : getGeneratedRecipes(level)) { + List converted = convertAll(holder, level); + if (converted.isEmpty()) { + continue; + } + if (AdapterUtils.matchesRequired(mergedInputs, requiredCounts(converted.get(0).inputs()))) { + matches.add(holder); + } + } + return List.copyOf(matches); + } + + private static Map requiredCounts(List inputs) { + Map required = new LinkedHashMap<>(); + for (CountedIngredient input : inputs) { + if (input == null || input.ingredient() == null || input.ingredient().isEmpty() + || input.count() <= 0) { + continue; + } + AdapterUtils.mergeIngredient(required, input.ingredient(), input.count()); + } + return required; + } + + /** + * 承载转换结果的合成配方。 + * + *

该配方不参与原版合成匹配,仅作为适配器向配方管理器传递转换产物的载体; + * 真正的匹配与产出由 {@link AdvancedAlloyFurnaceRecipe} 决定。

+ */ + public static final class AnvilSmashingRecipe implements Recipe { + private final AdvancedAlloyFurnaceRecipe convertedRecipe; + + AnvilSmashingRecipe(AdvancedAlloyFurnaceRecipe convertedRecipe) { + this.convertedRecipe = convertedRecipe; + } + + public AdvancedAlloyFurnaceRecipe convertedRecipe() { + return convertedRecipe; + } + + @Override + public net.minecraft.resources.ResourceLocation getId() { return convertedRecipe.id(); } + + @Override + public boolean matches(Container input, Level level) { + return false; + } + + @Override + public ItemStack assemble(Container input, net.minecraft.core.RegistryAccess registries) { + return ItemStack.EMPTY; + } + + @Override + public boolean canCraftInDimensions(int width, int height) { + return false; + } + + @Override + public ItemStack getResultItem(net.minecraft.core.RegistryAccess registries) { + return ItemStack.EMPTY; + } + + @Override + public RecipeSerializer getSerializer() { + return null; + } + + @Override + public RecipeType getType() { + return null; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/apotheosis/ApotheosisGemIngredient.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/apotheosis/ApotheosisGemIngredient.java new file mode 100644 index 00000000..cc7af0f2 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/apotheosis/ApotheosisGemIngredient.java @@ -0,0 +1,79 @@ +package com.sorrowmist.useless.content.recipe.adapters.apotheosis; + +import com.mojang.serialization.MapCodec; + +import dev.shadowsoffire.apotheosis.adventure.socket.gem.Gem; +import dev.shadowsoffire.apotheosis.adventure.socket.gem.GemItem; +import dev.shadowsoffire.apotheosis.adventure.socket.gem.GemRegistry; +import dev.shadowsoffire.placebo.reload.DynamicHolder; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.CodecIngredient; +import com.sorrowmist.useless.compat.forge1201.recipe.CodecIngredientSerializer; + + +import java.util.ArrayList; +import java.util.List; + +/** + * 匹配任意神化宝石的原料。 + * + *

神化宝石是动态物品,其名称、效果与渲染均取自 {@code apotheosis:gem} 数据组件。 + * {@code Ingredient.of(gem)} 生成的候选栈不含该组件,展示时会退化为 + * {@code item.apotheosis.gem} 并附带 {@code Errored gem with no bonus!} 提示。

+ * + *

本类在匹配时只要求目标栈为宝石物品且持有已绑定的宝石组件;候选栈则返回带完整组件的 + * 合法宝石栈,供 JEI 与配方界面展示使用。

+ * + *

本类引用了神化的类型,因此只能在神化存在时被加载:注册入口 + * {@code ModIngredientTypes.APOTHEOSIS_GEM} 已做加载检查,不要在任何无条件执行的静态 + * 初始化里触碰本类。

+ */ +public final class ApotheosisGemIngredient extends CodecIngredient { + + public static final ApotheosisGemIngredient INSTANCE = new ApotheosisGemIngredient(); + + public static final MapCodec CODEC = MapCodec.unit(INSTANCE); + + public static final CodecIngredientSerializer TYPE = new CodecIngredientSerializer<>(CODEC.codec()); + + private ApotheosisGemIngredient() { + } + + /** 构造匹配任意神化宝石的原料。 */ + public static Ingredient of() { + return INSTANCE; + } + + @Override + public boolean test(ItemStack stack) { + if (stack == null || stack.isEmpty() || !(stack.getItem() instanceof GemItem)) { + return false; + } + DynamicHolder holder = GemItem.getGem(stack); + return holder.isBound(); + } + + @Override + public ItemStack[] getItems() { + List stacks = new ArrayList<>(); + for (Gem gem : GemRegistry.INSTANCE.getValues()) { + // 每种宝石仅提供一个代表栈,避免候选列表随品级数量膨胀。 + stacks.add(GemRegistry.createGemStack(gem,gem.getMinRarity())); + } + return stacks.toArray(ItemStack[]::new); + } + + @Override + public CodecIngredientSerializer getSerializer() { return TYPE; } + + @Override + public boolean equals(Object object) { + return object instanceof ApotheosisGemIngredient; + } + + @Override + public int hashCode() { + return ApotheosisGemIngredient.class.hashCode(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/apothicflux/InfusionRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/apothicflux/InfusionRecipeAdapter.java new file mode 100644 index 00000000..f318fc0a --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/apothicflux/InfusionRecipeAdapter.java @@ -0,0 +1,262 @@ +package com.sorrowmist.useless.content.recipe.adapters.apothicflux; + +import appeng.api.stacks.AEKey; +import com.chuan.apothicflux.block.Tier; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.api.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import dev.shadowsoffire.apothic_enchanting.Ench; +import dev.shadowsoffire.apothic_enchanting.table.EnchantmentTableStats; +import dev.shadowsoffire.apothic_enchanting.table.infusion.InfusionRecipe; +import net.minecraft.core.component.DataComponents; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Apothic Enchanting infusion recipes using configured Flux bookshelves as molds. */ +public final class InfusionRecipeAdapter implements IRecipeAdapter { + private static final float BASE_QUANTA = EnchantmentTableStats.vanilla(0).quanta(); + + private static final List SHELVES = List.of( + new ShelfDefinition("flux_stats_bookshelf_tier_1", Tier.TIER_1), + new ShelfDefinition("flux_stats_bookshelf_tier_2", Tier.TIER_2), + new ShelfDefinition("flux_stats_bookshelf_tier_3", Tier.TIER_3), + new ShelfDefinition("flux_stats_bookshelf_tier_4", Tier.TIER_4)); + + private static final Map SHELF_BY_ID = SHELVES.stream() + .collect(java.util.stream.Collectors.toUnmodifiableMap( + definition -> definition.id(), definition -> definition)); + + @Override + public String sourceId() { + return RecipeSourceIds.APOTHIC_FLUX; + } + + @Override + public Class getRecipeClass() { + return InfusionRecipe.class; + } + + /** This adapter handles all four shelf items, so it must use the fallback lookup path. */ + @Override + @Nullable + public ItemStack getMoldItem() { + return null; + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + return shelfDefinition(mold) != null; + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + return convertRecipes(holder, null); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level, List actualInputs) { + InfusionRecipe source = holder == null ? null : holder.value(); + ItemStack actualInput = source == null ? null : findMatchingInput(source, actualInputs); + return convertRecipes(holder, actualInput); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + return findMatchingRecipes(level, mergedInputs, mergedFluids, Map.of(), mold, List.of()); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, Map mergedKeys, + @Nullable ItemStack mold, List actualInputs) { + if (level == null || !matchesMold(mold)) return List.of(); + + ShelfDefinition shelf = shelfDefinition(mold); + if (shelf == null) return List.of(); + ShelfStats stats = readStats(mold, shelf); + + RecipeHolder bestMatch = null; + boolean hasActualInputs = actualInputs != null && actualInputs.stream() + .anyMatch(stack -> stack != null && !stack.isEmpty()); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(level.getRecipeManager(), Ench.RecipeTypes.INFUSION)) { + InfusionRecipe recipe = holder.value(); + ItemStack input = findMatchingInput(recipe, actualInputs); + boolean inputMatches; + if (hasActualInputs) { + inputMatches = input != null && recipe.matches( + input, stats.eterna(), stats.quanta(), stats.arcana()); + } else { + ItemStack mergedInput = firstMergedInput(recipe, mergedInputs); + inputMatches = mergedInput != null && recipe.matches( + mergedInput, stats.eterna(), stats.quanta(), stats.arcana()); + } + if (!inputMatches) continue; + + if (bestMatch == null || recipe.getRequirements().eterna() + > bestMatch.value().getRequirements().eterna()) { + bestMatch = holder; + } + } + return bestMatch == null ? List.of() : List.of(bestMatch); + } + + private static List convertRecipes( + @Nullable RecipeHolder holder, @Nullable ItemStack actualInput) { + if (holder == null || holder.value() == null) return List.of(); + + InfusionRecipe source = holder.value(); + if (source.getInput() == null || source.getInput().isEmpty() + || source.getOutput() == null || source.getOutput().isEmpty()) { + return List.of(); + } + + ItemStack output = outputFor(source, actualInput); + if (output.isEmpty()) return List.of(); + + List converted = new ArrayList<>(SHELVES.size()); + for (ShelfDefinition shelf : SHELVES) { + Item item = shelfItem(shelf); + if (item == null) continue; + + converted.add(new AdvancedAlloyFurnaceRecipe( + variantId(holder.id(), shelf), + List.of(new CountedIngredient(source.getInput(), 1)), + List.of(), + List.of(output.copy()), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + AdapterUtils.DEFAULT_PROCESS_TIME, + Ingredient.EMPTY, + 0, + Ingredient.of(item), + AlloyFurnaceMode.NORMAL)); + } + return List.copyOf(converted); + } + + @Nullable + private static ItemStack findMatchingInput( + InfusionRecipe recipe, @Nullable List actualInputs) { + if (actualInputs == null || actualInputs.isEmpty()) return null; + return actualInputs.stream() + .filter(stack -> stack != null && !stack.isEmpty() && recipe.getInput().test(stack)) + .findFirst() + .orElse(null); + } + + @Nullable + private static ItemStack firstMergedInput( + InfusionRecipe recipe, @Nullable Map mergedInputs) { + if (mergedInputs == null || mergedInputs.isEmpty()) return null; + + for (Map.Entry entry : mergedInputs.entrySet()) { + if (entry.getValue() == null || entry.getValue() <= 0 + || entry.getKey() == null || entry.getKey().isEmpty()) { + continue; + } + try { + for (ItemStack stack : entry.getKey().getItems()) { + if (stack != null && !stack.isEmpty() && recipe.getInput().test(stack)) { + return stack; + } + } + } catch (RuntimeException ignored) { + // Opaque custom ingredients are handled by the concrete-input path. + } + } + return null; + } + + private static ItemStack outputFor(InfusionRecipe recipe, @Nullable ItemStack actualInput) { + if (actualInput == null || actualInput.isEmpty()) return recipe.getOutput().copy(); + + return recipe.assemble(actualInput, + recipe.getRequirements().eterna(), + recipe.getRequirements().quanta(), + recipe.getRequirements().arcana()); + } + + @Nullable + private static ShelfDefinition shelfDefinition(@Nullable ItemStack mold) { + if (mold == null || mold.isEmpty()) return null; + return SHELF_BY_ID.get(BuiltInRegistries.ITEM.getKey(mold.getItem())); + } + + @Nullable + private static Item shelfItem(ShelfDefinition shelf) { + return BuiltInRegistries.ITEM.getOptional(shelf.id()).orElse(null); + } + + private static ShelfStats readStats(ItemStack mold, ShelfDefinition shelf) { + // A plain shelf item has the block entity defaults: zero configurable stats. Configured + // values are read from the block-entity/custom-data component when the item carries them. + float rawQuanta = clamp(readStat(mold, "Quanta", 0F), Tier.MIN_QUANTA, shelf.maxQuanta()); + return new ShelfStats( + clamp(readStat(mold, "Eterna", 0F), 0F, shelf.maxEterna()), + clamp(rawQuanta + BASE_QUANTA, 0F, 100F), + clamp(readStat(mold, "Arcana", 0F), 0F, shelf.maxArcana())); + } + + private static float readStat(ItemStack stack, String key, float fallback) { + CustomData blockEntityData = stack.get(DataComponents.BLOCK_ENTITY_DATA); + Float value = readStat(blockEntityData == null ? null : blockEntityData.copyTag(), key); + if (value != null) return value; + + CustomData customData = stack.get(DataComponents.CUSTOM_DATA); + value = readStat(customData == null ? null : customData.copyTag(), key); + return value == null ? fallback : value; + } + + @Nullable + private static Float readStat(@Nullable CompoundTag tag, String key) { + if (tag == null) return null; + if (tag.contains(key, Tag.TAG_INT)) return (float) tag.getInt(key); + if (tag.contains(key, Tag.TAG_FLOAT)) return tag.getFloat(key); + if (tag.contains(key, Tag.TAG_DOUBLE)) return (float) tag.getDouble(key); + if (tag.contains(key, Tag.TAG_LONG)) return (float) tag.getLong(key); + return null; + } + + private static float clamp(float value, float min, float max) { + if (!Float.isFinite(value)) return min; + return Math.max(min, Math.min(max, value)); + } + + private record ShelfDefinition(ResourceLocation id, float maxEterna, float maxQuanta, float maxArcana) { + private ShelfDefinition(String path, Tier tier) { + this(new ResourceLocation("apothic_flux", path), + tier.getMaxEterna(), tier.getMaxQuanta(), tier.getMaxArcana()); + } + } + + private record ShelfStats(float eterna, float quanta, float arcana) { + } + + private static ResourceLocation variantId(ResourceLocation source, ShelfDefinition shelf) { + return new ResourceLocation( + source.getNamespace(), + source.getPath() + "_converted_" + shelf.id().getPath()); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/apothicflux/RitualCraftingRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/apothicflux/RitualCraftingRecipeAdapter.java new file mode 100644 index 00000000..374fddb5 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/apothicflux/RitualCraftingRecipeAdapter.java @@ -0,0 +1,139 @@ +package com.sorrowmist.useless.content.recipe.adapters.apothicflux; + +import com.chuan.apothicflux.recipe.RitualCraftingRecipe; +import com.chuan.apothicflux.registry.ModRegistry; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.api.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** Converts Apothic Flux ritual-crafting recipes into alloy-furnace recipes. */ +public final class RitualCraftingRecipeAdapter implements IRecipeAdapter { + private static final ResourceLocation INFUSED_BREATH_ID = + new ResourceLocation("apothic_enchanting", "infused_breath"); + private static final int OUTPUT_FLUID_AMOUNT = 1_000; + + @Override + public String sourceId() { + return RecipeSourceIds.APOTHIC_FLUX; + } + + @Override + public Class getRecipeClass() { + return RitualCraftingRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return BuiltInRegistries.ITEM.getOptional(INFUSED_BREATH_ID) + .map(ItemStack::new) + .orElse(ItemStack.EMPTY); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + AdvancedAlloyFurnaceRecipe converted = convertRecipe(holder); + return converted == null ? List.of() : List.of(converted); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold) + || mergedInputs == null || mergedInputs.isEmpty()) { + return List.of(); + } + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(level.getRecipeManager(), ModRegistry.RITUAL_TYPE.get())) { + AdvancedAlloyFurnaceRecipe converted = convertRecipe(holder); + if (converted == null) continue; + + if (AdapterUtils.matchesRequired(mergedInputs, requiredCounts(converted.inputs()))) { + matches.add(holder); + } + } + return List.copyOf(matches); + } + + @Nullable + private AdvancedAlloyFurnaceRecipe convertRecipe( + @Nullable RecipeHolder holder) { + if (holder == null || holder.value() == null) return null; + + RitualCraftingRecipe source = holder.value(); + List inputs = AdapterUtils.mergeIngredients(source.inputs()); + if (inputs.isEmpty()) return null; + + ItemStack outputItem = source.outputItem(); + List outputs = outputItem == null || outputItem.isEmpty() + ? List.of() + : List.of(outputItem.copy()); + List outputFluids = fluidOutputs(source.outputFluid()); + if (outputs.isEmpty() && outputFluids.isEmpty()) { + // Entity-only rituals cannot be represented by the alloy-furnace recipe model. + return null; + } + + ItemStack mold = getMoldItem(); + if (mold.isEmpty()) return null; + + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + inputs, + List.of(), + List.of(), + outputs, + outputFluids, + List.of(), + AdapterUtils.DEFAULT_ENERGY, + Math.max(1, source.craftTime()), + Ingredient.EMPTY, + 0, + AdapterUtils.toMoldIngredient(mold), + AlloyFurnaceMode.NORMAL); + } + + private static Map requiredCounts(List inputs) { + Map required = new LinkedHashMap<>(); + for (CountedIngredient input : inputs) { + if (input == null || input.ingredient() == null || input.ingredient().isEmpty() + || input.count() <= 0) { + continue; + } + AdapterUtils.mergeIngredient(required, input.ingredient(), input.count()); + } + return required; + } + + private static List fluidOutputs(@Nullable Optional outputFluid) { + if (outputFluid == null || outputFluid.isEmpty()) return List.of(); + + ResourceLocation id = ResourceLocation.tryParse(outputFluid.get()); + if (id == null) return List.of(); + + Fluid fluid = BuiltInRegistries.FLUID.getOptional(id).orElse(null); + if (fluid == null || fluid == Fluids.EMPTY) return List.of(); + return List.of(new FluidStack(fluid, OUTPUT_FLUID_AMOUNT)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/arsnouveau/EnchantingApparatusRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/arsnouveau/EnchantingApparatusRecipeAdapter.java new file mode 100644 index 00000000..6a181448 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/arsnouveau/EnchantingApparatusRecipeAdapter.java @@ -0,0 +1,152 @@ +package com.sorrowmist.useless.content.recipe.adapters.arsnouveau; + +import com.hollingsworth.arsnouveau.common.crafting.recipes.EnchantingApparatusRecipe; +import com.hollingsworth.arsnouveau.setup.registry.RecipeRegistry; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Ars Nouveau 附魔装置配方适配器 + *

+ * 将附魔装置配方转换为高级合金熔炉配方 + *

+ * 处理逻辑: + * - 催化剂(reagent) + 基座物品(pedestalItems) → 普通输入(分别消耗) + * - 产物(result) → 输出 + * - ars_nouveau:enchanting_apparatus → 模具(不消耗) + * - 魔力消耗(sourceCost) → 能量消耗 + */ +public class EnchantingApparatusRecipeAdapter implements IRecipeAdapter { + @Override + public Class getRecipeClass() { + return EnchantingApparatusRecipe.class; + } + + @Nullable + @Override + public ItemStack getMoldItem() { + return new ItemStack( + BuiltInRegistries.ITEM.get( + new ResourceLocation("ars_nouveau", "enchanting_apparatus"))); + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + return mold != null + && BuiltInRegistries.ITEM.getKey(mold.getItem()).equals( + new ResourceLocation("ars_nouveau", "enchanting_apparatus")); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder == null) return List.of(); + + EnchantingApparatusRecipe originalRecipe = holder.value(); + ResourceLocation originalId = holder.id(); + + Ingredient reagent = originalRecipe.reagent(); + List pedestalItems = originalRecipe.pedestalItems(); + ItemStack output = originalRecipe.result(); + int sourceCost = originalRecipe.sourceCost(); + + if (output.isEmpty()) { + return List.of(); + } + + List countedIngredients = AdapterUtils.mergeIngredients( + new ArrayList<>() {{ + if (!reagent.isEmpty()) add(reagent); + for (Ingredient ing : pedestalItems) { + if (!ing.isEmpty()) add(ing); + } + }} + ); + + if (countedIngredients.isEmpty()) { + return List.of(); + } + + // 魔力消耗转换为能量(1 source = 10 FE) + int energy = Math.max(sourceCost * 10, 1000); + int processTime = AdapterUtils.DEFAULT_PROCESS_TIME + pedestalItems.size() * 20; + + AdvancedAlloyFurnaceRecipe convertedRecipe = new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(originalId), + countedIngredients, + List.of(), + List.of(output.copy()), + List.of(), + energy, + processTime, + Ingredient.EMPTY, + 0, + AdapterUtils.toMoldIngredient(getMoldItem()), + AlloyFurnaceMode.NORMAL + ); + + return List.of(convertedRecipe); + } + + @Override + @Nullable + @SuppressWarnings("unchecked") + public List> findMatchingRecipes(Level level, Map mergedInputs, Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || mergedInputs.isEmpty()) { + return List.of(); + } + + if (!matchesMold(mold)) { + return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + List> recipes = (List>) (List) com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + RecipeRegistry.APPARATUS_TYPE.get() + ); + + List> matches = new java.util.ArrayList<>(); + for (RecipeHolder holder : recipes) { + EnchantingApparatusRecipe recipe = holder.value(); + + Ingredient reagent = recipe.reagent(); + List pedestalItems = recipe.pedestalItems(); + ItemStack output = recipe.result(); + + if (output.isEmpty()) continue; + + Map requiredCounts = new LinkedHashMap<>(); + if (!reagent.isEmpty()) { + AdapterUtils.mergeIngredient(requiredCounts, reagent, 1L); + } + for (Ingredient pedestalIngredient : pedestalItems) { + if (!pedestalIngredient.isEmpty()) { + AdapterUtils.mergeIngredient(requiredCounts, pedestalIngredient, 1L); + } + } + + if (requiredCounts.isEmpty()) continue; + + if (AdapterUtils.matchesRequired(mergedInputs, requiredCounts)) { + matches.add(holder); + } + } + return matches; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/arsnouveau/ImbuementRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/arsnouveau/ImbuementRecipeAdapter.java new file mode 100644 index 00000000..3b7649af --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/arsnouveau/ImbuementRecipeAdapter.java @@ -0,0 +1,126 @@ +package com.sorrowmist.useless.content.recipe.adapters.arsnouveau; + +import com.hollingsworth.arsnouveau.common.crafting.recipes.ImbuementRecipe; +import com.hollingsworth.arsnouveau.setup.registry.RecipeRegistry; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Map; + +/** + * Ars Nouveau 灌魔室配方适配器 + *

+ * 将灌魔室配方转换为高级合金熔炉配方 + *

+ * 处理逻辑: + * - 中心物品(input) → 普通输入(被消耗) + * - 产物(output) → 模具(不消耗) + * - 基座物品(pedestalItems) → 忽略 + * - 魔力消耗(source) → 能量消耗 + */ +public class ImbuementRecipeAdapter implements IRecipeAdapter { + + @Override + public Class getRecipeClass() { + return ImbuementRecipe.class; + } + + @Nullable + @Override + public ItemStack getMoldItem() { + return null; // 产物作为模具,无固定模具物品 + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + // Imbuement 的模具是配方产物本身 + // 因为接口没有父 RecipeHolder 上下文,无法直接判断; + // 返回 true 让 findMatchingRecipe 自行做产物匹配 + return true; + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder == null) return List.of(); + + ImbuementRecipe originalRecipe = holder.value(); + ResourceLocation originalId = holder.id(); + + Ingredient input = originalRecipe.getInput(); + ItemStack output = originalRecipe.getOutput(); + int source = originalRecipe.getSource(); + + if (input.isEmpty() || output.isEmpty()) { + return List.of(); + } + + int energy = Math.max(source * 10, 500); + int processTime = AdapterUtils.DEFAULT_PROCESS_TIME; + + // 产物作为模具 + Ingredient moldIngredient = Ingredient.of(output.copy()); + + AdvancedAlloyFurnaceRecipe convertedRecipe = new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(originalId), + List.of(new CountedIngredient(input, 1)), + List.of(), + List.of(output.copy()), + List.of(), + energy, + processTime, + Ingredient.EMPTY, + 0, + moldIngredient, + AlloyFurnaceMode.NORMAL + ); + + return List.of(convertedRecipe); + } + + @Override + @Nullable + @SuppressWarnings("unchecked") + public List> findMatchingRecipes(Level level, Map mergedInputs, Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || mergedInputs.isEmpty()) { + return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + List> recipes = (List>) (List) com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + RecipeRegistry.IMBUEMENT_TYPE.get() + ); + + List> matches = new java.util.ArrayList<>(); + for (RecipeHolder holder : recipes) { + ImbuementRecipe recipe = holder.value(); + + Ingredient input = recipe.getInput(); + ItemStack output = recipe.getOutput(); + + if (input.isEmpty() || output.isEmpty()) continue; + if (!AdapterUtils.hasMatchingIngredient(mergedInputs, input)) continue; + + // 如果有模具,检查模具是否与产物匹配 + if (mold != null && !mold.isEmpty()) { + if (!ItemStack.isSameItemSameTags(mold, output)) { + continue; + } + } + + matches.add(holder); + } + return matches; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/astralsorcery/AltarRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/astralsorcery/AltarRecipeAdapter.java new file mode 100644 index 00000000..73861875 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/astralsorcery/AltarRecipeAdapter.java @@ -0,0 +1,406 @@ +package com.sorrowmist.useless.content.recipe.adapters.astralsorcery; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import hellfirepvp.astralsorcery.common.component.AttunedConstellationComponent; +import hellfirepvp.astralsorcery.common.constellation.BaseConstellation; +import hellfirepvp.astralsorcery.common.ingredient.IngredientBridge; +import hellfirepvp.astralsorcery.common.lib.DataComponentsAS; +import hellfirepvp.astralsorcery.common.lib.ItemsAS; +import hellfirepvp.astralsorcery.common.lib.RecipeTypesAS; +import hellfirepvp.astralsorcery.common.recipe.altar.AltarRecipe; +import hellfirepvp.astralsorcery.common.tile.TileAltar; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.common.crafting.SizedIngredient; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * 将星辉祭坛(Astral Sorcery 的祭坛合成配方)转换为高级合金炉配方。 + * + *

四类祭坛(启明工作台、共振工作台、辉光合成祭坛、虹彩合成祭坛)共用同一个配方类型 + * {@code astralsorcery:altar_crafting},仅以 {@code requiredType} 字段区分所需祭坛等级。 + * 本适配器对四类祭坛统一转换,并在模具中编码祭坛等级,使同一配方不会在错误的祭坛等级下匹配。

+ * + *

配方输入由三部分组成:3×3 祭坛网格、5×5 继电器网格,以及附加输入。继电器网格在祭坛结构中 + * 由四周的星辉继电器提供,其位置固定,因此转换为合金炉输入时按材料种类合并计数。

+ * + *

祭坛的聚焦槽承载星座维度:配方以 {@code focusConstellation} 声明所需星座,该星座由聚焦槽中 + * 带 {@code attuned_constellation} 组件的调谐水晶石提供。要求聚焦星座的配方在合金炉中额外把 + * 对应星座的调谐水晶石登记为二级模具,使星座维度在模具槽位上得到校验。

+ * + *

网格与继电器网格中的共鸣水晶石是普通材料,不提供星座,按普通输入处理。

+ */ +public final class AltarRecipeAdapter implements IRecipeAdapter { + + @Override + public String sourceId() { + return RecipeSourceIds.ASTRAL_SORCERY; + } + + @Override + public Class getRecipeClass() { + return AltarRecipe.class; + } + + /** + * 祭坛配方为动态模具:一级模具随配方所属祭坛等级变化,可能还存在二级模具, + * 因此此处返回 {@code null} 表示不使用固定的预索引模具。 + * + * @return 恒为 {@code null} + */ + @Override + public ItemStack getMoldItem() { + return null; + } + + /** + * 判定模具槽位是否可能承载本适配器的配方。 + * + *

本适配器不使用固定模具,因此默认实现会把任意模具都视为可处理。祭坛配方的模具 + * 槽位实际承载祭坛等级信息,必须放入四类祭坛物品之一才可能匹配,故在此收窄判定范围, + * 避免与其他适配器在同一模具槽位上产生无谓的候选枚举。

+ * + *

要求聚焦星座的配方另需一颗调谐水晶石作为第二级模具(见 {@link #buildMolds}), + * 该模具位于模具仓的独立槽位而非当前槽位,因此本判定不将其纳入:当前槽位承载的 + * 始终是祭坛等级。

+ * + * @param mold 模具槽位中的物品 + * @return 模具是否为任一等级的祭坛物品 + */ + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + if (mold == null || mold.isEmpty()) return false; + for (TileAltar.AltarType type : TileAltar.AltarType.values()) { + if (ItemStack.isSameItem(type.getAltarItem(), mold)) return true; + } + return false; + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) return List.of(); + + AltarRecipe recipe = holder.value(); + List inputs = collectInputs(recipe); + List outputs = recipe.getOutputs(); + + // 无输入或无产出的配方在合金炉中不可执行,直接跳过。 + if (inputs.isEmpty() || outputs.isEmpty()) return List.of(); + + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + inputs, + buildInputFluids(recipe), + List.of(), + outputs.stream().map(ItemStack::copy).toList(), + List.of(), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + resolveProcessTime(recipe), + Ingredient.EMPTY, + 0, + buildMolds(recipe), + AlloyFurnaceMode.NORMAL)); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null) return List.of(); + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(level.getRecipeManager(), RecipeTypesAS.ALTAR_CRAFTING_TYPE.get())) { + AltarRecipe recipe = holder.value(); + if (recipe == null) continue; + + // 模具槽位放置的祭坛物品决定可用配方等级,等级不符的配方不得参与匹配。 + if (!matchesAltarTier(recipe, mold)) continue; + + List required = collectInputs(recipe); + if (required.isEmpty()) continue; + if (!AdapterUtils.matchesRequired(mergedInputs, toCountMap(required))) continue; + if (!AdapterUtils.matchesLongFluidIngredients(mergedFluids, buildInputFluids(recipe))) continue; + + matches.add(holder); + } + return List.copyOf(matches); + } + + /** + * 判定模具槽位中的祭坛物品是否与配方的祭坛等级一致。 + * + *

祭坛配方不使用固定模具,模具槽位承载的是等级信息:只有放入与 {@code requiredType} 对应的祭坛物品, + * 该等级的配方才可被匹配。模具槽位为空或物品不是祭坛时,不返回任何配方,避免跨等级误匹配。

+ * + *

配方要求的聚焦星座由 {@link #buildMolds} 生成的第二级模具承载,不影响此处的等级判定: + * 该模具在模具仓中占用独立槽位,与承载等级的槽位互不干涉。

+ * + * @param recipe 源祭坛配方 + * @param mold 模具槽位中的物品 + * @return 祭坛等级是否一致 + */ + private static boolean matchesAltarTier(AltarRecipe recipe, @Nullable ItemStack mold) { + if (mold == null || mold.isEmpty()) return false; + TileAltar.AltarType requiredType = recipe.getRequiredType(); + if (requiredType == null) return false; + return ItemStack.isSameItem(requiredType.getAltarItem(), mold); + } + + /** + * 收集配方的全部物品输入。 + * + *

祭坛网格与继电器网格中的流体型条目({@link IngredientBridge.Type#FLUID})不纳入物品输入, + * 它们在祭坛上表现为容器盛装流体,与合金炉的流体输入语义不同,交由流体输入另行处理。

+ * + * @param recipe 源祭坛配方 + * @return 合并计数后的物品输入 + */ + private static List collectInputs(AltarRecipe recipe) { + // 网格与继电器网格中,同一字符键的多次出现必须合并为单个输入项并累计份数; + // 若逐槽位独立登记,一份材料会被拆成多条输入,导致配方在匹配时无法满足。 + // + // 归并依据是 Ingredient 的语义等价,而非对象身份:配方经网络同步后,各槽位由 + // 独立的流解码产生,同一材料对应的是不同实例,身份比较会失效。 + // + // 亦不能直接复用 AdapterUtils.mergeIngredients:该实现调用的 areIngredientsEqual + // 对自定义条目(如 neoforge:components 声明的组件型材料)直接判定为不相同, + // 其候选物品列表在该类型下仅作展示提示,因此组件型材料无法参与合并, + // 会退化为一材料一条输入。此处改用不排斥自定义条目的等价比较。 + List gathered = new ArrayList<>(); + for (IngredientBridge bridge : recipe.getGrid().getInputs()) { + Ingredient ingredient = itemIngredientOf(bridge); + if (ingredient != null) gathered.add(ingredient); + } + for (IngredientBridge bridge : recipe.getGrid().getRelayInputs()) { + Ingredient ingredient = itemIngredientOf(bridge); + if (ingredient != null) gathered.add(ingredient); + } + + // 附加输入以 count 声明份数,逐份展开后一并参与合并,保持数量语义。 + // 元素类型显式写为 SizedIngredient 而非 var:var 会把推断出的旧类 CountIngredient 写进字节码, + // 2.0.1.26 起 AS 已移除该类,运行期即 NoClassDefFoundError。 + for (SizedIngredient additional : recipe.getRequiredAdditionalInputs()) { + if (additional == null) continue; + Ingredient ingredient = additional.ingredient(); + if (AdapterUtils.isIngredientEmpty(ingredient)) continue; + for (int i = 0; i < additional.count(); i++) { + gathered.add(ingredient); + } + } + + return mergeIngredientsIncludingCustom(gathered); + } + + /** + * 合并语义等价的 Ingredient 并累计份数。 + * + *

与 {@link AdapterUtils#mergeIngredients(List)} 的差异在于自定义条目的处理: + * 后者把 {@code isCustom()} 的条目视为不可比较,因而组件型材料无法合并。 + * 此处在两种条目均为自定义类型时回退到候选物品与组件的逐一比对, + * 使组件型材料的重复出现能够归并为单条带份数的输入。

+ * + * @param gathered 逐槽位收集的输入材料,可含重复项 + * @return 按语义归并计数后的输入列表 + */ + private static List mergeIngredientsIncludingCustom(List gathered) { + List keys = new ArrayList<>(); + List counts = new ArrayList<>(); + + for (Ingredient ingredient : gathered) { + if (AdapterUtils.isIngredientEmpty(ingredient)) continue; + int matched = -1; + for (int i = 0; i < keys.size(); i++) { + if (sameMaterial(keys.get(i), ingredient)) { + matched = i; + break; + } + } + if (matched >= 0) { + counts.set(matched, counts.get(matched) + 1L); + } else { + keys.add(ingredient); + counts.add(1L); + } + } + + List merged = new ArrayList<>(); + for (int i = 0; i < keys.size(); i++) { + merged.add(new CountedIngredient(keys.get(i), counts.get(i))); + } + return List.copyOf(merged); + } + + /** + * 判定两个 Ingredient 是否代表同一种材料。 + * + *

先交由 {@link AdapterUtils#areIngredientsEqual} 处理常规条目;该判定对自定义条目 + * 恒返回否,因此当两者均为自定义条目时,进一步比对候选物品集合:候选数量一致, + * 且每个候选物品及其组件都能在另一方找到,即视为同一种材料。

+ * + * @param a 第一个 Ingredient + * @param b 第二个 Ingredient + * @return 两者是否代表同一种材料 + */ + private static boolean sameMaterial(Ingredient a, Ingredient b) { + if (AdapterUtils.areIngredientsEqual(a, b)) return true; + if (!a.isCustom() || !b.isCustom()) return false; + + ItemStack[] stacksA = a.getItems(); + ItemStack[] stacksB = b.getItems(); + if (stacksA.length == 0 || stacksA.length != stacksB.length) return false; + + for (ItemStack stackA : stacksA) { + boolean found = false; + for (ItemStack stackB : stacksB) { + if (ItemStack.isSameItem(stackA, stackB) + && ItemStack.isSameItemSameTags(stackA, stackB)) { + found = true; + break; + } + } + if (!found) return false; + } + return true; + } + + /** 取出物品型桥接条目的 Ingredient;流体型或空条目返回 {@code null}。 */ + @Nullable + private static Ingredient itemIngredientOf(@Nullable IngredientBridge bridge) { + if (bridge == null || bridge.isEmpty()) return null; + if (bridge.getIngredientType() != IngredientBridge.Type.ITEM) return null; + Ingredient ingredient = bridge.getIngredient(); + return AdapterUtils.isIngredientEmpty(ingredient) ? null : ingredient; + } + + /** + * 构造配方所需的星能液输入。 + * + *

祭坛的 {@code requiredFluid} 是逐份列出的流体需求,同一流体可能出现多条, + * 因此按流体合并后作为合金炉的流体输入。

+ * + * @param recipe 源祭坛配方 + * @return 合并后的流体输入列表;无流体需求时返回空列表 + */ + private static List buildInputFluids(AltarRecipe recipe) { + List fluids = new ArrayList<>(); + for (FluidStack stack : recipe.getRequiredFluid()) { + if (stack == null || stack.isEmpty() || stack.getAmount() <= 0) continue; + fluids.add(LongSizedFluidIngredient.from(stack)); + } + return List.copyOf(fluids); + } + + /** + * 构造配方模具。 + * + *

第一级模具为配方所属祭坛等级对应的祭坛物品,用于把配方约束到正确的祭坛等级。

+ * + *

第二级模具承载星座维度。祭坛的聚焦星座来自专属聚焦槽:该槽位仅接受 + * {@code #astralsorcery:attuned_crystal}(即 {@code attuned_rock_crystal} 与 + * {@code attuned_celestial_crystal}),并从物品的 {@code ATTUNED_CONSTELLATION} + * 组件读出星座,再与 {@link AltarRecipe#getFocusConstellation()} 比对。合金炉没有 + * 等价的「玩家当前聚焦星座」输入,因此在配方要求聚焦星座时,追加一颗带该星座组件的 + * 调谐水晶石作为第二级模具,使星座限制在模具槽位上可被表达与校验。

+ * + *

多模具配方依赖多方块结构的模具仓承载各模具槽位:普通合金炉仅暴露单个模具槽, + * 相关配方在普通路径下不参与选择。这是二级模具语义的既定适用范围。

+ * + * @param recipe 源祭坛配方 + * @return 模具列表;祭坛物品不可用时返回空列表 + */ + private static List buildMolds(AltarRecipe recipe) { + List molds = new ArrayList<>(); + + TileAltar.AltarType requiredType = recipe.getRequiredType(); + if (requiredType != null) { + ItemStack altarItem = requiredType.getAltarItem(); + Ingredient altarMold = AdapterUtils.toMoldIngredient(altarItem); + if (!AdapterUtils.isIngredientEmpty(altarMold)) molds.add(altarMold); + } + + Ingredient constellationMold = constellationMold(recipe); + if (constellationMold != null) molds.add(constellationMold); + + return List.copyOf(molds); + } + + /** + * 按配方的聚焦星座要求构造第二级模具。 + * + *

模具为带 {@code ATTUNED_CONSTELLATION} 组件的调谐水晶石,组件值即配方要求的 + * 星座。模具槽位因此承载星座限制:放入的调谐水晶石必须携带同一星座才可匹配。

+ * + *

候选物品同时覆盖 {@code attuned_rock_crystal} 与 {@code attuned_celestial_crystal}: + * 两者同属 {@code #astralsorcery:attuned_crystal},即 {@code TileAltar} 专属聚焦槽的准入 + * 集合,且均以 {@code ATTUNED_CONSTELLATION} 组件承载星座。模具据此按物品集合与组件 + * 共同限定,任一种调谐水晶石携带指定星座即可满足。

+ * + * @param recipe 源祭坛配方 + * @return 调谐水晶石模具;配方不要求聚焦星座时返回 {@code null} + */ + @Nullable + private static Ingredient constellationMold(AltarRecipe recipe) { + BaseConstellation constellation = recipe.getFocusConstellation().orElse(null); + if (constellation == null) return null; + + List candidates = new ArrayList<>(2); + candidates.add(attunedCrystal(ItemsAS.ATTUNED_ROCK_CRYSTAL.get(), constellation)); + candidates.add(attunedCrystal(ItemsAS.ATTUNED_CELESTIAL_CRYSTAL.get(), constellation)); + + Ingredient mold = Ingredient.of(candidates.stream()); + return AdapterUtils.isIngredientEmpty(mold) ? null : mold; + } + + /** + * 构造一颗携带指定星座组件的调谐水晶石。 + * + * @param item 调谐水晶石物品 + * @param constellation 该水晶石承载的星座 + * @return 已写入 {@code ATTUNED_CONSTELLATION} 组件的物品栈 + */ + private static ItemStack attunedCrystal(Item item, BaseConstellation constellation) { + ItemStack crystal = new ItemStack(item); + crystal.set(DataComponentsAS.ATTUNED_CONSTELLATION, new AttunedConstellationComponent(constellation)); + return crystal; + } + + /** 把已计数输入转换为匹配所需的映射形式。 */ + private static Map toCountMap(List counted) { + Map map = new LinkedHashMap<>(); + for (CountedIngredient entry : counted) { + if (entry == null) continue; + AdapterUtils.mergeIngredient(map, entry.ingredient(), entry.count()); + } + return map; + } + + /** + * 解析加工耗时。 + * + *

祭坛配方的 {@code duration} 单位为游戏刻,与合金炉的加工时间单位一致; + * 非正值视为未配置并回落到默认耗时。

+ * + * @param recipe 源祭坛配方 + * @return 合金炉加工耗时(刻) + */ + private static int resolveProcessTime(AltarRecipe recipe) { + int duration = recipe.getDuration(); + return duration > 0 ? duration : AdapterUtils.DEFAULT_PROCESS_TIME; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/astralsorcery/InfusionRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/astralsorcery/InfusionRecipeAdapter.java new file mode 100644 index 00000000..6f140a11 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/astralsorcery/InfusionRecipeAdapter.java @@ -0,0 +1,261 @@ +package com.sorrowmist.useless.content.recipe.adapters.astralsorcery; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import hellfirepvp.astralsorcery.common.lib.BlocksAS; +import hellfirepvp.astralsorcery.common.lib.RecipeTypesAS; +import hellfirepvp.astralsorcery.common.recipe.infusion.InfusionRecipe; +import hellfirepvp.astralsorcery.common.tile.TileInfuser; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluid; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.FluidType; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * 将星能注入器(Astral Sorcery 的液态星能注入配方)转换为高级合金炉配方。 + * + *

注入器以物品作为核心输入,并以星能液作为消耗对象。配方可从两条互斥路径取液: + * 若附近的圣杯能够供给,则只扣减圣杯储罐,世界中的星能液方块不受影响;否则回落到逐个 + * 判定注液位、命中后将星能液方块替换为空气的路径。

+ * + *

该差异无法在配方的静态数据中统一表达,因此本适配器为每个源配方产出两条独立变体, + * 以模具构成区分取液路径,两条变体的流体折算口径不同:

+ *
    + *
  • 方块路径变体:模具仅包含注入器,按单次执行耗尽一个星能液方块折算,即一桶容量。 + * 该变体恒存在,与 {@code accept_chalice_input} 的取值无关。
  • + *
  • 圣杯路径变体:模具在注入器之外追加圣杯作为第二级模具,按 + * {@link InfusionRecipe#getChaliceInputFluidStack()} 声明的取液量折算, + * 该值即圣杯路径在单次执行中的实际扣减量,与方块路径的随机判定无关。 + * 该变体仅在配方允许圣杯供给且取液量大于零时产出。
  • + *
+ * + *

两条变体的配方 id 分别以 {@code _block} 与 {@code _chalice} 为后缀, + * 使其在配方目录中各自稳定可寻。

+ * + *

注入器配方不产生流体输出,因此转换结果的流体输出列表恒为空。

+ */ +public final class InfusionRecipeAdapter implements IRecipeAdapter { + + @Override + public String sourceId() { + return RecipeSourceIds.ASTRAL_SORCERY; + } + + @Override + public Class getRecipeClass() { + return InfusionRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(BlocksAS.INFUSER.get()); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) return List.of(); + + InfusionRecipe recipe = holder.value(); + Ingredient itemInput = recipe.getItemInput(); + ItemStack output = recipe.getOutput(); + + // 物品输入与产出缺一的配方在合金炉中不可执行,直接跳过。 + if (AdapterUtils.isIngredientEmpty(itemInput) || output.isEmpty()) return List.of(); + + // 方块取液路径恒存在:模具仅含注入器,按单次执行耗尽一个星能液方块折算,即固定一桶。 + // 概率非正表示配方不消耗流体,此时该路径不声明流体需求,但仍作为可执行变体保留。 + ResourceLocation baseId = AdapterUtils.convertedId(holder.id()); + List converted = new ArrayList<>(2); + converted.add(buildVariant(recipe, baseId.withSuffix("_block"), itemInput, output, + blockPathAmount(recipe), false)); + + // 圣杯取液路径为附加变体:仅当配方允许圣杯供给时存在,模具追加圣杯作为第二级模具, + // 流体按圣杯在单次执行中的实际取液量折算,即期望值口径。 + if (acceptsChalice(recipe)) { + int chaliceAmount = recipe.getChaliceInputFluidStack().getAmount(); + if (chaliceAmount > 0) { + converted.add(buildVariant(recipe, baseId.withSuffix("_chalice"), itemInput, output, + chaliceAmount, true)); + } + } + + return List.copyOf(converted); + } + + /** + * 构造单一取液路径对应的合金炉配方。 + * + *

两条路径共用物品输入与产出,差异仅在模具构成与流体折算量:方块路径的模具为注入器 + * 本体,圣杯路径额外要求圣杯作为二级模具。id 后缀与模具构成一一对应,使两条配方在配方 + * 目录中各自稳定可寻。

+ * + * @param recipe 源注入配方 + * @param id 该变体的配方 ID + * @param itemInput 物品输入 + * @param output 产出 + * @param fluidAmount 该路径的星能液折算量(毫桶) + * @param withChalice 是否追加圣杯作为二级模具 + * @return 转换后的合金炉配方 + */ + private static AdvancedAlloyFurnaceRecipe buildVariant(InfusionRecipe recipe, ResourceLocation id, + Ingredient itemInput, ItemStack output, + int fluidAmount, boolean withChalice) { + List inputFluids = fluidAmount > 0 + ? List.of(LongSizedFluidIngredient.from(new FluidStack(recipe.getFluidInput(), fluidAmount))) + : List.of(); + + return new AdvancedAlloyFurnaceRecipe( + id, + List.of(new CountedIngredient(itemInput, 1L)), + inputFluids, + List.of(), + List.of(output.copy()), + List.of(), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + resolveProcessTime(recipe), + Ingredient.EMPTY, + 0, + buildMolds(withChalice), + AlloyFurnaceMode.NORMAL); + } + + /** + * 构造配方模具。 + * + *

一级模具恒为注入器本体。当该变体对应圣杯取液路径时,追加圣杯作为二级模具, + * 使配方在合金炉中显式要求圣杯参与,并与按圣杯实际取液量折算的流体需求保持一致。

+ * + *

多模具配方依赖多方块结构的模具仓承载各模具槽位:普通合金炉仅暴露单个模具槽, + * 相关配方在普通路径下不参与选择。这是二级模具语义的既定适用范围。

+ * + * @param withChalice 是否追加圣杯作为二级模具 + * @return 模具列表 + */ + private static List buildMolds(boolean withChalice) { + List molds = new ArrayList<>(); + molds.add(AdapterUtils.toMoldIngredient(new ItemStack(BlocksAS.INFUSER.get()))); + + if (withChalice) { + Ingredient chaliceMold = AdapterUtils.toMoldIngredient(new ItemStack(BlocksAS.CHALICE.get())); + if (!AdapterUtils.isIngredientEmpty(chaliceMold)) molds.add(chaliceMold); + } + + return List.copyOf(molds); + } + + /** + * 判定配方是否接受圣杯供给。 + * + *

该判定同时决定模具构成与流体折算口径,是两条取液路径的分流依据。

+ * + * @param recipe 源注入配方 + * @return 配方是否允许圣杯参与取液 + */ + private static boolean acceptsChalice(InfusionRecipe recipe) { + return recipe.acceptChaliceInput() && recipe.getFluidConsumptionChance() > 0F; + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) return List.of(); + if (mergedInputs == null || mergedInputs.isEmpty()) return List.of(); + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(level.getRecipeManager(), RecipeTypesAS.INFUSION_TYPE.get())) { + InfusionRecipe recipe = holder.value(); + if (recipe == null || AdapterUtils.isIngredientEmpty(recipe.getItemInput())) continue; + if (!AdapterUtils.matchesRequired(mergedInputs, Map.of(recipe.getItemInput(), 1L))) continue; + + // 一条源配方对应两条变体(方块路径与圣杯路径),各自的流体需求不同; + // 只要任一变体的流体需求被满足,该源配方即存在可执行路径。 + boolean fluidSatisfied = AdapterUtils.matchesLongFluidIngredients( + mergedFluids, buildInputFluids(recipe, false)); + if (!fluidSatisfied && acceptsChalice(recipe)) { + fluidSatisfied = AdapterUtils.matchesLongFluidIngredients( + mergedFluids, buildInputFluids(recipe, true)); + } + if (!fluidSatisfied) continue; + + matches.add(holder); + } + return List.copyOf(matches); + } + + /** + * 构造配方所需的星能液输入。 + * + *

折算口径随圣杯是否参与而分流,与 {@link #acceptsChalice(InfusionRecipe)} 的判定保持一致:

+ *
    + *
  • 圣杯不参与时,注入器只走方块消耗路径:在 {@link TileInfuser#getLiquidOffsets()} + * 的各个注液位上逐个判定,命中后把该位置的星能液方块替换为空气。此处以单次执行 + * 耗尽一个星能液方块为口径,即一桶容量;{@code consume_multiple_fluids} 为真时, + * 多个注液位会在同一次执行中依次被耗尽。
  • + *
  • 圣杯参与时,取液量由 {@link InfusionRecipe#getChaliceInputFluidStack()} 给出, + * 该值是圣杯路径在单次执行中的实际扣减量,与方块路径的随机判定无关, + * 因此直接采用而不再按 {@code fluid_consumption_chance} 二次折算。
  • + *
+ * + *

两条路径各自对应一条独立的合金炉配方,因此本方法按路径分别求值。

+ * + * @param recipe 源注入配方 + * @param withChalice 是否按圣杯取液路径折算 + * @return 星能液输入列表;不需要消耗流体时返回空列表 + */ + private static List buildInputFluids(InfusionRecipe recipe, boolean withChalice) { + Fluid fluid = recipe.getFluidInput(); + if (fluid == null) return List.of(); + + // 概率非正表示该配方不消耗流体,无需声明流体输入。 + if (recipe.getFluidConsumptionChance() <= 0F) return List.of(); + + int amount = withChalice + ? recipe.getChaliceInputFluidStack().getAmount() + : blockPathAmount(recipe); + if (amount <= 0) return List.of(); + + return List.of(LongSizedFluidIngredient.from(new FluidStack(fluid, amount))); + } + + /** + * 计算方块消耗路径下单次执行的星能液折算量。 + * + * @param recipe 源注入配方 + * @return 折算量(毫桶) + */ + private static int blockPathAmount(InfusionRecipe recipe) { + int liquidSlots = Math.max(1, TileInfuser.getLiquidOffsets().size()); + int consumedBlocks = recipe.consumeMultipleFluids() ? liquidSlots : 1; + return consumedBlocks * FluidType.BUCKET_VOLUME; + } + + /** + * 解析加工耗时。 + * + *

注入器的 {@code duration} 单位为游戏刻,合金炉以刻为加工时间单位,二者语义一致; + * 非正值视为未配置并回落到默认耗时。

+ * + * @param recipe 源注入配方 + * @return 合金炉加工耗时(刻) + */ + private static int resolveProcessTime(InfusionRecipe recipe) { + int duration = recipe.getDuration(); + return duration > 0 ? duration : AdapterUtils.DEFAULT_PROCESS_TIME; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/astralsorcery/LightwellRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/astralsorcery/LightwellRecipeAdapter.java new file mode 100644 index 00000000..1dff634c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/astralsorcery/LightwellRecipeAdapter.java @@ -0,0 +1,119 @@ +package com.sorrowmist.useless.content.recipe.adapters.astralsorcery; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import hellfirepvp.astralsorcery.common.lib.BlocksAS; +import hellfirepvp.astralsorcery.common.lib.RecipeTypesAS; +import hellfirepvp.astralsorcery.common.recipe.lightwell.LightwellRecipe; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluid; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * 将聚星缸(Astral Sorcery 的 Lightwell 流明井配方)转换为高级合金炉配方。 + * + *

聚星缸以催化物品为输入、以流体为唯一产出。源配方本身只声明产出流体与生产倍率, + * 不含确定的产出量:实际产量由方块按水晶石阶数计算后再乘以该倍率。本适配器以固定基准容量 + * 乘以生产倍率的方式还原产出量,使转换结果具备稳定的流体输出语义。

+ * + *

聚星缸配方不消耗流体,因此转换结果的流体输入列表恒为空。催化物品的星辉属性组件 + * 不参与匹配,源配方本身也只按物品标签判定,因此同一标签下的不同组件物品共用一条转换配方。

+ */ +public final class LightwellRecipeAdapter implements IRecipeAdapter { + + /** 生产倍率折算基准容量(mB)。 */ + private static final int PRODUCTION_BASE_AMOUNT = 1000; + + @Override + public String sourceId() { + return RecipeSourceIds.ASTRAL_SORCERY; + } + + @Override + public Class getRecipeClass() { + return LightwellRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(BlocksAS.LIGHTWELL.get()); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) return List.of(); + + LightwellRecipe recipe = holder.value(); + Ingredient input = recipe.getInput(); + if (AdapterUtils.isIngredientEmpty(input)) return List.of(); + + List outputFluids = buildOutputFluids(recipe); + // 无有效流体产出的配方在合金炉中不产生任何结果,直接跳过。 + if (outputFluids.isEmpty()) return List.of(); + + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + List.of(new CountedIngredient(input, 1L)), + List.of(), + List.of(), + List.of(), + outputFluids, + List.of(), + AdapterUtils.DEFAULT_ENERGY, + AdapterUtils.DEFAULT_PROCESS_TIME, + Ingredient.EMPTY, + 0, + AdapterUtils.toMoldIngredient(getMoldItem()), + AlloyFurnaceMode.NORMAL)); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) return List.of(); + if (mergedInputs == null || mergedInputs.isEmpty()) return List.of(); + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(level.getRecipeManager(), RecipeTypesAS.LIGHTWELL_TYPE.get())) { + LightwellRecipe recipe = holder.value(); + if (recipe == null || AdapterUtils.isIngredientEmpty(recipe.getInput())) continue; + if (!AdapterUtils.matchesRequired(mergedInputs, Map.of(recipe.getInput(), 1L))) continue; + matches.add(holder); + } + return List.copyOf(matches); + } + + /** + * 构造配方产出的流体。 + * + *

产出量按基准容量与生产倍率的乘积向上取整;倍率非正或结果不足 1mB 时视为无产出。

+ * + * @param recipe 源聚星缸配方 + * @return 流体产出列表;无有效产出时返回空列表 + */ + private static List buildOutputFluids(LightwellRecipe recipe) { + Fluid fluid = recipe.getGeneratedFluid(); + if (fluid == null) return List.of(); + + float multiplier = recipe.getProductionMultiplier(); + if (multiplier <= 0F) return List.of(); + + int amount = (int) Math.ceil(PRODUCTION_BASE_AMOUNT * multiplier); + if (amount <= 0) return List.of(); + + return List.of(new FluidStack(fluid, amount)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/avaritia/ReAvaritiaCollectorRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/avaritia/ReAvaritiaCollectorRecipeAdapter.java new file mode 100644 index 00000000..4817aa9a --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/avaritia/ReAvaritiaCollectorRecipeAdapter.java @@ -0,0 +1,144 @@ +package com.sorrowmist.useless.content.recipe.adapters.avaritia; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import committee.nova.mods.avaritia.init.registry.ModBlocks; +import committee.nova.mods.avaritia.init.registry.enums.CollectorTier; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Exposes the four passive neutron collector tiers as timed alloy-furnace recipes. */ +public final class ReAvaritiaCollectorRecipeAdapter + implements IRecipeAdapter { + private static final long WATER_AMOUNT = 1_000L; + + @Override + public String sourceId() { + return RecipeSourceIds.AVARITIA; + } + + @Override + public Class getRecipeClass() { + return ReAvaritiaSyntheticRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return null; + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + return tierFor(mold) != null; + } + + @Override + public List> getGeneratedRecipes(Level level) { + if (level == null) { + return List.of(); + } + + List> result = new ArrayList<>(); + for (CollectorTier tier : CollectorTier.values()) { + ItemStack output = production(tier); + if (output.isEmpty()) { + continue; + } + AdvancedAlloyFurnaceRecipe converted = new AdvancedAlloyFurnaceRecipe( + id(tier), + List.of(), + List.of(new LongSizedFluidIngredient( + FluidIngredient.tag(FluidTags.WATER), WATER_AMOUNT)), + List.of(output), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + Math.max(1, tier.production_ticks), + Ingredient.EMPTY, + 0, + AdapterUtils.toMoldIngredient(collectorMold(tier)), + AlloyFurnaceMode.NORMAL); + result.add(new RecipeHolder<>(converted.id(), new ReAvaritiaSyntheticRecipe(converted))); + } + return List.copyOf(result); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null || holder.value().convertedRecipe() == null) { + return List.of(); + } + return List.of(holder.value().convertedRecipe()); + } + + @Override + public List> findMatchingRecipes( + Level level, + Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + CollectorTier tier = tierFor(mold); + if (level == null || tier == null) { + return List.of(); + } + + for (RecipeHolder holder : getGeneratedRecipes(level)) { + if (holder.value().convertedRecipe().mold().test(mold)) { + return List.of(holder); + } + } + return List.of(); + } + + private static ResourceLocation id(CollectorTier tier) { + return new ResourceLocation( + RecipeSourceIds.AVARITIA, + "neutron_collector/" + tier.name + "_converted"); + } + + private static ItemStack production(CollectorTier tier) { + ItemStack[] items = tier.production.getItems(); + if (items == null || items.length == 0 || items[0].isEmpty()) { + return ItemStack.EMPTY; + } + return items[0].copyWithCount(1); + } + + private static ItemStack collectorMold(CollectorTier tier) { + return switch (tier) { + case DEFAULT -> new ItemStack(ModBlocks.neutron_collector.get()); + case DENSE -> new ItemStack(ModBlocks.dense_neutron_collector.get()); + case DENSER -> new ItemStack(ModBlocks.denser_neutron_collector.get()); + case DENSEST -> new ItemStack(ModBlocks.densest_neutron_collector.get()); + }; + } + + @Nullable + private static CollectorTier tierFor(@Nullable ItemStack mold) { + if (mold == null || mold.isEmpty()) { + return null; + } + for (CollectorTier tier : CollectorTier.values()) { + if (collectorMold(tier).is(mold.getItem())) { + return tier; + } + } + return null; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/avaritia/ReAvaritiaCompressorRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/avaritia/ReAvaritiaCompressorRecipeAdapter.java new file mode 100644 index 00000000..e0e45e4b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/avaritia/ReAvaritiaCompressorRecipeAdapter.java @@ -0,0 +1,225 @@ +package com.sorrowmist.useless.content.recipe.adapters.avaritia; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.extendedcrafting.ExtendedCraftingAdapterUtils; +import committee.nova.mods.avaritia.api.common.crafting.ICompressorRecipe; +import committee.nova.mods.avaritia.init.registry.ModBlocks; +import committee.nova.mods.avaritia.init.registry.ModRecipeTypes; +import committee.nova.mods.avaritia.init.registry.enums.CompressorTier; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Converts Re-Avaritia neutron compressor recipes and all four machine tiers. */ +public final class ReAvaritiaCompressorRecipeAdapter + implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + + @Override + public String sourceId() { + return RecipeSourceIds.AVARITIA; + } + + @Override + public Class getRecipeClass() { + return ICompressorRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return null; + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + if (mold == null || mold.isEmpty()) { + return false; + } + for (CompressorTier tier : CompressorTier.values()) { + if (compressorMold(tier).is(mold.getItem())) { + return true; + } + } + return false; + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + + List converted = new ArrayList<>(); + for (CompressorTier tier : CompressorTier.values()) { + Converted data = convertData(holder.value(), tier); + if (data == null) { + RateLimitedLogger.warn(LOGGER, "Skipping invalid Re-Avaritia compressor recipe {} ({})", holder.id(), tier); + continue; + } + converted.add(new AdvancedAlloyFurnaceRecipe( + variantId(holder, tier), + data.inputs(), + List.of(), + data.outputs(), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + data.processTime(), + Ingredient.EMPTY, + 0, + AdapterUtils.toMoldIngredient(compressorMold(tier)), + AlloyFurnaceMode.NORMAL + )); + } + return List.copyOf(converted); + } + + @Override + public List> findMatchingRecipes( + Level level, + Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold) + || mergedInputs == null || mergedInputs.isEmpty()) { + return List.of(); + } + + CompressorTier tier = tierFor(mold); + if (tier == null) { + return List.of(); + } + + List> matches = new ArrayList<>(); + RecipeManager recipeManager = level.getRecipeManager(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + ModRecipeTypes.COMPRESSOR_RECIPE.get())) { + Converted converted = convertData(holder.value(), tier); + if (converted != null && AdapterUtils.matchesRequired(mergedInputs, converted.requirements())) { + matches.add(holder); + } + } + return matches; + } + + @Nullable + private static Converted convertData( + @Nullable ICompressorRecipe source, CompressorTier tier) { + if (source == null || tier == null) { + return null; + } + + Ingredient input; + int inputCount; + int baseTime; + ItemStack result; + try { + input = source.getInput(); + inputCount = source.getInputCount(); + baseTime = source.getTimeCost(); + result = ExtendedCraftingAdapterUtils.copyResult(source); + } catch (RuntimeException exception) { + return null; + } + if (input == null || input.isEmpty() || inputCount <= 0 || baseTime < 0 + || result.isEmpty() || result.getCount() <= 0) { + return null; + } + + long requiredInput = scaleCeiling(inputCount, tier.inputAmplifier); + int processTime = scaleCeilingToInt(baseTime, tier.timeAmplifier); + int outputCount = multiplyToInt(result.getCount(), tier.outputAmplifier); + if (requiredInput <= 0 || processTime <= 0 || outputCount <= 0) { + return null; + } + + result.setCount(outputCount); + Map requirements = new LinkedHashMap<>(); + AdapterUtils.mergeIngredient(requirements, input, requiredInput); + return new Converted( + List.of(new CountedIngredient(input, requiredInput)), + List.of(result), + requirements, + processTime); + } + + private static ResourceLocation variantId( + RecipeHolder holder, CompressorTier tier) { + return new net.minecraft.resources.ResourceLocation( + holder.id().getNamespace(), + holder.id().getPath() + "_" + tier.name + "_converted"); + } + + private static long scaleCeiling(long value, float multiplier) { + if (value < 0 || !Float.isFinite(multiplier) || multiplier <= 0f) { + return -1L; + } + double scaled = value * (double) multiplier; + if (!Double.isFinite(scaled) || scaled > Long.MAX_VALUE) { + return -1L; + } + return (long) Math.ceil(scaled); + } + + private static int scaleCeilingToInt(long value, float multiplier) { + long scaled = scaleCeiling(value, multiplier); + if (scaled <= 0) { + return -1; + } + return scaled >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) scaled; + } + + private static int multiplyToInt(int value, int multiplier) { + long result = (long) value * multiplier; + return result <= 0 || result > Integer.MAX_VALUE ? -1 : (int) result; + } + + @Nullable + private static CompressorTier tierFor(@Nullable ItemStack mold) { + if (mold == null || mold.isEmpty()) { + return null; + } + for (CompressorTier tier : CompressorTier.values()) { + if (compressorMold(tier).is(mold.getItem())) { + return tier; + } + } + return null; + } + + private static ItemStack compressorMold(CompressorTier tier) { + return switch (tier) { + case DEFAULT -> new ItemStack(ModBlocks.neutron_compressor.get()); + case DENSE -> new ItemStack(ModBlocks.dense_neutron_compressor.get()); + case DENSER -> new ItemStack(ModBlocks.denser_neutron_compressor.get()); + case DENSEST -> new ItemStack(ModBlocks.densest_neutron_compressor.get()); + }; + } + + private record Converted( + List inputs, + List outputs, + Map requirements, + int processTime) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/avaritia/ReAvaritiaExtremeSmithingRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/avaritia/ReAvaritiaExtremeSmithingRecipeAdapter.java new file mode 100644 index 00000000..96a5cac6 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/avaritia/ReAvaritiaExtremeSmithingRecipeAdapter.java @@ -0,0 +1,224 @@ +package com.sorrowmist.useless.content.recipe.adapters.avaritia; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.extendedcrafting.ExtendedCraftingAdapterUtils; +import committee.nova.mods.avaritia.common.crafting.input.ExtremeSmithingRecipeInput; +import committee.nova.mods.avaritia.common.crafting.recipe.ExtremeSmithingRecipe; +import committee.nova.mods.avaritia.init.registry.ModBlocks; +import committee.nova.mods.avaritia.init.registry.ModRecipeTypes; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + +/** Converts Re-Avaritia's five-slot extreme smithing recipes. */ +public final class ReAvaritiaExtremeSmithingRecipeAdapter + implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + + @Override + public String sourceId() { + return RecipeSourceIds.AVARITIA; + } + + @Override + public Class getRecipeClass() { + return ExtremeSmithingRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(ModBlocks.extreme_smithing_table.get()); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + return convertAll(holder, level, List.of()); + } + + @Override + public List convertAll( + RecipeHolder holder, + Level level, + List actualInputs) { + if (holder == null || holder.value() == null) { + return List.of(); + } + + Converted converted = convertData(holder.value()); + if (converted == null) { + RateLimitedLogger.warn(LOGGER, "Skipping invalid Re-Avaritia extreme smithing recipe: {}", holder.id()); + return List.of(); + } + + if (actualInputs != null && !actualInputs.isEmpty()) { + ExtremeSmithingRecipeInput input = resolveInput(holder.value(), actualInputs); + if (input == null || level == null || !holder.value().matches(input, level)) { + return List.of(); + } + ItemStack assembled; + try { + assembled = holder.value().assemble(input, level.registryAccess()); + } catch (RuntimeException exception) { + return List.of(); + } + if (assembled == null || assembled.isEmpty() || assembled.getCount() <= 0) { + return List.of(); + } + converted = converted.withOutputs(List.of(assembled.copy())); + } + + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + converted.inputs(), + List.of(), + converted.outputs(), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + AdapterUtils.DEFAULT_PROCESS_TIME, + Ingredient.EMPTY, + 0, + AdapterUtils.toMoldIngredient(getMoldItem()), + AlloyFurnaceMode.NORMAL + )); + } + + @Override + public List> findMatchingRecipes( + Level level, + Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold) + || mergedInputs == null || mergedInputs.isEmpty()) { + return List.of(); + } + + List> matches = new ArrayList<>(); + RecipeManager recipeManager = level.getRecipeManager(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + ModRecipeTypes.EXTREME_SMITHING_RECIPE.get())) { + Converted converted = convertData(holder.value()); + if (converted != null && AdapterUtils.matchesRequired(mergedInputs, converted.requirements())) { + matches.add(holder); + } + } + return matches; + } + + @Nullable + private static Converted convertData(@Nullable ExtremeSmithingRecipe source) { + if (source == null || source.template == null || source.base == null || source.additions == null + || source.template.isEmpty() || source.base.isEmpty() || source.additions.isEmpty()) { + return null; + } + + List slots; + try { + slots = source.getIngredients(); + } catch (RuntimeException exception) { + return null; + } + if (slots == null || slots.size() != 5 + || slots.stream().anyMatch(ingredient -> ingredient == null || ingredient.isEmpty())) { + return null; + } + + // Extreme smithing has five physical slots. The additions Ingredient is a three-item + // display/matching predicate, so using it with count=3 loses the three displayed inputs. + List inputs = new ArrayList<>(slots.size()); + Map requirements = new IdentityHashMap<>(); + for (Ingredient ingredient : slots) { + inputs.add(new CountedIngredient(ingredient, 1L)); + requirements.put(ingredient, requirements.getOrDefault(ingredient, 0L) + 1L); + } + + ItemStack result = ExtendedCraftingAdapterUtils.copyResult(source); + if (result.isEmpty() || result.getCount() <= 0) { + return null; + } + + return new Converted( + List.copyOf(inputs), + List.of(result), + Collections.unmodifiableMap(requirements)); + } + + @Nullable + private static ExtremeSmithingRecipeInput resolveInput( + ExtremeSmithingRecipe source, List actualInputs) { + List available = new ArrayList<>(); + for (ItemStack stack : actualInputs) { + if (stack != null && !stack.isEmpty() && stack.getCount() > 0) { + available.add(stack.copy()); + } + } + if (available.isEmpty()) { + return null; + } + + Ingredient[] slots = { + source.template, + source.base, + source.additions, + source.additions, + source.additions + }; + ItemStack[] selected = new ItemStack[slots.length]; + if (!assignSlot(0, slots, available, selected)) { + return null; + } + return new ExtremeSmithingRecipeInput( + selected[0], selected[1], selected[2], selected[3], selected[4]); + } + + private static boolean assignSlot( + int slot, Ingredient[] ingredients, List available, ItemStack[] selected) { + if (slot >= ingredients.length) { + return true; + } + Ingredient ingredient = ingredients[slot]; + for (int index = 0; index < available.size(); index++) { + ItemStack candidate = available.get(index); + if (!ingredient.test(candidate)) { + continue; + } + candidate.shrink(1); + selected[slot] = candidate.copyWithCount(1); + if (assignSlot(slot + 1, ingredients, available, selected)) { + return true; + } + candidate.grow(1); + } + selected[slot] = ItemStack.EMPTY; + return false; + } + + private record Converted( + List inputs, + List outputs, + Map requirements) { + private Converted withOutputs(List replacement) { + return new Converted(inputs, replacement, requirements); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/avaritia/ReAvaritiaSculkCraftingRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/avaritia/ReAvaritiaSculkCraftingRecipeAdapter.java new file mode 100644 index 00000000..c9257339 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/avaritia/ReAvaritiaSculkCraftingRecipeAdapter.java @@ -0,0 +1,172 @@ +package com.sorrowmist.useless.content.recipe.adapters.avaritia; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.extendedcrafting.ExtendedCraftingAdapterUtils; +import committee.nova.mods.avaritia.init.registry.ModBlocks; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.CraftingRecipe; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** Converts the vanilla shaped recipe used to create Re-Avaritia's sculk table. */ +public final class ReAvaritiaSculkCraftingRecipeAdapter implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + + @Override + public String sourceId() { + return RecipeSourceIds.AVARITIA; + } + + @Override + public Class getRecipeClass() { + return CraftingRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(ModBlocks.sculk_crafting_table.get()); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || !isSculkTableRecipe(holder.value())) { + return List.of(); + } + Converted converted = convertData(holder.value()); + if (converted == null) { + RateLimitedLogger.warn(LOGGER, "Skipping invalid Re-Avaritia sculk crafting recipe: {}", holder.id()); + return List.of(); + } + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + converted.inputs(), + List.of(), + converted.outputs(), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + AdapterUtils.DEFAULT_PROCESS_TIME, + Ingredient.EMPTY, + 0, + AdapterUtils.toMoldIngredient(getMoldItem()), + AlloyFurnaceMode.NORMAL + )); + } + + @Override + public List> findMatchingRecipes( + Level level, + Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold) + || mergedInputs == null || mergedInputs.isEmpty()) { + return List.of(); + } + + List> matches = new ArrayList<>(); + RecipeManager recipeManager = level.getRecipeManager(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, RecipeType.CRAFTING)) { + if (!isSculkTableRecipe(holder.value())) { + continue; + } + Converted converted = convertData(holder.value()); + if (converted != null && AdapterUtils.matchesRequired(mergedInputs, converted.requirements())) { + matches.add(holder); + } + } + return matches; + } + + private static boolean isSculkTableRecipe(@Nullable CraftingRecipe recipe) { + return recipe != null + && ExtendedCraftingAdapterUtils.copyResult(recipe).is(ModBlocks.sculk_crafting_table.get().asItem()); + } + + @Nullable + private static Converted convertData(@Nullable CraftingRecipe source) { + if (source == null) { + return null; + } + + List ingredients; + try { + ingredients = source.getIngredients(); + } catch (RuntimeException exception) { + return null; + } + if (ingredients == null || ingredients.isEmpty()) { + return null; + } + + Map requirements = new LinkedHashMap<>(); + if (!ExtendedCraftingAdapterUtils.mergeIngredients(requirements, ingredients)) { + return null; + } + + ItemStack result = ExtendedCraftingAdapterUtils.copyResult(source); + if (result.isEmpty() || result.getCount() <= 0) { + return null; + } + + Optional> remainders; + try { + remainders = ExtendedCraftingAdapterUtils.deterministicRemainders( + ingredients, + Set.of(), + stacks -> { + int[] dimensions = ExtendedCraftingAdapterUtils.gridDimensions(source, stacks.size()); + return com.sorrowmist.useless.compat.forge1201.recipe.CraftingInputs.of(dimensions[0], dimensions[1], stacks); + }, + source::getRemainingItems + ); + } catch (RuntimeException exception) { + return null; + } + if (remainders.isEmpty()) { + return null; + } + + List outputs = new ArrayList<>(); + if (!ExtendedCraftingAdapterUtils.mergeOutput(outputs, result)) { + return null; + } + for (ItemStack remainder : remainders.get()) { + if (!ExtendedCraftingAdapterUtils.mergeOutput(outputs, remainder)) { + return null; + } + } + + return new Converted( + ExtendedCraftingAdapterUtils.countedIngredients(requirements), + outputs, + requirements); + } + + private record Converted( + List inputs, + List outputs, + Map requirements) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/avaritia/ReAvaritiaSyntheticRecipe.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/avaritia/ReAvaritiaSyntheticRecipe.java new file mode 100644 index 00000000..3e4327da --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/avaritia/ReAvaritiaSyntheticRecipe.java @@ -0,0 +1,55 @@ +package com.sorrowmist.useless.content.recipe.adapters.avaritia; + +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.Container; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; + +/** Recipe-manager payload for Re-Avaritia interactions that are not recipe entries. */ +public final class ReAvaritiaSyntheticRecipe implements Recipe { + private final AdvancedAlloyFurnaceRecipe convertedRecipe; + + public ReAvaritiaSyntheticRecipe(AdvancedAlloyFurnaceRecipe convertedRecipe) { + this.convertedRecipe = convertedRecipe; + } + + public AdvancedAlloyFurnaceRecipe convertedRecipe() { + return convertedRecipe; + } + + @Override + public net.minecraft.resources.ResourceLocation getId() { return convertedRecipe.id(); } + + @Override + public boolean matches(Container input, Level level) { + return convertedRecipe != null && convertedRecipe.matches(input, level); + } + + @Override + public ItemStack assemble(Container input, net.minecraft.core.RegistryAccess registries) { + return convertedRecipe == null ? ItemStack.EMPTY : convertedRecipe.assemble(input, registries); + } + + @Override + public boolean canCraftInDimensions(int width, int height) { + return convertedRecipe != null && convertedRecipe.canCraftInDimensions(width, height); + } + + @Override + public ItemStack getResultItem(net.minecraft.core.RegistryAccess registries) { + return convertedRecipe == null ? ItemStack.EMPTY : convertedRecipe.getResultItem(registries); + } + + @Override + public RecipeSerializer getSerializer() { + return convertedRecipe == null ? null : convertedRecipe.getSerializer(); + } + + @Override + public RecipeType getType() { + return convertedRecipe == null ? null : convertedRecipe.getType(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/avaritia/ReAvaritiaTableRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/avaritia/ReAvaritiaTableRecipeAdapter.java new file mode 100644 index 00000000..8e59d725 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/avaritia/ReAvaritiaTableRecipeAdapter.java @@ -0,0 +1,315 @@ +package com.sorrowmist.useless.content.recipe.adapters.avaritia; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.extendedcrafting.ExtendedCraftingAdapterUtils; +import committee.nova.mods.avaritia.api.common.crafting.ITierCraftingRecipe; +import committee.nova.mods.avaritia.api.common.crafting.TierInput; +import committee.nova.mods.avaritia.common.ingredient.StackIngredient; +import committee.nova.mods.avaritia.init.registry.ModBlocks; +import committee.nova.mods.avaritia.init.registry.ModRecipeTypes; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import com.sorrowmist.useless.compat.forge1201.recipe.DataComponentIngredient; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** Converts Re-Avaritia's tiered (sculk through extreme) crafting recipes. */ +public final class ReAvaritiaTableRecipeAdapter implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final int MAX_TABLE_SLOTS = 81; + + @Override + public String sourceId() { + return RecipeSourceIds.AVARITIA; + } + + @Override + public Class getRecipeClass() { + return ITierCraftingRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return null; + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + if (mold == null || mold.isEmpty()) { + return false; + } + for (int tier = 1; tier <= 4; tier++) { + if (tableMold(tier).is(mold.getItem())) { + return true; + } + } + return false; + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + Converted converted = convertData(holder.value()); + if (converted == null) { + RateLimitedLogger.warn(LOGGER, "Skipping invalid Re-Avaritia table recipe: {}", holder.id()); + return List.of(); + } + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + converted.inputs(), + List.of(), + converted.outputs(), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + AdapterUtils.DEFAULT_PROCESS_TIME, + Ingredient.EMPTY, + 0, + AdapterUtils.toMoldIngredient(converted.mold()), + AlloyFurnaceMode.NORMAL + )); + } + + @Override + public List> findMatchingRecipes( + Level level, + Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold) + || mergedInputs == null || mergedInputs.isEmpty()) { + return List.of(); + } + + List> matches = new ArrayList<>(); + RecipeManager recipeManager = level.getRecipeManager(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + ModRecipeTypes.CRAFTING_TABLE_RECIPE.get())) { + Converted converted = convertData(holder.value()); + if (converted != null + && converted.mold().is(mold.getItem()) + && AdapterUtils.matchesRequired(mergedInputs, converted.requirements())) { + matches.add(holder); + } + } + return matches; + } + + @Nullable + private static Converted convertData(@Nullable ITierCraftingRecipe source) { + if (source == null) { + return null; + } + + final int tier; + try { + tier = source.getTier(); + } catch (RuntimeException exception) { + return null; + } + if (tier < 1 || tier > 4) { + return null; + } + + List ingredients; + try { + ingredients = source.getIngredients(); + } catch (RuntimeException exception) { + return null; + } + if (ingredients == null || ingredients.isEmpty() || ingredients.size() > MAX_TABLE_SLOTS) { + return null; + } + + // Ingredient.equals() does not include item components. Re-Avaritia represents + // singularities as the same item with a different SINGULARITY_ID component, so a normal + // equality-based map would collapse all of them into one requirement. The identity map is + // only the storage layer; equal exact stacks are still merged explicitly below. + Map requirements = new IdentityHashMap<>(); + if (!mergeTableIngredients(requirements, ingredients)) { + return null; + } + List inputs = ExtendedCraftingAdapterUtils.countedIngredients(requirements); + if (inputs.isEmpty() || requirements.isEmpty()) { + return null; + } + + ItemStack result = ExtendedCraftingAdapterUtils.copyResult(source); + if (result.isEmpty() || result.getCount() <= 0) { + return null; + } + + Optional> remainders; + try { + remainders = ExtendedCraftingAdapterUtils.deterministicRemainders( + ingredients, + Set.of(), + stacks -> { + int[] dimensions = ExtendedCraftingAdapterUtils.gridDimensions(source, stacks.size()); + return TierInput.of(dimensions[0], dimensions[1], stacks, tier); + }, + source::getRemainingItems + ); + } catch (RuntimeException exception) { + return null; + } + if (remainders.isEmpty()) { + return null; + } + + List outputs = new ArrayList<>(); + if (!ExtendedCraftingAdapterUtils.mergeOutput(outputs, result)) { + return null; + } + for (ItemStack remainder : remainders.get()) { + if (!ExtendedCraftingAdapterUtils.mergeOutput(outputs, remainder)) { + return null; + } + } + + return new Converted( + inputs, + outputs, + requirements, + tableMold(tier)); + } + + private static boolean mergeTableIngredients( + Map target, List ingredients) { + if (target == null || ingredients == null) { + return false; + } + + boolean found = false; + for (Ingredient ingredient : ingredients) { + if (ingredient == null || ingredient.isEmpty()) { + continue; + } + if (!mergeExactStackIngredient(target, ingredient)) { + mergeNonExactIngredient(target, ingredient); + } + found = true; + } + return found; + } + + private static void mergeNonExactIngredient( + Map target, Ingredient ingredient) { + for (Map.Entry entry : target.entrySet()) { + if (AdapterUtils.areIngredientsEqual(entry.getKey(), ingredient)) { + entry.setValue(entry.getValue() + 1L); + return; + } + } + target.put(ingredient, 1L); + } + + /** + * Aggregates ingredients whose semantics identify one exact item stack. Ingredient equality + * ignores item components for vanilla item values, while Re-Avaritia's StackIngredient is + * custom and therefore intentionally opaque to the generic equality helper. Comparing the + * canonical stack makes both representations follow the same rule without treating tags or + * arbitrary custom display candidates as exact identities. + */ + private static boolean mergeExactStackIngredient( + Map target, Ingredient ingredient) { + Optional currentStack = exactStack(ingredient); + if (currentStack.isEmpty()) { + return false; + } + + for (Map.Entry entry : target.entrySet()) { + Optional existingStack = exactStack(entry.getKey()); + if (existingStack.isEmpty()) { + continue; + } + if (ItemStack.isSameItemSameTags(existingStack.get(), currentStack.get())) { + entry.setValue(entry.getValue() + 1L); + return true; + } + } + + target.put(ingredient, 1L); + return true; + } + + private static Optional exactStack(@Nullable Ingredient ingredient) { + if (ingredient == null || ingredient.isEmpty()) { + return Optional.empty(); + } + + if (ingredient.getCustomIngredient() instanceof StackIngredient) { + return singleCandidate(ingredient); + } + if (ingredient.getCustomIngredient() instanceof DataComponentIngredient dataComponent) { + return dataComponent.isStrict() ? singleCandidate(ingredient) : Optional.empty(); + } + if (ingredient.isCustom()) { + return Optional.empty(); + } + + try { + Ingredient.Value[] values = ingredient.getValues(); + if (values.length != 1 || !(values[0] instanceof Ingredient.ItemValue)) { + return Optional.empty(); + } + } catch (RuntimeException exception) { + return Optional.empty(); + } + return singleCandidate(ingredient); + } + + private static Optional singleCandidate(@Nullable Ingredient ingredient) { + if (ingredient == null) { + return Optional.empty(); + } + try { + ItemStack[] candidates = ingredient.getItems(); + if (candidates.length != 1 || candidates[0] == null || candidates[0].isEmpty()) { + return Optional.empty(); + } + return Optional.of(candidates[0].copyWithCount(1)); + } catch (RuntimeException exception) { + return Optional.empty(); + } + } + + private static ItemStack tableMold(int tier) { + return switch (tier) { + case 1 -> new ItemStack(ModBlocks.sculk_crafting_table.get()); + case 2 -> new ItemStack(ModBlocks.nether_crafting_table.get()); + case 3 -> new ItemStack(ModBlocks.end_crafting_table.get()); + case 4 -> new ItemStack(ModBlocks.extreme_crafting_table.get()); + default -> ItemStack.EMPTY; + }; + } + + private record Converted( + List inputs, + List outputs, + Map requirements, + ItemStack mold) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/botanypots/BotanyPotsCropSyntheticRecipe.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/botanypots/BotanyPotsCropSyntheticRecipe.java new file mode 100644 index 00000000..417829fb --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/botanypots/BotanyPotsCropSyntheticRecipe.java @@ -0,0 +1,55 @@ +package com.sorrowmist.useless.content.recipe.adapters.botanypots; + +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.Container; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; + +/** 承载已转换的植物盆作物配方;作物配方属于 Botany Pots 自有配方类型,不参与原版配方匹配。 */ +public final class BotanyPotsCropSyntheticRecipe implements Recipe { + private final AdvancedAlloyFurnaceRecipe convertedRecipe; + + public BotanyPotsCropSyntheticRecipe(AdvancedAlloyFurnaceRecipe convertedRecipe) { + this.convertedRecipe = convertedRecipe; + } + + public AdvancedAlloyFurnaceRecipe convertedRecipe() { + return convertedRecipe; + } + + @Override + public net.minecraft.resources.ResourceLocation getId() { return convertedRecipe.id(); } + + @Override + public boolean matches(Container input, Level level) { + return convertedRecipe != null && convertedRecipe.matches(input, level); + } + + @Override + public ItemStack assemble(Container input, net.minecraft.core.RegistryAccess registries) { + return convertedRecipe == null ? ItemStack.EMPTY : convertedRecipe.assemble(input, registries); + } + + @Override + public boolean canCraftInDimensions(int width, int height) { + return convertedRecipe != null && convertedRecipe.canCraftInDimensions(width, height); + } + + @Override + public ItemStack getResultItem(net.minecraft.core.RegistryAccess registries) { + return convertedRecipe == null ? ItemStack.EMPTY : convertedRecipe.getResultItem(registries); + } + + @Override + public RecipeSerializer getSerializer() { + return convertedRecipe == null ? null : convertedRecipe.getSerializer(); + } + + @Override + public RecipeType getType() { + return convertedRecipe == null ? null : convertedRecipe.getType(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/botanypots/BotanyPotsRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/botanypots/BotanyPotsRecipeAdapter.java new file mode 100644 index 00000000..7ffa9609 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/botanypots/BotanyPotsRecipeAdapter.java @@ -0,0 +1,69 @@ +package com.sorrowmist.useless.content.recipe.adapters.botanypots; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess; +import com.sorrowmist.useless.content.recipe.*; +import net.darkhax.botanypots.BotanyPotHelper; +import net.darkhax.botanypots.block.BlockBotanyPot; +import net.darkhax.botanypots.data.recipes.crop.BasicCrop; +import net.darkhax.botanypots.data.recipes.crop.Crop; +import net.darkhax.botanypots.data.recipes.crop.HarvestEntry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluids; +import java.util.*; + +/** Native BotanyPots 13 crops: retain seed predicates, stack NBT and harvest probabilities. */ +public final class BotanyPotsRecipeAdapter implements IRecipeAdapter { + private static volatile Ingredient potMoldCache; + @Override public String sourceId(){return RecipeSourceIds.BOTANY_POTS;} + @Override public Class getRecipeClass(){return BotanyPotsCropSyntheticRecipe.class;} + @Override public ItemStack getMoldItem(){return null;} + @Override public List> getGeneratedRecipes(Level level){ + if(level==null) return List.of(); + List> result=new ArrayList<>(); + for(RecipeHolder holder:RecipeManagerAccess.getAllRecipesFor(level.getRecipeManager(),BotanyPotHelper.CROP_TYPE.get())){ + if(!(holder.value() instanceof BasicCrop crop)) continue; + var converted=convertCrop(holder.id(),crop,potMold()); + if(converted!=null) result.add(new RecipeHolder<>(converted.id(),new BotanyPotsCropSyntheticRecipe(converted))); + } + return List.copyOf(result); + } + @Override public List convertAll(RecipeHolder holder,Level level){ + return holder==null || holder.value().convertedRecipe()==null?List.of():List.of(holder.value().convertedRecipe()); + } + static AdvancedAlloyFurnaceRecipe convertCrop(net.minecraft.resources.ResourceLocation source,BasicCrop crop,Ingredient pot){ + if(crop.getSeed().isEmpty() || pot.isEmpty()) return null; + var scaled=ExpectedOutputScaler.scale(collectDrops(crop.getResults())); + if(scaled.isEmpty() || scaled.get().outputs().isEmpty()) return null; + var batch=scaled.get(); + return new AdvancedAlloyFurnaceRecipe(AdapterUtils.convertedId(source),List.of(), + List.of(new LongSizedFluidIngredient(FluidIngredient.single(Fluids.WATER),Math.multiplyExact(1000L,batch.operations()))), + List.of(),batch.outputs(),List.of(),List.of(),Math.multiplyExact((long)AdapterUtils.DEFAULT_ENERGY,batch.operations()), + AdapterUtils.safeInt(Math.multiplyExact((long)Math.max(1,crop.getGrowthTicks()),batch.operations())), + Ingredient.EMPTY,0,List.of(pot,crop.getSeed()),AlloyFurnaceMode.NORMAL); + } + static List collectDrops(List entries){ + List result=new ArrayList<>(); + for(var entry:entries){ + var stack=entry.getItem(); + if(stack==null || stack.isEmpty()) continue; + // Native generateDrops rolls min..max copies of the entire item stack. + int min=Math.multiplyExact(entry.getMinRolls(),stack.getCount()); + int max=Math.multiplyExact(entry.getMaxRolls(),stack.getCount()); + result.add(new ExpectedOutputScaler.WeightedItemOutput(stack.copyWithCount(1),min,max,entry.getChance())); + } + return List.copyOf(result); + } + private static Ingredient potMold(){ + var cached=potMoldCache; if(cached!=null) return cached; + var stacks=new ArrayList(); + for(var block:BuiltInRegistries.BLOCK) if(block instanceof BlockBotanyPot && block.asItem()!=Items.AIR) stacks.add(new ItemStack(block)); + return potMoldCache=stacks.isEmpty()?Ingredient.EMPTY:Ingredient.of(stacks.stream()); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/create/CreateBlastingRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/create/CreateBlastingRecipeAdapter.java new file mode 100644 index 00000000..4013c17b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/create/CreateBlastingRecipeAdapter.java @@ -0,0 +1,98 @@ +package com.sorrowmist.useless.content.recipe.adapters.create; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.BlastingRecipe; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Create fan blasting without registering smoking or smelting recipes. */ +public final class CreateBlastingRecipeAdapter implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + + @Override + public Class getRecipeClass() { + return BlastingRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return ItemStack.EMPTY; + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + return CreateRecipeAdapterUtils.isCreateMold(mold, "encased_fan"); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) return List.of(); + BlastingRecipe source = holder.value(); + if (source.getType() != RecipeType.BLASTING) return List.of(); + List inputs = AdapterUtils.mergeIngredients(source.getIngredients()); + ItemStack output = source.getResultItem(level == null ? null : level.registryAccess()); + Ingredient fanMold = CreateRecipeAdapterUtils.blockMold( + CreateRecipeAdapterUtils.createBlock("encased_fan")); + if (inputs.isEmpty() || output.isEmpty() || output.getCount() <= 0 || fanMold.isEmpty()) { + RateLimitedLogger.warn(LOGGER, "Skipping invalid Create blasting recipe: {}", holder.id()); + return List.of(); + } + int declaredTime = source.getCookingTime(); + if (declaredTime < 0) { + RateLimitedLogger.warn(LOGGER, "Skipping Create blasting recipe {} with a negative processing time", holder.id()); + return List.of(); + } + int processTime = declaredTime > 0 ? declaredTime : AdapterUtils.DEFAULT_PROCESS_TIME; + AdvancedAlloyFurnaceRecipe converted = new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + inputs, + List.of(), + List.of(), + List.of(output.copy()), + List.of(), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + processTime, + Ingredient.EMPTY, + 0, + List.of(fanMold), + AlloyFurnaceMode.NORMAL); + return List.of(converted); + } + + @Override + @SuppressWarnings("unchecked") + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) return List.of(); + List> result = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getRecipes(level.getRecipeManager())) { + if (!(holder.value() instanceof BlastingRecipe blasting) + || blasting.getType() != RecipeType.BLASTING) continue; + RecipeHolder typed = (RecipeHolder) (RecipeHolder) holder; + List converted = convertAll(typed, level); + if (!converted.isEmpty() && CreateRecipeAdapterUtils.matchesConverted( + converted.get(0), mergedInputs, mergedFluids)) result.add(typed); + } + return result; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/create/CreateItemApplicationRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/create/CreateItemApplicationRecipeAdapter.java new file mode 100644 index 00000000..0980de50 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/create/CreateItemApplicationRecipeAdapter.java @@ -0,0 +1,216 @@ +package com.sorrowmist.useless.content.recipe.adapters.create; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.mojang.logging.LogUtils; +import com.simibubi.create.content.kinetics.deployer.ItemApplicationRecipe; +import com.simibubi.create.content.processing.recipe.ProcessingOutput; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.ExpectedOutputScaler; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalLong; + +/** + * 将 Create 的「物品应用」配方自动适配为合金炉配方。 + *

+ * 适配器注册在基类 {@link ItemApplicationRecipe} 上,因此无需逐类登记即可同时覆盖: + * 手动应用配方(create:item_application)与机械手应用配方(create:deploying)。 + *

+ * 被处理的物品始终作为输入;手持物品按是否消耗分流:不消耗的工具(斧头、砂纸等) + * 作为模具,消耗的材料(木板等)作为输入;机械手方块始终作为第一个模具。 + */ +public final class CreateItemApplicationRecipeAdapter + implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + + @Override + public Class getRecipeClass() { + return ItemApplicationRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return CreateRecipeAdapterUtils.createBlockItem("deployer"); + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + return CreateRecipeAdapterUtils.isCreateMold(mold, "deployer"); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) return List.of(); + AdvancedAlloyFurnaceRecipe converted = convert(holder.id(), holder.value(), level); + return converted == null ? List.of() : List.of(converted); + } + + @Nullable + static AdvancedAlloyFurnaceRecipe convert( + ResourceLocation originalId, ItemApplicationRecipe source, Level level) { + if (originalId == null || source == null) return null; + try { + return build(originalId, source, level); + } catch (RuntimeException exception) { + RateLimitedLogger.warn(LOGGER, "Skipping malformed Create item application recipe {}", originalId, exception); + return null; + } + } + + @Nullable + private static AdvancedAlloyFurnaceRecipe build( + ResourceLocation originalId, ItemApplicationRecipe source, Level level) { + Ingredient processedItem = source.getProcessedItem(); + Ingredient requiredHeldItem = source.getRequiredHeldItem(); + if (processedItem == null || processedItem.isEmpty() + || requiredHeldItem == null || requiredHeldItem.isEmpty()) { + RateLimitedLogger.warn(LOGGER, "Skipping Create item application recipe {} with empty inputs", originalId); + return null; + } + + Ingredient deployerMold = CreateRecipeAdapterUtils.blockMold( + CreateRecipeAdapterUtils.createBlock("deployer")); + if (deployerMold.isEmpty()) { + RateLimitedLogger.warn(LOGGER, "Skipping Create item application recipe {} because the deployer block is missing", originalId); + return null; + } + + // 手持物品按是否消耗分流:不消耗的工具(斧头、砂纸等)作为模具, + // 消耗的材料(木板等)作为输入。机械手方块始终作为第一个模具。 + // 工具配方因此有两个模具,而普通合金炉只有一个模具槽、会跳过模具数大于 1 的配方 + // (见 AlloyFurnaceRecipeManager.selectBestRecipe),需通过多方块结构的 + // 模具仓(Omniversal Mold Hub)同时提供机械手和工具才能运行。 + Map itemRequirements = new LinkedHashMap<>(); + AdapterUtils.mergeIngredient(itemRequirements, processedItem, 1L); + List molds = new ArrayList<>(); + molds.add(deployerMold); + if (source.shouldKeepHeldItem()) { + CreateRecipeAdapterUtils.addUniqueMold(molds, requiredHeldItem); + } else { + AdapterUtils.mergeIngredient(itemRequirements, requiredHeldItem, 1L); + } + + List sourceFluidInputs = new ArrayList<>(); + if (source.getFluidIngredients() != null) { + for (var nativeIngredient : source.getFluidIngredients()) { + SizedFluidIngredient ingredient = CreateRecipeAdapterUtils.nativeFluid(nativeIngredient); + if (ingredient == null || ingredient.ingredient() == null + || ingredient.ingredient().isEmpty() || ingredient.amount() <= 0) { + RateLimitedLogger.warn(LOGGER, "Skipping empty Create fluid input declaration in {}", originalId); + return null; + } + sourceFluidInputs.add(ingredient); + } + } + + // getRollableResults() 返回完整产物列表(含手动配方中被排除的主方块产物), + // 因此手动配方与机械手配方可以统一处理。 + List weightedOutputs = new ArrayList<>(); + for (ProcessingOutput output : source.getRollableResults()) { + collectOutput(weightedOutputs, output, originalId); + } + + Optional scaled = ExpectedOutputScaler.scale(weightedOutputs); + if (scaled.isEmpty()) { + RateLimitedLogger.warn(LOGGER, "Skipping Create item application recipe {} because its item output batch cannot be represented", originalId); + return null; + } + int operations = scaled.get().operations(); + + List inputs = CreateRecipeAdapterUtils.scaleItemInputs( + itemRequirements, operations, originalId, LOGGER); + List inputFluids = CreateRecipeAdapterUtils.scaleFluidInputs( + sourceFluidInputs, operations, originalId, LOGGER); + List outputFluids = CreateRecipeAdapterUtils.scaleFluidOutputs( + source.getFluidResults(), operations, originalId, LOGGER); + if (inputs == null || inputFluids == null || outputFluids == null) return null; + if (inputs.isEmpty() && inputFluids.isEmpty()) { + RateLimitedLogger.warn(LOGGER, "Skipping Create item application recipe {} because it has no valid inputs", originalId); + return null; + } + if (scaled.get().outputs().isEmpty() && outputFluids.isEmpty()) { + RateLimitedLogger.warn(LOGGER, "Skipping Create item application recipe {} because it has no valid outputs", originalId); + return null; + } + + int declaredTime = source.getProcessingDuration(); + if (declaredTime < 0) { + RateLimitedLogger.warn(LOGGER, "Skipping Create item application recipe {} with a negative processing time", originalId); + return null; + } + int baseTime = declaredTime > 0 ? declaredTime : AdapterUtils.DEFAULT_PROCESS_TIME; + + OptionalLong energy = CreateRecipeAdapterUtils.multiplyToLong(AdapterUtils.DEFAULT_ENERGY, operations); + OptionalLong processTime = CreateRecipeAdapterUtils.multiplyToLong(baseTime, operations); + if (energy.isEmpty() || processTime.isEmpty() || processTime.getAsLong() > Integer.MAX_VALUE) { + RateLimitedLogger.warn(LOGGER, "Skipping overflowing Create item application recipe {}", originalId); + return null; + } + + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(originalId), + inputs, + inputFluids, + List.of(), + scaled.get().outputs(), + outputFluids, + List.of(), + energy.getAsLong(), + (int) processTime.getAsLong(), + Ingredient.EMPTY, + 0, + molds, + AlloyFurnaceMode.NORMAL); + } + + private static void collectOutput( + List weightedOutputs, + @Nullable ProcessingOutput output, ResourceLocation originalId) { + if (output == null) return; + ItemStack stack = output.getStack(); + float chance = output.getChance(); + if (stack.isEmpty() || stack.getCount() <= 0 || !Float.isFinite(chance)) { + RateLimitedLogger.warn(LOGGER, "Skipping invalid Create item output declaration in {}", originalId); + return; + } + weightedOutputs.add(new ExpectedOutputScaler.WeightedItemOutput( + stack, stack.getCount(), stack.getCount(), chance)); + } + + @Override + @SuppressWarnings("unchecked") + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) return List.of(); + List> result = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getRecipes(level.getRecipeManager())) { + if (!(holder.value() instanceof ItemApplicationRecipe source)) continue; + AdvancedAlloyFurnaceRecipe converted = convert(holder.id(), source, level); + if (converted != null && CreateRecipeAdapterUtils.matchesConverted( + converted, mergedInputs, mergedFluids)) { + result.add((RecipeHolder) (RecipeHolder) holder); + } + } + return result; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/create/CreateMechanicalCraftingRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/create/CreateMechanicalCraftingRecipeAdapter.java new file mode 100644 index 00000000..1da388be --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/create/CreateMechanicalCraftingRecipeAdapter.java @@ -0,0 +1,98 @@ +package com.sorrowmist.useless.content.recipe.adapters.create; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.mojang.logging.LogUtils; +import com.simibubi.create.content.kinetics.crafter.MechanicalCraftingRecipe; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Converts Create's unordered mechanical crafting material list. */ +public final class CreateMechanicalCraftingRecipeAdapter + implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + + @Override + public Class getRecipeClass() { + return MechanicalCraftingRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return CreateRecipeAdapterUtils.createBlockItem("mechanical_crafter"); + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + return CreateRecipeAdapterUtils.isCreateMold(mold, "mechanical_crafter"); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) return List.of(); + MechanicalCraftingRecipe source = holder.value(); + Map requirements = new LinkedHashMap<>(); + for (Ingredient ingredient : source.getIngredients()) { + if (ingredient == null || ingredient.isEmpty()) continue; + AdapterUtils.mergeIngredient(requirements, ingredient, 1L); + } + ItemStack output = source.getResultItem(level == null ? null : level.registryAccess()); + var crafterMold = CreateRecipeAdapterUtils.blockMold( + CreateRecipeAdapterUtils.createBlock("mechanical_crafter")); + if (requirements.isEmpty() || output.isEmpty() || output.getCount() <= 0 || crafterMold.isEmpty()) { + RateLimitedLogger.warn(LOGGER, "Skipping invalid Create mechanical crafting recipe: {}", holder.id()); + return List.of(); + } + List inputs = requirements.entrySet().stream() + .map(entry -> new CountedIngredient(entry.getKey(), entry.getValue())) + .toList(); + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + inputs, + List.of(), + List.of(), + List.of(output.copy()), + List.of(), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + AdapterUtils.DEFAULT_PROCESS_TIME, + Ingredient.EMPTY, + 0, + List.of(crafterMold), + AlloyFurnaceMode.NORMAL)); + } + + @Override + @SuppressWarnings("unchecked") + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) return List.of(); + List> result = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getRecipes(level.getRecipeManager())) { + if (!(holder.value() instanceof MechanicalCraftingRecipe)) continue; + RecipeHolder typed = + (RecipeHolder) (RecipeHolder) holder; + List converted = convertAll(typed, level); + if (!converted.isEmpty() && CreateRecipeAdapterUtils.matchesConverted( + converted.get(0), mergedInputs, mergedFluids)) result.add(typed); + } + return result; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/create/CreateProcessingRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/create/CreateProcessingRecipeAdapter.java new file mode 100644 index 00000000..da4b3809 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/create/CreateProcessingRecipeAdapter.java @@ -0,0 +1,76 @@ +package com.sorrowmist.useless.content.recipe.adapters.create; + +import com.mojang.logging.LogUtils; +import com.simibubi.create.content.processing.recipe.ProcessingRecipe; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.world.item.ItemStack; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Create's item/fluid processing recipes into multi-machine mold recipes. */ +public final class CreateProcessingRecipeAdapter implements IRecipeAdapter> { + private static final Logger LOGGER = LogUtils.getLogger(); + + @SuppressWarnings("unchecked") + @Override + public Class> getRecipeClass() { + return (Class>) (Class) ProcessingRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return null; + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + return CreateRecipeAdapterUtils.isCreateMachineMold(mold); + } + + @Override + public List convertAll( + RecipeHolder> holder, Level level) { + if (holder == null || holder.value() == null) return List.of(); + ProcessingRecipe source = holder.value(); + if (!CreateRecipeAdapterUtils.supportsProcessing(source)) return List.of(); + return converted(holder.id(), source); + } + + static List converted( + net.minecraft.resources.ResourceLocation id, ProcessingRecipe source) { + AdvancedAlloyFurnaceRecipe converted = CreateRecipeAdapterUtils.convertProcessing( + id, source, CreateRecipeAdapterUtils.processingMolds(source), LOGGER); + return converted == null ? List.of() : List.of(converted); + } + + @Override + @SuppressWarnings("unchecked") + public List>> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) return List.of(); + List>> result = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getRecipes(level.getRecipeManager())) { + if (!(holder.value() instanceof ProcessingRecipe source) + || !CreateRecipeAdapterUtils.supportsProcessing(source) + || !CreateRecipeAdapterUtils.hasMoldForProcessing(source, mold)) { + continue; + } + List converted = converted(holder.id(), source); + if (converted.isEmpty() || !CreateRecipeAdapterUtils.matchesConverted( + converted.get(0), mergedInputs, mergedFluids)) continue; + result.add((RecipeHolder>) (RecipeHolder) holder); + } + return result; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/create/CreateRecipeAdapterUtils.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/create/CreateRecipeAdapterUtils.java new file mode 100644 index 00000000..6f9d1b3c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/create/CreateRecipeAdapterUtils.java @@ -0,0 +1,350 @@ +package com.sorrowmist.useless.content.recipe.adapters.create; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.simibubi.create.content.fluids.transfer.FillingRecipe; +import com.simibubi.create.content.kinetics.millstone.MillingRecipe; +import com.simibubi.create.content.kinetics.mixer.CompactingRecipe; +import com.simibubi.create.content.kinetics.mixer.MixingRecipe; +import com.simibubi.create.content.kinetics.press.PressingRecipe; +import com.simibubi.create.content.kinetics.crusher.CrushingRecipe; +import com.simibubi.create.content.kinetics.fan.processing.HauntingRecipe; +import com.simibubi.create.content.kinetics.fan.processing.SplashingRecipe; +import com.simibubi.create.content.processing.basin.BasinRecipe; +import com.simibubi.create.content.processing.recipe.HeatCondition; +import com.simibubi.create.content.processing.recipe.ProcessingOutput; +import com.simibubi.create.content.processing.recipe.ProcessingRecipe; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.ExpectedOutputScaler; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.ItemLike; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalLong; +import java.util.Set; + +/** Shared conversion rules for the optional Create adapters. */ +final class CreateRecipeAdapterUtils { + private static final Set MACHINE_MOLD_PATHS = Set.of( + "spout", + "mechanical_press", + "mechanical_mixer", + "basin", + "millstone", + "crushing_wheel", + "encased_fan", + "mechanical_crafter", + "deployer", + "mechanical_saw", + "blaze_burner" + ); + + private CreateRecipeAdapterUtils() { + } + + static boolean isCreateMachineMold(@Nullable ItemStack stack) { + if (stack == null || stack.isEmpty()) return false; + ResourceLocation id = BuiltInRegistries.ITEM.getKey(stack.getItem()); + return id != null && "create".equals(id.getNamespace()) && MACHINE_MOLD_PATHS.contains(id.getPath()); + } + + static boolean isCreateMold(@Nullable ItemStack stack, String path) { + if (stack == null || stack.isEmpty()) return false; + ResourceLocation id = BuiltInRegistries.ITEM.getKey(stack.getItem()); + return id != null && "create".equals(id.getNamespace()) && path.equals(id.getPath()); + } + + static SizedFluidIngredient nativeFluid(com.simibubi.create.foundation.fluid.FluidIngredient source) { + if (source == null || source.getRequiredAmount() <= 0) return null; + com.google.gson.JsonObject json; + try { + json = source.serialize().deepCopy(); + } catch (NullPointerException networkTag) { + // Create sends tag members, not the tag ID, to clients in 1.20.1. + if (!(source instanceof com.simibubi.create.foundation.fluid.FluidIngredient.FluidTagIngredient)) throw networkTag; + var choices = source.getMatchingFluidStacks().stream() + .map(com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient::single).toList(); + if (choices.isEmpty()) return null; + return new SizedFluidIngredient(com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient.any(choices), + source.getRequiredAmount()); + } + if (json.has("fluidTag")) json.add("tag", json.remove("fluidTag")); + if (json.has("nbt")) json.addProperty("type", "useless_mod:merge_nbt_fluid"); + return new SizedFluidIngredient(com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient.fromJson(json), + source.getRequiredAmount()); + } + + static boolean supportsProcessing(ProcessingRecipe recipe) { + return recipe instanceof FillingRecipe + || recipe instanceof PressingRecipe + || recipe instanceof CompactingRecipe + || recipe instanceof MixingRecipe + || recipe instanceof MillingRecipe + || recipe instanceof CrushingRecipe + || recipe instanceof HauntingRecipe + || recipe instanceof SplashingRecipe; + } + + static List processingMolds(ProcessingRecipe recipe) { + List molds = new ArrayList<>(); + if (recipe instanceof FillingRecipe) { + addBlockMold(molds, "spout"); + } else if (recipe instanceof PressingRecipe) { + addBlockMold(molds, "mechanical_press"); + } else if (recipe instanceof CompactingRecipe) { + addBlockMold(molds, "mechanical_press"); + addBlockMold(molds, "basin"); + } else if (recipe instanceof MixingRecipe) { + addBlockMold(molds, "mechanical_mixer"); + addBlockMold(molds, "basin"); + } else if (recipe instanceof MillingRecipe) { + addBlockMold(molds, "millstone"); + } else if (recipe instanceof CrushingRecipe) { + addBlockMold(molds, "crushing_wheel"); + } else if (recipe instanceof HauntingRecipe || recipe instanceof SplashingRecipe) { + addBlockMold(molds, "encased_fan"); + } else { + return List.of(); + } + + if (recipe instanceof BasinRecipe basin + && basin.getRequiredHeat() != HeatCondition.NONE) { + addBlockMold(molds, "blaze_burner"); + } + return List.copyOf(molds); + } + + static Ingredient blockMold(@Nullable ItemLike block) { + if (block == null || block.asItem() == Items.AIR) return Ingredient.EMPTY; + return Ingredient.of(block); + } + + static Block createBlock(String path) { + return BuiltInRegistries.BLOCK.get(new ResourceLocation("create", path)); + } + + static ItemStack createBlockItem(String path) { + Block block = createBlock(path); + return block == null || block == Blocks.AIR || block.asItem() == Items.AIR + ? ItemStack.EMPTY + : block.asItem().getDefaultInstance(); + } + + private static void addBlockMold(List molds, String path) { + Ingredient mold = blockMold(createBlock(path)); + if (!mold.isEmpty()) molds.add(mold); + } + + static boolean hasMoldForProcessing(ProcessingRecipe recipe, @Nullable ItemStack mold) { + if (mold == null || mold.isEmpty()) return false; + return processingMolds(recipe).stream().anyMatch(required -> AdapterUtils.matchesMold(required, mold)); + } + + @Nullable + static AdvancedAlloyFurnaceRecipe convertProcessing( + ResourceLocation originalId, ProcessingRecipe source, + List molds, Logger logger) { + if (source == null || originalId == null || molds == null || molds.isEmpty()) return null; + if (!supportsProcessing(source)) return null; + + int declaredTime = source.getProcessingDuration(); + if (declaredTime < 0) { + RateLimitedLogger.warn(logger, "Skipping Create recipe {} with a negative processing time", originalId); + return null; + } + int baseTime = declaredTime > 0 ? declaredTime : AdapterUtils.DEFAULT_PROCESS_TIME; + + Map itemRequirements = new LinkedHashMap<>(); + if (source.getIngredients() != null) { + for (Ingredient ingredient : source.getIngredients()) { + if (ingredient == null || ingredient.isEmpty()) { + RateLimitedLogger.warn(logger, "Skipping empty Create item input declaration in {}", originalId); + return null; + } + AdapterUtils.mergeIngredient(itemRequirements, ingredient, 1L); + } + } + + List sourceFluidInputs = new ArrayList<>(); + if (source.getFluidIngredients() != null) { + for (var nativeIngredient : source.getFluidIngredients()) { + SizedFluidIngredient ingredient = CreateRecipeAdapterUtils.nativeFluid(nativeIngredient); + if (ingredient == null || ingredient.ingredient() == null + || ingredient.ingredient().isEmpty() || ingredient.amount() <= 0) { + RateLimitedLogger.warn(logger, "Skipping empty Create fluid input declaration in {}", originalId); + return null; + } + sourceFluidInputs.add(ingredient); + } + } + + List weightedOutputs = new ArrayList<>(); + if (source.getRollableResults() != null) { + for (ProcessingOutput output : source.getRollableResults()) { + if (output == null || output.getStack().isEmpty() || output.getStack().getCount() <= 0 + || !Float.isFinite(output.getChance())) { + RateLimitedLogger.warn(logger, "Skipping invalid Create item output declaration in {}", originalId); + return null; + } + ItemStack stack = output.getStack(); + weightedOutputs.add(new ExpectedOutputScaler.WeightedItemOutput( + stack, stack.getCount(), stack.getCount(), output.getChance())); + } + } + + Optional scaled = ExpectedOutputScaler.scale(weightedOutputs); + if (scaled.isEmpty()) { + RateLimitedLogger.warn(logger, "Skipping Create recipe {} because its item output batch cannot be represented", originalId); + return null; + } + int operations = scaled.get().operations(); + + List inputs = scaleItemInputs(itemRequirements, operations, originalId, logger); + List inputFluids = scaleFluidInputs( + sourceFluidInputs, operations, originalId, logger); + List outputFluids = scaleFluidOutputs( + source.getFluidResults(), operations, originalId, logger); + if (inputs == null || inputFluids == null || outputFluids == null) return null; + if (inputs.isEmpty() && inputFluids.isEmpty()) { + RateLimitedLogger.warn(logger, "Skipping Create recipe {} because it has no valid inputs", originalId); + return null; + } + if (scaled.get().outputs().isEmpty() && outputFluids.isEmpty()) { + RateLimitedLogger.warn(logger, "Skipping Create recipe {} because it has no valid outputs", originalId); + return null; + } + + OptionalLong energy = multiplyToLong(AdapterUtils.DEFAULT_ENERGY, operations); + OptionalLong processTime = multiplyToLong(baseTime, operations); + if (energy.isEmpty() || processTime.isEmpty() || processTime.getAsLong() > Integer.MAX_VALUE) { + RateLimitedLogger.warn(logger, "Skipping overflowing Create recipe {}", originalId); + return null; + } + + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(originalId), + inputs, + inputFluids, + List.of(), + scaled.get().outputs(), + outputFluids, + List.of(), + energy.getAsLong(), + (int) processTime.getAsLong(), + Ingredient.EMPTY, + 0, + molds, + AlloyFurnaceMode.NORMAL + ); + } + + static List scaleItemInputs( + Map requirements, long operations, + ResourceLocation id, Logger logger) { + Map scaled = new LinkedHashMap<>(); + for (Map.Entry entry : requirements.entrySet()) { + OptionalLong count = multiplyToLong(entry.getValue(), operations); + if (count.isEmpty() || count.getAsLong() <= 0) { + RateLimitedLogger.warn(logger, "Skipping overflowing Create item input declaration in {}", id); + return null; + } + AdapterUtils.mergeIngredient(scaled, entry.getKey(), count.getAsLong()); + } + return scaled.entrySet().stream() + .map(entry -> new CountedIngredient(entry.getKey(), entry.getValue())) + .toList(); + } + + static List scaleFluidInputs( + List inputs, long operations, + ResourceLocation id, Logger logger) { + List result = new ArrayList<>(); + for (SizedFluidIngredient input : inputs) { + OptionalLong amount = multiplyToLong(input.amount(), operations); + if (amount.isEmpty() || amount.getAsLong() <= 0 || amount.getAsLong() > Integer.MAX_VALUE) { + RateLimitedLogger.warn(logger, "Skipping overflowing Create fluid input declaration in {}", id); + return null; + } + result.add(new SizedFluidIngredient(input.ingredient(), (int) amount.getAsLong())); + } + return List.copyOf(result); + } + + static List scaleFluidOutputs( + @Nullable List outputs, long operations, + ResourceLocation id, Logger logger) { + if (outputs == null || outputs.isEmpty()) return List.of(); + List result = new ArrayList<>(); + for (FluidStack output : outputs) { + if (output == null || output.isEmpty() || output.getAmount() <= 0) { + RateLimitedLogger.warn(logger, "Skipping empty Create fluid output declaration in {}", id); + return null; + } + OptionalLong amount = multiplyToLong(output.getAmount(), operations); + if (amount.isEmpty() || amount.getAsLong() <= 0 || amount.getAsLong() > Integer.MAX_VALUE) { + RateLimitedLogger.warn(logger, "Skipping overflowing Create fluid output declaration in {}", id); + return null; + } + result.add(new net.minecraftforge.fluids.FluidStack(output, (int) amount.getAsLong())); + } + return List.copyOf(result); + } + + static OptionalLong multiplyToLong(long left, long right) { + if (left < 0 || right < 0) return OptionalLong.empty(); + try { + return OptionalLong.of(Math.multiplyExact(left, right)); + } catch (ArithmeticException exception) { + return OptionalLong.empty(); + } + } + + static void addUniqueMold(List molds, Ingredient mold) { + if (mold == null || mold.isEmpty()) return; + if (molds.stream().noneMatch(existing -> AdapterUtils.areIngredientsEqual(existing, mold))) { + molds.add(mold); + } + } + + static List machinesToMolds(Set machines) { + List molds = new ArrayList<>(); + for (ItemLike machine : new LinkedHashSet<>(machines)) { + if (machine != null) addUniqueMold(molds, Ingredient.of(machine)); + } + return molds; + } + + static boolean matchesConverted( + AdvancedAlloyFurnaceRecipe recipe, + Map mergedInputs, + @Nullable Map mergedFluids) { + if (recipe == null) return false; + Map requiredItems = new LinkedHashMap<>(); + for (CountedIngredient input : recipe.inputs()) { + if (input != null && input.ingredient() != null && input.count() > 0) { + AdapterUtils.mergeIngredient(requiredItems, input.ingredient(), input.count()); + } + } + return AdapterUtils.matchesRequired(mergedInputs == null ? Map.of() : mergedInputs, requiredItems) + && AdapterUtils.matchesLongFluidIngredients( + mergedFluids == null ? Map.of() : mergedFluids, recipe.inputFluids()); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/create/CreateSequencedAssemblyRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/create/CreateSequencedAssemblyRecipeAdapter.java new file mode 100644 index 00000000..df3c7658 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/create/CreateSequencedAssemblyRecipeAdapter.java @@ -0,0 +1,283 @@ +package com.sorrowmist.useless.content.recipe.adapters.create; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.mojang.logging.LogUtils; +import com.simibubi.create.content.kinetics.deployer.ItemApplicationRecipe; +import com.simibubi.create.content.processing.recipe.ProcessingRecipe; +import com.simibubi.create.content.processing.sequenced.IAssemblyRecipe; +import com.simibubi.create.content.processing.sequenced.SequencedAssemblyRecipe; +import com.simibubi.create.content.processing.sequenced.SequencedRecipe; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.ItemLike; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** Converts a complete Create sequenced assembly into one deterministic recipe. */ +public final class CreateSequencedAssemblyRecipeAdapter + implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + + @Override + public Class getRecipeClass() { + return SequencedAssemblyRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return null; + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + return CreateRecipeAdapterUtils.isCreateMachineMold(mold); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) return List.of(); + AdvancedAlloyFurnaceRecipe converted = convert(holder.id(), holder.value()); + return converted == null ? List.of() : List.of(converted); + } + + static AdvancedAlloyFurnaceRecipe convert( + ResourceLocation originalId, SequencedAssemblyRecipe source) { + if (originalId == null || source == null) return null; + int loops = source.getLoops(); + if (loops <= 0 || source.getSequence() == null || source.getSequence().isEmpty()) { + RateLimitedLogger.warn(LOGGER, "Skipping invalid Create sequenced assembly recipe: {}", originalId); + return null; + } + + Ingredient initial = source.getIngredient(); + if (initial == null || initial.isEmpty()) { + RateLimitedLogger.warn(LOGGER, "Skipping Create sequenced assembly recipe {} without an initial input", originalId); + return null; + } + + Map itemRequirements = new LinkedHashMap<>(); + AdapterUtils.mergeIngredient(itemRequirements, initial, 1L); + List fluidRequirements = new ArrayList<>(); + List molds = new ArrayList<>(); + long totalTime = 0L; + long totalEnergy = 0L; + int validSteps = 0; + + for (SequencedRecipe wrapper : source.getSequence()) { + if (wrapper == null || wrapper.getRecipe() == null) { + RateLimitedLogger.warn(LOGGER, "Skipping invalid step in Create sequenced assembly recipe {}", originalId); + return null; + } + try { + ProcessingRecipe step = wrapper.getRecipe(); + IAssemblyRecipe assembly = wrapper.getAsAssemblyRecipe(); + if (!assembly.supportsAssembly()) { + RateLimitedLogger.warn(LOGGER, "Skipping Create assembly step that does not support assembly in {}", originalId); + return null; + } + List stepIngredients = new ArrayList<>(); + assembly.addAssemblyIngredients(stepIngredients); + List nativeStepFluids = new ArrayList<>(); + assembly.addAssemblyFluidIngredients(nativeStepFluids); + List stepFluids = new ArrayList<>(); + for (var fluid : nativeStepFluids) { + var convertedFluid = CreateRecipeAdapterUtils.nativeFluid(fluid); + if (convertedFluid == null) return null; + stepFluids.add(convertedFluid); + } + Set machines = new LinkedHashSet<>(); + assembly.addRequiredMachines(machines); + if (machines.isEmpty()) { + RateLimitedLogger.warn(LOGGER, "Skipping Create assembly step without a required machine in {}", originalId); + return null; + } + + List convertedStepIngredients = new ArrayList<>(); + for (Ingredient ingredient : stepIngredients) { + if (ingredient == null || ingredient.isEmpty()) { + RateLimitedLogger.warn(LOGGER, "Skipping empty material declaration in Create assembly step of {}", originalId); + return null; + } + convertedStepIngredients.add(ingredient); + } + + if (step instanceof ItemApplicationRecipe application) { + Ingredient tool = application.getRequiredHeldItem(); + if (tool == null || tool.isEmpty()) { + RateLimitedLogger.warn(LOGGER, "Skipping empty deployer tool declaration in {}", originalId); + } else if (application.shouldKeepHeldItem()) { + removeOneIngredient(convertedStepIngredients, tool); + CreateRecipeAdapterUtils.addUniqueMold(molds, tool); + } else if (convertedStepIngredients.stream() + .noneMatch(ingredient -> AdapterUtils.areIngredientsEqual(ingredient, tool))) { + convertedStepIngredients.add(tool); + } + } + + Map stepItems = new LinkedHashMap<>(); + for (Ingredient ingredient : convertedStepIngredients) { + AdapterUtils.mergeIngredient(stepItems, ingredient, 1L); + } + List stepFluidsWithLoops = new ArrayList<>(); + for (SizedFluidIngredient fluid : stepFluids) { + if (fluid == null || fluid.ingredient() == null || fluid.ingredient().isEmpty() + || fluid.amount() <= 0) { + RateLimitedLogger.warn(LOGGER, "Skipping empty fluid declaration in Create assembly step of {}", originalId); + return null; + } + long amount = Math.multiplyExact((long) fluid.amount(), loops); + if (amount > Integer.MAX_VALUE) { + RateLimitedLogger.warn(LOGGER, "Skipping overflowing fluid declaration in Create assembly recipe {}", originalId); + return null; + } + stepFluidsWithLoops.add(new SizedFluidIngredient(fluid.ingredient(), (int) amount)); + } + + int stepTime = step.getProcessingDuration(); + if (stepTime < 0) { + RateLimitedLogger.warn(LOGGER, "Skipping negative processing time declaration in {}", originalId); + return null; + } + if (stepTime == 0) stepTime = AdapterUtils.DEFAULT_PROCESS_TIME; + long stepTotalTime = Math.multiplyExact((long) stepTime, loops); + long newTotalTime = Math.addExact(totalTime, stepTotalTime); + long stepEnergy = Math.multiplyExact((long) AdapterUtils.DEFAULT_ENERGY, loops); + long newTotalEnergy = Math.addExact(totalEnergy, stepEnergy); + + for (Map.Entry entry : stepItems.entrySet()) { + long amount = Math.multiplyExact(entry.getValue(), loops); + AdapterUtils.mergeIngredient(itemRequirements, entry.getKey(), amount); + } + fluidRequirements.addAll(stepFluidsWithLoops); + for (ItemLike machine : machines) { + if (machine != null) CreateRecipeAdapterUtils.addUniqueMold(molds, Ingredient.of(machine)); + } + totalTime = newTotalTime; + totalEnergy = newTotalEnergy; + validSteps++; + } catch (RuntimeException exception) { + RateLimitedLogger.warn(LOGGER, "Skipping unsupported Create assembly step in {}", originalId, exception); + } + } + + double totalWeight = 0.0D; + if (source.resultPool == null || source.resultPool.isEmpty()) return null; + for (var result : source.resultPool) { + if (result == null || result.getStack().isEmpty() || !Float.isFinite(result.getChance()) + || result.getChance() < 0) return null; + totalWeight += result.getChance(); + } + if (!(totalWeight > 0) || !Double.isFinite(totalWeight)) return null; + var weighted = new ArrayList(); + for (var result : source.resultPool) { + weighted.add(new com.sorrowmist.useless.content.recipe.ExpectedOutputScaler.WeightedItemOutput( + result.getStack(), result.getStack().getCount(), result.getStack().getCount(), + result.getChance() / totalWeight)); + } + var scaled = com.sorrowmist.useless.content.recipe.ExpectedOutputScaler.scale(weighted); + if (scaled.isEmpty()) return null; + int batch = scaled.get().operations(); + List outputs = scaled.get().outputs(); + var inputs = CreateRecipeAdapterUtils.scaleItemInputs(itemRequirements, batch, originalId, LOGGER); + var batchFluids = CreateRecipeAdapterUtils.scaleFluidInputs(fluidRequirements, batch, originalId, LOGGER); + if (inputs == null || batchFluids == null) return null; + fluidRequirements = batchFluids; + try { + totalTime = Math.multiplyExact(totalTime, batch); + totalEnergy = Math.multiplyExact(totalEnergy, batch); + } catch (ArithmeticException overflow) { + return null; + } + + if (validSteps == 0 || molds.isEmpty() || totalTime <= 0 || outputs.isEmpty()) { + RateLimitedLogger.warn(LOGGER, "Skipping Create sequenced assembly recipe {} without convertible steps/results", originalId); + return null; + } + + if (inputs.isEmpty() && fluidRequirements.isEmpty()) { + RateLimitedLogger.warn(LOGGER, "Skipping Create sequenced assembly recipe {} without valid inputs", originalId); + return null; + } + if (totalTime > Integer.MAX_VALUE) { + RateLimitedLogger.warn(LOGGER, "Skipping overflowing Create sequenced assembly recipe: {}", originalId); + return null; + } + + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(originalId), + inputs, + fluidRequirements, + List.of(), + outputs, + List.of(), + List.of(), + totalEnergy, + (int) totalTime, + Ingredient.EMPTY, + 0, + molds, + AlloyFurnaceMode.NORMAL); + } + + private static void removeOneIngredient(List ingredients, Ingredient target) { + for (int i = 0; i < ingredients.size(); i++) { + if (AdapterUtils.areIngredientsEqual(ingredients.get(i), target)) { + ingredients.remove(i); + return; + } + } + } + + private static boolean mergeOutput(List outputs, ItemStack output) { + for (ItemStack existing : outputs) { + if (ItemStack.isSameItemSameTags(existing, output)) { + long count = (long) existing.getCount() + output.getCount(); + if (count > Integer.MAX_VALUE) return false; + existing.setCount((int) count); + return true; + } + } + outputs.add(output.copy()); + return true; + } + + @Override + @SuppressWarnings("unchecked") + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) return List.of(); + List> result = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getRecipes(level.getRecipeManager())) { + if (!(holder.value() instanceof SequencedAssemblyRecipe source)) continue; + AdvancedAlloyFurnaceRecipe converted = convert(holder.id(), source); + if (converted != null && CreateRecipeAdapterUtils.matchesConverted( + converted, mergedInputs, mergedFluids) + && converted.molds().stream().anyMatch(moldIngredient -> + moldIngredient != null && !moldIngredient.isEmpty() + && AdapterUtils.matchesMold(moldIngredient, mold))) { + result.add((RecipeHolder) (RecipeHolder) holder); + } + } + return result; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/CookingPotRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/CookingPotRecipeAdapter.java new file mode 100644 index 00000000..b950d842 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/CookingPotRecipeAdapter.java @@ -0,0 +1,150 @@ +package com.sorrowmist.useless.content.recipe.adapters.delight; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import vectorwing.farmersdelight.common.crafting.CookingPotRecipe; +import vectorwing.farmersdelight.common.registry.ModItems; +import vectorwing.farmersdelight.common.registry.ModRecipeTypes; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Converts every Cooking Pot recipe registered by Farmer's Delight and its addons. */ +public final class CookingPotRecipeAdapter implements IRecipeAdapter { + private static final int BASE_COOK_TIME = 200; + + @Override + public Class getRecipeClass() { + return CookingPotRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(ModItems.COOKING_POT.get()); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + + CookingPotRecipe source = holder.value(); + if (!isFarmersDelightRecipe(source)) { + return List.of(); + } + List inputs = AdapterUtils.mergeIngredients(sourceIngredients(source)); + if (inputs.isEmpty()) { + return List.of(); + } + + ItemStack result = source.getResultItem(level == null ? null : level.registryAccess()); + if (result == null || result.isEmpty()) { + return List.of(); + } + + List outputs = new ArrayList<>(); + outputs.add(result.copy()); + + int processTime = source.getCookTime() > 0 ? source.getCookTime() : BASE_COOK_TIME; + long energy = Math.max(1L, + (long) processTime * AdapterUtils.DEFAULT_ENERGY / BASE_COOK_TIME); + + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + inputs, + List.of(), + List.of(), + outputs, + List.of(), + List.of(), + energy, + processTime, + Ingredient.EMPTY, + 0, + molds(source), + AlloyFurnaceMode.NORMAL + )); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || mergedInputs == null || mergedInputs.isEmpty() + || !matchesMold(mold)) { + return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + ModRecipeTypes.COOKING.get())) { + Map requirements = ingredientRequirements(holder.value()); + if (!requirements.isEmpty() && AdapterUtils.matchesRequired(mergedInputs, requirements)) { + matches.add(holder); + } + } + return matches; + } + + private static boolean isFarmersDelightRecipe(CookingPotRecipe recipe) { + return recipe != null && recipe.getType() == ModRecipeTypes.COOKING.get(); + } + + private static Map ingredientRequirements(CookingPotRecipe recipe) { + Map requirements = new LinkedHashMap<>(); + if (recipe == null) { + return requirements; + } + for (Ingredient ingredient : sourceIngredients(recipe)) { + if (!AdapterUtils.isIngredientEmpty(ingredient)) { + AdapterUtils.mergeIngredient(requirements, ingredient, 1L); + } + } + return requirements; + } + + /** The cooking pot consumes its serving container while producing the meal. */ + private static List sourceIngredients(CookingPotRecipe recipe) { + if (recipe == null) { + return List.of(); + } + + List ingredients = new ArrayList<>(recipe.getIngredients()); + ItemStack container = recipe.getOutputContainer(); + if (!DelightRecipeAdapterUtils.isBakingTray(container) + && container != null && !container.isEmpty() && container.getCount() > 0) { + for (int i = 0; i < container.getCount(); i++) { + ingredients.add(Ingredient.of(container.copyWithCount(1))); + } + } + return ingredients; + } + + private static List molds(CookingPotRecipe recipe) { + List molds = new ArrayList<>(); + Ingredient cookingPot = AdapterUtils.toMoldIngredient(new ItemStack(ModItems.COOKING_POT.get())); + if (!cookingPot.isEmpty()) { + molds.add(cookingPot); + } + molds.addAll(DelightRecipeAdapterUtils.bakingTrayMolds( + recipe == null ? ItemStack.EMPTY : recipe.getOutputContainer())); + return List.copyOf(molds); + } + +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/CuttingBoardRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/CuttingBoardRecipeAdapter.java new file mode 100644 index 00000000..0e76b402 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/CuttingBoardRecipeAdapter.java @@ -0,0 +1,174 @@ +package com.sorrowmist.useless.content.recipe.adapters.delight; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.ExpectedOutputScaler; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import vectorwing.farmersdelight.common.crafting.CuttingBoardRecipe; +import vectorwing.farmersdelight.common.crafting.ingredient.ChanceResult; +import vectorwing.farmersdelight.common.registry.ModItems; +import vectorwing.farmersdelight.common.registry.ModRecipeTypes; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Converts Cutting Board recipes into alloy-furnace recipes. */ +public final class CuttingBoardRecipeAdapter implements IRecipeAdapter { + @Override + public Class getRecipeClass() { + return CuttingBoardRecipe.class; + } + + /** The board is the reusable workstation mold for converted cutting recipes. */ + @Override + public ItemStack getMoldItem() { + return new ItemStack(ModItems.CUTTING_BOARD.get()); + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + return mold != null && !mold.isEmpty() && mold.is(ModItems.CUTTING_BOARD.get()); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + + CuttingBoardRecipe source = holder.value(); + if (!hasTool(source)) { + return List.of(); + } + + List inputs = AdapterUtils.mergeIngredients(source.getIngredients()); + if (inputs.isEmpty()) { + return List.of(); + } + + List weightedOutputs = new ArrayList<>(); + for (ChanceResult result : source.getRollableResults()) { + if (result == null || result.stack() == null || result.stack().isEmpty() + || result.stack().getCount() <= 0 || !Float.isFinite(result.chance())) { + continue; + } + ItemStack stack = result.stack(); + weightedOutputs.add(new ExpectedOutputScaler.WeightedItemOutput( + stack.copy(), stack.getCount(), stack.getCount(), result.chance())); + } + + var scaled = ExpectedOutputScaler.scale(weightedOutputs); + if (scaled.isEmpty() || scaled.get().outputs().isEmpty()) { + return List.of(); + } + + int operations = scaled.get().operations(); + List scaledInputs = scaleInputs(inputs, operations); + var energy = ExpectedOutputScaler.multiplyToInt(AdapterUtils.DEFAULT_ENERGY, operations); + var processTime = ExpectedOutputScaler.multiplyToInt( + AdapterUtils.DEFAULT_PROCESS_TIME, operations); + if (scaledInputs.isEmpty() || energy.isEmpty() || processTime.isEmpty()) { + return List.of(); + } + + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + scaledInputs, + List.of(), + List.of(), + scaled.get().outputs(), + List.of(), + List.of(), + energy.getAsInt(), + processTime.getAsInt(), + Ingredient.EMPTY, + 0, + List.of(AdapterUtils.toMoldIngredient(getMoldItem())), + AlloyFurnaceMode.NORMAL + )); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || mergedInputs == null || mergedInputs.isEmpty() + || !matchesMold(mold)) { + return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + ModRecipeTypes.CUTTING.get())) { + CuttingBoardRecipe source = holder.value(); + if (!hasTool(source)) { + continue; + } + + Map requirements = ingredientRequirements(source); + if (!requirements.isEmpty() + && AdapterUtils.matchesRequired(mergedInputs, requirements)) { + matches.add(holder); + } + } + return matches; + } + + private static List scaleInputs( + List inputs, int operations) { + List scaled = new ArrayList<>(inputs.size()); + for (CountedIngredient input : inputs) { + if (input == null || input.ingredient() == null || input.ingredient().isEmpty() + || input.count() <= 0) { + continue; + } + final long count; + try { + count = Math.multiplyExact(input.count(), (long) operations); + } catch (ArithmeticException exception) { + return List.of(); + } + if (count <= 0) { + return List.of(); + } + scaled.add(new CountedIngredient(input.ingredient(), count)); + } + return List.copyOf(scaled); + } + + private static Map ingredientRequirements(CuttingBoardRecipe recipe) { + Map requirements = new LinkedHashMap<>(); + if (recipe == null) { + return requirements; + } + for (Ingredient ingredient : recipe.getIngredients()) { + if (!AdapterUtils.isIngredientEmpty(ingredient)) { + AdapterUtils.mergeIngredient(requirements, ingredient, 1L); + } + } + return requirements; + } + + /** + * The tool is a non-consumed requirement of the original Cutting Board recipe. The board mold + * replaces that workstation in the converted recipe, so every non-empty tool ingredient is + * supported, including tool-action ingredients used by axes, pickaxes, hoes and shears. + */ + private static boolean hasTool(CuttingBoardRecipe recipe) { + return recipe != null && recipe.getTool() != null && !recipe.getTool().isEmpty(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/DelightRecipeAdapterUtils.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/DelightRecipeAdapterUtils.java new file mode 100644 index 00000000..09215228 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/DelightRecipeAdapterUtils.java @@ -0,0 +1,171 @@ +package com.sorrowmist.useless.content.recipe.adapters.delight; + +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.Recipe; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import vectorwing.farmersdelight.common.crafting.CookingPotRecipe; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Shared lookup and container handling for the Delight family of recipe adapters. */ +public final class DelightRecipeAdapterUtils { + private static final TagKey EXTRA_DELIGHT_BAKING_TRAYS = TagKey.create( + Registries.ITEM, + new ResourceLocation("extradelight", "baking_trays")); + + private DelightRecipeAdapterUtils() { + } + + public static > List> allOf( + RecipeManager recipeManager, Class recipeClass) { + if (recipeManager == null || recipeClass == null) { + return List.of(); + } + + List> result = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getRecipes(recipeManager)) { + if (recipeClass.isInstance(holder.value())) { + result.add(new RecipeHolder<>(holder.id(), recipeClass.cast(holder.value()))); + } + } + return List.copyOf(result); + } + + /** Farmer's Delight stores the serving container separately from the item ingredients. */ + public static List cookingIngredients(CookingPotRecipe recipe) { + if (recipe == null) { + return List.of(); + } + + List ingredients = new ArrayList<>(recipe.getIngredients()); + ItemStack container = recipe.getOutputContainer(); + if (!isBakingTray(container) && container != null && !container.isEmpty() + && container.getCount() > 0) { + for (int i = 0; i < container.getCount(); i++) { + ingredients.add(Ingredient.of(container.copyWithCount(1))); + } + } + return List.copyOf(ingredients); + } + + public static List cookingInputs(CookingPotRecipe recipe) { + return AdapterUtils.mergeIngredients(cookingIngredients(recipe)); + } + + public static boolean isBakingTray(@Nullable ItemStack stack) { + return stack != null && !stack.isEmpty() && stack.is(EXTRA_DELIGHT_BAKING_TRAYS); + } + + public static List bakingTrayMolds(@Nullable ItemStack stack) { + if (!isBakingTray(stack) || stack.getCount() <= 0) { + return List.of(); + } + List molds = new ArrayList<>(stack.getCount()); + for (int i = 0; i < stack.getCount(); i++) { + molds.add(Ingredient.of(stack.copyWithCount(1))); + } + return List.copyOf(molds); + } + + public static Map requirements(List inputs) { + if (inputs == null || inputs.isEmpty()) { + return Map.of(); + } + + Map result = new LinkedHashMap<>(); + for (CountedIngredient input : inputs) { + if (input == null || AdapterUtils.isIngredientEmpty(input.ingredient()) + || input.count() <= 0) { + continue; + } + AdapterUtils.mergeIngredient(result, input.ingredient(), input.count()); + } + return result; + } + + public static boolean hasConcreteInputs(@Nullable List inputs) { + return inputs != null && inputs.stream().anyMatch(stack -> + stack != null && !stack.isEmpty() && stack.getCount() > 0); + } + + public static boolean matchesItems(List requirements, + @Nullable Map mergedInputs, + @Nullable List actualInputs) { + boolean hasActualInputs = hasConcreteInputs(actualInputs); + boolean hasMergedInputs = mergedInputs != null && !mergedInputs.isEmpty(); + if (requirements == null || requirements.isEmpty()) { + return !hasActualInputs && !hasMergedInputs; + } + if (hasActualInputs) { + return com.sorrowmist.useless.content.recipe.ItemIngredientAllocator.matches( + requirements, actualInputs, 1L); + } + return hasMergedInputs && AdapterUtils.matchesRequired( + mergedInputs, requirements(requirements)); + } + + public static boolean matchesFluids( + List requirements, + @Nullable Map mergedFluids) { + boolean hasFluids = mergedFluids != null && !mergedFluids.isEmpty(); + if (requirements == null || requirements.isEmpty()) { + return !hasFluids; + } + return hasFluids && com.sorrowmist.useless.content.recipe.FluidIngredientAllocator + .matchesLong(requirements, mergedFluids, 1L); + } + + @Nullable + public static Item registeredItem(ResourceLocation id) { + return BuiltInRegistries.ITEM.getOptional(id).orElse(null); + } + + /** Reads an external recipe field without linking the adapter to its implementation classes. */ + @Nullable + public static Object fieldValue(@Nullable Object source, String name) { + if (source == null || name == null || name.isEmpty()) { + return null; + } + + Class current = source.getClass(); + while (current != null) { + try { + Field field = current.getDeclaredField(name); + if (!field.trySetAccessible()) { + return null; + } + return field.get(source); + } catch (NoSuchFieldException exception) { + current = current.getSuperclass(); + } catch (IllegalAccessException | RuntimeException exception) { + return null; + } + } + return null; + } + + @Nullable + public static T fieldValue(@Nullable Object source, String name, Class type) { + Object value = fieldValue(source, name); + return type != null && type.isInstance(value) ? type.cast(value) : null; + } + + public static int intField(@Nullable Object source, String name, int fallback) { + Object value = fieldValue(source, name); + return value instanceof Number number ? number.intValue() : fallback; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/DelightSyntheticRecipe.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/DelightSyntheticRecipe.java new file mode 100644 index 00000000..e49190fc --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/DelightSyntheticRecipe.java @@ -0,0 +1,55 @@ +package com.sorrowmist.useless.content.recipe.adapters.delight; + +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.Container; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; + +/** Recipe-manager payload for conversions built from external recipe data. */ +public final class DelightSyntheticRecipe implements Recipe { + private final AdvancedAlloyFurnaceRecipe convertedRecipe; + + public DelightSyntheticRecipe(AdvancedAlloyFurnaceRecipe convertedRecipe) { + this.convertedRecipe = convertedRecipe; + } + + public AdvancedAlloyFurnaceRecipe convertedRecipe() { + return convertedRecipe; + } + + @Override + public net.minecraft.resources.ResourceLocation getId() { return convertedRecipe.id(); } + + @Override + public boolean matches(Container input, Level level) { + return convertedRecipe != null && convertedRecipe.matches(input, level); + } + + @Override + public ItemStack assemble(Container input, net.minecraft.core.RegistryAccess registries) { + return convertedRecipe == null ? ItemStack.EMPTY : convertedRecipe.assemble(input, registries); + } + + @Override + public boolean canCraftInDimensions(int width, int height) { + return convertedRecipe != null && convertedRecipe.canCraftInDimensions(width, height); + } + + @Override + public ItemStack getResultItem(net.minecraft.core.RegistryAccess registries) { + return convertedRecipe == null ? ItemStack.EMPTY : convertedRecipe.getResultItem(registries); + } + + @Override + public RecipeSerializer getSerializer() { + return convertedRecipe == null ? null : convertedRecipe.getSerializer(); + } + + @Override + public RecipeType getType() { + return convertedRecipe == null ? null : convertedRecipe.getType(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/barbequesdelight/GrillingRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/barbequesdelight/GrillingRecipeAdapter.java new file mode 100644 index 00000000..3013c5ea --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/barbequesdelight/GrillingRecipeAdapter.java @@ -0,0 +1,140 @@ +package com.sorrowmist.useless.content.recipe.adapters.delight.barbequesdelight; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightRecipeAdapterUtils; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightSyntheticRecipe; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Barbeque's Delight grill recipes. */ +public final class GrillingRecipeAdapter implements IRecipeAdapter { + private static final String SOURCE_CLASS = + "com.mao.barbequesdelight.content.recipe.SimpleGrillingRecipe"; + private static final ResourceLocation GRILL_ID = + new ResourceLocation("barbequesdelight", "grill"); + + @Override + public String sourceId() { + return RecipeSourceIds.BARBEQUES_DELIGHT; + } + + @Override + public Class getRecipeClass() { + return DelightSyntheticRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + Item grill = DelightRecipeAdapterUtils.registeredItem(GRILL_ID); + return grill == null ? ItemStack.EMPTY : grill.getDefaultInstance(); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null + || holder.value().convertedRecipe() == null) { + return List.of(); + } + return List.of(holder.value().convertedRecipe()); + } + + @Override + public List> getGeneratedRecipes(Level level) { + if (level == null) { + return List.of(); + } + + List> generated = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getRecipes(level.getRecipeManager())) { + if (holder.value() == null || !SOURCE_CLASS.equals(holder.value().getClass().getName())) { + continue; + } + AdvancedAlloyFurnaceRecipe converted = convertSource(holder.id(), holder.value()); + if (converted != null) { + generated.add(new RecipeHolder<>(converted.id(), new DelightSyntheticRecipe(converted))); + } + } + return List.copyOf(generated); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || mergedInputs == null || mergedInputs.isEmpty() + || !matchesMold(mold)) { + return List.of(); + } + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : getGeneratedRecipes(level)) { + AdvancedAlloyFurnaceRecipe recipe = holder.value().convertedRecipe(); + if (recipe != null && DelightRecipeAdapterUtils.matchesItems( + recipe.inputs(), mergedInputs, List.of()) + && DelightRecipeAdapterUtils.matchesFluids(recipe.inputFluids(), mergedFluids)) { + matches.add(holder); + } + } + return List.copyOf(matches); + } + + @Nullable + private static AdvancedAlloyFurnaceRecipe convertSource(ResourceLocation sourceId, + Object source) { + Ingredient ingredient = DelightRecipeAdapterUtils.fieldValue(source, "ingredient", + Ingredient.class); + ItemStack output = DelightRecipeAdapterUtils.fieldValue(source, "output", ItemStack.class); + if (AdapterUtils.isIngredientEmpty(ingredient) || output == null || output.isEmpty() + || output.getCount() <= 0) { + return null; + } + + int processTime = Math.max(1, DelightRecipeAdapterUtils.intField( + source, "barbecuingTime", AdapterUtils.DEFAULT_PROCESS_TIME)); + int energy = AdapterUtils.safeInt( + (long) AdapterUtils.DEFAULT_ENERGY * processTime / 200L); + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(sourceId), + List.of(new CountedIngredient(ingredient, 1L)), + List.of(), + List.of(), + List.of(output.copy()), + List.of(), + List.of(), + Math.max(1, energy), + processTime, + Ingredient.EMPTY, + 0, + List.of(AdapterUtils.toMoldIngredient(moldStack())), + AlloyFurnaceMode.NORMAL + ); + } + + @Nullable + private static Item moldItem() { + return BuiltInRegistries.ITEM.getOptional(GRILL_ID).orElse(null); + } + + @Nullable + private static ItemStack moldStack() { + Item item = moldItem(); + return item == null ? null : item.getDefaultInstance(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/barbequesdelight/SkeweringRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/barbequesdelight/SkeweringRecipeAdapter.java new file mode 100644 index 00000000..10004663 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/barbequesdelight/SkeweringRecipeAdapter.java @@ -0,0 +1,155 @@ +package com.sorrowmist.useless.content.recipe.adapters.delight.barbequesdelight; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightRecipeAdapterUtils; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightSyntheticRecipe; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Converts Barbeque's Delight skewer recipes, consuming the skewer as an input. */ +public final class SkeweringRecipeAdapter implements IRecipeAdapter { + private static final String SOURCE_CLASS = + "com.mao.barbequesdelight.content.recipe.SimpleSkeweringRecipe"; + private static final ResourceLocation BASIN_ID = + new ResourceLocation("barbequesdelight", "basin"); + + @Override + public String sourceId() { + return RecipeSourceIds.BARBEQUES_DELIGHT; + } + + @Override + public Class getRecipeClass() { + return DelightSyntheticRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + Item basin = DelightRecipeAdapterUtils.registeredItem(BASIN_ID); + return basin == null ? ItemStack.EMPTY : basin.getDefaultInstance(); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null + || holder.value().convertedRecipe() == null) { + return List.of(); + } + return List.of(holder.value().convertedRecipe()); + } + + @Override + public List> getGeneratedRecipes(Level level) { + if (level == null) { + return List.of(); + } + + List> generated = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getRecipes(level.getRecipeManager())) { + if (holder.value() == null || !SOURCE_CLASS.equals(holder.value().getClass().getName())) { + continue; + } + AdvancedAlloyFurnaceRecipe converted = convertSource(holder.id(), holder.value()); + if (converted != null) { + generated.add(new RecipeHolder<>(converted.id(), new DelightSyntheticRecipe(converted))); + } + } + return List.copyOf(generated); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || mergedInputs == null || mergedInputs.isEmpty() + || !matchesMold(mold)) { + return List.of(); + } + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : getGeneratedRecipes(level)) { + AdvancedAlloyFurnaceRecipe recipe = holder.value().convertedRecipe(); + if (recipe != null && DelightRecipeAdapterUtils.matchesItems( + recipe.inputs(), mergedInputs, List.of()) + && DelightRecipeAdapterUtils.matchesFluids(recipe.inputFluids(), mergedFluids)) { + matches.add(holder); + } + } + return List.copyOf(matches); + } + + @Nullable + private static AdvancedAlloyFurnaceRecipe convertSource(ResourceLocation sourceId, + Object source) { + Ingredient tool = DelightRecipeAdapterUtils.fieldValue(source, "tool", Ingredient.class); + Ingredient ingredient = DelightRecipeAdapterUtils.fieldValue(source, "ingredient", + Ingredient.class); + Ingredient side = DelightRecipeAdapterUtils.fieldValue(source, "side", Ingredient.class); + ItemStack output = DelightRecipeAdapterUtils.fieldValue(source, "output", ItemStack.class); + int ingredientCount = DelightRecipeAdapterUtils.intField(source, "ingredientCount", 0); + int sideCount = DelightRecipeAdapterUtils.intField(source, "sideCount", 0); + if (AdapterUtils.isIngredientEmpty(tool) || AdapterUtils.isIngredientEmpty(ingredient) + || ingredientCount <= 0 || (sideCount > 0 && AdapterUtils.isIngredientEmpty(side)) + || output == null || output.isEmpty() || output.getCount() <= 0) { + return null; + } + + Map inputMap = new LinkedHashMap<>(); + AdapterUtils.mergeIngredient(inputMap, tool, 1L); + AdapterUtils.mergeIngredient(inputMap, ingredient, ingredientCount); + if (sideCount > 0) { + AdapterUtils.mergeIngredient(inputMap, side, sideCount); + } + List inputs = inputMap.entrySet().stream() + .map(entry -> new CountedIngredient(entry.getKey(), entry.getValue())) + .toList(); + if (inputs.isEmpty()) { + return null; + } + + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(sourceId), + inputs, + List.of(), + List.of(), + List.of(output.copy()), + List.of(), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + AdapterUtils.DEFAULT_PROCESS_TIME, + Ingredient.EMPTY, + 0, + List.of(AdapterUtils.toMoldIngredient(moldStack())), + AlloyFurnaceMode.NORMAL + ); + } + + @Nullable + private static Item moldItem() { + return BuiltInRegistries.ITEM.getOptional(BASIN_ID).orElse(null); + } + + @Nullable + private static ItemStack moldStack() { + Item item = moldItem(); + return item == null ? null : item.getDefaultInstance(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/brewinandchewin/KegFermentingRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/brewinandchewin/KegFermentingRecipeAdapter.java new file mode 100644 index 00000000..a6eb1b2a --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/brewinandchewin/KegFermentingRecipeAdapter.java @@ -0,0 +1,267 @@ +package com.sorrowmist.useless.content.recipe.adapters.delight.brewinandchewin; + +import com.mojang.datafixers.util.Either; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightRecipeAdapterUtils; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.CompoundFluidIngredient; +import com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient; +import org.jetbrains.annotations.Nullable; +import umpaz.brewinandchewin.common.crafting.FluidIngredientWithAmount; +import umpaz.brewinandchewin.common.crafting.KegFermentingRecipe; +import umpaz.brewinandchewin.common.crafting.KegPouringRecipe; +import umpaz.brewinandchewin.common.registry.BnCItems; +import umpaz.brewinandchewin.common.registry.BnCRecipeTypes; +import umpaz.brewinandchewin.common.utility.AbstractedFluidIngredient; +import umpaz.brewinandchewin.common.utility.AbstractedFluidStack; +import umpaz.brewinandchewin.neoforge.utility.KegCompatibleFluidIngredients; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** Converts Brewin And Chewin keg fermentation recipes. */ +public final class KegFermentingRecipeAdapter implements IRecipeAdapter { + @Override + public String sourceId() { + return RecipeSourceIds.BREWIN_AND_CHEWIN; + } + + @Override + public Class getRecipeClass() { + return KegFermentingRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(BnCItems.KEG); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + + KegFermentingRecipe source = holder.value(); + List inputs = AdapterUtils.mergeIngredients(source.getIngredients()); + List inputFluids = fluidInputs(source.getFluidIngredient()); + if (inputs.isEmpty() && inputFluids.isEmpty()) { + return List.of(); + } + + Either result = source.getResult(); + if (result == null) { + return List.of(); + } + + List outputs = new ArrayList<>(); + List outputFluids = List.of(); + Optional itemResult = result.right(); + if (itemResult.isPresent()) { + ItemStack output = itemResult.get(); + if (!output.isEmpty() && output.getCount() > 0) { + outputs.add(output.copy()); + } + } else { + Optional fluidResult = result.left(); + if (fluidResult.isPresent()) { + BottledResult bottled = bottledResult(level, fluidResult.get()); + if (bottled == null) { + return List.of(); + } + inputs = addContainer(inputs, bottled.container(), bottled.operations()); + outputs.add(bottled.output()); + } + } + if (outputs.isEmpty() && outputFluids.isEmpty()) { + return List.of(); + } + + int processTime = Math.max(1, source.getFermentTime()); + long energy = Math.max(1L, + (long) processTime * AdapterUtils.DEFAULT_ENERGY / 200L); + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + inputs, + inputFluids, + List.of(), + outputs, + outputFluids, + List.of(), + energy, + processTime, + Ingredient.EMPTY, + 0, + List.of(AdapterUtils.toMoldIngredient(getMoldItem())), + AlloyFurnaceMode.NORMAL + )); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold) + || (mergedInputs == null || mergedInputs.isEmpty()) + && (mergedFluids == null || mergedFluids.isEmpty())) { + return List.of(); + } + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : DelightRecipeAdapterUtils.allOf( + level.getRecipeManager(), KegFermentingRecipe.class)) { + KegFermentingRecipe source = holder.value(); + List itemRequirements = + AdapterUtils.mergeIngredients(source.getIngredients()); + Optional fluidResult = source.getResult().left(); + if (fluidResult.isPresent()) { + BottledResult bottled = bottledResult(level, fluidResult.get()); + if (bottled == null) { + continue; + } + itemRequirements = addContainer(itemRequirements, bottled.container(), bottled.operations()); + } + List fluidRequirements = + fluidInputs(source.getFluidIngredient()); + if (isMatch(itemRequirements, fluidRequirements, mergedInputs, mergedFluids)) { + matches.add(holder); + } + } + return List.copyOf(matches); + } + + private static List addContainer(List inputs, + ItemStack container, int operations) { + if (container == null || container.isEmpty() || operations <= 0) { + return List.of(); + } + Ingredient containerIngredient = Ingredient.of(container.copyWithCount(1)); + List result = new ArrayList<>(inputs); + for (int i = 0; i < result.size(); i++) { + CountedIngredient existing = result.get(i); + if (AdapterUtils.areIngredientsEqual(existing.ingredient(), containerIngredient)) { + long count; + try { + count = Math.addExact(existing.count(), operations); + } catch (ArithmeticException exception) { + return List.of(); + } + result.set(i, new CountedIngredient(existing.ingredient(), count)); + return List.copyOf(result); + } + } + result.add(new CountedIngredient(containerIngredient, operations)); + return List.copyOf(result); + } + + @Nullable + private static BottledResult bottledResult(Level level, AbstractedFluidStack fluid) { + if (level == null || fluid == null || fluid.isEmpty()) { + return null; + } + long available = fluid.unit().convertToLoader(fluid.amount()); + if (available <= 0L) { + return null; + } + + return com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(level.getRecipeManager(), BnCRecipeTypes.KEG_POURING).stream() + .map(RecipeHolder::value) + .sorted(Comparator.comparing(KegPouringRecipe::isStrict)) + .filter(pouring -> pouring.getRawFluid().matches(fluid)) + .map(pouring -> { + long perContainer = pouring.getLoaderAmount(); + if (perContainer <= 0L || available % perContainer != 0L) { + return null; + } + long operations = available / perContainer; + ItemStack container = pouring.getContainer(); + ItemStack output = pouring.getOutput(); + if (operations <= 0L || operations > Integer.MAX_VALUE + || container == null || container.isEmpty() + || output == null || output.isEmpty()) { + return null; + } + long outputCount = (long) output.getCount() * operations; + if (outputCount <= 0L || outputCount > Integer.MAX_VALUE) { + return null; + } + return new BottledResult(container.copyWithCount(1), + output.copyWithCount((int) outputCount), (int) operations); + }) + .filter(java.util.Objects::nonNull) + .findFirst() + .orElse(null); + } + + private static boolean isMatch(List itemRequirements, + List fluidRequirements, + @Nullable Map mergedInputs, + @Nullable Map mergedFluids) { + return DelightRecipeAdapterUtils.matchesItems( + itemRequirements, mergedInputs, List.of()) + && DelightRecipeAdapterUtils.matchesFluids(fluidRequirements, mergedFluids); + } + + private static List fluidInputs( + Optional source) { + if (source == null || source.isEmpty()) { + return List.of(); + } + + FluidIngredientWithAmount input = source.get(); + FluidIngredient ingredient = toFluidIngredient(input.ingredient()); + long amount = input.loaderAmount(); + if (ingredient == null || ingredient.isEmpty() || amount <= 0L) { + return List.of(); + } + return List.of(new LongSizedFluidIngredient(ingredient, amount)); + } + + @Nullable + private static FluidIngredient toFluidIngredient(AbstractedFluidIngredient source) { + if (source == null) { + return null; + } + + if (source instanceof KegCompatibleFluidIngredients.Tag tag) { + var key = tag.getTagKey(); + if (key != null) { + return FluidIngredient.tag(key); + } + } + + List choices = new ArrayList<>(); + for (AbstractedFluidStack display : source.displayStacks()) { + if (display == null || display.isEmpty()) { + continue; + } + Object loaderSpecific = display.loaderSpecific(); + if (!(loaderSpecific instanceof FluidStack fluid) || fluid.isEmpty()) { + continue; + } + FluidIngredient choice = AdapterUtils.toSizedFluidIngredient(fluid).ingredient(); + choices.add(choice); + } + if (choices.isEmpty()) { + return null; + } + return choices.size() == 1 ? choices.get(0) : CompoundFluidIngredient.of(choices); + } + + private record BottledResult(ItemStack container, ItemStack output, int operations) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/brewinandchewin/KegPouringRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/brewinandchewin/KegPouringRecipeAdapter.java new file mode 100644 index 00000000..015d55fa --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/brewinandchewin/KegPouringRecipeAdapter.java @@ -0,0 +1,119 @@ +package com.sorrowmist.useless.content.recipe.adapters.delight.brewinandchewin; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightRecipeAdapterUtils; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import umpaz.brewinandchewin.common.crafting.KegPouringRecipe; +import umpaz.brewinandchewin.common.registry.BnCItems; +import umpaz.brewinandchewin.common.utility.AbstractedFluidStack; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Brewin And Chewin keg pouring recipes, including unripe cheese wheels. */ +public final class KegPouringRecipeAdapter implements IRecipeAdapter { + @Override + public String sourceId() { + return RecipeSourceIds.BREWIN_AND_CHEWIN; + } + + @Override + public Class getRecipeClass() { + return KegPouringRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(BnCItems.KEG); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + + KegPouringRecipe source = holder.value(); + FluidStack fluid = toFluidStack(source.getRawFluid()); + ItemStack container = source.getContainer(); + ItemStack output = source.getOutput(); + if (fluid == null || container == null || container.isEmpty() + || output == null || output.isEmpty()) { + return List.of(); + } + + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + List.of(new CountedIngredient(Ingredient.of(container.copyWithCount(1)), 1L)), + List.of(LongSizedFluidIngredient.from(fluid)), + List.of(), + List.of(output.copy()), + List.of(), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + AdapterUtils.DEFAULT_PROCESS_TIME, + Ingredient.EMPTY, + 0, + List.of(AdapterUtils.toMoldIngredient(getMoldItem())), + AlloyFurnaceMode.NORMAL + )); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold) + || (mergedInputs == null || mergedInputs.isEmpty()) + && (mergedFluids == null || mergedFluids.isEmpty())) { + return List.of(); + } + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : DelightRecipeAdapterUtils.allOf( + level.getRecipeManager(), KegPouringRecipe.class)) { + KegPouringRecipe source = holder.value(); + FluidStack fluid = toFluidStack(source.getRawFluid()); + ItemStack container = source.getContainer(); + if (fluid == null || container == null || container.isEmpty() + || source.getOutput().isEmpty()) { + continue; + } + List itemRequirements = List.of( + new CountedIngredient(Ingredient.of(container.copyWithCount(1)), 1L)); + List fluidRequirements = List.of( + LongSizedFluidIngredient.from(fluid)); + if (DelightRecipeAdapterUtils.matchesItems(itemRequirements, mergedInputs, List.of()) + && DelightRecipeAdapterUtils.matchesFluids(fluidRequirements, mergedFluids)) { + matches.add(holder); + } + } + return List.copyOf(matches); + } + + @Nullable + private static FluidStack toFluidStack(AbstractedFluidStack source) { + if (source == null || source.isEmpty()) { + return null; + } + long amount = source.unit().convertToLoader(source.amount()); + if (amount <= 0L || amount > Integer.MAX_VALUE) { + return null; + } + return new FluidStack(source.fluid().builtInRegistryHolder(), (int) amount, + source.componentPatch()); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/casualnessdelight/DeepFryingRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/casualnessdelight/DeepFryingRecipeAdapter.java new file mode 100644 index 00000000..4d744872 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/casualnessdelight/DeepFryingRecipeAdapter.java @@ -0,0 +1,107 @@ +package com.sorrowmist.useless.content.recipe.adapters.delight.casualnessdelight; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import top.tobitobi.casualnessdelight.common.crafting.DeepFryingRecipe; +import top.tobitobi.casualnessdelight.common.registry.ModItems; +import top.tobitobi.casualnessdelight.common.registry.ModRecipeTypes; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Converts Casualness Delight deep-frying recipes. */ +public final class DeepFryingRecipeAdapter implements IRecipeAdapter { + private static final int BASE_COOK_TIME = 200; + + @Override + public String sourceId() { + return RecipeSourceIds.CASUALNESS_DELIGHT; + } + + @Override + public Class getRecipeClass() { + return DeepFryingRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return ModItems.DEEP_FRYING_PAN.get().getDefaultInstance(); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + + DeepFryingRecipe source = holder.value(); + Ingredient input = source.getIngredients().isEmpty() + ? Ingredient.EMPTY : source.getIngredients().get(0); + ItemStack output = source.getResultItem(level == null ? null : level.registryAccess()); + if (input == null || input.isEmpty() || output == null || output.isEmpty() + || output.getCount() <= 0) { + return List.of(); + } + + int processTime = Math.max(1, source.getCookingTime()); + long energy = Math.max(1L, + (long) processTime * AdapterUtils.DEFAULT_ENERGY / BASE_COOK_TIME); + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + List.of(new CountedIngredient(input, 1L)), + List.of(), + List.of(), + List.of(output.copy()), + List.of(), + List.of(), + energy, + processTime, + Ingredient.EMPTY, + 0, + List.of(AdapterUtils.toMoldIngredient(getMoldItem())), + AlloyFurnaceMode.NORMAL + )); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || mergedInputs == null || mergedInputs.isEmpty() + || (mergedFluids != null && !mergedFluids.isEmpty()) + || !matchesMold(mold)) { + return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + ModRecipeTypes.DEEP_FRYING.get())) { + Ingredient ingredient = holder.value().getIngredients().isEmpty() + ? Ingredient.EMPTY : holder.value().getIngredients().get(0); + if (ingredient != null && !ingredient.isEmpty()) { + Map requirements = new LinkedHashMap<>(); + AdapterUtils.mergeIngredient(requirements, ingredient, 1L); + if (com.sorrowmist.useless.content.recipe.ItemIngredientAllocator + .matches(mergedInputs, requirements)) { + matches.add(holder); + } + } + } + return List.copyOf(matches); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/crabbersdelight/CrabTrapRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/crabbersdelight/CrabTrapRecipeAdapter.java new file mode 100644 index 00000000..c2f0b4e0 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/crabbersdelight/CrabTrapRecipeAdapter.java @@ -0,0 +1,432 @@ +package com.sorrowmist.useless.content.recipe.adapters.delight.crabbersdelight; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.FluidIngredientAllocator; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.ItemIngredientAllocator; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.tags.TagKey; +import net.minecraft.util.GsonHelper; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluids; +import com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** Converts Crabbers Delight's data-driven crab-trap loot into deterministic recipes. */ +public final class CrabTrapRecipeAdapter implements IRecipeAdapter { + private static final String MOD_ID = "crabbersdelight"; + private static final ResourceLocation CRAB_TRAP_ID = + new ResourceLocation(MOD_ID, "crab_trap"); + private static final ResourceLocation CRAB_TRAP_BAIT_TAG = + new ResourceLocation(MOD_ID, "crab_trap_bait"); + private static final String DISPLAY_RESULT_TAG_PREFIX = "jei_display_results/"; + private static final int WATER_PER_OUTPUT = 1_000; + + @Override + public String sourceId() { + return RecipeSourceIds.CRABBERS_DELIGHT; + } + + @Override + public Class getRecipeClass() { + return CrabTrapSyntheticRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + Item trap = registeredItem(CRAB_TRAP_ID); + return trap == null ? null : trap.getDefaultInstance(); + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + Item trap = registeredItem(CRAB_TRAP_ID); + return trap != null && mold != null && !mold.isEmpty() && mold.is(trap); + } + + @Override + public List> getGeneratedRecipes(Level level) { + if (level == null) { + return List.of(); + } + + // Item tags are rebound on BuiltInRegistries.ITEM on the client when the server sends its + // tag packet. ClientLevel.registryAccess() may still expose the pre-sync view here. + Registry itemRegistry = BuiltInRegistries.ITEM; + ResourceManager resourceManager = resourceManager(level); + + List> recipes = new ArrayList<>(); + for (Item bait : baitItems(itemRegistry)) { + ResourceLocation baitId = itemRegistry.getKey(bait); + if (baitId == null || baitId.getPath().isBlank()) { + continue; + } + + // The JEI display tags intentionally omit some weighted entries (for example, + // message_bottle), so the actual loot table is the authoritative source for the + // deterministic all-output conversion. + JsonObject table = resourceManager == null ? null : readLootTable(resourceManager, + new ResourceLocation( + MOD_ID, "gameplay/crab_trap_loot/" + baitId.getPath())); + List fallbackOutputs = table == null + ? displayOutputs(itemRegistry, baitId) : List.of(); + AdvancedAlloyFurnaceRecipe converted = table == null + ? convertOutputs(itemRegistry, bait, baitId, fallbackOutputs) + : convertLootTable(itemRegistry, bait, baitId, table); + + if (converted != null) { + recipes.add(new RecipeHolder<>(converted.id(), + new CrabTrapSyntheticRecipe(converted))); + } + } + return List.copyOf(recipes); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null + || holder.value().convertedRecipe() == null) { + return List.of(); + } + return List.of(holder.value().convertedRecipe()); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + return findMatchingRecipes(level, mergedInputs, mergedFluids, Map.of(), mold, List.of()); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, + Map mergedKeys, + @Nullable ItemStack mold, List actualInputs) { + if (level == null || !matchesMold(mold)) { + return List.of(); + } + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : getGeneratedRecipes(level)) { + AdvancedAlloyFurnaceRecipe recipe = holder.value().convertedRecipe(); + if (recipe == null || !recipe.keyInputs().isEmpty()) { + continue; + } + + boolean hasConcreteInputs = hasConcreteInputs(actualInputs); + boolean hasMergedInputs = mergedInputs != null && !mergedInputs.isEmpty(); + if (recipe.inputs().isEmpty() && (hasConcreteInputs || hasMergedInputs)) { + continue; + } + + boolean itemsMatch = hasConcreteInputs + ? ItemIngredientAllocator.matches(recipe.inputs(), actualInputs, 1L) + : matchesMergedInputs(recipe.inputs(), mergedInputs); + boolean fluidsMatch = FluidIngredientAllocator + .matchesLong(recipe.inputFluids(), mergedFluids == null ? Map.of() : mergedFluids, 1L); + if (itemsMatch && fluidsMatch) { + matches.add(holder); + } + } + return matches; + } + + private static List baitItems(Registry itemRegistry) { + TagKey tag = TagKey.create(Registries.ITEM, CRAB_TRAP_BAIT_TAG); + List items = new ArrayList<>(); + // The source tag contains AIR for the trap's no-bait mode. Keep it so that mode is + // represented by a water-only converted recipe. + for (net.minecraft.core.Holder holder : itemRegistry.getTagOrEmpty(tag)) { + Item item = holder.value(); + if (item != null) { + items.add(item); + } + } + return List.copyOf(items); + } + + private static List displayOutputs(Registry itemRegistry, + ResourceLocation baitId) { + TagKey outputTag = TagKey.create(Registries.ITEM, + new ResourceLocation( + MOD_ID, DISPLAY_RESULT_TAG_PREFIX + baitId.getNamespace() + + "/" + baitId.getPath())); + List outputs = new ArrayList<>(); + for (var holder : itemRegistry.getTagOrEmpty(outputTag)) { + Item item = holder.value(); + if (item != null && item != Items.AIR) { + outputs.add(item.getDefaultInstance()); + } + } + return List.copyOf(outputs); + } + + @Nullable + private static Item registeredItem(ResourceLocation id) { + return registeredItem(BuiltInRegistries.ITEM, id); + } + + @Nullable + private static Item registeredItem(Registry itemRegistry, ResourceLocation id) { + return itemRegistry.getOptional(id).orElse(null); + } + + @Nullable + private static ResourceManager resourceManager(Level level) { + MinecraftServer server = level.getServer(); + if (server != null) { + return server.getResourceManager(); + } + + // A single-player client can read the integrated server's data resources. A remote client + // cannot, so it deliberately falls back to the synced JEI output tags above. + try { + Class minecraftClass = Class.forName("net.minecraft.client.Minecraft"); + Object minecraft = minecraftClass.getMethod("getInstance").invoke(null); + Object integratedServer = minecraftClass.getMethod("getSingleplayerServer") + .invoke(minecraft); + if (integratedServer instanceof MinecraftServer minecraftServer) { + return minecraftServer.getResourceManager(); + } + } catch (ReflectiveOperationException | LinkageError | ClassCastException ignored) { + } + return null; + } + + @Nullable + private static JsonObject readLootTable(ResourceManager resourceManager, + ResourceLocation tableId) { + ResourceLocation resourceId = new ResourceLocation( + tableId.getNamespace(), "loot_table/" + tableId.getPath() + ".json"); + Optional resource = resourceManager.getResource(resourceId); + if (resource.isEmpty()) { + return null; + } + + try (var reader = resource.get().openAsReader()) { + return GsonHelper.parse(reader); + } catch (IOException | RuntimeException ignored) { + return null; + } + } + + @Nullable + static AdvancedAlloyFurnaceRecipe convertLootTable( + Item bait, ResourceLocation baitId, JsonObject table) { + return convertLootTable(BuiltInRegistries.ITEM, bait, baitId, table); + } + + @Nullable + static AdvancedAlloyFurnaceRecipe convertLootTable( + Registry itemRegistry, Item bait, ResourceLocation baitId, JsonObject table) { + if (bait == null || baitId == null || table == null + || !isSupportedTable(table)) { + return null; + } + + List outputs = new ArrayList<>(); + JsonArray pools = table.getAsJsonArray("pools"); + for (JsonElement poolElement : pools) { + JsonObject pool = poolElement.isJsonObject() ? poolElement.getAsJsonObject() : null; + if (pool == null || !hasNoEntries(pool, "conditions") + || !hasNoEntries(pool, "functions")) { + return null; + } + + int rolls = fixedInteger(pool.get("rolls"), 1); + if (rolls < 0) { + return null; + } + if (rolls == 0) { + continue; + } + if (!isZero(pool.get("bonus_rolls"))) { + return null; + } + + JsonElement entriesElement = pool.get("entries"); + if (entriesElement == null || !entriesElement.isJsonArray()) { + return null; + } + JsonArray entries = entriesElement.getAsJsonArray(); + for (int roll = 0; roll < rolls; roll++) { + for (JsonElement entryElement : entries) { + JsonObject entry = entryElement.isJsonObject() + ? entryElement.getAsJsonObject() : null; + if (entry == null || !hasNoEntries(entry, "conditions") + || !hasNoEntries(entry, "functions")) { + return null; + } + + String type = stringValue(entry.get("type")); + if ("minecraft:empty".equals(type)) { + continue; + } + if (!"minecraft:item".equals(type)) { + return null; + } + + ResourceLocation itemId = ResourceLocation.tryParse(stringValue(entry.get("name"))); + Item item = itemId == null ? null : registeredItem(itemRegistry, itemId); + if (item == null || item == Items.AIR) { + return null; + } + // Crabbers Delight uses loot weights for random selection. The converted + // recipe intentionally makes every item entry deterministic and produces one. + outputs.add(item.getDefaultInstance()); + } + } + } + + if (outputs.isEmpty()) { + return null; + } + + return convertOutputs(itemRegistry, bait, baitId, outputs); + } + + @Nullable + static AdvancedAlloyFurnaceRecipe convertOutputs( + Registry itemRegistry, Item bait, ResourceLocation baitId, + List outputs) { + if (itemRegistry == null || bait == null || baitId == null + || outputs == null || outputs.isEmpty()) { + return null; + } + + List normalizedOutputs = new ArrayList<>(); + for (ItemStack output : outputs) { + if (output != null && !output.isEmpty() && output.getCount() > 0) { + normalizedOutputs.add(output.copy()); + } + } + if (normalizedOutputs.isEmpty()) { + return null; + } + + int operations = normalizedOutputs.size(); + long waterAmount = saturatingMultiply(operations, WATER_PER_OUTPUT); + long energy = saturatingMultiply(AdapterUtils.DEFAULT_ENERGY, operations); + int processTime = AdapterUtils.safeInt( + saturatingMultiply(AdapterUtils.DEFAULT_PROCESS_TIME, operations)); + Item trap = registeredItem(CRAB_TRAP_ID); + if (trap == null) { + return null; + } + + List inputs = bait == Items.AIR + ? List.of() + : List.of(new CountedIngredient(Ingredient.of(bait.getDefaultInstance()), operations)); + return new AdvancedAlloyFurnaceRecipe( + recipeId(baitId), + inputs, + List.of(new LongSizedFluidIngredient(FluidIngredient.single(Fluids.WATER), waterAmount)), + List.of(), + normalizedOutputs, + List.of(), + List.of(), + energy, + processTime, + Ingredient.EMPTY, + 0, + List.of(Ingredient.of(trap)), + AlloyFurnaceMode.NORMAL); + } + + private static boolean isSupportedTable(JsonObject table) { + return "minecraft:fishing".equals(stringValue(table.get("type"))) + && hasNoEntries(table, "functions") + && table.has("pools") + && table.get("pools").isJsonArray() + && !table.getAsJsonArray("pools").isEmpty(); + } + + private static boolean hasNoEntries(JsonObject object, String key) { + JsonElement value = object.get(key); + return value == null || (value.isJsonArray() && value.getAsJsonArray().isEmpty()); + } + + private static boolean isZero(@Nullable JsonElement element) { + return element == null || (element.isJsonPrimitive() + && element.getAsJsonPrimitive().isNumber() + && element.getAsDouble() == 0.0D); + } + + private static int fixedInteger(@Nullable JsonElement element, int defaultValue) { + if (element == null) { + return defaultValue; + } + if (!element.isJsonPrimitive() || !element.getAsJsonPrimitive().isNumber()) { + return -1; + } + double value = element.getAsDouble(); + return Double.isFinite(value) && value >= 0.0D && value <= Integer.MAX_VALUE + && Math.rint(value) == value ? (int) value : -1; + } + + private static String stringValue(@Nullable JsonElement element) { + return element != null && element.isJsonPrimitive() && element.getAsJsonPrimitive().isString() + ? element.getAsString() : ""; + } + + private static boolean matchesMergedInputs( + List requirements, Map mergedInputs) { + Map required = new LinkedHashMap<>(); + for (CountedIngredient requirement : requirements) { + AdapterUtils.mergeIngredient(required, requirement.ingredient(), requirement.count()); + } + return AdapterUtils.matchesRequired(mergedInputs == null ? Map.of() : mergedInputs, required); + } + + private static boolean hasConcreteInputs(@Nullable List inputs) { + if (inputs == null) { + return false; + } + return inputs.stream().anyMatch(stack -> stack != null && !stack.isEmpty() + && stack.getCount() > 0); + } + + private static ResourceLocation recipeId(ResourceLocation baitId) { + String path = baitId.getNamespace() + "_" + baitId.getPath().replace('/', '_'); + return new ResourceLocation(RecipeSourceIds.CRABBERS_DELIGHT, + "crab_trap_" + path + "_converted"); + } + + private static long saturatingMultiply(long left, long right) { + if (left <= 0L || right <= 0L) { + return 0L; + } + return left > Long.MAX_VALUE / right ? Long.MAX_VALUE : left * right; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/crabbersdelight/CrabTrapSyntheticRecipe.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/crabbersdelight/CrabTrapSyntheticRecipe.java new file mode 100644 index 00000000..9fa2bffa --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/crabbersdelight/CrabTrapSyntheticRecipe.java @@ -0,0 +1,55 @@ +package com.sorrowmist.useless.content.recipe.adapters.delight.crabbersdelight; + +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.Container; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; + +/** Holder payload for crab-trap loot tables, which are not RecipeManager recipes. */ +public final class CrabTrapSyntheticRecipe implements Recipe { + private final AdvancedAlloyFurnaceRecipe convertedRecipe; + + public CrabTrapSyntheticRecipe(AdvancedAlloyFurnaceRecipe convertedRecipe) { + this.convertedRecipe = convertedRecipe; + } + + public AdvancedAlloyFurnaceRecipe convertedRecipe() { + return convertedRecipe; + } + + @Override + public net.minecraft.resources.ResourceLocation getId() { return convertedRecipe.id(); } + + @Override + public boolean matches(Container input, Level level) { + return convertedRecipe != null && convertedRecipe.matches(input, level); + } + + @Override + public ItemStack assemble(Container input, net.minecraft.core.RegistryAccess registries) { + return convertedRecipe == null ? ItemStack.EMPTY : convertedRecipe.assemble(input, registries); + } + + @Override + public boolean canCraftInDimensions(int width, int height) { + return convertedRecipe != null && convertedRecipe.canCraftInDimensions(width, height); + } + + @Override + public ItemStack getResultItem(net.minecraft.core.RegistryAccess registries) { + return convertedRecipe == null ? ItemStack.EMPTY : convertedRecipe.getResultItem(registries); + } + + @Override + public RecipeSerializer getSerializer() { + return convertedRecipe == null ? null : convertedRecipe.getSerializer(); + } + + @Override + public RecipeType getType() { + return convertedRecipe == null ? null : convertedRecipe.getType(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/expandeddelight/JuicerRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/expandeddelight/JuicerRecipeAdapter.java new file mode 100644 index 00000000..29de5705 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/expandeddelight/JuicerRecipeAdapter.java @@ -0,0 +1,127 @@ +package com.sorrowmist.useless.content.recipe.adapters.delight.expandeddelight; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightRecipeAdapterUtils; +import ianm1647.expandeddelight.common.crafting.JuicerRecipe; +import ianm1647.expandeddelight.common.registry.EDItems; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Expanded Delight juicer recipes, including their output container input. */ +public final class JuicerRecipeAdapter implements IRecipeAdapter { + private static final int BASE_JUICE_TIME = 200; + + @Override + public String sourceId() { + return RecipeSourceIds.EXPANDED_DELIGHT; + } + + @Override + public Class getRecipeClass() { + return JuicerRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return EDItems.JUICER.get().getDefaultInstance(); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + + JuicerRecipe source = holder.value(); + List inputs = AdapterUtils.mergeIngredients(sourceIngredients(source)); + if (inputs.isEmpty()) { + return List.of(); + } + + ItemStack output = source.getResultItem(level == null ? null : level.registryAccess()); + if (output == null || output.isEmpty() || output.getCount() <= 0) { + return List.of(); + } + + int processTime = Math.max(1, source.getJuiceTime()); + long energy = Math.max(1L, + (long) processTime * AdapterUtils.DEFAULT_ENERGY / BASE_JUICE_TIME); + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + inputs, + List.of(), + List.of(), + List.of(output.copy()), + List.of(), + List.of(), + energy, + processTime, + Ingredient.EMPTY, + 0, + molds(source), + AlloyFurnaceMode.NORMAL + )); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || mergedInputs == null || mergedInputs.isEmpty() + || (mergedFluids != null && !mergedFluids.isEmpty()) + || !matchesMold(mold)) { + return List.of(); + } + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : DelightRecipeAdapterUtils.allOf( + level.getRecipeManager(), JuicerRecipe.class)) { + JuicerRecipe source = holder.value(); + List requirements = + AdapterUtils.mergeIngredients(sourceIngredients(source)); + if (!requirements.isEmpty() + && DelightRecipeAdapterUtils.matchesItems( + requirements, mergedInputs, List.of())) { + matches.add(holder); + } + } + return List.copyOf(matches); + } + + private static List sourceIngredients(JuicerRecipe recipe) { + List ingredients = new ArrayList<>(recipe.getIngredients()); + ItemStack container = recipe.getOutputContainer(); + if (!DelightRecipeAdapterUtils.isBakingTray(container) + && container != null && !container.isEmpty() && container.getCount() > 0) { + for (int i = 0; i < container.getCount(); i++) { + ingredients.add(Ingredient.of(container.copyWithCount(1))); + } + } + return ingredients; + } + + private static List molds(JuicerRecipe recipe) { + List molds = new ArrayList<>(); + Ingredient juicer = AdapterUtils.toMoldIngredient(EDItems.JUICER.get().getDefaultInstance()); + if (!juicer.isEmpty()) { + molds.add(juicer); + } + molds.addAll(DelightRecipeAdapterUtils.bakingTrayMolds( + recipe == null ? ItemStack.EMPTY : recipe.getOutputContainer())); + return List.copyOf(molds); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/extradelight/ChillerRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/extradelight/ChillerRecipeAdapter.java new file mode 100644 index 00000000..3bf0c732 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/extradelight/ChillerRecipeAdapter.java @@ -0,0 +1,137 @@ +package com.sorrowmist.useless.content.recipe.adapters.delight.extradelight; + +import com.lance5057.extradelight.workstations.chiller.ChillerRecipe; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightRecipeAdapterUtils; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Extra Delight chiller recipes, including their fluid and container inputs. */ +public final class ChillerRecipeAdapter implements IRecipeAdapter { + private static final ResourceLocation MOLD_ID = + new ResourceLocation("extradelight", "chiller"); + private static final ResourceLocation BAR_MOLD_ID = + new ResourceLocation("extradelight", "bar_mold"); + + @Override + public String sourceId() { + return RecipeSourceIds.EXTRA_DELIGHT; + } + + @Override + public Class getRecipeClass() { + return ChillerRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + ItemStack mold = ExtraDelightRecipeAdapterUtils.mold(MOLD_ID); + return mold == null ? ItemStack.EMPTY : mold; + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + ChillerRecipe source = holder.value(); + List inputs = AdapterUtils.mergeIngredients(itemInputs(source)); + FluidStack inputFluid = source.getFluid(); + ItemStack output = source.getResultItem(level == null ? null : level.registryAccess()); + if ((inputs.isEmpty() && (inputFluid == null || inputFluid.isEmpty())) + || output == null || output.isEmpty()) { + return List.of(); + } + int time = ExtraDelightRecipeAdapterUtils.processTime(source.getCookTime()); + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), inputs, + ExtraDelightRecipeAdapterUtils.fluid(inputFluid), List.of(), List.of(output.copy()), + List.of(), List.of(), ExtraDelightRecipeAdapterUtils.energy(time), time, + Ingredient.EMPTY, 0, molds(source), + AlloyFurnaceMode.NORMAL)); + } + + @Override + public List> findMatchingRecipes(Level level, + Map mergedInputs, Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) { + return List.of(); + } + List> matches = new ArrayList<>(); + for (RecipeHolder holder : DelightRecipeAdapterUtils.allOf( + level.getRecipeManager(), ChillerRecipe.class)) { + ChillerRecipe source = holder.value(); + List requirements = AdapterUtils.mergeIngredients( + itemInputs(source)); + List fluids = + ExtraDelightRecipeAdapterUtils.fluid(source.getFluid()); + if ((requirements.isEmpty() && fluids.isEmpty()) + || !DelightRecipeAdapterUtils.matchesItems(requirements, mergedInputs, List.of()) + || !DelightRecipeAdapterUtils.matchesFluids(fluids, mergedFluids)) { + continue; + } + matches.add(holder); + } + return List.copyOf(matches); + } + + private static List itemInputs(ChillerRecipe source) { + if (source == null) { + return List.of(); + } + ItemStack container = source.getOutputContainer(); + return isSecondaryMold(container) + ? source.getIngredients() + : ExtraDelightRecipeAdapterUtils.withContainer(source.getIngredients(), container); + } + + private static List molds(ChillerRecipe source) { + List molds = new ArrayList<>(); + Ingredient chiller = AdapterUtils.toMoldIngredient(getMoldStack()); + if (!chiller.isEmpty()) { + molds.add(chiller); + } + + if (source != null) { + ItemStack container = source.getOutputContainer(); + if (isSecondaryMold(container)) { + for (int i = 0; i < container.getCount(); i++) { + molds.add(Ingredient.of(container.copyWithCount(1))); + } + } + } + return List.copyOf(molds); + } + + private static ItemStack getMoldStack() { + ItemStack mold = ExtraDelightRecipeAdapterUtils.mold(MOLD_ID); + return mold == null ? ItemStack.EMPTY : mold; + } + + private static boolean isSecondaryMold(ItemStack stack) { + if (stack == null || stack.isEmpty()) { + return false; + } + if (DelightRecipeAdapterUtils.isBakingTray(stack)) { + return true; + } + ResourceLocation id = BuiltInRegistries.ITEM.getKey(stack.getItem()); + return BAR_MOLD_ID.equals(id); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/extradelight/DoughShapingRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/extradelight/DoughShapingRecipeAdapter.java new file mode 100644 index 00000000..1579a3d0 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/extradelight/DoughShapingRecipeAdapter.java @@ -0,0 +1,80 @@ +package com.sorrowmist.useless.content.recipe.adapters.delight.extradelight; + +import com.lance5057.extradelight.workstations.doughshaping.recipes.DoughShapingRecipe; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightRecipeAdapterUtils; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public final class DoughShapingRecipeAdapter implements IRecipeAdapter { + private static final ResourceLocation MOLD_ID = + new ResourceLocation("extradelight", "dough_shaping"); + + @Override + public String sourceId() { + return RecipeSourceIds.EXTRA_DELIGHT; + } + + @Override + public Class getRecipeClass() { + return DoughShapingRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + ItemStack mold = ExtraDelightRecipeAdapterUtils.mold(MOLD_ID); + return mold == null ? ItemStack.EMPTY : mold; + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + DoughShapingRecipe source = holder.value(); + ItemStack output = source.getResultItem(level == null ? null : level.registryAccess()); + if (source.getIngredients().isEmpty() || output == null || output.isEmpty()) { + return List.of(); + } + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + List.of(new CountedIngredient(source.getIngredients().get(0), 1L)), + List.of(), List.of(), List.of(output.copy()), List.of(), List.of(), + AdapterUtils.DEFAULT_ENERGY, AdapterUtils.DEFAULT_PROCESS_TIME, Ingredient.EMPTY, 0, + List.of(AdapterUtils.toMoldIngredient(getMoldItem())), AlloyFurnaceMode.NORMAL)); + } + + @Override + public List> findMatchingRecipes(Level level, + Map mergedInputs, Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold) || mergedFluids != null && !mergedFluids.isEmpty()) { + return List.of(); + } + List> matches = new ArrayList<>(); + for (RecipeHolder holder : DelightRecipeAdapterUtils.allOf( + level.getRecipeManager(), DoughShapingRecipe.class)) { + if (!holder.value().getIngredients().isEmpty() + && DelightRecipeAdapterUtils.matchesItems( + List.of(new CountedIngredient(holder.value().getIngredients().get(0), 1L)), + mergedInputs, List.of())) { + matches.add(holder); + } + } + return List.copyOf(matches); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/extradelight/DryingRackRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/extradelight/DryingRackRecipeAdapter.java new file mode 100644 index 00000000..bfc43fd0 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/extradelight/DryingRackRecipeAdapter.java @@ -0,0 +1,80 @@ +package com.sorrowmist.useless.content.recipe.adapters.delight.extradelight; + +import com.lance5057.extradelight.workstations.dryingrack.DryingRackRecipe; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightRecipeAdapterUtils; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public final class DryingRackRecipeAdapter implements IRecipeAdapter { + private static final ResourceLocation MOLD_ID = + new ResourceLocation("extradelight", "drying_rack_item"); + + @Override + public String sourceId() { + return RecipeSourceIds.EXTRA_DELIGHT; + } + + @Override + public Class getRecipeClass() { + return DryingRackRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + ItemStack mold = ExtraDelightRecipeAdapterUtils.mold(MOLD_ID); + return mold == null ? ItemStack.EMPTY : mold; + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + DryingRackRecipe source = holder.value(); + ItemStack output = source.getResultItem(level == null ? null : level.registryAccess()); + if (source.getInput().isEmpty() || output == null || output.isEmpty()) { + return List.of(); + } + int time = ExtraDelightRecipeAdapterUtils.processTime(source.getCookingTime()); + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + List.of(new CountedIngredient(source.getInput(), 1L)), List.of(), List.of(), + List.of(output.copy()), List.of(), List.of(), ExtraDelightRecipeAdapterUtils.energy(time), time, + Ingredient.EMPTY, 0, List.of(AdapterUtils.toMoldIngredient(getMoldItem())), + AlloyFurnaceMode.NORMAL)); + } + + @Override + public List> findMatchingRecipes(Level level, + Map mergedInputs, Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold) || mergedFluids != null && !mergedFluids.isEmpty()) { + return List.of(); + } + List> matches = new ArrayList<>(); + for (RecipeHolder holder : DelightRecipeAdapterUtils.allOf( + level.getRecipeManager(), DryingRackRecipe.class)) { + Ingredient input = holder.value().getInput(); + if (!input.isEmpty() && DelightRecipeAdapterUtils.matchesItems( + List.of(new CountedIngredient(input, 1L)), mergedInputs, List.of())) { + matches.add(holder); + } + } + return List.copyOf(matches); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/extradelight/EvaporatorRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/extradelight/EvaporatorRecipeAdapter.java new file mode 100644 index 00000000..c676d4ca --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/extradelight/EvaporatorRecipeAdapter.java @@ -0,0 +1,90 @@ +package com.sorrowmist.useless.content.recipe.adapters.delight.extradelight; + +import com.lance5057.extradelight.workstations.evaporator.recipes.EvaporatorRecipe; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightRecipeAdapterUtils; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts evaporator recipes using their deterministic display item output. */ +public final class EvaporatorRecipeAdapter implements IRecipeAdapter { + private static final ResourceLocation MOLD_ID = + new ResourceLocation("extradelight", "evaporator"); + + @Override + public String sourceId() { + return RecipeSourceIds.EXTRA_DELIGHT; + } + + @Override + public Class getRecipeClass() { + return EvaporatorRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + ItemStack mold = ExtraDelightRecipeAdapterUtils.mold(MOLD_ID); + return mold == null ? ItemStack.EMPTY : mold; + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + EvaporatorRecipe source = holder.value(); + ItemStack output = source.getResultItem(); + if (!hasFluid(source.getFluid()) + || output == null || output.isEmpty()) { + return List.of(); + } + int time = ExtraDelightRecipeAdapterUtils.processTime(source.getCookTime()); + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), List.of(), + List.of(com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient.from( + source.getFluid())), List.of(), List.of(output.copy()), List.of(), List.of(), + ExtraDelightRecipeAdapterUtils.energy(time), time, Ingredient.EMPTY, 0, + List.of(AdapterUtils.toMoldIngredient(getMoldItem())), AlloyFurnaceMode.NORMAL)); + } + + @Override + public List> findMatchingRecipes(Level level, + Map mergedInputs, Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) { + return List.of(); + } + List> matches = new ArrayList<>(); + for (RecipeHolder holder : DelightRecipeAdapterUtils.allOf( + level.getRecipeManager(), EvaporatorRecipe.class)) { + EvaporatorRecipe source = holder.value(); + if (hasFluid(source.getFluid()) + && source.getResultItem() != null && !source.getResultItem().isEmpty() + && DelightRecipeAdapterUtils.matchesFluids( + List.of(com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient.from( + source.getFluid())), mergedFluids)) { + matches.add(holder); + } + } + return List.copyOf(matches); + } + + private static boolean hasFluid(@Nullable SizedFluidIngredient fluid) { + return fluid != null && fluid.amount() > 0 && fluid.ingredient() != null + && !fluid.ingredient().isEmpty(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/extradelight/ExtraDelightRecipeAdapterUtils.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/extradelight/ExtraDelightRecipeAdapterUtils.java new file mode 100644 index 00000000..f0ad941c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/extradelight/ExtraDelightRecipeAdapterUtils.java @@ -0,0 +1,102 @@ +package com.sorrowmist.useless.content.recipe.adapters.delight.extradelight; + +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightRecipeAdapterUtils; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +final class ExtraDelightRecipeAdapterUtils { + private static final int BASE_PROCESS_TIME = 200; + + private ExtraDelightRecipeAdapterUtils() { + } + + @Nullable + static ItemStack mold(ResourceLocation id) { + Item item = DelightRecipeAdapterUtils.registeredItem(id); + return item == null ? null : item.getDefaultInstance(); + } + + static List withContainer(List ingredients, @Nullable ItemStack container) { + List result = new ArrayList<>(); + if (ingredients != null) { + result.addAll(ingredients); + } + if (!DelightRecipeAdapterUtils.isBakingTray(container) + && container != null && !container.isEmpty() && container.getCount() > 0) { + for (int i = 0; i < container.getCount(); i++) { + result.add(Ingredient.of(container.copyWithCount(1))); + } + } + return List.copyOf(result); + } + + static List fluids(List ingredients) { + if (ingredients == null || ingredients.isEmpty()) { + return List.of(); + } + return ingredients.stream() + .filter(ingredient -> ingredient != null && !ingredient.ingredient().isEmpty() + && ingredient.amount() > 0) + .map(LongSizedFluidIngredient::from) + .toList(); + } + + static List fluid(@Nullable FluidStack stack) { + if (stack == null || stack.isEmpty() || stack.getAmount() <= 0) { + return List.of(); + } + return List.of(LongSizedFluidIngredient.from(stack)); + } + + static int processTime(int time) { + return Math.max(1, time); + } + + static long energy(int time) { + return Math.max(1L, (long) processTime(time) * AdapterUtils.DEFAULT_ENERGY / BASE_PROCESS_TIME); + } + + static List scaleInputs(List inputs, int operations) { + if (inputs == null || inputs.isEmpty() || operations <= 0) { + return List.of(); + } + List result = new ArrayList<>(inputs.size()); + for (CountedIngredient input : inputs) { + if (input == null || input.ingredient() == null || input.ingredient().isEmpty() + || input.count() <= 0) { + return List.of(); + } + long count; + try { + count = Math.multiplyExact(input.count(), (long) operations); + } catch (ArithmeticException exception) { + return List.of(); + } + result.add(new CountedIngredient(input.ingredient(), count)); + } + return List.copyOf(result); + } + + @Nullable + static FluidStack scaledFluid(@Nullable FluidStack fluid, int operations) { + if (fluid == null || fluid.isEmpty() || fluid.getAmount() <= 0 || operations <= 0) { + return null; + } + var amount = Math.multiplyExact((long) fluid.getAmount(), (long) operations); + if (amount > Integer.MAX_VALUE) { + return null; + } + return new net.minecraftforge.fluids.FluidStack(fluid, (int) amount); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/extradelight/JuicerRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/extradelight/JuicerRecipeAdapter.java new file mode 100644 index 00000000..7ffde4d5 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/extradelight/JuicerRecipeAdapter.java @@ -0,0 +1,112 @@ +package com.sorrowmist.useless.content.recipe.adapters.delight.extradelight; + +import com.lance5057.extradelight.workstations.juicer.JuicerRecipe; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.ExpectedOutputScaler; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightRecipeAdapterUtils; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** Converts Extra Delight juicer recipes into deterministic input batches. */ +public final class JuicerRecipeAdapter implements IRecipeAdapter { + private static final ResourceLocation MOLD_ID = + new ResourceLocation("extradelight", "juicer"); + + @Override + public String sourceId() { + return RecipeSourceIds.EXTRA_DELIGHT; + } + + @Override + public Class getRecipeClass() { + return JuicerRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + ItemStack mold = ExtraDelightRecipeAdapterUtils.mold(MOLD_ID); + return mold == null ? ItemStack.EMPTY : mold; + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + JuicerRecipe source = holder.value(); + Ingredient input = source.getInput(); + FluidStack fluidOutput = source.getFluid(); + ItemStack itemOutput = source.getResultItem(level == null ? null : level.registryAccess()); + if (input == null || input.isEmpty() + || (fluidOutput == null || fluidOutput.isEmpty()) + && (itemOutput == null || itemOutput.isEmpty())) { + return List.of(); + } + + List weightedOutputs = itemOutput == null + || itemOutput.isEmpty() ? List.of() : List.of(new ExpectedOutputScaler.WeightedItemOutput( + itemOutput.copy(), itemOutput.getCount(), itemOutput.getCount(), + Math.max(0, Math.min(100, source.getChance())) / 100.0)); + Optional scaled = ExpectedOutputScaler.scale(weightedOutputs); + if (scaled.isEmpty()) { + return List.of(); + } + + int operations = scaled.get().operations(); + List inputs = ExtraDelightRecipeAdapterUtils.scaleInputs( + List.of(new CountedIngredient(input, 1L)), operations); + FluidStack scaledFluid; + try { + scaledFluid = ExtraDelightRecipeAdapterUtils.scaledFluid(fluidOutput, operations); + } catch (ArithmeticException exception) { + return List.of(); + } + var energy = ExpectedOutputScaler.multiplyToInt(AdapterUtils.DEFAULT_ENERGY, operations); + var processTime = ExpectedOutputScaler.multiplyToInt(AdapterUtils.DEFAULT_PROCESS_TIME, operations); + if (inputs.isEmpty() || energy.isEmpty() || processTime.isEmpty() + || scaled.get().outputs().isEmpty() && scaledFluid == null) { + return List.of(); + } + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), inputs, List.of(), List.of(), + scaled.get().outputs(), scaledFluid == null ? List.of() : List.of(scaledFluid), List.of(), + energy.getAsInt(), processTime.getAsInt(), Ingredient.EMPTY, 0, + List.of(AdapterUtils.toMoldIngredient(getMoldItem())), AlloyFurnaceMode.NORMAL)); + } + + @Override + public List> findMatchingRecipes(Level level, + Map mergedInputs, Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) { + return List.of(); + } + List> matches = new ArrayList<>(); + for (RecipeHolder holder : DelightRecipeAdapterUtils.allOf( + level.getRecipeManager(), JuicerRecipe.class)) { + JuicerRecipe source = holder.value(); + if (source.getInput() != null && !source.getInput().isEmpty() + && DelightRecipeAdapterUtils.matchesItems( + List.of(new CountedIngredient(source.getInput(), 1L)), mergedInputs, List.of()) + && (mergedFluids == null || mergedFluids.isEmpty())) { + matches.add(holder); + } + } + return List.copyOf(matches); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/extradelight/MeltingPotRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/extradelight/MeltingPotRecipeAdapter.java new file mode 100644 index 00000000..e37a121f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/extradelight/MeltingPotRecipeAdapter.java @@ -0,0 +1,83 @@ +package com.sorrowmist.useless.content.recipe.adapters.delight.extradelight; + +import com.lance5057.extradelight.workstations.meltingpot.MeltingPotRecipe; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightRecipeAdapterUtils; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Extra Delight melting-pot recipes, which produce fluids. */ +public final class MeltingPotRecipeAdapter implements IRecipeAdapter { + private static final ResourceLocation MOLD_ID = + new ResourceLocation("extradelight", "melting_pot"); + + @Override + public String sourceId() { + return RecipeSourceIds.EXTRA_DELIGHT; + } + + @Override + public Class getRecipeClass() { + return MeltingPotRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + ItemStack mold = ExtraDelightRecipeAdapterUtils.mold(MOLD_ID); + return mold == null ? ItemStack.EMPTY : mold; + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + MeltingPotRecipe source = holder.value(); + Ingredient input = source.input; + FluidStack output = source.result; + if (input == null || input.isEmpty() || output == null || output.isEmpty()) { + return List.of(); + } + int time = ExtraDelightRecipeAdapterUtils.processTime(source.cooktime); + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + List.of(new CountedIngredient(input, 1L)), List.of(), List.of(), List.of(), + List.of(output.copy()), List.of(), ExtraDelightRecipeAdapterUtils.energy(time), time, + Ingredient.EMPTY, 0, List.of(AdapterUtils.toMoldIngredient(getMoldItem())), + AlloyFurnaceMode.NORMAL)); + } + + @Override + public List> findMatchingRecipes(Level level, + Map mergedInputs, Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold) || mergedFluids != null && !mergedFluids.isEmpty()) { + return List.of(); + } + List> matches = new ArrayList<>(); + for (RecipeHolder holder : DelightRecipeAdapterUtils.allOf( + level.getRecipeManager(), MeltingPotRecipe.class)) { + Ingredient input = holder.value().input; + if (input != null && !input.isEmpty() + && DelightRecipeAdapterUtils.matchesItems( + List.of(new CountedIngredient(input, 1L)), mergedInputs, List.of())) { + matches.add(holder); + } + } + return List.copyOf(matches); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/extradelight/MixingBowlRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/extradelight/MixingBowlRecipeAdapter.java new file mode 100644 index 00000000..227104e0 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/extradelight/MixingBowlRecipeAdapter.java @@ -0,0 +1,94 @@ +package com.sorrowmist.useless.content.recipe.adapters.delight.extradelight; + +import com.lance5057.extradelight.workstations.mixingbowl.recipes.MixingBowlRecipe; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightRecipeAdapterUtils; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public final class MixingBowlRecipeAdapter implements IRecipeAdapter { + private static final ResourceLocation MOLD_ID = + new ResourceLocation("extradelight", "mixing_bowl"); + + @Override + public String sourceId() { + return RecipeSourceIds.EXTRA_DELIGHT; + } + + @Override + public Class getRecipeClass() { + return MixingBowlRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + ItemStack mold = ExtraDelightRecipeAdapterUtils.mold(MOLD_ID); + return mold == null ? ItemStack.EMPTY : mold; + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + MixingBowlRecipe source = holder.value(); + List inputs = AdapterUtils.mergeIngredients( + ExtraDelightRecipeAdapterUtils.withContainer(source.getIngredients(), source.getContainer())); + List molds = new ArrayList<>(); + Ingredient mixingBowl = AdapterUtils.toMoldIngredient(getMoldItem()); + if (!mixingBowl.isEmpty()) { + molds.add(mixingBowl); + } + molds.addAll(DelightRecipeAdapterUtils.bakingTrayMolds(source.getContainer())); + List fluids = ExtraDelightRecipeAdapterUtils.fluids(source.getFluids()); + ItemStack output = source.getResultItem(level == null ? null : level.registryAccess()); + if (inputs.isEmpty() && fluids.isEmpty() || output == null || output.isEmpty()) { + return List.of(); + } + int time = ExtraDelightRecipeAdapterUtils.processTime(source.getStirs()); + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), inputs, fluids, List.of(), List.of(output.copy()), + List.of(), List.of(), ExtraDelightRecipeAdapterUtils.energy(time), time, + Ingredient.EMPTY, 0, molds, + AlloyFurnaceMode.NORMAL)); + } + + @Override + public List> findMatchingRecipes(Level level, + Map mergedInputs, Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) { + return List.of(); + } + List> matches = new ArrayList<>(); + for (RecipeHolder holder : DelightRecipeAdapterUtils.allOf( + level.getRecipeManager(), MixingBowlRecipe.class)) { + MixingBowlRecipe source = holder.value(); + List requirements = AdapterUtils.mergeIngredients( + ExtraDelightRecipeAdapterUtils.withContainer(source.getIngredients(), source.getContainer())); + List fluids = ExtraDelightRecipeAdapterUtils.fluids(source.getFluids()); + if ((requirements.isEmpty() && fluids.isEmpty()) + || !DelightRecipeAdapterUtils.matchesItems(requirements, mergedInputs, List.of()) + || !DelightRecipeAdapterUtils.matchesFluids(fluids, mergedFluids)) { + continue; + } + matches.add(holder); + } + return List.copyOf(matches); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/extradelight/MortarRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/extradelight/MortarRecipeAdapter.java new file mode 100644 index 00000000..858ac5e7 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/extradelight/MortarRecipeAdapter.java @@ -0,0 +1,89 @@ +package com.sorrowmist.useless.content.recipe.adapters.delight.extradelight; + +import com.lance5057.extradelight.workstations.mortar.recipes.MortarRecipe; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightRecipeAdapterUtils; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public final class MortarRecipeAdapter implements IRecipeAdapter { + private static final ResourceLocation MOLD_ID = + new ResourceLocation("extradelight", "mortar_stone"); + + @Override + public String sourceId() { + return RecipeSourceIds.EXTRA_DELIGHT; + } + + @Override + public Class getRecipeClass() { + return MortarRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + ItemStack mold = ExtraDelightRecipeAdapterUtils.mold(MOLD_ID); + return mold == null ? ItemStack.EMPTY : mold; + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + MortarRecipe source = holder.value(); + Ingredient input = source.getIngredients().isEmpty() ? Ingredient.EMPTY + : source.getIngredients().get(0); + ItemStack output = source.getResultItem(level == null ? null : level.registryAccess()); + FluidStack outputFluid = source.getFluid(); + if (input.isEmpty() + || (output == null || output.isEmpty()) && (outputFluid == null || outputFluid.isEmpty())) { + return List.of(); + } + int time = ExtraDelightRecipeAdapterUtils.processTime(source.getGrinds()); + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + List.of(new CountedIngredient(input, 1L)), List.of(), List.of(), + output == null || output.isEmpty() ? List.of() : List.of(output.copy()), + outputFluid == null || outputFluid.isEmpty() ? List.of() : List.of(outputFluid.copy()), List.of(), + ExtraDelightRecipeAdapterUtils.energy(time), time, Ingredient.EMPTY, 0, + List.of(AdapterUtils.toMoldIngredient(getMoldItem())), AlloyFurnaceMode.NORMAL)); + } + + @Override + public List> findMatchingRecipes(Level level, + Map mergedInputs, Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) { + return List.of(); + } + List> matches = new ArrayList<>(); + for (RecipeHolder holder : DelightRecipeAdapterUtils.allOf( + level.getRecipeManager(), MortarRecipe.class)) { + MortarRecipe source = holder.value(); + Ingredient input = source.getIngredients().isEmpty() ? Ingredient.EMPTY + : source.getIngredients().get(0); + if (!input.isEmpty() + && DelightRecipeAdapterUtils.matchesItems( + List.of(new CountedIngredient(input, 1L)), mergedInputs, List.of()) + && (mergedFluids == null || mergedFluids.isEmpty())) { + matches.add(holder); + } + } + return List.copyOf(matches); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/extradelight/OvenRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/extradelight/OvenRecipeAdapter.java new file mode 100644 index 00000000..83cba77d --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/extradelight/OvenRecipeAdapter.java @@ -0,0 +1,89 @@ +package com.sorrowmist.useless.content.recipe.adapters.delight.extradelight; + +import com.lance5057.extradelight.workstations.oven.recipes.OvenRecipe; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightRecipeAdapterUtils; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public final class OvenRecipeAdapter implements IRecipeAdapter { + private static final ResourceLocation MOLD_ID = + new ResourceLocation("extradelight", "oven"); + + @Override + public String sourceId() { + return RecipeSourceIds.EXTRA_DELIGHT; + } + + @Override + public Class getRecipeClass() { + return OvenRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + ItemStack mold = ExtraDelightRecipeAdapterUtils.mold(MOLD_ID); + return mold == null ? ItemStack.EMPTY : mold; + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + OvenRecipe source = holder.value(); + List inputs = AdapterUtils.mergeIngredients( + ExtraDelightRecipeAdapterUtils.withContainer(source.getIngredients(), source.getOutputContainer())); + List molds = new ArrayList<>(); + Ingredient oven = AdapterUtils.toMoldIngredient(getMoldItem()); + if (!oven.isEmpty()) { + molds.add(oven); + } + molds.addAll(DelightRecipeAdapterUtils.bakingTrayMolds(source.getOutputContainer())); + ItemStack output = source.getResultItem(level == null ? null : level.registryAccess()); + if (inputs.isEmpty() || output == null || output.isEmpty()) { + return List.of(); + } + int time = ExtraDelightRecipeAdapterUtils.processTime(source.getCookTime()); + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), inputs, List.of(), List.of(), List.of(output.copy()), + List.of(), List.of(), ExtraDelightRecipeAdapterUtils.energy(time), time, + Ingredient.EMPTY, 0, molds, + AlloyFurnaceMode.NORMAL)); + } + + @Override + public List> findMatchingRecipes(Level level, + Map mergedInputs, Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold) || mergedFluids != null && !mergedFluids.isEmpty()) { + return List.of(); + } + List> matches = new ArrayList<>(); + for (RecipeHolder holder : DelightRecipeAdapterUtils.allOf( + level.getRecipeManager(), OvenRecipe.class)) { + List requirements = AdapterUtils.mergeIngredients( + ExtraDelightRecipeAdapterUtils.withContainer(holder.value().getIngredients(), + holder.value().getOutputContainer())); + if (!requirements.isEmpty() && DelightRecipeAdapterUtils.matchesItems( + requirements, mergedInputs, List.of())) { + matches.add(holder); + } + } + return List.copyOf(matches); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/extradelight/VatRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/extradelight/VatRecipeAdapter.java new file mode 100644 index 00000000..36d122e7 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/extradelight/VatRecipeAdapter.java @@ -0,0 +1,125 @@ +package com.sorrowmist.useless.content.recipe.adapters.delight.extradelight; + +import com.lance5057.extradelight.workstations.vat.recipes.VatRecipe; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightRecipeAdapterUtils; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public final class VatRecipeAdapter implements IRecipeAdapter { + private static final ResourceLocation MOLD_ID = + new ResourceLocation("extradelight", "vat"); + + @Override + public String sourceId() { + return RecipeSourceIds.EXTRA_DELIGHT; + } + + @Override + public Class getRecipeClass() { + return VatRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + ItemStack mold = ExtraDelightRecipeAdapterUtils.mold(MOLD_ID); + return mold == null ? ItemStack.EMPTY : mold; + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + VatRecipe source = holder.value(); + List sourceIngredients = new ArrayList<>(source.getIngredients()); + long totalTime = 0L; + for (VatRecipe.StageIngredient stage : source.getStageIngredients()) { + if (stage == null) { + continue; + } + if (stage.ingredient != null && !stage.ingredient.isEmpty()) { + sourceIngredients.add(stage.ingredient); + } + totalTime = Math.min(Integer.MAX_VALUE, totalTime + Math.max(0, (long) stage.time)); + } + sourceIngredients = new ArrayList<>(ExtraDelightRecipeAdapterUtils.withContainer( + sourceIngredients, source.getUsedItem())); + List inputs = AdapterUtils.mergeIngredients(sourceIngredients); + List fluids = source.getFluid() == null + ? List.of() : ExtraDelightRecipeAdapterUtils.fluids(List.of(source.getFluid())); + ItemStack output = source.getResultItem(level == null ? null : level.registryAccess()); + if (inputs.isEmpty() && fluids.isEmpty() || output == null || output.isEmpty()) { + return List.of(); + } + int time = ExtraDelightRecipeAdapterUtils.processTime( + totalTime <= 0L ? AdapterUtils.DEFAULT_PROCESS_TIME : (int) totalTime); + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), inputs, fluids, List.of(), List.of(output.copy()), + List.of(), List.of(), ExtraDelightRecipeAdapterUtils.energy(time), time, + Ingredient.EMPTY, 0, molds(source), + AlloyFurnaceMode.NORMAL)); + } + + @Override + public List> findMatchingRecipes(Level level, + Map mergedInputs, Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) { + return List.of(); + } + List> matches = new ArrayList<>(); + for (RecipeHolder holder : DelightRecipeAdapterUtils.allOf( + level.getRecipeManager(), VatRecipe.class)) { + VatRecipe source = holder.value(); + List sourceIngredients = new ArrayList<>(source.getIngredients()); + for (VatRecipe.StageIngredient stage : source.getStageIngredients()) { + if (stage != null && stage.ingredient != null && !stage.ingredient.isEmpty()) { + sourceIngredients.add(stage.ingredient); + } + } + List requirements = AdapterUtils.mergeIngredients( + ExtraDelightRecipeAdapterUtils.withContainer(sourceIngredients, source.getUsedItem())); + List fluids = source.getFluid() == null + ? List.of() : ExtraDelightRecipeAdapterUtils.fluids(List.of(source.getFluid())); + if ((requirements.isEmpty() && fluids.isEmpty()) + || !DelightRecipeAdapterUtils.matchesItems(requirements, mergedInputs, List.of()) + || !DelightRecipeAdapterUtils.matchesFluids(fluids, mergedFluids)) { + continue; + } + matches.add(holder); + } + return List.copyOf(matches); + } + + private static List molds(VatRecipe source) { + List molds = new ArrayList<>(); + Ingredient vat = AdapterUtils.toMoldIngredient(getMoldStack()); + if (!vat.isEmpty()) { + molds.add(vat); + } + molds.addAll(DelightRecipeAdapterUtils.bakingTrayMolds( + source == null ? ItemStack.EMPTY : source.getUsedItem())); + return List.copyOf(molds); + } + + private static ItemStack getMoldStack() { + ItemStack mold = ExtraDelightRecipeAdapterUtils.mold(MOLD_ID); + return mold == null ? ItemStack.EMPTY : mold; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/nomadsdelight/ButterChurnRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/nomadsdelight/ButterChurnRecipeAdapter.java new file mode 100644 index 00000000..6eb800c7 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/nomadsdelight/ButterChurnRecipeAdapter.java @@ -0,0 +1,131 @@ +package com.sorrowmist.useless.content.recipe.adapters.delight.nomadsdelight; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightRecipeAdapterUtils; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightSyntheticRecipe; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Nomad's Delight's JEI-only butter churn recipes. */ +public final class ButterChurnRecipeAdapter implements IRecipeAdapter { + private static final ResourceLocation MOLD_ID = + new ResourceLocation("nomads_delight", "butter_churn"); + private static final ResourceLocation HORSE_MILK_BUCKET_ID = + new ResourceLocation("nomads_delight", "horse_milk_bucket"); + private static final ResourceLocation QUMYZ_BUCKET_ID = + new ResourceLocation("nomads_delight", "qumyz_bucket"); + private static final ResourceLocation CAMEL_MILK_BUCKET_ID = + new ResourceLocation("nomads_delight", "camel_milk_bucket"); + private static final ResourceLocation SHUBAT_BUCKET_ID = + new ResourceLocation("nomads_delight", "shubat_bucket"); + private static final ResourceLocation BUTTER_ID = + new ResourceLocation("nomads_delight", "butter"); + private static final int CHURN_TIME_TICKS = 6000; + + @Override + public String sourceId() { + return RecipeSourceIds.NOMADS_DELIGHT; + } + + @Override + public Class getRecipeClass() { + return DelightSyntheticRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return itemStack(MOLD_ID); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null + || holder.value().convertedRecipe() == null) { + return List.of(); + } + return List.of(holder.value().convertedRecipe()); + } + + @Override + public List> getGeneratedRecipes(Level level) { + if (level == null) { + return List.of(); + } + + List> generated = new ArrayList<>(); + addRecipe(generated, "horse_milk", HORSE_MILK_BUCKET_ID, QUMYZ_BUCKET_ID); + addRecipe(generated, "camel_milk", CAMEL_MILK_BUCKET_ID, SHUBAT_BUCKET_ID); + addRecipe(generated, "milk", new ResourceLocation("milk_bucket"), BUTTER_ID); + return List.copyOf(generated); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold) + || mergedInputs == null || mergedInputs.isEmpty() + || (mergedFluids != null && !mergedFluids.isEmpty())) { + return List.of(); + } + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : getGeneratedRecipes(level)) { + AdvancedAlloyFurnaceRecipe recipe = holder.value().convertedRecipe(); + if (recipe != null && DelightRecipeAdapterUtils.matchesItems( + recipe.inputs(), mergedInputs, List.of())) { + matches.add(holder); + } + } + return List.copyOf(matches); + } + + private static void addRecipe(List> recipes, + String name, ResourceLocation inputId, + ResourceLocation outputId) { + ItemStack input = itemStack(inputId); + ItemStack output = itemStack(outputId); + if (input.isEmpty() || output.isEmpty()) { + return; + } + + ResourceLocation sourceId = new ResourceLocation( + "nomads_delight", "butter_churn/" + name); + AdvancedAlloyFurnaceRecipe converted = new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(sourceId), + AdapterUtils.mergeIngredients(List.of(Ingredient.of(input))), + List.of(), + List.of(), + List.of(output), + List.of(), + List.of(), + Math.max(1L, (long) CHURN_TIME_TICKS * AdapterUtils.DEFAULT_ENERGY / 200L), + CHURN_TIME_TICKS, + Ingredient.EMPTY, + 0, + List.of(AdapterUtils.toMoldIngredient(itemStack(MOLD_ID))), + AlloyFurnaceMode.NORMAL + ); + recipes.add(new RecipeHolder<>(converted.id(), new DelightSyntheticRecipe(converted))); + } + + private static ItemStack itemStack(ResourceLocation id) { + Item item = DelightRecipeAdapterUtils.registeredItem(id); + return item == null ? ItemStack.EMPTY : item.getDefaultInstance(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/nomadsdelight/CurdBagRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/nomadsdelight/CurdBagRecipeAdapter.java new file mode 100644 index 00000000..bbcea105 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/nomadsdelight/CurdBagRecipeAdapter.java @@ -0,0 +1,114 @@ +package com.sorrowmist.useless.content.recipe.adapters.delight.nomadsdelight; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightRecipeAdapterUtils; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightSyntheticRecipe; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Map; + +/** Converts Nomad's Delight's JEI-only curd straining recipe. */ +public final class CurdBagRecipeAdapter implements IRecipeAdapter { + private static final ResourceLocation MOLD_ID = + new ResourceLocation("nomads_delight", "curd_bag"); + private static final ResourceLocation QATYQ_BUCKET_ID = + new ResourceLocation("nomads_delight", "qatyq_bucket"); + private static final ResourceLocation CURD_ID = + new ResourceLocation("nomads_delight", "curd"); + private static final int STRAINING_TIME_TICKS = 2400; + + @Override + public String sourceId() { + return RecipeSourceIds.NOMADS_DELIGHT; + } + + @Override + public Class getRecipeClass() { + return DelightSyntheticRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return itemStack(MOLD_ID); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null + || holder.value().convertedRecipe() == null) { + return List.of(); + } + return List.of(holder.value().convertedRecipe()); + } + + @Override + public List> getGeneratedRecipes(Level level) { + if (level == null) { + return List.of(); + } + + ItemStack input = itemStack(QATYQ_BUCKET_ID); + ItemStack output = itemStack(CURD_ID); + ItemStack mold = itemStack(MOLD_ID); + if (input.isEmpty() || output.isEmpty() || mold.isEmpty()) { + return List.of(); + } + + ResourceLocation sourceId = new ResourceLocation( + "nomads_delight", "curd_bag"); + AdvancedAlloyFurnaceRecipe converted = new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(sourceId), + AdapterUtils.mergeIngredients(List.of(Ingredient.of(input))), + List.of(), + List.of(), + List.of(output), + List.of(), + List.of(), + Math.max(1L, (long) STRAINING_TIME_TICKS * AdapterUtils.DEFAULT_ENERGY / 200L), + STRAINING_TIME_TICKS, + Ingredient.EMPTY, + 0, + List.of(AdapterUtils.toMoldIngredient(mold)), + AlloyFurnaceMode.NORMAL + ); + return List.of(new RecipeHolder<>( + converted.id(), new DelightSyntheticRecipe(converted))); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold) + || mergedInputs == null || mergedInputs.isEmpty() + || (mergedFluids != null && !mergedFluids.isEmpty())) { + return List.of(); + } + + return getGeneratedRecipes(level).stream() + .filter(holder -> { + AdvancedAlloyFurnaceRecipe recipe = holder.value().convertedRecipe(); + return recipe != null && DelightRecipeAdapterUtils.matchesItems( + recipe.inputs(), mergedInputs, List.of()); + }) + .toList(); + } + + private static ItemStack itemStack(ResourceLocation id) { + Item item = DelightRecipeAdapterUtils.registeredItem(id); + return item == null ? ItemStack.EMPTY : item.getDefaultInstance(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/ubesdelight/BakingMatRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/ubesdelight/BakingMatRecipeAdapter.java new file mode 100644 index 00000000..3983cd8b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/ubesdelight/BakingMatRecipeAdapter.java @@ -0,0 +1,196 @@ +package com.sorrowmist.useless.content.recipe.adapters.delight.ubesdelight; + +import com.chefmooon.ubesdelight.common.crafting.neoforge.BakingMatRecipeImpl; +import com.chefmooon.ubesdelight.common.crafting.ingredient.ChanceResult; +import com.chefmooon.ubesdelight.common.tag.CommonTags; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.ExpectedOutputScaler; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightRecipeAdapterUtils; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Converts Ube's Delight baking-mat recipes into deterministic alloy-furnace batches. */ +public final class BakingMatRecipeAdapter implements IRecipeAdapter { + private static final ResourceLocation BAKING_MAT_ID = + new ResourceLocation("ubesdelight", "baking_mat_bamboo"); + + @Override + public String sourceId() { + return RecipeSourceIds.UBES_DELIGHT; + } + + @Override + public Class getRecipeClass() { + return BakingMatRecipeImpl.class; + } + + @Override + public ItemStack getMoldItem() { + Item item = DelightRecipeAdapterUtils.registeredItem(BAKING_MAT_ID); + return item == null ? ItemStack.EMPTY : item.getDefaultInstance(); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + + BakingMatRecipeImpl source = holder.value(); + if (!isSupported(source)) { + return List.of(); + } + + List inputs = AdapterUtils.mergeIngredients(source.getIngredients()); + if (inputs.isEmpty()) { + return List.of(); + } + + List weightedOutputs = new ArrayList<>(); + for (ChanceResult result : source.getRollableResults()) { + if (result == null || result.stack() == null || result.stack().isEmpty() + || result.stack().getCount() <= 0 || !Float.isFinite(result.chance())) { + continue; + } + int count = result.stack().getCount(); + weightedOutputs.add(new ExpectedOutputScaler.WeightedItemOutput( + result.stack().copy(), count, count, result.chance())); + } + + var scaled = ExpectedOutputScaler.scale(weightedOutputs); + if (scaled.isEmpty() || scaled.get().outputs().isEmpty()) { + return List.of(); + } + + int operations = scaled.get().operations(); + List scaledInputs = scaleInputs(inputs, operations); + var energy = ExpectedOutputScaler.multiplyToInt(AdapterUtils.DEFAULT_ENERGY, operations); + var processTime = ExpectedOutputScaler.multiplyToInt( + AdapterUtils.DEFAULT_PROCESS_TIME, operations); + if (scaledInputs.isEmpty() || energy.isEmpty() || processTime.isEmpty()) { + return List.of(); + } + + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + scaledInputs, + List.of(), + List.of(), + scaled.get().outputs(), + List.of(), + List.of(), + energy.getAsInt(), + processTime.getAsInt(), + Ingredient.EMPTY, + 0, + List.of(AdapterUtils.toMoldIngredient(getMoldItem())), + AlloyFurnaceMode.NORMAL + )); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || mergedInputs == null || mergedInputs.isEmpty() + || !matchesMold(mold)) { + return List.of(); + } + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : DelightRecipeAdapterUtils.allOf( + level.getRecipeManager(), BakingMatRecipeImpl.class)) { + BakingMatRecipeImpl source = holder.value(); + if (!isSupported(source)) { + continue; + } + Map requirements = requirements(source.getIngredients()); + if (!requirements.isEmpty() && AdapterUtils.matchesRequired(mergedInputs, requirements)) { + matches.add(holder); + } + } + return List.copyOf(matches); + } + + private static List scaleInputs( + List inputs, int operations) { + List result = new ArrayList<>(inputs.size()); + for (CountedIngredient input : inputs) { + if (input == null || input.ingredient() == null || input.ingredient().isEmpty() + || input.count() <= 0) { + continue; + } + final long count; + try { + count = Math.multiplyExact(input.count(), (long) operations); + } catch (ArithmeticException exception) { + return List.of(); + } + if (count <= 0) { + return List.of(); + } + result.add(new CountedIngredient(input.ingredient(), count)); + } + return List.copyOf(result); + } + + private static Map requirements(List ingredients) { + Map result = new LinkedHashMap<>(); + if (ingredients == null) { + return result; + } + for (Ingredient ingredient : ingredients) { + if (!AdapterUtils.isIngredientEmpty(ingredient)) { + AdapterUtils.mergeIngredient(result, ingredient, 1L); + } + } + return result; + } + + private static boolean isSupported(BakingMatRecipeImpl recipe) { + return recipe != null + && recipe.getProcessStages() != null + && recipe.getProcessStages().isEmpty() + && isRollingPinTool(recipe.getTool()); + } + + private static boolean isRollingPinTool(@Nullable Ingredient tool) { + if (tool == null || tool.isEmpty()) { + return false; + } + + try { + for (Ingredient.Value value : tool.getValues()) { + if (value instanceof Ingredient.TagValue tagValue + && tagValue.tag().equals(CommonTags.C_TOOLS_ROLLING_PIN)) { + return true; + } + } + for (ItemStack rollingPin : Ingredient.of(CommonTags.C_TOOLS_ROLLING_PIN).getItems()) { + if (!rollingPin.isEmpty() && tool.test(rollingPin)) { + return true; + } + } + } catch (RuntimeException ignored) { + return false; + } + return false; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/youkaishomecoming/BasinRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/youkaishomecoming/BasinRecipeAdapter.java new file mode 100644 index 00000000..10f2a5a3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/youkaishomecoming/BasinRecipeAdapter.java @@ -0,0 +1,149 @@ +package com.sorrowmist.useless.content.recipe.adapters.delight.youkaishomecoming; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightRecipeAdapterUtils; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightSyntheticRecipe; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Youkai's wood-basin recipes into bottled outputs. */ +public final class BasinRecipeAdapter implements IRecipeAdapter { + private static final String SOURCE_CLASS = + "dev.xkmc.youkaishomecoming.content.pot.basin.SimpleBasinRecipe"; + private static final ResourceLocation BASIN_ID = + new ResourceLocation("youkaisfeasts", "wood_basin"); + + @Override + public String sourceId() { + return RecipeSourceIds.YOUKAI_HOMECOMING; + } + + @Override + public Class getRecipeClass() { + return DelightSyntheticRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + Item item = DelightRecipeAdapterUtils.registeredItem(BASIN_ID); + return item == null ? ItemStack.EMPTY : item.getDefaultInstance(); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null + || holder.value().convertedRecipe() == null) { + return List.of(); + } + return List.of(holder.value().convertedRecipe()); + } + + @Override + public List> getGeneratedRecipes(Level level) { + if (level == null) { + return List.of(); + } + + List> generated = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getRecipes(level.getRecipeManager())) { + if (holder.value() == null || !SOURCE_CLASS.equals(holder.value().getClass().getName())) { + continue; + } + AdvancedAlloyFurnaceRecipe converted = convertSource(holder.id(), holder.value()); + if (converted != null) { + generated.add(new RecipeHolder<>(converted.id(), new DelightSyntheticRecipe(converted))); + } + } + return List.copyOf(generated); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) { + return List.of(); + } + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : getGeneratedRecipes(level)) { + AdvancedAlloyFurnaceRecipe recipe = holder.value().convertedRecipe(); + if (recipe != null + && DelightRecipeAdapterUtils.matchesItems(recipe.inputs(), mergedInputs, List.of()) + && DelightRecipeAdapterUtils.matchesFluids(recipe.inputFluids(), mergedFluids)) { + matches.add(holder); + } + } + return List.copyOf(matches); + } + + @Nullable + private static AdvancedAlloyFurnaceRecipe convertSource(ResourceLocation sourceId, + Object source) { + Ingredient input = DelightRecipeAdapterUtils.fieldValue(source, "input", Ingredient.class); + FluidStack output = DelightRecipeAdapterUtils.fieldValue(source, "output", FluidStack.class); + if (input == null || input.isEmpty() || output == null || output.isEmpty() + || output.getAmount() <= 0) { + return null; + } + + int operations = YoukaiRecipeAdapterUtils.operationsPerContainer(output); + ItemStack bottledOutput = YoukaiRecipeAdapterUtils.bottledOutputForOperations(output, operations); + if (bottledOutput != null && operations > 0) { + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(sourceId), + List.of(new CountedIngredient(input, operations)), + List.of(), + List.of(), + List.of(bottledOutput), + List.of(), + List.of(), + Math.max(1L, (long) AdapterUtils.DEFAULT_ENERGY * operations), + Math.max(1, AdapterUtils.safeInt( + (long) AdapterUtils.DEFAULT_PROCESS_TIME * operations)), + Ingredient.EMPTY, + 0, + List.of(AdapterUtils.toMoldIngredient(moldStack())), + AlloyFurnaceMode.NORMAL + ); + } + + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(sourceId), + List.of(new CountedIngredient(input, 1L)), + List.of(), + List.of(), + List.of(), + List.of(output.copy()), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + AdapterUtils.DEFAULT_PROCESS_TIME, + Ingredient.EMPTY, + 0, + List.of(AdapterUtils.toMoldIngredient(moldStack())), + AlloyFurnaceMode.NORMAL + ); + } + + @Nullable + private static ItemStack moldStack() { + Item item = DelightRecipeAdapterUtils.registeredItem(BASIN_ID); + return item == null ? null : item.getDefaultInstance(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/youkaishomecoming/CuisineRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/youkaishomecoming/CuisineRecipeAdapter.java new file mode 100644 index 00000000..0440b245 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/youkaishomecoming/CuisineRecipeAdapter.java @@ -0,0 +1,212 @@ +package com.sorrowmist.useless.content.recipe.adapters.delight.youkaishomecoming; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightRecipeAdapterUtils; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightSyntheticRecipe; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Youkai's cuisine-board recipes. */ +public final class CuisineRecipeAdapter implements IRecipeAdapter { + private static final List SOURCE_CLASSES = List.of( + "dev.xkmc.youkaishomecoming.content.pot.table.recipe.FixedCuisineRecipe", + "dev.xkmc.youkaishomecoming.content.pot.table.recipe.MixedCuisineRecipe", + "dev.xkmc.youkaishomecoming.content.pot.table.recipe.OrderedCuisineRecipe", + "dev.xkmc.youkaishomecoming.content.pot.table.recipe.UnorderedCuisineRecipe" + ); + private static final ResourceLocation CUISINE_BOARD_ID = + new ResourceLocation("youkaisfeasts", "cuisine_board"); + + @Override + public String sourceId() { + return RecipeSourceIds.YOUKAI_HOMECOMING; + } + + @Override + public Class getRecipeClass() { + return DelightSyntheticRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + Item item = DelightRecipeAdapterUtils.registeredItem(CUISINE_BOARD_ID); + return item == null ? ItemStack.EMPTY : item.getDefaultInstance(); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null + || holder.value().convertedRecipe() == null) { + return List.of(); + } + return List.of(holder.value().convertedRecipe()); + } + + @Override + public List> getGeneratedRecipes(Level level) { + if (level == null) { + return List.of(); + } + + List> generated = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getRecipes(level.getRecipeManager())) { + if (holder.value() == null + || !SOURCE_CLASSES.contains(holder.value().getClass().getName())) { + continue; + } + AdvancedAlloyFurnaceRecipe converted = convertSource(holder.id(), holder.value()); + if (converted != null) { + generated.add(new RecipeHolder<>(converted.id(), new DelightSyntheticRecipe(converted))); + } + } + return List.copyOf(generated); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) { + return List.of(); + } + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : getGeneratedRecipes(level)) { + AdvancedAlloyFurnaceRecipe recipe = holder.value().convertedRecipe(); + if (recipe != null + && DelightRecipeAdapterUtils.matchesItems(recipe.inputs(), mergedInputs, List.of()) + && DelightRecipeAdapterUtils.matchesFluids(recipe.inputFluids(), mergedFluids)) { + matches.add(holder); + } + } + return List.copyOf(matches); + } + + @Nullable + private static AdvancedAlloyFurnaceRecipe convertSource(ResourceLocation sourceId, + Object source) { + ResourceLocation base = DelightRecipeAdapterUtils.fieldValue( + source, "base", ResourceLocation.class); + ItemStack output = DelightRecipeAdapterUtils.fieldValue(source, "result", ItemStack.class); + if (base == null || output == null || output.isEmpty() || output.getCount() <= 0) { + return null; + } + + List ingredients = sourceIngredients(source, base); + if (ingredients.isEmpty()) { + return null; + } + + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(sourceId), + AdapterUtils.mergeIngredients(ingredients), + List.of(), + List.of(), + List.of(output.copy()), + List.of(), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + AdapterUtils.DEFAULT_PROCESS_TIME, + Ingredient.EMPTY, + 0, + List.of(AdapterUtils.toMoldIngredient(moldStack())), + AlloyFurnaceMode.NORMAL + ); + } + + private static List sourceIngredients(Object source, ResourceLocation base) { + List baseIngredients = new ArrayList<>(); + List customIngredients = new ArrayList<>(); + collectBaseIngredients(base, baseIngredients, customIngredients); + + String className = source.getClass().getName(); + if (className.endsWith("MixedCuisineRecipe")) { + appendIngredients(customIngredients, source, "first"); + appendIngredients(customIngredients, source, "second"); + } else if (!className.endsWith("FixedCuisineRecipe")) { + appendIngredients(customIngredients, source, "input"); + } + + baseIngredients.addAll(customIngredients); + return baseIngredients.stream() + .filter(ingredient -> ingredient != null && !ingredient.isEmpty()) + .toList(); + } + + private static void appendIngredients(List target, Object source, String field) { + List values = DelightRecipeAdapterUtils.fieldValue(source, field, List.class); + if (values == null) { + return; + } + for (Object value : values) { + if (value instanceof Ingredient ingredient) { + target.add(ingredient); + } + } + } + + private static void collectBaseIngredients(ResourceLocation base, + List baseIngredients, + List customIngredients) { + try { + Class variantClass = Class.forName( + "dev.xkmc.youkaishomecoming.content.pot.table.item.VariantTableItemBase"); + Field mapField = variantClass.getDeclaredField("MAP"); + if (!mapField.trySetAccessible()) { + return; + } + Object mapValue = mapField.get(null); + if (mapValue instanceof Map map && map.get(base) != null) { + Method collect = variantClass.getMethod( + "collectIngredients", List.class, List.class); + collect.invoke(map.get(base), baseIngredients, customIngredients); + return; + } + + Class fixedClass = Class.forName( + "dev.xkmc.youkaishomecoming.content.pot.table.item.IngredientTableItem"); + Field fixedField = fixedClass.getDeclaredField("FIXED"); + if (!fixedField.trySetAccessible()) { + return; + } + Object fixedValue = fixedField.get(null); + if (fixedValue instanceof Map fixed && fixed.get(base) != null) { + Method collect = fixedClass.getMethod("collectIngredients", List.class); + collect.invoke(fixed.get(base), baseIngredients); + return; + } + } catch (ReflectiveOperationException | RuntimeException | LinkageError ignored) { + // Fall back to a direct item base below when the optional table model is unavailable. + } + + Item item = BuiltInRegistries.ITEM.get(base); + if (item != null && item != Items.AIR) { + baseIngredients.add(Ingredient.of(item)); + } + } + + @Nullable + private static ItemStack moldStack() { + Item item = DelightRecipeAdapterUtils.registeredItem(CUISINE_BOARD_ID); + return item == null ? null : item.getDefaultInstance(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/youkaishomecoming/DryingRackRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/youkaishomecoming/DryingRackRecipeAdapter.java new file mode 100644 index 00000000..6fc9ad10 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/youkaishomecoming/DryingRackRecipeAdapter.java @@ -0,0 +1,105 @@ +package com.sorrowmist.useless.content.recipe.adapters.delight.youkaishomecoming; + +import dev.xkmc.youkaishomecoming.content.pot.rack.DryingRackRecipe; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightRecipeAdapterUtils; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Youkai's Homecoming drying-rack recipes. */ +public final class DryingRackRecipeAdapter implements IRecipeAdapter { + private static final ResourceLocation RACK_ID = + new ResourceLocation("youkaisfeasts", "drying_rack"); + + @Override + public String sourceId() { + return RecipeSourceIds.YOUKAI_HOMECOMING; + } + + @Override + public Class getRecipeClass() { + return DryingRackRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + var item = DelightRecipeAdapterUtils.registeredItem(RACK_ID); + return item == null ? ItemStack.EMPTY : item.getDefaultInstance(); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + + DryingRackRecipe source = holder.value(); + Ingredient ingredient = source.getIngredients().isEmpty() + ? Ingredient.EMPTY : source.getIngredients().get(0); + if (ingredient.isEmpty()) { + return List.of(); + } + ItemStack result = source.getResultItem(level == null ? null : level.registryAccess()); + if (result == null || result.isEmpty()) { + return List.of(); + } + + int processTime = source.getCookingTime() > 0 ? source.getCookingTime() : 100; + int energy = AdapterUtils.safeInt( + (long) processTime * AdapterUtils.DEFAULT_ENERGY / 100L); + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + List.of(new CountedIngredient(ingredient, 1L)), + List.of(), + List.of(), + List.of(result.copy()), + List.of(), + List.of(), + Math.max(1, energy), + processTime, + Ingredient.EMPTY, + 0, + List.of(AdapterUtils.toMoldIngredient(getMoldItem())), + AlloyFurnaceMode.NORMAL + )); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || mergedInputs == null || mergedInputs.isEmpty() + || !matchesMold(mold)) { + return List.of(); + } + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : DelightRecipeAdapterUtils.allOf( + level.getRecipeManager(), DryingRackRecipe.class)) { + DryingRackRecipe source = holder.value(); + Ingredient ingredient = source.getIngredients().isEmpty() + ? Ingredient.EMPTY : source.getIngredients().get(0); + if (!ingredient.isEmpty() + && AdapterUtils.matchesRequired(mergedInputs, + Map.of(ingredient, 1L))) { + matches.add(holder); + } + } + return List.copyOf(matches); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/youkaishomecoming/KettleRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/youkaishomecoming/KettleRecipeAdapter.java new file mode 100644 index 00000000..482c6dc2 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/youkaishomecoming/KettleRecipeAdapter.java @@ -0,0 +1,144 @@ +package com.sorrowmist.useless.content.recipe.adapters.delight.youkaishomecoming; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightRecipeAdapterUtils; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightSyntheticRecipe; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Lite Youkai's Homecoming kettle recipes. */ +public final class KettleRecipeAdapter implements IRecipeAdapter { + private static final String SOURCE_CLASS = + "dev.xkmc.youkaishomecoming.content.pot.kettle.KettleRecipe"; + private static final ResourceLocation KETTLE_ID = + new ResourceLocation("youkaisfeasts", "kettle"); + private static final long WATER_AMOUNT = 1_000L; + + @Override + public String sourceId() { + return RecipeSourceIds.YOUKAI_HOMECOMING; + } + + @Override + public Class getRecipeClass() { + return DelightSyntheticRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + Item item = DelightRecipeAdapterUtils.registeredItem(KETTLE_ID); + return item == null ? ItemStack.EMPTY : item.getDefaultInstance(); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null + || holder.value().convertedRecipe() == null) { + return List.of(); + } + return List.of(holder.value().convertedRecipe()); + } + + @Override + public List> getGeneratedRecipes(Level level) { + if (level == null) { + return List.of(); + } + + List> generated = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getRecipes(level.getRecipeManager())) { + if (holder.value() == null || !SOURCE_CLASS.equals(holder.value().getClass().getName())) { + continue; + } + AdvancedAlloyFurnaceRecipe converted = convertSource(holder.id(), holder.value()); + if (converted != null) { + generated.add(new RecipeHolder<>(converted.id(), new DelightSyntheticRecipe(converted))); + } + } + return List.copyOf(generated); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) { + return List.of(); + } + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : getGeneratedRecipes(level)) { + AdvancedAlloyFurnaceRecipe recipe = holder.value().convertedRecipe(); + if (recipe != null + && DelightRecipeAdapterUtils.matchesItems(recipe.inputs(), mergedInputs, List.of()) + && DelightRecipeAdapterUtils.matchesFluids(recipe.inputFluids(), mergedFluids) + && (!recipe.inputs().isEmpty() || !recipe.inputFluids().isEmpty())) { + matches.add(holder); + } + } + return List.copyOf(matches); + } + + @Nullable + private static AdvancedAlloyFurnaceRecipe convertSource(ResourceLocation sourceId, + Object source) { + @SuppressWarnings("unchecked") + List ingredients = DelightRecipeAdapterUtils.fieldValue( + source, "input", List.class); + FluidStack result = DelightRecipeAdapterUtils.fieldValue( + source, "result", FluidStack.class); + List inputs = AdapterUtils.mergeIngredients(ingredients); + if (result == null || result.isEmpty() || result.getAmount() <= 0) { + return null; + } + + ItemStack bottledOutput = YoukaiRecipeAdapterUtils.bottledOutput(result); + List outputs = bottledOutput == null + ? List.of() : List.of(bottledOutput); + List outputFluids = bottledOutput == null + ? List.of(result.copy()) : List.of(); + + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(sourceId), + inputs, + List.of(new LongSizedFluidIngredient( + FluidIngredient.tag(FluidTags.WATER), WATER_AMOUNT)), + List.of(), + outputs, + outputFluids, + List.of(), + AdapterUtils.DEFAULT_ENERGY, + Math.max(1, DelightRecipeAdapterUtils.intField( + source, "time", AdapterUtils.DEFAULT_PROCESS_TIME)), + Ingredient.EMPTY, + 0, + List.of(AdapterUtils.toMoldIngredient(moldStack())), + AlloyFurnaceMode.NORMAL + ); + } + + @Nullable + private static ItemStack moldStack() { + Item item = DelightRecipeAdapterUtils.registeredItem(KETTLE_ID); + return item == null ? null : item.getDefaultInstance(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/youkaishomecoming/PotCookingRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/youkaishomecoming/PotCookingRecipeAdapter.java new file mode 100644 index 00000000..a7056e3e --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/youkaishomecoming/PotCookingRecipeAdapter.java @@ -0,0 +1,131 @@ +package com.sorrowmist.useless.content.recipe.adapters.delight.youkaishomecoming; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightRecipeAdapterUtils; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightSyntheticRecipe; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Lite Youkai's Homecoming unordered cooking recipes. */ +public final class PotCookingRecipeAdapter implements IRecipeAdapter { + private static final String SOURCE_CLASS = + "dev.xkmc.youkaishomecoming.content.pot.cooking.core.UnorderedCookingRecipe"; + + @Override + public String sourceId() { + return RecipeSourceIds.YOUKAI_HOMECOMING; + } + + @Override + public Class getRecipeClass() { + return DelightSyntheticRecipe.class; + } + + /** The native cooking-pot recipe selects its station from the output remainder. */ + @Override + public ItemStack getMoldItem() { + return ItemStack.EMPTY; + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null + || holder.value().convertedRecipe() == null) { + return List.of(); + } + return List.of(holder.value().convertedRecipe()); + } + + @Override + public List> getGeneratedRecipes(Level level) { + if (level == null) { + return List.of(); + } + + List> generated = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getRecipes(level.getRecipeManager())) { + if (holder.value() == null || !SOURCE_CLASS.equals(holder.value().getClass().getName())) { + continue; + } + AdvancedAlloyFurnaceRecipe converted = convertSource(holder.id(), holder.value()); + if (converted != null) { + generated.add(new RecipeHolder<>(converted.id(), new DelightSyntheticRecipe(converted))); + } + } + return List.copyOf(generated); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || mold == null || mold.isEmpty()) { + return List.of(); + } + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : getGeneratedRecipes(level)) { + AdvancedAlloyFurnaceRecipe recipe = holder.value().convertedRecipe(); + if (recipe == null || recipe.molds().isEmpty() + || !AdapterUtils.matchesMold(recipe.mold(), mold)) { + continue; + } + if (DelightRecipeAdapterUtils.matchesItems(recipe.inputs(), mergedInputs, List.of()) + && DelightRecipeAdapterUtils.matchesFluids(recipe.inputFluids(), mergedFluids)) { + matches.add(holder); + } + } + return List.copyOf(matches); + } + + @Nullable + private static AdvancedAlloyFurnaceRecipe convertSource(ResourceLocation sourceId, + Object source) { + @SuppressWarnings("unchecked") + List ingredients = DelightRecipeAdapterUtils.fieldValue( + source, "input", List.class); + ItemStack result = DelightRecipeAdapterUtils.fieldValue( + source, "result", ItemStack.class); + if (result == null || result.isEmpty() || result.getCount() <= 0) { + return null; + } + + ItemStack mold = result.getCraftingRemainingItem(); + List inputs = AdapterUtils.mergeIngredients(ingredients); + if (inputs.isEmpty() || mold.isEmpty()) { + return null; + } + + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(sourceId), + inputs, + List.of(), + List.of(), + List.of(result.copy()), + List.of(), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + Math.max(1, DelightRecipeAdapterUtils.intField( + source, "time", AdapterUtils.DEFAULT_PROCESS_TIME)), + Ingredient.EMPTY, + 0, + List.of(AdapterUtils.toMoldIngredient(mold)), + AlloyFurnaceMode.NORMAL + ); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/youkaishomecoming/SimpleFermentationRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/youkaishomecoming/SimpleFermentationRecipeAdapter.java new file mode 100644 index 00000000..99739fa2 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/youkaishomecoming/SimpleFermentationRecipeAdapter.java @@ -0,0 +1,191 @@ +package com.sorrowmist.useless.content.recipe.adapters.delight.youkaishomecoming; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightRecipeAdapterUtils; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightSyntheticRecipe; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Lite Youkai's Homecoming simple fermentation recipes. */ +public final class SimpleFermentationRecipeAdapter + implements IRecipeAdapter { + private static final String SOURCE_CLASS = + "dev.xkmc.youkaishomecoming.content.pot.ferment.SimpleFermentationRecipe"; + private static final ResourceLocation FERMENTATION_TANK_ID = + new ResourceLocation("youkaisfeasts", "fermentation_tank"); + + @Override + public String sourceId() { + return RecipeSourceIds.YOUKAI_HOMECOMING; + } + + @Override + public Class getRecipeClass() { + return DelightSyntheticRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + Item item = DelightRecipeAdapterUtils.registeredItem(FERMENTATION_TANK_ID); + return item == null ? ItemStack.EMPTY : item.getDefaultInstance(); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null + || holder.value().convertedRecipe() == null) { + return List.of(); + } + return List.of(holder.value().convertedRecipe()); + } + + @Override + public List> getGeneratedRecipes(Level level) { + if (level == null) { + return List.of(); + } + + List> generated = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getRecipes(level.getRecipeManager())) { + if (holder.value() == null || !SOURCE_CLASS.equals(holder.value().getClass().getName())) { + continue; + } + AdvancedAlloyFurnaceRecipe converted = convertSource(holder.id(), holder.value()); + if (converted != null) { + generated.add(new RecipeHolder<>(converted.id(), new DelightSyntheticRecipe(converted))); + } + } + return List.copyOf(generated); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) { + return List.of(); + } + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : getGeneratedRecipes(level)) { + AdvancedAlloyFurnaceRecipe recipe = holder.value().convertedRecipe(); + if (recipe != null + && DelightRecipeAdapterUtils.matchesItems(recipe.inputs(), mergedInputs, List.of()) + && DelightRecipeAdapterUtils.matchesFluids(recipe.inputFluids(), mergedFluids) + && (!recipe.inputs().isEmpty() || !recipe.inputFluids().isEmpty())) { + matches.add(holder); + } + } + return List.copyOf(matches); + } + + @Nullable + private static AdvancedAlloyFurnaceRecipe convertSource(ResourceLocation sourceId, + Object source) { + @SuppressWarnings("unchecked") + List ingredients = DelightRecipeAdapterUtils.fieldValue( + source, "ingredients", List.class); + @SuppressWarnings("unchecked") + List results = DelightRecipeAdapterUtils.fieldValue( + source, "results", List.class); + FluidStack inputFluid = DelightRecipeAdapterUtils.fieldValue( + source, "inputFluid", FluidStack.class); + FluidStack outputFluid = DelightRecipeAdapterUtils.fieldValue( + source, "outputFluid", FluidStack.class); + + List itemInputs = new ArrayList<>(); + if (ingredients != null) { + itemInputs.addAll(ingredients); + } + List inputs = AdapterUtils.mergeIngredients(itemInputs); + List outputs = itemOutputs(results); + List inputFluids = fluidInputs(inputFluid); + List outputFluids = fluidOutputs(outputFluid); + + ItemStack bottledOutput = YoukaiRecipeAdapterUtils.bottledOutput(outputFluid); + if (bottledOutput != null) { + outputs = new ArrayList<>(outputs); + outputs.add(bottledOutput); + outputFluids = List.of(); + + Item container = YoukaiRecipeAdapterUtils.emptyContainer(outputFluid); + if (container != null) { + for (int i = 0; i < bottledOutput.getCount(); i++) { + itemInputs.add(Ingredient.of(container)); + } + inputs = AdapterUtils.mergeIngredients(itemInputs); + } + } + if ((inputs.isEmpty() && inputFluids.isEmpty()) + || (outputs.isEmpty() && outputFluids.isEmpty())) { + return null; + } + + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(sourceId), + inputs, + inputFluids, + List.of(), + outputs, + outputFluids, + List.of(), + AdapterUtils.DEFAULT_ENERGY, + Math.max(1, DelightRecipeAdapterUtils.intField( + source, "time", AdapterUtils.DEFAULT_PROCESS_TIME)), + Ingredient.EMPTY, + 0, + List.of(AdapterUtils.toMoldIngredient(moldStack())), + AlloyFurnaceMode.NORMAL + ); + } + + private static List itemOutputs(@Nullable List source) { + if (source == null || source.isEmpty()) { + return List.of(); + } + List result = new ArrayList<>(); + for (ItemStack stack : source) { + if (stack != null && !stack.isEmpty() && stack.getCount() > 0) { + result.add(stack.copy()); + } + } + return List.copyOf(result); + } + + private static List fluidInputs(@Nullable FluidStack inputFluid) { + if (inputFluid == null || inputFluid.isEmpty() || inputFluid.getAmount() <= 0) { + return List.of(); + } + return List.of(LongSizedFluidIngredient.from(inputFluid)); + } + + private static List fluidOutputs(@Nullable FluidStack outputFluid) { + if (outputFluid == null || outputFluid.isEmpty() || outputFluid.getAmount() <= 0) { + return List.of(); + } + return List.of(outputFluid.copy()); + } + + @Nullable + private static ItemStack moldStack() { + Item item = DelightRecipeAdapterUtils.registeredItem(FERMENTATION_TANK_ID); + return item == null ? null : item.getDefaultInstance(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/youkaishomecoming/SteamingRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/youkaishomecoming/SteamingRecipeAdapter.java new file mode 100644 index 00000000..3488d449 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/youkaishomecoming/SteamingRecipeAdapter.java @@ -0,0 +1,104 @@ +package com.sorrowmist.useless.content.recipe.adapters.delight.youkaishomecoming; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightRecipeAdapterUtils; +import dev.xkmc.youkaishomecoming.content.pot.steamer.SteamingRecipe; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Youkai's steamer recipes. */ +public final class SteamingRecipeAdapter implements IRecipeAdapter { + private static final ResourceLocation STEAMER_POT_ID = + new ResourceLocation("youkaisfeasts", "steamer_pot"); + + @Override + public String sourceId() { + return RecipeSourceIds.YOUKAI_HOMECOMING; + } + + @Override + public Class getRecipeClass() { + return SteamingRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + Item item = DelightRecipeAdapterUtils.registeredItem(STEAMER_POT_ID); + return item == null ? ItemStack.EMPTY : item.getDefaultInstance(); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + + SteamingRecipe source = holder.value(); + Ingredient input = source.getIngredients().isEmpty() + ? Ingredient.EMPTY : source.getIngredients().get(0); + ItemStack output = source.getResultItem(level == null ? null : level.registryAccess()); + if (input.isEmpty() || output == null || output.isEmpty() || output.getCount() <= 0) { + return List.of(); + } + + int processTime = source.getCookingTime() > 0 + ? source.getCookingTime() : AdapterUtils.DEFAULT_PROCESS_TIME; + long energy = Math.max(1L, + (long) processTime * AdapterUtils.DEFAULT_ENERGY + / AdapterUtils.DEFAULT_PROCESS_TIME); + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + List.of(new CountedIngredient(input, 1L)), + List.of(), + List.of(), + List.of(output.copy()), + List.of(), + List.of(), + energy, + processTime, + Ingredient.EMPTY, + 0, + List.of(AdapterUtils.toMoldIngredient(getMoldItem())), + AlloyFurnaceMode.NORMAL + )); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || mergedInputs == null || mergedInputs.isEmpty() + || !matchesMold(mold)) { + return List.of(); + } + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : DelightRecipeAdapterUtils.allOf( + level.getRecipeManager(), SteamingRecipe.class)) { + SteamingRecipe recipe = holder.value(); + Ingredient input = recipe.getIngredients().isEmpty() + ? Ingredient.EMPTY : recipe.getIngredients().get(0); + if (!input.isEmpty() && AdapterUtils.matchesRequired( + mergedInputs, Map.of(input, 1L))) { + matches.add(holder); + } + } + return List.copyOf(matches); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/youkaishomecoming/YoukaiRecipeAdapterUtils.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/youkaishomecoming/YoukaiRecipeAdapterUtils.java new file mode 100644 index 00000000..a2fec33c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/delight/youkaishomecoming/YoukaiRecipeAdapterUtils.java @@ -0,0 +1,68 @@ +package com.sorrowmist.useless.content.recipe.adapters.delight.youkaishomecoming; + +import dev.xkmc.youkaishomecoming.content.item.fluid.IYHFluidItem; +import dev.xkmc.youkaishomecoming.content.item.fluid.YHFluidHandler; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +/** Shared handling for Youkai's fluid recipes and their item containers. */ +final class YoukaiRecipeAdapterUtils { + private YoukaiRecipeAdapterUtils() { + } + + @Nullable + static IYHFluidItem fluidItem(@Nullable FluidStack fluid) { + if (fluid == null || fluid.isEmpty() || fluid.getAmount() <= 0) { + return null; + } + Object handler = YHFluidHandler.of(fluid); + return handler instanceof IYHFluidItem item ? item : null; + } + + @Nullable + static ItemStack bottledOutput(@Nullable FluidStack fluid) { + IYHFluidItem item = fluidItem(fluid); + if (item == null || fluid == null || item.amount() <= 0 + || fluid.getAmount() < item.amount()) { + return null; + } + + ItemStack output = item.toStack(fluid); + return output.isEmpty() || output.getCount() <= 0 ? null : output; + } + + @Nullable + static ItemStack bottledOutputForOperations(@Nullable FluidStack fluid, int operations) { + IYHFluidItem item = fluidItem(fluid); + if (item == null || fluid == null || operations <= 0 || item.amount() <= 0 + || (long) fluid.getAmount() * operations != item.amount()) { + return null; + } + + ItemStack output = item.asStack(1); + return output.isEmpty() || output.getCount() <= 0 ? null : output; + } + + /** Returns the number of source fluid operations needed for one full container. */ + static int operationsPerContainer(@Nullable FluidStack fluid) { + IYHFluidItem item = fluidItem(fluid); + if (item == null || fluid == null || fluid.getAmount() <= 0 || item.amount() <= 0 + || item.amount() % fluid.getAmount() != 0) { + return 0; + } + return item.amount() / fluid.getAmount(); + } + + @Nullable + static Item emptyContainer(@Nullable FluidStack fluid) { + IYHFluidItem item = fluidItem(fluid); + if (item == null) { + return null; + } + Item container = item.getContainer(); + return container == null || container == Items.AIR ? null : container; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/draconicevolution/DraconicFusionRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/draconicevolution/DraconicFusionRecipeAdapter.java new file mode 100644 index 00000000..e146eb67 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/draconicevolution/DraconicFusionRecipeAdapter.java @@ -0,0 +1,645 @@ +package com.sorrowmist.useless.content.recipe.adapters.draconicevolution; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import appeng.api.stacks.GenericStack; +import com.brandon3055.brandonscore.api.TechLevel; +import com.brandon3055.draconicevolution.DEConfig; +import com.brandon3055.draconicevolution.api.DraconicAPI; +import com.brandon3055.draconicevolution.api.crafting.IFusionDataTransfer; +import com.brandon3055.draconicevolution.api.crafting.IFusionInjector; +import com.brandon3055.draconicevolution.api.crafting.IFusionInventory; +import com.brandon3055.draconicevolution.api.crafting.IFusionRecipe; +import com.brandon3055.draconicevolution.init.DEContent; +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.PatternStackView; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.ItemIngredientAllocator; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import com.sorrowmist.useless.compat.forge1201.recipe.DataComponentIngredient; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +public final class DraconicFusionRecipeAdapter implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final int[] DEFAULT_CHARGE_TICKS = {300, 220, 140, 60}; + private static final int[] DEFAULT_CRAFT_TICKS = {300, 220, 140, 60}; + + @Override + public Class getRecipeClass() { + return IFusionRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return null; + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + return injectorTier(mold) >= 0; + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder == null) { + return List.of(); + } + + IFusionRecipe source = holder.value(); + ConvertedFusionData data = convertData(source, level); + if (data == null) { + RateLimitedLogger.warn(LOGGER, "Skipping unsupported Draconic Evolution fusion recipe: {}", holder.id()); + return List.of(); + } + + return List.of(createRecipe(holder, source, data)); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level, List actualInputs) { + if (holder == null) { + return List.of(); + } + + IFusionRecipe source = holder.value(); + ItemStack template = resultItem(source, level); + if (template.isEmpty()) { + RateLimitedLogger.warn(LOGGER, "Skipping unsupported Draconic Evolution fusion recipe: {}", holder.id()); + return List.of(); + } + if (!(template.getItem() instanceof IFusionDataTransfer)) { + return convertAll(holder, level); + } + + CatalystRequirement sourceCatalyst = catalystRequirement(source.getCatalyst()); + if (sourceCatalyst.ingredient().isEmpty()) { + return List.of(); + } + + List converted = new ArrayList<>(); + for (ItemStack catalyst : matchingCatalysts(source.getCatalyst(), actualInputs)) { + ItemStack assembled; + try { + FusionInventorySnapshot inventory = new FusionInventorySnapshot(catalyst, source.getRecipeTier()); + assembled = source.assemble(inventory, level == null ? null : level.registryAccess()).copy(); + } catch (RuntimeException exception) { + RateLimitedLogger.warn(LOGGER, "Failed to assemble dynamic Draconic Evolution fusion recipe {}", holder.id(), exception); + continue; + } + if (assembled.isEmpty()) { + continue; + } + + Ingredient exactCatalyst = DataComponentIngredient.of(true, catalyst.copyWithCount(1)); + ConvertedFusionData data = convertData(source, assembled, exactCatalyst, sourceCatalyst.count()); + if (data != null) { + converted.add(createRecipe(holder, source, data)); + } + } + + if (converted.isEmpty()) { + RateLimitedLogger.warn(LOGGER, "Skipping dynamic Draconic Evolution fusion recipe without a valid catalyst result: {}", + holder.id()); + } + return converted; + } + + /** + * Identifies component-carrying inputs and outputs in a plain JEI processing + * pattern. Canonical inputs connect a parent recipe to the static output key + * advertised by a dynamic Draconic Evolution child recipe. + */ + public static Optional findDynamicPatternProfile( + Level level, List patternInputs, List patternOutputs) { + return findDynamicPatternProfileLong(level, PatternStackView.fromLegacy(patternInputs, patternOutputs)); + } + + static Optional findDynamicPatternProfile( + Iterable> recipes, @Nullable Level level, + List patternInputs, List patternOutputs) { + return findDynamicPatternProfileLong( + recipes, level, PatternStackView.fromLegacy(patternInputs, patternOutputs)); + } + + public static Optional findDynamicPatternProfileLong( + Level level, PatternStackView pattern) { + if (level == null) { + return Optional.empty(); + } + return findDynamicPatternProfileLong( + com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(level.getRecipeManager(), DraconicAPI.FUSION_RECIPE_TYPE.get()), + level, pattern); + } + + static Optional findDynamicPatternProfileLong( + Iterable> recipes, @Nullable Level level, + PatternStackView pattern) { + if (recipes == null || pattern == null || pattern.inputs().isEmpty() + || pattern.outputs().isEmpty()) { + return Optional.empty(); + } + + List inputRepresentatives = pattern.inputRepresentatives(); + List outputRepresentatives = pattern.outputRepresentatives(); + if (inputRepresentatives.size() != pattern.inputs().size() + || outputRepresentatives.size() != pattern.outputs().size()) { + return Optional.empty(); + } + + GenericStack primaryPatternOutput = pattern.outputs().get(0); + ItemStack primaryOutputRepresentative = outputRepresentatives.get(0); + + List> recipeList = new ArrayList<>(); + for (RecipeHolder holder : recipes) { + if (holder == null || holder.value() == null) { + continue; + } + recipeList.add(holder); + } + + Map canonicalDynamicOutputs = canonicalDynamicOutputs(recipeList, level); + List matches = new ArrayList<>(); + for (RecipeHolder holder : recipeList) { + + IFusionRecipe source = holder.value(); + ItemStack staticResult = resultItem(source, level); + if (!matchesPrimaryPatternOutput(staticResult, primaryPatternOutput, primaryOutputRepresentative)) { + continue; + } + + ConvertedFusionData data = convertData(source, level); + if (data == null || totalRequiredItems(data.inputs()) != totalPatternItems(pattern.inputs()) + || !ItemIngredientAllocator.matches(data.inputs(), List.of(), pattern.inputs(), 1L)) { + continue; + } + + Map canonicalInputs = new LinkedHashMap<>(); + Set idOnlyInputs = new LinkedHashSet<>(); + boolean ambiguousDynamicInput = false; + for (int slot = 0; slot < pattern.inputs().size(); slot++) { + ItemStack input = inputRepresentatives.get(slot); + + ItemStack canonical = canonicalDynamicOutputs.get(input.getItem()); + if (canonical == null) { + continue; + } + if (hasDuplicateInputItem(inputRepresentatives, slot) + || !hasComponentAgnosticRequirement(data.inputs(), input)) { + ambiguousDynamicInput = true; + break; + } + + canonicalInputs.put(slot, canonical.copyWithCount(1)); + idOnlyInputs.add(slot); + } + if (ambiguousDynamicInput) { + continue; + } + + Set idOnlyOutputs = new LinkedHashSet<>(); + if (staticResult.getItem() instanceof IFusionDataTransfer) { + CatalystRequirement catalyst = catalystRequirement(source.getCatalyst()); + int catalystSlot = findUniqueCatalystSlot(pattern.inputs(), inputRepresentatives, catalyst); + if (catalystSlot < 0 || !catalyst.ingredient().isVanilla()) { + continue; + } + + idOnlyInputs.add(catalystSlot); + idOnlyOutputs.add(0); + } + + if (idOnlyInputs.isEmpty() && idOnlyOutputs.isEmpty()) { + continue; + } + + matches.add(new DynamicPatternProfile(canonicalInputs, idOnlyInputs, idOnlyOutputs)); + if (matches.size() > 1) { + return Optional.empty(); + } + } + + return matches.size() == 1 ? Optional.of(matches.get(0)) : Optional.empty(); + } + + private static Map canonicalDynamicOutputs( + List> recipes, @Nullable Level level) { + Map results = new LinkedHashMap<>(); + Set ambiguous = new LinkedHashSet<>(); + for (RecipeHolder holder : recipes) { + ItemStack result = resultItem(holder.value(), level); + if (result.isEmpty() || !(result.getItem() instanceof IFusionDataTransfer) + || ambiguous.contains(result.getItem())) { + continue; + } + + ItemStack normalized = result.copyWithCount(1); + ItemStack existing = results.get(result.getItem()); + if (existing == null) { + results.put(result.getItem(), normalized); + } else if (!ItemStack.isSameItemSameTags(existing, normalized)) { + results.remove(result.getItem()); + ambiguous.add(result.getItem()); + } + } + return results; + } + + private static boolean matchesPrimaryPatternOutput( + ItemStack recipeOutput, GenericStack patternOutput, ItemStack patternOutputRepresentative) { + if (recipeOutput.isEmpty() || patternOutput == null || patternOutput.amount() <= 0L + || patternOutputRepresentative == null || patternOutputRepresentative.isEmpty() + || recipeOutput.getCount() != patternOutput.amount() + || !recipeOutput.is(patternOutputRepresentative.getItem())) { + return false; + } + return recipeOutput.getItem() instanceof IFusionDataTransfer + || ItemStack.isSameItemSameTags( + recipeOutput.copyWithCount(1), patternOutputRepresentative.copyWithCount(1)); + } + + private static int findUniqueCatalystSlot( + List patternInputs, List inputRepresentatives, + CatalystRequirement catalyst) { + if (catalyst.ingredient().isEmpty() || catalyst.count() <= 0 + || inputRepresentatives.size() != patternInputs.size()) { + return -1; + } + + int catalystSlot = -1; + for (int slot = 0; slot < patternInputs.size(); slot++) { + GenericStack input = patternInputs.get(slot); + ItemStack representative = inputRepresentatives.get(slot); + if (input == null || input.amount() != catalyst.count() + || representative == null || representative.isEmpty() + || !catalyst.ingredient().test(representative)) { + continue; + } + if (catalystSlot >= 0) { + return -1; + } + catalystSlot = slot; + } + return catalystSlot >= 0 && !hasDuplicateInputItem(inputRepresentatives, catalystSlot) + ? catalystSlot + : -1; + } + + private static boolean hasComponentAgnosticRequirement( + List requirements, ItemStack input) { + boolean found = false; + for (CountedIngredient requirement : requirements) { + Ingredient ingredient = requirement.ingredient(); + if (ingredient == null || !ingredient.test(input)) { + continue; + } + if (!ingredient.isVanilla()) { + return false; + } + found = true; + } + return found; + } + + private static long totalRequiredItems(List inputs) { + long total = 0L; + for (CountedIngredient input : inputs) { + if (input != null && input.count() > 0) { + total = total > Long.MAX_VALUE - input.count() + ? Long.MAX_VALUE + : total + input.count(); + } + } + return total; + } + + private static long totalPatternItems(List inputs) { + long total = 0L; + for (GenericStack input : inputs) { + if (input != null && input.amount() > 0L) { + total = total > Long.MAX_VALUE - input.amount() + ? Long.MAX_VALUE + : total + input.amount(); + } + } + return total; + } + + private static boolean hasDuplicateInputItem(List inputs, int catalystSlot) { + ItemStack catalyst = inputs.get(catalystSlot); + for (int slot = 0; slot < inputs.size(); slot++) { + if (slot != catalystSlot) { + ItemStack candidate = inputs.get(slot); + if (candidate != null && !candidate.isEmpty() && candidate.is(catalyst.getItem())) { + return true; + } + } + } + return false; + } + + private static AdvancedAlloyFurnaceRecipe createRecipe( + RecipeHolder holder, IFusionRecipe source, ConvertedFusionData data) { + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + data.inputs(), + List.of(), + data.outputs(), + List.of(), + source.getEnergyCost(), + processTime(source.getRecipeTier()), + Ingredient.EMPTY, + 0, + injectorMold(source.getRecipeTier()), + AlloyFurnaceMode.NORMAL + ); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + @Nullable ItemStack mold) { + int moldTier = injectorTier(mold); + if (level == null || moldTier < 0 || mergedInputs.isEmpty()) { + return List.of(); + } + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(level.getRecipeManager(), + DraconicAPI.FUSION_RECIPE_TYPE.get())) { + IFusionRecipe recipe = holder.value(); + if (moldTier < tierIndex(recipe.getRecipeTier())) { + continue; + } + ConvertedFusionData data = convertData(recipe, level); + if (data != null && AdapterUtils.matchesRequired(mergedInputs, data.requirements())) { + matches.add(holder); + } + } + return matches; + } + + @Nullable + private static ConvertedFusionData convertData(IFusionRecipe source, @Nullable Level level) { + if (source == null || source.getEnergyCost() < 0L || source.getCatalyst().isEmpty()) { + return null; + } + + CatalystRequirement catalyst = catalystRequirement(source.getCatalyst()); + ItemStack result = resultItem(source, level); + return convertData(source, result, catalyst.ingredient(), catalyst.count()); + } + + @Nullable + private static ConvertedFusionData convertData( + IFusionRecipe source, ItemStack result, Ingredient catalystRequirement, long catalystAmount) { + if (source == null || source.getEnergyCost() < 0L || catalystRequirement == null + || catalystRequirement.isEmpty() || catalystAmount <= 0L) { + return null; + } + if (result.isEmpty()) { + return null; + } + + Map requirements = new LinkedHashMap<>(); + AdapterUtils.mergeIngredient(requirements, catalystRequirement, catalystAmount); + + List outputs = new ArrayList<>(); + mergeOutput(outputs, result); + for (IFusionRecipe.IFusionIngredient fusionIngredient : source.fusionIngredients()) { + Ingredient ingredient = fusionIngredient.get(); + if (ingredient == null || ingredient.isEmpty()) { + return null; + } + AdapterUtils.mergeIngredient(requirements, ingredient, 1L); + if (!fusionIngredient.consume()) { + ItemStack returned = singleReturnStack(ingredient); + if (returned.isEmpty()) { + return null; + } + mergeOutput(outputs, returned); + } + } + + List inputs = requirements.entrySet().stream() + .map(entry -> new CountedIngredient(entry.getKey(), entry.getValue())) + .toList(); + return new ConvertedFusionData(inputs, outputs, requirements); + } + + private static ItemStack resultItem(IFusionRecipe source, @Nullable Level level) { + if (source == null) { + return ItemStack.EMPTY; + } + return source.getResultItem(level == null ? null : level.registryAccess()).copy(); + } + + private static List matchingCatalysts(Ingredient catalyst, List actualInputs) { + if (catalyst == null || catalyst.isEmpty() || actualInputs == null || actualInputs.isEmpty()) { + return List.of(); + } + + List matches = new ArrayList<>(); + for (ItemStack stack : actualInputs) { + if (stack == null || stack.isEmpty() || !catalyst.test(stack)) { + continue; + } + boolean duplicate = matches.stream() + .anyMatch(existing -> ItemStack.isSameItemSameTags(existing, stack)); + if (!duplicate) { + matches.add(stack.copy()); + } + } + return matches; + } + + private static CatalystRequirement catalystRequirement(Ingredient catalyst) { + if (catalyst == null || catalyst.isEmpty()) { + return new CatalystRequirement(Ingredient.EMPTY, 0L); + } + return new CatalystRequirement(catalyst, 1L); + } + + private static ItemStack singleReturnStack(Ingredient ingredient) { + ItemStack[] candidates = ingredient.getItems(); + if (candidates.length == 0) { + return ItemStack.EMPTY; + } + + ItemStack first = candidates[0].copyWithCount(1); + for (int i = 1; i < candidates.length; i++) { + if (!ItemStack.isSameItemSameTags(first, candidates[i])) { + return ItemStack.EMPTY; + } + } + return first; + } + + private static void mergeOutput(List outputs, ItemStack output) { + for (ItemStack existing : outputs) { + if (ItemStack.isSameItemSameTags(existing, output)) { + existing.grow(output.getCount()); + return; + } + } + outputs.add(output.copy()); + } + + private static Ingredient injectorMold(TechLevel minimumTier) { + int minimum = tierIndex(minimumTier); + List injectors = new ArrayList<>(); + if (minimum <= TechLevel.DRACONIUM.index) { + injectors.add(new ItemStack(DEContent.BASIC_CRAFTING_INJECTOR.get())); + } + if (minimum <= TechLevel.WYVERN.index) { + injectors.add(new ItemStack(DEContent.WYVERN_CRAFTING_INJECTOR.get())); + } + if (minimum <= TechLevel.DRACONIC.index) { + injectors.add(new ItemStack(DEContent.AWAKENED_CRAFTING_INJECTOR.get())); + } + injectors.add(new ItemStack(DEContent.CHAOTIC_CRAFTING_INJECTOR.get())); + return Ingredient.of(injectors.stream()); + } + + private static int injectorTier(@Nullable ItemStack mold) { + if (mold == null || mold.isEmpty()) { + return -1; + } + if (mold.is(DEContent.BASIC_CRAFTING_INJECTOR.get().asItem())) { + return TechLevel.DRACONIUM.index; + } + if (mold.is(DEContent.WYVERN_CRAFTING_INJECTOR.get().asItem())) { + return TechLevel.WYVERN.index; + } + if (mold.is(DEContent.AWAKENED_CRAFTING_INJECTOR.get().asItem())) { + return TechLevel.DRACONIC.index; + } + if (mold.is(DEContent.CHAOTIC_CRAFTING_INJECTOR.get().asItem())) { + return TechLevel.CHAOTIC.index; + } + return -1; + } + + private static int processTime(TechLevel tier) { + int index = tierIndex(tier); + long charge = configTicks(DEConfig.fusionChargeTime, DEFAULT_CHARGE_TICKS, index); + long craft = configTicks(DEConfig.fusionCraftTime, DEFAULT_CRAFT_TICKS, index); + return (int) Math.min(Integer.MAX_VALUE, Math.max(1L, charge + craft)); + } + + private static int configTicks(@Nullable List configured, int[] defaults, int index) { + if (configured != null && index < configured.size() && configured.get(index) != null) { + return Math.max(1, configured.get(index)); + } + return defaults[index]; + } + + private static int tierIndex(@Nullable TechLevel tier) { + return tier == null ? TechLevel.DRACONIUM.index + : Math.max(TechLevel.DRACONIUM.index, Math.min(TechLevel.CHAOTIC.index, tier.index)); + } + + private static final class FusionInventorySnapshot implements IFusionInventory { + private ItemStack catalyst; + private ItemStack output = ItemStack.EMPTY; + private final TechLevel minimumTier; + + private FusionInventorySnapshot(ItemStack catalyst, @Nullable TechLevel minimumTier) { + this.catalyst = catalyst.copy(); + this.minimumTier = minimumTier == null ? TechLevel.DRACONIUM : minimumTier; + } + + @Override + public ItemStack getCatalystStack() { + return catalyst; + } + + @Override + public ItemStack getOutputStack() { + return output; + } + + @Override + public void setCatalystStack(ItemStack stack) { + catalyst = stack.copy(); + } + + @Override + public void setOutputStack(ItemStack stack) { + output = stack.copy(); + } + + @Override + public List getInjectors() { + return List.of(); + } + + @Override + public TechLevel getMinimumTier() { + return minimumTier; + } + + @Override + public ItemStack getItem(int index) { + return index == 0 ? catalyst : ItemStack.EMPTY; + } + + @Override + public int getContainerSize() { + return 1; + } + } + + private record ConvertedFusionData( + List inputs, + List outputs, + Map requirements) { + } + + private record CatalystRequirement(Ingredient ingredient, long count) { + } + + public record DynamicPatternProfile( + Map canonicalInputs, + Set idOnlyInputSlots, + Set idOnlyOutputSlots) { + public DynamicPatternProfile { + Map inputCopies = new LinkedHashMap<>(); + for (Map.Entry entry : canonicalInputs.entrySet()) { + if (entry.getKey() == null || entry.getKey() < 0 + || entry.getValue() == null || entry.getValue().isEmpty()) { + throw new IllegalArgumentException("Canonical pattern inputs must be non-empty and non-negative"); + } + inputCopies.put(entry.getKey(), entry.getValue().copyWithCount(1)); + } + canonicalInputs = Collections.unmodifiableMap(inputCopies); + idOnlyInputSlots = Set.copyOf(idOnlyInputSlots); + idOnlyOutputSlots = Set.copyOf(idOnlyOutputSlots); + if (idOnlyInputSlots.stream().anyMatch(slot -> slot == null || slot < 0) + || idOnlyOutputSlots.stream().anyMatch(slot -> slot == null || slot < 0) + || !idOnlyInputSlots.containsAll(canonicalInputs.keySet())) { + throw new IllegalArgumentException("Dynamic pattern slots must be non-negative and consistent"); + } + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/eco/IntegratedWorkingStationRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/eco/IntegratedWorkingStationRecipeAdapter.java new file mode 100644 index 00000000..46085e75 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/eco/IntegratedWorkingStationRecipeAdapter.java @@ -0,0 +1,192 @@ +package com.sorrowmist.useless.content.recipe.adapters.eco; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import cn.dancingsnow.neoecoae.all.NERecipeTypes; +import cn.dancingsnow.neoecoae.recipe.IntegratedWorkingStationRecipe; +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.common.crafting.SizedIngredient; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Converts ECO Integrated Working Station recipes to alloy-furnace recipes. */ +public final class IntegratedWorkingStationRecipeAdapter + implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final ResourceLocation INTEGRATED_WORKING_STATION = + new ResourceLocation("neoecoae", "integrated_working_station"); + + @Override + public Class getRecipeClass() { + return IntegratedWorkingStationRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(BuiltInRegistries.ITEM.get(INTEGRATED_WORKING_STATION)); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + + Converted converted = convertData(holder.value()); + if (converted == null) { + RateLimitedLogger.warn(LOGGER, "Skipping invalid ECO Integrated Working Station recipe: {}", holder.id()); + return List.of(); + } + + return List.of(createRecipe(AdapterUtils.convertedId(holder.id()), converted)); + } + + @Override + public List> findMatchingRecipes( + Level level, + Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || mergedInputs == null || mergedFluids == null || !matchesMold(mold)) { + return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + NERecipeTypes.INTEGRATED_WORKING_STATION.get())) { + Converted converted = convertData(holder.value()); + if (converted != null + && AdapterUtils.matchesRequired(mergedInputs, converted.itemRequirements()) + && matchesFluidRequirement(mergedFluids, converted.inputFluids())) { + matches.add(holder); + } + } + return matches; + } + + private AdvancedAlloyFurnaceRecipe createRecipe( + ResourceLocation id, Converted converted) { + return new AdvancedAlloyFurnaceRecipe( + id, + converted.itemInputs(), + converted.inputFluids(), + List.of(), + copyItems(converted.itemOutputs()), + copyFluids(converted.fluidOutputs()), + List.of(), + converted.energy(), + AdapterUtils.DEFAULT_PROCESS_TIME, + Ingredient.EMPTY, + 0, + List.of(AdapterUtils.toMoldIngredient(getMoldItem())), + AlloyFurnaceMode.NORMAL + ); + } + + @Nullable + private static Converted convertData(@Nullable IntegratedWorkingStationRecipe source) { + if (source == null || source.energy() < 0) { + return null; + } + + List sourceInputs = source.inputItems(); + if (sourceInputs == null || sourceInputs.size() > 9) { + return null; + } + + Map requirements = new LinkedHashMap<>(); + for (SizedIngredient input : sourceInputs) { + if (input == null || input.count() < 0 || input.ingredient() == null) { + return null; + } + if (input.count() == 0) { + continue; + } + if (input.ingredient().isEmpty()) { + return null; + } + AdapterUtils.mergeIngredient(requirements, input.ingredient(), input.count()); + } + + SizedFluidIngredient inputFluid = source.inputFluid(); + if (inputFluid == null || inputFluid.ingredient() == null) { + return null; + } + List inputFluids = List.of(); + if (!inputFluid.ingredient().isEmpty()) { + if (inputFluid.amount() <= 0) { + return null; + } + inputFluids = List.of(inputFluid); + } + + ItemStack itemOutput = source.itemOutput(); + FluidStack fluidOutput = source.fluidOutput(); + List itemOutputs = new ArrayList<>(); + if (itemOutput != null && !itemOutput.isEmpty() && itemOutput.getCount() > 0) { + itemOutputs.add(itemOutput.copy()); + } + List fluidOutputs = new ArrayList<>(); + if (fluidOutput != null && !fluidOutput.isEmpty() && fluidOutput.getAmount() > 0) { + fluidOutputs.add(fluidOutput.copy()); + } + + if ((requirements.isEmpty() && inputFluids.isEmpty()) + || (itemOutputs.isEmpty() && fluidOutputs.isEmpty())) { + return null; + } + + List itemInputs = requirements.entrySet().stream() + .map(entry -> new CountedIngredient(entry.getKey(), entry.getValue())) + .toList(); + return new Converted(itemInputs, requirements, inputFluids, itemOutputs, fluidOutputs, + source.energy()); + } + + private static boolean matchesFluidRequirement( + Map mergedFluids, List required) { + if (required.isEmpty()) { + return true; + } + return com.sorrowmist.useless.content.recipe.FluidIngredientAllocator.matches( + required, mergedFluids, 1L); + } + + private static List copyItems(List stacks) { + return stacks.stream().map(ItemStack::copy).toList(); + } + + private static List copyFluids(List stacks) { + return stacks.stream().map(FluidStack::copy).toList(); + } + + private record Converted( + List itemInputs, + Map itemRequirements, + List inputFluids, + List itemOutputs, + List fluidOutputs, + long energy) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/AlloySmeltingRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/AlloySmeltingRecipeAdapter.java new file mode 100644 index 00000000..7132fce0 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/AlloySmeltingRecipeAdapter.java @@ -0,0 +1,82 @@ +package com.sorrowmist.useless.content.recipe.adapters.enderio; + +import com.sorrowmist.useless.utils.RateLimitedLogger; +import com.enderio.machines.common.recipe.AlloySmeltingRecipe; +import com.enderio.machines.common.init.MachineBlocks; +import com.enderio.machines.common.init.MachineRecipes; +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Ender IO alloy-smelter recipes to the alloy furnace. */ +public final class AlloySmeltingRecipeAdapter implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + + @Override + public Class getRecipeClass() { + return AlloySmeltingRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(MachineBlocks.ALLOY_SMELTER.get()); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + AlloySmeltingRecipe source = holder.value(); + List inputs = EnderIOAdapterUtils.counted(source.getInputs()); + List outputs = EnderIOAdapterUtils.itemOutputs(source.getResultStacks( + level == null ? net.minecraft.core.RegistryAccess.EMPTY : level.registryAccess())); + if (inputs == null || inputs.isEmpty() || outputs == null || outputs.isEmpty() || source.getBaseEnergyCost() < 0) { + RateLimitedLogger.warn(LOGGER, "Skipping invalid Ender IO alloy-smelting recipe: {}", holder.id()); + return List.of(); + } + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), inputs, List.of(), outputs, List.of(), + source.getBaseEnergyCost(), AdapterUtils.DEFAULT_PROCESS_TIME, Ingredient.EMPTY, 0, + AdapterUtils.toMoldIngredient(getMoldItem()), AlloyFurnaceMode.NORMAL)); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || mergedInputs == null || mergedInputs.isEmpty() || !matchesMold(mold)) { + return List.of(); + } + List> matches = new ArrayList<>(); + RecipeManager manager = level.getRecipeManager(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(manager, + MachineRecipes.ALLOY_SMELTING.type().get())) { + AlloySmeltingRecipe source = holder.value(); + List inputs = EnderIOAdapterUtils.counted(source.getInputs()); + List outputs = EnderIOAdapterUtils.itemOutputs(source.getResultStacks( + level == null ? net.minecraft.core.RegistryAccess.EMPTY : level.registryAccess())); + if (inputs != null && outputs != null && !outputs.isEmpty() + && AdapterUtils.matchesRequired(mergedInputs, EnderIOAdapterUtils.requirements(inputs))) { + matches.add(holder); + } + } + return matches; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/EnchanterRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/EnchanterRecipeAdapter.java new file mode 100644 index 00000000..7f894d90 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/EnchanterRecipeAdapter.java @@ -0,0 +1,110 @@ +package com.sorrowmist.useless.content.recipe.adapters.enderio; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.enderio.machines.common.recipe.EnchanterRecipe; +import com.enderio.machines.common.init.MachineBlocks; +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.common.Tags; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Ender IO enchanter recipes to the alloy furnace. */ +public final class EnchanterRecipeAdapter implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + + @Override + public Class getRecipeClass() { + return EnchanterRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(MachineBlocks.ENCHANTER.get()); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + EnchanterRecipe source = holder.value(); + CountedIngredient catalyst = EnderIOAdapterUtils.counted(source.getInput()); + if (catalyst == null || source.getEnchantment() == null) { + RateLimitedLogger.warn(LOGGER, "Skipping invalid Ender IO enchanter recipe: {}", holder.id()); + return List.of(); + } + List result = new ArrayList<>(); + for (int enchantmentLevel = 1; enchantmentLevel <= source.getEnchantment().getMaxLevel(); enchantmentLevel++) { + long catalystCount; + try { catalystCount = Math.multiplyExact(catalyst.count(), (long)enchantmentLevel); } + catch (ArithmeticException overflow) { continue; } + int lapis = source.getLapisForLevel(enchantmentLevel); + int xpLevels = source.getXPCostForLevel(enchantmentLevel); + if (lapis <= 0 || xpLevels < 0) continue; + FluidStack experience = xpLevels == 0 ? FluidStack.EMPTY : experienceFluid(xpLevels); + if (experience == null || (xpLevels > 0 && experience.isEmpty())) continue; + ItemStack output = source.getBookForLevel(enchantmentLevel); + if (output == null || output.isEmpty()) continue; + var id = AdapterUtils.convertedId(holder.id()); + if (enchantmentLevel > 1) id = new net.minecraft.resources.ResourceLocation( + id.getNamespace(), id.getPath() + "_level_" + enchantmentLevel); + result.add(new AdvancedAlloyFurnaceRecipe( + id, + List.of(new CountedIngredient(Ingredient.of(Items.WRITABLE_BOOK), 1), + new CountedIngredient(catalyst.ingredient(), catalystCount), + new CountedIngredient(Ingredient.of(Tags.Items.GEMS_LAPIS), lapis)), + experience.isEmpty() ? List.of() : List.of( + com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient.of(experience)), + List.of(output.copy()), List.of(), + 0, AdapterUtils.DEFAULT_PROCESS_TIME, Ingredient.EMPTY, 0, + AdapterUtils.toMoldIngredient(getMoldItem()), AlloyFurnaceMode.NORMAL)); + } + return List.copyOf(result); + } + + /** Uses the installed Ender IO XP-level curve and fluid conversion configuration. */ + FluidStack experienceFluid(int levels) { + return EnderIOAdapterUtils.experienceFluid(levels); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || mergedInputs == null || mergedInputs.isEmpty() || !matchesMold(mold)) { + return List.of(); + } + List> matches = new ArrayList<>(); + RecipeManager manager = level.getRecipeManager(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(manager, + com.enderio.machines.common.init.MachineRecipes.ENCHANTING.type().get())) { + for (var converted : convertAll(holder, level)) { + if (AdapterUtils.matchesRequired(mergedInputs, EnderIOAdapterUtils.requirements(converted.inputs())) + && com.sorrowmist.useless.content.recipe.FluidIngredientAllocator.matchesLong( + converted.inputFluids(), mergedFluids, 1L)) { + matches.add(holder); + break; + } + } + } + return matches; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/EnderIOAdapterUtils.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/EnderIOAdapterUtils.java new file mode 100644 index 00000000..cbf836b8 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/EnderIOAdapterUtils.java @@ -0,0 +1,142 @@ +package com.sorrowmist.useless.content.recipe.adapters.enderio; + +import com.enderio.base.common.util.ExperienceUtil; +import com.enderio.base.common.init.EIOFluids; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.FluidIngredientAllocator; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; + +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import net.minecraft.world.level.material.FlowingFluid; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Small validation and conversion helpers shared by the Ender IO adapters. */ +final class EnderIOAdapterUtils { + private EnderIOAdapterUtils() { + } + + @Nullable + static CountedIngredient counted(@Nullable com.enderio.core.common.recipes.CountedIngredient ingredient) { + if (ingredient == null || ingredient.count() <= 0 || ingredient.ingredient() == null + || ingredient.ingredient().isEmpty()) { + return null; + } + return new CountedIngredient(ingredient.ingredient(), ingredient.count()); + } + + @Nullable + static List counted(@Nullable List ingredients) { + if (ingredients == null || ingredients.isEmpty()) { + return null; + } + Map requirements = new LinkedHashMap<>(); + for (com.enderio.core.common.recipes.CountedIngredient sized : ingredients) { + CountedIngredient counted = counted(sized); + if (counted == null) { + return null; + } + AdapterUtils.mergeIngredient(requirements, counted.ingredient(), counted.count()); + } + return requirements.entrySet().stream() + .map(entry -> new CountedIngredient(entry.getKey(), entry.getValue())) + .toList(); + } + + static Map requirements(List inputs) { + Map result = new LinkedHashMap<>(); + if (inputs != null) { + for (CountedIngredient input : inputs) { + if (input != null && input.ingredient() != null && input.count() > 0) { + AdapterUtils.mergeIngredient(result, input.ingredient(), input.count()); + } + } + } + return result; + } + + @Nullable + static List fluidChoices(@Nullable SizedFluidIngredient ingredient) { + if (ingredient == null || ingredient.amount() <= 0 || ingredient.ingredient().isEmpty()) { + return null; + } + Map unique = new LinkedHashMap<>(); + for (FluidStack candidate : ingredient.getFluids()) { + if (candidate == null || candidate.isEmpty()) { + continue; + } + var fluid = candidate.getFluid(); + if (fluid instanceof FlowingFluid flowing) { + fluid = flowing.getSource(); + } + FluidStack normalized = new FluidStack(fluid, ingredient.amount()); + if (candidate.hasTag()) normalized.setTag(candidate.getTag().copy()); + unique.putIfAbsent(fluidKey(normalized), normalized); + } + return unique.isEmpty() ? null : List.copyOf(unique.values()); + } + + static boolean matchesFluid( + Map available, SizedFluidIngredient required) { + return required != null && required.amount() > 0 + && FluidIngredientAllocator.matches(List.of(required), available, 1L); + } + + @Nullable + static FluidStack experienceFluid(int levels) { + if (levels <= 0) { + return null; + } + long amount = ExperienceUtil.getFluidFromLevel(levels); + if (amount <= 0 || amount > Integer.MAX_VALUE) { + return null; + } + return new FluidStack(EIOFluids.XP_JUICE.getSource(), (int) amount); + } + + static String fluidKey(FluidStack stack) { + return stack.getFluid().builtInRegistryHolder().key().location() + + "|" + com.sorrowmist.useless.compat.forge1201.recipe.NbtCanonical.snbt(stack.getTag()); + } + + @Nullable + static List itemOutputs(List results) { + if (results == null || results.isEmpty()) return null; + List items = new ArrayList<>(); + for (var result : results) { + if (result == null || result.isEmpty() || !result.isItem()) return null; + ItemStack item = result.getItem(); + if (item == null || item.isEmpty() || item.getCount() <= 0) return null; + items.add(item.copy()); + } + return List.copyOf(items); + } + + static boolean sameItemStack(ItemStack left, ItemStack right) { + return left != null && right != null && !left.isEmpty() && !right.isEmpty() + && ItemStack.isSameItemSameTags(left, right); + } + + static List distinctStacks(@Nullable List stacks) { + List result = new ArrayList<>(); + if (stacks == null) { + return result; + } + for (ItemStack stack : stacks) { + if (stack == null || stack.isEmpty()) { + continue; + } + if (result.stream().noneMatch(existing -> sameItemStack(existing, stack))) { + result.add(stack.copy()); + } + } + return result; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/FireCraftingRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/FireCraftingRecipeAdapter.java new file mode 100644 index 00000000..2e205e5f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/FireCraftingRecipeAdapter.java @@ -0,0 +1,265 @@ +package com.sorrowmist.useless.content.recipe.adapters.enderio; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.enderio.base.common.recipe.FireCraftingRecipe; +import com.enderio.base.common.init.EIORecipes; +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.ExpectedOutputScaler; +import com.sorrowmist.useless.content.recipe.FluidIngredientAllocator; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.TreeMap; + +/** + * 把 Ender IO 的火焰合成(fire_crafting)配方转换到合金炉。 + * + *

火焰合成原本是「在世界上对基方块点火」,没有物品输入,因此这里改用一桶水 + * ({@value #WATER_AMOUNT} mB)作为输入,模具固定为打火石。基方块与产物一律从 + * {@link RecipeManager} 动态读取,不在代码里硬编码任何具体配方。

+ * + *

原配方的每条产物各拆成一条独立配方,避免「无限粉」与「可疑的种子」混在同一条 + * 配方里:无限粉按需求固定折算为单个产出,其余产物按概率期望折算成最小确定性批次。

+ * + *

转换过程只依赖配方自身的静态数据(基方块列表、产物概率),不会查询世界、世界种子或 + * 玩家位置,保证服务端与客户端生成完全一致的配方集合。

+ */ +public final class FireCraftingRecipeAdapter implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + + /** 所有火焰合成配方统一消耗一桶水。 */ + private static final int WATER_AMOUNT = 1000; + + /** 需要固定折算为单个产出的物品:Ender IO 的无限粉。 */ + private static final ResourceLocation GRAINS_OF_INFINITY = + new ResourceLocation("enderio", "grains_of_infinity"); + + @Override + public Class getRecipeClass() { + return FireCraftingRecipe.class; + } + + /** 火焰合成的模具固定为打火石。 */ + @Override + public ItemStack getMoldItem() { + return new ItemStack(Items.FLINT_AND_STEEL); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + FireCraftingRecipe source = holder.value(); + List bases = sortedBases(source); + if (bases.isEmpty()) { + return List.of(); + } + + Ingredient mold = AdapterUtils.toMoldIngredient(getMoldItem()); + List recipes = new ArrayList<>(); + for (Block base : bases) { + for (NativeFireLootResults.Result result : NativeFireLootResults.read(source, level)) { + AdvancedAlloyFurnaceRecipe recipe = convertResult(holder.id(), base, result, mold); + if (recipe != null) { + recipes.add(recipe); + } + } + } + return recipes; + } + + /** + * 把单条产物转换成一个合金炉配方。 + * + *

每条产物独立成配方,因此同一个基方块下的无限粉与可疑的种子互不混合。

+ */ + @Nullable + private static AdvancedAlloyFurnaceRecipe convertResult( + ResourceLocation sourceId, Block base, + NativeFireLootResults.Result result, Ingredient mold) { + List weighted = weightedOutput(result); + if (weighted == null || weighted.isEmpty()) { + return null; + } + + // 概率产物折算成最小的确定性批次:operations 次操作恰好产出期望数量的物品。 + Optional scaled = ExpectedOutputScaler.scale(weighted); + if (scaled.isEmpty() || scaled.get().outputs().isEmpty()) { + RateLimitedLogger.warn(LOGGER, "Skipping unsupported Ender IO fire crafting result in recipe: {}", sourceId); + return null; + } + + int operations = scaled.get().operations(); + OptionalInt energy = ExpectedOutputScaler.multiplyToInt(AdapterUtils.DEFAULT_ENERGY, operations); + OptionalInt processTime = ExpectedOutputScaler.multiplyToInt(AdapterUtils.DEFAULT_PROCESS_TIME, operations); + if (energy.isEmpty() || processTime.isEmpty()) { + RateLimitedLogger.warn(LOGGER, "Skipping overflowing Ender IO fire crafting recipe: {}", sourceId); + return null; + } + + return new AdvancedAlloyFurnaceRecipe( + variantId(sourceId, base, scaled.get().outputs()), + List.of(), + waterRequirement(), + List.of(), + copyOutputs(scaled.get().outputs()), + List.of(), + List.of(), + energy.getAsInt(), + processTime.getAsInt(), + Ingredient.EMPTY, + 0, + List.of(mold), + AlloyFurnaceMode.NORMAL); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) { + return List.of(); + } + + // 输入只有水,不再依赖任何物品输入。 + Map safeFluids = mergedFluids == null ? Map.of() : mergedFluids; + if (!FluidIngredientAllocator.matchesLong(waterRequirement(), safeFluids, 1L)) { + return List.of(); + } + + List> matches = new ArrayList<>(); + RecipeManager manager = level.getRecipeManager(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(manager, EIORecipes.FIRE_CRAFTING.type().get())) { + FireCraftingRecipe source = holder.value(); + if (source == null || sortedBases(source).isEmpty() || NativeFireLootResults.read(source, level).isEmpty()) { + continue; + } + matches.add(holder); + } + return matches; + } + + /** 统一的水输入需求。 */ + private static List waterRequirement() { + return List.of(new LongSizedFluidIngredient(FluidIngredient.single(Fluids.WATER), WATER_AMOUNT)); + } + + /** + * 把单条产物整理成期望值折算器的输入。 + * + *

无限粉固定折算为单个产出(1 次操作恰好 1 个),其余产物保留原有的 + * 概率与数量区间,由折算器换算成等期望的确定性批次。

+ */ + @Nullable + private static List weightedOutput( + NativeFireLootResults.Result result) { + if (result == null) { + return null; + } + ItemStack stack = result.result(); + if (stack == null || stack.isEmpty() || stack.getCount() <= 0) { + return null; + } + if (result.minCount() < 0 || result.maxCount() < result.minCount()) { + return null; + } + double chance = result.chance(); + if (!Double.isFinite(chance)) { + return null; + } + + if (isGrainsOfInfinity(stack)) { + return List.of(new ExpectedOutputScaler.WeightedItemOutput( + stack.copyWithCount(1), 1, 1, 1.0)); + } + return List.of(new ExpectedOutputScaler.WeightedItemOutput( + stack.copy(), result.minCount(), result.maxCount(), clampChance(chance))); + } + + private static boolean isGrainsOfInfinity(ItemStack stack) { + return GRAINS_OF_INFINITY.equals(BuiltInRegistries.ITEM.getKey(stack.getItem())); + } + + /** + * 按方块注册名排序去重后返回基方块。 + * + *

基方块列表可能包含标签,标签展开顺序不稳定;排序后既保证产物输出顺序确定, + * 也让下面基于方块 id 的配方 id 与实际遍历顺序无关。

+ */ + private static List sortedBases(FireCraftingRecipe source) { + Map unique = new TreeMap<>(); + for (Block block : BuiltInRegistries.BLOCK) { + if (!source.isBaseValid(block)) continue; + if (block == null) { + continue; + } + ResourceLocation id = BuiltInRegistries.BLOCK.getKey(block); + if (id == null) { + continue; + } + unique.putIfAbsent(id, block); + } + return List.copyOf(unique.values()); + } + + /** + * 生成与内容绑定的稳定配方 id:原配方 id + 基方块注册名 + 产物注册名。 + * + *

id 只能由配方内容决定,不能掺入集合迭代顺序或身份哈希,否则重启后同一配方会换 id, + * 已编码的样板会失配。产物后缀用于区分同一个基方块下拆分出来的多条配方。

+ */ + private static ResourceLocation variantId( + ResourceLocation sourceId, Block base, List outputs) { + StringBuilder path = new StringBuilder(sourceId.getPath()) + .append("_on_") + .append(registrySuffix(BuiltInRegistries.BLOCK.getKey(base))); + for (ItemStack output : outputs) { + path.append("_to_").append(registrySuffix(BuiltInRegistries.ITEM.getKey(output.getItem()))); + } + return new ResourceLocation(sourceId.getNamespace(), path.toString()); + } + + private static String registrySuffix(@Nullable ResourceLocation id) { + return id == null + ? "unknown" + : id.getNamespace() + "_" + id.getPath().replace('/', '_'); + } + + /** 每条配方持有独立的产物副本,避免多条配方共享同一个 ItemStack 实例。 */ + private static List copyOutputs(List outputs) { + List copies = new ArrayList<>(outputs.size()); + for (ItemStack output : outputs) { + copies.add(output.copy()); + } + return copies; + } + + private static double clampChance(double chance) { + return Math.max(0.0, Math.min(1.0, chance)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/NativeFireLootResults.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/NativeFireLootResults.java new file mode 100644 index 00000000..a1effb67 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/NativeFireLootResults.java @@ -0,0 +1,82 @@ +package com.sorrowmist.useless.content.recipe.adapters.enderio; + +import com.enderio.base.common.recipe.FireCraftingRecipe; +import com.google.gson.*; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.nbt.TagParser; +import net.minecraftforge.server.ServerLifecycleHooks; +import java.util.*; + +/** Native EnderIO fire recipes reference server-data loot tables instead of inline results. */ +final class NativeFireLootResults { + record Result(ItemStack result,int minCount,int maxCount,double chance) {} + static List read(FireCraftingRecipe recipe,Level level) { + var server=level==null?null:level.getServer(); + if(server==null) server=ServerLifecycleHooks.getCurrentServer(); + if(server==null) return List.of(); + ResourceLocation id=recipe.getLootTable(); + var resource=server.getResourceManager().getResource(new ResourceLocation(id.getNamespace(),"loot_tables/"+id.getPath()+".json")); + if(resource.isEmpty()) return List.of(); + try(var reader=resource.get().openAsReader()) { + return parse(JsonParser.parseReader(reader).getAsJsonObject(),recipe.getMaxItemDrops()); + } catch(Exception unsupported) { throw new IllegalArgumentException("Cannot convert EnderIO fire loot "+id,unsupported); } + } + static List parse(JsonObject table,int maximum) throws Exception { + if(table.has("functions")) throw new IllegalArgumentException("Table functions require runtime loot context"); + var results=new ArrayList(); + long maximumTotal=0; + for(var poolElement:table.getAsJsonArray("pools")) { + var pool=poolElement.getAsJsonObject(); + if(pool.has("functions")) throw new IllegalArgumentException("Pool functions require runtime loot context"); + // Multiple weighted entries with independent conditions need a joint probability model. + var entries=pool.getAsJsonArray("entries"); + if(entries.size()!=1) throw new IllegalArgumentException("Unsupported conditional weighted fire pool"); + double chance=conditions(pool); + if(pool.has("bonus_rolls") && pool.get("bonus_rolls").getAsDouble()!=0) throw new IllegalArgumentException("Luck-dependent fire pool"); + double rolls=pool.get("rolls").getAsDouble(); + if(rolls!=1) throw new IllegalArgumentException("Unsupported fire pool rolls: "+rolls); + var entry=entries.get(0).getAsJsonObject(); chance*=conditions(entry); + if(!"minecraft:item".equals(entry.get("type").getAsString())) throw new IllegalArgumentException("Non-item fire entry"); + var id=new ResourceLocation(entry.get("name").getAsString()); + var item=BuiltInRegistries.ITEM.getOptional(id).orElseThrow(()->new IllegalArgumentException("Unknown fire output: "+id)); + var stack=new ItemStack(item); int min=1,max=1; + if(entry.has("functions")) for(var element:entry.getAsJsonArray("functions")) { + var function=element.getAsJsonObject(); + if(function.has("conditions")) throw new IllegalArgumentException("Conditional fire function"); + switch(function.get("function").getAsString()) { + case "minecraft:set_count" -> { + if(function.has("add") && function.get("add").getAsBoolean()) throw new IllegalArgumentException("Additive count"); + var count=function.get("count"); + if(count.isJsonPrimitive()) min=max=count.getAsInt(); + else { + var range=count.getAsJsonObject(); + if(range.has("type") && !range.get("type").getAsString().equals("minecraft:uniform")) throw new IllegalArgumentException("Context fire count"); + min=range.get("min").getAsInt();max=range.get("max").getAsInt(); + } + } + case "minecraft:set_nbt" -> stack.setTag(TagParser.parseTag(function.get("tag").getAsString())); + default -> throw new IllegalArgumentException("Unknown fire loot function"); + } + } + if(min<0 || maxmaximum || !Double.isFinite(chance) || chance<0 || chance>1) throw new IllegalArgumentException("Invalid or capped fire loot quantity"); + maximumTotal+=max; + if(maximumTotal>maximum) throw new IllegalArgumentException("Combined fire pools exceed native drop cap"); + results.add(new Result(stack,min,max,chance)); + } + return List.copyOf(results); + } + private static double conditions(JsonObject object) { + double chance=1; + if(object.has("conditions")) for(var element:object.getAsJsonArray("conditions")) { + var condition=element.getAsJsonObject(); + if(!"minecraft:random_chance".equals(condition.get("condition").getAsString())) throw new IllegalArgumentException("Fire condition requires runtime context"); + double p=condition.get("chance").getAsDouble(); + if(!Double.isFinite(p) || p<0 || p>1) throw new IllegalArgumentException("Invalid fire probability"); + chance*=p; + } + return chance; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/SagMillingRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/SagMillingRecipeAdapter.java new file mode 100644 index 00000000..232b731d --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/SagMillingRecipeAdapter.java @@ -0,0 +1,298 @@ +package com.sorrowmist.useless.content.recipe.adapters.enderio; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.enderio.api.grindingball.IGrindingBallData; +import com.enderio.machines.common.recipe.SagMillingRecipe; +import com.enderio.machines.common.init.MachineBlocks; +import com.enderio.machines.common.init.MachineRecipes; +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.ExpectedOutputScaler; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; + +/** Converts Ender IO SAG Mill recipes into deterministic expected-output batches. */ +public final class SagMillingRecipeAdapter implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final double EPSILON = 1.0E-9; + + @Override + public Class getRecipeClass() { + return SagMillingRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(MachineBlocks.SAG_MILL.get()); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + + SagMillingRecipe source = holder.value(); + List result = new ArrayList<>(); + AdvancedAlloyFurnaceRecipe base = convertRecipe( + AdapterUtils.convertedId(holder.id()), + source, + GrindingBallMultipliers.IDENTITY, + 1.0, + List.of(AdapterUtils.toMoldIngredient(getMoldItem()))); + if (base != null) { + result.add(base); + } + + for (Map.Entry entry : grindingBallData(level).entrySet()) { + Item ball = entry.getKey(); + IGrindingBallData data = entry.getValue(); + ResourceLocation ballId = BuiltInRegistries.ITEM.getKey(ball); + if (ballId == null || data == null) { + continue; + } + AdvancedAlloyFurnaceRecipe variant = convertRecipe( + grindingBallVariantId(holder.id(), ballId), + source, + new GrindingBallMultipliers(data.getOutputMultiplier(), data.getBonusMultiplier()), + data.getPowerUse(), + List.of( + AdapterUtils.toMoldIngredient(getMoldItem()), + AdapterUtils.toMoldIngredient(new ItemStack(ball)))); + if (variant != null) { + result.add(variant); + } + } + return List.copyOf(result); + } + + /** Package-private seam used by tests to provide deterministic data-map values. */ + static List convertAll( + RecipeHolder holder, Level level, + GrindingBallMultipliers grindingBallMultipliers) { + if (holder == null || holder.value() == null) { + return List.of(); + } + + AdvancedAlloyFurnaceRecipe converted = convertRecipe( + AdapterUtils.convertedId(holder.id()), holder.value(), grindingBallMultipliers, + 1.0, List.of(AdapterUtils.toMoldIngredient(new ItemStack(MachineBlocks.SAG_MILL.get())))); + return converted == null ? List.of() : List.of(converted); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || mergedInputs == null || mergedInputs.isEmpty() + || !matchesMold(mold)) { + return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + MachineRecipes.SAG_MILLING.type().get())) { + SagMillingRecipe source = holder.value(); + AdvancedAlloyFurnaceRecipe converted = convertRecipe( + AdapterUtils.convertedId(holder.id()), source, + GrindingBallMultipliers.IDENTITY, 1.0, + List.of(AdapterUtils.toMoldIngredient(new ItemStack(MachineBlocks.SAG_MILL.get())))); + if (converted == null || converted.inputs().isEmpty()) { + continue; + } + + CountedIngredient input = converted.inputs().get(0); + if (AdapterUtils.hasMatchingIngredient(mergedInputs, input.ingredient(), input.count())) { + matches.add(holder); + } + } + return matches; + } + + @Nullable + static AdvancedAlloyFurnaceRecipe convertRecipe( + ResourceLocation convertedId, + SagMillingRecipe source, + @Nullable GrindingBallMultipliers grindingBallMultipliers, + double powerUse, + List molds) { + if (source == null || source.getInput() == null || source.getInput().isEmpty() + || source.getBaseEnergyCost() < 0 || !isFiniteNonNegative(powerUse) + || molds == null || molds.isEmpty()) { + return null; + } + + GrindingBallMultipliers multipliers = grindingBallMultipliers == null + ? GrindingBallMultipliers.IDENTITY + : grindingBallMultipliers; + Optional scaled = scaleOutputs(source, multipliers); + if (scaled.isEmpty() || scaled.get().outputs().isEmpty()) { + RateLimitedLogger.warn(LOGGER, "Skipping unsupported Ender IO SAG Mill recipe: {}", convertedId); + return null; + } + + int operations = scaled.get().operations(); + OptionalInt energy = scaledEnergy(source.getBaseEnergyCost(), powerUse, operations); + OptionalInt processTime = ExpectedOutputScaler.multiplyToInt( + AdapterUtils.DEFAULT_PROCESS_TIME, operations); + if (energy.isEmpty() || processTime.isEmpty()) { + RateLimitedLogger.warn(LOGGER, "Skipping overflowing Ender IO SAG Mill recipe: {}", convertedId); + return null; + } + + return new AdvancedAlloyFurnaceRecipe( + convertedId, + List.of(new CountedIngredient(source.getInput(), operations)), + List.of(), + List.of(), + scaled.get().outputs(), + List.of(), + List.of(), + energy.getAsInt(), + processTime.getAsInt(), + Ingredient.EMPTY, + 0, + molds, + AlloyFurnaceMode.NORMAL + ); + } + + private static Optional scaleOutputs( + SagMillingRecipe source, GrindingBallMultipliers grindingBallMultipliers) { + List weightedOutputs = new ArrayList<>(); + boolean applyOutputMultiplier = source.getBonusType().canMultiply(); + boolean applyBonusMultiplier = source.getBonusType().doChance(); + double outputMultiplier = applyOutputMultiplier + ? grindingBallMultipliers.outputMultiplier() + : 1.0; + double bonusMultiplier = applyBonusMultiplier + ? grindingBallMultipliers.bonusMultiplier() + : 1.0; + + if (!isFiniteNonNegative(outputMultiplier) || !isFiniteNonNegative(bonusMultiplier) + || outputMultiplier > Integer.MAX_VALUE) { + return Optional.empty(); + } + + for (SagMillingRecipe.OutputItem output : source.getOutputs()) { + if (output == null || !output.isPresent()) { + continue; + } + ItemStack stack = output.getItemStack(); + if (stack == null || stack.isEmpty() || stack.getCount() <= 0) { + continue; + } + + double chance = output.chance(); + if (!Double.isFinite(chance)) { + return Optional.empty(); + } + if (applyBonusMultiplier) { + chance *= bonusMultiplier; + } + if (!Double.isFinite(chance)) { + return Optional.empty(); + } + chance = clampChance(chance); + + int wholeRolls = (int) Math.floor(outputMultiplier); + double fractionalRoll = outputMultiplier - wholeRolls; + if (wholeRolls > 0) { + long count = (long) stack.getCount() * wholeRolls; + if (count > Integer.MAX_VALUE) { + return Optional.empty(); + } + weightedOutputs.add(new ExpectedOutputScaler.WeightedItemOutput( + stack.copyWithCount((int) count), (int) count, (int) count, chance)); + } + + if (fractionalRoll > EPSILON) { + double fractionalChance = chance * fractionalRoll; + if (!Double.isFinite(fractionalChance)) { + return Optional.empty(); + } + weightedOutputs.add(new ExpectedOutputScaler.WeightedItemOutput( + stack.copy(), stack.getCount(), stack.getCount(), fractionalChance)); + } + } + + return ExpectedOutputScaler.scale(weightedOutputs); + } + + private static Map grindingBallData(@Nullable Level level) { + if (level == null) return Map.of(); + Map result = new LinkedHashMap<>(); + // Read the actual level recipe manager instead of 1.21 data maps or a global cache. + // This respects KubeJS/datapack additions, removals and resource reloads. + for (var recipe : level.getRecipeManager().getAllRecipesFor( + com.enderio.base.common.init.EIORecipes.GRINDING_BALL.type().get())) { + if (recipe.getItem() != null && recipe.getDurability() > 0) { + result.put(recipe.getItem(), recipe); + } + } + return result; + } + + private static OptionalInt scaledEnergy(int baseEnergy, double powerUse, int operations) { + if (baseEnergy < 0 || !isFiniteNonNegative(powerUse) || operations < 0) { + return OptionalInt.empty(); + } + // Native Ender IO rounds the float product down before multiplying the batch. + double scaled = (float) baseEnergy * (float) powerUse; + if (!Double.isFinite(scaled) || scaled > Integer.MAX_VALUE) { + return OptionalInt.empty(); + } + long perOperation = (long) scaled; + return ExpectedOutputScaler.multiplyToInt(perOperation, operations); + } + + private static ResourceLocation grindingBallVariantId( + ResourceLocation sourceId, ResourceLocation ballId) { + String suffix = ballId.getNamespace() + "_" + ballId.getPath().replace('/', '_'); + return new ResourceLocation( + sourceId.getNamespace(), + AdapterUtils.convertedId(sourceId).getPath() + "_with_grinding_ball_" + suffix); + } + + private static double clampChance(double chance) { + return Math.max(0.0, Math.min(1.0, chance)); + } + + private static boolean isFiniteNonNegative(double value) { + return Double.isFinite(value) && value >= 0.0; + } + + record GrindingBallMultipliers(double outputMultiplier, double bonusMultiplier) { + private static final GrindingBallMultipliers IDENTITY = new GrindingBallMultipliers(1.0, 1.0); + + GrindingBallMultipliers { + if (!isFiniteNonNegative(outputMultiplier) || !isFiniteNonNegative(bonusMultiplier)) { + throw new IllegalArgumentException("Grinding ball multipliers must be finite and non-negative"); + } + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/SlicingRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/SlicingRecipeAdapter.java new file mode 100644 index 00000000..21d83546 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/SlicingRecipeAdapter.java @@ -0,0 +1,95 @@ +package com.sorrowmist.useless.content.recipe.adapters.enderio; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.enderio.machines.common.recipe.SlicingRecipe; +import com.enderio.machines.common.init.MachineBlocks; +import com.enderio.machines.common.init.MachineRecipes; +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Ender IO Slice and Splice recipes to the alloy furnace. */ +public final class SlicingRecipeAdapter implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + + @Override + public Class getRecipeClass() { + return SlicingRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(MachineBlocks.SLICE_AND_SPLICE.get()); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + SlicingRecipe source = holder.value(); + List inputs = counted(source.getInputs()); + List outputs = EnderIOAdapterUtils.itemOutputs(source.getResultStacks( + level == null ? net.minecraft.core.RegistryAccess.EMPTY : level.registryAccess())); + if (inputs == null || inputs.isEmpty() || outputs == null || outputs.isEmpty() || source.getBaseEnergyCost() < 0) { + RateLimitedLogger.warn(LOGGER, "Skipping invalid Ender IO slicing recipe: {}", holder.id()); + return List.of(); + } + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), inputs, List.of(), outputs, List.of(), + source.getBaseEnergyCost(), AdapterUtils.DEFAULT_PROCESS_TIME, Ingredient.EMPTY, 0, + AdapterUtils.toMoldIngredient(getMoldItem()), AlloyFurnaceMode.NORMAL)); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || mergedInputs == null || mergedInputs.isEmpty() || !matchesMold(mold)) { + return List.of(); + } + List> matches = new ArrayList<>(); + RecipeManager manager = level.getRecipeManager(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(manager, MachineRecipes.SLICING.type().get())) { + SlicingRecipe source = holder.value(); + List inputs = counted(source.getInputs()); + if (inputs != null && EnderIOAdapterUtils.itemOutputs(source.getResultStacks(level.registryAccess())) != null + && AdapterUtils.matchesRequired(mergedInputs, EnderIOAdapterUtils.requirements(inputs))) { + matches.add(holder); + } + } + return matches; + } + + private static List counted(List source) { + if (source == null || source.isEmpty()) { + return null; + } + return source.stream() + .filter(ingredient -> ingredient != null && !ingredient.isEmpty()) + .collect(java.util.stream.Collectors.groupingBy( + ingredient -> ingredient, + java.util.LinkedHashMap::new, + java.util.stream.Collectors.counting())) + .entrySet().stream() + .map(entry -> new CountedIngredient(entry.getKey(), entry.getValue())) + .toList(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/SoulBindingRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/SoulBindingRecipeAdapter.java new file mode 100644 index 00000000..3d37c119 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/SoulBindingRecipeAdapter.java @@ -0,0 +1,748 @@ +package com.sorrowmist.useless.content.recipe.adapters.enderio; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.GenericStack; +import com.enderio.core.common.recipes.OutputStack; +import com.sorrowmist.useless.compat.forge1201.enderio.NativeSoul; +import com.sorrowmist.useless.compat.forge1201.enderio.NativeSoulStorage; +import com.sorrowmist.useless.compat.forge1201.enderio.FilledSoulIngredient; +import com.enderio.machines.common.recipe.SoulBindingRecipe; +import com.enderio.base.common.item.tool.SoulVialItem; +import com.enderio.machines.common.souldata.SoulDataReloadListener; +import com.enderio.machines.common.init.MachineBlocks; +import com.enderio.base.common.init.EIOItems; +import com.enderio.machines.common.init.MachineRecipes; +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.DynamicComponentPatternDetails; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.PatternStackView; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.ItemIngredientAllocator; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import com.sorrowmist.useless.compat.forge1201.recipe.DataComponentIngredient; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.HexFormat; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** Converts Ender IO soul-binding recipes to the alloy furnace. */ +public final class SoulBindingRecipeAdapter implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + + @Override + public Class getRecipeClass() { + return SoulBindingRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(MachineBlocks.SOUL_BINDER.get()); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + + SoulBindingRecipe source = holder.value(); + FluidStack xp = EnderIOAdapterUtils.experienceFluid(source.getExpCost()); + if (!validSource(source) + || (source.getExpCost() > 0 && xp == null)) { + RateLimitedLogger.warn(LOGGER, "Skipping invalid Ender IO soul-binding recipe: {}", holder.id()); + return List.of(); + } + + List vials = displayVials(source); + if (vials.isEmpty()) { + return List.of(); + } + + if (hasDynamicOutput(source, vials)) { + List result = new ArrayList<>(); + for (ItemStack vial : vials) { + ItemStack output = boundDisplayOutput(displayOutput(source), vial); + if (output == null || output.isEmpty()) { + // Ender IO's JEI category omits a vial when the result cannot actually be + // bound to that soul. Never replace it with the unbound/default result: that + // is precisely the misleading powered-spawner display this adapter must avoid. + continue; + } + + List inputs = baseInputs(exact(vial), source.getInput()); + if (inputs == null) { + continue; + } + result.add(createRecipe(holder, source, inputs, + List.of(output, emptySoulVial()), + variantId(holder.id(), vial, null))); + } + return result; + } + + // If the output is independent of the soul, mirror SoulBindingCategory's single entry: + // show every allowed filled vial together and keep the original target Ingredient. The + // runtime overload below still validates the concrete vial and target with Ender IO. + Ingredient vialIngredient = allowedVialIngredient(source, vials); + List inputs = baseInputs(vialIngredient, source.getInput()); + if (inputs == null) { + return List.of(); + } + return List.of(createRecipe(holder, source, inputs, + List.of(displayOutput(source).copy(), emptySoulVial()), + staticVariantId(holder.id()))); + } + + /** + * A soul-binding result receives the soul from the vial at craft time. The catalogue contains + * exact component-aware vial variants when the result is dynamic; this overload still + * replaces the displayed result with the real output for the vial and target that are actually + * in the furnace. + */ + @Override + public List convertAll( + RecipeHolder holder, Level level, List actualInputs) { + if (holder == null || holder.value() == null || level == null) { + return convertAll(holder, level); + } + + SoulBindingRecipe source = holder.value(); + if (!validSource(source) || actualInputs == null || actualInputs.isEmpty()) { + return List.of(); + } + + List vials = filledVials(actualInputs); + List targets = matchingTargets(actualInputs, source.getInput()); + if (vials.isEmpty() || targets.isEmpty()) { + return List.of(); + } + + FluidStack xp = experienceInput(source.getExpCost()); + List result = new ArrayList<>(); + for (ItemStack vial : vials) { + for (ItemStack target : targets) { + SoulBindingRecipe.Container input = nativeInput( + vial.copyWithCount(1), target.copyWithCount(1), xp.copy()); + if (!matches(source, input, level, holder)) { + continue; + } + + ConvertedOutputs outputs = craft(source, input, level, holder); + if (outputs == null || outputs.items().isEmpty() && outputs.fluids().isEmpty()) { + continue; + } + + Ingredient vialIngredient = exact(vial); + // Ender IO 6 binds the soul but does not copy the target item NBT. + Ingredient targetIngredient = source.getInput(); + List inputs = baseInputs(vialIngredient, targetIngredient); + if (inputs == null) { + continue; + } + + result.add(createRecipe(holder, source, inputs, outputs.items(), outputs.fluids(), + variantId(holder.id(), vial, null))); + } + } + return result; + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || mergedInputs == null || mergedFluids == null || !matchesMold(mold)) { + return List.of(); + } + + List> result = new ArrayList<>(); + RecipeManager manager = level.getRecipeManager(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(manager, + MachineRecipes.SOUL_BINDING.type().get())) { + SoulBindingRecipe source = holder.value(); + List inputs = baseInputs(soulVialIngredient(), source.getInput()); + FluidStack xp = EnderIOAdapterUtils.experienceFluid(source.getExpCost()); + boolean fluidMatches = source.getExpCost() == 0 || xp != null + && com.sorrowmist.useless.content.recipe.FluidIngredientAllocator.matchesLong( + List.of(com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient.from(xp)), + mergedFluids, 1L); + if (inputs != null && fluidMatches + && AdapterUtils.matchesRequired(mergedInputs, EnderIOAdapterUtils.requirements(inputs))) { + result.add(holder); + } + } + return result; + } + + /** Uses Ender IO's own filled-storage predicate for unrestricted soul-vial inputs. */ + private static Ingredient soulVialIngredient() { + try { + return FilledSoulIngredient.of(EIOItems.FILLED_SOUL_VIAL.get()); + } catch (RuntimeException exception) { + // Keep catalogue construction resilient during very early optional-mod setup. The + // public filled-vial list is still a useful display fallback in that phase. + RateLimitedLogger.warn(LOGGER, "Failed to create Ender IO's filled soul-vial ingredient", exception); + return vialIngredient(SoulVialItem.getAllFilled()); + } + } + + /** + * Mirrors the vial list that Ender IO's SoulBindingCategory puts in its JEI input slot. + * Selector properties are mutually exclusive in the Ender IO serializer, so the first + * matching branch is also the category's display precedence. + */ + private static List displayVials(SoulBindingRecipe source) { + List candidates = new ArrayList<>(); + if (source.getEntityType() != null) { + addVial(candidates, NativeSoulStorage.forSoul(NativeSoul.of(source.getEntityType()))); + } else if (source.getMobCategory() != null) { + var category = source.getMobCategory(); + BuiltInRegistries.ENTITY_TYPE.stream() + .filter(entityType -> entityType.getCategory().equals(category)) + .map(NativeSoul::of) + .map(NativeSoulStorage::forSoul) + .forEach(vial -> addVial(candidates, vial)); + } else if (source.getSouldata() != null) { + try { + SoulDataReloadListener soulData = + SoulDataReloadListener.fromString(source.getSouldata()); + BuiltInRegistries.ENTITY_TYPE.stream() + .filter(entityType -> { + var id = BuiltInRegistries.ENTITY_TYPE.getKey(entityType); + return id != null && soulData.matches(id).isPresent(); + }) + .map(NativeSoul::of) + .map(NativeSoulStorage::forSoul) + .forEach(vial -> addVial(candidates, vial)); + } catch (RuntimeException exception) { + RateLimitedLogger.warn(LOGGER, "Failed to enumerate Ender IO soul-data vials for {}", + source.getSouldata(), exception); + } + } else { + List filled = SoulVialItem.getAllFilled(); + if (filled != null) { + filled.forEach(vial -> addVial(candidates, vial)); + } + } + return distinctFilledVials(candidates); + } + + private static void addVial(List target, @Nullable ItemStack vial) { + if (vial != null && !vial.isEmpty()) { + target.add(vial.copyWithCount(1)); + } + } + + private static List distinctFilledVials(@Nullable Iterable stacks) { + List result = new ArrayList<>(); + if (stacks == null) { + return result; + } + for (ItemStack stack : stacks) { + if (!isFilledVial(stack) + || result.stream().anyMatch(existing -> + ItemStack.isSameItemSameTags(existing, stack))) { + continue; + } + result.add(stack.copyWithCount(1)); + } + return result; + } + + private static Ingredient vialIngredient(@Nullable Iterable vials) { + List distinct = distinctFilledVials(vials); + if (distinct.isEmpty()) { + return Ingredient.EMPTY; + } + if (distinct.size() == 1) { + return exact(distinct.get(0)); + } + return SoulVialSetIngredient.of(distinct); + } + + private static Ingredient allowedVialIngredient( + SoulBindingRecipe source, List vials) { + // FilledSoulStorageIngredient is the only merged ingredient that preserves the important + // "must already contain a soul" predicate. For a recipe without a selector this is + // exactly Ender IO's complete candidate set; selector recipes normally take the exact + // per-vial branch above when their output is soul-dependent. + if (source.getEntityType() == null && source.getMobCategory() == null + && source.getSouldata() == null) { + return soulVialIngredient(); + } + return vialIngredient(vials); + } + + private static ItemStack emptySoulVial() { + return EIOItems.EMPTY_SOUL_VIAL.asStack(); + } + + private static boolean hasDynamicOutput(SoulBindingRecipe source, List vials) { + if (!NativeSoulStorage.canBindSoul(displayOutput(source))) { + return false; + } + for (ItemStack vial : vials) { + ItemStack candidate = boundDisplayOutput(displayOutput(source), vial); + if (candidate != null + && !ItemStack.isSameItemSameTags(candidate, displayOutput(source))) { + return true; + } + } + return false; + } + + /** Generates the same bound result that Ender IO's JEI category previews. */ + @Nullable + private static ItemStack boundDisplayOutput(ItemStack output, ItemStack vial) { + ItemStack result = output.copy(); + NativeSoul soul = NativeSoulStorage.getBoundSoul(vial); + if (soul != null && !soul.isEmpty() + && NativeSoulStorage.canBindSoul(result) + // SoulBindingCategory deliberately previews the entity type only. Captured + // entity NBT belongs to the runtime craft path, not to the static JEI variant. + && NativeSoulStorage.tryBindSoul(result, NativeSoul.of(soul.entityType()))) { + return result; + } + return null; + } + + /** + * Keeps the AE component-relaxed vial slot semantically equivalent to Ender IO's + * {@code SoulBindingRecipe.matches}: extra vial components are allowed, but an empty vial or + * a soul that does not satisfy the recipe selector is not. + */ + private static boolean matchesSoulVial(SoulBindingRecipe source, ItemStack vial) { + if (source == null || !isFilledVial(vial)) { + return false; + } + NativeSoul soul; + try { + soul = NativeSoulStorage.getBoundSoul(vial); + } catch (RuntimeException exception) { + return false; + } + if (soul == null || soul.isEmpty()) { + return false; + } + if (source.getSouldata() != null) { + try { + SoulDataReloadListener listener = SoulDataReloadListener.fromString(source.getSouldata()); + return listener != null && listener.matches(soul.entityTypeId()).isPresent(); + } catch (RuntimeException exception) { + return false; + } + } + if (source.getMobCategory() != null) { + return soul.entityType() != null && soul.entityType().getCategory().equals(source.getMobCategory()); + } + if (source.getEntityType() != null) { + return source.getEntityType().equals(soul.entityTypeId()); + } + return true; + } + + private static DynamicComponentPatternDetails.InputMatcher soulVialMatcher( + SoulBindingRecipe source) { + return input -> input instanceof AEItemKey itemKey + && matchesSoulVial(source, itemKey.toStack(1)); + } + + /** Used by the pattern resolver to mark the soul vial/output component slots as dynamic. */ + public static Optional findDynamicPatternProfile( + @Nullable Level level, List patternInputs, List patternOutputs) { + return findDynamicPatternProfileLong(level, PatternStackView.fromLegacy(patternInputs, patternOutputs)); + } + + public static Optional findDynamicPatternProfileLong( + @Nullable Level level, @Nullable PatternStackView pattern) { + return findDynamicPatternProfileLong("enderio", level, pattern); + } + + public static Optional findDynamicPatternProfile( + String sourceId, @Nullable Level level, + List patternInputs, List patternOutputs) { + return findDynamicPatternProfileLong(sourceId, level, + PatternStackView.fromLegacy(patternInputs, patternOutputs)); + } + + public static Optional findDynamicPatternProfileLong( + String sourceId, @Nullable Level level, @Nullable PatternStackView pattern) { + if (sourceId == null || !sourceId.equals("enderio")) return Optional.empty(); + if (level == null) { + return Optional.empty(); + } + return findDynamicPatternProfileLong( + com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(level.getRecipeManager(), MachineRecipes.SOUL_BINDING.type().get()), + level, pattern); + } + + static Optional findDynamicPatternProfile( + Iterable> recipes, @Nullable Level level, + List patternInputs, List patternOutputs) { + return findDynamicPatternProfileLong(recipes, level, + PatternStackView.fromLegacy(patternInputs, patternOutputs)); + } + + static Optional findDynamicPatternProfileLong( + Iterable> recipes, @Nullable Level level, + @Nullable PatternStackView pattern) { + if (recipes == null || pattern == null || pattern.inputs().isEmpty() + || pattern.outputs().isEmpty() || !validItemStacks(pattern)) { + return Optional.empty(); + } + + List matches = new ArrayList<>(); + for (RecipeHolder holder : recipes) { + SoulBindingRecipe source = holder == null ? null : holder.value(); + if (!validSource(source)) { + continue; + } + + List requirements = baseInputs(soulVialIngredient(), source.getInput()); + boolean dynamicOutput = hasDynamicOutput(source, displayVials(source)); + if (requirements == null || !matchesPatternInputs(requirements, pattern) + || !matchesStaticOutputs(source, pattern.outputs(), dynamicOutput)) { + continue; + } + + List patternInputs = pattern.inputRepresentatives(); + int vialSlot = uniqueFilledVialSlot(patternInputs); + int targetSlot = uniqueTargetSlot(patternInputs, source.getInput(), vialSlot); + if (vialSlot < 0 || targetSlot < 0) { + continue; + } + + SoulBindingRecipe.Container input = nativeInput( + patternInputs.get(vialSlot).copyWithCount(1), + patternInputs.get(targetSlot).copyWithCount(1), + experienceInput(source.getExpCost())); + if (!matches(source, input, level, holder)) { + continue; + } + + Set dynamicInputs = new LinkedHashSet<>(); + dynamicInputs.add(vialSlot); + matches.add(new DynamicPatternProfile( + dynamicInputs, + dynamicOutput ? Set.of(0) : Set.of(), + Map.of(vialSlot, soulVialMatcher(source)))); + if (matches.size() > 1) { + return Optional.empty(); + } + } + return matches.isEmpty() ? Optional.empty() : Optional.of(matches.get(0)); + } + + @Nullable + private static List baseInputs(Ingredient soul, Ingredient target) { + if (soul == null || soul.isEmpty() || target == null || target.isEmpty()) { + return null; + } + Map requirements = new LinkedHashMap<>(); + AdapterUtils.mergeIngredient(requirements, soul, 1L); + AdapterUtils.mergeIngredient(requirements, target, 1L); + return requirements.entrySet().stream() + .map(entry -> new CountedIngredient(entry.getKey(), entry.getValue())) + .toList(); + } + + private static AdvancedAlloyFurnaceRecipe createRecipe( + RecipeHolder holder, SoulBindingRecipe source, + List inputs, List outputs, + net.minecraft.resources.ResourceLocation id) { + return createRecipe(holder, source, inputs, outputs, List.of(), id); + } + + private static AdvancedAlloyFurnaceRecipe createRecipe( + RecipeHolder holder, SoulBindingRecipe source, + List inputs, List outputs, List outputFluids, + net.minecraft.resources.ResourceLocation id) { + FluidStack xp = EnderIOAdapterUtils.experienceFluid(source.getExpCost()); + return new AdvancedAlloyFurnaceRecipe( + id, inputs, xp == null ? List.of() : List.of(xp), outputs, outputFluids, + source.getBaseEnergyCost(), AdapterUtils.DEFAULT_PROCESS_TIME, Ingredient.EMPTY, 0, + AdapterUtils.toMoldIngredient(new ItemStack(MachineBlocks.SOUL_BINDER.get())), AlloyFurnaceMode.NORMAL); + } + + private static boolean validSource(@Nullable SoulBindingRecipe source) { + return source != null && source.getInput() != null && !source.getInput().isEmpty() + && displayOutput(source) != null && !displayOutput(source).isEmpty() + && displayOutput(source).getCount() > 0 && source.getBaseEnergyCost() >= 0 + && source.getExpCost() >= 0; + } + + private static FluidStack experienceInput(int levels) { + FluidStack xp = EnderIOAdapterUtils.experienceFluid(levels); + return xp == null ? FluidStack.EMPTY : xp; + } + + private static Ingredient exact(ItemStack stack) { + return DataComponentIngredient.of(true, stack.copyWithCount(1)); + } + + private static List filledVials(List stacks) { + return distinctFilledVials(stacks); + } + + private static List matchingTargets(List stacks, Ingredient target) { + List result = new ArrayList<>(); + if (stacks == null || target == null || target.isEmpty()) { + return result; + } + for (ItemStack stack : stacks) { + if (stack == null || stack.isEmpty() || !target.test(stack) + || result.stream().anyMatch(existing -> ItemStack.isSameItemSameTags(existing, stack))) { + continue; + } + result.add(stack.copyWithCount(1)); + } + return result; + } + + private static boolean isFilledVial(@Nullable ItemStack stack) { + return stack != null && !stack.isEmpty() && stack.is(EIOItems.FILLED_SOUL_VIAL.get()) + && !NativeSoulStorage.getBoundSoul(stack).isEmpty(); + } + + private static SoulBindingRecipe.Container nativeInput(ItemStack vial, ItemStack target, FluidStack xp) { + var slots = new net.minecraftforge.items.ItemStackHandler(2); + slots.setStackInSlot(0, vial.copy()); + slots.setStackInSlot(1, target.copy()); + return new SoulBindingRecipe.Container(slots, xp::getAmount); + } + + private static ItemStack displayOutput(SoulBindingRecipe source) { + var outputs = source.getResultStacks(net.minecraft.core.RegistryAccess.EMPTY); + return outputs == null || outputs.isEmpty() ? ItemStack.EMPTY : outputs.get(0).getItem(); + } + + private static boolean matches( + SoulBindingRecipe source, SoulBindingRecipe.Container input, Level level, + RecipeHolder holder) { + try { + return source.matches(input, level); + } catch (RuntimeException exception) { + RateLimitedLogger.warn(LOGGER, "Failed to evaluate Ender IO soul-binding recipe {}", holder.id(), exception); + return false; + } + } + + @Nullable + private static ConvertedOutputs craft( + SoulBindingRecipe source, SoulBindingRecipe.Container input, Level level, + RecipeHolder holder) { + try { + List crafted = source.craft(input, level.registryAccess()); + if (crafted == null || crafted.isEmpty()) { + return null; + } + + List items = new ArrayList<>(); + List fluids = new ArrayList<>(); + for (OutputStack output : crafted) { + if (output == null || output.isEmpty()) { + continue; + } + if (output.isItem()) { + ItemStack item = output.getItem(); + if (item != null && !item.isEmpty() && item.getCount() > 0) { + items.add(item.copy()); + } + } else if (output.isFluid()) { + FluidStack fluid = output.getFluid(); + if (fluid != null && !fluid.isEmpty() && fluid.getAmount() > 0) { + fluids.add(fluid.copy()); + } + } + } + return new ConvertedOutputs(List.copyOf(items), List.copyOf(fluids)); + } catch (RuntimeException exception) { + RateLimitedLogger.warn(LOGGER, "Failed to craft dynamic Ender IO soul-binding recipe {}", holder.id(), exception); + return null; + } + } + + private static boolean matchesPatternInputs( + List requirements, PatternStackView pattern) { + long required = totalRequiredItems(requirements); + long actual = totalPatternItems(pattern.inputs()); + return required == actual + && ItemIngredientAllocator.matches(requirements, List.of(), pattern.inputs(), 1L); + } + + private static long totalRequiredItems(List requirements) { + long result = 0L; + for (CountedIngredient requirement : requirements) { + if (requirement == null || requirement.count() <= 0L) { + continue; + } + if (result > Long.MAX_VALUE - requirement.count()) { + return Long.MAX_VALUE; + } + result += requirement.count(); + } + return result; + } + + private static long totalPatternItems(List inputs) { + long result = 0L; + for (GenericStack input : inputs) { + if (input == null || input.amount() <= 0L || !(input.what() instanceof AEItemKey)) { + continue; + } + if (result > Long.MAX_VALUE - input.amount()) { + return Long.MAX_VALUE; + } + result += input.amount(); + } + return result; + } + + private static boolean matchesStaticOutputs( + SoulBindingRecipe source, List patternOutputs, boolean dynamicOutput) { + List expected = staticOutputs(source); + if (patternOutputs.size() != expected.size()) { + return false; + } + for (int index = 0; index < expected.size(); index++) { + ItemStack expectedStack = expected.get(index); + GenericStack actual = patternOutputs.get(index); + if (actual == null || actual.amount() <= 0L || actual.amount() != expectedStack.getCount() + || !(actual.what() instanceof AEItemKey itemKey)) { + return false; + } + ItemStack actualStack = itemKey.toStack(1); + if (index == 0) { + // A soul-bound result is intentionally matched by item id only. Static outputs + // must remain component-exact so unrelated component variants cannot share a + // pattern profile. + if (dynamicOutput + ? !actualStack.is(expectedStack.getItem()) + : !ItemStack.isSameItemSameTags(expectedStack, actualStack)) { + return false; + } + } else if (!ItemStack.isSameItemSameTags(expectedStack, actualStack)) { + return false; + } + } + return true; + } + + private static boolean validItemStacks(PatternStackView pattern) { + return pattern.inputs().stream().allMatch(stack -> stack != null && stack.amount() > 0L + && stack.what() instanceof AEItemKey) + && pattern.outputs().stream().allMatch(stack -> stack != null && stack.amount() > 0L + && stack.what() instanceof AEItemKey); + } + + private static List staticOutputs(SoulBindingRecipe source) { + List result = new ArrayList<>(); + result.add(displayOutput(source).copy()); + result.add(EIOItems.EMPTY_SOUL_VIAL.asStack()); + return result; + } + + private static int uniqueFilledVialSlot(List inputs) { + int result = -1; + for (int slot = 0; slot < inputs.size(); slot++) { + if (!isFilledVial(inputs.get(slot))) { + continue; + } + if (result >= 0) { + return -1; + } + result = slot; + } + return result; + } + + private static int uniqueTargetSlot( + List inputs, Ingredient target, int vialSlot) { + int result = -1; + for (int slot = 0; slot < inputs.size(); slot++) { + if (slot == vialSlot || !target.test(inputs.get(slot))) { + continue; + } + if (result >= 0) { + return -1; + } + result = slot; + } + return result; + } + + private static net.minecraft.resources.ResourceLocation variantId( + net.minecraft.resources.ResourceLocation source, ItemStack vial, @Nullable ItemStack target) { + StringBuilder suffix = new StringBuilder("_converted_soul_"); + appendFingerprint(suffix, vial); + if (target != null) { + suffix.append("_target_"); + appendFingerprint(suffix, target); + } + return new net.minecraft.resources.ResourceLocation( + source.getNamespace(), source.getPath() + suffix); + } + + private static net.minecraft.resources.ResourceLocation staticVariantId( + net.minecraft.resources.ResourceLocation source) { + return new net.minecraft.resources.ResourceLocation( + source.getNamespace(), source.getPath() + "_converted_soul_static"); + } + + private static void appendFingerprint(StringBuilder result, ItemStack stack) { + net.minecraft.resources.ResourceLocation item = BuiltInRegistries.ITEM.getKey(stack.getItem()); + result.append(item == null ? "unknown" : item.getNamespace() + "_" + item.getPath().replace('/', '_')) + .append('_').append(componentFingerprint(stack)); + } + + private static String componentFingerprint(ItemStack stack) { + net.minecraft.resources.ResourceLocation item = BuiltInRegistries.ITEM.getKey(stack.getItem()); + String value = (item == null ? "unknown" : item.toString()) + "|" + com.sorrowmist.useless.compat.forge1201.recipe.NbtCanonical.snbt(stack.getTag()); + try { + return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256") + .digest(value.getBytes(StandardCharsets.UTF_8))); + } catch (NoSuchAlgorithmException exception) { + throw new IllegalStateException("SHA-256 is unavailable", exception); + } + } + + private record ConvertedOutputs(List items, List fluids) { + } + + public record DynamicPatternProfile( + Set idOnlyInputSlots, + Set idOnlyOutputSlots, + Map inputMatchers) { + public DynamicPatternProfile(Set idOnlyInputSlots, Set idOnlyOutputSlots) { + this(idOnlyInputSlots, idOnlyOutputSlots, Map.of()); + } + + public DynamicPatternProfile { + idOnlyInputSlots = Set.copyOf(new LinkedHashSet<>(idOnlyInputSlots)); + idOnlyOutputSlots = Set.copyOf(new LinkedHashSet<>(idOnlyOutputSlots)); + inputMatchers = inputMatchers == null ? Map.of() : Map.copyOf(inputMatchers); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/SoulVialRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/SoulVialRecipeAdapter.java new file mode 100644 index 00000000..ad657534 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/SoulVialRecipeAdapter.java @@ -0,0 +1,242 @@ +package com.sorrowmist.useless.content.recipe.adapters.enderio; + +import com.sorrowmist.useless.compat.forge1201.enderio.NativeSoul; +import com.sorrowmist.useless.compat.forge1201.enderio.NativeSoulStorage; +import com.enderio.machines.common.blockentity.task.SpawnerMachineTask.SpawnType; +import com.enderio.machines.common.souldata.SpawnerSoul; +import com.enderio.base.common.util.EntityCaptureUtils; +import com.enderio.machines.common.tag.MachineTags; +import com.enderio.machines.common.config.MachinesConfig; +import com.enderio.base.common.init.EIOItems; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.SpawnEggItem; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.Recipe; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.Container; +import net.minecraft.world.level.Level; +import com.sorrowmist.useless.compat.forge1201.recipe.DataComponentIngredient; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Generates Ender IO soul-vial capture recipes for the alloy furnace. */ +public final class SoulVialRecipeAdapter implements IRecipeAdapter { + private static final int INPUT_COUNT = 1; + + private final Ingredient emptySoulVial = DataComponentIngredient.of( + true, EIOItems.EMPTY_SOUL_VIAL.get().getDefaultInstance()); + + @Override + public Class getRecipeClass() { + return SoulVialDummyRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return null; + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + if (mold == null || mold.isEmpty() || !(mold.getItem() instanceof SpawnEggItem spawnEgg)) { + return false; + } + return isSupportedEntity(spawnEgg.getType(mold.getTag())); + } + + @Override + public List> getGeneratedRecipes(Level level) { + if (level == null) { + return List.of(); + } + + List> result = new ArrayList<>(); + for (ResourceLocation entityId : EntityCaptureUtils.getCapturableEntities()) { + if (!BuiltInRegistries.ENTITY_TYPE.containsKey(entityId)) continue; + EntityType entityType = BuiltInRegistries.ENTITY_TYPE.get(entityId); + if (!isSupportedEntity(entityType)) { + continue; + } + + SpawnEggItem spawnEgg = SpawnEggItem.byId(entityType); + int energy = energyCost(entityType); + if (spawnEgg == null || energy <= 0) { + continue; + } + + ItemStack mold = new ItemStack(spawnEgg); + result.add(createHolder(entityType, mold, energy)); + } + return List.copyOf(result); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null || holder.value().convertedRecipe() == null) { + return List.of(); + } + return List.of(holder.value().convertedRecipe()); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level, List actualInputs) { + return convertAll(holder, level); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + return findMatchingRecipes(level, mergedInputs, mergedFluids, Map.of(), mold, List.of()); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, + Map mergedKeys, + @Nullable ItemStack mold, List actualInputs) { + if (level == null || !matchesMold(mold) || !matchesEmptySoulVial(mergedInputs, actualInputs)) { + return List.of(); + } + + SpawnEggItem spawnEgg = (SpawnEggItem) mold.getItem(); + EntityType entityType = spawnEgg.getType(mold.getTag()); + int energy = energyCost(entityType); + if (energy <= 0) { + return List.of(); + } + + return List.of(createHolder(entityType, mold.copyWithCount(1), energy)); + } + + private RecipeHolder createHolder( + EntityType entityType, ItemStack mold, int energy) { + ResourceLocation recipeId = recipeId(entityType); + ItemStack output = NativeSoulStorage.forSoul(captureSoul(entityType, mold)); + AdvancedAlloyFurnaceRecipe converted = new AdvancedAlloyFurnaceRecipe( + recipeId, + List.of(new CountedIngredient(emptySoulVial, INPUT_COUNT)), + List.of(), + List.of(output), + List.of(), + energy, + AdapterUtils.DEFAULT_PROCESS_TIME, + Ingredient.EMPTY, + 0, + AdapterUtils.toMoldIngredient(mold), + AlloyFurnaceMode.NORMAL); + return new RecipeHolder<>(recipeId, new SoulVialDummyRecipe(converted)); + } + + private boolean matchesEmptySoulVial( + Map mergedInputs, List actualInputs) { + if (actualInputs != null && !actualInputs.isEmpty()) { + return actualInputs.stream() + .filter(stack -> stack != null && !stack.isEmpty()) + .anyMatch(emptySoulVial::test); + } + return AdapterUtils.matchesRequired(mergedInputs, Map.of(emptySoulVial, (long) INPUT_COUNT)); + } + + static boolean isSupportedEntity(@Nullable EntityType entityType) { + return entityType != null + && SpawnEggItem.byId(entityType) != null + && EntityCaptureUtils.getCapturableEntities().contains(BuiltInRegistries.ENTITY_TYPE.getKey(entityType)) + && !entityType.is(MachineTags.EntityTypes.SPAWNER_BLACKLIST); + } + + static int energyCost(EntityType entityType) { + return SpawnerSoul.SPAWNER.matches(BuiltInRegistries.ENTITY_TYPE.getKey(entityType)) + .map(SpawnerSoul.SoulData::power) + .orElse(50_000); // Native SpawnerMachineTask fallback in Ender IO 6.2.13 + } + + private static SpawnType spawnMode(EntityType entityType) { + return SpawnerSoul.SPAWNER.matches(BuiltInRegistries.ENTITY_TYPE.getKey(entityType)) + .map(SpawnerSoul.SoulData::spawnType) + .orElseGet(() -> MachinesConfig.COMMON.SPAWN_TYPE.get()); + } + + static NativeSoul captureSoul(EntityType entityType, ItemStack mold) { + if (spawnMode(entityType) == SpawnType.COPY) { + try { + NativeSoul soul = NativeSoulStorage.getBoundSoul(mold); + if (soul != null && !soul.isEmpty()) { + return soul.copy(); + } + } catch (RuntimeException ignored) { + // Fall back to the entity type when the spawn-egg capability is unavailable. + } + } + return NativeSoul.of(entityType); + } + + private static ResourceLocation recipeId(EntityType entityType) { + ResourceLocation entityId = BuiltInRegistries.ENTITY_TYPE.getKey(entityType); + String path = entityId == null ? "unknown" : entityId.toString().replace(':', '_').replace('/', '_'); + return new ResourceLocation("enderio", "soul_vial_" + path); + } + + public static final class SoulVialDummyRecipe implements Recipe { + private final AdvancedAlloyFurnaceRecipe convertedRecipe; + + SoulVialDummyRecipe(AdvancedAlloyFurnaceRecipe convertedRecipe) { + this.convertedRecipe = convertedRecipe; + } + + AdvancedAlloyFurnaceRecipe convertedRecipe() { + return convertedRecipe; + } + + @Override + public net.minecraft.resources.ResourceLocation getId() { return convertedRecipe.id(); } + + @Override + public boolean matches(Container input, Level level) { + return false; + } + + @Override + public ItemStack assemble(Container input, net.minecraft.core.RegistryAccess registries) { + return ItemStack.EMPTY; + } + + @Override + public boolean canCraftInDimensions(int width, int height) { + return false; + } + + @Override + public ItemStack getResultItem(net.minecraft.core.RegistryAccess registries) { + return ItemStack.EMPTY; + } + + @Override + public RecipeSerializer getSerializer() { + return null; + } + + @Override + public RecipeType getType() { + return null; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/SoulVialSetIngredient.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/SoulVialSetIngredient.java new file mode 100644 index 00000000..65735f28 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/SoulVialSetIngredient.java @@ -0,0 +1,80 @@ +package com.sorrowmist.useless.content.recipe.adapters.enderio; + +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; + +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.CodecIngredient; +import com.sorrowmist.useless.compat.forge1201.recipe.CodecIngredientSerializer; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** A component-exact OR ingredient used for filtered Ender IO soul-vial displays. */ +public final class SoulVialSetIngredient extends CodecIngredient { + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(instance -> + instance.group( + ItemStack.CODEC.listOf().fieldOf("vials") + .forGetter(SoulVialSetIngredient::vials) + ).apply(instance, SoulVialSetIngredient::new)); + + public static final CodecIngredientSerializer TYPE = new CodecIngredientSerializer<>(CODEC.codec()); + + private final List vials; + + SoulVialSetIngredient(List vials) { + List normalized = new ArrayList<>(); + if (vials != null) { + for (ItemStack vial : vials) { + if (vial == null || vial.isEmpty()) { + continue; + } + ItemStack single = vial.copyWithCount(1); + if (normalized.stream().noneMatch(existing -> + ItemStack.isSameItemSameTags(existing, single))) { + normalized.add(single); + } + } + } + if (normalized.isEmpty()) { + throw new IllegalArgumentException("A soul-vial set ingredient needs at least one vial"); + } + this.vials = List.copyOf(normalized); + } + + static Ingredient of(List vials) { + return new SoulVialSetIngredient(vials); + } + + List vials() { + return vials.stream().map(ItemStack::copy).toList(); + } + + @Override + public boolean test(ItemStack stack) { + return stack != null && !stack.isEmpty() && vials.stream() + .anyMatch(vial -> ItemStack.isSameItemSameTags(vial, stack)); + } + + @Override + public ItemStack[] getItems() { + return vials.stream().map(ItemStack::copy).toArray(ItemStack[]::new); + } + + @Override + public CodecIngredientSerializer getSerializer() { return TYPE; } + + @Override + public boolean equals(Object object) { + return this == object + || object instanceof SoulVialSetIngredient other + && vials.equals(other.vials); + } + + @Override + public int hashCode() { + return Objects.hash(vials); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/TankRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/TankRecipeAdapter.java new file mode 100644 index 00000000..b7ab0410 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/TankRecipeAdapter.java @@ -0,0 +1,68 @@ +package com.sorrowmist.useless.content.recipe.adapters.enderio; + +import com.enderio.machines.common.init.MachineBlocks; +import com.enderio.machines.common.init.MachineRecipes; +import com.enderio.machines.common.recipe.TankRecipe; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess; +import com.sorrowmist.useless.content.recipe.*; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Ender IO 6's actual tank recipes: filling consumes fluid, emptying produces it. */ +public final class TankRecipeAdapter implements IRecipeAdapter { + @Override public Class getRecipeClass() { return TankRecipe.class; } + // Both tank blocks implement this unpowered recipe type. Register as a dynamic mold + // so the manager does not pre-index only one of them. + @Override public ItemStack getMoldItem() { return null; } + Ingredient moldIngredient() { + return Ingredient.of(MachineBlocks.FLUID_TANK.asItem(), MachineBlocks.PRESSURIZED_FLUID_TANK.asItem()); + } + @Override public boolean matchesMold(@Nullable ItemStack mold) { + return mold != null && !mold.isEmpty() + && (mold.is(MachineBlocks.FLUID_TANK.asItem()) + || mold.is(MachineBlocks.PRESSURIZED_FLUID_TANK.asItem())); + } + + @Override public List convertAll(RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) return List.of(); + var source = holder.value(); + if (source.getInput() == null || source.getInput().isEmpty() + || source.getOutput() == null || source.getOutput().isEmpty() + || source.getFluid() == null || source.getFluid().isEmpty()) return List.of(); + var fluid = source.getFluid().copy(); + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + List.of(new CountedIngredient(source.getInput(), 1L)), + source.isEmptying() ? List.of() : List.of(SizedFluidIngredient.of(fluid)), + List.of(source.getOutput().copy()), + source.isEmptying() ? List.of(fluid) : List.of(), + 0, AdapterUtils.DEFAULT_PROCESS_TIME, Ingredient.EMPTY, 0, + moldIngredient(), AlloyFurnaceMode.NORMAL)); + } + + @Override public List> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || mergedInputs == null || mergedFluids == null || !matchesMold(mold)) return List.of(); + var matches = new ArrayList>(); + for (var source : RecipeManagerAccess.getAllRecipesFor(level.getRecipeManager(), MachineRecipes.TANK.type().get())) { + for (var converted : convertAll(source, level)) { + if (AdapterUtils.matchesRequired(mergedInputs, EnderIOAdapterUtils.requirements(converted.inputs())) + && FluidIngredientAllocator.matchesLong(converted.inputFluids(), mergedFluids, 1L)) { + matches.add(source); + break; + } + } + } + return List.copyOf(matches); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/VatFermentingRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/VatFermentingRecipeAdapter.java new file mode 100644 index 00000000..beea6b54 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/enderio/VatFermentingRecipeAdapter.java @@ -0,0 +1,221 @@ +package com.sorrowmist.useless.content.recipe.adapters.enderio; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.enderio.enderio.content.machines.vat.FermentingRecipe; +import com.enderio.base.common.init.EIOBlocks; +import com.enderio.base.common.init.EIORecipes; +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import com.sorrowmist.useless.compat.forge1201.recipe.DataComponentIngredient; +import net.minecraft.tags.TagKey; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** Converts Ender IO Vat recipes into static multiplier-specific alloy-furnace recipes. */ +public final class VatFermentingRecipeAdapter implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + + @Override + public Class getRecipeClass() { + return FermentingRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(EIOBlocks.VAT.get()); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + + FermentingRecipe source = holder.value(); + if (!valid(source)) { + RateLimitedLogger.warn(LOGGER, "Skipping invalid Ender IO Vat recipe: {}", holder.id()); + return List.of(); + } + + List pairs = candidatePairs(source); + if (pairs.isEmpty()) { + return List.of(); + } + if (allPairsHaveSameResult(pairs)) { + long outputAmount = pairs.get(0).outputAmount(); + return List.of(createRecipe(holder.id(), source, + new CountedIngredient(Ingredient.of(source.firstReagent()), 1), + new CountedIngredient(Ingredient.of(source.secondReagent()), 1), + source.input(), new net.minecraftforge.fluids.FluidStack(source.output(), (int) outputAmount), + AdapterUtils.convertedId(holder.id()))); + } + + List result = new ArrayList<>(); + int variantIndex = 0; + for (PairResult pair : pairs) { + result.add(createRecipe(holder.id(), source, + new CountedIngredient(exact(pair.first()), 1), + new CountedIngredient(exact(pair.second()), 1), + source.input(), new net.minecraftforge.fluids.FluidStack(source.output(), (int) pair.outputAmount()), + variantId(holder.id(), 0, variantIndex++))); + } + return result; + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || mergedInputs == null || mergedFluids == null || !matchesMold(mold)) { + return List.of(); + } + List> result = new ArrayList<>(); + RecipeManager manager = level.getRecipeManager(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(manager, + EIORecipes.VAT_FERMENTING.type().get())) { + FermentingRecipe source = holder.value(); + if (!valid(source) || !EnderIOAdapterUtils.matchesFluid(mergedFluids, source.input())) { + continue; + } + Map requirements = new LinkedHashMap<>(); + AdapterUtils.mergeIngredient(requirements, Ingredient.of(source.firstReagent()), 1L); + AdapterUtils.mergeIngredient(requirements, Ingredient.of(source.secondReagent()), 1L); + if (AdapterUtils.matchesRequired(mergedInputs, requirements)) { + result.add(holder); + } + } + return result; + } + + private static long scaledAmount(int baseAmount, double multiplier) { + if (baseAmount <= 0 || !Double.isFinite(multiplier) || multiplier <= 0) { + return 0; + } + double scaled = baseAmount * multiplier; + if (!Double.isFinite(scaled) || scaled <= 0 || scaled > Integer.MAX_VALUE) { + return 0; + } + return (long) Math.floor(scaled); + } + + private static List tagItems(@Nullable TagKey tag) { + if (tag == null) { + return List.of(); + } + return EnderIOAdapterUtils.distinctStacks( + Arrays.asList(Ingredient.of(tag).getItems())); + } + + private static AdvancedAlloyFurnaceRecipe createRecipe( + net.minecraft.resources.ResourceLocation id, FermentingRecipe source, + CountedIngredient first, CountedIngredient second, FluidStack inputFluid, + FluidStack output, net.minecraft.resources.ResourceLocation recipeId) { + return createRecipe(id, source, first, second, + SizedFluidIngredient.of(inputFluid.copy()), output, recipeId); + } + + private static AdvancedAlloyFurnaceRecipe createRecipe( + net.minecraft.resources.ResourceLocation id, FermentingRecipe source, + CountedIngredient first, CountedIngredient second, SizedFluidIngredient inputFluid, + FluidStack output, net.minecraft.resources.ResourceLocation recipeId) { + return new AdvancedAlloyFurnaceRecipe( + recipeId, List.of(first, second), List.of(inputFluid), List.of(), + List.of(), List.of(output.copy()), List.of(), 0L, + Math.max(1, source.ticks()), Ingredient.EMPTY, 0, + List.of(AdapterUtils.toMoldIngredient(new ItemStack(EIOBlocks.VAT.get()))), AlloyFurnaceMode.NORMAL); + } + + private static boolean valid(FermentingRecipe source) { + return source != null && source.input() != null && source.input().amount() > 0 + && source.firstReagent() != null && source.secondReagent() != null + && source.output() != null && !source.output().isEmpty() + && source.output().getAmount() > 0 && source.ticks() > 0 + && source.input().ingredient() != null && !source.input().ingredient().isEmpty(); + } + + /** + * Enumerates the complete reagent product. An empty result means that the tags are + * unresolved or that at least one possible pair cannot be represented safely. + */ + private static List candidatePairs(FermentingRecipe source) { + List firstItems = tagItems(source.firstReagent()); + List secondItems = tagItems(source.secondReagent()); + if (firstItems.isEmpty() || secondItems.isEmpty()) return List.of(); + + List pairs = new ArrayList<>(); + Set seen = new LinkedHashSet<>(); + for (ItemStack first : firstItems) { + for (ItemStack second : secondItems) { + if (!addPairResult(source, first, second, pairs, seen)) return List.of(); + if (first.is(source.secondReagent()) && second.is(source.firstReagent())) { + if (!addPairResult(source, second, first, pairs, seen)) return List.of(); + } + } + } + return List.copyOf(pairs); + } + + private static boolean addPairResult(FermentingRecipe source, ItemStack first, ItemStack second, + List pairs, Set seen) { + if (first == null || first.isEmpty() || second == null || second.isEmpty()) return false; + String key = stackFingerprint(first) + "|" + stackFingerprint(second); + if (!seen.add(key)) return true; + double multiplier = FermentingRecipe.getModifier(first, source.firstReagent()) + * FermentingRecipe.getModifier(second, source.secondReagent()); + long amount = scaledAmount(source.output().getAmount(), multiplier); + if (!Double.isFinite(multiplier) || amount <= 0 || amount > Integer.MAX_VALUE) return false; + pairs.add(new PairResult(first.copyWithCount(1), second.copyWithCount(1), multiplier, amount)); + return true; + } + + private static boolean allPairsHaveSameResult(List pairs) { + if (pairs.isEmpty()) return false; + PairResult first = pairs.get(0); + return pairs.stream().allMatch(pair -> Double.compare(pair.multiplier(), first.multiplier()) == 0 + && pair.outputAmount() == first.outputAmount()); + } + + private static Ingredient exact(ItemStack stack) { + return DataComponentIngredient.of(true, stack.copyWithCount(1)); + } + + private static net.minecraft.resources.ResourceLocation variantId( + net.minecraft.resources.ResourceLocation original, int fluidIndex, int variantIndex) { + if (fluidIndex == 0 && variantIndex == 0) { + return AdapterUtils.convertedId(original); + } + return new net.minecraft.resources.ResourceLocation( + original.getNamespace(), original.getPath() + "_fluid_" + fluidIndex + + "_multiplier_" + variantIndex + "_converted"); + } + + private static String stackFingerprint(ItemStack stack) { + return stack.getItemHolder().unwrapKey().map(key -> key.location().toString()).orElse("unknown") + + "|" + stack.getComponents(); + } + + private record PairResult(ItemStack first, ItemStack second, double multiplier, long outputAmount) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/BarrelCompostRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/BarrelCompostRecipeAdapter.java new file mode 100644 index 00000000..f92d93d5 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/BarrelCompostRecipeAdapter.java @@ -0,0 +1,109 @@ +package com.sorrowmist.useless.content.recipe.adapters.exdeorum; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; +import thedarkcolour.exdeorum.recipe.barrel.BarrelCompostRecipe; + +import java.util.List; + +/** + * 把 Ex Deorum 桶的堆肥配方转换为合金炉配方。 + * + *

堆肥配方描述「投入一件物品可增加多少堆肥量」,当桶内堆肥量累计到 + * {@link ExDeorumRecipeAdapterUtils#COMPOST_FULL_VOLUME} 时转化为一个泥土。 + * 合金炉没有分步累积的中间状态,因此按单次产出折算:凑满一次完整堆肥所需的 + * 物品数量取 {@code ceil(满容量 / 单件体积)},产物固定为泥土。

+ * + *

石桶与木桶共用同一套堆肥配方,模具以 Ex Deorum 的 {@code exdeorum:barrels} + * 物品标签表示,因此二者无需分别适配。

+ */ +public final class BarrelCompostRecipeAdapter implements IRecipeAdapter { + + @Override + public String sourceId() { + return RecipeSourceIds.EX_DEORUM; + } + + @Override + public Class getRecipeClass() { + return BarrelCompostRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return null; + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + return ExDeorumRecipeAdapterUtils.isBarrel(mold); + } + + @Override + public List> findMatchingRecipes( + Level level, java.util.Map mergedInputs, + java.util.Map mergedFluids, + java.util.Map mergedKeys, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold) || mergedInputs == null || mergedInputs.isEmpty()) { + return List.of(); + } + + List> matches = new java.util.ArrayList<>(); + for (RecipeHolder holder : ExDeorumRecipeAdapterUtils.allOf( + level.getRecipeManager(), BarrelCompostRecipe.class)) { + BarrelCompostRecipe recipe = holder.value(); + if (recipe == null || AdapterUtils.isIngredientEmpty(recipe.ingredient)) { + continue; + } + if (AdapterUtils.countMatchingIngredient(mergedInputs, recipe.ingredient) > 0L) { + matches.add(holder); + } + } + return List.copyOf(matches); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + + BarrelCompostRecipe recipe = holder.value(); + int volume = recipe.getVolume(); + if (volume <= 0) { + return List.of(); + } + + // 凑满一次堆肥所需的物品数量;单件体积超过满容量时按 1 件计。 + long required = Math.max(1L, (long) Math.ceil( + (double) ExDeorumRecipeAdapterUtils.COMPOST_FULL_VOLUME / volume)); + + int processTime = ExDeorumRecipeAdapterUtils.processTimeFor(AdapterUtils.safeInt(required)); + + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + List.of(new CountedIngredient(recipe.ingredient, required)), + List.of(), + List.of(), + List.of(new ItemStack(Items.DIRT)), + List.of(), + List.of(), + ExDeorumRecipeAdapterUtils.energyFor(processTime), + processTime, + Ingredient.EMPTY, + 0, + ExDeorumRecipeAdapterUtils.barrelMolds(), + AlloyFurnaceMode.NORMAL)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/BarrelFluidMixingRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/BarrelFluidMixingRecipeAdapter.java new file mode 100644 index 00000000..af21a365 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/BarrelFluidMixingRecipeAdapter.java @@ -0,0 +1,126 @@ +package com.sorrowmist.useless.content.recipe.adapters.exdeorum; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import thedarkcolour.exdeorum.recipe.barrel.BarrelFluidMixingRecipe; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * 把 Ex Deorum 桶的双流体混合配方转换为合金炉配方。 + * + *

此类配方由桶内基础流体与相邻方块提供的添加流体共同作用,产出物品。转换后:

+ *
    + *
  • 基础流体按配方声明的定量作为流体输入;
  • + *
  • 添加流体本身不声明用量,按一整桶({@link ExDeorumRecipeAdapterUtils#BUCKET_VOLUME} mB)折算;
  • + *
  • {@code consumes_additive} 为假时不消耗添加流体,对应 Ex Deorum 中「世界里的添加源不被消耗」 + * 的设定,此时该流体仅作为模具参与配方判定。
  • + *
+ */ +public final class BarrelFluidMixingRecipeAdapter implements IRecipeAdapter { + + @Override + public String sourceId() { + return RecipeSourceIds.EX_DEORUM; + } + + @Override + public Class getRecipeClass() { + return BarrelFluidMixingRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return null; + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + return ExDeorumRecipeAdapterUtils.isBarrel(mold); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + Map mergedKeys, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold) || mergedFluids == null || mergedFluids.isEmpty()) { + return List.of(); + } + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : ExDeorumRecipeAdapterUtils.allOf( + level.getRecipeManager(), BarrelFluidMixingRecipe.class)) { + BarrelFluidMixingRecipe recipe = holder.value(); + if (recipe == null) { + continue; + } + if (!ExDeorumRecipeAdapterUtils.matchesFluids(requiredFluids(recipe), mergedFluids)) { + continue; + } + matches.add(holder); + } + return List.copyOf(matches); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + + BarrelFluidMixingRecipe recipe = holder.value(); + ItemStack result = recipe.getResultItem(level.registryAccess()); + if (result == null || result.isEmpty()) { + return List.of(); + } + + int processTime = ExDeorumRecipeAdapterUtils.processTimeFor(1); + + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + List.of(), + requiredFluids(recipe), + List.of(), + List.of(result.copy()), + List.of(), + List.of(), + ExDeorumRecipeAdapterUtils.energyFor(processTime), + processTime, + Ingredient.EMPTY, + 0, + ExDeorumRecipeAdapterUtils.barrelMolds(), + AlloyFurnaceMode.NORMAL)); + } + + /** + * 汇总配方要求的流体输入。 + * + *

基础流体与添加流体都下发为输入。添加流体不声明用量,按一整桶折算;其 + * {@code consumes_additive} 标志不影响输入构成,因为合金炉没有「提供该流体但不消耗」的 + * 表达方式,省略该输入只会使配方无法匹配。

+ */ + private static List requiredFluids(BarrelFluidMixingRecipe recipe) { + List fluids = new ArrayList<>(2); + if (recipe.baseFluid != null) { + fluids.add(new LongSizedFluidIngredient(com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient.single(recipe.baseFluid), recipe.baseFluidAmount)); + } + if (recipe.additiveFluid != null) { + fluids.add(new LongSizedFluidIngredient(com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient.single(recipe.additiveFluid), ExDeorumRecipeAdapterUtils.BUCKET_VOLUME)); + } + return List.copyOf(fluids); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/BarrelMixingRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/BarrelMixingRecipeAdapter.java new file mode 100644 index 00000000..146ab2d5 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/BarrelMixingRecipeAdapter.java @@ -0,0 +1,113 @@ +package com.sorrowmist.useless.content.recipe.adapters.exdeorum; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import thedarkcolour.exdeorum.recipe.barrel.BarrelMixingRecipe; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * 把 Ex Deorum 桶的物品混合配方转换为合金炉配方。 + * + *

此类配方要求桶内同时具备一件指定物品与定量流体,两者在桶内发生反应后 + * 直接替换为产物物品。转换后物品与流体都成为合金炉的常规输入,产物为配方结果。

+ * + *

物品输入按配方声明原样下发,即使其本身是装满流体的桶也不折算为流体:Ex Deorum 对 + * 同一产物同时存在物品混合与流体混合两条配方(例如牛奶桶与水、水与牛奶流体),把桶折算为 + * 流体会使两条配方的输入输出完全一致,在配方目录中产生重复条目。

+ * + *

石桶与木桶共用同一套混合配方,模具以 {@code exdeorum:barrels} 物品标签表示。

+ */ +public final class BarrelMixingRecipeAdapter implements IRecipeAdapter { + + @Override + public String sourceId() { + return RecipeSourceIds.EX_DEORUM; + } + + @Override + public Class getRecipeClass() { + return BarrelMixingRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return null; + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + return ExDeorumRecipeAdapterUtils.isBarrel(mold); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + Map mergedKeys, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold) || mergedInputs == null || mergedInputs.isEmpty()) { + return List.of(); + } + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : ExDeorumRecipeAdapterUtils.allOf( + level.getRecipeManager(), BarrelMixingRecipe.class)) { + BarrelMixingRecipe recipe = holder.value(); + if (recipe == null || AdapterUtils.isIngredientEmpty(recipe.ingredient)) { + continue; + } + if (AdapterUtils.countMatchingIngredient(mergedInputs, recipe.ingredient) <= 0L) { + continue; + } + if (!ExDeorumRecipeAdapterUtils.matchesFluids( + List.of(new LongSizedFluidIngredient(com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient.single(recipe.fluid), recipe.fluidAmount)), mergedFluids)) { + continue; + } + matches.add(holder); + } + return List.copyOf(matches); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + + BarrelMixingRecipe recipe = holder.value(); + ItemStack result = recipe.getResultItem(level.registryAccess()); + if (result == null || result.isEmpty()) { + return List.of(); + } + + int processTime = ExDeorumRecipeAdapterUtils.processTimeFor(1); + + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + List.of(new CountedIngredient(recipe.ingredient, 1L)), + List.of(new LongSizedFluidIngredient(com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient.single(recipe.fluid), recipe.fluidAmount)), + List.of(), + List.of(result.copy()), + List.of(), + List.of(), + ExDeorumRecipeAdapterUtils.energyFor(processTime), + processTime, + Ingredient.EMPTY, + 0, + ExDeorumRecipeAdapterUtils.barrelMolds(), + AlloyFurnaceMode.NORMAL)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/CrucibleRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/CrucibleRecipeAdapter.java new file mode 100644 index 00000000..e0b6cb6f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/CrucibleRecipeAdapter.java @@ -0,0 +1,126 @@ +package com.sorrowmist.useless.content.recipe.adapters.exdeorum; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import thedarkcolour.exdeorum.recipe.crucible.CrucibleRecipe; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * 把 Ex Deorum 坩埚的熔化配方转换为合金炉配方。 + * + *

坩埚配方描述「投入一件物品,熔化后产出定量流体」,其中熔岩坩埚与水源坩埚共用同一套 + * 配方结构(LAVA_CRUCIBLE 与 WATER_CRUCIBLE),仅设备与配方类型不同, + * 因此两者由本适配器一并处理。

+ * + *

坩埚没有对应的物品标签,模具改由 Ex Deorum 公开的 {@code CompatHelper} 枚举当前可用的坩埚物品, + * 判定与模具列表都据此生成。

+ * + *

配方本身不声明耗时,实际熔化速率由设备决定,故统一按单次操作的基准耗时换算处理时间与能量。

+ */ +public final class CrucibleRecipeAdapter implements IRecipeAdapter { + + @Override + public String sourceId() { + return RecipeSourceIds.EX_DEORUM; + } + + @Override + public Class getRecipeClass() { + return CrucibleRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return null; + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + return ExDeorumRecipeAdapterUtils.isLavaCrucible(mold) + || ExDeorumRecipeAdapterUtils.isWaterCrucible(mold); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + Map mergedKeys, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold) || mergedInputs == null || mergedInputs.isEmpty()) { + return List.of(); + } + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : ExDeorumRecipeAdapterUtils.allOf( + level.getRecipeManager(), CrucibleRecipe.class)) { + CrucibleRecipe recipe = holder.value(); + if (recipe == null || AdapterUtils.isIngredientEmpty(recipe.ingredient)) { + continue; + } + if (AdapterUtils.countMatchingIngredient(mergedInputs, recipe.ingredient) > 0L) { + matches.add(holder); + } + } + return List.copyOf(matches); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + + CrucibleRecipe recipe = holder.value(); + Ingredient ingredient = recipe.ingredient; + FluidStack result = recipe.getResult(); + if (AdapterUtils.isIngredientEmpty(ingredient) || result == null || result.isEmpty()) { + return List.of(); + } + + List molds = moldsFor(recipe); + if (molds.isEmpty()) { + // 坩埚物品集合为空说明材料注册表尚未就绪,此时放弃转换而不是产出无模具配方。 + return List.of(); + } + + int processTime = ExDeorumRecipeAdapterUtils.processTimeFor(1); + + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + List.of(new CountedIngredient(ingredient, 1L)), + List.of(), + List.of(), + List.of(), + List.of(result.copy()), + List.of(), + ExDeorumRecipeAdapterUtils.energyFor(processTime), + processTime, + Ingredient.EMPTY, + 0, + molds, + AlloyFurnaceMode.NORMAL)); + } + + /** 按配方所属的坩埚类别选取对应模具,避免熔岩坩埚配方出现水源坩埚模具。 */ + private static List moldsFor(CrucibleRecipe recipe) { + if (recipe.getType() == thedarkcolour.exdeorum.registry.ERecipeTypes.LAVA_CRUCIBLE.get()) { + return ExDeorumRecipeAdapterUtils.lavaCrucibleMolds(); + } + if (recipe.getType() == thedarkcolour.exdeorum.registry.ERecipeTypes.WATER_CRUCIBLE.get()) { + return ExDeorumRecipeAdapterUtils.waterCrucibleMolds(); + } + return List.of(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/ExDeorumRecipeAdapterUtils.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/ExDeorumRecipeAdapterUtils.java new file mode 100644 index 00000000..9c1d704a --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/ExDeorumRecipeAdapterUtils.java @@ -0,0 +1,509 @@ +package com.sorrowmist.useless.content.recipe.adapters.exdeorum; + +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeFingerprint; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.ExpectedOutputScaler; +import com.sorrowmist.useless.content.recipe.FluidIngredientAllocator; +import com.sorrowmist.useless.content.recipe.ItemIngredientAllocator; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.Recipe; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.ItemLike; +import net.minecraft.world.level.storage.loot.providers.number.BinomialDistributionGenerator; +import net.minecraft.world.level.storage.loot.providers.number.ConstantValue; +import net.minecraft.world.level.storage.loot.providers.number.NumberProvider; +import thedarkcolour.exdeorum.loot.SummationGenerator; +import net.minecraft.world.level.storage.loot.providers.number.UniformGenerator; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import thedarkcolour.exdeorum.compat.CompatHelper; +import thedarkcolour.exdeorum.registry.EItems; +import thedarkcolour.exdeorum.tag.EItemTags; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.OptionalDouble; +import java.util.Set; + +/** + * Ex Deorum 桶与坩埚配方转换的共用判定与换算。 + * + *

桶类配方以 {@code exdeorum:barrels} 物品标签判定模具,坩埚类配方没有对应标签,改由 + * Ex Deorum 公开的 {@link CompatHelper} 枚举当前可用的坩埚物品。两类判定结果都在首次使用时 + * 缓存,避免每次配方查找都重复遍历材料注册表。

+ */ +public final class ExDeorumRecipeAdapterUtils { + /** 与 Ex Deorum {@code BarrelBlockEntity.MAX_CAPACITY} 一致的堆肥充满量。 */ + public static final int COMPOST_FULL_VOLUME = 1000; + /** 流体类配方按整桶体积换算。 */ + public static final int BUCKET_VOLUME = 1000; + /** 处理时间与能量的换算基准:200 tick 对应 {@link AdapterUtils#DEFAULT_ENERGY}。 */ + private static final int BASE_PROCESS_TIME = 200; + + @Nullable + private static volatile Set lavaCrucibleItems; + @Nullable + private static volatile Set waterCrucibleItems; + @Nullable + private static volatile Set sieveItems; + @Nullable + private static volatile Set compressedSieveItems; + + private ExDeorumRecipeAdapterUtils() { + } + + /** 判定模具是否为任意材质的 Ex Deorum 桶,涵盖石桶与各类木桶。 */ + public static boolean isBarrel(@Nullable ItemStack mold) { + return mold != null && !mold.isEmpty() && mold.is(EItemTags.BARRELS); + } + + public static boolean isLavaCrucible(@Nullable ItemStack mold) { + return containsItem(lavaCrucibleItems(), mold); + } + + public static boolean isWaterCrucible(@Nullable ItemStack mold) { + return containsItem(waterCrucibleItems(), mold); + } + + /** 桶配方的模具字段,直接使用 Ex Deorum 的物品标签以保留标签语义。 */ + public static List barrelMolds() { + return List.of(Ingredient.of(EItemTags.BARRELS)); + } + + public static List lavaCrucibleMolds() { + return moldsOf(lavaCrucibleItems()); + } + + public static List waterCrucibleMolds() { + return moldsOf(waterCrucibleItems()); + } + + /** + * 按操作次数换算处理时间。 + * + *

Ex Deorum 的桶与坩埚配方本身不声明耗时,其进度由设备按 tick 推进,因此这里以 + * {@link #BASE_PROCESS_TIME} 作为单次操作的基准耗时,操作次数放大后即为总耗时。

+ * + * @param operations 操作次数,小于 1 时按 1 计 + * @return 处理时间(tick) + */ + public static int processTimeFor(int operations) { + return AdapterUtils.safeInt((long) BASE_PROCESS_TIME * Math.max(1, operations)); + } + + /** 按处理时间换算能量,保持与其它适配器一致的能量标度。 */ + public static long energyFor(int processTime) { + long time = Math.max(1, processTime); + return Math.max(1L, (long) AdapterUtils.safeInt(time * AdapterUtils.DEFAULT_ENERGY / BASE_PROCESS_TIME)); + } + + /** 枚举某一配方类的全部条目,避免依赖 Ex Deorum 的注册表静态字段。 */ + public static > List> allOf( + @Nullable RecipeManager recipeManager, Class recipeClass) { + if (recipeManager == null || recipeClass == null) { + return List.of(); + } + + List> result = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getRecipes(recipeManager)) { + if (recipeClass.isInstance(holder.value())) { + result.add(new RecipeHolder<>(holder.id(), recipeClass.cast(holder.value()))); + } + } + return List.copyOf(result); + } + + /** 把输入物品列表合并成需求映射,供基于 Ingredient 计数的匹配使用。 */ + public static Map requirements(List inputs) { + if (inputs == null || inputs.isEmpty()) { + return Map.of(); + } + + Map required = new LinkedHashMap<>(); + for (CountedIngredient input : inputs) { + if (input == null || AdapterUtils.isIngredientEmpty(input.ingredient()) || input.count() <= 0) { + continue; + } + AdapterUtils.mergeIngredient(required, input.ingredient(), input.count()); + } + return required; + } + + /** + * 判定物品输入是否满足配方需求。 + * + *

优先使用机器中的具体物品栈,缺失时回退到合并后的 Ingredient 计数。配方不需求物品时, + * 只有在机器同样没有物品输入的情况下才判定为匹配,避免与其它配方产生歧义。

+ */ + public static boolean matchesItems(List requirements, + @Nullable Map mergedInputs, + @Nullable List actualInputs) { + boolean hasActual = hasConcreteInputs(actualInputs); + boolean hasMerged = mergedInputs != null && !mergedInputs.isEmpty(); + if (requirements == null || requirements.isEmpty()) { + return !hasActual && !hasMerged; + } + if (hasActual) { + return ItemIngredientAllocator.matches(requirements, actualInputs, 1L); + } + return hasMerged && AdapterUtils.matchesRequired(mergedInputs, requirements(requirements)); + } + + /** 判定流体输入是否满足配方需求;配方不需求流体时要求机器同样为空。 */ + public static boolean matchesFluids(@Nullable List requirements, + @Nullable Map mergedFluids) { + boolean hasFluids = mergedFluids != null && !mergedFluids.isEmpty(); + if (requirements == null || requirements.isEmpty()) { + return !hasFluids; + } + return hasFluids && FluidIngredientAllocator.matchesLong(requirements, mergedFluids, 1L); + } + + private static boolean hasConcreteInputs(@Nullable List inputs) { + return inputs != null && inputs.stream().anyMatch(stack -> + stack != null && !stack.isEmpty() && stack.getCount() > 0); + } + + private static boolean containsItem(@Nullable Set items, @Nullable ItemStack mold) { + return items != null && !items.isEmpty() && mold != null && !mold.isEmpty() + && items.contains(mold.getItem()); + } + + private static List moldsOf(@Nullable Set items) { + if (items == null || items.isEmpty()) { + return List.of(); + } + + List stacks = new ArrayList<>(items.size()); + for (Item item : items) { + stacks.add(item.getDefaultInstance()); + } + return List.of(Ingredient.of(stacks.stream())); + } + + private static Set lavaCrucibleItems() { + Set cached = lavaCrucibleItems; + if (cached != null) { + return cached; + } + Set collected = collectCrucibleItems(true); + lavaCrucibleItems = collected; + return collected; + } + + private static Set waterCrucibleItems() { + Set cached = waterCrucibleItems; + if (cached != null) { + return cached; + } + Set collected = collectCrucibleItems(false); + waterCrucibleItems = collected; + return collected; + } + + /** + * 枚举 Ex Deorum 当前可用的坩埚物品。 + * + *

{@link CompatHelper} 的枚举会触发材料注册表的类初始化,因此仅在首次需要判定模具时调用。 + * 第三方材料缺失或注册表尚未就绪时返回空集合,由调用方按不匹配处理,不向配方查找抛出异常。

+ */ + private static Set collectCrucibleItems(boolean lava) { + Set items = new HashSet<>(); + try { + List materials = lava + ? CompatHelper.getAvailableLavaCrucibles(true) + : CompatHelper.getAvailableWaterCrucibles(true); + for (ItemLike material : materials) { + items.add(material.asItem()); + } + } catch (RuntimeException | LinkageError ignored) { + return Set.of(); + } + return Set.copyOf(items); + } + + /** + * 判定模具是否为钻石锤。 + * + *

锤子配方统一以钻石锤作为模具,不按材质区分:同一配方的产出由锤击行为决定, + * 与手持锤子的材质无关,故仅暴露一个确定性的模具槽位,避免同一配方因材质产生多份 + * 等价条目。

+ */ + public static boolean isHammer(@Nullable ItemStack mold) { + return mold != null && !mold.isEmpty() && mold.is(EItems.DIAMOND_HAMMER.get()); + } + + /** 判定模具是否为钻石压缩锤。 */ + public static boolean isCompressedHammer(@Nullable ItemStack mold) { + return mold != null && !mold.isEmpty() && mold.is(EItems.COMPRESSED_DIAMOND_HAMMER.get()); + } + + /** 锤子的模具材料,固定为钻石锤。 */ + public static List hammerMolds() { + return List.of(Ingredient.of(EItems.DIAMOND_HAMMER.get())); + } + + /** 压缩锤的模具材料,固定为钻石压缩锤,与普通锤互不重叠。 */ + public static List compressedHammerMolds() { + return List.of(Ingredient.of(EItems.COMPRESSED_DIAMOND_HAMMER.get())); + } + + /** 判定模具是否为任意材质的普通筛子。 */ + public static boolean isSieve(@Nullable ItemStack mold) { + return containsItem(sieveItems(), mold); + } + + /** 判定模具是否为任意材质的重型筛子。 */ + public static boolean isCompressedSieve(@Nullable ItemStack mold) { + return containsItem(compressedSieveItems(), mold); + } + + /** + * 普通筛子的模具材料。 + * + *

筛子没有对应的物品标签,改由 {@link CompatHelper} 枚举当前可用的筛子物品,第三方材料 + * 缺失时返回空列表,由调用方放弃转换。

+ */ + public static List sieveMolds() { + return moldsOf(sieveItems()); + } + + /** 重型筛子的模具材料,枚举方式与普通筛子一致。 */ + public static List compressedSieveMolds() { + return moldsOf(compressedSieveItems()); + } + + private static Set sieveItems() { + Set cached = sieveItems; + if (cached != null) { + return cached; + } + Set collected = collectSieveItems(false); + sieveItems = collected; + return collected; + } + + private static Set compressedSieveItems() { + Set cached = compressedSieveItems; + if (cached != null) { + return cached; + } + Set collected = collectSieveItems(true); + compressedSieveItems = collected; + return collected; + } + + /** + * 枚举 Ex Deorum 当前可用的筛子物品。 + * + *

普通筛子与重型筛子分别来自两套材料注册表,重型筛子额外依赖压缩方块材质,因此不共用缓存。

+ */ + private static Set collectSieveItems(boolean compressed) { + Set items = new HashSet<>(); + try { + List materials = compressed + ? CompatHelper.getAvailableCompressedSieves(true) + : CompatHelper.getAvailableSieves(true, false); + for (ItemLike material : materials) { + items.add(material.asItem()); + } + } catch (RuntimeException | LinkageError ignored) { + return Set.of(); + } + return Set.copyOf(items); + } + + /** + * 展开筛网材料为具体物品,供按筛网拆分模具槽使用。 + * + *

筛网集合由 {@code exdeorum:sieve_meshes} 物品标签在运行期决定,数据包或脚本新增的 + * 筛网会随标签一并生效。条目在展开时去重,并过滤空物品,避免生成重复模具。

+ * + * @param mesh 配方声明的筛网材料 + * @return 每个筛网对应一个单件物品栈;无法展开时返回空列表 + */ + public static List meshItems(@Nullable Ingredient mesh) { + if (mesh == null || mesh.isEmpty()) { + return List.of(); + } + + ItemStack[] candidates; + try { + candidates = mesh.getItems(); + } catch (RuntimeException exception) { + return List.of(); + } + + Set seen = new HashSet<>(); + List items = new ArrayList<>(); + for (ItemStack candidate : candidates) { + if (candidate == null || candidate.isEmpty() || !seen.add(candidate.getItem())) { + continue; + } + // 只保留物品本身而不继承候选栈的组件:原版筛子按 mesh.getItem() 查找配方, + // 附魔等组件由效率与幸运另行处理,不参与配方匹配。 + items.add(new ItemStack(candidate.getItem())); + } + return List.copyOf(items); + } + + /** + * 取物品的稳定标识片段,用于构造与遍历顺序无关的配方 id。 + * + *

筛网与输入材料可以以标签或候选列表的形式声明,其展开顺序在客户端与服务端不保证 + * 一致,因此配方 id 只能由物品注册名一类的内容特征派生,不能使用集合下标。未注册物品 + * 回退为占位片段:该情形仅出现在注册表尚未就绪时,此时转换结果本身也不会被采纳。

+ * + * @param stack 参与构成配方身份的物品栈 + * @return 由注册名拼成的稳定片段,命名空间与路径中的分隔符统一替换为下划线 + */ + public static String stableItemKey(@Nullable ItemStack stack) { + if (stack == null || stack.isEmpty()) { + return "empty"; + } + + ResourceLocation key = BuiltInRegistries.ITEM.getKey(stack.getItem()); + if (key == null) { + return "unregistered"; + } + return key.getNamespace() + "_" + key.getPath().replace('/', '_'); + } + + /** + * 取输入材料的稳定标识片段,用于构造与遍历顺序无关的配方 id。 + * + *

材料以 {@link Ingredient} 声明,可以是标签或候选列表,其候选展开顺序在客户端与 + * 服务端不保证一致。此处对候选物品排序去重后再拼接,使结果只取决于材料集合本身。 + * + *

候选的编码必须与 {@link AdapterUtils#areIngredientsEqual} 的判等口径一致——后者以 + * 「物品 + 组件」判定材料是否相同,因此仅取注册名不足以区分组件不同的候选,会让两条不同 + * 材料映射到同一个 id。单候选且无组件时直接使用注册名以保持可读;其余情形用集合内容的 + * SHA-256 摘要表达,避免拼接结果含资源路径不允许的分隔符或长度不可控。

+ * + * @param ingredient 参与构成配方身份的输入材料 + * @param registries 注册表访问器,用于编码物品栈组件 + * @return 与候选展开顺序无关的稳定片段 + */ + public static String stableIngredientKey( + @Nullable Ingredient ingredient, HolderLookup.Provider registries) { + if (ingredient == null || ingredient.isEmpty()) { + return "empty"; + } + + ItemStack[] candidates; + try { + candidates = ingredient.getItems(); + } catch (RuntimeException exception) { + return "unavailable"; + } + + Set keys = new HashSet<>(); + @Nullable ItemStack single = null; + boolean simple = true; + for (ItemStack candidate : candidates) { + if (candidate == null || candidate.isEmpty()) { + continue; + } + keys.add(AlloyFurnaceRecipeFingerprint.safeItemStack(candidate, registries)); + if (single == null) { + single = candidate; + } + if (candidate.hasTag()) { + simple = false; + } + } + if (keys.isEmpty() || single == null) { + return "empty"; + } + if (simple && keys.size() == 1) { + return stableItemKey(single); + } + + List sorted = new ArrayList<>(keys); + sorted.sort(Comparator.naturalOrder()); + return AdapterUtils.stableHash(String.join("|", sorted)); + } + + /** + * 计算数量提供者的期望产出量。 + * + *

换算规则与 Ex Deorum 的 {@code RecipeUtil#getExpectedValue} 一致:常量取声明值, + * 均匀分布取区间期望,二项分布取 {@code n * p},求和分布取各项期望之和。无法静态求值的 + * 提供者返回空值,由调用方放弃该条目。

+ * + * @param provider 配方声明的产出数量提供者 + * @return 期望产出量;无法计算时为 {@link OptionalDouble#empty()} + */ + public static OptionalDouble expectedAmount(@Nullable NumberProvider provider) { + if (provider == null) { + return OptionalDouble.empty(); + } + if (provider instanceof SummationGenerator summation) { + double total = 0; + for (NumberProvider child : summation.providers()) { + OptionalDouble value = expectedAmount(child); + if (value.isEmpty()) return OptionalDouble.empty(); + total += value.getAsDouble(); + } + return Double.isFinite(total) ? OptionalDouble.of(total) : OptionalDouble.empty(); + } + if (!(provider instanceof ConstantValue || provider instanceof UniformGenerator + || provider instanceof BinomialDistributionGenerator)) return OptionalDouble.empty(); + try { + return NativeNumberExpectations.expected(NUMBER_JSON.toJsonTree(provider, NumberProvider.class)); + } catch (RuntimeException e) { + return OptionalDouble.empty(); + } + } + + private static final com.google.gson.Gson NUMBER_JSON = + net.minecraft.world.level.storage.loot.Deserializers.createLootTableSerializer().create(); + + public static ItemStack resultStack(Item item, @Nullable net.minecraft.nbt.CompoundTag nbt) { + ItemStack stack = new ItemStack(item); + if (nbt != null) stack.setTag(nbt.copy()); + return stack; + } + + /** + * 把单个产出的期望量追加到带权产出列表。 + * + *

期望量被拆分为整数部分与小数部分:整数部分按确定产出表达,小数部分按单件概率产出 + * 表达。{@link ExpectedOutputScaler} 只接受 [0, 1] 区间内的概率,直接传入大于 1 的期望值 + * 会被当作概率截断而丢失产出,故必须拆分。

+ * + * @param outputs 带权产出列表,原地追加 + * @param result 产出物品,数量部分由期望量决定 + * @param expectedAmount 该产出的期望数量 + */ + public static void appendExpectedOutput(List outputs, + ItemStack result, double expectedAmount) { + if (outputs == null || result == null || result.isEmpty() || expectedAmount <= 0.0D) { + return; + } + + ItemStack single = result.copyWithCount(1); + long whole = (long) Math.floor(expectedAmount); + if (whole > 0 && whole <= Integer.MAX_VALUE) { + outputs.add(new ExpectedOutputScaler.WeightedItemOutput( + single.copy(), (int) whole, (int) whole, 1.0D)); + } + double fraction = expectedAmount - whole; + if (fraction > 1.0E-9D) { + outputs.add(new ExpectedOutputScaler.WeightedItemOutput( + single.copy(), 1, 1, fraction)); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/FluidTransformationRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/FluidTransformationRecipeAdapter.java new file mode 100644 index 00000000..c0e173a9 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/FluidTransformationRecipeAdapter.java @@ -0,0 +1,114 @@ +package com.sorrowmist.useless.content.recipe.adapters.exdeorum; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import thedarkcolour.exdeorum.recipe.barrel.FluidTransformationRecipe; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * 把 Ex Deorum 桶的流体转化配方转换为合金炉配方。 + * + *

此类配方描述「满桶的基础流体在下方方块作为催化剂时转化为另一种流体」。

+ * + *

催化剂是配方查表的必要组成部分,其判据为方块谓词而非物品,与合金炉的模具槽语义不兼容, + * 因此本适配器不引入催化剂模具:只要基础流体与桶模具同时满足即可转换。该取舍会放宽 + * Ex Deorum 对催化剂位置的约束,属于转换到合金炉后的固有差异。

+ * + *

配方自带的 {@code duration} 表示催化方块数量为 1 时的推进总时长,直接作为处理时间沿用。

+ */ +public final class FluidTransformationRecipeAdapter implements IRecipeAdapter { + + @Override + public String sourceId() { + return RecipeSourceIds.EX_DEORUM; + } + + @Override + public Class getRecipeClass() { + return FluidTransformationRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return null; + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + return ExDeorumRecipeAdapterUtils.isBarrel(mold); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + Map mergedKeys, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold) || mergedFluids == null || mergedFluids.isEmpty()) { + return List.of(); + } + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : ExDeorumRecipeAdapterUtils.allOf( + level.getRecipeManager(), FluidTransformationRecipe.class)) { + FluidTransformationRecipe recipe = holder.value(); + if (recipe == null || recipe.baseFluid == null) { + continue; + } + // 转化以满桶为触发条件,故基础流体按整桶计量。 + LongSizedFluidIngredient base = new LongSizedFluidIngredient(com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient.single(recipe.baseFluid), ExDeorumRecipeAdapterUtils.BUCKET_VOLUME); + if (!ExDeorumRecipeAdapterUtils.matchesFluids(List.of(base), mergedFluids)) { + continue; + } + matches.add(holder); + } + return List.copyOf(matches); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + + FluidTransformationRecipe recipe = holder.value(); + if (recipe.baseFluid == null || recipe.resultFluid == null) { + return List.of(); + } + + int duration = recipe.duration; + int processTime = duration > 0 + ? AdapterUtils.safeInt(duration) + : ExDeorumRecipeAdapterUtils.processTimeFor(1); + + LongSizedFluidIngredient base = new LongSizedFluidIngredient(com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient.single(recipe.baseFluid), ExDeorumRecipeAdapterUtils.BUCKET_VOLUME); + + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + List.of(), + List.of(base), + List.of(), + List.of(), + List.of(new FluidStack(recipe.resultFluid, ExDeorumRecipeAdapterUtils.BUCKET_VOLUME)), + List.of(), + ExDeorumRecipeAdapterUtils.energyFor(processTime), + processTime, + Ingredient.EMPTY, + 0, + ExDeorumRecipeAdapterUtils.barrelMolds(), + AlloyFurnaceMode.NORMAL)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/HammerRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/HammerRecipeAdapter.java new file mode 100644 index 00000000..df8f0ddb --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/HammerRecipeAdapter.java @@ -0,0 +1,164 @@ +package com.sorrowmist.useless.content.recipe.adapters.exdeorum; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.ExpectedOutputScaler; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; +import thedarkcolour.exdeorum.recipe.hammer.CompressedHammerRecipe; +import thedarkcolour.exdeorum.recipe.hammer.HammerRecipe; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.OptionalDouble; + +/** + * 把 Ex Deorum 的锤子与压缩锤配方转换为合金炉配方。 + * + *

锤子配方描述「锤击一件方块,产出定量物品」,压缩锤配方是其子类,两者结构相同而模具不同: + * 锤子以钻石锤为模具,压缩锤以钻石压缩锤为模具,二者互不重叠,因此同一输入在两类设备下可 + * 对应不同产出,转换时按配方实际类型分别指定模具。模具槽位只有一个,与单模具配方语义一致。

+ * + *

模具固定为钻石品质而不按材质区分:锤击行为与手持锤子的材质无关,若改用整个 + * {@code exdeorum:hammers} 标签,同一配方会因可选模具过多而在模具仓中产生歧义匹配。

+ * + *

产出语义:产出量由数量提供者描述,其随机性表达在数量本身而非独立的触发概率上,例如 + * {@code uniform(1, 6)} 表示产出 1 至 6 个。转换时先按 Ex Deorum 的期望值规则把提供者折算为 + * 期望产出量,再经 {@link ExpectedOutputScaler} 折算为最小确定性批次,输入数量、能耗与处理 + * 时间按同一批次倍数同步放大,避免把概率产出错误地当作必出。

+ * + *

动态读取:配方来自 RecipeManager,在运行期解析,因此数据包或脚本新增的锤子配方会一并 + * 生效;模具为固定的钻石锤与钻石压缩锤。

+ * + *

已知取舍:锤击速度受效率附魔影响,幸运附魔提供额外产出机会,二者均为运行时随机行为, + * 无法在不引入随机的前提下表达为确定性配方,故此处按未附魔的基准折算。

+ */ +public final class HammerRecipeAdapter implements IRecipeAdapter { + + @Override + public String sourceId() { + return RecipeSourceIds.EX_DEORUM; + } + + @Override + public Class getRecipeClass() { + return HammerSyntheticRecipe.class; + } + + /** 模具由配方类型决定,无法在注册阶段固定,因此以动态模具方式登记。 */ + @Override + @Nullable + public ItemStack getMoldItem() { + return null; + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + return ExDeorumRecipeAdapterUtils.isHammer(mold) + || ExDeorumRecipeAdapterUtils.isCompressedHammer(mold); + } + + @Override + public List> getGeneratedRecipes(Level level) { + if (level == null) { + return List.of(); + } + + List> sources; + try { + // 压缩锤配方是锤子配方的子类,按基类枚举即可同时覆盖两类配方。 + sources = ExDeorumRecipeAdapterUtils.allOf(level.getRecipeManager(), HammerRecipe.class); + } + catch (RuntimeException exception) { + // Ex Deorum 未安装或配方类型未注册时静默跳过,不影响其余来源的转换。 + return List.of(); + } + if (sources.isEmpty()) { + return List.of(); + } + + List> recipes = new ArrayList<>(); + for (RecipeHolder holder : sources) { + AdvancedAlloyFurnaceRecipe converted = convertHammer(holder); + if (converted != null) { + recipes.add(new RecipeHolder<>(converted.id(), new HammerSyntheticRecipe(converted))); + } + } + return List.copyOf(recipes); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null || holder.value().convertedRecipe() == null) { + return List.of(); + } + return List.of(holder.value().convertedRecipe()); + } + + /** + * 转换单条锤子配方。 + * + * @param holder 锤子配方持有者 + * @return 转换后的合金炉配方,无法转换时返回 {@code null} + */ + @Nullable + private static AdvancedAlloyFurnaceRecipe convertHammer(RecipeHolder holder) { + if (holder == null || holder.value() == null) { + return null; + } + + HammerRecipe recipe = holder.value(); + if (AdapterUtils.isIngredientEmpty(recipe.ingredient) + || recipe.result == null || recipe.result == net.minecraft.world.item.Items.AIR) { + return null; + } + + OptionalDouble expected = ExDeorumRecipeAdapterUtils.expectedAmount(recipe.resultAmount); + if (expected.isEmpty() || expected.getAsDouble() <= 0.0D) { + return null; + } + + List weightedOutputs = new ArrayList<>(); + ExDeorumRecipeAdapterUtils.appendExpectedOutput( + weightedOutputs, ExDeorumRecipeAdapterUtils.resultStack(recipe.result, recipe.getResultNbt()), expected.getAsDouble()); + + Optional scaled = + ExpectedOutputScaler.scale(weightedOutputs); + if (scaled.isEmpty() || scaled.get().outputs().isEmpty()) { + return null; + } + ExpectedOutputScaler.ScaledOutputs scaledOutputs = scaled.get(); + int operations = scaledOutputs.operations(); + + // 压缩锤与锤子分属不同标签,按配方实际类型选取模具,避免产出无法在对应设备上复现。 + List molds = recipe instanceof CompressedHammerRecipe + ? ExDeorumRecipeAdapterUtils.compressedHammerMolds() + : ExDeorumRecipeAdapterUtils.hammerMolds(); + + int processTime = ExDeorumRecipeAdapterUtils.processTimeFor(operations); + + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + List.of(new CountedIngredient(recipe.ingredient, Math.max(1L, operations))), + List.of(), + List.of(), + scaledOutputs.outputs(), + List.of(), + List.of(), + ExDeorumRecipeAdapterUtils.energyFor(processTime), + processTime, + Ingredient.EMPTY, + 0, + molds, + AlloyFurnaceMode.NORMAL); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/HammerSyntheticRecipe.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/HammerSyntheticRecipe.java new file mode 100644 index 00000000..de452f24 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/HammerSyntheticRecipe.java @@ -0,0 +1,60 @@ +package com.sorrowmist.useless.content.recipe.adapters.exdeorum; + +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.Container; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; + +/** + * 承载已转换的锤子与压缩锤配方。 + * + *

锤子配方属于 Ex Deorum 自有配方类型,不参与原版配方匹配,因此转换产物由本类型持有, + * 使 {@link HammerRecipeAdapter#convertAll} 能原样取出转换结果。

+ */ +public final class HammerSyntheticRecipe implements Recipe { + private final AdvancedAlloyFurnaceRecipe convertedRecipe; + + public HammerSyntheticRecipe(AdvancedAlloyFurnaceRecipe convertedRecipe) { + this.convertedRecipe = convertedRecipe; + } + + public AdvancedAlloyFurnaceRecipe convertedRecipe() { + return convertedRecipe; + } + + @Override + public net.minecraft.resources.ResourceLocation getId() { return convertedRecipe.id(); } + + @Override + public boolean matches(Container input, Level level) { + return convertedRecipe != null && convertedRecipe.matches(input, level); + } + + @Override + public ItemStack assemble(Container input, net.minecraft.core.RegistryAccess registries) { + return convertedRecipe == null ? ItemStack.EMPTY : convertedRecipe.assemble(input, registries); + } + + @Override + public boolean canCraftInDimensions(int width, int height) { + return convertedRecipe != null && convertedRecipe.canCraftInDimensions(width, height); + } + + @Override + public ItemStack getResultItem(net.minecraft.core.RegistryAccess registries) { + return convertedRecipe == null ? ItemStack.EMPTY : convertedRecipe.getResultItem(registries); + } + + @Override + public RecipeSerializer getSerializer() { + return convertedRecipe == null ? null : convertedRecipe.getSerializer(); + } + + @Override + public RecipeType getType() { + return convertedRecipe == null ? null : convertedRecipe.getType(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/NativeNumberExpectations.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/NativeNumberExpectations.java new file mode 100644 index 00000000..58dfecd3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/NativeNumberExpectations.java @@ -0,0 +1,42 @@ +package com.sorrowmist.useless.content.recipe.adapters.exdeorum; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import java.util.OptionalDouble; + +/** Static expectations for vanilla loot-number JSON; context-dependent providers are rejected. */ +public final class NativeNumberExpectations { + private NativeNumberExpectations() {} + + public static OptionalDouble expected(JsonElement json) { + if (json == null || json.isJsonNull()) return OptionalDouble.empty(); + if (json.isJsonPrimitive()) { + if (!json.getAsJsonPrimitive().isNumber()) return OptionalDouble.empty(); + return finite(json.getAsDouble()); + } + if (!json.isJsonObject()) return OptionalDouble.empty(); + JsonObject object = json.getAsJsonObject(); + if (!object.has("type")) return OptionalDouble.empty(); + String type = object.get("type").getAsString(); + if (type.equals("minecraft:constant")) return expected(object.get("value")); + if (type.equals("minecraft:uniform")) { + OptionalDouble min = expected(object.get("min")); + OptionalDouble max = expected(object.get("max")); + if (min.isEmpty() || max.isEmpty() || min.getAsDouble() > max.getAsDouble()) + return OptionalDouble.empty(); + return finite((min.getAsDouble() + max.getAsDouble()) / 2.0); + } + if (type.equals("minecraft:binomial")) { + OptionalDouble n = expected(object.get("n")); + OptionalDouble p = expected(object.get("p")); + if (n.isEmpty() || p.isEmpty() || n.getAsDouble() < 0 + || p.getAsDouble() < 0 || p.getAsDouble() > 1) return OptionalDouble.empty(); + return finite(n.getAsDouble() * p.getAsDouble()); + } + return OptionalDouble.empty(); + } + + private static OptionalDouble finite(double value) { + return Double.isFinite(value) ? OptionalDouble.of(value) : OptionalDouble.empty(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/SieveRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/SieveRecipeAdapter.java new file mode 100644 index 00000000..372b4b03 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/SieveRecipeAdapter.java @@ -0,0 +1,302 @@ +package com.sorrowmist.useless.content.recipe.adapters.exdeorum; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.ExpectedOutputScaler; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.core.HolderLookup; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; +import thedarkcolour.exdeorum.recipe.sieve.CompressedSieveRecipe; +import thedarkcolour.exdeorum.recipe.sieve.SieveRecipe; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.Set; +import java.util.TreeSet; + +/** + * 把 Ex Deorum 的筛子与重型筛子配方转换为合金炉配方。 + * + *

筛子配方由三部分构成:被筛方块材料、产出物品及产出量提供者、以及限定可用筛网的材料。 + * 转换后模具占用两个槽位:一号槽为筛子方块,二号槽为该配方限定的筛网。普通筛子与重型筛子 + * 分别使用各自的设备材料集合,二者共用同一份配方结构({@link CompressedSieveRecipe} 是 + * {@link SieveRecipe} 的子类),因此由本适配器一并处理。

+ * + *

产出语义:单条筛子配方的产出量由数量提供者描述,其随机性表达在数量本身而非独立的触发 + * 概率上,例如 {@code binomial(1, p)} 表示以概率 p 产出 1 个。转换时先按 Ex Deorum 的期望值 + * 规则把提供者折算为期望产出量,再经 {@link ExpectedOutputScaler} 折算为最小确定性批次, + * 输入数量、能耗与处理时间按同一批次倍数同步放大,避免把概率产出错误地当作必出。

+ * + *

筛网拆分:同一条配方可声明多个可用筛网,而不同筛网对应的产出概率互不相同。转换时按 + * 「输入材料 + 具体筛网」重新分组,每个筛网生成独立配方,同一分组内多条配方的期望产出累加, + * 与筛子实际逐个判定产出条目的行为一致。

+ * + *

动态读取:配方来自 RecipeManager,筛网集合来自 {@code exdeorum:sieve_meshes} 物品标签, + * 筛子与重型筛子的设备集合来自 Ex Deorum 的材料注册表,三者均在运行期解析,因此数据包或脚本 + * 新增的筛网、配方与设备材料会一并生效。

+ * + *

已知取舍:筛子效率受筛网效率附魔影响,幸运附魔额外提供重掷机会,二者均为运行时随机 + * 行为,无法在不引入随机的前提下表达为确定性配方,故此处按未附魔的基准效率折算;标记为仅 + * 手动筛选的配方不会从机械筛产出,合金炉属机械装置,故不纳入转换范围。

+ * + *

模具数量:筛子配方占用两个模具槽位,超出单槽配方的假设,因此仅多方块结构的万能模具仓 + * 支持此类配方;AE 网络与样板计算器路径按单模具语义处理,遇到多模具配方会跳过。

+ */ +public final class SieveRecipeAdapter implements IRecipeAdapter { + + @Override + public String sourceId() { + return RecipeSourceIds.EX_DEORUM; + } + + @Override + public Class getRecipeClass() { + return SieveSyntheticRecipe.class; + } + + /** 模具由配方限定的筛网决定,无法在注册阶段固定,因此以动态模具方式登记。 */ + @Override + @Nullable + public ItemStack getMoldItem() { + return null; + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + return ExDeorumRecipeAdapterUtils.isSieve(mold) + || ExDeorumRecipeAdapterUtils.isCompressedSieve(mold); + } + + @Override + public List> getGeneratedRecipes(Level level) { + if (level == null) { + return List.of(); + } + + List> sources; + try { + // 重型筛子配方是普通筛子配方的子类,按基类枚举即可同时覆盖两类配方。 + sources = ExDeorumRecipeAdapterUtils.allOf(level.getRecipeManager(), SieveRecipe.class); + } + catch (RuntimeException exception) { + // Ex Deorum 未安装或配方类型未注册时静默跳过,不影响其余来源的转换。 + return List.of(); + } + if (sources.isEmpty()) { + return List.of(); + } + + List> recipes = new ArrayList<>(); + for (AdvancedAlloyFurnaceRecipe converted : convertSieves(sources, level.registryAccess())) { + recipes.add(new RecipeHolder<>(converted.id(), new SieveSyntheticRecipe(converted))); + } + return List.copyOf(recipes); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null || holder.value().convertedRecipe() == null) { + return List.of(); + } + return List.of(holder.value().convertedRecipe()); + } + + /** + * 按「输入材料 + 具体筛网」分组并转换全部筛子配方。 + * + * @param sources 运行期枚举到的筛子配方 + * @param registries 注册表访问器,用于编码材料组件以派生配方 id + * @return 转换后的合金炉配方列表 + */ + private static List convertSieves( + List> sources, HolderLookup.Provider registries) { + List groups = new ArrayList<>(); + + for (RecipeHolder holder : sources) { + SieveRecipe recipe = holder.value(); + if (recipe == null || recipe.byHandOnly) { + continue; + } + if (AdapterUtils.isIngredientEmpty(recipe.ingredient) + || recipe.result == null || recipe.result == net.minecraft.world.item.Items.AIR) { + continue; + } + + OptionalDouble expected = ExDeorumRecipeAdapterUtils.expectedAmount(recipe.resultAmount); + if (expected.isEmpty() || expected.getAsDouble() <= 0.0D) { + continue; + } + + // 筛网无法展开时该配方没有可用模具,放弃转换而不是产出无筛网配方。 + List meshes = ExDeorumRecipeAdapterUtils.meshItems(Ingredient.of(recipe.mesh)); + if (meshes.isEmpty()) { + continue; + } + + boolean compressed = recipe instanceof CompressedSieveRecipe; + for (ItemStack mesh : meshes) { + SieveGroup group = findGroup(groups, compressed, mesh, recipe.ingredient); + if (group == null) { + group = new SieveGroup(compressed, mesh, recipe.ingredient, registries); + groups.add(group); + } + group.add(ExDeorumRecipeAdapterUtils.resultStack(recipe.result, recipe.getResultNbt()), expected.getAsDouble()); + group.addSourceId(holder.id()); + } + } + + // 分组顺序由源配方枚举顺序决定,而该顺序在客户端与服务端不保证一致。转换结果本身 + // 不依赖顺序,但排序后可以让配方目录的条目次序保持稳定,便于比对与缓存。 + groups.sort(Comparator.comparing(SieveGroup::sortKey)); + + // 内容键相同的分组无法共存:分组判等的三个分量与内容键一一对应,因此该统计只用于 + // 识别材料以物品级键表达、无法区分组件差异的碰撞,正常结果为全部为 1。 + Map pathCounts = new HashMap<>(); + for (SieveGroup group : groups) { + pathCounts.merge(group.idPath(), 1, Integer::sum); + } + + List recipes = new ArrayList<>(); + for (SieveGroup group : groups) { + AdvancedAlloyFurnaceRecipe converted = + group.toRecipe(pathCounts.getOrDefault(group.idPath(), 1) > 1); + if (converted != null) { + recipes.add(converted); + } + } + return List.copyOf(recipes); + } + + /** 在既有分组中查找输入材料语义相同的同筛网分组。 */ + @Nullable + private static SieveGroup findGroup( + List groups, boolean compressed, ItemStack mesh, Ingredient input) { + for (SieveGroup group : groups) { + if (group.matches(compressed, mesh, input)) { + return group; + } + } + return null; + } + + /** 同一「输入材料 + 具体筛网 + 设备类别」下的产出累加单元。 */ + private static final class SieveGroup { + private final boolean compressed; + private final ItemStack mesh; + private final Ingredient input; + private final String contentKey; + private final Set sourceIds = new TreeSet<>(); + private final List weightedOutputs = new ArrayList<>(); + + private SieveGroup(boolean compressed, ItemStack mesh, Ingredient input, + HolderLookup.Provider registries) { + this.compressed = compressed; + this.mesh = mesh; + this.input = input; + // 分组判等的三个分量即配方身份的全部内容来源,故 id 只由它们派生。 + this.contentKey = (compressed ? "compressed_sieve_" : "sieve_") + + ExDeorumRecipeAdapterUtils.stableItemKey(mesh) + "_" + + ExDeorumRecipeAdapterUtils.stableIngredientKey(input, registries); + } + + /** 与配方枚举顺序无关的排序键。 */ + private String sortKey() { + return contentKey; + } + + /** + * 记录合入本分组的源配方 id。 + * + *

源配方 id 仅在内容键发生碰撞时用于消歧,正常路径不进入配方 id:分组合并时哪条 + * 源配方先被枚举到由遍历顺序决定,若把它作为 id 的固定组成部分,两端就会生成不同的 + * 配方 id。此处用有序集合消除记录顺序的影响。

+ */ + private void addSourceId(ResourceLocation id) { + if (id != null) { + sourceIds.add(id); + } + } + + /** 配方 id 的主体路径,不含命名空间。 */ + private String idPath() { + return contentKey + "_converted"; + } + + private boolean matches(boolean compressed, ItemStack mesh, Ingredient input) { + return this.compressed == compressed + && this.mesh.getItem() == mesh.getItem() + && AdapterUtils.areIngredientsEqual(this.input, input); + } + + /** 累加一条配方的期望产出;期望量的拆分规则见共用方法。 */ + private void add(ItemStack result, double expectedAmount) { + ExDeorumRecipeAdapterUtils.appendExpectedOutput(weightedOutputs, result, expectedAmount); + } + + @Nullable + private AdvancedAlloyFurnaceRecipe toRecipe(boolean disambiguate) { + List deviceMolds = compressed + ? ExDeorumRecipeAdapterUtils.compressedSieveMolds() + : ExDeorumRecipeAdapterUtils.sieveMolds(); + if (deviceMolds.isEmpty()) { + // 设备材料集合为空说明材料注册表尚未就绪,此时放弃转换而不是产出无设备模具配方。 + return null; + } + + Optional scaled = + ExpectedOutputScaler.scale(weightedOutputs); + if (scaled.isEmpty() || scaled.get().outputs().isEmpty()) { + return null; + } + ExpectedOutputScaler.ScaledOutputs scaledOutputs = scaled.get(); + int operations = scaledOutputs.operations(); + + // 模具按槽位排列:一号槽为筛子方块,二号槽为该配方限定的筛网。 + List molds = new ArrayList<>(deviceMolds.size() + 1); + molds.addAll(deviceMolds); + molds.add(Ingredient.of(mesh)); + + int processTime = ExDeorumRecipeAdapterUtils.processTimeFor(operations); + + // 同一源配方会按筛网拆分为多条,配方 id 必须由分组内容派生:源配方与筛网的枚举 + // 顺序在客户端与服务端不保证一致,若改用集合下标或「首条源配方 id」,两端会把 + // 同一个 id 指向不同的配方,导致 JEI 侧选中的配方在服务端目录中无法解析。 + String path = idPath(); + if (disambiguate) { + path = path + "_" + AdapterUtils.stableHash( + String.join("|", sourceIds.stream().map(ResourceLocation::toString).toList())); + } + ResourceLocation recipeId = new ResourceLocation( + RecipeSourceIds.EX_DEORUM, path); + + return new AdvancedAlloyFurnaceRecipe( + recipeId, + List.of(new CountedIngredient(input, Math.max(1L, operations))), + List.of(), + List.of(), + scaledOutputs.outputs(), + List.of(), + List.of(), + ExDeorumRecipeAdapterUtils.energyFor(processTime), + processTime, + Ingredient.EMPTY, + 0, + molds, + AlloyFurnaceMode.NORMAL); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/SieveSyntheticRecipe.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/SieveSyntheticRecipe.java new file mode 100644 index 00000000..41379f09 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/SieveSyntheticRecipe.java @@ -0,0 +1,61 @@ +package com.sorrowmist.useless.content.recipe.adapters.exdeorum; + +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.Container; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; + +/** + * 承载按筛网拆分后生成的单个合金炉配方。 + * + *

Ex Deorum 的一条筛子配方可能同时匹配多个筛网,而不同筛网对应的产出概率互不相同, + * 因此转换阶段会按「输入方块 + 具体筛网」重新分组,一条源配方可展开为多条合金炉配方。 + * 每条展开结果由本类型持有,使 {@link SieveRecipeAdapter#convertAll} 能原样取出转换产物。

+ */ +public final class SieveSyntheticRecipe implements Recipe { + private final AdvancedAlloyFurnaceRecipe convertedRecipe; + + public SieveSyntheticRecipe(AdvancedAlloyFurnaceRecipe convertedRecipe) { + this.convertedRecipe = convertedRecipe; + } + + public AdvancedAlloyFurnaceRecipe convertedRecipe() { + return convertedRecipe; + } + + @Override + public net.minecraft.resources.ResourceLocation getId() { return convertedRecipe.id(); } + + @Override + public boolean matches(Container input, Level level) { + return convertedRecipe != null && convertedRecipe.matches(input, level); + } + + @Override + public ItemStack assemble(Container input, net.minecraft.core.RegistryAccess registries) { + return convertedRecipe == null ? ItemStack.EMPTY : convertedRecipe.assemble(input, registries); + } + + @Override + public boolean canCraftInDimensions(int width, int height) { + return convertedRecipe != null && convertedRecipe.canCraftInDimensions(width, height); + } + + @Override + public ItemStack getResultItem(net.minecraft.core.RegistryAccess registries) { + return convertedRecipe == null ? ItemStack.EMPTY : convertedRecipe.getResultItem(registries); + } + + @Override + public RecipeSerializer getSerializer() { + return convertedRecipe == null ? null : convertedRecipe.getSerializer(); + } + + @Override + public RecipeType getType() { + return convertedRecipe == null ? null : convertedRecipe.getType(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/extendedcrafting/ExtendedCraftingAdapterUtils.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/extendedcrafting/ExtendedCraftingAdapterUtils.java new file mode 100644 index 00000000..4e916fdf --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/extendedcrafting/ExtendedCraftingAdapterUtils.java @@ -0,0 +1,500 @@ +package com.sorrowmist.useless.content.recipe.adapters.extendedcrafting; + +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.Container; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.Set; +import java.util.function.Function; + +/** Shared, defensive conversion helpers for Extended Crafting recipes. */ +public final class ExtendedCraftingAdapterUtils { + private static final int MAX_REMAINDER_VARIANTS = 512; + + private ExtendedCraftingAdapterUtils() { + } + + /** Adds every non-empty source ingredient, retaining component/custom ingredient identity. */ + public static boolean mergeIngredients( + Map target, Iterable ingredients) { + if (target == null || ingredients == null) { + return false; + } + boolean found = false; + for (Ingredient ingredient : ingredients) { + if (ingredient == null || ingredient.isEmpty()) { + continue; + } + AdapterUtils.mergeIngredient(target, ingredient, 1L); + found = true; + } + return found; + } + + public static List countedIngredients(Map ingredients) { + if (ingredients == null || ingredients.isEmpty()) { + return List.of(); + } + List result = new ArrayList<>(); + for (Map.Entry entry : ingredients.entrySet()) { + Ingredient ingredient = entry.getKey(); + long count = entry.getValue() == null ? 0L : entry.getValue(); + if (ingredient == null || ingredient.isEmpty() || count <= 0L) { + continue; + } + result.add(new CountedIngredient(ingredient, count)); + } + return List.copyOf(result); + } + + /** Returns a canonical stack only when every candidate is exactly the same stack. */ + public static Optional deterministicStack(@Nullable Ingredient ingredient) { + if (ingredient == null || ingredient.isEmpty()) { + return Optional.empty(); + } + ItemStack[] candidates; + try { + candidates = ingredient.getItems(); + } catch (RuntimeException exception) { + return Optional.empty(); + } + if (candidates == null || candidates.length == 0) { + return Optional.empty(); + } + + ItemStack first = null; + for (ItemStack candidate : candidates) { + if (candidate == null || candidate.isEmpty() || candidate.getCount() <= 0) { + return Optional.empty(); + } + if (first == null) { + first = candidate.copy(); + } else if (!ItemStack.isSameItemSameTags(first, candidate) + || first.getCount() != candidate.getCount()) { + return Optional.empty(); + } + } + return first == null ? Optional.empty() : Optional.of(first); + } + + public static ItemStack copyResult(@Nullable Recipe recipe) { + if (recipe == null) { + return ItemStack.EMPTY; + } + try { + ItemStack result = recipe.getResultItem(null); + return result == null ? ItemStack.EMPTY : result.copy(); + } catch (RuntimeException exception) { + return ItemStack.EMPTY; + } + } + + /** + * Finds remainders which are invariant for all displayed ingredient candidates. + * + *

The resolver is normally {@code Recipe#getRemainingItems}. A recipe with a + * component-sensitive or dynamic remainder is rejected rather than creating a + * free or incorrect output.

+ * + * @param ingredientSlots input slots in the same order used by the resolver + * @param ignoredSlots slots whose remainder is intentionally not copied + * @param resolver creates the source recipe input and returns its remaining items + */ + public static Optional> deterministicRemainders( + List ingredientSlots, + Set ignoredSlots, + Function, I> inputFactory, + Function> resolver) { + return deterministicRemaindersInternal( + ingredientSlots, ignoredSlots, inputFactory, resolver, false); + } + + /** + * Finds remainders which are invariant for all displayed ingredient candidates and keeps + * one result for each source slot. Empty slots and ignored slots are represented by an empty + * stack. Unlike {@link #deterministicRemainders(List, Set, Function, Function)}, this method + * does not merge equal remainders because callers may need to retain their source-slot + * identity (for example, each crafting remainder is an independent mold requirement). + */ + public static Optional> deterministicRemaindersBySlot( + List ingredientSlots, + Set ignoredSlots, + Function, I> inputFactory, + Function> resolver) { + return deterministicRemaindersInternal( + ingredientSlots, ignoredSlots, inputFactory, resolver, true); + } + + /** + * Resolves every bounded ingredient candidate combination and keeps the source input beside + * its normalized slot remainders. Callers which have semantic equivalence rules (for example, + * reusable tools whose damage changes) can classify the variants without treating the exact + * remainder stack as the identity of the recipe. + */ + public static Optional> remainderVariantsBySlot( + List ingredientSlots, + Set ignoredSlots, + Function, I> inputFactory, + Function> resolver) { + if (ingredientSlots == null || inputFactory == null || resolver == null) { + return Optional.empty(); + } + + Set ignored = ignoredSlots == null ? Set.of() : new HashSet<>(ignoredSlots); + List> candidates = new ArrayList<>(ingredientSlots.size()); + long combinations = 1L; + for (Ingredient ingredient : ingredientSlots) { + if (ingredient == null || ingredient.isEmpty()) { + candidates.add(List.of(ItemStack.EMPTY)); + continue; + } + ItemStack[] values; + try { + values = ingredient.getItems(); + } catch (RuntimeException exception) { + return Optional.empty(); + } + if (values == null || values.length == 0) { + // Preserve the unresolved-tag behavior used by the existing conversion path. + candidates.add(List.of(ItemStack.EMPTY)); + continue; + } + List safeValues = new ArrayList<>(values.length); + for (ItemStack value : values) { + if (value == null || value.isEmpty() || value.getCount() <= 0) { + return Optional.empty(); + } + safeValues.add(value.copy()); + } + candidates.add(List.copyOf(safeValues)); + if (combinations <= MAX_REMAINDER_VARIANTS) { + combinations = Math.min( + MAX_REMAINDER_VARIANTS + 1L, + combinations * Math.max(1, safeValues.size())); + } + } + + if (combinations > MAX_REMAINDER_VARIANTS) { + return Optional.empty(); + } + + List> variants = new ArrayList<>(); + buildVariants(candidates, 0, new ArrayList<>(), variants); + List result = new ArrayList<>(variants.size()); + for (List variant : variants) { + List remainder; + try { + I input = inputFactory.apply(copyStacks(variant)); + remainder = resolver.apply(input); + } catch (RuntimeException exception) { + return Optional.empty(); + } + Optional> normalized = normalizeRemainderBySlot( + remainder, ingredientSlots.size(), ignored); + if (normalized.isEmpty()) { + return Optional.empty(); + } + result.add(new RemainderVariant(copyStacks(variant), normalized.get())); + } + return Optional.of(List.copyOf(result)); + } + + private static Optional> deterministicRemaindersInternal( + List ingredientSlots, + Set ignoredSlots, + Function, I> inputFactory, + Function> resolver, + boolean preserveSlots) { + Optional> variants = remainderVariantsBySlot( + ingredientSlots, ignoredSlots, inputFactory, resolver); + if (variants.isEmpty()) { + return Optional.empty(); + } + + Set ignored = ignoredSlots == null ? Set.of() : new HashSet<>(ignoredSlots); + List expected = null; + for (RemainderVariant variant : variants.get()) { + List normalized = preserveSlots + ? variant.remainders() + : normalizeRemainder(variant.remainders(), ignored).orElse(null); + if (normalized == null) { + return Optional.empty(); + } + if (expected == null) { + expected = normalized; + } else if (preserveSlots + ? !sameStacksBySlot(expected, normalized) + : !sameStacks(expected, normalized)) { + return Optional.empty(); + } + } + return Optional.of(expected == null ? List.of() : expected); + } + + public record RemainderVariant(List inputs, List remainders) { + public RemainderVariant { + inputs = inputs == null ? List.of() : List.copyOf(inputs); + remainders = remainders == null ? List.of() : List.copyOf(remainders); + } + } + + private static void buildVariants( + List> candidates, int slot, List current, + List> result) { + if (result.size() >= MAX_REMAINDER_VARIANTS) { + return; + } + if (slot >= candidates.size()) { + result.add(copyStacks(current)); + return; + } + for (ItemStack candidate : candidates.get(slot)) { + current.add(candidate); + buildVariants(candidates, slot + 1, current, result); + current.remove(current.size()-1); + if (result.size() >= MAX_REMAINDER_VARIANTS) { + return; + } + } + } + + private static List copyStacks(List stacks) { + List result = new ArrayList<>(stacks.size()); + for (ItemStack stack : stacks) { + result.add(stack == null ? ItemStack.EMPTY : stack.copy()); + } + return result; + } + + private static Optional> normalizeRemainder( + @Nullable List remainder, Set ignoredSlots) { + List result = new ArrayList<>(); + if (remainder == null) { + return Optional.empty(); + } + for (int index = 0; index < remainder.size(); index++) { + if (ignoredSlots.contains(index)) { + continue; + } + ItemStack stack = remainder.get(index); + if (stack == null || stack.isEmpty() || stack.getCount() <= 0) { + continue; + } + if (!mergeOutput(result, stack)) { + return Optional.empty(); + } + } + return Optional.of(result); + } + + private static Optional> normalizeRemainderBySlot( + @Nullable List remainder, int slotCount, Set ignoredSlots) { + if (remainder == null) { + return Optional.empty(); + } + + List result = new ArrayList<>(slotCount); + for (int slot = 0; slot < slotCount; slot++) { + if (ignoredSlots.contains(slot) || slot >= remainder.size()) { + result.add(ItemStack.EMPTY); + continue; + } + ItemStack stack = remainder.get(slot); + if (stack == null || stack.isEmpty()) { + result.add(ItemStack.EMPTY); + continue; + } + if (stack.getCount() <= 0) { + return Optional.empty(); + } + result.add(stack.copy()); + } + + // A resolver should return one remainder entry per source slot. Preserve the old + // aggregate helper's permissive handling of trailing empty entries, but reject a + // non-empty remainder that cannot be assigned to a source slot. + for (int slot = slotCount; slot < remainder.size(); slot++) { + ItemStack stack = remainder.get(slot); + if (stack != null && !stack.isEmpty() && stack.getCount() > 0) { + return Optional.empty(); + } + } + return Optional.of(List.copyOf(result)); + } + + private static boolean sameStacks(List left, List right) { + if (left.size() != right.size()) { + return false; + } + boolean[] used = new boolean[right.size()]; + for (ItemStack expected : left) { + boolean found = false; + for (int index = 0; index < right.size(); index++) { + ItemStack actual = right.get(index); + if (!used[index] + && expected.getCount() == actual.getCount() + && ItemStack.isSameItemSameTags(expected, actual)) { + used[index] = true; + found = true; + break; + } + } + if (!found) { + return false; + } + } + return true; + } + + private static boolean sameStacksBySlot(List left, List right) { + if (left.size() != right.size()) { + return false; + } + for (int slot = 0; slot < left.size(); slot++) { + ItemStack expected = left.get(slot); + ItemStack actual = right.get(slot); + if (expected.getCount() != actual.getCount() + || !ItemStack.isSameItemSameTags(expected, actual)) { + return false; + } + } + return true; + } + + /** + * Cucumber's untransformed table recipes resolve each item's crafting remainder independently. + * Check every candidate in each slot, rather than the Cartesian product of unrelated slots. + * This is only valid for callers which have established that native slot-local behavior. + */ + public static Optional> independentItemRemainders(List slots) { + if (slots == null) return Optional.empty(); + List outputs = new ArrayList<>(); + for (Ingredient ingredient : slots) { + if (ingredient == null || ingredient.isEmpty()) continue; + ItemStack expected = null; + try { + ItemStack[] candidates = ingredient.getItems(); + if (candidates.length == 0) return Optional.empty(); + for (ItemStack candidate : candidates) { + if (candidate == null || candidate.isEmpty()) return Optional.empty(); + ItemStack stack = candidate.copy(); + ItemStack remainder = stack.hasCraftingRemainingItem() + ? stack.getCraftingRemainingItem() : ItemStack.EMPTY; + if (remainder == null) return Optional.empty(); + if (expected == null) expected = remainder.copy(); + else if (expected.getCount() != remainder.getCount() + || !ItemStack.isSameItemSameTags(expected, remainder)) return Optional.empty(); + } + } catch (RuntimeException exception) { return Optional.empty(); } + if (expected != null && !expected.isEmpty() && !mergeOutput(outputs, expected)) + return Optional.empty(); + } + return Optional.of(List.copyOf(outputs)); + } + + /** Merges equal component-aware output stacks and rejects an int overflow. */ + public static boolean mergeOutput(List outputs, ItemStack output) { + if (outputs == null || output == null || output.isEmpty() || output.getCount() <= 0) { + return false; + } + for (ItemStack existing : outputs) { + if (!ItemStack.isSameItemSameTags(existing, output)) { + continue; + } + long merged = (long) existing.getCount() + output.getCount(); + if (merged > Integer.MAX_VALUE) { + return false; + } + existing.setCount((int) merged); + return true; + } + outputs.add(output.copy()); + return true; + } + + /** Converts a powered recipe's total cost/rate pair to positive furnace ticks. */ + public static OptionalInt powerProcessTime(long cost, long rate) { + if (cost < 0L) { + return OptionalInt.empty(); + } + if (cost == 0L) { + return OptionalInt.of(1); + } + if (rate <= 0L) { + return OptionalInt.empty(); + } + long ticks = cost / rate; + if (cost % rate != 0L) { + ticks++; + } + return OptionalInt.of(safePositiveTicks(ticks)); + } + + /** Converts source seconds to ticks using long arithmetic and an int upper bound. */ + public static OptionalInt secondsToTicks(long seconds) { + if (seconds < 0L) { + return OptionalInt.empty(); + } + if (seconds == 0L) { + return OptionalInt.of(1); + } + long ticks; + if (seconds > Long.MAX_VALUE / 20L) { + ticks = Long.MAX_VALUE; + } else { + ticks = seconds * 20L; + } + return OptionalInt.of(safePositiveTicks(ticks)); + } + + public static int safePositiveTicks(long ticks) { + if (ticks <= 0L) { + return 1; + } + return ticks >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) ticks; + } + + /** Picks a compact grid for synthetic shaped/shapeless remainder inputs. */ + public static int[] gridDimensions(int slots) { + int count = Math.max(1, slots); + int width = (int) Math.ceil(Math.sqrt(count)); + while (width > 1 && count % width != 0) { + width--; + } + int height = (count + width - 1) / width; + return new int[]{Math.max(1, width), Math.max(1, height)}; + } + + /** Uses shaped recipe dimensions when the source exposes getWidth/getHeight. */ + public static int[] gridDimensions(@Nullable Object recipe, int slots) { + if (recipe != null) { + try { + Method widthMethod = recipe.getClass().getMethod("getWidth"); + Method heightMethod = recipe.getClass().getMethod("getHeight"); + Object widthValue = widthMethod.invoke(recipe); + Object heightValue = heightMethod.invoke(recipe); + if (widthValue instanceof Number widthNumber && heightValue instanceof Number heightNumber) { + int width = widthNumber.intValue(); + int height = heightNumber.intValue(); + if (width > 0 && height > 0 && (long) width * height == Math.max(1, slots)) { + return new int[]{width, height}; + } + } + } catch (ReflectiveOperationException | RuntimeException ignored) { + // Shapeless and third-party recipe implementations need no explicit dimensions. + } + } + return gridDimensions(slots); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/extendedcrafting/ExtendedCraftingCombinationRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/extendedcrafting/ExtendedCraftingCombinationRecipeAdapter.java new file mode 100644 index 00000000..7f67eccc --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/extendedcrafting/ExtendedCraftingCombinationRecipeAdapter.java @@ -0,0 +1,163 @@ +package com.sorrowmist.useless.content.recipe.adapters.extendedcrafting; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.blakebr0.extendedcrafting.api.crafting.ICombinationRecipe; +import com.blakebr0.extendedcrafting.init.ModBlocks; +import com.blakebr0.extendedcrafting.init.ModRecipeTypes; +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.Set; + +/** Converts Extended Crafting combination/core recipes. */ +public class ExtendedCraftingCombinationRecipeAdapter implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + + @Override + public Class getRecipeClass() { + return ICombinationRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(ModBlocks.CRAFTING_CORE.get()); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + Converted converted = convertData(holder.value()); + if (converted == null) { + RateLimitedLogger.warn(LOGGER, "Skipping invalid Extended Crafting combination recipe: {}", holder.id()); + return List.of(); + } + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + converted.inputs(), + List.of(), + converted.outputs(), + List.of(), + converted.energy(), + converted.processTime(), + Ingredient.EMPTY, + 0, + AdapterUtils.toMoldIngredient(getMoldItem()), + AlloyFurnaceMode.NORMAL + )); + } + + @Override + public List> findMatchingRecipes( + Level level, + Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || mergedInputs == null || mergedInputs.isEmpty() || !matchesMold(mold)) { + return List.of(); + } + List> matches = new ArrayList<>(); + RecipeManager recipeManager = level.getRecipeManager(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, ModRecipeTypes.COMBINATION.get())) { + Converted converted = convertData(holder.value()); + if (converted != null && AdapterUtils.matchesRequired(mergedInputs, converted.requirements())) { + matches.add(holder); + } + } + return matches; + } + + @Nullable + private static Converted convertData(@Nullable ICombinationRecipe source) { + if (source == null) { + return null; + } + List all = source.getIngredients(); + if (all == null || all.isEmpty()) return null; + Ingredient center = all.get(0); + List pedestals = all.subList(1, all.size()); + if (center == null || center.isEmpty() || pedestals == null) { + return null; + } + for (Ingredient ingredient : pedestals) { + if (ingredient == null || ingredient.isEmpty()) { + return null; + } + } + + long powerCost = source.getPowerCost(); + OptionalInt processTime = ExtendedCraftingAdapterUtils.powerProcessTime(powerCost, source.getPowerRate()); + if (processTime.isEmpty()) { + return null; + } + + ItemStack result = ExtendedCraftingAdapterUtils.copyResult(source); + if (result.isEmpty() || result.getCount() <= 0) { + return null; + } + + List slots = new ArrayList<>(pedestals.size() + 1); + slots.add(center); + slots.addAll(pedestals); + + Map requirements = new LinkedHashMap<>(); + if (!ExtendedCraftingAdapterUtils.mergeIngredients(requirements, slots)) { + return null; + } + + Optional> remainders = ExtendedCraftingAdapterUtils.deterministicRemainders( + slots, + Set.of(0), // the center slot is replaced by the crafted result + stacks -> { + int[] dimensions = ExtendedCraftingAdapterUtils.gridDimensions(stacks.size()); + return com.sorrowmist.useless.compat.forge1201.recipe.CraftingInputs.of(dimensions[0], dimensions[1], stacks); + }, + source::getRemainingItems + ); + if (remainders.isEmpty()) { + return null; + } + + List outputs = new ArrayList<>(); + if (!ExtendedCraftingAdapterUtils.mergeOutput(outputs, result)) { + return null; + } + for (ItemStack remainder : remainders.get()) { + if (!ExtendedCraftingAdapterUtils.mergeOutput(outputs, remainder)) { + return null; + } + } + + List counted = ExtendedCraftingAdapterUtils.countedIngredients(requirements); + return new Converted(counted, outputs, requirements, powerCost, processTime.getAsInt()); + } + + private record Converted( + List inputs, + List outputs, + Map requirements, + long energy, + int processTime) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/extendedcrafting/ExtendedCraftingCompressorRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/extendedcrafting/ExtendedCraftingCompressorRecipeAdapter.java new file mode 100644 index 00000000..f68073e2 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/extendedcrafting/ExtendedCraftingCompressorRecipeAdapter.java @@ -0,0 +1,188 @@ +package com.sorrowmist.useless.content.recipe.adapters.extendedcrafting; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.blakebr0.extendedcrafting.api.crafting.ICompressorRecipe; +import com.blakebr0.extendedcrafting.init.ModBlocks; +import com.blakebr0.extendedcrafting.init.ModRecipeTypes; +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.Set; + +/** Converts Extended Crafting compressor recipes, retaining the catalyst as a mold requirement. */ +public class ExtendedCraftingCompressorRecipeAdapter implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + + @Override + public Class getRecipeClass() { + return ICompressorRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(ModBlocks.COMPRESSOR.get()); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + Converted converted = convertData(holder.value()); + if (converted == null) { + RateLimitedLogger.warn(LOGGER, "Skipping invalid Extended Crafting compressor recipe: {}", holder.id()); + return List.of(); + } + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + converted.inputs(), + List.of(), + List.of(), + converted.outputs(), + List.of(), + List.of(), + converted.energy(), + converted.processTime(), + Ingredient.EMPTY, + 0, + converted.molds(), + AlloyFurnaceMode.NORMAL + )); + } + + @Override + public List> findMatchingRecipes( + Level level, + Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || mergedInputs == null || mergedInputs.isEmpty() || !matchesMold(mold)) { + return List.of(); + } + List> matches = new ArrayList<>(); + RecipeManager recipeManager = level.getRecipeManager(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, ModRecipeTypes.COMPRESSOR.get())) { + Converted converted = convertData(holder.value()); + if (converted != null && AdapterUtils.matchesRequired(mergedInputs, converted.requirements())) { + matches.add(holder); + } + } + return matches; + } + + @Nullable + private static Converted convertData(@Nullable ICompressorRecipe source) { + if (source == null) { + return null; + } + List sourceIngredients = source.getIngredients(); + if (sourceIngredients == null || sourceIngredients.isEmpty()) { + return null; + } + + Ingredient countedInput = sourceIngredients.get(0); + if (countedInput == null || countedInput.isEmpty()) { + return null; + } + int inputCount; + try { + inputCount = source.getInputCount(); + } catch (RuntimeException exception) { + return null; + } + if (inputCount <= 0) { + return null; + } + + // IngredientWithCount already checks the stack count in test(). Keep its + // quantity as the CountedIngredient amount and unwrap only the item + // candidates, otherwise the count would be applied twice. + Ingredient input = unwrapCountIngredient(countedInput); + if (input == null || input.isEmpty()) { + return null; + } + + Ingredient catalyst = source.getCatalyst(); + if (catalyst == null || catalyst.isEmpty()) { + return null; + } + long powerCost = source.getPowerCost(); + long powerRate = source.getPowerRate(); + OptionalInt processTime = ExtendedCraftingAdapterUtils.powerProcessTime(powerCost, powerRate); + if (processTime.isEmpty()) { + return null; + } + + ItemStack result = ExtendedCraftingAdapterUtils.copyResult(source); + if (result.isEmpty() || result.getCount() <= 0) { + return null; + } + + Map requirements = new LinkedHashMap<>(); + AdapterUtils.mergeIngredient(requirements, input, inputCount); + + Optional> remainders = ExtendedCraftingAdapterUtils.deterministicRemainders( + List.of(input, catalyst), + Set.of(1), + stacks -> com.sorrowmist.useless.compat.forge1201.recipe.CraftingInputs.of(2, 1, stacks), + source::getRemainingItems + ); + if (remainders.isEmpty()) { + return null; + } + + List outputs = new ArrayList<>(); + if (!ExtendedCraftingAdapterUtils.mergeOutput(outputs, result)) { + return null; + } + for (ItemStack remainder : remainders.get()) { + if (!ExtendedCraftingAdapterUtils.mergeOutput(outputs, remainder)) { + return null; + } + } + + List counted = ExtendedCraftingAdapterUtils.countedIngredients(requirements); + List molds = List.of( + AdapterUtils.toMoldIngredient(new ItemStack(ModBlocks.COMPRESSOR.get())), + catalyst); + return new Converted(counted, outputs, requirements, molds, powerCost, processTime.getAsInt()); + } + + @Nullable + private static Ingredient unwrapCountIngredient(@Nullable Ingredient ingredient) { + if (ingredient == null || ingredient.isEmpty()) { + return null; + } + return ingredient; // Native compressor keeps quantity in getInputCount(). + } + + private record Converted( + List inputs, + List outputs, + Map requirements, + List molds, + long energy, + int processTime) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/extendedcrafting/ExtendedCraftingEnderCrafterRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/extendedcrafting/ExtendedCraftingEnderCrafterRecipeAdapter.java new file mode 100644 index 00000000..aa3fe846 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/extendedcrafting/ExtendedCraftingEnderCrafterRecipeAdapter.java @@ -0,0 +1,153 @@ +package com.sorrowmist.useless.content.recipe.adapters.extendedcrafting; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.blakebr0.extendedcrafting.api.crafting.IEnderCrafterRecipe; +import com.blakebr0.extendedcrafting.init.ModBlocks; +import com.blakebr0.extendedcrafting.init.ModRecipeTypes; +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; + +/** Converts Ender Crafter recipes; source crafting time is expressed in seconds. */ +public class ExtendedCraftingEnderCrafterRecipeAdapter implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + + @Override + public Class getRecipeClass() { + return IEnderCrafterRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(ModBlocks.ENDER_CRAFTER.get()); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + Converted converted = convertData(holder.value()); + if (converted == null) { + RateLimitedLogger.warn(LOGGER, "Skipping invalid Extended Crafting ender crafter recipe: {}", holder.id()); + return List.of(); + } + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + converted.inputs(), + List.of(), + converted.outputs(), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + converted.processTime(), + Ingredient.EMPTY, + 0, + AdapterUtils.toMoldIngredient(getMoldItem()), + AlloyFurnaceMode.NORMAL + )); + } + + @Override + public List> findMatchingRecipes( + Level level, + Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || mergedInputs == null || mergedInputs.isEmpty() || !matchesMold(mold)) { + return List.of(); + } + List> matches = new ArrayList<>(); + RecipeManager recipeManager = level.getRecipeManager(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, ModRecipeTypes.ENDER_CRAFTER.get())) { + Converted converted = convertData(holder.value()); + if (converted != null && AdapterUtils.matchesRequired(mergedInputs, converted.requirements())) { + matches.add(holder); + } + } + return matches; + } + + @Nullable + private static Converted convertData(@Nullable IEnderCrafterRecipe source) { + if (source == null) { + return null; + } + List ingredients = source.getIngredients(); + if (ingredients == null || ingredients.isEmpty()) { + return null; + } + + Map requirements = new LinkedHashMap<>(); + if (!ExtendedCraftingAdapterUtils.mergeIngredients(requirements, ingredients)) { + return null; + } + + OptionalInt processTime; + try { + processTime = ExtendedCraftingAdapterUtils.secondsToTicks(source.getCraftingTime()); + } catch (RuntimeException exception) { + return null; + } + if (processTime.isEmpty()) { + return null; + } + + ItemStack result = ExtendedCraftingAdapterUtils.copyResult(source); + if (result.isEmpty() || result.getCount() <= 0) { + return null; + } + + Optional> remainders = ExtendedCraftingAdapterUtils.deterministicRemainders( + ingredients, + java.util.Set.of(), + stacks -> { + int[] dimensions = ExtendedCraftingAdapterUtils.gridDimensions(source, stacks.size()); + return com.sorrowmist.useless.compat.forge1201.recipe.CraftingInputs.of(dimensions[0], dimensions[1], stacks); + }, + source::getRemainingItems + ); + if (remainders.isEmpty()) { + return null; + } + + List outputs = new ArrayList<>(); + if (!ExtendedCraftingAdapterUtils.mergeOutput(outputs, result)) { + return null; + } + for (ItemStack remainder : remainders.get()) { + if (!ExtendedCraftingAdapterUtils.mergeOutput(outputs, remainder)) { + return null; + } + } + + List counted = ExtendedCraftingAdapterUtils.countedIngredients(requirements); + return new Converted(counted, outputs, requirements, processTime.getAsInt()); + } + + private record Converted( + List inputs, + List outputs, + Map requirements, + int processTime) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/extendedcrafting/ExtendedCraftingFluxCrafterRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/extendedcrafting/ExtendedCraftingFluxCrafterRecipeAdapter.java new file mode 100644 index 00000000..8d5453ef --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/extendedcrafting/ExtendedCraftingFluxCrafterRecipeAdapter.java @@ -0,0 +1,149 @@ +package com.sorrowmist.useless.content.recipe.adapters.extendedcrafting; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.blakebr0.extendedcrafting.api.crafting.IFluxCrafterRecipe; +import com.blakebr0.extendedcrafting.init.ModBlocks; +import com.blakebr0.extendedcrafting.init.ModRecipeTypes; +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; + +/** Converts Flux Crafter recipes while preserving their power rate/time ratio. */ +public class ExtendedCraftingFluxCrafterRecipeAdapter implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + + @Override + public Class getRecipeClass() { + return IFluxCrafterRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(ModBlocks.FLUX_CRAFTER.get()); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + Converted converted = convertData(holder.value()); + if (converted == null) { + RateLimitedLogger.warn(LOGGER, "Skipping invalid Extended Crafting flux crafter recipe: {}", holder.id()); + return List.of(); + } + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + converted.inputs(), + List.of(), + converted.outputs(), + List.of(), + converted.energy(), + converted.processTime(), + Ingredient.EMPTY, + 0, + AdapterUtils.toMoldIngredient(getMoldItem()), + AlloyFurnaceMode.NORMAL + )); + } + + @Override + public List> findMatchingRecipes( + Level level, + Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || mergedInputs == null || mergedInputs.isEmpty() || !matchesMold(mold)) { + return List.of(); + } + List> matches = new ArrayList<>(); + RecipeManager recipeManager = level.getRecipeManager(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, ModRecipeTypes.FLUX_CRAFTER.get())) { + Converted converted = convertData(holder.value()); + if (converted != null && AdapterUtils.matchesRequired(mergedInputs, converted.requirements())) { + matches.add(holder); + } + } + return matches; + } + + @Nullable + private static Converted convertData(@Nullable IFluxCrafterRecipe source) { + if (source == null) { + return null; + } + List ingredients = source.getIngredients(); + if (ingredients == null || ingredients.isEmpty()) { + return null; + } + + Map requirements = new LinkedHashMap<>(); + if (!ExtendedCraftingAdapterUtils.mergeIngredients(requirements, ingredients)) { + return null; + } + + long powerRequired = source.getPowerRequired(); + OptionalInt processTime = ExtendedCraftingAdapterUtils.powerProcessTime(powerRequired, source.getPowerRate()); + if (processTime.isEmpty()) { + return null; + } + ItemStack result = ExtendedCraftingAdapterUtils.copyResult(source); + if (result.isEmpty() || result.getCount() <= 0) { + return null; + } + + Optional> remainders = ExtendedCraftingAdapterUtils.deterministicRemainders( + ingredients, + java.util.Set.of(), + stacks -> { + int[] dimensions = ExtendedCraftingAdapterUtils.gridDimensions(source, stacks.size()); + return com.sorrowmist.useless.compat.forge1201.recipe.CraftingInputs.of(dimensions[0], dimensions[1], stacks); + }, + source::getRemainingItems + ); + if (remainders.isEmpty()) { + return null; + } + + List outputs = new ArrayList<>(); + if (!ExtendedCraftingAdapterUtils.mergeOutput(outputs, result)) { + return null; + } + for (ItemStack remainder : remainders.get()) { + if (!ExtendedCraftingAdapterUtils.mergeOutput(outputs, remainder)) { + return null; + } + } + + List counted = ExtendedCraftingAdapterUtils.countedIngredients(requirements); + return new Converted(counted, outputs, requirements, powerRequired, processTime.getAsInt()); + } + + private record Converted( + List inputs, + List outputs, + Map requirements, + long energy, + int processTime) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/extendedcrafting/ExtendedCraftingTableRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/extendedcrafting/ExtendedCraftingTableRecipeAdapter.java new file mode 100644 index 00000000..83935907 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/extendedcrafting/ExtendedCraftingTableRecipeAdapter.java @@ -0,0 +1,218 @@ +package com.sorrowmist.useless.content.recipe.adapters.extendedcrafting; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.blakebr0.extendedcrafting.api.crafting.ITableRecipe; +import com.blakebr0.extendedcrafting.init.ModBlocks; +import com.blakebr0.extendedcrafting.init.ModRecipeTypes; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** Converts the Extended Crafting tables, including the pack's epic 11x11 table (tier 1-5). */ +public class ExtendedCraftingTableRecipeAdapter implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + + @Override + public Class getRecipeClass() { + return ITableRecipe.class; + } + + /** A table adapter serves five possible molds, so it uses the fallback registry. */ + @Override + @Nullable + public ItemStack getMoldItem() { + return null; + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + if (mold == null || mold.isEmpty()) { + return false; + } + return isTableItem(mold, 1) || isTableItem(mold, 2) + || isTableItem(mold, 3) || isTableItem(mold, 4) || isTableItem(mold, 5); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + Converted converted = convertData(holder.value()); + if (converted == null) { + RateLimitedLogger.warn(LOGGER, "Skipping invalid Extended Crafting table recipe: {}", holder.id()); + return List.of(); + } + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + converted.inputs(), + List.of(), + converted.outputs(), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + AdapterUtils.DEFAULT_PROCESS_TIME, + Ingredient.EMPTY, + 0, + AdapterUtils.toMoldIngredient(converted.mold()), + AlloyFurnaceMode.NORMAL + )); + } + + @Override + public List> findMatchingRecipes( + Level level, + Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || mergedInputs == null || mergedInputs.isEmpty() || !matchesMold(mold)) { + return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, ModRecipeTypes.TABLE.get())) { + Converted converted = convertData(holder.value()); + if (converted != null && converted.mold().getItem() == mold.getItem() + && AdapterUtils.matchesRequired(mergedInputs, converted.requirements())) { + matches.add(holder); + } + } + return matches; + } + + @Nullable + private static Converted convertData(@Nullable ITableRecipe source) { + if (source == null) { + return null; + } + + final int tier; + try { + tier = source.getTier(); + } catch (RuntimeException exception) { + return null; + } + if (tier < 1 || tier > 5) { + return null; + } + + List ingredients = source.getIngredients(); + if (ingredients == null || ingredients.isEmpty() + || ingredients.size() > tableCapacity(tier)) { + return null; + } + Map requirements = new LinkedHashMap<>(); + if (!ExtendedCraftingAdapterUtils.mergeIngredients(requirements, ingredients)) { + return null; + } + + ItemStack result = ExtendedCraftingAdapterUtils.copyResult(source); + if (result.isEmpty() || result.getCount() <= 0) { + return null; + } + + Optional> remainders = tableRemainders(source, ingredients); + if (remainders.isEmpty()) { + return null; + } + + List outputs = new ArrayList<>(); + if (!ExtendedCraftingAdapterUtils.mergeOutput(outputs, result)) { + return null; + } + for (ItemStack remainder : remainders.get()) { + if (!ExtendedCraftingAdapterUtils.mergeOutput(outputs, remainder)) { + return null; + } + } + + List counted = ExtendedCraftingAdapterUtils.countedIngredients(requirements); + return new Converted(counted, outputs, requirements, tableMold(tier)); + } + + static Optional> tableRemainders(ITableRecipe source, List ingredients) { + // Only these exact native classes are known to inherit Cucumber's slot-local fallback. + // KubeJS transformers and third-party subclasses retain the complete variant validation. + Class type = source.getClass(); + if (type == com.blakebr0.extendedcrafting.crafting.recipe.ShapedTableRecipe.class + || type == com.blakebr0.extendedcrafting.crafting.recipe.ShapelessTableRecipe.class) { + try { + var transformer = type.getDeclaredField("transformer"); + transformer.setAccessible(true); + if (transformer.get(source) == null) + return ExtendedCraftingAdapterUtils.independentItemRemainders(ingredients); + } catch (ReflectiveOperationException | RuntimeException ignored) { + // Unknown native versions must use the bounded full resolver below. + } + } + int side = source.getTier() * 2 + 1; + int[] shape = ExtendedCraftingAdapterUtils.gridDimensions(source, ingredients.size()); + List grid = new ArrayList<>(java.util.Collections.nCopies(side * side, Ingredient.EMPTY)); + for (int index = 0; index < ingredients.size(); index++) { + int slot = source instanceof com.blakebr0.extendedcrafting.crafting.recipe.ShapedTableRecipe + ? (index / shape[0]) * side + index % shape[0] : index; + if (slot >= grid.size()) return Optional.empty(); + grid.set(slot, ingredients.get(index)); + } + return ExtendedCraftingAdapterUtils.deterministicRemainders(grid, Set.of(), + stacks -> com.sorrowmist.useless.compat.forge1201.recipe.CraftingInputs.of(side, side, stacks), + source::getRemainingItems); + } + + private static ItemStack tableMold(int tier) { + return switch (tier) { + case 1 -> new ItemStack(ModBlocks.BASIC_TABLE.get()); + case 2 -> new ItemStack(ModBlocks.ADVANCED_TABLE.get()); + case 3 -> new ItemStack(ModBlocks.ELITE_TABLE.get()); + case 4 -> new ItemStack(ModBlocks.ULTIMATE_TABLE.get()); + // The unofficial build registers this table; the official API has no EPIC_TABLE field. + case 5 -> net.minecraft.core.registries.BuiltInRegistries.BLOCK + .getOptional(new net.minecraft.resources.ResourceLocation("extendedcrafting", "epic_table")) + .map(ItemStack::new).orElse(ItemStack.EMPTY); + default -> ItemStack.EMPTY; + }; + } + + static int tableCapacity(int tier) { + return switch (tier) { + case 1 -> 9; + case 2 -> 25; + case 3 -> 49; + case 4 -> 81; + case 5 -> 121; + default -> 0; + }; + } + + private static boolean isTableItem(ItemStack stack, int tier) { + ItemStack mold = tableMold(tier); + return !mold.isEmpty() && stack.getItem() == mold.getItem(); + } + + private record Converted( + List inputs, + List outputs, + Map requirements, + ItemStack mold) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/extremereactors/ExtremeReactorsRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/extremereactors/ExtremeReactorsRecipeAdapter.java new file mode 100644 index 00000000..7afdb89f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/extremereactors/ExtremeReactorsRecipeAdapter.java @@ -0,0 +1,511 @@ +package com.sorrowmist.useless.content.recipe.adapters.extremereactors; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.FluidIngredientAllocator; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.ItemIngredientAllocator; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import it.zerono.mods.extremereactors.api.IMapping; +import it.zerono.mods.extremereactors.api.reactor.Reactant; +import it.zerono.mods.extremereactors.api.reactor.ReactantMappingsRegistry; +import it.zerono.mods.extremereactors.api.reactor.ReactantType; +import it.zerono.mods.extremereactors.api.reactor.Reaction; +import it.zerono.mods.extremereactors.api.reactor.ReactionsRegistry; +import it.zerono.mods.extremereactors.config.Config; +import it.zerono.mods.extremereactors.gamecontent.multiblock.fluidizer.recipe.FluidizerFluidMixingRecipe; +import it.zerono.mods.extremereactors.gamecontent.multiblock.fluidizer.recipe.FluidizerSolidMixingRecipe; +import it.zerono.mods.extremereactors.gamecontent.multiblock.fluidizer.recipe.FluidizerSolidRecipe; +import it.zerono.mods.extremereactors.gamecontent.multiblock.fluidizer.recipe.IFluidizerRecipe; +import it.zerono.mods.extremereactors.gamecontent.multiblock.reprocessor.recipe.ReprocessorRecipe; +import it.zerono.mods.zerocore.lib.recipe.ModRecipe; +import it.zerono.mods.zerocore.lib.recipe.ingredient.FluidStackRecipeIngredient; +import it.zerono.mods.zerocore.lib.recipe.ingredient.ItemStackRecipeIngredient; +import it.zerono.mods.zerocore.lib.recipe.result.FluidStackRecipeResult; +import it.zerono.mods.zerocore.lib.recipe.result.ItemStackRecipeResult; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluid; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Converts Extreme Reactors recipes and fuel reactions to alloy-furnace recipes. */ +public final class ExtremeReactorsRecipeAdapter implements IRecipeAdapter { + private static final String MOD_ID = "bigreactors"; + private static final int FLUIDIZER_TICK_INTERVAL = 10; + private static final int REPROCESSOR_TICK_INTERVAL = 5; + private static final int REPROCESSOR_REQUIRED_TICKS = 40; + private static final int REPROCESSOR_ENERGY_PER_TICK = 25; + private static final List MOLD_IDS = List.of( + "fluidizercontroller", + "reprocessorcontroller", + "basic_reactorcontroller", + "reinforced_reactorcontroller"); + private volatile List> generatedRecipes; + + @Override + public String sourceId() { + return RecipeSourceIds.BIG_REACTORS; + } + + @Override + public Class getRecipeClass() { + return ModRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return null; + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + if (mold == null || mold.isEmpty()) return false; + for (String moldId : MOLD_IDS) { + Item item = registeredItem(moldId); + if (item != null && mold.is(item)) return true; + } + return false; + } + + @Override + public List> getGeneratedRecipes(Level level) { + List> cached = generatedRecipes; + if (cached != null) return cached; + synchronized (this) { + if (generatedRecipes == null) { + generatedRecipes = createGeneratedRecipes(); + } + return generatedRecipes; + } + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) return List.of(); + + ModRecipe source = holder.value(); + if (source instanceof ExtremeReactorsSyntheticRecipe synthetic) { + return synthetic.convertedRecipe() == null + ? List.of() : List.of(synthetic.convertedRecipe()); + } + if (source instanceof FluidizerSolidRecipe recipe) { + return singleton(convertSolidFluidizer(holder, recipe)); + } + if (source instanceof FluidizerSolidMixingRecipe recipe) { + return singleton(convertSolidMixingFluidizer(holder, recipe)); + } + if (source instanceof FluidizerFluidMixingRecipe recipe) { + return singleton(convertFluidMixingFluidizer(holder, recipe)); + } + if (source instanceof ReprocessorRecipe recipe) { + return singleton(convertReprocessor(holder, recipe)); + } + return List.of(); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + return findMatchingRecipes(level, mergedInputs, mergedFluids, Map.of(), mold, List.of()); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, + Map mergedKeys, + @Nullable ItemStack mold, List actualInputs) { + if (level == null || !matchesMold(mold)) return List.of(); + + List> matches = new ArrayList<>(); + for (RecipeHolder rawHolder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getRecipes(level.getRecipeManager())) { + if (!(rawHolder.value() instanceof ModRecipe source) + || source instanceof ExtremeReactorsSyntheticRecipe + || !isSupportedSource(source)) { + continue; + } + @SuppressWarnings("unchecked") + RecipeHolder holder = (RecipeHolder) rawHolder; + if (matchesConverted(holder, level, mergedInputs, mergedFluids, mold, actualInputs)) { + matches.add(holder); + } + } + for (RecipeHolder holder : getGeneratedRecipes(level)) { + if (matchesConverted(holder, level, mergedInputs, mergedFluids, mold, actualInputs)) { + matches.add(holder); + } + } + return matches; + } + + private static boolean isSupportedSource(ModRecipe source) { + return source instanceof FluidizerSolidRecipe + || source instanceof FluidizerSolidMixingRecipe + || source instanceof FluidizerFluidMixingRecipe + || source instanceof ReprocessorRecipe; + } + + private boolean matchesConverted( + RecipeHolder holder, Level level, + Map mergedInputs, Map mergedFluids, + @Nullable ItemStack mold, List actualInputs) { + for (AdvancedAlloyFurnaceRecipe recipe : convertAll(holder, level)) { + boolean itemMatch = actualInputs != null && !actualInputs.isEmpty() + ? ItemIngredientAllocator.matches(recipe.inputs(), actualInputs, 1L) + : matchesMergedItems(recipe.inputs(), mergedInputs); + if (itemMatch + && FluidIngredientAllocator.matchesLong(recipe.inputFluids(), mergedFluids, 1L) + && recipe.molds().size() == 1 + && AdapterUtils.matchesMold(recipe.mold(), mold)) { + return true; + } + } + return false; + } + + private static boolean matchesMergedItems( + List requirements, Map mergedInputs) { + Map required = new LinkedHashMap<>(); + for (CountedIngredient requirement : requirements) { + AdapterUtils.mergeIngredient(required, requirement.ingredient(), requirement.count()); + } + return ItemIngredientAllocator.matches( + mergedInputs == null ? Map.of() : mergedInputs, required); + } + + @Nullable + private static AdvancedAlloyFurnaceRecipe convertSolidFluidizer( + RecipeHolder holder, FluidizerSolidRecipe source) { + CountedIngredient input = itemInput(source.getIngredient()); + FluidStackRecipeResult result = source.getResult(); + FluidStack output = result == null ? FluidStack.EMPTY : result.getResult(); + if (input == null || output == null || output.isEmpty()) return null; + return createRecipe(holder.id(), List.of(input), List.of(), List.of(), List.of(output), + source, fluidizerMold(), fluidizerEnergy(source), fluidizerProcessTime(source)); + } + + @Nullable + private static AdvancedAlloyFurnaceRecipe convertSolidMixingFluidizer( + RecipeHolder holder, FluidizerSolidMixingRecipe source) { + CountedIngredient first = itemInput(source.getIngredient1()); + CountedIngredient second = itemInput(source.getIngredient2()); + FluidStackRecipeResult result = source.getResult(); + FluidStack output = result == null ? FluidStack.EMPTY : result.getResult(); + if (first == null || second == null || output == null || output.isEmpty()) return null; + return createRecipe(holder.id(), List.of(first, second), List.of(), List.of(), List.of(output), + source, fluidizerMold(), fluidizerEnergy(source), fluidizerProcessTime(source)); + } + + @Nullable + private static AdvancedAlloyFurnaceRecipe convertFluidMixingFluidizer( + RecipeHolder holder, FluidizerFluidMixingRecipe source) { + SizedFluidIngredient first = fluidInput(source.getIngredient1()); + SizedFluidIngredient second = fluidInput(source.getIngredient2()); + FluidStackRecipeResult result = source.getResult(); + FluidStack output = result == null ? FluidStack.EMPTY : result.getResult(); + if (first == null || second == null || output == null || output.isEmpty()) return null; + return createRecipe(holder.id(), List.of(), List.of(first, second), List.of(), List.of(output), + source, fluidizerMold(), fluidizerEnergy(source), fluidizerProcessTime(source)); + } + + @Nullable + private static AdvancedAlloyFurnaceRecipe convertReprocessor( + RecipeHolder holder, ReprocessorRecipe source) { + CountedIngredient waste = itemInput(source.getIngredient1()); + SizedFluidIngredient fluid = fluidInput(source.getIngredient2()); + ItemStackRecipeResult result = source.getResult(); + ItemStack output = result == null ? ItemStack.EMPTY : result.getResult(); + if (waste == null || fluid == null || output == null || output.isEmpty()) return null; + + long energy = (long) REPROCESSOR_ENERGY_PER_TICK * REPROCESSOR_REQUIRED_TICKS; + int processTime = REPROCESSOR_TICK_INTERVAL * REPROCESSOR_REQUIRED_TICKS; + return createRecipe(holder.id(), List.of(waste), List.of(fluid), List.of(output), List.of(), + null, reprocessorMold(), energy, processTime); + } + + private static AdvancedAlloyFurnaceRecipe createRecipe( + ResourceLocation sourceId, + List inputs, + List inputFluids, + List outputs, + List outputFluids, + @Nullable IFluidizerRecipe fluidizerSource, + Ingredient mold, + long energy, + int processTime) { + if (mold == null || mold.isEmpty() || energy < 0 || processTime <= 0) return null; + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(sourceId), inputs, inputFluids, List.of(), outputs, + outputFluids, List.of(), energy, processTime, Ingredient.EMPTY, 0, + List.of(mold), AlloyFurnaceMode.NORMAL); + } + + private static long fluidizerEnergy(IFluidizerRecipe source) { + try { + long perTick = Config.COMMON.fluidizer.energyPerRecipeTick.get(); + return Math.multiplyExact( + Math.multiplyExact(perTick, source.getEnergyUsageMultiplier()), + source.getRecipeType().getTicks()); + } catch (ArithmeticException exception) { + return -1L; + } + } + + private static int fluidizerProcessTime(IFluidizerRecipe source) { + long processTime = (long) source.getRecipeType().getTicks() * FLUIDIZER_TICK_INTERVAL; + return processTime > 0 && processTime <= Integer.MAX_VALUE ? (int) processTime : -1; + } + + @Nullable + private static CountedIngredient itemInput(ItemStackRecipeIngredient source) { + if (source == null || source.isEmpty() || source.asVanillaIngredients().isEmpty()) return null; + List matching = source.getMatchingElements(); + if (matching.isEmpty()) return null; + long amount = matching.get(0).getCount(); + return amount > 0 ? new CountedIngredient(source.asVanillaIngredients().get(0), amount) : null; + } + + @Nullable + private static SizedFluidIngredient fluidInput(FluidStackRecipeIngredient source) { + if (source == null || source.isEmpty()) return null; + List matching = source.getMatchingElements(); + return matching.isEmpty() ? null : AdapterUtils.toSizedFluidIngredient(matching.get(0)); + } + + private static Ingredient fluidizerMold() { + return mold("fluidizercontroller"); + } + + private static Ingredient reprocessorMold() { + return mold("reprocessorcontroller"); + } + + private static Ingredient reactorMold() { + return mold("basic_reactorcontroller", "reinforced_reactorcontroller"); + } + + private static Ingredient mold(String... ids) { + List stacks = new ArrayList<>(); + for (String id : ids) { + Item item = registeredItem(id); + if (item != null) stacks.add(item.getDefaultInstance()); + } + return stacks.isEmpty() ? Ingredient.EMPTY : Ingredient.of(stacks.toArray(ItemStack[]::new)); + } + + @Nullable + private static Item registeredItem(String path) { + return BuiltInRegistries.ITEM.getOptional(id(path)).orElse(null); + } + + private static ResourceLocation id(String path) { + return new ResourceLocation(MOD_ID, path); + } + + private List> createGeneratedRecipes() { + // ReactionsRegistry 底层是 HashMap,getReactions() 的迭代顺序由 Reactant 的哈希决定; + // 而 Reactant.hashCode() 含 ReactantType 枚举,枚举沿用 Object 的身份哈希,跨 JVM 重启 + // 并不保证一致。若把该顺序编进配方 id,重启后同一个反应会拿到不同的 id,玩家已编码的 + // 万象样板就会因 id 失配而解析失败。这里先按反应自身的名字排序,让下面的 reactionIndex + // 只由数据内容决定,从而跨会话稳定。 + List reactions = new ArrayList<>(ReactionsRegistry.getReactions()); + reactions.sort(Comparator.comparing(ExtremeReactorsRecipeAdapter::reactionKey)); + + List> result = new ArrayList<>(); + int reactionIndex = 0; + for (Reaction reaction : reactions) { + Reactant fuel = reaction.getSource(); + Reactant waste = reaction.getProduct(); + if (fuel == null || waste == null + || !fuel.getType().isFuel() || !waste.getType().isWaste()) { + continue; + } + + List inputs = physicalMappings(fuel); + List outputs = physicalMappings(waste); + for (int inputIndex = 0; inputIndex < inputs.size(); inputIndex++) { + for (int outputIndex = 0; outputIndex < outputs.size(); outputIndex++) { + ResourceLocation recipeId = new ResourceLocation( + MOD_ID, "reactor/" + slug(fuel.getName()) + "_to_" + + slug(waste.getName()) + "/" + inputIndex + "_" + outputIndex + + "_" + reactionIndex); + AdvancedAlloyFurnaceRecipe converted = convertReaction( + recipeId, reaction, inputs.get(inputIndex), outputs.get(outputIndex)); + if (converted != null) { + result.add(new RecipeHolder<>(recipeId, + new ExtremeReactorsSyntheticRecipe(converted))); + } + } + } + reactionIndex++; + } + return List.copyOf(result); + } + + private static List physicalMappings(Reactant reactant) { + List result = new ArrayList<>(); + ReactantMappingsRegistry.getToSolid(reactant) + .ifPresent(mappings -> mappings.forEach(mapping -> result.add(new PhysicalMapping(false, mapping)))); + ReactantMappingsRegistry.getToFluid(reactant) + .ifPresent(mappings -> mappings.forEach(mapping -> result.add(new PhysicalMapping(true, mapping)))); + // 映射列表按数据包注册顺序追加,同一集合在不同会话中的先后并不保证一致;而配方 id 里 + // 编入了该列表的下标,所以必须先按 tag 位置排序,下标才是稳定的。 + result.sort(Comparator.comparing(PhysicalMapping::sortKey)); + return result; + } + + /** 排序键:只依赖固/液类型与 tag 位置,与注册顺序无关。 */ + private static String reactionKey(Reaction reaction) { + Reactant fuel = reaction == null ? null : reaction.getSource(); + Reactant waste = reaction == null ? null : reaction.getProduct(); + return (fuel == null ? "" : fuel.getName()) + "->" + (waste == null ? "" : waste.getName()); + } + + @Nullable + private static AdvancedAlloyFurnaceRecipe convertReaction( + ResourceLocation recipeId, Reaction reaction, + PhysicalMapping input, PhysicalMapping output) { + Batch batch = calculateBatch(reaction, input, output); + if (batch == null) return null; + + List itemInputs = new ArrayList<>(); + List fluidInputs = new ArrayList<>(); + if (input.fluid()) { + TagKey tag = fluidTag(input); + if (tag == null) return null; + fluidInputs.add(new SizedFluidIngredient(FluidIngredient.tag(tag), batch.inputAmount())); + } else { + TagKey tag = itemTag(input); + if (tag == null) return null; + itemInputs.add(new CountedIngredient(Ingredient.of(tag), batch.inputAmount())); + } + + List itemOutputs = new ArrayList<>(); + List fluidOutputs = new ArrayList<>(); + if (output.fluid()) { + FluidStack stack = ReactantMappingsRegistry.getFluidStackFrom(fluidMapping(output), batch.outputAmount()); + if (stack == null || stack.isEmpty()) return null; + fluidOutputs.add(stack); + } else { + ItemStack stack = ReactantMappingsRegistry.getSolidStackFrom(itemMapping(output), batch.outputAmount()); + if (stack == null || stack.isEmpty()) return null; + itemOutputs.add(stack); + } + + Ingredient mold = reactorMold(); + if (mold.isEmpty()) return null; + return new AdvancedAlloyFurnaceRecipe( + recipeId, itemInputs, fluidInputs, List.of(), itemOutputs, fluidOutputs, List.of(), + AdapterUtils.DEFAULT_ENERGY, AdapterUtils.DEFAULT_PROCESS_TIME, + Ingredient.EMPTY, 0, List.of(mold), AlloyFurnaceMode.NORMAL); + } + + @Nullable + private static Batch calculateBatch(Reaction reaction, PhysicalMapping input, PhysicalMapping output) { + int reactionSource = reaction.getSourceAmount(); + int inputSource = input.mapping().getSourceAmount(); + int outputSource = output.mapping().getSourceAmount(); + if (reactionSource <= 0 || inputSource <= 0 || outputSource <= 0) return null; + + long batch = lcm(reactionSource, inputSource); + for (int attempt = 0; attempt < 4; attempt++) { + if (batch <= 0 || batch > Integer.MAX_VALUE) return null; + int wasteUnits = reaction.getProductAmount((int) batch); + if (wasteUnits > 0 && wasteUnits % outputSource == 0) { + int inputAmount = input.mapping().getProductAmount((int) batch); + int outputAmount = output.mapping().getProductAmount(wasteUnits); + if (inputAmount > 0 && outputAmount > 0) { + return new Batch(inputAmount, outputAmount); + } + } + long next = lcm(batch, outputSource); + if (next == batch) return null; + batch = next; + } + return null; + } + + @SuppressWarnings("unchecked") + private static IMapping> itemMapping(PhysicalMapping mapping) { + return (IMapping>) (IMapping) mapping.mapping(); + } + + @SuppressWarnings("unchecked") + private static IMapping> fluidMapping(PhysicalMapping mapping) { + return (IMapping>) (IMapping) mapping.mapping(); + } + + @SuppressWarnings("unchecked") + private static TagKey itemTag(PhysicalMapping mapping) { + return (TagKey) mapping.mapping().getProduct(); + } + + @SuppressWarnings("unchecked") + private static TagKey fluidTag(PhysicalMapping mapping) { + return (TagKey) mapping.mapping().getProduct(); + } + + private static long lcm(long left, long right) { + if (left <= 0 || right <= 0) return -1L; + long gcd = gcd(left, right); + if (left / gcd > Long.MAX_VALUE / right) return -1L; + return left / gcd * right; + } + + private static long gcd(long left, long right) { + while (right != 0) { + long remainder = left % right; + left = right; + right = remainder; + } + return Math.abs(left); + } + + private static String slug(String value) { + StringBuilder result = new StringBuilder(value == null ? 7 : value.length()); + if (value != null) { + for (int i = 0; i < value.length(); i++) { + char character = Character.toLowerCase(value.charAt(i)); + result.append(Character.isLetterOrDigit(character) + || character == '_' || character == '-' || character == '.' ? character : '_'); + } + } + return result.isEmpty() ? "unknown" : result.toString(); + } + + private static List singleton(@Nullable T value) { + return value == null ? List.of() : List.of(value); + } + + private record PhysicalMapping(boolean fluid, IMapping mapping) { + /** 排序键:只依赖固/液类型与 tag 位置,与数据包注册顺序无关。 */ + private String sortKey() { + Object product = mapping == null ? null : mapping.getProduct(); + String location = product instanceof TagKey tag ? tag.location().toString() : ""; + return (fluid ? "1" : "0") + location; + } + } + + private record Batch(int inputAmount, int outputAmount) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/extremereactors/ExtremeReactorsSyntheticRecipe.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/extremereactors/ExtremeReactorsSyntheticRecipe.java new file mode 100644 index 00000000..53fd88d6 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/extremereactors/ExtremeReactorsSyntheticRecipe.java @@ -0,0 +1,17 @@ +package com.sorrowmist.useless.content.recipe.adapters.extremereactors; + +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import it.zerono.mods.zerocore.lib.recipe.ModRecipe; + +/** Holder payload for Extreme Reactors mappings that are not RecipeManager recipes. */ +public final class ExtremeReactorsSyntheticRecipe extends ModRecipe { + private final AdvancedAlloyFurnaceRecipe convertedRecipe; + + public ExtremeReactorsSyntheticRecipe(AdvancedAlloyFurnaceRecipe convertedRecipe) { + this.convertedRecipe = convertedRecipe; + } + + public AdvancedAlloyFurnaceRecipe convertedRecipe() { + return convertedRecipe; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/fluxnetworks/FluxNetworksRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/fluxnetworks/FluxNetworksRecipeAdapter.java new file mode 100644 index 00000000..d65d60e6 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/fluxnetworks/FluxNetworksRecipeAdapter.java @@ -0,0 +1,161 @@ +package com.sorrowmist.useless.content.recipe.adapters.fluxnetworks; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.ItemIngredientAllocator; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import sonar.fluxnetworks.register.RegistryItems; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * 把 Flux Networks 的通量粉尘转化机制转换为合金炉配方。 + * + *

Flux Networks 并未把通量粉尘做成数据包配方:它由 {@code EventHandler.onPlayerInteract} + * 硬编码实现——左键点击黑曜石、其下方第 2 格为基岩或通量块、消耗上方掉落的红石粉, + * 按 1:1 产出通量粉尘(单次上限 512)。因此本适配器走 {@code getGeneratedRecipes} + * 合成路径,用黑曜石充当合金炉的模具,把「红石粉 → 通量粉尘」这一转化搬进合金炉。

+ */ +public final class FluxNetworksRecipeAdapter + implements IRecipeAdapter { + + /** 合金炉里每一次转化消耗的红石粉数量。 */ + private static final long REDSTONE_PER_CRAFT = 1L; + + /** 模具槽中的黑曜石只用于判定,不参与消耗。 */ + private static final ItemStack MOLD = new ItemStack(Blocks.OBSIDIAN); + + private static final ResourceLocation RECIPE_ID = new ResourceLocation( + RecipeSourceIds.FLUX_NETWORKS, "compat/flux_dust"); + + @Override + public String sourceId() { + return RecipeSourceIds.FLUX_NETWORKS; + } + + @Override + public Class getRecipeClass() { + return FluxNetworksSyntheticRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return MOLD.copy(); + } + + @Override + public List> getGeneratedRecipes(Level level) { + if (level == null) return List.of(); + + AdvancedAlloyFurnaceRecipe recipe = buildRecipe(); + if (recipe == null) return List.of(); + return List.of(holder(recipe)); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null || holder.value().convertedRecipe() == null) { + return List.of(); + } + return List.of(holder.value().convertedRecipe()); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + return findMatchingRecipes(level, mergedInputs, mergedFluids, Map.of(), mold, List.of()); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, + Map mergedKeys, + @Nullable ItemStack mold, List actualInputs) { + if (level == null || !matchesMold(mold)) return List.of(); + + List> result = new ArrayList<>(); + for (RecipeHolder holder : getGeneratedRecipes(level)) { + AdvancedAlloyFurnaceRecipe recipe = holder.value().convertedRecipe(); + if (recipe == null || recipe.molds().size() != 1 || !AdapterUtils.matchesMold(recipe.mold(), mold)) { + continue; + } + + boolean matches = actualInputs != null && !actualInputs.isEmpty() + ? ItemIngredientAllocator.matches( + recipe.inputs(), actualInputs, mergedKeyInputs(mergedKeys), 1L) + : matchesMergedInputs(recipe.inputs(), mergedInputs); + if (matches) result.add(holder); + } + return List.copyOf(result); + } + + /** 构造「红石粉 → 通量粉尘」这一条合成配方;通量粉尘物品不可用时返回 {@code null}。 */ + @Nullable + private static AdvancedAlloyFurnaceRecipe buildRecipe() { + ItemStack dust; + try { + dust = new ItemStack(RegistryItems.FLUX_DUST.get()); + } catch (RuntimeException | LinkageError ignored) { + return null; + } + if (dust.isEmpty()) return null; + + return new AdvancedAlloyFurnaceRecipe( + RECIPE_ID, + List.of(new CountedIngredient(Ingredient.of(Items.REDSTONE), REDSTONE_PER_CRAFT)), + List.of(), + List.of(), + List.of(dust), + List.of(), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + AdapterUtils.DEFAULT_PROCESS_TIME, + Ingredient.EMPTY, + 0, + List.of(AdapterUtils.toMoldIngredient(MOLD)), + AlloyFurnaceMode.NORMAL); + } + + private static RecipeHolder holder(AdvancedAlloyFurnaceRecipe recipe) { + return new RecipeHolder<>(recipe.id(), new FluxNetworksSyntheticRecipe(recipe)); + } + + private static boolean matchesMergedInputs( + List requirements, Map available) { + Map required = new LinkedHashMap<>(); + for (CountedIngredient requirement : requirements) { + AdapterUtils.mergeIngredient(required, requirement.ingredient(), requirement.count()); + } + return ItemIngredientAllocator.matches(available, required); + } + + private static List mergedKeyInputs( + Map mergedKeys) { + if (mergedKeys == null || mergedKeys.isEmpty()) return List.of(); + List result = new ArrayList<>(); + for (Map.Entry entry : mergedKeys.entrySet()) { + long amount = Math.max(0L, entry.getValue()); + if (amount > 0L) result.add(new appeng.api.stacks.GenericStack(entry.getKey(), amount)); + } + return result; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/fluxnetworks/FluxNetworksSyntheticRecipe.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/fluxnetworks/FluxNetworksSyntheticRecipe.java new file mode 100644 index 00000000..119d5335 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/fluxnetworks/FluxNetworksSyntheticRecipe.java @@ -0,0 +1,61 @@ +package com.sorrowmist.useless.content.recipe.adapters.fluxnetworks; + +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.Container; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; + +/** + * Recipe-manager payload for Flux Networks recipes generated from runtime data. + * + *

Flux Networks does not ship the flux-dust conversion as a datapack recipe; it is a hard-coded + * interaction handled by its own event handler. The adapter therefore synthesizes the alloy-furnace + * view of that mechanic and needs this wrapper to satisfy the recipe-manager contract.

+ */ +public final class FluxNetworksSyntheticRecipe implements Recipe { + private final AdvancedAlloyFurnaceRecipe convertedRecipe; + + public FluxNetworksSyntheticRecipe(AdvancedAlloyFurnaceRecipe convertedRecipe) { + this.convertedRecipe = convertedRecipe; + } + + public AdvancedAlloyFurnaceRecipe convertedRecipe() { + return convertedRecipe; + } + + @Override + public net.minecraft.resources.ResourceLocation getId() { return convertedRecipe.id(); } + + @Override + public boolean matches(Container input, Level level) { + return convertedRecipe != null && convertedRecipe.matches(input, level); + } + + @Override + public ItemStack assemble(Container input, net.minecraft.core.RegistryAccess registries) { + return convertedRecipe == null ? ItemStack.EMPTY : convertedRecipe.assemble(input, registries); + } + + @Override + public boolean canCraftInDimensions(int width, int height) { + return convertedRecipe != null && convertedRecipe.canCraftInDimensions(width, height); + } + + @Override + public ItemStack getResultItem(net.minecraft.core.RegistryAccess registries) { + return convertedRecipe == null ? ItemStack.EMPTY : convertedRecipe.getResultItem(registries); + } + + @Override + public RecipeSerializer getSerializer() { + return convertedRecipe == null ? null : convertedRecipe.getSerializer(); + } + + @Override + public RecipeType getType() { + return convertedRecipe == null ? null : convertedRecipe.getType(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/forbiddenarcanus/ForbiddenArcanusSyntheticRecipe.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/forbiddenarcanus/ForbiddenArcanusSyntheticRecipe.java new file mode 100644 index 00000000..22130ad5 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/forbiddenarcanus/ForbiddenArcanusSyntheticRecipe.java @@ -0,0 +1,55 @@ +package com.sorrowmist.useless.content.recipe.adapters.forbiddenarcanus; + +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.Container; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; + +/** Recipe wrapper for Ritual data that lives in Forbidden Arcanus' custom registry. */ +public final class ForbiddenArcanusSyntheticRecipe implements Recipe { + private final AdvancedAlloyFurnaceRecipe convertedRecipe; + + public ForbiddenArcanusSyntheticRecipe(AdvancedAlloyFurnaceRecipe convertedRecipe) { + this.convertedRecipe = convertedRecipe; + } + + public AdvancedAlloyFurnaceRecipe convertedRecipe() { + return convertedRecipe; + } + + @Override + public net.minecraft.resources.ResourceLocation getId() { return convertedRecipe.id(); } + + @Override + public boolean matches(Container input, Level level) { + return convertedRecipe != null && convertedRecipe.matches(input, level); + } + + @Override + public ItemStack assemble(Container input, net.minecraft.core.RegistryAccess registries) { + return convertedRecipe == null ? ItemStack.EMPTY : convertedRecipe.assemble(input, registries); + } + + @Override + public boolean canCraftInDimensions(int width, int height) { + return convertedRecipe != null && convertedRecipe.canCraftInDimensions(width, height); + } + + @Override + public ItemStack getResultItem(net.minecraft.core.RegistryAccess registries) { + return convertedRecipe == null ? ItemStack.EMPTY : convertedRecipe.getResultItem(registries); + } + + @Override + public RecipeSerializer getSerializer() { + return convertedRecipe == null ? null : convertedRecipe.getSerializer(); + } + + @Override + public RecipeType getType() { + return convertedRecipe == null ? null : convertedRecipe.getType(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/forbiddenarcanus/HephaestusForgeRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/forbiddenarcanus/HephaestusForgeRecipeAdapter.java new file mode 100644 index 00000000..ebdfdc1b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/forbiddenarcanus/HephaestusForgeRecipeAdapter.java @@ -0,0 +1,247 @@ +package com.sorrowmist.useless.content.recipe.adapters.forbiddenarcanus; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.stal111.forbidden_arcanus.common.block.entity.forge.ritual.Ritual; +import com.stal111.forbidden_arcanus.common.block.entity.forge.ritual.RitualInput; +import com.stal111.forbidden_arcanus.common.block.entity.forge.ritual.RitualRequirements; +import com.stal111.forbidden_arcanus.common.block.entity.forge.ritual.result.CreateItemResult; +import com.stal111.forbidden_arcanus.common.block.entity.forge.ritual.result.TransmuteInputResult; +import com.stal111.forbidden_arcanus.common.block.entity.forge.essence.EssencesDefinition; +import com.stal111.forbidden_arcanus.core.init.ModBlocks; +import com.stal111.forbidden_arcanus.core.registry.FARegistries; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Converts item-producing Forbidden Arcanus Hephaestus Forge rituals. */ +public final class HephaestusForgeRecipeAdapter implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final int MAX_RITUAL_INPUTS = 8; + + @Override + public Class getRecipeClass() { + return ForbiddenArcanusSyntheticRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(ModBlocks.HEPHAESTUS_FORGE_TIER_1.get()); + } + + @Override + public List> getGeneratedRecipes(Level level) { + if (level == null) { + return List.of(); + } + + Registry registry = level.registryAccess().registryOrThrow(FARegistries.RITUAL); + List> result = new ArrayList<>(); + for (Holder holder : registry.holders().toList()) { + ResourceLocation sourceId = holder.unwrapKey().map(key -> key.location()).orElse(null); + if (sourceId == null) { + continue; + } + + for (AdvancedAlloyFurnaceRecipe converted : convertRitual(sourceId, holder.value())) { + result.add(new RecipeHolder<>(converted.id(), new ForbiddenArcanusSyntheticRecipe(converted))); + } + } + return result; + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null || holder.value().convertedRecipe() == null) { + return List.of(); + } + return List.of(holder.value().convertedRecipe()); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold) || mergedInputs == null || mergedInputs.isEmpty()) { + return List.of(); + } + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : getGeneratedRecipes(level)) { + AdvancedAlloyFurnaceRecipe recipe = holder.value().convertedRecipe(); + if (recipe != null && AdapterUtils.matchesRequired(mergedInputs, requirements(recipe.inputs()))) { + matches.add(holder); + } + } + return matches; + } + + static List convertRitual(ResourceLocation sourceId, Ritual ritual) { + if (sourceId == null || ritual == null) { + return List.of(); + } + if (ritual.mainIngredient() == null || ritual.mainIngredient().isEmpty()) { + warn(sourceId, "has an empty main ingredient"); + return List.of(); + } + if (ritual.result() == null) { + warn(sourceId, "has no ritual result"); + return List.of(); + } + if (ritual.duration() <= 0) { + warn(sourceId, "has an invalid duration"); + return List.of(); + } + if (!(ritual.result() instanceof CreateItemResult) + && !(ritual.result() instanceof TransmuteInputResult)) { + warn(sourceId, "does not produce a supported item result"); + return List.of(); + } + + List ritualInputs = new ArrayList<>(); + long pedestalInputCount = 0L; + if (ritual.inputs() == null || ritual.inputs().isEmpty()) { + warn(sourceId, "has no pedestal inputs"); + return List.of(); + } + for (RitualInput input : ritual.inputs()) { + if (input == null || input.ingredient() == null || input.ingredient().isEmpty() || input.amount() <= 0) { + warn(sourceId, "has an invalid pedestal input"); + return List.of(); + } + pedestalInputCount = saturatingAdd(pedestalInputCount, input.amount()); + if (pedestalInputCount > MAX_RITUAL_INPUTS) { + warn(sourceId, "has more than eight pedestal inputs"); + return List.of(); + } + ritualInputs.add(new CountedIngredient(input.ingredient(), input.amount())); + } + + List mainRepresentations = concreteRepresentations(ritual.mainIngredient()); + if (mainRepresentations.isEmpty()) { + warn(sourceId, "has no enumerable main ingredient"); + return List.of(); + } + + long energy = energyFor(sourceId, ritual); + List result = new ArrayList<>(); + for (int index = 0; index < mainRepresentations.size(); index++) { + ItemStack main = mainRepresentations.get(index); + ItemStack output; + try { + output = ritual.result().getResultItem(main.copy()).copy(); + } catch (RuntimeException exception) { + warn(sourceId, "failed to resolve its result"); + return List.of(); + } + if (output.isEmpty() || output.getCount() <= 0) { + warn(sourceId, "has an empty item result"); + return List.of(); + } + + List inputs = new ArrayList<>(ritualInputs.size() + 1); + inputs.add(new CountedIngredient(Ingredient.of(main.copyWithCount(1)), 1L)); + inputs.addAll(ritualInputs); + + ResourceLocation id = variantId(sourceId, main, index); + result.add(new AdvancedAlloyFurnaceRecipe( + id, + List.copyOf(inputs), + List.of(), + List.of(output), + List.of(), + energy, + Math.max(1, ritual.duration()), + Ingredient.EMPTY, + 0, + AdapterUtils.toMoldIngredient(new ItemStack(ModBlocks.HEPHAESTUS_FORGE_TIER_1.get())), + AlloyFurnaceMode.NORMAL)); + } + return List.copyOf(result); + } + + private static long energyFor(ResourceLocation sourceId, Ritual ritual) { + try { + RitualRequirements requirements = ritual.requirements(); + EssencesDefinition essences = requirements == null ? null : requirements.essences(); + if (essences == null) { + return AdapterUtils.DEFAULT_ENERGY; + } + + long multiplier = 0L; + multiplier = saturatingAdd(multiplier, Math.max(0, essences.aureal())); + multiplier = saturatingAdd(multiplier, Math.max(0, essences.souls())); + multiplier = saturatingAdd(multiplier, Math.max(0, essences.blood())); + multiplier = saturatingAdd(multiplier, Math.max(0, essences.experience())); + return saturatingMultiply(AdapterUtils.DEFAULT_ENERGY, Math.max(1L, multiplier)); + } catch (RuntimeException exception) { + return AdapterUtils.DEFAULT_ENERGY; + } + } + + private static Map requirements(List inputs) { + Map result = new LinkedHashMap<>(); + for (CountedIngredient input : inputs) { + if (input == null || input.ingredient() == null || input.ingredient().isEmpty() || input.count() <= 0) { + continue; + } + AdapterUtils.mergeIngredient(result, input.ingredient(), input.count()); + } + return result; + } + + private static List concreteRepresentations(Ingredient ingredient) { + List result = new ArrayList<>(); + for (ItemStack stack : ingredient.getItems()) { + if (stack.isEmpty()) { + continue; + } + ItemStack copy = stack.copyWithCount(1); + if (result.stream().noneMatch(existing -> ItemStack.isSameItemSameTags(existing, copy))) { + result.add(copy); + } + } + return result; + } + + private static ResourceLocation variantId(ResourceLocation sourceId, ItemStack main, int index) { + ResourceLocation itemId = net.minecraft.core.registries.BuiltInRegistries.ITEM.getKey(main.getItem()); + String itemSuffix = itemId == null ? "unknown" : itemId.getNamespace() + "_" + itemId.getPath(); + return new ResourceLocation( + sourceId.getNamespace(), + sourceId.getPath() + "_main_" + itemSuffix.replace('/', '_') + "_" + index); + } + + private static long saturatingAdd(long left, long right) { + return right > 0L && left > Long.MAX_VALUE - right ? Long.MAX_VALUE : left + right; + } + + private static long saturatingMultiply(long left, long right) { + if (left <= 0L || right <= 0L) { + return 0L; + } + return left > Long.MAX_VALUE / right ? Long.MAX_VALUE : left * right; + } + + private static void warn(ResourceLocation sourceId, String reason) { + RateLimitedLogger.warn(LOGGER, "Skipping Forbidden Arcanus Hephaestus Forge ritual {}: {}", sourceId, reason); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/hostilenetworks/DataModelRangeIngredient.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/hostilenetworks/DataModelRangeIngredient.java new file mode 100644 index 00000000..7f0ad4e0 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/hostilenetworks/DataModelRangeIngredient.java @@ -0,0 +1,153 @@ +package com.sorrowmist.useless.content.recipe.adapters.hostilenetworks; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; + +import dev.shadowsoffire.hostilenetworks.Hostile; +import dev.shadowsoffire.hostilenetworks.data.DataModel; +import dev.shadowsoffire.hostilenetworks.data.DataModelRegistry; +import dev.shadowsoffire.hostilenetworks.data.ModelTier; +import dev.shadowsoffire.hostilenetworks.data.ModelTierRegistry; +import dev.shadowsoffire.hostilenetworks.item.DataModelItem; +import dev.shadowsoffire.placebo.reload.DynamicHolder; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.CodecIngredient; +import com.sorrowmist.useless.compat.forge1201.recipe.CodecIngredientSerializer; + + +import java.util.List; +import java.util.Objects; + +/** + * 按数据区间匹配数据模型的原料。 + * + *

品级本身就是一个数据区间:{@code ModelTierRegistry.getByData} 从高到低取第一个满足 + * {@code data >= requiredData} 的品级。所以「某个品级的模型」对应的数据范围是 + * {@code [requiredData(当前品级), requiredData(下一品级) - 1]},最高品级没有上界。 + * + *

{@code DataComponentIngredient} 只能表达精确值,无法表达区间,因此这里自建实现。 + * 序列化时只保存模型 id 与数据上下界。 + * + *

本类引用了 HNN 与 Placebo 的类型,因此只能在 HNN 存在时被加载: + * 注册入口 {@code ModIngredientTypes.DATA_MODEL_RANGE} 已经做了加载检查, + * 不要在任何无条件执行的静态初始化里触碰本类。 + */ +public final class DataModelRangeIngredient extends CodecIngredient { + + /** 最高品级没有上界时使用的哨兵值。 */ + public static final int UNBOUNDED = Integer.MAX_VALUE; + + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(instance -> + instance.group( + ResourceLocation.CODEC.fieldOf("model") + .forGetter(DataModelRangeIngredient::modelId), + Codec.INT.fieldOf("min_data") + .forGetter(DataModelRangeIngredient::minData), + Codec.INT.fieldOf("max_data") + .forGetter(DataModelRangeIngredient::maxData) + ).apply(instance, DataModelRangeIngredient::new)); + + public static final CodecIngredientSerializer TYPE = new CodecIngredientSerializer<>(CODEC.codec()); + + private final ResourceLocation modelId; + private final int minData; + private final int maxData; + + DataModelRangeIngredient(ResourceLocation modelId, int minData, int maxData) { + if (modelId == null) { + throw new IllegalArgumentException("数据模型区间原料必须指定模型 id"); + } + if (maxData < minData) { + throw new IllegalArgumentException("数据模型区间原料的上界不能小于下界"); + } + this.modelId = modelId; + this.minData = minData; + this.maxData = maxData; + } + + /** 用指定品级构造区间原料;模型或品级不可用时返回 null。 */ + static Ingredient of(DataModel model, ModelTier tier) { + if (model == null || tier == null) return null; + ResourceLocation id = DataModelRegistry.INSTANCE.getKey(model); + if (id == null) return null; + int min = Math.max(0, model.getTierData(tier)); + int max = tierUpperBound(model, tier); + if (max < min) return null; + return new DataModelRangeIngredient(id, min, max); + } + + /** 下一品级的起始数据减一即为本品级上界;已是最高品级时没有上界。 */ + private static int tierUpperBound(DataModel model, ModelTier tier) { + List tiers = java.util.Arrays.asList(ModelTier.values()); + int index = tiers.indexOf(tier); + if (index < 0 || index + 1 >= tiers.size()) return UNBOUNDED; + int next = model.getTierData(tiers.get(index + 1)); + return next <= 0 ? UNBOUNDED : next - 1; + } + + ResourceLocation modelId() { + return this.modelId; + } + + int minData() { + return this.minData; + } + + int maxData() { + return this.maxData; + } + + @Override + public boolean test(ItemStack stack) { + if (stack == null || stack.isEmpty() || !stack.is(Hostile.Items.DATA_MODEL.get())) return false; + try { + DynamicHolder stored = DataModelItem.getStoredModel(stack); + if (!stored.isBound()) return false; + if (!this.modelId.equals(DataModelRegistry.INSTANCE.getKey(stored.get()))) return false; + int data = DataModelItem.getData(stack); + return data >= this.minData && data <= this.maxData; + } catch (RuntimeException exception) { + return false; + } + } + + @Override + public ItemStack[] getItems() { + ItemStack representative = this.representative(); + return representative.isEmpty() ? new ItemStack[0] : new ItemStack[]{representative}; + } + + /** 返回一个位于区间下界的展示用模型,用于配方展示与代表性编码。 */ + private ItemStack representative() { + try { + DynamicHolder holder = DataModelRegistry.INSTANCE.holder(this.modelId); + if (!holder.isBound()) return ItemStack.EMPTY; + ItemStack stack = new ItemStack(Hostile.Items.DATA_MODEL.get()); + DataModelItem.setStoredModel(stack, holder.get()); + DataModelItem.setData(stack, this.minData); + return stack; + } catch (RuntimeException exception) { + return ItemStack.EMPTY; + } + } + + @Override + public CodecIngredientSerializer getSerializer() { return TYPE; } + + @Override + public boolean equals(Object object) { + return this == object + || object instanceof DataModelRangeIngredient other + && this.minData == other.minData + && this.maxData == other.maxData + && this.modelId.equals(other.modelId); + } + + @Override + public int hashCode() { + return Objects.hash(this.modelId, this.minData, this.maxData); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/hostilenetworks/HostileNetworksRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/hostilenetworks/HostileNetworksRecipeAdapter.java new file mode 100644 index 00000000..9b9356bb --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/hostilenetworks/HostileNetworksRecipeAdapter.java @@ -0,0 +1,476 @@ +package com.sorrowmist.useless.content.recipe.adapters.hostilenetworks; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.ExpectedOutputScaler; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.ItemIngredientAllocator; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import dev.shadowsoffire.hostilenetworks.Hostile; +import dev.shadowsoffire.hostilenetworks.HostileConfig; +import dev.shadowsoffire.hostilenetworks.data.DataModel; +import dev.shadowsoffire.hostilenetworks.data.DataModelRegistry; +import dev.shadowsoffire.hostilenetworks.data.ModelTier; +import dev.shadowsoffire.hostilenetworks.item.DataModelItem; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import com.sorrowmist.useless.compat.forge1201.recipe.DataComponentIngredient; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** Converts Hostile Neural Networks simulation and fabricator operations. */ +public final class HostileNetworksRecipeAdapter + implements IRecipeAdapter { + private static final String MOD_ID = RecipeSourceIds.HOSTILE_NETWORKS; + private static final int SIMULATION_TRAINING_TIME = 300; + private static final int SIMULATION_INFERENCE_TIME = 300; + private static final int FABRICATOR_TIME = 60; + private static final float TRAINING_COST_MULTIPLIER = 1.0F; + + @Override + public String sourceId() { + return RecipeSourceIds.HOSTILE_NETWORKS; + } + + @Override + public Class getRecipeClass() { + return HostileNetworksSyntheticRecipe.class; + } + + /** The adapter covers two machines, so it uses the fallback mold path. */ + @Override + @Nullable + public ItemStack getMoldItem() { + return null; + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + if (mold == null || mold.isEmpty()) return false; + // 数据模型整体作为模具参与匹配,具体品级交给模具原料的数据区间判定。 + return mold.is(Hostile.Items.SIM_CHAMBER.get()) + || mold.is(Hostile.Items.LOOT_FABRICATOR.get()) + || mold.is(Hostile.Items.DATA_MODEL.get()); + } + + private GenerationKey generationKey; + private List> generated = List.of(); + + private record GenerationKey(List models, List ids, + List accuracies, boolean continuous, int upgrades, int fabricatorCost) {} + + @Override + public synchronized List> getGeneratedRecipes(Level level) { + if (level == null) return List.of(); + + var models = List.copyOf(DataModelRegistry.INSTANCE.getValues()); + var ids = models.stream().map(DataModelRegistry.INSTANCE::getKey).toList(); + var accuracies = java.util.Arrays.stream(ModelTier.values()).map(ModelTier::accuracy).toList(); + var current = new GenerationKey(models, ids, accuracies, + HostileConfig.continuousAccuracy, HostileConfig.simModelUpgrade, HostileConfig.fabPowerCost); + if (current.equals(generationKey)) return generated; + + List> result = new ArrayList<>(); + for (DataModel model : models) { + addTrainingRecipes(result, model); + addInferenceRecipes(result, model); + addFabricatorRecipes(result, model); + } + generated = List.copyOf(result); + generationKey = current; + return generated; + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null || holder.value().convertedRecipe() == null) { + return List.of(); + } + return List.of(holder.value().convertedRecipe()); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + return findMatchingRecipes(level, mergedInputs, mergedFluids, Map.of(), mold, List.of()); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, + Map mergedKeys, + @Nullable ItemStack mold, List actualInputs) { + if (level == null || !matchesMold(mold)) return List.of(); + + List> result = new ArrayList<>(); + for (RecipeHolder holder : getGeneratedRecipes(level)) { + AdvancedAlloyFurnaceRecipe recipe = holder.value().convertedRecipe(); + if (recipe == null || recipe.molds().size() != 1 || !matchesMold(recipe, mold)) continue; + + boolean matches = actualInputs != null && !actualInputs.isEmpty() + ? ItemIngredientAllocator.matches(recipe.inputs(), actualInputs, mergedKeyInputs(mergedKeys), 1L) + : matchesMergedInputs(recipe.inputs(), mergedInputs); + if (matches) result.add(holder); + } + return List.copyOf(result); + } + + /** + * 按品级为模型生成升级配方。 + * + *

每个品级本身就是一个数据区间 {@code [req(T), req(next(T)) - 1]},与 + * {@code ModelTierRegistry.getByData} 的判定保持一致。升级到下一品级消耗的矩阵数固定为 + * 该品级的跨度 {@code req(next(T)) - req(T)},产出的模型数据正好落在下一品级起点。 + * + *

这样任意数据值的模型都能被唯一一条升级配方命中,不再要求数据精确等于某个阈值。 + */ + private static void addTrainingRecipes( + List> result, DataModel model) { + // The target pack disables simulation upgrades. Never synthesize a bypass recipe. + if (HostileConfig.simModelUpgrade <= 0 || model.input().isEmpty()) return; + Ingredient matrix = new com.sorrowmist.useless.compat.forge1201.recipe.TopLevelNbtIngredient(model.input()); + if (AdapterUtils.isIngredientEmpty(matrix)) return; + + List tiers = java.util.Arrays.asList(ModelTier.values()); + for (int index = 0; index + 1 < tiers.size(); index++) { + addTrainingRecipe(result, model, tiers.get(index), tiers.get(index + 1), matrix); + } + } + + /** 生成由 {@code tier} 升到 {@code nextTier} 的单条配方。 */ + private static void addTrainingRecipe( + List> result, DataModel model, + ModelTier tier, ModelTier nextTier, Ingredient matrix) { + // 真实模拟室拒绝无法模拟的品级,这些品级不存在升级配方。 + if (tier == null || nextTier == null || tier == ModelTier.FAULTY) return; + + int fromData = Math.max(0, model.getTierData(tier)); + int targetData = model.getTierData(nextTier); + int span = targetData - fromData; + if (span <= 0) return; + + Ingredient inputModel = DataModelRangeIngredient.of(model, tier); + if (inputModel == null) return; + + // 每次模拟必定产出一个基础掉落,预测掉落按该品级准确率的期望值取整。 + long predictions = 0L; + float accuracy = Math.min(1.0F, tier.accuracy()); + if (Float.isFinite(accuracy) && accuracy > 0F) { + predictions = Math.round(span * (double) accuracy); + } + + List outputs = new ArrayList<>(); + outputs.add(modelStack(model, targetData)); + if (!addOptionalOutput(outputs, model.baseDrop(), span) + || !addOptionalOutput(outputs, model.getPredictionDrop(), predictions)) { + return; + } + + long energy = trainingEnergy(model.simCost(), span, 1); + int processTime = safeProcessTime(multiply(SIMULATION_TRAINING_TIME, span)); + if (energy < 0L || processTime <= 0) return; + + ResourceLocation id = recipeId(model, "training/" + tier.name); + if (id == null) return; + + AdvancedAlloyFurnaceRecipe recipe = new AdvancedAlloyFurnaceRecipe( + id, + List.of( + new CountedIngredient(inputModel, 1L), + new CountedIngredient(matrix, span)), + List.of(), + List.of(), + outputs, + List.of(), + List.of(), + energy, + processTime, + Ingredient.EMPTY, + 0, + List.of(simChamberMold()), + AlloyFurnaceMode.NORMAL); + result.add(holder(recipe)); + } + + /** + * The target pack enables HNN 5's continuous accuracy. Every intermediate data value + * therefore has its own rate; a tier-wide predicate with the tier's base rate is wrong. + * Generate exact-data variants below the final tier, retaining an unbounded final tier. + * The immutable generation snapshot is reused until models/tier rates/config reload. + */ + private static void addInferenceRecipes( + List> result, DataModel model) { + if (model.input().isEmpty()) return; + Ingredient matrix = new com.sorrowmist.useless.compat.forge1201.recipe.TopLevelNbtIngredient(model.input()); + if (AdapterUtils.isIngredientEmpty(matrix)) return; + for (ModelTier tier : ModelTier.values()) { + if (tier == ModelTier.FAULTY) continue; + Ingredient ingredient = DataModelRangeIngredient.of(model, tier); + if (!(ingredient instanceof DataModelRangeIngredient range)) continue; + if (HostileConfig.continuousAccuracy && tier.next() != tier) { + for (long data = range.minData(); data <= range.maxData(); data++) { + int exact = (int) data; + addInferenceRecipe(result, model, tier, matrix, + new DataModelRangeIngredient(range.modelId(), exact, exact), + accuracyAt(model, tier, exact), tier.name + "/data_" + exact); + } + } else { + addInferenceRecipe(result, model, tier, matrix, range, + accuracyAt(model, tier, range.minData()), tier.name); + } + } + } + + /** Mirrors CachedModel.getAccuracy's float interpolation in the installed HNN 5 jar. */ + static float accuracyAt(DataModel model, ModelTier tier, int data) { + if (tier == null || tier == ModelTier.FAULTY) return 0F; + float accuracy = tier.accuracy(); + ModelTier next = tier.next(); + if (HostileConfig.continuousAccuracy && next != tier) { + int min = model.getTierData(tier); + int max = model.getTierData(next); + int span = max - min; + if (span <= 0 || data < min || data >= max) return Float.NaN; + accuracy += (next.accuracy() - accuracy) * (span - (max - data)) / (float) span; + } + return Math.max(0F, Math.min(1F, accuracy)); + } + + private static void addInferenceRecipe( + List> result, + DataModel model, ModelTier tier, Ingredient matrix, Ingredient mold, + float accuracy, String suffix) { + if (!Float.isFinite(accuracy) || accuracy < 0F) return; + + Optional scaled = inferenceOutputs(model, accuracy); + if (scaled.isEmpty()) return; + + ExpectedOutputScaler.ScaledOutputs outputsFor = scaled.get(); + int operations = outputsFor.operations(); + List outputs = new ArrayList<>(); + if (!addAllOutputs(outputs, outputsFor.outputs()) || outputs.isEmpty()) return; + + long perRunEnergy = multiply(model.simCost(), SIMULATION_INFERENCE_TIME); + long energy = multiply(perRunEnergy, operations); + int processTime = safeProcessTime(multiply(SIMULATION_INFERENCE_TIME, operations)); + if (energy < 0L || processTime <= 0) return; + + ResourceLocation id = recipeId(model, "inference/" + suffix); + if (id == null) return; + + AdvancedAlloyFurnaceRecipe recipe = new AdvancedAlloyFurnaceRecipe( + id, + List.of(new CountedIngredient(matrix, operations)), + List.of(), + List.of(), + outputs, + List.of(), + List.of(), + energy, + processTime, + Ingredient.EMPTY, + 0, + List.of(mold), + AlloyFurnaceMode.NORMAL); + result.add(holder(recipe)); + } + + private static void addFabricatorRecipes( + List> result, DataModel model) { + ItemStack prediction = model.getPredictionDrop(); + if (prediction.isEmpty()) return; + + List drops = model.fabDrops(); + for (int index = 0; index < drops.size(); index++) { + ItemStack drop = drops.get(index); + if (drop == null || drop.isEmpty()) continue; + + long energy = multiply(Math.max(0, HostileConfig.fabPowerCost), FABRICATOR_TIME); + if (energy < 0L) continue; + + ResourceLocation id = recipeId(model, "fabricator/" + index); + if (id == null) continue; + + AdvancedAlloyFurnaceRecipe recipe = new AdvancedAlloyFurnaceRecipe( + id, + List.of(new CountedIngredient(exact(prediction), 1L)), + List.of(), + List.of(), + List.of(drop.copy()), + List.of(), + List.of(), + energy, + FABRICATOR_TIME, + Ingredient.EMPTY, + 0, + List.of(lootFabricatorMold()), + AlloyFurnaceMode.NORMAL); + result.add(holder(recipe)); + } + } + + /** + * Calculates the smallest deterministic inference batch with the same expected outputs as + * the Simulation Chamber. A base drop is guaranteed for each operation; prediction drops + * use native per-data accuracy, clamped to one prediction per operation. + */ + private static Optional inferenceOutputs( + DataModel model, float accuracy) { + List weightedOutputs = new ArrayList<>(); + addGuaranteedWeightedOutput(weightedOutputs, model.baseDrop()); + + ItemStack prediction = model.getPredictionDrop(); + if (!prediction.isEmpty()) { + int guaranteed = (int) Math.floor(accuracy); + double fractional = accuracy - guaranteed; + if (guaranteed > 0) { + weightedOutputs.add(new ExpectedOutputScaler.WeightedItemOutput( + prediction.copyWithCount(1), guaranteed, guaranteed, 1.0D)); + } + if (fractional > 1.0E-9D) { + weightedOutputs.add(new ExpectedOutputScaler.WeightedItemOutput( + prediction.copyWithCount(1), 1, 1, fractional)); + } + } + return ExpectedOutputScaler.scale(weightedOutputs); + } + + private static void addGuaranteedWeightedOutput( + List weightedOutputs, ItemStack stack) { + if (stack == null || stack.isEmpty() || stack.getCount() <= 0) return; + int count = stack.getCount(); + weightedOutputs.add(new ExpectedOutputScaler.WeightedItemOutput( + stack.copyWithCount(1), count, count, 1.0D)); + } + + private static boolean addAllOutputs(List outputs, List additions) { + if (additions == null) return true; + for (ItemStack addition : additions) { + if (addition == null || addition.isEmpty()) continue; + // The scaler has already materialized the complete batch count into this stack. + if (!addCountedOutput(outputs, addition, 1L)) return false; + } + return true; + } + + /** + * 追加一个可选产物:数量为零或产物为空时静默跳过,不会导致整条配方失效。 + * + *

部分模型没有预测掉落或基础掉落,这类模型仍应保留升级与推理配方, + * 只是缺少对应产物,因此这里与 {@link #addCountedOutput} 的失败语义区分开。 + * + * @return 仅在数值溢出等无法继续的情况下返回 false + */ + private static boolean addOptionalOutput(List outputs, ItemStack stack, long count) { + if (count <= 0L || stack == null || stack.isEmpty()) return true; + return addCountedOutput(outputs, stack, count); + } + + private static boolean addCountedOutput(List outputs, ItemStack stack, long count) { + if (count <= 0L) return true; + if (stack == null || stack.isEmpty() || count > Integer.MAX_VALUE) return false; + long stackCount = Math.max(1L, stack.getCount()); + long total; + try { + total = Math.multiplyExact(stackCount, count); + } catch (ArithmeticException exception) { + return false; + } + if (total > Integer.MAX_VALUE) return false; + ItemStack output = stack.copy(); + output.setCount((int) total); + outputs.add(output); + return true; + } + + private static RecipeHolder holder(AdvancedAlloyFurnaceRecipe recipe) { + return new RecipeHolder<>(recipe.id(), new HostileNetworksSyntheticRecipe(recipe)); + } + + private static ItemStack modelStack(DataModel model, int data) { + ItemStack stack = new ItemStack(Hostile.Items.DATA_MODEL.get()); + DataModelItem.setStoredModel(stack, model); + DataModelItem.setData(stack, data); + return stack; + } + + private static Ingredient exact(ItemStack stack) { + return DataComponentIngredient.of(false, stack.copyWithCount(1)); + } + + private static Ingredient simChamberMold() { + return AdapterUtils.toMoldIngredient(Hostile.Items.SIM_CHAMBER.get().getDefaultInstance()); + } + + private static Ingredient lootFabricatorMold() { + return AdapterUtils.toMoldIngredient(Hostile.Items.LOOT_FABRICATOR.get().getDefaultInstance()); + } + + private static boolean matchesMold(AdvancedAlloyFurnaceRecipe recipe, ItemStack mold) { + return AdapterUtils.matchesMold(recipe.mold(), mold); + } + + private static boolean matchesMergedInputs( + List requirements, Map available) { + Map required = new LinkedHashMap<>(); + for (CountedIngredient requirement : requirements) { + AdapterUtils.mergeIngredient(required, requirement.ingredient(), requirement.count()); + } + return ItemIngredientAllocator.matches(available, required); + } + + private static List mergedKeyInputs( + Map mergedKeys) { + if (mergedKeys == null || mergedKeys.isEmpty()) return List.of(); + List result = new ArrayList<>(); + for (Map.Entry entry : mergedKeys.entrySet()) { + long amount = Math.max(0L, entry.getValue()); + if (amount > 0L) result.add(new appeng.api.stacks.GenericStack(entry.getKey(), amount)); + } + return result; + } + + @Nullable + private static ResourceLocation recipeId(DataModel model, String operation) { + ResourceLocation modelId = DataModelRegistry.INSTANCE.getKey(model); + if (modelId == null || operation == null || operation.isBlank()) return null; + String path = (modelId.getNamespace() + "_" + modelId.getPath()).replace('/', '_'); + return new ResourceLocation(MOD_ID, "compat/" + operation + "/" + path); + } + + private static long trainingEnergy(int simCost, int trainings, int operations) { + long perTick = Math.max(0, Mth.ceil(simCost * TRAINING_COST_MULTIPLIER)); + long perTraining = multiply(perTick, SIMULATION_TRAINING_TIME); + if (perTraining < 0L) return -1L; + long allTrainings = multiply(trainings, operations); + if (allTrainings < 0L) return -1L; + return multiply(perTraining, allTrainings); + } + + private static long multiply(long left, long right) { + try { + return Math.multiplyExact(Math.max(0L, left), Math.max(0L, right)); + } catch (ArithmeticException exception) { + return -1L; + } + } + + private static int safeProcessTime(long value) { + return value <= 0L || value > Integer.MAX_VALUE ? 0 : (int) value; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/hostilenetworks/HostileNetworksSyntheticRecipe.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/hostilenetworks/HostileNetworksSyntheticRecipe.java new file mode 100644 index 00000000..5afae1cf --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/hostilenetworks/HostileNetworksSyntheticRecipe.java @@ -0,0 +1,55 @@ +package com.sorrowmist.useless.content.recipe.adapters.hostilenetworks; + +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.Container; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; + +/** Recipe-manager payload for Hostile Neural Networks' runtime-generated recipes. */ +public final class HostileNetworksSyntheticRecipe implements Recipe { + private final AdvancedAlloyFurnaceRecipe convertedRecipe; + + public HostileNetworksSyntheticRecipe(AdvancedAlloyFurnaceRecipe convertedRecipe) { + this.convertedRecipe = convertedRecipe; + } + + public AdvancedAlloyFurnaceRecipe convertedRecipe() { + return this.convertedRecipe; + } + + @Override + public net.minecraft.resources.ResourceLocation getId() { return convertedRecipe.id(); } + + @Override + public boolean matches(Container input, Level level) { + return this.convertedRecipe != null && this.convertedRecipe.matches(input, level); + } + + @Override + public ItemStack assemble(Container input, net.minecraft.core.RegistryAccess registries) { + return this.convertedRecipe == null ? ItemStack.EMPTY : this.convertedRecipe.assemble(input, registries); + } + + @Override + public boolean canCraftInDimensions(int width, int height) { + return this.convertedRecipe != null && this.convertedRecipe.canCraftInDimensions(width, height); + } + + @Override + public ItemStack getResultItem(net.minecraft.core.RegistryAccess registries) { + return this.convertedRecipe == null ? ItemStack.EMPTY : this.convertedRecipe.getResultItem(registries); + } + + @Override + public RecipeSerializer getSerializer() { + return this.convertedRecipe == null ? null : this.convertedRecipe.getSerializer(); + } + + @Override + public RecipeType getType() { + return this.convertedRecipe == null ? null : this.convertedRecipe.getType(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/immersiveengineering/ImmersiveEngineeringRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/immersiveengineering/ImmersiveEngineeringRecipeAdapter.java new file mode 100644 index 00000000..a47f1fd6 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/immersiveengineering/ImmersiveEngineeringRecipeAdapter.java @@ -0,0 +1,726 @@ +package com.sorrowmist.useless.content.recipe.adapters.immersiveengineering; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import blusunrize.immersiveengineering.api.crafting.AlloyRecipe; +import blusunrize.immersiveengineering.api.crafting.ArcFurnaceRecipe; +import blusunrize.immersiveengineering.api.crafting.BlastFurnaceRecipe; +import blusunrize.immersiveengineering.api.crafting.BlueprintCraftingRecipe; +import blusunrize.immersiveengineering.api.crafting.BottlingMachineRecipe; +import blusunrize.immersiveengineering.api.crafting.ClocheRecipe; +import blusunrize.immersiveengineering.api.crafting.CokeOvenRecipe; +import blusunrize.immersiveengineering.api.crafting.CrusherRecipe; +import blusunrize.immersiveengineering.api.crafting.FermenterRecipe; +import blusunrize.immersiveengineering.api.crafting.IngredientWithSize; +import blusunrize.immersiveengineering.api.crafting.IESerializableRecipe; +import blusunrize.immersiveengineering.api.crafting.MetalPressRecipe; +import blusunrize.immersiveengineering.api.crafting.MixerRecipe; +import blusunrize.immersiveengineering.api.crafting.MultiblockRecipe; +import blusunrize.immersiveengineering.api.crafting.RefineryRecipe; +import blusunrize.immersiveengineering.api.crafting.SawmillRecipe; +import blusunrize.immersiveengineering.api.crafting.SqueezerRecipe; +import blusunrize.immersiveengineering.api.crafting.StackWithChance; +import blusunrize.immersiveengineering.api.crafting.TagOutput; +import blusunrize.immersiveengineering.api.crafting.TagOutputList; +import blusunrize.immersiveengineering.common.config.IEServerConfig; +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.ExpectedOutputScaler; +import com.sorrowmist.useless.content.recipe.FluidIngredientAllocator; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluid; +import com.sorrowmist.useless.compat.forge1201.recipe.DataComponentIngredient; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** Converts Immersive Engineering machine recipes to alloy-furnace recipes. */ +public final class ImmersiveEngineeringRecipeAdapter + implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final String MOD_ID = "immersiveengineering"; + private static final Set MACHINE_MOLD_PATHS = Set.of( + "alloy_smelter", + "arc_furnace", + "blast_furnace", + "workbench", + "bottling_machine", + "cloche", + "coke_oven", + "crusher", + "fermenter", + "metal_press", + "mixer", + "refinery", + "sawmill", + "squeezer"); + + @Override + public String sourceId() { + return RecipeSourceIds.IMMERSIVE_ENGINEERING; + } + + @Override + public Class getRecipeClass() { + return IESerializableRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return null; + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + if (mold == null || mold.isEmpty()) return false; + ResourceLocation id = BuiltInRegistries.ITEM.getKey(mold.getItem()); + return id != null && MOD_ID.equals(id.getNamespace()) && MACHINE_MOLD_PATHS.contains(id.getPath()); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null || !isSupported(holder.value())) { + return List.of(); + } + + try { + AdvancedAlloyFurnaceRecipe converted = convert(holder, holder.value()); + return converted == null ? List.of() : List.of(converted); + } catch (RuntimeException exception) { + RateLimitedLogger.warn(LOGGER, "Skipping Immersive Engineering recipe conversion: {}", holder.id(), exception); + return List.of(); + } + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) return List.of(); + + Map safeInputs = mergedInputs == null ? Map.of() : mergedInputs; + Map safeFluids = mergedFluids == null ? Map.of() : mergedFluids; + List> matches = new ArrayList<>(); + for (RecipeHolder rawHolder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getRecipes(level.getRecipeManager())) { + if (!(rawHolder.value() instanceof IESerializableRecipe source) || !isSupported(source)) { + continue; + } + + @SuppressWarnings("unchecked") + RecipeHolder holder = + (RecipeHolder) (RecipeHolder) rawHolder; + for (AdvancedAlloyFurnaceRecipe recipe : convertAll(holder, level)) { + if (recipe.molds().size() != 1 + || !AdapterUtils.matchesMold(recipe.mold(), mold) + || !matchesInputs(recipe, safeInputs, safeFluids)) { + continue; + } + matches.add(holder); + break; + } + } + return matches; + } + + private static boolean isSupported(IESerializableRecipe recipe) { + return recipe instanceof AlloyRecipe + || recipe instanceof ArcFurnaceRecipe + || recipe instanceof BlastFurnaceRecipe + || recipe instanceof BlueprintCraftingRecipe + || recipe instanceof BottlingMachineRecipe + || recipe instanceof ClocheRecipe + || recipe instanceof CokeOvenRecipe + || recipe instanceof CrusherRecipe + || recipe instanceof FermenterRecipe + || recipe instanceof MetalPressRecipe + || recipe instanceof MixerRecipe + || recipe instanceof RefineryRecipe + || recipe instanceof SawmillRecipe + || recipe instanceof SqueezerRecipe; + } + + private static AdvancedAlloyFurnaceRecipe convert( + RecipeHolder holder, IESerializableRecipe source) { + if (source instanceof AlloyRecipe recipe) return convertAlloy(holder.id(), recipe); + if (source instanceof ArcFurnaceRecipe recipe) return convertArcFurnace(holder.id(), recipe); + if (source instanceof BlastFurnaceRecipe recipe) return convertBlastFurnace(holder.id(), recipe); + if (source instanceof BlueprintCraftingRecipe recipe) return convertBlueprint(holder.id(), recipe); + if (source instanceof BottlingMachineRecipe recipe) return convertBottling(holder.id(), recipe); + if (source instanceof ClocheRecipe recipe) return convertCloche(holder.id(), recipe); + if (source instanceof CokeOvenRecipe recipe) return convertCokeOven(holder.id(), recipe); + if (source instanceof CrusherRecipe recipe) return convertCrusher(holder.id(), recipe); + if (source instanceof FermenterRecipe recipe) return convertFermenter(holder.id(), recipe); + if (source instanceof MetalPressRecipe recipe) return convertMetalPress(holder.id(), recipe); + if (source instanceof MixerRecipe recipe) return convertMixer(holder.id(), recipe); + if (source instanceof RefineryRecipe recipe) return convertRefinery(holder.id(), recipe); + if (source instanceof SawmillRecipe recipe) return convertSawmill(holder.id(), recipe); + if (source instanceof SqueezerRecipe recipe) return convertSqueezer(holder.id(), recipe); + return null; + } + + private static AdvancedAlloyFurnaceRecipe convertAlloy( + ResourceLocation id, AlloyRecipe source) { + return build(id, + countedInputs(source.input0, source.input1), + List.of(), + tagOutput(source.output), + List.of(), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + source.time, + molds("alloy_smelter")); + } + + private static AdvancedAlloyFurnaceRecipe convertArcFurnace( + ResourceLocation id, ArcFurnaceRecipe source) { + List inputs = new ArrayList<>(); + inputs.addAll(countedInputs(source.input)); + for (IngredientWithSize additive : source.additives) { + inputs.addAll(countedInputs(additive)); + } + return build(id, + inputs, + List.of(), + append(tagOutputs(source.output), tagOutput(source.slag)), + weightedOutputs(source.secondaryOutputs), + List.of(), + processEnergy(source), + processTime(source), + molds("arc_furnace")); + } + + private static AdvancedAlloyFurnaceRecipe convertBlastFurnace( + ResourceLocation id, BlastFurnaceRecipe source) { + return build(id, + countedInputs(source.input), + List.of(), + append(tagOutput(source.output), tagOutput(source.slag)), + List.of(), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + source.time, + molds("blast_furnace")); + } + + private static AdvancedAlloyFurnaceRecipe convertBlueprint( + ResourceLocation id, BlueprintCraftingRecipe source) { + Ingredient blueprint = blueprintMold(source.blueprintCategory); + if (blueprint.isEmpty()) return null; + return build(id, + countedInputs(source.inputs), + List.of(), + tagOutput(source.output), + List.of(), + List.of(), + processEnergy(source), + processTime(source), + molds("workbench", blueprint)); + } + + private static AdvancedAlloyFurnaceRecipe convertBottling( + ResourceLocation id, BottlingMachineRecipe source) { + List remainingInputs = new ArrayList<>(); + List reusableMolds = new ArrayList<>(); + List outputs = new ArrayList<>(tagOutputs(source.output)); + if (source.inputs != null) { + for (IngredientWithSize input : source.inputs) { + Ingredient ingredient = input == null ? null : input.getBaseIngredient(); + int remaining = input == null ? 0 : input.getCount(); + if (ingredient == null || ingredient.isEmpty() || remaining <= 0) continue; + + while (remaining > 0) { + int outputIndex = -1; + for (int index = 0; index < outputs.size(); index++) { + if (ingredient.test(outputs.get(index))) { + outputIndex = index; + break; + } + } + if (outputIndex < 0) break; + + ItemStack output = outputs.get(outputIndex); + if (output.getCount() == 1) { + outputs.remove(outputIndex); + } else { + output.shrink(1); + } + reusableMolds.add(ingredient); + remaining--; + } + if (remaining > 0) remainingInputs.add(input.withSize(remaining)); + } + } + + return build(id, + countedInputs(remainingInputs), + fluidInputs(source.fluidInput), + outputs, + List.of(), + List.of(), + processEnergy(source), + processTime(source), + molds("bottling_machine", reusableMolds.toArray(Ingredient[]::new))); + } + + private static AdvancedAlloyFurnaceRecipe convertCloche( + ResourceLocation id, ClocheRecipe source) { + List inputFluids = new ArrayList<>(); + int fluidAmount = IEServerConfig.getOrDefault(IEServerConfig.MACHINES.cloche_fluid); + if (source.requiredFluid != null && !source.requiredFluid.isEmpty()) { + if (fluidAmount <= 0) return null; + inputFluids.add(new SizedFluidIngredient(source.requiredFluid, fluidAmount)); + } + + long energyPerTick = IEServerConfig.getOrDefault(IEServerConfig.MACHINES.cloche_consumption); + return build(id, + List.of(), + inputFluids, + List.of(), + weightedOutputs(source.outputs), + List.of(), + Math.multiplyExact(energyPerTick, (long) source.time), + source.time, + molds("cloche", source.seed, source.soil), + true); + } + + private static AdvancedAlloyFurnaceRecipe convertCokeOven( + ResourceLocation id, CokeOvenRecipe source) { + List outputFluids = new ArrayList<>(); + if (source.creosoteOutput > 0) { + Fluid creosote = BuiltInRegistries.FLUID.getOptional(id("creosote")).orElse(null); + if (creosote == null) return null; + outputFluids.add(new FluidStack(creosote, source.creosoteOutput)); + } + return build(id, + countedInputs(source.input), + List.of(), + tagOutput(source.output), + List.of(), + outputFluids, + AdapterUtils.DEFAULT_ENERGY, + source.time, + molds("coke_oven")); + } + + private static AdvancedAlloyFurnaceRecipe convertCrusher( + ResourceLocation id, CrusherRecipe source) { + return build(id, + countedInputs(new IngredientWithSize(source.input)), + List.of(), + tagOutput(source.output), + weightedOutputs(source.secondaryOutputs), + List.of(), + processEnergy(source), + processTime(source), + molds("crusher")); + } + + private static AdvancedAlloyFurnaceRecipe convertFermenter( + ResourceLocation id, FermenterRecipe source) { + return build(id, + countedInputs(source.input), + List.of(), + tagOutput(source.itemOutput), + List.of(), + fluidOutput(source.fluidOutput), + processEnergy(source), + processTime(source), + molds("fermenter")); + } + + private static AdvancedAlloyFurnaceRecipe convertMetalPress( + ResourceLocation id, MetalPressRecipe source) { + if (source.mold == null) return null; + return build(id, + countedInputs(source.input), + List.of(), + tagOutput(source.output), + List.of(), + List.of(), + processEnergy(source), + processTime(source), + molds("metal_press", Ingredient.of(source.mold))); + } + + private static AdvancedAlloyFurnaceRecipe convertMixer( + ResourceLocation id, MixerRecipe source) { + return build(id, + countedInputs(source.itemInputs), + fluidInputs(source.fluidInput), + List.of(), + List.of(), + fluidOutput(source.fluidOutput), + processEnergy(source), + processTime(source), + molds("mixer")); + } + + private static AdvancedAlloyFurnaceRecipe convertRefinery( + ResourceLocation id, RefineryRecipe source) { + return build(id, + List.of(), + fluidInputs(source.input0, source.input1), + List.of(), + List.of(), + fluidOutput(source.output), + processEnergy(source), + processTime(source), + molds("refinery", source.catalyst)); + } + + private static AdvancedAlloyFurnaceRecipe convertSawmill( + ResourceLocation id, SawmillRecipe source) { + return build(id, + countedInputs(new IngredientWithSize(source.input)), + List.of(), + itemStacks(source.getActualItemOutputs()), + List.of(), + List.of(), + processEnergy(source), + processTime(source), + molds("sawmill")); + } + + private static AdvancedAlloyFurnaceRecipe convertSqueezer( + ResourceLocation id, SqueezerRecipe source) { + return build(id, + countedInputs(source.input), + List.of(), + tagOutput(source.itemOutput), + List.of(), + fluidOutput(source.fluidOutput), + processEnergy(source), + processTime(source), + molds("squeezer")); + } + + private static AdvancedAlloyFurnaceRecipe build( + ResourceLocation id, + List inputs, + List inputFluids, + List deterministicOutputs, + List weightedOutputs, + List outputFluids, + long energy, + int processTime, + List molds) { + return build(id, inputs, inputFluids, deterministicOutputs, weightedOutputs, + outputFluids, energy, processTime, molds, false); + } + + private static AdvancedAlloyFurnaceRecipe build( + ResourceLocation id, + List inputs, + List inputFluids, + List deterministicOutputs, + List weightedOutputs, + List outputFluids, + long energy, + int processTime, + List molds, + boolean allowEmptyInputs) { + if (!allowEmptyInputs && inputs.isEmpty() && inputFluids.isEmpty()) return null; + if (energy < 0 || processTime <= 0 || molds.isEmpty()) return null; + + var scaledChance = ExpectedOutputScaler.scale(weightedOutputs); + if (scaledChance.isEmpty()) return null; + int operations = scaledChance.get().operations(); + + List scaledInputs = scaleInputs(inputs, operations); + List scaledInputFluids = scaleFluidInputs(inputFluids, operations); + List outputs = scaleOutputs(deterministicOutputs, operations); + if (outputs == null) return null; + for (ItemStack output : scaledChance.get().outputs()) { + if (!addOutput(outputs, output)) return null; + } + List scaledOutputFluids = scaleFluidOutputs(outputFluids, operations); + if (scaledInputs == null || scaledInputFluids == null || scaledOutputFluids == null + || (outputs.isEmpty() && scaledOutputFluids.isEmpty())) { + return null; + } + + try { + long scaledEnergy = Math.multiplyExact(energy, (long) operations); + long scaledTime = Math.multiplyExact((long) processTime, operations); + if (scaledTime <= 0 || scaledTime > Integer.MAX_VALUE) return null; + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(id), + scaledInputs, + scaledInputFluids, + List.of(), + outputs, + scaledOutputFluids, + List.of(), + scaledEnergy, + (int) scaledTime, + Ingredient.EMPTY, + 0, + molds, + AlloyFurnaceMode.NORMAL); + } catch (ArithmeticException exception) { + return null; + } + } + + private static boolean matchesInputs( + AdvancedAlloyFurnaceRecipe recipe, + Map mergedInputs, + Map mergedFluids) { + Map required = new LinkedHashMap<>(); + for (CountedIngredient input : recipe.inputs()) { + AdapterUtils.mergeIngredient(required, input.ingredient(), input.count()); + } + return AdapterUtils.matchesRequired(mergedInputs, required) + && FluidIngredientAllocator.matchesLong(recipe.inputFluids(), mergedFluids, 1L); + } + + private static long processEnergy(MultiblockRecipe recipe) { + try { + return recipe.getTotalProcessEnergy(); + } catch (RuntimeException exception) { + return recipe.getBaseEnergy(); + } + } + + private static int processTime(MultiblockRecipe recipe) { + try { + return recipe.getTotalProcessTime(); + } catch (RuntimeException exception) { + return recipe.getBaseTime(); + } + } + + private static List countedInputs(IngredientWithSize... inputs) { + Map requirements = new LinkedHashMap<>(); + if (inputs != null) { + for (IngredientWithSize input : inputs) { + if (input == null || input.getBaseIngredient() == null + || input.getBaseIngredient().isEmpty() || input.getCount() <= 0) { + continue; + } + AdapterUtils.mergeIngredient( + requirements, input.getBaseIngredient(), input.getCount()); + } + } + return requirements.entrySet().stream() + .map(entry -> new CountedIngredient(entry.getKey(), entry.getValue())) + .toList(); + } + + private static List countedInputs(List inputs) { + return countedInputs(inputs == null ? null : inputs.toArray(IngredientWithSize[]::new)); + } + + private static List fluidInputs( + @Nullable SizedFluidIngredient... inputs) { + List result = new ArrayList<>(); + if (inputs != null) { + for (SizedFluidIngredient input : inputs) { + if (input != null && input.ingredient() != null + && !input.ingredient().isEmpty() && input.amount() > 0) { + result.add(input); + } + } + } + return List.copyOf(result); + } + + private static List scaleInputs( + List inputs, int operations) { + Map result = new LinkedHashMap<>(); + for (CountedIngredient input : inputs) { + try { + long count = Math.multiplyExact(input.count(), (long) operations); + if (count <= 0) return null; + AdapterUtils.mergeIngredient(result, input.ingredient(), count); + } catch (ArithmeticException exception) { + return null; + } + } + return result.entrySet().stream() + .map(entry -> new CountedIngredient(entry.getKey(), entry.getValue())) + .toList(); + } + + private static List scaleFluidInputs( + List inputs, int operations) { + List result = new ArrayList<>(); + for (SizedFluidIngredient input : inputs) { + try { + long amount = Math.multiplyExact((long) input.amount(), operations); + if (amount <= 0 || amount > Integer.MAX_VALUE) return null; + result.add(new SizedFluidIngredient(input.ingredient(), (int) amount)); + } catch (ArithmeticException exception) { + return null; + } + } + return List.copyOf(result); + } + + private static List scaleFluidOutputs( + List outputs, int operations) { + List result = new ArrayList<>(); + for (FluidStack output : outputs) { + if (output == null || output.isEmpty() || output.getAmount() <= 0) continue; + try { + long amount = Math.multiplyExact((long) output.getAmount(), operations); + if (amount <= 0 || amount > Integer.MAX_VALUE) return null; + if (!addFluidOutput(result, new net.minecraftforge.fluids.FluidStack(output, (int) amount))) return null; + } catch (ArithmeticException exception) { + return null; + } + } + return List.copyOf(result); + } + + private static List scaleOutputs(List outputs, int operations) { + List result = new ArrayList<>(); + for (ItemStack output : outputs) { + if (output == null || output.isEmpty() || output.getCount() <= 0) continue; + try { + long count = Math.multiplyExact((long) output.getCount(), operations); + if (count <= 0 || count > Integer.MAX_VALUE) return null; + if (!addOutput(result, output.copyWithCount((int) count))) return null; + } catch (ArithmeticException exception) { + return null; + } + } + return result; + } + + private static boolean addOutput(List outputs, ItemStack output) { + if (output == null || output.isEmpty() || output.getCount() <= 0) return true; + for (ItemStack existing : outputs) { + if (!ItemStack.isSameItemSameTags(existing, output)) continue; + long count = (long) existing.getCount() + output.getCount(); + if (count > Integer.MAX_VALUE) return false; + existing.setCount((int) count); + return true; + } + outputs.add(output.copy()); + return true; + } + + private static boolean addFluidOutput(List outputs, FluidStack output) { + for (FluidStack existing : outputs) { + if (!com.sorrowmist.useless.compat.forge1201.fluid.FluidStacks.same(existing, output)) continue; + if (output.getAmount() > Integer.MAX_VALUE - existing.getAmount()) return false; + existing.grow(output.getAmount()); + return true; + } + outputs.add(output.copy()); + return true; + } + + private static List tagOutputs(TagOutputList outputs) { + if (outputs == null) return List.of(); + return itemStacks(outputs.get()); + } + + private static List tagOutput(TagOutput output) { + return output == null ? List.of() : itemStacks(List.of(output.get())); + } + + private static List itemStacks(Iterable stacks) { + List result = new ArrayList<>(); + for (ItemStack stack : stacks) { + if (stack != null && !stack.isEmpty() && stack.getCount() > 0) { + result.add(stack.copy()); + } + } + return List.copyOf(result); + } + + private static List fluidOutput(@Nullable FluidStack output) { + return output == null || output.isEmpty() || output.getAmount() <= 0 + ? List.of() + : List.of(output.copy()); + } + + private static List fluidInputs( + @Nullable SizedFluidIngredient first, @Nullable SizedFluidIngredient second) { + return fluidInputs(new SizedFluidIngredient[]{first, second}); + } + + private static List weightedOutputs( + List outputs) { + List result = new ArrayList<>(); + if (outputs != null) { + for (StackWithChance output : outputs) { + if (output == null || output.stack() == null) continue; + ItemStack stack = output.stack().get(); + if (stack != null && !stack.isEmpty() && stack.getCount() > 0) { + result.add(new ExpectedOutputScaler.WeightedItemOutput( + stack.copy(), stack.getCount(), stack.getCount(), output.chance())); + } + } + } + return List.copyOf(result); + } + + private static List molds(String machine, Ingredient... extras) { + Ingredient machineMold = machineMold(machine); + if (machineMold.isEmpty()) return List.of(); + + List result = new ArrayList<>(); + result.add(machineMold); + if (extras != null) { + for (Ingredient extra : extras) { + if (extra != null && !extra.isEmpty()) result.add(extra); + } + } + return List.copyOf(result); + } + + private static Ingredient machineMold(String path) { + Item item = BuiltInRegistries.ITEM.getOptional(id(path)).orElse(null); + return item == null || item == Items.AIR ? Ingredient.EMPTY : Ingredient.of(item); + } + + private static Ingredient blueprintMold(@Nullable String category) { + if (category == null || category.isBlank()) return Ingredient.EMPTY; + ItemStack blueprint; + try { + blueprint = BlueprintCraftingRecipe.getTypedBlueprint(category); + } catch (RuntimeException exception) { + return Ingredient.EMPTY; + } + return blueprint == null || blueprint.isEmpty() + ? Ingredient.EMPTY + : DataComponentIngredient.of(true, blueprint); + } + + private static ResourceLocation id(String path) { + return new ResourceLocation(MOD_ID, path); + } + + private static List append(List first, List second) { + List result = new ArrayList<>(first); + for (ItemStack stack : second) { + addOutput(result, stack); + } + return result; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/BioReactorRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/BioReactorRecipeAdapter.java new file mode 100644 index 00000000..218fb0e7 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/BioReactorRecipeAdapter.java @@ -0,0 +1,139 @@ +package com.sorrowmist.useless.content.recipe.adapters.industrialforegoing; + +import com.buuz135.industrial.block.generator.tile.BioReactorTile; +import com.buuz135.industrial.config.machine.generator.BioReactorConfig; +import com.buuz135.industrial.module.ModuleCore; +import com.buuz135.industrial.module.ModuleGenerator; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.FluidIngredientAllocator; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** Converts Industrial Foregoing's tag-driven bioreactor operation. */ +public final class BioReactorRecipeAdapter + implements IRecipeAdapter { + private static final int SINGLE_SLOT_FLUID = 95; + + @Override + public String sourceId() { + return RecipeSourceIds.INDUSTRIAL_FOREGOING; + } + + @Override + public Class getRecipeClass() { + return IndustrialForegoingSyntheticRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(ModuleGenerator.BIOREACTOR.getLeft().get()); + } + + @Override + public List> getGeneratedRecipes(Level level) { + if (level == null) return List.of(); + + List> result = new ArrayList<>(); + Set> tags = new LinkedHashSet<>(); + for (TagKey tag : BioReactorTile.VALID) { + if (tag != null && tags.add(tag)) { + AdvancedAlloyFurnaceRecipe recipe = createRecipe(tag); + result.add(new RecipeHolder<>(recipe.id(), + new IndustrialForegoingSyntheticRecipe(recipe))); + } + } + return List.copyOf(result); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null + || holder.value().convertedRecipe() == null) { + return List.of(); + } + return List.of(holder.value().convertedRecipe()); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + return findMatchingRecipes(level, mergedInputs, mergedFluids, Map.of(), mold, List.of()); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, + Map mergedKeys, + @Nullable ItemStack mold, List actualInputs) { + if (level == null || !matchesMold(mold)) return List.of(); + + Map safeInputs = mergedInputs == null ? Map.of() : mergedInputs; + Map safeFluids = mergedFluids == null ? Map.of() : mergedFluids; + List> matches = new ArrayList<>(); + for (RecipeHolder holder : getGeneratedRecipes(level)) { + AdvancedAlloyFurnaceRecipe recipe = holder.value().convertedRecipe(); + if (matchesRecipe(recipe, safeInputs, safeFluids)) matches.add(holder); + } + return matches; + } + + private static boolean matchesRecipe(AdvancedAlloyFurnaceRecipe recipe, + Map mergedInputs, + Map mergedFluids) { + if (recipe == null || recipe.inputs().isEmpty()) return false; + Map requiredInputs = new java.util.LinkedHashMap<>(); + for (CountedIngredient input : recipe.inputs()) { + AdapterUtils.mergeIngredient(requiredInputs, input.ingredient(), input.count()); + } + return AdapterUtils.matchesRequired(mergedInputs, requiredInputs) + && FluidIngredientAllocator.matchesLong(recipe.inputFluids(), mergedFluids, 1L); + } + + private static AdvancedAlloyFurnaceRecipe createRecipe(TagKey inputTag) { + ResourceLocation tagId = inputTag.location(); + String tagPath = tagId.getNamespace() + "_" + tagId.getPath().replace('/', '_'); + ResourceLocation recipeId = new ResourceLocation( + RecipeSourceIds.INDUSTRIAL_FOREGOING, "bioreactor_" + tagPath); + FluidIngredient water = FluidIngredient.single(Fluids.WATER); + FluidStack biofuel = new FluidStack( + ModuleCore.BIOFUEL.getSourceFluid().get(), SINGLE_SLOT_FLUID); + return new AdvancedAlloyFurnaceRecipe( + recipeId, + List.of(new CountedIngredient(Ingredient.of(inputTag), 1L)), + List.of(new LongSizedFluidIngredient(water, SINGLE_SLOT_FLUID)), + List.of(), + List.of(), + List.of(biofuel), + List.of(), + Math.max(1, BioReactorConfig.powerPerOperation), + Math.max(1, BioReactorConfig.maxProgress), + Ingredient.EMPTY, + 0, + List.of(Ingredient.of(new ItemStack(ModuleGenerator.BIOREACTOR.getLeft().get()))), + AlloyFurnaceMode.NORMAL); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/DissolutionChamberRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/DissolutionChamberRecipeAdapter.java new file mode 100644 index 00000000..6f1a023d --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/DissolutionChamberRecipeAdapter.java @@ -0,0 +1,175 @@ +package com.sorrowmist.useless.content.recipe.adapters.industrialforegoing; + +import com.buuz135.industrial.module.ModuleCore; +import com.buuz135.industrial.recipe.DissolutionChamberRecipe; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Industrial Foregoing 溶解成型机配方适配器 + *

+ * 将溶解成型机配方(物品+流体→物品/流体)转换为高级合金熔炉配方 + * 支持流体输入和流体输出 + */ +public class DissolutionChamberRecipeAdapter implements IRecipeAdapter { + + @Override + public Class getRecipeClass() { + return DissolutionChamberRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return new ItemStack( + BuiltInRegistries.ITEM.get( + new ResourceLocation("industrialforegoing", "dissolution_chamber"))); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + List result = new ArrayList<>(); + + if (holder == null) return result; + + DissolutionChamberRecipe originalRecipe = holder.value(); + ResourceLocation originalId = holder.id(); + + // 获取输入物品列表 + List itemInputs = nativeInputs(originalRecipe.input); + + // 获取输出 + ItemStack itemOutput = originalRecipe.output == null ? ItemStack.EMPTY : originalRecipe.output; + FluidStack fluidOutput = originalRecipe.outputFluid == null ? FluidStack.EMPTY : originalRecipe.outputFluid; + + // 如果没有物品输出且没有流体输出,则不转换 + if (itemOutput.isEmpty() && fluidOutput.isEmpty()) { + return result; + } + + // 构建物品输入列表 - 合并同类型的输入 + List countedIngredients = AdapterUtils.mergeIngredients(itemInputs); + + SizedFluidIngredient inputFluidIngredient = originalRecipe.inputFluid == null || originalRecipe.inputFluid.isEmpty() + ? null : SizedFluidIngredient.of(originalRecipe.inputFluid); + if (inputFluidIngredient == null) { + return result; + } + + if (inputFluidIngredient.ingredient().isEmpty()) { + return result; + } + + // 创建溶解成型机模具要求 + Ingredient moldIngredient = AdapterUtils.toMoldIngredient(getMoldItem()); + + // 计算总能量消耗 (Industrial Foregoing: 90 FE/tick * processingTime * 倍率) + int processingTime = originalRecipe.processingTime; + int totalEnergy = AdapterUtils.safeInt((long) AdapterUtils.IF_BASE_ENERGY_PER_TICK * processingTime * AdapterUtils.IF_ENERGY_MULTIPLIER); + + // 构建物品输出列表 + List outputs = new ArrayList<>(); + if (!itemOutput.isEmpty()) { + outputs.add(itemOutput.copy()); + } + + // 构建流体输出列表 + List outputFluids = new ArrayList<>(); + if (!fluidOutput.isEmpty()) { + outputFluids.add(fluidOutput.copy()); + } + + result.add(createRecipe(originalId, countedIngredients, inputFluidIngredient, outputs, + outputFluids, totalEnergy, processingTime, moldIngredient)); + return result; + } + + private AdvancedAlloyFurnaceRecipe createRecipe(ResourceLocation originalId, + List countedIngredients, + SizedFluidIngredient inputFluid, + List outputs, + List outputFluids, + int totalEnergy, + int processingTime, + Ingredient moldIngredient) { + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(originalId), + countedIngredients, + List.of(inputFluid), + List.of(), + outputs, + outputFluids, + List.of(), + totalEnergy, + processingTime, + Ingredient.EMPTY, + 0, + List.of(moldIngredient), + AlloyFurnaceMode.NORMAL + ); + } + + @Override + @Nullable + @SuppressWarnings({"unchecked"}) + public List> findMatchingRecipes(Level level, Map mergedInputs, Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || (mergedInputs.isEmpty() && mergedFluids.isEmpty()) || !matchesMold(mold)) { + return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + RecipeType recipeType = (RecipeType) ModuleCore.DISSOLUTION_TYPE.get(); + + return com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, recipeType).stream() + .filter(holder -> matchesItemInputs(mergedInputs, nativeInputs(holder.value().input)) + && matchesFluidInput(mergedFluids, holder.value().inputFluid == null || holder.value().inputFluid.isEmpty() ? null : SizedFluidIngredient.of(holder.value().inputFluid))) + .toList(); + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + ItemStack moldItem = getMoldItem(); + return mold != null && !mold.isEmpty() && moldItem != null && ItemStack.isSameItem(mold, moldItem); + } + + static List nativeInputs(Ingredient.Value[] values) { + if (values == null) throw new IllegalArgumentException("Missing dissolution inputs"); + return java.util.Arrays.stream(values).map(value -> Ingredient.fromValues(java.util.stream.Stream.of(value))).toList(); + } + + private boolean matchesItemInputs(Map mergedInputs, List itemInputs) { + Map requirements = new java.util.LinkedHashMap<>(); + for (Ingredient input : itemInputs) { + if (input != null && !input.isEmpty()) { + AdapterUtils.mergeIngredient(requirements, input, 1L); + } + } + return AdapterUtils.matchesRequired(mergedInputs, requirements); + } + + private boolean matchesFluidInput(Map mergedFluids, SizedFluidIngredient inputFluid) { + if (inputFluid == null) return true; + if (mergedFluids == null || mergedFluids.isEmpty()) return false; + return com.sorrowmist.useless.content.recipe.FluidIngredientAllocator.matches( + List.of(inputFluid), mergedFluids, 1L); + } + +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/ExtraUpgradesCraftingRecipeAdapterSupport.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/ExtraUpgradesCraftingRecipeAdapterSupport.java new file mode 100644 index 00000000..8a894117 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/ExtraUpgradesCraftingRecipeAdapterSupport.java @@ -0,0 +1,154 @@ +package com.sorrowmist.useless.content.recipe.adapters.industrialforegoing; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.ExpectedOutputScaler; +import com.sorrowmist.useless.content.recipe.FluidIngredientAllocator; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraftforge.fluids.FluidStack; +import net.yxiao233.ifeu.common.config.machine.PrecisionCraftingTableConfig; +import net.yxiao233.ifeu.common.registry.IFEUContents; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** Shared conversion and matching logic for Extra Upgrades crafting tables. */ +final class ExtraUpgradesCraftingRecipeAdapterSupport { + private static final int PRECISION_PROCESS_TIME = 200; + + private ExtraUpgradesCraftingRecipeAdapterSupport() { + } + + @Nullable + static AdvancedAlloyFurnaceRecipe precisionRecipe( + net.minecraft.resources.ResourceLocation id, List sourceInputs, + ItemStack sourceOutput, float chance, ItemStack table) { + if (sourceInputs == null || sourceOutput == null || sourceOutput.isEmpty() + || sourceOutput.getCount() <= 0 || !Float.isFinite(chance) || chance <= 0.0F) { + return null; + } + + Optional scaled = ExpectedOutputScaler.scale( + List.of(new ExpectedOutputScaler.WeightedItemOutput( + sourceOutput, sourceOutput.getCount(), sourceOutput.getCount(), chance))); + if (scaled.isEmpty() || scaled.get().operations() <= 0 || scaled.get().outputs().isEmpty()) { + return null; + } + + int operations = scaled.get().operations(); + List inputs = countedInputs(sourceInputs, operations); + Long processTime = multiply(PRECISION_PROCESS_TIME, operations); + Long energy = multiply( + IndustrialForegoingRecipeAdapterUtils.energyPerTick( + PrecisionCraftingTableConfig.powerPerTick, PRECISION_PROCESS_TIME), + operations); + if (processTime == null || processTime <= 0L || processTime > Integer.MAX_VALUE + || energy == null || energy < 0L) { + return null; + } + + return new AdvancedAlloyFurnaceRecipe( + id, + inputs, + List.of(), + List.of(), + scaled.get().outputs(), + List.of(), + List.of(), + energy, + processTime.intValue(), + Ingredient.EMPTY, + 0, + List.of(AdapterUtils.toMoldIngredient(table)), + AlloyFurnaceMode.NORMAL); + } + + @Nullable + static AdvancedAlloyFurnaceRecipe fluidRecipe( + net.minecraft.resources.ResourceLocation id, List sourceInputs, + FluidStack sourceFluid, ItemStack sourceOutput, ItemStack table) { + if (sourceFluid == null || sourceFluid.isEmpty() || sourceFluid.getAmount() <= 0 + || sourceOutput == null || sourceOutput.isEmpty() || sourceOutput.getCount() <= 0) { + return null; + } + + return new AdvancedAlloyFurnaceRecipe( + id, + countedInputs(sourceInputs, 1), + List.of(LongSizedFluidIngredient.from(sourceFluid)), + List.of(), + List.of(sourceOutput.copy()), + List.of(), + List.of(), + 0L, + 1, + Ingredient.EMPTY, + 0, + List.of(AdapterUtils.toMoldIngredient(table)), + AlloyFurnaceMode.NORMAL); + } + + static boolean matches( + AdvancedAlloyFurnaceRecipe recipe, + @Nullable Map mergedInputs, + @Nullable Map mergedFluids) { + if (recipe == null) return false; + + Map requirements = new LinkedHashMap<>(); + for (CountedIngredient input : recipe.inputs()) { + if (input != null && input.ingredient() != null && !input.ingredient().isEmpty() + && input.count() > 0L) { + AdapterUtils.mergeIngredient(requirements, input.ingredient(), input.count()); + } + } + return AdapterUtils.matchesRequired( + mergedInputs == null ? Map.of() : mergedInputs, requirements) + && FluidIngredientAllocator.matchesLong( + recipe.inputFluids(), mergedFluids == null ? Map.of() : mergedFluids, 1L); + } + + private static List countedInputs( + List sourceInputs, int multiplier) { + if (sourceInputs == null || sourceInputs.isEmpty() || multiplier <= 0) return List.of(); + + Map counts = new LinkedHashMap<>(); + for (Ingredient ingredient : sourceInputs) { + if (ingredient == null || ingredient.isEmpty() || isAirPlaceholder(ingredient)) continue; + AdapterUtils.mergeIngredient(counts, ingredient, multiplier); + } + + List result = new ArrayList<>(counts.size()); + for (Map.Entry entry : counts.entrySet()) { + result.add(new CountedIngredient(entry.getKey(), entry.getValue())); + } + return List.copyOf(result); + } + + private static boolean isAirPlaceholder(Ingredient ingredient) { + ItemStack[] items = ingredient.getItems(); + if (items.length == 0) return false; + for (ItemStack item : items) { + if (item == null || item.isEmpty() || !item.is(IFEUContents.AIR.get())) { + return false; + } + } + return true; + } + + @Nullable + private static Long multiply(long left, long right) { + try { + return Math.multiplyExact(left, right); + } catch (ArithmeticException exception) { + return null; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/FermentationStationRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/FermentationStationRecipeAdapter.java new file mode 100644 index 00000000..91093c8d --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/FermentationStationRecipeAdapter.java @@ -0,0 +1,124 @@ +package com.sorrowmist.useless.content.recipe.adapters.industrialforegoing; + +import com.buuz135.industrial.config.machine.resourceproduction.FermentationStationConfig; +import com.buuz135.industrial.module.ModuleCore; +import com.buuz135.industrial.module.ModuleResourceProduction; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Industrial Foregoing's tag-driven ore fermentation modes. */ +public final class FermentationStationRecipeAdapter + implements IRecipeAdapter { + private static final int INPUT_AMOUNT = 100; + + @Override + public String sourceId() { + return RecipeSourceIds.INDUSTRIAL_FOREGOING; + } + + @Override + public Class getRecipeClass() { + return IndustrialForegoingSyntheticRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(ModuleResourceProduction.FERMENTATION_STATION.getLeft().get()); + } + + @Override + public List> getGeneratedRecipes(Level level) { + if (level == null) return List.of(); + List recipes = new ArrayList<>(); + for (ResourceLocation rawTag : IndustrialForegoingRecipeAdapterUtils.validRawMaterialTags()) { + for (Mode mode : Mode.values()) { + recipes.add(createRecipe(rawTag, mode)); + } + } + return IndustrialForegoingRecipeAdapterUtils.holders(recipes); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null + || holder.value().convertedRecipe() == null) { + return List.of(); + } + return List.of(holder.value().convertedRecipe()); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) return List.of(); + List> matches = new ArrayList<>(); + for (RecipeHolder holder : getGeneratedRecipes(level)) { + AdvancedAlloyFurnaceRecipe recipe = holder.value().convertedRecipe(); + if (IndustrialForegoingRecipeAdapterUtils.matches(recipe, mergedInputs, mergedFluids)) { + matches.add(holder); + } + } + return List.copyOf(matches); + } + + private static AdvancedAlloyFurnaceRecipe createRecipe(ResourceLocation rawTag, Mode mode) { + String tagPath = IndustrialForegoingRecipeAdapterUtils.tagPath(rawTag); + ResourceLocation id = new ResourceLocation( + RecipeSourceIds.INDUSTRIAL_FOREGOING, + "fermentation_station_" + tagPath + "_x" + mode.multiplier); + FluidStack input = IndustrialForegoingRecipeAdapterUtils.rawOreMeat(rawTag, INPUT_AMOUNT); + FluidStack output = IndustrialForegoingRecipeAdapterUtils.fermentedOreMeat( + rawTag, INPUT_AMOUNT * mode.multiplier); + List fluids = new ArrayList<>(); + fluids.add(LongSizedFluidIngredient.from(input)); + if (mode.catalyst != null) fluids.add(LongSizedFluidIngredient.from(mode.catalyst)); + + int processTime = IndustrialForegoingRecipeAdapterUtils.positive(mode.processTime()); + return new AdvancedAlloyFurnaceRecipe( + id, List.of(), fluids, List.of(), List.of(), List.of(output), List.of(), + IndustrialForegoingRecipeAdapterUtils.energyPerTick( + FermentationStationConfig.powerPerTick, processTime), + processTime, Ingredient.EMPTY, 0, + List.of(Ingredient.of(new ItemStack(ModuleResourceProduction.FERMENTATION_STATION.getLeft().get()))), + AlloyFurnaceMode.NORMAL); + } + + private enum Mode { + X2(2, FermentationStationConfig.ticksFor2XProduction, null), + X3(3, FermentationStationConfig.ticksFor3XProduction, null), + X4(4, FermentationStationConfig.ticksFor4XProduction, + new FluidStack(ModuleCore.PINK_SLIME.getSourceFluid().get(), 2)), + X5(5, FermentationStationConfig.ticksFor5XProduction, + new FluidStack(ModuleCore.ETHER.getSourceFluid().get(), 1)); + + private final int multiplier; + private final int processTime; + private final FluidStack catalyst; + + Mode(int multiplier, int processTime, @Nullable FluidStack catalyst) { + this.multiplier = multiplier; + this.processTime = processTime; + this.catalyst = catalyst; + } + + private int processTime() { + return processTime; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/FluidExtractorRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/FluidExtractorRecipeAdapter.java new file mode 100644 index 00000000..a4f7cff9 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/FluidExtractorRecipeAdapter.java @@ -0,0 +1,111 @@ +package com.sorrowmist.useless.content.recipe.adapters.industrialforegoing; + +import com.buuz135.industrial.config.machine.core.FluidExtractorConfig; +import com.buuz135.industrial.module.ModuleCore; +import com.buuz135.industrial.recipe.FluidExtractorRecipe; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Converts block-targeted fluid extractor recipes into deterministic item batches. */ +public final class FluidExtractorRecipeAdapter implements IRecipeAdapter { + private static final long LATEX_PER_SOURCE_UNIT = 125L; + + @Override + public String sourceId() { + return RecipeSourceIds.INDUSTRIAL_FOREGOING; + } + + @Override + public Class getRecipeClass() { + return FluidExtractorRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(ModuleCore.FLUID_EXTRACTOR.getLeft().get()); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) return List.of(); + FluidExtractorRecipe source = holder.value(); + if (source.input == null || Ingredient.fromValues(java.util.stream.Stream.of(source.input)).isEmpty() + || source.output == null || source.output.isEmpty() + || source.output.getAmount() <= 0) { + return List.of(); + } + + FluidStack output = source.output.copy(); + if (new net.minecraft.resources.ResourceLocation("industrialforegoing:latex").equals(net.minecraft.core.registries.BuiltInRegistries.FLUID.getKey(source.output.getFluid()))) { + int amount = IndustrialForegoingRecipeAdapterUtils.scaledLatexAmount( + source.output, LATEX_PER_SOURCE_UNIT); + if (amount <= 0) return List.of(); + output.setAmount(amount); + } + + int processTime = IndustrialForegoingRecipeAdapterUtils.positive( + FluidExtractorConfig.maxProgress); + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + List.of(new CountedIngredient(Ingredient.fromValues(java.util.stream.Stream.of(source.input)), 1L)), List.of(), List.of(), + List.of(), List.of(output), List.of(), + Math.max(1L, FluidExtractorConfig.powerPerOperation), processTime, + Ingredient.EMPTY, 0, + List.of(AdapterUtils.toMoldIngredient(getMoldItem())), AlloyFurnaceMode.NORMAL)); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || mergedInputs == null || mergedInputs.isEmpty() + || (mergedFluids != null && !mergedFluids.isEmpty()) + || !matchesMold(mold)) { + return List.of(); + } + + RecipeType type = extractorType(); + if (type == null) return List.of(); + RecipeManager recipeManager = level.getRecipeManager(); + List> customMatches = new ArrayList<>(); + List> defaultMatches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, type)) { + FluidExtractorRecipe source = holder.value(); + if (source == null || source.input == null || Ingredient.fromValues(java.util.stream.Stream.of(source.input)).isEmpty() + || source.output == null || source.output.isEmpty()) continue; + Map required = new LinkedHashMap<>(); + AdapterUtils.mergeIngredient(required, Ingredient.fromValues(java.util.stream.Stream.of(source.input)), 1L); + if (!AdapterUtils.matchesRequired(mergedInputs, required)) continue; + if (source.defaultRecipe) defaultMatches.add(holder); + else customMatches.add(holder); + } + return customMatches.isEmpty() ? List.copyOf(defaultMatches) : List.copyOf(customMatches); + } + + @SuppressWarnings("unchecked") + @Nullable + private static RecipeType extractorType() { + if (ModuleCore.FLUID_EXTRACTOR_TYPE == null + || ModuleCore.FLUID_EXTRACTOR_TYPE.get() == null) return null; + return (RecipeType) (RecipeType) + ModuleCore.FLUID_EXTRACTOR_TYPE.get(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/FluidShapedRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/FluidShapedRecipeAdapter.java new file mode 100644 index 00000000..dba003a3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/FluidShapedRecipeAdapter.java @@ -0,0 +1,77 @@ +package com.sorrowmist.useless.content.recipe.adapters.industrialforegoing; + +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import net.yxiao233.ifeu.common.recipe.ShapedRecipe; +import net.yxiao233.ifeu.common.registry.IFEUBlocks; +import net.yxiao233.ifeu.common.registry.IFEURecipes; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Extra Upgrades fluid crafting table shaped recipes. */ +public final class FluidShapedRecipeAdapter implements IRecipeAdapter { + @Override + public String sourceId() { + return RecipeSourceIds.INDUSTRIAL_FOREGOING; + } + + @Override + public Class getRecipeClass() { + return ShapedRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(IFEUBlocks.FLUID_CRAFTING_TABLE.getBlock()); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) return List.of(); + ShapedRecipe source = holder.value(); + AdvancedAlloyFurnaceRecipe converted = ExtraUpgradesCraftingRecipeAdapterSupport.fluidRecipe( + AdapterUtils.convertedId(holder.id()), source.inputs, source.inputFluid, + source.output, getMoldItem()); + return converted == null ? List.of() : List.of(converted); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) return List.of(); + RecipeType type = recipeType(); + if (type == null) return List.of(); + RecipeManager manager = level.getRecipeManager(); + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(manager, type)) { + List converted = convertAll(holder, level); + if (!converted.isEmpty() + && ExtraUpgradesCraftingRecipeAdapterSupport.matches( + converted.get(0), mergedInputs, mergedFluids)) { + matches.add(holder); + } + } + return List.copyOf(matches); + } + + @SuppressWarnings("unchecked") + @Nullable + private static RecipeType recipeType() { + if (IFEURecipes.SHAPED_TYPE == null || IFEURecipes.SHAPED_TYPE.get() == null) return null; + return (RecipeType) (RecipeType) IFEURecipes.SHAPED_TYPE.get(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/FluidShapelessRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/FluidShapelessRecipeAdapter.java new file mode 100644 index 00000000..3edb99e8 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/FluidShapelessRecipeAdapter.java @@ -0,0 +1,78 @@ +package com.sorrowmist.useless.content.recipe.adapters.industrialforegoing; + +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import net.yxiao233.ifeu.common.recipe.ShapelessRecipe; +import net.yxiao233.ifeu.common.registry.IFEUBlocks; +import net.yxiao233.ifeu.common.registry.IFEURecipes; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Extra Upgrades fluid crafting table shapeless recipes. */ +public final class FluidShapelessRecipeAdapter implements IRecipeAdapter { + @Override + public String sourceId() { + return RecipeSourceIds.INDUSTRIAL_FOREGOING; + } + + @Override + public Class getRecipeClass() { + return ShapelessRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(IFEUBlocks.FLUID_CRAFTING_TABLE.getBlock()); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) return List.of(); + ShapelessRecipe source = holder.value(); + AdvancedAlloyFurnaceRecipe converted = ExtraUpgradesCraftingRecipeAdapterSupport.fluidRecipe( + AdapterUtils.convertedId(holder.id()), source.inputs, source.inputFluid, + source.output, getMoldItem()); + return converted == null ? List.of() : List.of(converted); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) return List.of(); + RecipeType type = recipeType(); + if (type == null) return List.of(); + RecipeManager manager = level.getRecipeManager(); + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(manager, type)) { + List converted = convertAll(holder, level); + if (!converted.isEmpty() + && ExtraUpgradesCraftingRecipeAdapterSupport.matches( + converted.get(0), mergedInputs, mergedFluids)) { + matches.add(holder); + } + } + return List.copyOf(matches); + } + + @SuppressWarnings("unchecked") + @Nullable + private static RecipeType recipeType() { + if (IFEURecipes.SHAPELESS_TYPE == null + || IFEURecipes.SHAPELESS_TYPE.get() == null) return null; + return (RecipeType) (RecipeType) IFEURecipes.SHAPELESS_TYPE.get(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/FluidSievingMachineRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/FluidSievingMachineRecipeAdapter.java new file mode 100644 index 00000000..98e1ee18 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/FluidSievingMachineRecipeAdapter.java @@ -0,0 +1,95 @@ +package com.sorrowmist.useless.content.recipe.adapters.industrialforegoing; + +import com.buuz135.industrial.config.machine.resourceproduction.FluidSievingMachineConfig; +import com.buuz135.industrial.fluid.OreTitaniumFluidType; +import com.buuz135.industrial.module.ModuleResourceProduction; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.ItemTags; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Industrial Foregoing's fluid sieving recipes. */ +public final class FluidSievingMachineRecipeAdapter + implements IRecipeAdapter { + private static final int FLUID_AMOUNT = 100; + + @Override + public String sourceId() { + return RecipeSourceIds.INDUSTRIAL_FOREGOING; + } + + @Override + public Class getRecipeClass() { + return IndustrialForegoingSyntheticRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(ModuleResourceProduction.FLUID_SIEVING_MACHINE.getLeft().get()); + } + + @Override + public List> getGeneratedRecipes(Level level) { + if (level == null) return List.of(); + List recipes = new ArrayList<>(); + for (ResourceLocation rawTag : IndustrialForegoingRecipeAdapterUtils.validRawMaterialTags()) { + FluidStack input = IndustrialForegoingRecipeAdapterUtils.fermentedOreMeat(rawTag, FLUID_AMOUNT); + ItemStack output = OreTitaniumFluidType.getOutputDust(input); + if (output == null || output.isEmpty()) continue; + + ResourceLocation id = new ResourceLocation( + RecipeSourceIds.INDUSTRIAL_FOREGOING, + "fluid_sieving_" + IndustrialForegoingRecipeAdapterUtils.tagPath(rawTag)); + int processTime = IndustrialForegoingRecipeAdapterUtils.positive( + FluidSievingMachineConfig.maxProgress); + recipes.add(new AdvancedAlloyFurnaceRecipe( + id, + List.of(new CountedIngredient(Ingredient.of(ItemTags.SAND), 1L)), + List.of(LongSizedFluidIngredient.from(input)), List.of(), List.of(output.copy()), + List.of(), List.of(), IndustrialForegoingRecipeAdapterUtils.energyPerTick( + FluidSievingMachineConfig.powerPerTick, processTime), processTime, + Ingredient.EMPTY, 0, + List.of(Ingredient.of(new ItemStack( + ModuleResourceProduction.FLUID_SIEVING_MACHINE.getLeft().get()))), + AlloyFurnaceMode.NORMAL)); + } + return IndustrialForegoingRecipeAdapterUtils.holders(recipes); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null + || holder.value().convertedRecipe() == null) return List.of(); + return List.of(holder.value().convertedRecipe()); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) return List.of(); + List> matches = new ArrayList<>(); + for (RecipeHolder holder : getGeneratedRecipes(level)) { + if (IndustrialForegoingRecipeAdapterUtils.matches( + holder.value().convertedRecipe(), mergedInputs, mergedFluids)) { + matches.add(holder); + } + } + return List.copyOf(matches); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/IndustrialForegoingRecipeAdapterUtils.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/IndustrialForegoingRecipeAdapterUtils.java new file mode 100644 index 00000000..f959fbd9 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/IndustrialForegoingRecipeAdapterUtils.java @@ -0,0 +1,115 @@ +package com.sorrowmist.useless.content.recipe.adapters.industrialforegoing; + +import com.buuz135.industrial.fluid.OreTitaniumFluidType; +import com.buuz135.industrial.module.ModuleCore; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.FluidIngredientAllocator; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraftforge.fluids.FluidStack; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +final class IndustrialForegoingRecipeAdapterUtils { + private IndustrialForegoingRecipeAdapterUtils() { + } + + static List validRawMaterialTags() { + return BuiltInRegistries.ITEM.getTagNames() + .map(tag -> tag.location()) + .filter(IndustrialForegoingRecipeAdapterUtils::isValidRawMaterialTag) + .sorted(Comparator.comparing(ResourceLocation::toString)) + .toList(); + } + + static boolean isValidRawMaterialTag(ResourceLocation id) { + return id != null + && "c".equals(id.getNamespace()) + && id.getPath().startsWith("raw_materials/") + && OreTitaniumFluidType.isValid(id); + } + + static TagKey itemTag(ResourceLocation id) { + return TagKey.create(Registries.ITEM, id); + } + + static FluidStack rawOreMeat(ResourceLocation id, int amount) { + return OreTitaniumFluidType.getFluidWithTag(ModuleCore.RAW_ORE_MEAT, amount, id); + } + + static FluidStack fermentedOreMeat(ResourceLocation id, int amount) { + return OreTitaniumFluidType.getFluidWithTag(ModuleCore.FERMENTED_ORE_MEAT, amount, id); + } + + static String tagPath(ResourceLocation id) { + return id.getNamespace() + "_" + id.getPath().replace('/', '_'); + } + + static boolean matchesItems(List required, + Map available) { + if (required == null || required.isEmpty()) { + return available == null || available.isEmpty(); + } + if (available == null || available.isEmpty()) return false; + Map requirements = new LinkedHashMap<>(); + for (CountedIngredient input : required) { + if (input != null && input.ingredient() != null + && !input.ingredient().isEmpty() && input.count() > 0) { + AdapterUtils.mergeIngredient(requirements, input.ingredient(), input.count()); + } + } + return AdapterUtils.matchesRequired(available, requirements); + } + + static boolean matches(AdvancedAlloyFurnaceRecipe recipe, + Map availableItems, + Map availableFluids) { + return recipe != null + && matchesItems(recipe.inputs(), availableItems) + && FluidIngredientAllocator.matchesLong( + recipe.inputFluids(), availableFluids == null ? Map.of() : availableFluids, 1L); + } + + static List> holders( + List recipes) { + List> result = + new ArrayList<>(recipes.size()); + for (AdvancedAlloyFurnaceRecipe recipe : recipes) { + if (recipe != null) { + result.add(new RecipeHolder<>(recipe.id(), + new IndustrialForegoingSyntheticRecipe(recipe))); + } + } + return List.copyOf(result); + } + + static int positive(int value) { + return Math.max(1, value); + } + + static long energyPerTick(int powerPerTick, int processTime) { + return Math.max(1L, (long) positive(powerPerTick) * positive(processTime)); + } + + static long multiply(long left, long right) { + if (left <= 0L || right <= 0L) return 0L; + return left > Long.MAX_VALUE / right ? Long.MAX_VALUE : left * right; + } + + static int scaledLatexAmount(FluidStack output, long sourceUnitAmount) { + if (output == null || output.isEmpty() || output.getAmount() <= 0) return 0; + long amount = multiply(output.getAmount(), sourceUnitAmount); + return amount > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) amount; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/IndustrialForegoingSyntheticRecipe.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/IndustrialForegoingSyntheticRecipe.java new file mode 100644 index 00000000..24da35f1 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/IndustrialForegoingSyntheticRecipe.java @@ -0,0 +1,55 @@ +package com.sorrowmist.useless.content.recipe.adapters.industrialforegoing; + +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.Container; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; + +/** Recipe-manager payload for Industrial Foregoing recipes generated from runtime data. */ +public final class IndustrialForegoingSyntheticRecipe implements Recipe { + private final AdvancedAlloyFurnaceRecipe convertedRecipe; + + public IndustrialForegoingSyntheticRecipe(AdvancedAlloyFurnaceRecipe convertedRecipe) { + this.convertedRecipe = convertedRecipe; + } + + public AdvancedAlloyFurnaceRecipe convertedRecipe() { + return convertedRecipe; + } + + @Override + public net.minecraft.resources.ResourceLocation getId() { return convertedRecipe.id(); } + + @Override + public boolean matches(Container input, Level level) { + return convertedRecipe != null && convertedRecipe.matches(input, level); + } + + @Override + public ItemStack assemble(Container input, net.minecraft.core.RegistryAccess registries) { + return convertedRecipe == null ? ItemStack.EMPTY : convertedRecipe.assemble(input, registries); + } + + @Override + public boolean canCraftInDimensions(int width, int height) { + return convertedRecipe != null && convertedRecipe.canCraftInDimensions(width, height); + } + + @Override + public ItemStack getResultItem(net.minecraft.core.RegistryAccess registries) { + return convertedRecipe == null ? ItemStack.EMPTY : convertedRecipe.getResultItem(registries); + } + + @Override + public RecipeSerializer getSerializer() { + return convertedRecipe == null ? null : convertedRecipe.getSerializer(); + } + + @Override + public RecipeType getType() { + return convertedRecipe == null ? null : convertedRecipe.getType(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/InfuserRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/InfuserRecipeAdapter.java new file mode 100644 index 00000000..d5f34800 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/InfuserRecipeAdapter.java @@ -0,0 +1,162 @@ +package com.sorrowmist.useless.content.recipe.adapters.industrialforegoing; + +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.BucketItem; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.MobBucketItem; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import net.yxiao233.ifeu.common.config.machine.InfuserConfig; +import net.yxiao233.ifeu.common.recipe.InfuserRecipe; +import net.yxiao233.ifeu.common.registry.IFEUBlocks; +import net.yxiao233.ifeu.common.registry.IFEURecipes; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Industrial Foregoing Extra Upgrades infuser recipes. */ +public final class InfuserRecipeAdapter implements IRecipeAdapter { + private static final int COMPACT_RECIPE_TIME = 200; + private static final int COMPACT_RECIPE_FLUID_AMOUNT = 1000; + + @Override + public String sourceId() { + return RecipeSourceIds.INDUSTRIAL_FOREGOING; + } + + @Override + public Class getRecipeClass() { + return InfuserRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(IFEUBlocks.INFUSER.getBlock()); + } + + /** + * The infuser also handles every non-mob fluid bucket directly in its block entity instead + * of loading those operations from RecipeManager. Expose the same operations to the alloy + * furnace recipe catalog. + */ + @Override + public List> getGeneratedRecipes(Level level) { + if (level == null) return List.of(); + + List> result = new ArrayList<>(); + for (Item item : BuiltInRegistries.ITEM) { + if (!(item instanceof BucketItem bucket) + || item instanceof MobBucketItem + || item == Items.BUCKET) { + continue; + } + + FluidStack fluid = new FluidStack(bucket.content, COMPACT_RECIPE_FLUID_AMOUNT); + if (fluid.isEmpty()) continue; + + ResourceLocation itemId = BuiltInRegistries.ITEM.getKey(item); + ResourceLocation recipeId = new ResourceLocation( + "ifeu", "infuser/compact/" + itemId.getNamespace() + "/" + itemId.getPath()); + result.add(new RecipeHolder<>(recipeId, new InfuserRecipe( + Items.BUCKET.getDefaultInstance(), fluid, COMPACT_RECIPE_TIME, + item.getDefaultInstance()))); + } + return List.copyOf(result); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) return List.of(); + + InfuserRecipe source = holder.value(); + if (source.input == null || source.input.isEmpty() || source.input.getCount() <= 0 + || source.inputFluid == null || source.inputFluid.isEmpty() + || source.inputFluid.getAmount() <= 0 + || source.output == null || source.output.isEmpty()) { + return List.of(); + } + + int processTime = IndustrialForegoingRecipeAdapterUtils.positive(source.processingTime); + AdvancedAlloyFurnaceRecipe converted = new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + List.of(new CountedIngredient( + Ingredient.of(source.input), source.input.getCount())), + List.of(LongSizedFluidIngredient.from(source.inputFluid)), + List.of(), + List.of(source.output.copy()), + List.of(), + List.of(), + IndustrialForegoingRecipeAdapterUtils.energyPerTick( + InfuserConfig.powerPerTick, processTime), + processTime, + Ingredient.EMPTY, + 0, + List.of(AdapterUtils.toMoldIngredient(getMoldItem())), + AlloyFurnaceMode.NORMAL); + return List.of(converted); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold) + || (mergedInputs == null || mergedInputs.isEmpty()) + || (mergedFluids == null || mergedFluids.isEmpty())) { + return List.of(); + } + + RecipeType type = infuserType(); + if (type == null) return List.of(); + + List> matches = new ArrayList<>(); + RecipeManager recipeManager = level.getRecipeManager(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, type)) { + if (matchesConverted(holder, level, mergedInputs, mergedFluids)) { + matches.add(holder); + } + } + for (RecipeHolder holder : getGeneratedRecipes(level)) { + if (matchesConverted(holder, level, mergedInputs, mergedFluids)) { + matches.add(holder); + } + } + return List.copyOf(matches); + } + + private boolean matchesConverted(RecipeHolder holder, Level level, + Map mergedInputs, + Map mergedFluids) { + List converted = convertAll(holder, level); + return !converted.isEmpty() + && IndustrialForegoingRecipeAdapterUtils.matches( + converted.get(0), mergedInputs, mergedFluids); + } + + @SuppressWarnings("unchecked") + @Nullable + private static RecipeType infuserType() { + if (IFEURecipes.INFUSER_TYPE == null || IFEURecipes.INFUSER_TYPE.get() == null) { + return null; + } + return (RecipeType) (RecipeType) IFEURecipes.INFUSER_TYPE.get(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/LaserDrillAdapterSupport.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/LaserDrillAdapterSupport.java new file mode 100644 index 00000000..dd49112e --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/LaserDrillAdapterSupport.java @@ -0,0 +1,107 @@ +package com.sorrowmist.useless.content.recipe.adapters.industrialforegoing; + +import com.hrznstudio.titanium._impl.TagConfig; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.SpawnEggItem; +import net.minecraft.world.item.crafting.Ingredient; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Helpers for the Industrial Foregoing ore laser drill adapter. + * + *

Industrial Foregoing picks a laser drill result by lens colour and weighted random. The alloy + * furnace is deterministic, so the adapters group every source recipe by its catalyst lens (plus the + * entity requirement, when there is one) and emit a single recipe producing all of that group's + * results at once.

+ */ +final class LaserDrillAdapterSupport { + + private LaserDrillAdapterSupport() { + } + + /** Stable key for an ingredient, built from the sorted ids of the items it accepts. */ + static String ingredientKey(@Nullable Ingredient ingredient) { + if (ingredient == null || ingredient.isEmpty()) return ""; + List ids = new ArrayList<>(); + for (ItemStack stack : ingredient.getItems()) { + if (stack.isEmpty()) continue; + ResourceLocation id = BuiltInRegistries.ITEM.getKey(stack.getItem()); + if (id != null) ids.add(id.toString()); + } + if (ids.isEmpty()) return ""; + Collections.sort(ids); + return String.join("+", ids); + } + + /** Turns a group key into a path usable inside a {@link ResourceLocation}. */ + static String sanitize(String key) { + StringBuilder builder = new StringBuilder(key.length()); + for (int i = 0; i < key.length(); i++) { + char c = key.charAt(i); + builder.append(c == ':' || c == '/' || c == '+' || c == '|' ? '_' : c); + } + return builder.toString(); + } + + /** + * Returns the entity a recipe requires, or {@code null} when there is no requirement or the + * requirement is a tag that cannot be narrowed down to a single type. A requirement that cannot + * be represented is dropped rather than discarding the whole recipe. + */ + /** Returns the spawn egg mold for an entity, or an empty ingredient when it has no spawn egg. */ + static Ingredient spawnEggMold(@Nullable EntityType entityType) { + if (entityType == null) return Ingredient.EMPTY; + SpawnEggItem egg = SpawnEggItem.byId(entityType); + return egg == null ? Ingredient.EMPTY : Ingredient.of(egg); + } + + /** + * Picks the item a laser drill recipe would actually hand out, mirroring + * {@code OreLaserBaseTile#executeRecipe}: Titanium's configured mod preference first, otherwise + * the first matching stack. + */ + static ItemStack representativeOutput(@Nullable Ingredient output) { + if (output == null) return ItemStack.EMPTY; + ItemStack[] items = output.getItems(); + if (items == null || items.length == 0) return ItemStack.EMPTY; + + ItemStack chosen = preferredStack(items); + if (chosen.isEmpty()) { + for (ItemStack candidate : items) { + if (!candidate.isEmpty()) { + chosen = candidate; + break; + } + } + } + if (chosen.isEmpty()) return ItemStack.EMPTY; + + ItemStack copy = chosen.copy(); + copy.setCount(Math.max(1, chosen.getCount())); + return copy; + } + + private static ItemStack preferredStack(ItemStack[] items) { + try { + for (String modid : TagConfig.ITEM_PREFERENCE) { + for (ItemStack stack : items) { + if (stack.isEmpty()) continue; + ResourceLocation id = BuiltInRegistries.ITEM.getKey(stack.getItem()); + if (id != null && id.getNamespace().equals(modid)) { + return stack; + } + } + } + } catch (Throwable ignored) { + // Titanium internals are not part of a stable API; fall back to the first stack. + } + return ItemStack.EMPTY; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/LaserDrillFluidRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/LaserDrillFluidRecipeAdapter.java new file mode 100644 index 00000000..c644765d --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/LaserDrillFluidRecipeAdapter.java @@ -0,0 +1,126 @@ +package com.sorrowmist.useless.content.recipe.adapters.industrialforegoing; + +import com.buuz135.industrial.config.machine.resourceproduction.FluidLaserBaseConfig; +import com.buuz135.industrial.config.machine.resourceproduction.LaserDrillConfig; +import com.buuz135.industrial.module.ModuleResourceProduction; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Generates the fixed fluid-laser outputs that use special alloy-furnace molds. */ +public final class LaserDrillFluidRecipeAdapter + implements IRecipeAdapter { + private static final int WATER_AMOUNT = 1000; + private static final int OUTPUT_AMOUNT = 10; + + private static final List TARGETS = List.of( + new Target( + new ResourceLocation("industrialforegoing", "ether_gas"), + Items.NETHER_STAR), + new Target( + new ResourceLocation("ifeu", "liquid_sculk_matter"), + Items.WARDEN_SPAWN_EGG), + new Target( + new ResourceLocation("ifeu", "liquid_dragon_breath"), + Items.DRAGON_EGG)); + + @Override + public String sourceId() { + return RecipeSourceIds.INDUSTRIAL_FOREGOING; + } + + @Override + public Class getRecipeClass() { + return IndustrialForegoingSyntheticRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(ModuleResourceProduction.FLUID_LASER_BASE.getLeft().get()); + } + + @Override + public List> getGeneratedRecipes(Level level) { + if (level == null) return List.of(); + + List recipes = new ArrayList<>(TARGETS.size()); + for (Target target : TARGETS) { + BuiltInRegistries.FLUID.getOptional(target.fluidId()).ifPresent(fluid -> + recipes.add(createRecipe(target, fluid))); + } + return IndustrialForegoingRecipeAdapterUtils.holders(recipes); + } + + private static AdvancedAlloyFurnaceRecipe createRecipe(Target target, Fluid fluid) { + ResourceLocation id = new ResourceLocation( + RecipeSourceIds.INDUSTRIAL_FOREGOING, + "laser_drill_fluid_" + target.fluidId().getNamespace() + + "_" + target.fluidId().getPath()); + int processTime = IndustrialForegoingRecipeAdapterUtils.positive( + FluidLaserBaseConfig.maxProgress); + long energy = IndustrialForegoingRecipeAdapterUtils.energyPerTick( + LaserDrillConfig.powerPerOperation, processTime); + + return new AdvancedAlloyFurnaceRecipe( + id, + List.of(), + List.of(LongSizedFluidIngredient.from(new FluidStack(Fluids.WATER, WATER_AMOUNT))), + List.of(), + List.of(), + List.of(new FluidStack(fluid, OUTPUT_AMOUNT)), + List.of(), + energy, + processTime, + Ingredient.EMPTY, + 0, + List.of( + Ingredient.of(new ItemStack(ModuleResourceProduction.FLUID_LASER_BASE.getLeft().get())), + Ingredient.of(target.mold())), + AlloyFurnaceMode.NORMAL); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null + || holder.value().convertedRecipe() == null) return List.of(); + return List.of(holder.value().convertedRecipe()); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) return List.of(); + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : getGeneratedRecipes(level)) { + if (IndustrialForegoingRecipeAdapterUtils.matches( + holder.value().convertedRecipe(), mergedInputs, mergedFluids)) { + matches.add(holder); + } + } + return List.copyOf(matches); + } + + private record Target(ResourceLocation fluidId, Item mold) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/LaserDrillOreRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/LaserDrillOreRecipeAdapter.java new file mode 100644 index 00000000..ccc56844 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/LaserDrillOreRecipeAdapter.java @@ -0,0 +1,220 @@ +package com.sorrowmist.useless.content.recipe.adapters.industrialforegoing; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.buuz135.industrial.config.machine.resourceproduction.LaserDrillConfig; +import com.buuz135.industrial.config.machine.resourceproduction.OreLaserBaseConfig; +import com.buuz135.industrial.module.ModuleCore; +import com.buuz135.industrial.module.ModuleResourceProduction; +import com.buuz135.industrial.recipe.LaserDrillOreRecipe; +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.layout.AdvancedAlloyFurnaceLayout; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Industrial Foregoing 矿物镭射钻配方适配器。 + * + *

镭射钻用透镜颜色筛选、再加权随机出一种矿;合金炉是确定性匹配。因此这里按「透镜 + 实体需求」 + * 分组,每组合成一条配方:输入 1000mB 水,模具为矿物镭射基座 + 该组透镜(需要实体的组再加一个刷怪蛋 + * 模具),一次性产出该组所有可能的矿物。

+ * + *

合金炉单机只支持一个模具,所以这些多模具配方只能在多方块万象炉的模具集线器里使用。

+ */ +public final class LaserDrillOreRecipeAdapter + implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final int WATER_AMOUNT = 1000; + + private volatile Cached cached; + + @Override + public String sourceId() { + return RecipeSourceIds.INDUSTRIAL_FOREGOING; + } + + @Override + public Class getRecipeClass() { + return IndustrialForegoingSyntheticRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(ModuleResourceProduction.ORE_LASER_BASE.getLeft().get()); + } + + @Override + public List> getGeneratedRecipes(Level level) { + if (level == null) return List.of(); + RecipeManager manager = level.getRecipeManager(); + if (manager == null) return List.of(); + + Cached snapshot = cached; + if (snapshot != null && snapshot.manager() == manager) return snapshot.recipes(); + synchronized (this) { + Cached current = cached; + if (current != null && current.manager() == manager) return current.recipes(); + List> built = build(manager); + cached = new Cached(manager, built); + return built; + } + } + + @SuppressWarnings("unchecked") + private List> build(RecipeManager manager) { + RecipeType type = + (RecipeType) ModuleCore.LASER_DRILL_TYPE.get(); + + Map groups = new LinkedHashMap<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(manager, type)) { + LaserDrillOreRecipe source = holder == null ? null : holder.value(); + if (source == null) continue; + + String lensKey = LaserDrillAdapterSupport.ingredientKey(source.catalyst); + if (lensKey.isEmpty()) continue; + + ItemStack output = LaserDrillAdapterSupport.representativeOutput(source.output); + // 标签在当前整合包里没有内容时跳过该配方 + if (output.isEmpty()) continue; + + EntityType entity = null /* Native ore recipes have no entity requirement. */; + String entityKey = entity == null ? "" : String.valueOf(BuiltInRegistries.ENTITY_TYPE.getKey(entity)); + groups.computeIfAbsent(lensKey + "|" + entityKey, key -> new Group(source.catalyst, entity)) + .add(output); + } + + List recipes = new ArrayList<>(groups.size()); + for (Map.Entry entry : groups.entrySet()) { + AdvancedAlloyFurnaceRecipe recipe = convertGroup(entry.getKey(), entry.getValue()); + if (recipe != null) recipes.add(recipe); + } + return IndustrialForegoingRecipeAdapterUtils.holders(recipes); + } + + @Nullable + private AdvancedAlloyFurnaceRecipe convertGroup(String groupKey, Group group) { + List outputs = group.outputs(); + if (outputs.isEmpty()) return null; + if (outputs.size() > AdvancedAlloyFurnaceLayout.OUTPUT_SLOTS_COUNT) { + RateLimitedLogger.warn(LOGGER, "Laser drill lens group {} produces {} results, keeping the first {}", + groupKey, outputs.size(), AdvancedAlloyFurnaceLayout.OUTPUT_SLOTS_COUNT); + outputs = outputs.subList(0, AdvancedAlloyFurnaceLayout.OUTPUT_SLOTS_COUNT); + } + + Ingredient baseMold = AdapterUtils.toMoldIngredient(getMoldItem()); + if (baseMold.isEmpty()) return null; + List molds = new ArrayList<>(3); + molds.add(baseMold); + molds.add(group.catalyst()); + Ingredient spawnEgg = LaserDrillAdapterSupport.spawnEggMold(group.entity()); + if (!spawnEgg.isEmpty()) molds.add(spawnEgg); + + int processTime = IndustrialForegoingRecipeAdapterUtils.positive(OreLaserBaseConfig.maxProgress); + long energy = IndustrialForegoingRecipeAdapterUtils.energyPerTick( + LaserDrillConfig.powerPerOperation, processTime); + + return new AdvancedAlloyFurnaceRecipe( + new ResourceLocation(RecipeSourceIds.INDUSTRIAL_FOREGOING, + "laser_drill_ore_" + LaserDrillAdapterSupport.sanitize(groupKey)), + List.of(), + List.of(LongSizedFluidIngredient.from(new FluidStack(Fluids.WATER, WATER_AMOUNT))), + List.of(), + List.copyOf(outputs), + List.of(), + List.of(), + energy, + processTime, + Ingredient.EMPTY, + 0, + List.copyOf(molds), + AlloyFurnaceMode.NORMAL); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null + || holder.value().convertedRecipe() == null) return List.of(); + return List.of(holder.value().convertedRecipe()); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) return List.of(); + List> matches = new ArrayList<>(); + for (RecipeHolder holder : getGeneratedRecipes(level)) { + if (IndustrialForegoingRecipeAdapterUtils.matches( + holder.value().convertedRecipe(), mergedInputs, mergedFluids)) { + matches.add(holder); + } + } + return List.copyOf(matches); + } + + private record Cached(RecipeManager manager, + List> recipes) { + } + + /** One lens colour (plus optional entity requirement) and every result it can produce. */ + private static final class Group { + private final Ingredient catalyst; + private final EntityType entity; + private final List outputs = new ArrayList<>(); + + private Group(Ingredient catalyst, @Nullable EntityType entity) { + this.catalyst = catalyst; + this.entity = entity; + } + + private Ingredient catalyst() { + return catalyst; + } + + @Nullable + private EntityType entity() { + return entity; + } + + private void add(ItemStack stack) { + for (ItemStack existing : outputs) { + if (ItemStack.isSameItemSameTags(existing, stack)) { + existing.grow(stack.getCount()); + return; + } + } + outputs.add(stack); + } + + private List outputs() { + List sorted = new ArrayList<>(outputs); + sorted.sort(Comparator.comparing(stack -> + String.valueOf(BuiltInRegistries.ITEM.getKey(stack.getItem())))); + return sorted; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/LatexProcessingUnitRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/LatexProcessingUnitRecipeAdapter.java new file mode 100644 index 00000000..09682f98 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/LatexProcessingUnitRecipeAdapter.java @@ -0,0 +1,88 @@ +package com.sorrowmist.useless.content.recipe.adapters.industrialforegoing; + +import com.buuz135.industrial.config.machine.core.LatexProcessingUnitConfig; +import com.buuz135.industrial.module.ModuleCore; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Map; + +/** Converts Industrial Foregoing's latex plus water processing operation. */ +public final class LatexProcessingUnitRecipeAdapter + implements IRecipeAdapter { + private static final int LATEX_AMOUNT = 750; + private static final int WATER_AMOUNT = 500; + + @Override + public String sourceId() { + return RecipeSourceIds.INDUSTRIAL_FOREGOING; + } + + @Override + public Class getRecipeClass() { + return IndustrialForegoingSyntheticRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(ModuleCore.LATEX_PROCESSING.getLeft().get()); + } + + @Override + public List> getGeneratedRecipes(Level level) { + if (level == null) return List.of(); + ResourceLocation id = new ResourceLocation( + RecipeSourceIds.INDUSTRIAL_FOREGOING, "latex_processing_unit"); + AdvancedAlloyFurnaceRecipe recipe = new AdvancedAlloyFurnaceRecipe( + id, List.of(), List.of( + new LongSizedFluidIngredient( + FluidIngredient.single(ModuleCore.LATEX.getSourceFluid().get()), LATEX_AMOUNT), + new LongSizedFluidIngredient( + FluidIngredient.single(Fluids.WATER), WATER_AMOUNT)), + List.of(), List.of(ModuleCore.DRY_RUBBER.get().getDefaultInstance()), List.of(), List.of(), + IndustrialForegoingRecipeAdapterUtils.energyPerTick( + LatexProcessingUnitConfig.powerPerTick, + LatexProcessingUnitConfig.maxProgress), + IndustrialForegoingRecipeAdapterUtils.positive(LatexProcessingUnitConfig.maxProgress), + Ingredient.EMPTY, 0, + List.of(Ingredient.of(new ItemStack(ModuleCore.LATEX_PROCESSING.getLeft().get()))), + AlloyFurnaceMode.NORMAL); + return IndustrialForegoingRecipeAdapterUtils.holders(List.of(recipe)); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null + || holder.value().convertedRecipe() == null) return List.of(); + return List.of(holder.value().convertedRecipe()); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) return List.of(); + List> matches = new java.util.ArrayList<>(); + for (RecipeHolder holder : getGeneratedRecipes(level)) { + if (IndustrialForegoingRecipeAdapterUtils.matches( + holder.value().convertedRecipe(), mergedInputs, mergedFluids)) { + matches.add(holder); + } + } + return List.copyOf(matches); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/PrecisionShapedRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/PrecisionShapedRecipeAdapter.java new file mode 100644 index 00000000..793b8d9d --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/PrecisionShapedRecipeAdapter.java @@ -0,0 +1,79 @@ +package com.sorrowmist.useless.content.recipe.adapters.industrialforegoing; + +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; +import net.yxiao233.ifeu.common.recipe.PrecisionShapedRecipe; +import net.yxiao233.ifeu.common.registry.IFEUBlocks; +import net.yxiao233.ifeu.common.registry.IFEURecipes; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Extra Upgrades precision shaped recipes. */ +public final class PrecisionShapedRecipeAdapter + implements IRecipeAdapter { + @Override + public String sourceId() { + return RecipeSourceIds.INDUSTRIAL_FOREGOING; + } + + @Override + public Class getRecipeClass() { + return PrecisionShapedRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(IFEUBlocks.PRECISION_CRAFTING_TABLE.getBlock()); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) return List.of(); + PrecisionShapedRecipe source = holder.value(); + AdvancedAlloyFurnaceRecipe converted = ExtraUpgradesCraftingRecipeAdapterSupport.precisionRecipe( + com.sorrowmist.useless.content.recipe.AdapterUtils.convertedId(holder.id()), + source.inputs, source.output, source.chance, getMoldItem()); + return converted == null ? List.of() : List.of(converted); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) return List.of(); + RecipeType type = recipeType(); + if (type == null) return List.of(); + RecipeManager manager = level.getRecipeManager(); + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(manager, type)) { + List converted = convertAll(holder, level); + if (!converted.isEmpty() + && ExtraUpgradesCraftingRecipeAdapterSupport.matches( + converted.get(0), mergedInputs, mergedFluids)) { + matches.add(holder); + } + } + return List.copyOf(matches); + } + + @SuppressWarnings("unchecked") + @Nullable + private static RecipeType recipeType() { + if (IFEURecipes.PRECISION_SHAPED_TYPE == null + || IFEURecipes.PRECISION_SHAPED_TYPE.get() == null) return null; + return (RecipeType) (RecipeType) + IFEURecipes.PRECISION_SHAPED_TYPE.get(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/PrecisionShapelessRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/PrecisionShapelessRecipeAdapter.java new file mode 100644 index 00000000..8d13e964 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/PrecisionShapelessRecipeAdapter.java @@ -0,0 +1,79 @@ +package com.sorrowmist.useless.content.recipe.adapters.industrialforegoing; + +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; +import net.yxiao233.ifeu.common.recipe.PrecisionShapelessRecipe; +import net.yxiao233.ifeu.common.registry.IFEUBlocks; +import net.yxiao233.ifeu.common.registry.IFEURecipes; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Extra Upgrades precision shapeless recipes. */ +public final class PrecisionShapelessRecipeAdapter + implements IRecipeAdapter { + @Override + public String sourceId() { + return RecipeSourceIds.INDUSTRIAL_FOREGOING; + } + + @Override + public Class getRecipeClass() { + return PrecisionShapelessRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(IFEUBlocks.PRECISION_CRAFTING_TABLE.getBlock()); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) return List.of(); + PrecisionShapelessRecipe source = holder.value(); + AdvancedAlloyFurnaceRecipe converted = ExtraUpgradesCraftingRecipeAdapterSupport.precisionRecipe( + com.sorrowmist.useless.content.recipe.AdapterUtils.convertedId(holder.id()), + source.inputs, source.output, source.chance, getMoldItem()); + return converted == null ? List.of() : List.of(converted); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) return List.of(); + RecipeType type = recipeType(); + if (type == null) return List.of(); + RecipeManager manager = level.getRecipeManager(); + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(manager, type)) { + List converted = convertAll(holder, level); + if (!converted.isEmpty() + && ExtraUpgradesCraftingRecipeAdapterSupport.matches( + converted.get(0), mergedInputs, mergedFluids)) { + matches.add(holder); + } + } + return List.copyOf(matches); + } + + @SuppressWarnings("unchecked") + @Nullable + private static RecipeType recipeType() { + if (IFEURecipes.PRECISION_SHAPELESS_TYPE == null + || IFEURecipes.PRECISION_SHAPELESS_TYPE.get() == null) return null; + return (RecipeType) (RecipeType) + IFEURecipes.PRECISION_SHAPELESS_TYPE.get(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/SlaughterFactoryRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/SlaughterFactoryRecipeAdapter.java new file mode 100644 index 00000000..71c362f5 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/SlaughterFactoryRecipeAdapter.java @@ -0,0 +1,104 @@ +package com.sorrowmist.useless.content.recipe.adapters.industrialforegoing; + +import com.buuz135.industrial.config.machine.agriculturehusbandry.SlaughterFactoryConfig; +import com.buuz135.industrial.module.ModuleAgricultureHusbandry; +import com.buuz135.industrial.module.ModuleCore; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts the dynamic Industrial Foregoing slaughter operation to a fixed fluid recipe. */ +public final class SlaughterFactoryRecipeAdapter + implements IRecipeAdapter { + private static final int WATER_AMOUNT = 1000; + private static final int MEAT_AMOUNT = 100; + private static final int PINK_SLIME_AMOUNT = 1000; + private static final int ENERGY_MULTIPLIER = 100; + + @Override + public String sourceId() { + return RecipeSourceIds.INDUSTRIAL_FOREGOING; + } + + @Override + public Class getRecipeClass() { + return IndustrialForegoingSyntheticRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(ModuleAgricultureHusbandry.SLAUGHTER_FACTORY.getLeft().get()); + } + + @Override + public List> getGeneratedRecipes(Level level) { + if (level == null) return List.of(); + + ResourceLocation id = new ResourceLocation( + RecipeSourceIds.INDUSTRIAL_FOREGOING, "mob_slaughter_factory"); + int processTime = IndustrialForegoingRecipeAdapterUtils.positive( + SlaughterFactoryConfig.maxProgress); + long energy = Math.max(1L, IndustrialForegoingRecipeAdapterUtils.multiply( + IndustrialForegoingRecipeAdapterUtils.positive( + SlaughterFactoryConfig.powerPerOperation), + ENERGY_MULTIPLIER)); + AdvancedAlloyFurnaceRecipe recipe = new AdvancedAlloyFurnaceRecipe( + id, + List.of(), + List.of(new LongSizedFluidIngredient( + FluidIngredient.single(Fluids.WATER), WATER_AMOUNT)), + List.of(), + List.of(), + List.of( + new FluidStack(ModuleCore.MEAT.getSourceFluid().get(), MEAT_AMOUNT), + new FluidStack(ModuleCore.PINK_SLIME.getSourceFluid().get(), PINK_SLIME_AMOUNT)), + List.of(), + energy, + processTime, + Ingredient.EMPTY, + 0, + List.of(Ingredient.of(new ItemStack( + ModuleAgricultureHusbandry.SLAUGHTER_FACTORY.getLeft().get()))), + AlloyFurnaceMode.NORMAL); + return IndustrialForegoingRecipeAdapterUtils.holders(List.of(recipe)); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null + || holder.value().convertedRecipe() == null) return List.of(); + return List.of(holder.value().convertedRecipe()); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) return List.of(); + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : getGeneratedRecipes(level)) { + if (IndustrialForegoingRecipeAdapterUtils.matches( + holder.value().convertedRecipe(), mergedInputs, mergedFluids)) { + matches.add(holder); + } + } + return List.copyOf(matches); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/WashingFactoryRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/WashingFactoryRecipeAdapter.java new file mode 100644 index 00000000..474416c9 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/WashingFactoryRecipeAdapter.java @@ -0,0 +1,92 @@ +package com.sorrowmist.useless.content.recipe.adapters.industrialforegoing; + +import com.buuz135.industrial.config.machine.resourceproduction.WashingFactoryConfig; +import com.buuz135.industrial.module.ModuleCore; +import com.buuz135.industrial.module.ModuleResourceProduction; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Industrial Foregoing's tag-driven ore washing recipes. */ +public final class WashingFactoryRecipeAdapter + implements IRecipeAdapter { + private static final int FLUID_AMOUNT = 100; + + @Override + public String sourceId() { + return RecipeSourceIds.INDUSTRIAL_FOREGOING; + } + + @Override + public Class getRecipeClass() { + return IndustrialForegoingSyntheticRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(ModuleResourceProduction.WASHING_FACTORY.getLeft().get()); + } + + @Override + public List> getGeneratedRecipes(Level level) { + if (level == null) return List.of(); + List recipes = new ArrayList<>(); + for (ResourceLocation rawTag : IndustrialForegoingRecipeAdapterUtils.validRawMaterialTags()) { + FluidStack meat = new FluidStack(ModuleCore.MEAT.getSourceFluid().get(), FLUID_AMOUNT); + FluidStack output = IndustrialForegoingRecipeAdapterUtils.rawOreMeat(rawTag, FLUID_AMOUNT); + ResourceLocation id = new ResourceLocation( + RecipeSourceIds.INDUSTRIAL_FOREGOING, + "washing_factory_" + IndustrialForegoingRecipeAdapterUtils.tagPath(rawTag)); + int processTime = IndustrialForegoingRecipeAdapterUtils.positive( + WashingFactoryConfig.maxProgress); + recipes.add(new AdvancedAlloyFurnaceRecipe( + id, + List.of(new CountedIngredient( + Ingredient.of(IndustrialForegoingRecipeAdapterUtils.itemTag(rawTag)), 1L)), + List.of(LongSizedFluidIngredient.from(meat)), List.of(), List.of(), + List.of(output), List.of(), IndustrialForegoingRecipeAdapterUtils.energyPerTick( + WashingFactoryConfig.powerPerTick, processTime), processTime, + Ingredient.EMPTY, 0, + List.of(Ingredient.of(new ItemStack(ModuleResourceProduction.WASHING_FACTORY.getLeft().get()))), + AlloyFurnaceMode.NORMAL)); + } + return IndustrialForegoingRecipeAdapterUtils.holders(recipes); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null + || holder.value().convertedRecipe() == null) return List.of(); + return List.of(holder.value().convertedRecipe()); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) return List.of(); + List> matches = new ArrayList<>(); + for (RecipeHolder holder : getGeneratedRecipes(level)) { + if (IndustrialForegoingRecipeAdapterUtils.matches( + holder.value().convertedRecipe(), mergedInputs, mergedFluids)) { + matches.add(holder); + } + } + return List.copyOf(matches); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ironsspellbooks/AlchemistCauldronDynamicRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ironsspellbooks/AlchemistCauldronDynamicRecipeAdapter.java new file mode 100644 index 00000000..96fc7fec --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ironsspellbooks/AlchemistCauldronDynamicRecipeAdapter.java @@ -0,0 +1,387 @@ +package com.sorrowmist.useless.content.recipe.adapters.ironsspellbooks; + +import appeng.api.stacks.AEKey; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.ItemIngredientAllocator; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import io.redspace.ironsspellbooks.api.registry.SpellRegistry; +import io.redspace.ironsspellbooks.api.spells.AbstractSpell; +import io.redspace.ironsspellbooks.api.spells.ISpellContainer; +import io.redspace.ironsspellbooks.api.spells.SpellRarity; +import io.redspace.ironsspellbooks.config.ServerConfigs; +import io.redspace.ironsspellbooks.fluids.PotionFluid; +import io.redspace.ironsspellbooks.item.InkItem; +import io.redspace.ironsspellbooks.registries.ItemRegistry; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.alchemy.Potion; +import net.minecraft.world.item.alchemy.PotionBrewing; +import net.minecraft.world.item.alchemy.PotionContents; +import net.minecraft.world.item.alchemy.Potions; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.Recipe; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.Container; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluids; +import com.sorrowmist.useless.compat.forge1201.recipe.DataComponentIngredient; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.ConcurrentModificationException; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Generates item-only recipes for the dynamic alchemist-cauldron interactions. */ +public final class AlchemistCauldronDynamicRecipeAdapter + implements IRecipeAdapter { + private static final int POTION_AMOUNT = 250; + private static final int PROCESS_TIME = 100; + private static final Item[] POTION_CONTAINERS = { + Items.POTION, + Items.SPLASH_POTION, + Items.LINGERING_POTION + }; + /** 单次构建在遇到并发修改时的最大重试次数。 */ + private static final int MAX_BUILD_ATTEMPTS = 32; + + private volatile PotionBrewing cachedBrewing; + private volatile boolean cachedCauldronBrewing; + private volatile List> cachedRecipes = List.of(); + + @Override + public String sourceId() { + return RecipeSourceIds.IRONS_SPELLBOOKS; + } + + @Override + public Class getRecipeClass() { + return DynamicRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(ItemRegistry.ALCHEMIST_CAULDRON_BLOCK_ITEM.get()); + } + + @Override + public List> getGeneratedRecipes(Level level) { + if (level == null) { + return List.of(); + } + + PotionBrewing brewing = level.potionBrewing(); + boolean allowCauldronBrewing = ServerConfigs.ALLOW_CAULDRON_BREWING.get(); + if (cachedBrewing == brewing && cachedCauldronBrewing == allowCauldronBrewing) { + return cachedRecipes; + } + + synchronized (this) { + if (cachedBrewing != brewing || cachedCauldronBrewing != allowCauldronBrewing) { + cachedRecipes = buildRecipesWithRetry(brewing, allowCauldronBrewing); + cachedBrewing = brewing; + cachedCauldronBrewing = allowCauldronBrewing; + } + return cachedRecipes; + } + } + + /** + * 在 PotionBrewing 上构建配方,并在并发注册窗口内重试。 + * + *

本方法由后台线程调用,而登录与数据包同步期间主线程会向 PotionBrewing 注册混合配方。 + * 该类的查询方法直接迭代其内部列表,没有可供复制快照的入口,因此注册与查询重叠时 + * 迭代器会抛出 {@link ConcurrentModificationException},使对应条目被跳过。注册是一次性 + * 且短暂的,让出 CPU 后重试即可读到完整列表。

+ * + * @throws ConcurrentModificationException 重试耗尽后仍处于并发修改状态 + */ + private static List> buildRecipesWithRetry( + @Nullable PotionBrewing brewing, boolean allowCauldronBrewing) { + for (int attempt = 0; ; attempt++) { + try { + return createRecipes(brewing, allowCauldronBrewing); + } catch (ConcurrentModificationException exception) { + if (attempt >= MAX_BUILD_ATTEMPTS - 1) { + throw exception; + } + try { + Thread.sleep(1L); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw exception; + } + } + } + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null + || holder.value().convertedRecipe() == null) { + return List.of(); + } + return List.of(holder.value().convertedRecipe()); + } + + @Override + public List> findMatchingRecipes( + Level level, + Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + return findMatchingRecipes(level, mergedInputs, mergedFluids, Map.of(), mold, List.of()); + } + + @Override + public List> findMatchingRecipes( + Level level, + Map mergedInputs, + Map mergedFluids, + Map mergedKeys, + @Nullable ItemStack mold, + List actualInputs) { + if (level == null || !matchesMold(mold)) { + return List.of(); + } + + List> matchingRecipes = new ArrayList<>(); + for (RecipeHolder holder : getGeneratedRecipes(level)) { + AdvancedAlloyFurnaceRecipe recipe = holder.value().convertedRecipe(); + boolean matches = actualInputs != null && !actualInputs.isEmpty() + ? ItemIngredientAllocator.matches(recipe.inputs(), actualInputs, 1L) + : matchesMergedInputs(recipe, mergedInputs); + if (matches) { + matchingRecipes.add(holder); + } + } + return List.copyOf(matchingRecipes); + } + + private static List> createRecipes( + @Nullable PotionBrewing brewing, boolean allowCauldronBrewing) { + Map> recipes = new LinkedHashMap<>(); + addScrollRecipes(recipes); + if (allowCauldronBrewing && brewing != null) { + addPotionRecipes(recipes, brewing); + } + return List.copyOf(recipes.values()); + } + + private static void addScrollRecipes( + Map> recipes) { + ItemStack waterBottle = waterBottle(); + if (waterBottle.isEmpty()) { + return; + } + + for (AbstractSpell spell : SpellRegistry.getEnabledSpells()) { + for (int level = spell.getMinLevel(); level <= spell.getMaxLevel(); level++) { + SpellRarity rarity = spell.getRarity(level); + if (rarity == null) { + continue; + } + + ItemStack scroll = new ItemStack(ItemRegistry.SCROLL.get()); + ISpellContainer.createScrollContainer(spell, level, scroll); + ItemStack ink = InkItem.getInkForRarity(rarity).getDefaultInstance(); + ResourceLocation id = new ResourceLocation( + "useless_mod", + "irons_spellbooks/alchemist_cauldron/scroll_" + + idPart(spell.getSpellId()) + "_level_" + level); + addRecipe(recipes, createHolder(id, List.of( + new CountedIngredient(exact(waterBottle), 1L), + new CountedIngredient(exact(scroll), 1L)), + List.of(ink))); + } + } + } + + private static void addPotionRecipes( + Map> recipes, + PotionBrewing brewing) { + List reagents = AdapterUtils.reagentCandidates(brewing); + List> potions = potionHolders(); + for (Item container : POTION_CONTAINERS) { + for (Holder potion : potions) { + ItemStack input = PotionContents.createItemStack(container, potion); + if (PotionFluid.from(input).isEmpty()) { + continue; + } + + for (Item reagent : reagents) { + ItemStack reagentStack = reagent.getDefaultInstance(); + boolean hasPotionMix; + try { + hasPotionMix = brewing.hasPotionMix(input, reagentStack) + || brewing.hasContainerMix(input, reagentStack); + } catch (ConcurrentModificationException exception) { + // 并发注册窗口内的读取失败不能按「该组合无配方」处理,否则条目会静默缺失; + // 交由外层重试,读到完整列表后重新判定。 + throw exception; + } catch (RuntimeException ignored) { + continue; + } + if (!hasPotionMix) { + continue; + } + + ItemStack result; + try { + result = brewing.mix(reagentStack, input); + } catch (ConcurrentModificationException exception) { + // 同上:并发修改不是「无配方」,交由外层重试。 + throw exception; + } catch (RuntimeException ignored) { + continue; + } + FluidStack resultFluid = PotionFluid.from(result); + if (resultFluid.isEmpty()) { + continue; + } + ItemStack resultItem = PotionFluid.from(resultFluid); + if (resultItem.isEmpty()) { + continue; + } + + ResourceLocation id = new ResourceLocation( + "useless_mod", + "irons_spellbooks/alchemist_cauldron/potion_" + + idPart(BuiltInRegistries.ITEM.getKey(container)) + "_" + + idPart(BuiltInRegistries.POTION.getKey(potion.value())) + "_" + + idPart(BuiltInRegistries.ITEM.getKey(reagent))); + addRecipe(recipes, createHolder(id, List.of( + new CountedIngredient(exact(input), 1L), + new CountedIngredient(exact(reagentStack), 1L)), + List.of(resultItem))); + } + } + } + } + + private static List> potionHolders() { + return BuiltInRegistries.POTION.holders() + .map(holder -> (Holder) holder) + .toList(); + } + + private static ItemStack waterBottle() { + ItemStack water = PotionFluid.from(new FluidStack(Fluids.WATER, POTION_AMOUNT)); + return water.isEmpty() + ? PotionContents.createItemStack(Items.POTION, Potions.WATER) + : water; + } + + private static Ingredient exact(ItemStack stack) { + return DataComponentIngredient.of(true, stack.copyWithCount(1)); + } + + private static boolean matchesMergedInputs( + AdvancedAlloyFurnaceRecipe recipe, @Nullable Map mergedInputs) { + if (mergedInputs == null || mergedInputs.isEmpty()) { + return false; + } + Map required = new LinkedHashMap<>(); + for (CountedIngredient input : recipe.inputs()) { + AdapterUtils.mergeIngredient(required, input.ingredient(), input.count()); + } + return AdapterUtils.matchesRequired(mergedInputs, required); + } + + private static AdvancedAlloyFurnaceRecipe converted( + ResourceLocation id, List inputs, List outputs) { + return new AdvancedAlloyFurnaceRecipe( + id, + inputs, + List.of(), + List.of(), + outputs.stream().map(ItemStack::copy).toList(), + List.of(), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + PROCESS_TIME, + Ingredient.EMPTY, + 0, + List.of(AdapterUtils.toMoldIngredient( + new ItemStack(ItemRegistry.ALCHEMIST_CAULDRON_BLOCK_ITEM.get()))), + AlloyFurnaceMode.NORMAL); + } + + private static RecipeHolder createHolder( + ResourceLocation id, List inputs, List outputs) { + return new RecipeHolder<>(id, new DynamicRecipe(converted(id, inputs, outputs))); + } + + private static void addRecipe( + Map> recipes, + RecipeHolder holder) { + recipes.putIfAbsent(holder.id(), holder); + } + + private static String idPart(@Nullable ResourceLocation id) { + return id == null ? "unknown" : (id.getNamespace() + "_" + id.getPath()).replace('/', '_'); + } + + private static String idPart(String id) { + return id.replace(':', '_').replace('/', '_'); + } + + /** Recipe-manager payload for interactions owned by PotionBrewing or spell data. */ + public static final class DynamicRecipe implements Recipe { + private final AdvancedAlloyFurnaceRecipe convertedRecipe; + + private DynamicRecipe(AdvancedAlloyFurnaceRecipe convertedRecipe) { + this.convertedRecipe = convertedRecipe; + } + + public AdvancedAlloyFurnaceRecipe convertedRecipe() { + return convertedRecipe; + } + + @Override + public net.minecraft.resources.ResourceLocation getId() { return convertedRecipe.id(); } + + @Override + public boolean matches(Container input, Level level) { + return convertedRecipe != null && convertedRecipe.matches(input, level); + } + + @Override + public ItemStack assemble(Container input, net.minecraft.core.RegistryAccess registries) { + return convertedRecipe == null ? ItemStack.EMPTY : convertedRecipe.assemble(input, registries); + } + + @Override + public boolean canCraftInDimensions(int width, int height) { + return convertedRecipe != null && convertedRecipe.canCraftInDimensions(width, height); + } + + @Override + public ItemStack getResultItem(net.minecraft.core.RegistryAccess registries) { + return convertedRecipe == null ? ItemStack.EMPTY : convertedRecipe.getResultItem(registries); + } + + @Override + public RecipeSerializer getSerializer() { + return convertedRecipe == null ? null : convertedRecipe.getSerializer(); + } + + @Override + public RecipeType getType() { + return convertedRecipe == null ? null : convertedRecipe.getType(); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ironsspellbooks/AlchemistCauldronRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ironsspellbooks/AlchemistCauldronRecipeAdapter.java new file mode 100644 index 00000000..1dbf27ba --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ironsspellbooks/AlchemistCauldronRecipeAdapter.java @@ -0,0 +1,338 @@ +package com.sorrowmist.useless.content.recipe.adapters.ironsspellbooks; + +import io.redspace.ironsspellbooks.fluids.PotionFluid; +import io.redspace.ironsspellbooks.recipe_types.alchemist_cauldron.BrewAlchemistCauldronRecipe; +import io.redspace.ironsspellbooks.recipe_types.alchemist_cauldron.EmptyAlchemistCauldronRecipe; +import io.redspace.ironsspellbooks.recipe_types.alchemist_cauldron.FillAlchemistCauldronRecipe; +import io.redspace.ironsspellbooks.registries.ItemRegistry; +import io.redspace.ironsspellbooks.registries.RecipeRegistry; +import appeng.api.stacks.AEKey; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import com.sorrowmist.useless.compat.forge1201.recipe.DataComponentIngredient; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Converts Iron's Spells 'n Spellbooks alchemist-cauldron brews into item-only recipes. */ +public final class AlchemistCauldronRecipeAdapter implements IRecipeAdapter { + private static final int MILLIBUCKETS_PER_POTION = 250; + private static final int PROCESS_TIME = 100; + + @Override + public String sourceId() { + return RecipeSourceIds.IRONS_SPELLBOOKS; + } + + @Override + public Class getRecipeClass() { + return BrewAlchemistCauldronRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(ItemRegistry.ALCHEMIST_CAULDRON_BLOCK_ITEM.get()); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null || level == null) { + return List.of(); + } + + BrewAlchemistCauldronRecipe source = holder.value(); + if (source.fluidIn() == null || source.fluidIn().isEmpty() + || source.fluidIn().getAmount() <= 0 + || source.reagent() == null || source.reagent().isEmpty()) { + return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + List fluidInputs = itemInputsForFluid(source.fluidIn(), recipeManager); + if (fluidInputs.isEmpty()) { + return List.of(); + } + + List> outputVariants = outputVariants(source.results(), source.byproduct(), recipeManager); + if (outputVariants.isEmpty() || outputVariants.get(0).isEmpty()) { + return List.of(); + } + + List converted = new ArrayList<>(); + int variant = 0; + for (CountedIngredient fluidInput : fluidInputs) { + for (List output : outputVariants) { + List inputs = List.of( + fluidInput, + new CountedIngredient(source.reagent(), 1L)); + converted.add(new AdvancedAlloyFurnaceRecipe( + variantId(holder.id(), variant++), + inputs, + List.of(), + List.of(), + copyStacks(output), + List.of(), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + PROCESS_TIME, + Ingredient.EMPTY, + 0, + List.of(AdapterUtils.toMoldIngredient(getMoldItem())), + AlloyFurnaceMode.NORMAL)); + } + } + return List.copyOf(converted); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, Map mergedKeys, + @Nullable ItemStack mold, List actualInputs) { + if (level == null || mergedInputs == null || mergedInputs.isEmpty() + || !matchesMold(mold)) { + return List.of(); + } + + List> matches = new ArrayList<>(); + RecipeManager recipeManager = level.getRecipeManager(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + RecipeRegistry.ALCHEMIST_CAULDRON_BREW_TYPE.get())) { + for (AdvancedAlloyFurnaceRecipe converted : convertAll(holder, level)) { + if (AdapterUtils.matchesRequired(mergedInputs, requirements(converted.inputs()))) { + matches.add(holder); + break; + } + } + } + return matches; + } + + private static Map requirements(List inputs) { + Map requirements = new LinkedHashMap<>(); + for (CountedIngredient input : inputs) { + if (input != null && input.ingredient() != null && input.count() > 0) { + AdapterUtils.mergeIngredient(requirements, input.ingredient(), input.count()); + } + } + return requirements; + } + + private static List itemInputsForFluid( + FluidStack fluid, RecipeManager recipeManager) { + long units; + List result = new ArrayList<>(); + + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + RecipeRegistry.ALCHEMIST_CAULDRON_FILL_TYPE.get())) { + FillAlchemistCauldronRecipe recipe = holder.value(); + FluidStack filled = recipe.result(); + units = matchingUnits(fluid, filled); + if (units > 0 && recipe.input() != null && !recipe.input().isEmpty()) { + addDistinctIngredient(result, recipe.input(), units); + } + } + + if (!result.isEmpty()) { + return List.copyOf(result); + } + + // A custom datapack may define only the emptying direction. Use it as a fallback, + // without mixing it with the normal filling-direction choices above. + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + RecipeRegistry.ALCHEMIST_CAULDRON_EMPTY_TYPE.get())) { + EmptyAlchemistCauldronRecipe recipe = holder.value(); + units = matchingUnits(fluid, recipe.fluid()); + ItemStack resultStack = recipe.result(); + if (units > 0 && resultStack != null && !resultStack.isEmpty()) { + long count = safeMultiply(units, resultStack.getCount()); + if (count > 0) { + addDistinctIngredient(result, exact(resultStack), count); + } + } + } + + if (!result.isEmpty()) { + return List.copyOf(result); + } + + ItemStack potion = potionItem(fluid); + if (!potion.isEmpty()) { + units = fluid.getAmount() / MILLIBUCKETS_PER_POTION; + return units > 0 && fluid.getAmount() % MILLIBUCKETS_PER_POTION == 0 + ? List.of(new CountedIngredient(exact(potion), units)) + : List.of(); + } + return List.of(); + } + + private static List> outputVariants( + List fluids, java.util.Optional byproduct, + RecipeManager recipeManager) { + List> variants = new ArrayList<>(); + variants.add(new ArrayList<>()); + + if (fluids != null) { + for (FluidStack fluid : fluids) { + List choices = itemOutputsForFluid(fluid, recipeManager); + if (choices.isEmpty()) { + return List.of(); + } + List> next = new ArrayList<>(); + for (List prefix : variants) { + for (ItemStack choice : choices) { + List combined = new ArrayList<>(prefix.size() + 1); + combined.addAll(copyStacks(prefix)); + combined.add(choice.copy()); + next.add(combined); + } + } + variants = next; + } + } + + if (byproduct != null && byproduct.isPresent() + && byproduct.get() != null && !byproduct.get().isEmpty()) { + for (List variant : variants) { + variant.add(byproduct.get().copy()); + } + } + return variants.stream().map(List::copyOf).toList(); + } + + private static List itemOutputsForFluid(FluidStack fluid, RecipeManager recipeManager) { + if (fluid == null || fluid.isEmpty() || fluid.getAmount() <= 0) { + return List.of(); + } + + List result = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + RecipeRegistry.ALCHEMIST_CAULDRON_EMPTY_TYPE.get())) { + EmptyAlchemistCauldronRecipe recipe = holder.value(); + long units = matchingUnits(fluid, recipe.fluid()); + ItemStack stack = recipe.result(); + long count = stack.isEmpty() ? 0 : safeMultiply(units, stack.getCount()); + if (count > 0 && count <= Integer.MAX_VALUE) { + addDistinctStack(result, stack.copyWithCount((int) count)); + } + } + + if (!result.isEmpty()) { + return List.copyOf(result); + } + + // Fall back to filling recipes for custom fluids that have no item-emptying recipe. + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + RecipeRegistry.ALCHEMIST_CAULDRON_FILL_TYPE.get())) { + FillAlchemistCauldronRecipe recipe = holder.value(); + long units = matchingUnits(fluid, recipe.result()); + if (units <= 0 || recipe.input() == null || recipe.input().isEmpty()) { + continue; + } + int count = toIntCount(units); + if (count <= 0) { + continue; + } + for (ItemStack stack : recipe.input().getItems()) { + if (!stack.isEmpty()) { + addDistinctStack(result, stack.copyWithCount(count)); + } + } + } + + if (!result.isEmpty()) { + return List.copyOf(result); + } + + ItemStack potion = potionItem(fluid); + if (potion.isEmpty() || fluid.getAmount() % MILLIBUCKETS_PER_POTION != 0) { + return List.of(); + } + long units = fluid.getAmount() / MILLIBUCKETS_PER_POTION; + return units <= Integer.MAX_VALUE + ? List.of(potion.copyWithCount((int) units)) + : List.of(); + } + + private static long matchingUnits(FluidStack requested, FluidStack conversion) { + if (requested == null || requested.isEmpty() || conversion == null || conversion.isEmpty() + || conversion.getAmount() <= 0 + || !com.sorrowmist.useless.compat.forge1201.fluid.FluidStacks.same(requested, conversion) + || requested.getAmount() % conversion.getAmount() != 0) { + return 0; + } + return requested.getAmount() / conversion.getAmount(); + } + + @Nullable + private static ItemStack potionItem(FluidStack fluid) { + try { + return PotionFluid.from(fluid); + } catch (RuntimeException ignored) { + return ItemStack.EMPTY; + } + } + + private static Ingredient exact(ItemStack stack) { + return DataComponentIngredient.of(true, stack.copyWithCount(1)); + } + + private static void addDistinctIngredient( + List target, Ingredient ingredient, long count) { + for (CountedIngredient existing : target) { + if (existing.count() == count + && AdapterUtils.areIngredientsEqual(existing.ingredient(), ingredient)) { + return; + } + } + target.add(new CountedIngredient(ingredient, count)); + } + + private static void addDistinctStack(List target, ItemStack candidate) { + for (ItemStack existing : target) { + if (existing.getCount() == candidate.getCount() + && ItemStack.isSameItemSameTags(existing, candidate)) { + return; + } + } + target.add(candidate); + } + + private static List copyStacks(List stacks) { + return stacks.stream().map(ItemStack::copy).toList(); + } + + private static long safeMultiply(long left, long right) { + if (left <= 0 || right <= 0 || left > Long.MAX_VALUE / right) { + return 0; + } + return left * right; + } + + private static int toIntCount(long count) { + return count > 0 && count <= Integer.MAX_VALUE ? (int) count : 0; + } + + private static ResourceLocation variantId(ResourceLocation original, int variant) { + if (variant == 0) { + return AdapterUtils.convertedId(original); + } + return new ResourceLocation( + original.getNamespace(), original.getPath() + "_items_" + variant + "_converted"); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/justdirethings/FluidDropRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/justdirethings/FluidDropRecipeAdapter.java new file mode 100644 index 00000000..4db36d04 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/justdirethings/FluidDropRecipeAdapter.java @@ -0,0 +1,91 @@ +package com.sorrowmist.useless.content.recipe.adapters.justdirethings; + +import com.direwolf20.justdirethings.datagen.recipes.FluidDropRecipe; +import com.direwolf20.justdirethings.setup.Registration; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluid; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public final class FluidDropRecipeAdapter implements IRecipeAdapter { + @Override + public String sourceId() { + return RecipeSourceIds.JUSTDIRETHINGS; + } + + @Override + public Class getRecipeClass() { + return FluidDropRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return null; + } + + @Override + @Nullable + public AdvancedAlloyFurnaceRecipe convert(RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) return null; + + FluidDropRecipe source = holder.value(); + Fluid input = JustDireThingsRecipeAdapterUtils.fluid(source.getInput()); + Fluid output = JustDireThingsRecipeAdapterUtils.fluid(source.getOutput()); + ItemStack catalyst = source.getCatalyst() == null + ? ItemStack.EMPTY : new ItemStack(source.getCatalyst()); + if (input == null || output == null || catalyst.isEmpty()) return null; + + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + List.of(new CountedIngredient(Ingredient.of(catalyst), 1L)), + JustDireThingsRecipeAdapterUtils.fluidInput(input), + List.of(), + List.of(), + List.of(new FluidStack(output, JustDireThingsRecipeAdapterUtils.FLUID_AMOUNT)), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + AdapterUtils.DEFAULT_PROCESS_TIME, + Ingredient.EMPTY, + 0, + List.of(Ingredient.of(catalyst)), + AlloyFurnaceMode.NORMAL); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || mold == null || mold.isEmpty()) return List.of(); + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(level.getRecipeManager(), Registration.FLUID_DROP_RECIPE_TYPE.get())) { + FluidDropRecipe source = holder.value(); + Fluid input = JustDireThingsRecipeAdapterUtils.fluid(source.getInput()); + Fluid output = JustDireThingsRecipeAdapterUtils.fluid(source.getOutput()); + Ingredient catalyst = source.getCatalyst() == null + ? Ingredient.EMPTY : Ingredient.of(source.getCatalyst()); + if (input != null && output != null + && source.getCatalyst() != null + && mold.is(source.getCatalyst()) + && JustDireThingsRecipeAdapterUtils.matchesItem(catalyst, mergedInputs) + && JustDireThingsRecipeAdapterUtils.matchesFluid(input, mergedFluids)) { + matches.add(holder); + } + } + return List.copyOf(matches); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/justdirethings/GooSpreadRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/justdirethings/GooSpreadRecipeAdapter.java new file mode 100644 index 00000000..8f854689 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/justdirethings/GooSpreadRecipeAdapter.java @@ -0,0 +1,108 @@ +package com.sorrowmist.useless.content.recipe.adapters.justdirethings; + +import com.direwolf20.justdirethings.datagen.recipes.GooSpreadRecipe; +import com.direwolf20.justdirethings.setup.Registration; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluid; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public final class GooSpreadRecipeAdapter implements IRecipeAdapter { + @Override + public String sourceId() { + return RecipeSourceIds.JUSTDIRETHINGS; + } + + @Override + public Class getRecipeClass() { + return GooSpreadRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return null; + } + + @Override + @Nullable + public AdvancedAlloyFurnaceRecipe convert(RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) return null; + + GooSpreadRecipe source = holder.value(); + Ingredient input = JustDireThingsRecipeAdapterUtils.blockInput(source.getInput()); + Fluid inputFluid = input == null + ? JustDireThingsRecipeAdapterUtils.fluid(source.getInput()) : null; + ItemStack output = JustDireThingsRecipeAdapterUtils.blockOutput(source.getOutput()); + Fluid outputFluid = output.isEmpty() + ? JustDireThingsRecipeAdapterUtils.fluid(source.getOutput()) : null; + Ingredient mold = JustDireThingsRecipeAdapterUtils.gooMold(source.getTierRequirement()); + if ((input == null && inputFluid == null) + || (output.isEmpty() && outputFluid == null) + || mold.isEmpty()) return null; + + List itemInputs = input == null + ? List.of() + : List.of(new CountedIngredient(input, 1L)); + List inputFluids = inputFluid == null + ? List.of() + : JustDireThingsRecipeAdapterUtils.fluidInput(inputFluid); + List itemOutputs = output.isEmpty() ? List.of() : List.of(output); + List fluidOutputs = outputFluid == null + ? List.of() + : List.of(new FluidStack(outputFluid, JustDireThingsRecipeAdapterUtils.FLUID_AMOUNT)); + + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + itemInputs, + inputFluids, + List.of(), + itemOutputs, + fluidOutputs, + List.of(), + AdapterUtils.DEFAULT_ENERGY, + Math.max(1, source.getCraftingDuration()), + Ingredient.EMPTY, + 0, + List.of(mold), + AlloyFurnaceMode.NORMAL); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || mold == null || mold.isEmpty()) { + return List.of(); + } + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(level.getRecipeManager(), Registration.GOO_SPREAD_RECIPE_TYPE.get())) { + GooSpreadRecipe source = holder.value(); + ItemStack output = JustDireThingsRecipeAdapterUtils.blockOutput(source.getOutput()); + if (JustDireThingsRecipeAdapterUtils.hasConvertibleInput(source.getInput()) + && (!output.isEmpty() + || JustDireThingsRecipeAdapterUtils.fluid(source.getOutput()) != null) + && JustDireThingsRecipeAdapterUtils.matchesMold(source.getTierRequirement(), mold) + && JustDireThingsRecipeAdapterUtils.matchesInput( + source.getInput(), mergedInputs, mergedFluids)) { + matches.add(holder); + } + } + return List.copyOf(matches); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/justdirethings/GooSpreadRecipeTagAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/justdirethings/GooSpreadRecipeTagAdapter.java new file mode 100644 index 00000000..c900ebb1 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/justdirethings/GooSpreadRecipeTagAdapter.java @@ -0,0 +1,95 @@ +package com.sorrowmist.useless.content.recipe.adapters.justdirethings; + +import com.direwolf20.justdirethings.datagen.recipes.GooSpreadRecipeTag; +import com.direwolf20.justdirethings.setup.Registration; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluid; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public final class GooSpreadRecipeTagAdapter implements IRecipeAdapter { + @Override + public String sourceId() { + return RecipeSourceIds.JUSTDIRETHINGS; + } + + @Override + public Class getRecipeClass() { + return GooSpreadRecipeTag.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return null; + } + + @Override + @Nullable + public AdvancedAlloyFurnaceRecipe convert(RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) return null; + + GooSpreadRecipeTag source = holder.value(); + Ingredient input = JustDireThingsRecipeAdapterUtils.blockTagInput(source.getInput()); + ItemStack output = JustDireThingsRecipeAdapterUtils.blockOutput(source.getOutput()); + Fluid outputFluid = output.isEmpty() + ? JustDireThingsRecipeAdapterUtils.fluid(source.getOutput()) : null; + Ingredient mold = JustDireThingsRecipeAdapterUtils.gooMold(source.getTierRequirement()); + if (input.isEmpty() || (output.isEmpty() && outputFluid == null) || mold.isEmpty()) return null; + + List itemOutputs = output.isEmpty() ? List.of() : List.of(output); + List fluidOutputs = outputFluid == null + ? List.of() + : List.of(new FluidStack(outputFluid, JustDireThingsRecipeAdapterUtils.FLUID_AMOUNT)); + + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + List.of(new CountedIngredient(input, 1L)), + List.of(), + List.of(), + itemOutputs, + fluidOutputs, + List.of(), + AdapterUtils.DEFAULT_ENERGY, + Math.max(1, source.getCraftingDuration()), + Ingredient.EMPTY, + 0, + List.of(mold), + AlloyFurnaceMode.NORMAL); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || mold == null || mold.isEmpty()) return List.of(); + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(level.getRecipeManager(), Registration.GOO_SPREAD_RECIPE_TYPE_TAG.get())) { + GooSpreadRecipeTag source = holder.value(); + Ingredient input = JustDireThingsRecipeAdapterUtils.blockTagInput(source.getInput()); + ItemStack output = JustDireThingsRecipeAdapterUtils.blockOutput(source.getOutput()); + if (!input.isEmpty() + && JustDireThingsRecipeAdapterUtils.matchesMold(source.getTierRequirement(), mold) + && JustDireThingsRecipeAdapterUtils.matchesItem(input, mergedInputs) + && (!output.isEmpty() + || JustDireThingsRecipeAdapterUtils.fluid(source.getOutput()) != null)) { + matches.add(holder); + } + } + return List.copyOf(matches); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/justdirethings/JustDireThingsRecipeAdapterUtils.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/justdirethings/JustDireThingsRecipeAdapterUtils.java new file mode 100644 index 00000000..5ea01330 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/justdirethings/JustDireThingsRecipeAdapterUtils.java @@ -0,0 +1,140 @@ +package com.sorrowmist.useless.content.recipe.adapters.justdirethings; + +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.FluidIngredientAllocator; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import com.direwolf20.justdirethings.setup.Registration; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.common.crafting.BlockTagIngredient; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Map; + +final class JustDireThingsRecipeAdapterUtils { + static final int FLUID_AMOUNT = 1000; + static final int RAW_ORE_DROP_COUNT = 4; + private static final List GOO_ITEMS = List.of( + Registration.GooBlock_Tier1_ITEM.get(), + Registration.GooBlock_Tier2_ITEM.get(), + Registration.GooBlock_Tier3_ITEM.get(), + Registration.GooBlock_Tier4_ITEM.get()); + + private JustDireThingsRecipeAdapterUtils() { + } + + static Ingredient gooMold(int tier) { + if (tier < 1 || tier > GOO_ITEMS.size()) { + return Ingredient.EMPTY; + } + ItemStack[] molds = GOO_ITEMS.subList(tier - 1, GOO_ITEMS.size()).stream() + .map(ItemStack::new) + .toArray(ItemStack[]::new); + return Ingredient.of(molds); + } + + static boolean matchesMold(int tier, @Nullable ItemStack mold) { + Ingredient required = gooMold(tier); + return !required.isEmpty() && mold != null && !mold.isEmpty() && required.test(mold); + } + + @Nullable + static Ingredient blockInput(BlockState state) { + if (state == null) return null; + Item item = state.getBlock().asItem(); + return item == Items.AIR ? null : Ingredient.of(item); + } + + static ItemStack blockOutput(BlockState state) { + ItemStack rawOreDrop = rawOreDrop(state); + if (!rawOreDrop.isEmpty()) return rawOreDrop; + if (state == null) return ItemStack.EMPTY; + + Item item = state.getBlock().asItem(); + return item == Items.AIR ? ItemStack.EMPTY : new ItemStack(item); + } + + static Ingredient blockTagInput(BlockTagIngredient input) { + if (input == null) return Ingredient.EMPTY; + ItemStack[] items = input.getItems().toArray(ItemStack[]::new); + return items.length == 0 ? Ingredient.EMPTY : Ingredient.of(items); + } + + @Nullable + static Fluid fluid(BlockState state) { + if (state == null) return null; + Fluid fluid = state.getFluidState().getType(); + return fluid == Fluids.EMPTY ? null : fluid; + } + + static List fluidInput(Fluid fluid) { + return List.of(LongSizedFluidIngredient.from(new FluidStack(fluid, FLUID_AMOUNT))); + } + + static boolean matchesFluid(Fluid fluid, @Nullable Map available) { + if (fluid == null || available == null || available.isEmpty()) return false; + return FluidIngredientAllocator.matchesLong( + fluidInput(fluid), available, 1L); + } + + static boolean hasConvertibleInput(BlockState state) { + return blockInput(state) != null || fluid(state) != null; + } + + static boolean matchesInput(BlockState state, + @Nullable Map mergedInputs, + @Nullable Map mergedFluids) { + Ingredient itemInput = blockInput(state); + if (itemInput != null) { + return matchesItem(itemInput, mergedInputs); + } + return matchesFluid(fluid(state), mergedFluids); + } + + /** + * The raw ore loot tables produce 3-4 units of the corresponding resource. The alloy furnace + * recipe is deterministic, so use the table's maximum output rather than exposing the raw ore + * block as an intermediate item. + */ + static ItemStack rawOreDrop(BlockState state) { + if (state == null) return ItemStack.EMPTY; + ResourceLocation blockId = BuiltInRegistries.BLOCK.getKey(state.getBlock()); + if (blockId == null || !"justdirethings".equals(blockId.getNamespace())) { + return ItemStack.EMPTY; + } + + String itemPath = switch (blockId.getPath()) { + case "raw_ferricore_ore" -> "raw_ferricore"; + case "raw_blazegold_ore" -> "raw_blazegold"; + case "raw_celestigem_ore" -> "celestigem"; + case "raw_eclipsealloy_ore" -> "raw_eclipsealloy"; + case "raw_coal_t1_ore" -> "coal_t1"; + case "raw_coal_t2_ore" -> "coal_t2"; + case "raw_coal_t3_ore" -> "coal_t3"; + case "raw_coal_t4_ore" -> "coal_t4"; + default -> null; + }; + if (itemPath == null) return ItemStack.EMPTY; + + Item item = BuiltInRegistries.ITEM.get( + new ResourceLocation("justdirethings", itemPath)); + return item == Items.AIR ? ItemStack.EMPTY : new ItemStack(item, RAW_ORE_DROP_COUNT); + } + + static boolean matchesItem(Ingredient required, @Nullable Map available) { + if (required == null || required.isEmpty() || available == null || available.isEmpty()) { + return false; + } + return AdapterUtils.matchesRequired(available, Map.of(required, 1L)); + } + +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/justdirethings/jdte/BioFactoryRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/justdirethings/jdte/BioFactoryRecipeAdapter.java new file mode 100644 index 00000000..e000a8fb --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/justdirethings/jdte/BioFactoryRecipeAdapter.java @@ -0,0 +1,296 @@ +package com.sorrowmist.useless.content.recipe.adapters.justdirethings.jdte; + +import com.jdte.common.recipes.BioFactoryInput; +import com.jdte.common.recipes.BioFactoryOutput; +import com.jdte.common.recipes.BioFactoryRecipe; +import com.jdte.setup.JDTEBlocks; +import com.jdte.setup.JDTEConfig; +import com.jdte.setup.JDTEFluids; +import com.jdte.setup.JDTERecipes; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.ExpectedOutputScaler; +import com.sorrowmist.useless.content.recipe.FluidIngredientAllocator; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; + +/** Converts JDTE Bio Factory recipes into alloy-furnace recipes. */ +public final class BioFactoryRecipeAdapter implements IRecipeAdapter { + private static final ResourceLocation TIME_FLUID_ID = + new ResourceLocation("justdirethings", "time_fluid_source"); + private static final int WATER_PER_CYCLE = 1000; + + @Override + public String sourceId() { + return RecipeSourceIds.JUSTDIRETHINGS; + } + + @Override + public Class getRecipeClass() { + return BioFactoryRecipe.class; + } + + /** The reusable specimen is the recipe-specific mold; no item is consumed for it. */ + @Override + @Nullable + public ItemStack getMoldItem() { + return null; + } + + @Override + @Nullable + public AdvancedAlloyFurnaceRecipe convert( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) return null; + + BioFactoryRecipe source = holder.value(); + Ingredient specimen = source.specimen(); + if (specimen == null || specimen.isEmpty()) return null; + + InputParts inputParts = splitInputs(source.inputs()); + if (inputParts == null) return null; + + Optional scaledOutputs = scaleOutputs(source.outputs()); + if (scaledOutputs.isEmpty()) return null; + + int operations = scaledOutputs.get().operations(); + OptionalInt energy = ExpectedOutputScaler.multiplyToInt( + Math.max(0, source.energy()), operations); + OptionalInt processTime = ExpectedOutputScaler.multiplyToInt( + Math.max(1, source.processTicks()), operations); + if (energy.isEmpty() || processTime.isEmpty()) return null; + + FluidStack processFluid = resolveFluid(source.processFluid(), source.processFluidAmount()); + if (source.processFluid() != null && source.processFluid().isPresent() + && source.processFluidAmount() > 0 && processFluid == null) { + return null; + } + FluidStack outputFluid = resolveFluid(source.outputFluid(), source.outputFluidAmount()); + if (source.outputFluid() != null && source.outputFluid().isPresent() + && source.outputFluidAmount() > 0 && outputFluid == null) { + return null; + } + if (scaledOutputs.get().outputs().isEmpty() && outputFluid == null) return null; + + int timeFluidAmount = Math.max(0, JDTEConfig.COMMON.bioFactoryTimeFluidPerCycle.get()); + int lifeFluidAmount = Math.max(0, JDTEConfig.COMMON.bioFactoryLifeFluidPerCycle.get()); + FluidStack timeFluid = resolveFluid(Optional.of(TIME_FLUID_ID), timeFluidAmount); + FluidStack lifeFluid = lifeFluidAmount <= 0 + ? null : new FluidStack(JDTEFluids.LIFE_FLUID_SOURCE.get(), lifeFluidAmount); + if (timeFluidAmount > 0 && timeFluid == null) return null; + + List inputFluids = inputFluids( + processFluid, timeFluid, lifeFluid, + new FluidStack(Fluids.WATER, WATER_PER_CYCLE), operations); + if (inputFluids == null) return null; + + OptionalInt scaledOutputAmount = outputFluid == null + ? OptionalInt.empty() : scaleAmount(outputFluid.getAmount(), operations); + if (outputFluid != null && scaledOutputAmount.isEmpty()) return null; + List outputFluids = outputFluid == null ? List.of() : List.of( + new net.minecraftforge.fluids.FluidStack(outputFluid, scaledOutputAmount.getAsInt())); + + List scaledInputs = scaleInputs(inputParts.consumed(), operations); + if (scaledInputs == null) return null; + + List molds = new ArrayList<>(); + molds.add(AdapterUtils.toMoldIngredient(new ItemStack(JDTEBlocks.BIO_FACTORY.get()))); + molds.add(specimen); + molds.addAll(inputParts.reusable()); + + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + scaledInputs, + inputFluids, + List.of(), + scaledOutputs.get().outputs(), + outputFluids, + List.of(), + energy.getAsInt(), + processTime.getAsInt(), + Ingredient.EMPTY, + 0, + molds, + AlloyFurnaceMode.NORMAL); + } + + @Override + public List> findMatchingRecipes( + Level level, + Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || mold == null || mold.isEmpty()) return List.of(); + + Map availableInputs = mergedInputs == null ? Map.of() : mergedInputs; + Map availableFluids = mergedFluids == null ? Map.of() : mergedFluids; + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(level.getRecipeManager(), JDTERecipes.BIO_FACTORY_RECIPE_TYPE.get())) { + AdvancedAlloyFurnaceRecipe converted = convert(holder, level); + if (converted == null + // This lookup has one legacy mold argument. Use the dynamic specimen to + // select the source recipe; the complete machine + specimen requirement is + // enforced by the multiblock mold hub for the converted recipe. + || !AdapterUtils.matchesMold(holder.value().specimen(), mold) + || !AdapterUtils.matchesRequired(availableInputs, requiredInputs(converted)) + || !FluidIngredientAllocator.matchesLong( + converted.inputFluids(), availableFluids, 1L)) { + continue; + } + matches.add(holder); + } + return List.copyOf(matches); + } + + @Nullable + private static InputParts splitInputs(@Nullable List sourceInputs) { + if (sourceInputs == null || sourceInputs.isEmpty()) { + return new InputParts(List.of(), List.of()); + } + + List consumed = new ArrayList<>(); + List reusable = new ArrayList<>(); + for (BioFactoryInput input : sourceInputs) { + if (input == null) return null; + if (input.ingredient() == null || input.ingredient().isEmpty() || input.count() < 0) { + return null; + } + if (input.count() == 0) { + reusable.add(input.ingredient()); + } else { + consumed.add(new CountedIngredient(input.ingredient(), input.count())); + } + } + return new InputParts(List.copyOf(consumed), List.copyOf(reusable)); + } + + @Nullable + private static List inputFluids( + @Nullable FluidStack processFluid, @Nullable FluidStack timeFluid, + @Nullable FluidStack lifeFluid, FluidStack water, int operations) { + List requirements = new ArrayList<>(); + if (!addFluidRequirement(requirements, processFluid, operations) + || !addFluidRequirement(requirements, timeFluid, operations) + || !addFluidRequirement(requirements, lifeFluid, operations) + || !addFluidRequirement(requirements, water, operations)) { + return null; + } + + List result = new ArrayList<>(requirements.size()); + for (FluidRequirement requirement : requirements) { + result.add(new LongSizedFluidIngredient( + FluidIngredient.single(requirement.stack()), requirement.amount())); + } + return List.copyOf(result); + } + + private static boolean addFluidRequirement( + List requirements, @Nullable FluidStack fluid, int operations) { + if (fluid == null || fluid.isEmpty() || fluid.getAmount() <= 0) return true; + + long amount = (long) fluid.getAmount() * operations; + if (amount <= 0) return false; + for (int index = 0; index < requirements.size(); index++) { + FluidRequirement existing = requirements.get(index); + if (!com.sorrowmist.useless.compat.forge1201.fluid.FluidStacks.same(existing.stack(), fluid)) continue; + try { + requirements.set(index, new FluidRequirement( + existing.stack(), Math.addExact(existing.amount(), amount))); + } catch (ArithmeticException exception) { + return false; + } + return true; + } + + requirements.add(new FluidRequirement(new net.minecraftforge.fluids.FluidStack(fluid, 1), amount)); + return true; + } + + @Nullable + private static List scaleInputs( + List inputs, int operations) { + if (operations == 1 || inputs.isEmpty()) return inputs; + + List result = new ArrayList<>(inputs.size()); + for (CountedIngredient input : inputs) { + long count; + try { + count = Math.multiplyExact(input.count(), (long) operations); + } catch (ArithmeticException exception) { + return null; + } + result.add(new CountedIngredient(input.ingredient(), count)); + } + return List.copyOf(result); + } + + private static Map requiredInputs(AdvancedAlloyFurnaceRecipe recipe) { + java.util.LinkedHashMap required = new java.util.LinkedHashMap<>(); + for (CountedIngredient input : recipe.inputs()) { + if (input != null && input.ingredient() != null && input.count() > 0) { + AdapterUtils.mergeIngredient(required, input.ingredient(), input.count()); + } + } + return required; + } + + private static Optional scaleOutputs( + @Nullable List sourceOutputs) { + if (sourceOutputs == null || sourceOutputs.isEmpty()) { + return Optional.of(new ExpectedOutputScaler.ScaledOutputs(1, List.of())); + } + + List weighted = new ArrayList<>(); + for (BioFactoryOutput output : sourceOutputs) { + if (output == null || output.stack() == null || output.stack().isEmpty() + || output.stack().getCount() <= 0) { + continue; + } + weighted.add(new ExpectedOutputScaler.WeightedItemOutput( + output.stack().copy(), output.stack().getCount(), output.stack().getCount(), + output.chance())); + } + return ExpectedOutputScaler.scale(weighted); + } + + @Nullable + private static FluidStack resolveFluid( + @Nullable Optional id, int amount) { + if (id == null || id.isEmpty() || amount <= 0) return null; + Fluid fluid = BuiltInRegistries.FLUID.getOptional(id.get()).orElse(null); + if (fluid == null || fluid == Fluids.EMPTY) return null; + return new FluidStack(fluid, amount); + } + + private static OptionalInt scaleAmount(int amount, int operations) { + long scaled = (long) amount * operations; + return scaled <= 0 || scaled > Integer.MAX_VALUE + ? OptionalInt.empty() : OptionalInt.of((int) scaled); + } + + private record InputParts(List consumed, List reusable) { + } + + private record FluidRequirement(FluidStack stack, long amount) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/justdirethings/jdte/GreenhouseRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/justdirethings/jdte/GreenhouseRecipeAdapter.java new file mode 100644 index 00000000..b0301b56 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/justdirethings/jdte/GreenhouseRecipeAdapter.java @@ -0,0 +1,297 @@ +package com.sorrowmist.useless.content.recipe.adapters.justdirethings.jdte; + +import com.jdte.common.content.JDTEContentControl; +import com.jdte.common.integrations.MysticalAgricultureGreenhouseIntegration; +import com.jdte.common.recipes.GreenhouseCropDefinition; +import com.jdte.common.recipes.GreenhouseCropResolver; +import com.jdte.common.recipes.GreenhouseRecipe; +import com.jdte.setup.JDTEBlocks; +import com.jdte.setup.JDTEConfig; +import com.jdte.setup.JDTERecipes; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.FluidIngredientAllocator; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.WeakHashMap; + +/** Converts JDTE Greenhouse recipes into alloy-furnace recipes. */ +public final class GreenhouseRecipeAdapter implements IRecipeAdapter { + private static final Map GENERATED_RECIPES = + Collections.synchronizedMap(new WeakHashMap<>()); + + @Override + public String sourceId() { + return RecipeSourceIds.JUSTDIRETHINGS; + } + + @Override + public Class getRecipeClass() { + return GreenhouseRecipe.class; + } + + /** The reusable seed is the recipe-specific mold. */ + @Override + @Nullable + public ItemStack getMoldItem() { + return null; + } + + /** Mirrors JDTE's runtime crop discovery so every supported plant gets a recipe entry. */ + @Override + public List> getGeneratedRecipes(Level level) { + if (level == null) return List.of(); + + JDTEContentControl control = JDTEContentControl.current(); + if (!control.isDynamicRecipeGenerationEnabled( + JDTEContentControl.DynamicRecipeFamily.GREENHOUSE)) { + return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + long generation = GreenhouseCropResolver.cacheGeneration(); + GeneratedRecipes cached; + synchronized (GENERATED_RECIPES) { + cached = GENERATED_RECIPES.get(recipeManager); + if (cached == null || cached.generation() != generation) { + cached = new GeneratedRecipes(generation, buildGeneratedRecipes(level, control)); + GENERATED_RECIPES.put(recipeManager, cached); + } + } + return cached.recipes().stream() + .filter(holder -> control.isRecipeEnabled(holder.id())) + .toList(); + } + + @Override + @Nullable + public AdvancedAlloyFurnaceRecipe convert( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) return null; + + GreenhouseRecipe source = holder.value(); + Ingredient seed = source.seed(); + List outputs = copyOutputs(source.outputs()); + int fluidAmount = fluidCost(source.timeFluid()); + int processTime = processTime(source.growthWork()); + int energy = Math.max(0, JDTEConfig.COMMON.greenhouseEnergyPerHarvestV2.get()); + FluidStack fluid = resolveFluid(source.fluid(), fluidAmount); + if (seed == null || seed.isEmpty() || outputs.isEmpty() + || fluid == null || processTime <= 0) { + return null; + } + + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + List.of(), + List.of(LongSizedFluidIngredient.from(fluid)), + List.of(), + outputs, + List.of(), + List.of(), + energy, + processTime, + Ingredient.EMPTY, + 0, + List.of( + Ingredient.of( + JDTEBlocks.GREENHOUSE.get(), + JDTEBlocks.LARGE_GREENHOUSE.get()), + seed), + AlloyFurnaceMode.NORMAL); + } + + @Override + public List> findMatchingRecipes( + Level level, + Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || mold == null || mold.isEmpty()) return List.of(); + + Map availableFluids = mergedFluids == null ? Map.of() : mergedFluids; + List> matches = new ArrayList<>(); + for (RecipeHolder holder : allRecipes(level)) { + AdvancedAlloyFurnaceRecipe converted = convert(holder, level); + if (converted == null + // This lookup has one legacy mold argument. Use the dynamic seed to select + // the source recipe; the machine + seed pair is checked by the mold hub. + || !AdapterUtils.matchesMold(holder.value().seed(), mold) + || !FluidIngredientAllocator.matchesLong( + converted.inputFluids(), availableFluids, 1L)) { + continue; + } + matches.add(holder); + } + return List.copyOf(matches); + } + + private List> allRecipes(Level level) { + JDTEContentControl control = JDTEContentControl.current(); + Map> recipes = new LinkedHashMap<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(level.getRecipeManager(), JDTERecipes.GREENHOUSE_RECIPE_TYPE.get())) { + if (holder != null && holder.value() != null && control.isRecipeEnabled(holder.id())) { + recipes.putIfAbsent(holder.id(), holder); + } + } + for (RecipeHolder holder : getGeneratedRecipes(level)) { + if (holder != null && holder.value() != null) { + recipes.putIfAbsent(holder.id(), holder); + } + } + return List.copyOf(recipes.values()); + } + + private static List> buildGeneratedRecipes( + Level level, JDTEContentControl control) { + RecipeManager recipeManager = level.getRecipeManager(); + Set seen = new HashSet<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, JDTERecipes.GREENHOUSE_RECIPE_TYPE.get())) { + if (holder == null || holder.value() == null || !control.isRecipeEnabled(holder.id())) { + continue; + } + addStaticSeeds(seen, holder.value().seed()); + } + Map> generated = new LinkedHashMap<>(); + if (ModList.get().isLoaded("mysticalagriculture")) { + List> crops = new ArrayList<>( + MysticalAgricultureGreenhouseIntegration.getCrops().entrySet()); + crops.sort(Comparator.comparing(entry -> itemId(entry.getKey()).toString())); + for (Map.Entry entry : crops) { + Item item = entry.getKey(); + GreenhouseCropDefinition definition = entry.getValue(); + if (item == null || !seen.add(item) || definition == null + || definition.outputs().isEmpty()) { + continue; + } + addGeneratedRecipe(generated, dynamicRecipeId(item), + item.getDefaultInstance(), definition); + } + } + for (Item item : BuiltInRegistries.ITEM) { + if (item == null || seen.contains(item) || !(item instanceof BlockItem)) continue; + + ItemStack seed = item.getDefaultInstance(); + if (seed.isEmpty() || !control.isItemEnabled(seed)) continue; + GreenhouseCropDefinition definition = GreenhouseCropResolver.findGeneric(seed); + if (definition == null || definition.outputs().isEmpty()) continue; + + seen.add(item); + addGeneratedRecipe(generated, dynamicRecipeId(item), seed, definition); + } + + return generated.values().stream() + .sorted(Comparator.comparing(holder -> holder.id().toString())) + .toList(); + } + + private static void addStaticSeeds(Set seen, @Nullable Ingredient seed) { + if (seed == null || seed.isEmpty()) return; + try { + for (ItemStack stack : seed.getItems()) { + if (stack != null && !stack.isEmpty()) seen.add(stack.getItem()); + } + } catch (RuntimeException ignored) { + // Opaque custom ingredients cannot expose concrete seeds for de-duplication. + } + } + + private static void addGeneratedRecipe( + Map> recipes, + ResourceLocation id, ItemStack seed, GreenhouseCropDefinition definition) { + if (id == null || seed == null || seed.isEmpty() || definition == null + || definition.outputs().isEmpty() || definition.displayBlock() == null + || definition.fluid() == null) { + return; + } + + GreenhouseRecipe recipe = new GreenhouseRecipe( + Ingredient.of(seed.copyWithCount(1)), + copyOutputs(definition.outputs()), + definition.displayBlock(), + Optional.ofNullable(definition.harvestBlock()), + definition.useLootTable(), + definition.growthWork(), + definition.fluid(), + definition.timeFluid()); + recipes.putIfAbsent(id, new RecipeHolder<>(id, recipe)); + } + + private static ResourceLocation dynamicRecipeId(Item item) { + ResourceLocation itemId = itemId(item); + return new ResourceLocation("jdte", + "jei/greenhouse/" + itemId.getNamespace() + "/" + itemId.getPath()); + } + + private static ResourceLocation itemId(Item item) { + ResourceLocation id = item == null ? null : BuiltInRegistries.ITEM.getKey(item); + return id == null ? new ResourceLocation("minecraft", "air") : id; + } + + private static List copyOutputs(@Nullable List sourceOutputs) { + if (sourceOutputs == null || sourceOutputs.isEmpty()) return List.of(); + + List outputs = new ArrayList<>(sourceOutputs.size()); + for (ItemStack output : sourceOutputs) { + if (output != null && !output.isEmpty()) outputs.add(output.copy()); + } + return List.copyOf(outputs); + } + + private static int fluidCost(int rawAmount) { + int divisor = JDTEConfig.COMMON.greenhouseFluidCostDivisor.get(); + if (rawAmount <= 0 || divisor <= 0) return 0; + + long reduced = ((long) rawAmount + divisor - 1L) / divisor; + return reduced > Integer.MAX_VALUE ? 0 : Math.max(1, (int) reduced); + } + + private static int processTime(int growthWork) { + int baseMultiplier = JDTEConfig.COMMON.greenhouseBaseMultiplier.get(); + if (growthWork <= 0 || baseMultiplier <= 0) return 0; + + long ticks = ((long) growthWork + baseMultiplier - 1L) / baseMultiplier; + return ticks > Integer.MAX_VALUE ? 0 : Math.max(1, (int) ticks); + } + + @Nullable + private static FluidStack resolveFluid( + @Nullable ResourceLocation id, int amount) { + if (id == null || amount <= 0) return null; + Fluid fluid = BuiltInRegistries.FLUID.getOptional(id).orElse(null); + if (fluid == null || fluid == Fluids.EMPTY || !fluid.defaultFluidState().isSource()) { + return null; + } + return new FluidStack(fluid, amount); + } + + private record GeneratedRecipes(long generation, + List> recipes) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/justdirethings/jdte/InfusionRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/justdirethings/jdte/InfusionRecipeAdapter.java new file mode 100644 index 00000000..1a5ba519 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/justdirethings/jdte/InfusionRecipeAdapter.java @@ -0,0 +1,437 @@ +package com.sorrowmist.useless.content.recipe.adapters.justdirethings.jdte; + +import appeng.api.stacks.AEKey; +import com.jdte.common.blockentities.AdvancedInfusionMachineBE; +import com.jdte.common.blockentities.InfusionMachineBE; +import com.jdte.common.recipes.InfusionRecipe; +import com.jdte.common.utils.InfusionFluidHelper; +import com.jdte.common.utils.MobLootSpawnEggHelper; +import com.jdte.setup.JDTEBlocks; +import com.jdte.setup.JDTEFluids; +import com.jdte.setup.JDTERecipes; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeFingerprint; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.FluidIngredientAllocator; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.alchemy.PotionContents; +import net.minecraft.world.item.alchemy.Potions; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.Fluids; +import com.sorrowmist.useless.compat.forge1201.recipe.DataComponentIngredient; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.capability.IFluidHandler; +import net.minecraftforge.fluids.capability.IFluidHandlerItem; +import net.minecraftforge.common.capabilities.ForgeCapabilities; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; + +/** Converts JDTE Advanced Infusion Machine recipes to alloy-furnace recipes. */ +public final class InfusionRecipeAdapter implements IRecipeAdapter { + private static final Map>> GENERATED_BASE_RECIPES = + Collections.synchronizedMap(new WeakHashMap<>()); + + @Override + public String sourceId() { + return RecipeSourceIds.JUSTDIRETHINGS; + } + + @Override + public Class getRecipeClass() { + return InfusionRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return new ItemStack(JDTEBlocks.ADVANCED_INFUSION_MACHINE.get()); + } + + /** + * JDTE also creates these operations directly in the machine block entity instead of placing + * them in RecipeManager: fluid-container filling, vanilla bottle filling, and loot-derived + * spawn-egg conversion. Keep those operations in the shared recipe directory as well. + */ + @Override + public List> getGeneratedRecipes(Level level) { + if (level == null) return List.of(); + + List> result = new ArrayList<>(baseGeneratedRecipes(level)); + addSpawnEggRecipes(level, result); + return List.copyOf(result); + } + + @Override + @Nullable + public AdvancedAlloyFurnaceRecipe convert(RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) return null; + + InfusionRecipe source = holder.value(); + ItemStack input = source.getInput(); + FluidStack fluid = source.getFluidInput(); + ItemStack output = source.getOutput(); + if (!isValid(input, fluid, output)) return null; + + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + List.of(new CountedIngredient(itemIngredient(input), input.getCount())), + List.of(LongSizedFluidIngredient.from(fluid)), + List.of(), + List.of(output), + List.of(), + List.of(), + Math.max(1, source.getEnergyCost()), + AdapterUtils.DEFAULT_PROCESS_TIME, + Ingredient.EMPTY, + 0, + List.of(AdapterUtils.toMoldIngredient(getMoldItem())), + AlloyFurnaceMode.NORMAL); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || mold == null || mold.isEmpty()) return List.of(); + + Map availableInputs = mergedInputs == null ? Map.of() : mergedInputs; + Map availableFluids = mergedFluids == null ? Map.of() : mergedFluids; + Map> matches = new LinkedHashMap<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(level.getRecipeManager(), JDTERecipes.INFUSION_RECIPE_TYPE.get())) { + addMatchingHolder(matches, holder, availableInputs, availableFluids); + } + for (RecipeHolder holder : getGeneratedRecipes(level)) { + addMatchingHolder(matches, holder, availableInputs, availableFluids); + } + return List.copyOf(matches.values()); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, Map mergedKeys, + @Nullable ItemStack mold, List actualInputs) { + if (level == null || mold == null || mold.isEmpty()) return List.of(); + + Map> matches = new LinkedHashMap<>(); + for (RecipeHolder holder : findMatchingRecipes( + level, mergedInputs, mergedFluids, mold)) { + matches.put(holder.id(), holder); + } + + if (actualInputs == null || actualInputs.isEmpty()) { + return List.copyOf(matches.values()); + } + + Map availableFluids = mergedFluids == null ? Map.of() : mergedFluids; + for (ItemStack input : actualInputs) { + if (input == null || input.isEmpty()) continue; + addRuntimeContainerMatches(level, matches, input, availableFluids); + addRuntimeBottleMatches(level, matches, input, availableFluids); + addRuntimeSpawnEggMatch(matches, level, input, availableFluids); + } + return List.copyOf(matches.values()); + } + + private static List> baseGeneratedRecipes(Level level) { + RecipeManager recipeManager = level.getRecipeManager(); + HolderLookup.Provider registries = level.registryAccess(); + synchronized (GENERATED_BASE_RECIPES) { + return GENERATED_BASE_RECIPES.computeIfAbsent( + recipeManager, ignored -> buildBaseGeneratedRecipes(registries)); + } + } + + private static List> buildBaseGeneratedRecipes( + HolderLookup.Provider registries) { + Map> recipes = new LinkedHashMap<>(); + + for (Item item : BuiltInRegistries.ITEM) { + ItemStack input = item.getDefaultInstance(); + if (input.isEmpty()) continue; + + for (Fluid fluid : BuiltInRegistries.FLUID) { + if (!InfusionFluidHelper.isFillableSourceFluid(fluid)) continue; + try { + RecipeHolder holder = createFluidContainerRecipe( + registries, input, new FluidStack(fluid, InfusionMachineBE.BASE_FLUID_CAPACITY)); + if (holder != null) recipes.putIfAbsent(holder.id(), holder); + } catch (RuntimeException ignored) { + // A third-party fluid container may reject capability probing. + } + } + } + + addBottleRecipe(registries, recipes, new FluidStack(Fluids.WATER, + InfusionFluidHelper.BOTTLE_FLUID_AMOUNT)); + for (Fluid fluid : BuiltInRegistries.FLUID) { + if (fluid == Fluids.EMPTY || !InfusionFluidHelper.isFillableSourceFluid(fluid) + || !InfusionFluidHelper.isHoneyFluid(fluid)) { + continue; + } + addBottleRecipe(registries, recipes, new FluidStack(fluid, + InfusionFluidHelper.BOTTLE_FLUID_AMOUNT)); + } + return List.copyOf(recipes.values()); + } + + private static void addBottleRecipe( + HolderLookup.Provider registries, + Map> recipes, FluidStack fluid) { + RecipeHolder holder = createBottleRecipe(registries, fluid); + if (holder != null) recipes.putIfAbsent(holder.id(), holder); + } + + @Nullable + private static RecipeHolder createFluidContainerRecipe( + HolderLookup.Provider registries, ItemStack input, FluidStack available) { + if (input == null || input.isEmpty() || available == null || available.isEmpty()) return null; + + ItemStack container = input.copyWithCount(1); + ItemStack originalContainer = container.copy(); + IFluidHandlerItem handler = com.sorrowmist.useless.compat.forge1201.capability.CapabilityAccess.get(container, ForgeCapabilities.FLUID_HANDLER_ITEM); + if (handler == null || handler.getTanks() <= 0) return null; + + int fillAmount = handler.fill(available.copy(), IFluidHandler.FluidAction.SIMULATE); + if (fillAmount <= 0 || fillAmount > available.getAmount()) return null; + + FluidStack toFill = new net.minecraftforge.fluids.FluidStack(available, fillAmount); + int filled = handler.fill(toFill, IFluidHandler.FluidAction.EXECUTE); + if (filled <= 0) return null; + + ItemStack result = handler.getContainer(); + if (result.isEmpty() || ItemStack.isSameItemSameTags(originalContainer, result)) { + return null; + } + result.setCount(1); + return holder(registries, "container", originalContainer, + new net.minecraftforge.fluids.FluidStack(available, filled), result, + AdvancedInfusionMachineBE.BASE_ENERGY_COST); + } + + @Nullable + private static RecipeHolder createBottleRecipe( + HolderLookup.Provider registries, FluidStack fluid) { + if (fluid == null || fluid.isEmpty() + || fluid.getAmount() < InfusionFluidHelper.BOTTLE_FLUID_AMOUNT) return null; + + ItemStack output; + if (fluid.is(Fluids.WATER)) { + output = PotionContents.createItemStack(Items.POTION, Potions.WATER); + } else if (InfusionFluidHelper.isHoneyFluid(fluid)) { + output = new ItemStack(Items.HONEY_BOTTLE); + } else { + return null; + } + output.setCount(1); + return holder(registries, "bottle", Items.GLASS_BOTTLE.getDefaultInstance(), + new net.minecraftforge.fluids.FluidStack(fluid, InfusionFluidHelper.BOTTLE_FLUID_AMOUNT), output, + AdvancedInfusionMachineBE.BASE_ENERGY_COST); + } + + private static void addSpawnEggRecipes( + Level level, List> result) { + HolderLookup.Provider registries = level.registryAccess(); + for (Map.Entry entry : spawnEggRecipes(level).entrySet()) { + Item inputItem = entry.getKey(); + ItemStack output = entry.getValue(); + if (inputItem == null || output == null || output.isEmpty()) continue; + + ItemStack input = inputItem.getDefaultInstance(); + if (input.isEmpty() || input.getMaxStackSize() <= 1) continue; + input.setCount(input.getMaxStackSize()); + RecipeHolder holder = holder( + registries, "spawn_egg", input, + new FluidStack(JDTEFluids.LIFE_FLUID_SOURCE.get(), + MobLootSpawnEggHelper.LIFE_FLUID_COST), + output.copyWithCount(1), MobLootSpawnEggHelper.ENERGY_COST); + result.add(holder); + } + } + + private static void addRuntimeContainerMatches( + Level level, Map> matches, + ItemStack input, Map availableFluids) { + HolderLookup.Provider registries = level.registryAccess(); + for (FluidStack available : simulationFluids(availableFluids)) { + RecipeHolder holder; + try { + holder = createFluidContainerRecipe(registries, input, available); + } catch (RuntimeException ignored) { + continue; + } + addIfFluidIsAvailable(matches, holder, availableFluids); + } + } + + private static void addRuntimeBottleMatches( + Level level, Map> matches, + ItemStack input, Map availableFluids) { + if (!input.is(Items.GLASS_BOTTLE)) return; + HolderLookup.Provider registries = level.registryAccess(); + for (FluidStack available : simulationFluids(availableFluids)) { + RecipeHolder holder = createBottleRecipe(registries, available); + addIfFluidIsAvailable(matches, holder, availableFluids); + } + } + + private static void addRuntimeSpawnEggMatch( + Map> matches, + Level level, ItemStack input, Map availableFluids) { + ItemStack output = spawnEggRecipes(level).get(input.getItem()); + if (output == null || output.isEmpty() + || input.getMaxStackSize() <= 1 + || input.getCount() < input.getMaxStackSize()) { + return; + } + + RecipeHolder holder = holder( + level.registryAccess(), "spawn_egg", input.copyWithCount(input.getMaxStackSize()), + new FluidStack(JDTEFluids.LIFE_FLUID_SOURCE.get(), + MobLootSpawnEggHelper.LIFE_FLUID_COST), + output.copyWithCount(1), MobLootSpawnEggHelper.ENERGY_COST); + addIfFluidIsAvailable(matches, holder, availableFluids); + } + + private static List simulationFluids(Map availableFluids) { + if (availableFluids == null || availableFluids.isEmpty()) return List.of(); + List result = new ArrayList<>(); + for (Map.Entry entry : availableFluids.entrySet()) { + FluidStack stack = entry.getKey(); + long amount = entry.getValue() == null ? 0L : entry.getValue(); + if (stack == null || stack.isEmpty() || amount <= 0L) continue; + long simulationAmount = Math.min( + Math.min(amount, InfusionMachineBE.BASE_FLUID_CAPACITY), Integer.MAX_VALUE); + if (simulationAmount > 0L) { + result.add(new net.minecraftforge.fluids.FluidStack(stack, (int) simulationAmount)); + } + } + return List.copyOf(result); + } + + private static void addIfFluidIsAvailable( + Map> matches, + @Nullable RecipeHolder holder, + Map availableFluids) { + if (holder == null || !hasFluid(availableFluids, holder.value().getFluidInput())) return; + matches.putIfAbsent(holder.id(), holder); + } + + private static boolean hasFluid(Map availableFluids, FluidStack required) { + if (required == null || required.isEmpty() || availableFluids == null) return false; + for (Map.Entry entry : availableFluids.entrySet()) { + FluidStack available = entry.getKey(); + long amount = entry.getValue() == null ? 0L : entry.getValue(); + if (available != null && com.sorrowmist.useless.compat.forge1201.fluid.FluidStacks.same(available, required) + && amount >= required.getAmount()) { + return true; + } + } + return false; + } + + private static void addMatchingHolder( + Map> matches, + RecipeHolder holder, + Map availableInputs, + Map availableFluids) { + if (holder == null || holder.value() == null) return; + InfusionRecipe source = holder.value(); + ItemStack input = source.getInput(); + FluidStack fluid = source.getFluidInput(); + ItemStack output = source.getOutput(); + if (!isValid(input, fluid, output) + || !AdapterUtils.matchesRequired(availableInputs, + Map.of(itemIngredient(input), (long) input.getCount())) + || !FluidIngredientAllocator.matchesLong( + List.of(LongSizedFluidIngredient.from(fluid)), availableFluids, 1L)) { + return; + } + matches.putIfAbsent(holder.id(), holder); + } + + private static RecipeHolder holder( + HolderLookup.Provider registries, + String kind, ItemStack input, FluidStack fluid, ItemStack output, int energyCost) { + ResourceLocation id = dynamicId(registries, kind, input, fluid, output); + return new RecipeHolder<>(id, new InfusionRecipe( + id, input.copy(), fluid.copy(), output.copy(), Math.max(1, energyCost))); + } + + /** + * 由参与方数据派生的配方 id。 + * + *

物品栈与流体栈的组件映射(含组件值内部嵌套的映射)迭代顺序取决于引用哈希,跨 JVM 重启 + * 并不保证一致。若直接拼接组件映射的字符串表示,这些栈会在每次启动时得到不同签名,进而生成 + * 不同 id;已按旧 id 编码的万象样板会因失配被判为缺失并丢弃,表现为重启后样板随机失效。 + * 这里复用指纹模块的编解码路径(组件补丁 codec + JSON 递归排序),使签名只由数据内容决定。 + */ + private static ResourceLocation dynamicId( + HolderLookup.Provider registries, + String kind, ItemStack input, FluidStack fluid, ItemStack output) { + String signature = kind + + "|" + AlloyFurnaceRecipeFingerprint.safeItemStack(input, registries) + + "|" + AlloyFurnaceRecipeFingerprint.safeFluidStack(fluid, registries) + + "|" + AlloyFurnaceRecipeFingerprint.safeItemStack(output, registries); + return new ResourceLocation( + "jdte", "infusion/" + kind + "/" + + Integer.toUnsignedString(signature.hashCode(), 16)); + } + + private static Map spawnEggRecipes(Level level) { + if (level instanceof ServerLevel serverLevel && serverLevel.getServer() != null) { + return MobLootSpawnEggHelper.getRecipes(serverLevel.getServer().getResourceManager()); + } + + // JDTE synchronizes the server's loot-derived map to remote clients. Use reflection here so + // this common-side adapter never links a client-only JDTE class on a dedicated server. + try { + Class cache = Class.forName("com.jdte.client.SpawnEggRecipeClientCache"); + Object value = cache.getMethod("get").invoke(null); + if (!(value instanceof Map synced)) return Map.of(); + + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : synced.entrySet()) { + if (!(entry.getKey() instanceof ResourceLocation inputId) + || !(entry.getValue() instanceof ResourceLocation outputId)) continue; + Item input = BuiltInRegistries.ITEM.getOptional(inputId).orElse(null); + Item output = BuiltInRegistries.ITEM.getOptional(outputId).orElse(null); + if (input != null && output != null) result.put(input, output.getDefaultInstance()); + } + return Map.copyOf(result); + } catch (ReflectiveOperationException | LinkageError | RuntimeException ignored) { + return Map.of(); + } + } + + private static Ingredient itemIngredient(ItemStack stack) { + return DataComponentIngredient.of(true, stack.copyWithCount(1)); + } + + private static boolean isValid(ItemStack input, FluidStack fluid, ItemStack output) { + return input != null && !input.isEmpty() && input.getCount() > 0 + && fluid != null && !fluid.isEmpty() && fluid.getAmount() > 0 + && output != null && !output.isEmpty(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/justdirethings/jdte/JDTERecipeCompatLoader.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/justdirethings/jdte/JDTERecipeCompatLoader.java new file mode 100644 index 00000000..f8d4e649 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/justdirethings/jdte/JDTERecipeCompatLoader.java @@ -0,0 +1,16 @@ +package com.sorrowmist.useless.content.recipe.adapters.justdirethings.jdte; + +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; + +/** Optional JDTE entrypoint, loaded only when JDTE is present with Just Dire Things. */ +public final class JDTERecipeCompatLoader { + private JDTERecipeCompatLoader() { + } + + public static void register() { + AlloyFurnaceRecipeManager.getInstance().registerAdapter(new InfusionRecipeAdapter()); + AlloyFurnaceRecipeManager.getInstance().registerAdapter(new LifeSynthesisRecipeAdapter()); + AlloyFurnaceRecipeManager.getInstance().registerAdapter(new BioFactoryRecipeAdapter()); + AlloyFurnaceRecipeManager.getInstance().registerAdapter(new GreenhouseRecipeAdapter()); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/justdirethings/jdte/LifeSynthesisRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/justdirethings/jdte/LifeSynthesisRecipeAdapter.java new file mode 100644 index 00000000..51671703 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/justdirethings/jdte/LifeSynthesisRecipeAdapter.java @@ -0,0 +1,134 @@ +package com.sorrowmist.useless.content.recipe.adapters.justdirethings.jdte; + +import com.jdte.common.recipes.LifeSynthesisRecipe; +import com.jdte.setup.JDTEBlocks; +import com.jdte.setup.JDTERecipes; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.FluidIngredientAllocator; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Converts JDTE Life Synthesis Vat recipes into alloy-furnace recipes. */ +public final class LifeSynthesisRecipeAdapter implements IRecipeAdapter { + @Override + public String sourceId() { + return RecipeSourceIds.JUSTDIRETHINGS; + } + + @Override + public Class getRecipeClass() { + return LifeSynthesisRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(JDTEBlocks.LIFE_SYNTHESIS_VAT.get()); + } + + @Override + @Nullable + public AdvancedAlloyFurnaceRecipe convert( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) return null; + + LifeSynthesisRecipe source = holder.value(); + List inputs = inputs(source); + FluidStack nutrient = source.nutrient(); + FluidStack output = source.output(); + if (nutrient == null || output == null || output.isEmpty() + || output.getAmount() <= 0 || source.processTicks() <= 0 || source.energy() <= 0) { + return null; + } + + List inputFluids = nutrient.isEmpty() + ? List.of() : List.of(LongSizedFluidIngredient.from(nutrient)); + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + inputs, + inputFluids, + List.of(), + List.of(), + List.of(output.copy()), + List.of(), + source.energy(), + source.processTicks(), + Ingredient.EMPTY, + 0, + List.of(AdapterUtils.toMoldIngredient(getMoldItem())), + AlloyFurnaceMode.NORMAL); + } + + @Override + public List> findMatchingRecipes( + Level level, + Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) return List.of(); + + Map availableInputs = mergedInputs == null ? Map.of() : mergedInputs; + Map availableFluids = mergedFluids == null ? Map.of() : mergedFluids; + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(level.getRecipeManager(), JDTERecipes.LIFE_SYNTHESIS_RECIPE_TYPE.get())) { + LifeSynthesisRecipe source = holder.value(); + if (convert(holder, level) == null || !matchesInputs(source, availableInputs) + || !matchesNutrient(source, availableFluids)) { + continue; + } + matches.add(holder); + } + return List.copyOf(matches); + } + + private static List inputs(LifeSynthesisRecipe source) { + if (source.inputs() == null || source.inputs().isEmpty()) return List.of(); + + List result = new ArrayList<>(); + for (LifeSynthesisRecipe.InputSlot slot : source.inputs()) { + if (slot == null || slot.ingredient() == null || slot.ingredient().isEmpty() + || slot.count() <= 0) { + continue; + } + result.add(new CountedIngredient(slot.ingredient(), slot.count())); + } + return List.copyOf(result); + } + + private static boolean matchesInputs( + LifeSynthesisRecipe source, Map availableInputs) { + Map required = new LinkedHashMap<>(); + if (source.inputs() != null) { + for (LifeSynthesisRecipe.InputSlot slot : source.inputs()) { + if (slot == null || slot.ingredient() == null || slot.ingredient().isEmpty() + || slot.count() <= 0) { + continue; + } + AdapterUtils.mergeIngredient(required, slot.ingredient(), slot.count()); + } + } + return AdapterUtils.matchesRequired(availableInputs, required); + } + + private static boolean matchesNutrient( + LifeSynthesisRecipe source, Map availableFluids) { + FluidStack nutrient = source.nutrient(); + if (nutrient == null || nutrient.isEmpty()) return true; + return FluidIngredientAllocator.matchesLong( + List.of(LongSizedFluidIngredient.from(nutrient)), availableFluids, 1L); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/cookery/AbstractCookeryRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/cookery/AbstractCookeryRecipeAdapter.java new file mode 100644 index 00000000..491566b7 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/cookery/AbstractCookeryRecipeAdapter.java @@ -0,0 +1,242 @@ +package com.sorrowmist.useless.content.recipe.adapters.kaleidoscope.cookery; + +import appeng.api.stacks.AEKey; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightRecipeAdapterUtils; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.Recipe; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +abstract class AbstractCookeryRecipeAdapter> implements IRecipeAdapter { + private static final int BASE_PROCESS_TIME = 200; + + protected abstract ResourceLocation moldId(); + + protected abstract AdvancedAlloyFurnaceRecipe convertSource( + ResourceLocation recipeId, T source, Level level); + + @Override + public final String sourceId() { + return RecipeSourceIds.KALEIDOSCOPE_COOKERY; + } + + @Override + public ItemStack getMoldItem() { + Item item = BuiltInRegistries.ITEM.getOptional(moldId()).orElse(null); + return item == null ? ItemStack.EMPTY : item.getDefaultInstance(); + } + + @Override + public final List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + AdvancedAlloyFurnaceRecipe converted = convertSource(holder.id(), holder.value(), level); + return converted == null ? List.of() : List.of(converted); + } + + @Override + public final List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + return findMatchingRecipes(level, mergedInputs, mergedFluids, Map.of(), mold, List.of()); + } + + @Override + public final List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, Map mergedKeys, + @Nullable ItemStack mold, List actualInputs) { + if (level == null || !matchesMold(mold) + || (mergedKeys != null && !mergedKeys.isEmpty())) { + return List.of(); + } + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : DelightRecipeAdapterUtils.allOf( + level.getRecipeManager(), getRecipeClass())) { + AdvancedAlloyFurnaceRecipe converted = convertSource( + holder.id(), holder.value(), level); + if (converted != null + && DelightRecipeAdapterUtils.matchesItems( + converted.inputs(), mergedInputs, actualInputs) + && DelightRecipeAdapterUtils.matchesFluids( + converted.inputFluids(), mergedFluids)) { + matches.add(holder); + } + } + return List.copyOf(matches); + } + + protected List moldIngredients() { + Ingredient mold = AdapterUtils.toMoldIngredient(getMoldItem()); + return mold.isEmpty() ? List.of() : List.of(mold); + } + + protected AdvancedAlloyFurnaceRecipe createItemRecipe( + ResourceLocation recipeId, List ingredients, Ingredient carrier, + ItemStack output, int processTime) { + if (output == null || output.isEmpty() || output.getCount() <= 0) { + return null; + } + + List inputs = countedInputs( + ingredients, carrier, output.getCount()); + if (inputs.isEmpty()) { + return null; + } + + int time = Math.max(1, processTime); + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(recipeId), + inputs, + List.of(), + List.of(), + List.of(output.copy()), + List.of(), + List.of(), + energyFor(time), + time, + Ingredient.EMPTY, + 0, + moldIngredients(), + AlloyFurnaceMode.NORMAL + ); + } + + protected AdvancedAlloyFurnaceRecipe createItemRecipe( + ResourceLocation recipeId, List ingredients, ItemStack output, + int processTime) { + return createItemRecipe(recipeId, ingredients, Ingredient.EMPTY, output, processTime); + } + + protected AdvancedAlloyFurnaceRecipe createMultiOutputItemRecipe( + ResourceLocation recipeId, List ingredients, + List outputs, int processTime) { + if (outputs == null || outputs.isEmpty()) { + return null; + } + + List normalizedOutputs = outputs.stream() + .filter(stack -> stack != null && !stack.isEmpty() && stack.getCount() > 0) + .map(ItemStack::copy) + .toList(); + if (normalizedOutputs.isEmpty()) { + return null; + } + + List inputs = countedInputs(ingredients, Ingredient.EMPTY, 0); + if (inputs.isEmpty()) { + return null; + } + + int time = Math.max(1, processTime); + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(recipeId), + inputs, + List.of(), + List.of(), + normalizedOutputs, + List.of(), + List.of(), + energyFor(time), + time, + Ingredient.EMPTY, + 0, + moldIngredients(), + AlloyFurnaceMode.NORMAL + ); + } + + protected AdvancedAlloyFurnaceRecipe createStockpotRecipe( + ResourceLocation recipeId, List ingredients, ResourceLocation soupBase, + ItemStack output, int processTime, Ingredient carrier) { + if (output == null || output.isEmpty() || output.getCount() <= 0 + || soupBase == null) { + return null; + } + + List itemIngredients = new ArrayList<>(); + if (ingredients != null) { + itemIngredients.addAll(ingredients); + } + + List inputFluids = new ArrayList<>(); + var fluid = BuiltInRegistries.FLUID.getOptional(soupBase).orElse(null); + if (fluid != null && fluid != net.minecraft.world.level.material.Fluids.EMPTY) { + inputFluids.add(new LongSizedFluidIngredient( + com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient.single( + new FluidStack(fluid, 1_000)), + 1_000)); + } else { + Item item = BuiltInRegistries.ITEM.getOptional(soupBase).orElse(null); + if (item != null) { + itemIngredients.add(Ingredient.of(item)); + } + } + + List inputs = countedInputs( + itemIngredients, carrier, output.getCount()); + if (inputs.isEmpty() && inputFluids.isEmpty()) { + return null; + } + + int time = Math.max(1, processTime); + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(recipeId), + inputs, + inputFluids, + List.of(), + List.of(output.copy()), + List.of(), + List.of(), + energyFor(time), + time, + Ingredient.EMPTY, + 0, + moldIngredients(), + AlloyFurnaceMode.NORMAL + ); + } + + protected static List countedInputs( + List ingredients, Ingredient carrier, long carrierCount) { + Map counts = new LinkedHashMap<>(); + if (ingredients != null) { + for (Ingredient ingredient : ingredients) { + if (!AdapterUtils.isIngredientEmpty(ingredient)) { + AdapterUtils.mergeIngredient(counts, ingredient, 1L); + } + } + } + if (!AdapterUtils.isIngredientEmpty(carrier) && carrierCount > 0) { + AdapterUtils.mergeIngredient(counts, carrier, carrierCount); + } + return counts.entrySet().stream() + .map(entry -> new CountedIngredient(entry.getKey(), entry.getValue())) + .toList(); + } + + protected static long energyFor(int processTime) { + return Math.max(1L, (long) processTime * AdapterUtils.DEFAULT_ENERGY / BASE_PROCESS_TIME); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/cookery/AbstractStockpotRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/cookery/AbstractStockpotRecipeAdapter.java new file mode 100644 index 00000000..1c2dc6bf --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/cookery/AbstractStockpotRecipeAdapter.java @@ -0,0 +1,42 @@ +package com.sorrowmist.useless.content.recipe.adapters.kaleidoscope.cookery; + +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.Recipe; + +import java.util.ArrayList; +import java.util.List; + +abstract class AbstractStockpotRecipeAdapter> + extends AbstractCookeryRecipeAdapter { + private static final ResourceLocation STOCKPOT_ID = + new ResourceLocation("kaleidoscope_cookery", "stockpot"); + private static final ResourceLocation LID_ID = + new ResourceLocation("kaleidoscope_cookery", "stockpot_lid"); + + @Override + protected final ResourceLocation moldId() { + return STOCKPOT_ID; + } + + @Override + protected final List moldIngredients() { + List molds = new ArrayList<>(super.moldIngredients()); + Item lid = BuiltInRegistries.ITEM.getOptional(LID_ID).orElse(null); + if (lid != null) { + molds.add(Ingredient.of(lid)); + } + return List.copyOf(molds); + } + + protected final AdvancedAlloyFurnaceRecipe createStockpotRecipe( + ResourceLocation recipeId, List ingredients, ResourceLocation soupBase, + ItemStack output, int processTime, Ingredient carrier) { + return super.createStockpotRecipe( + recipeId, ingredients, soupBase, output, processTime, carrier); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/cookery/ChoppingBoardRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/cookery/ChoppingBoardRecipeAdapter.java new file mode 100644 index 00000000..e61f3ab8 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/cookery/ChoppingBoardRecipeAdapter.java @@ -0,0 +1,31 @@ +package com.sorrowmist.useless.content.recipe.adapters.kaleidoscope.cookery; + +import com.github.ysbbbbbb.kaleidoscopecookery.crafting.recipe.ChoppingBoardRecipe; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.Level; + +import java.util.List; + +public final class ChoppingBoardRecipeAdapter + extends AbstractCookeryRecipeAdapter { + private static final ResourceLocation MOLD_ID = + new ResourceLocation("kaleidoscope_cookery", "chopping_board"); + + @Override + public Class getRecipeClass() { + return ChoppingBoardRecipe.class; + } + + @Override + protected ResourceLocation moldId() { + return MOLD_ID; + } + + @Override + protected AdvancedAlloyFurnaceRecipe convertSource( + ResourceLocation recipeId, ChoppingBoardRecipe source, Level level) { + return createItemRecipe(recipeId, List.of(source.getIngredient()), + source.getResult(), source.getCutCount()); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/cookery/FlexPotRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/cookery/FlexPotRecipeAdapter.java new file mode 100644 index 00000000..5bb49ec3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/cookery/FlexPotRecipeAdapter.java @@ -0,0 +1,28 @@ +package com.sorrowmist.useless.content.recipe.adapters.kaleidoscope.cookery; + +import com.github.ysbbbbbb.kaleidoscopecookery.crafting.recipe.FlexPotRecipe; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.Level; + +public final class FlexPotRecipeAdapter extends AbstractCookeryRecipeAdapter { + private static final ResourceLocation MOLD_ID = + new ResourceLocation("kaleidoscope_cookery", "pot"); + + @Override + public Class getRecipeClass() { + return FlexPotRecipe.class; + } + + @Override + protected ResourceLocation moldId() { + return MOLD_ID; + } + + @Override + protected AdvancedAlloyFurnaceRecipe convertSource( + ResourceLocation recipeId, FlexPotRecipe source, Level level) { + return createItemRecipe(recipeId, source.ingredients(), source.carrier(), + source.result(), source.time()); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/cookery/FlexStockpotRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/cookery/FlexStockpotRecipeAdapter.java new file mode 100644 index 00000000..7c9eaa91 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/cookery/FlexStockpotRecipeAdapter.java @@ -0,0 +1,20 @@ +package com.sorrowmist.useless.content.recipe.adapters.kaleidoscope.cookery; + +import com.github.ysbbbbbb.kaleidoscopecookery.crafting.recipe.FlexStockpotRecipe; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.Level; + +public final class FlexStockpotRecipeAdapter extends AbstractStockpotRecipeAdapter { + @Override + public Class getRecipeClass() { + return FlexStockpotRecipe.class; + } + + @Override + protected AdvancedAlloyFurnaceRecipe convertSource( + ResourceLocation recipeId, FlexStockpotRecipe source, Level level) { + return createStockpotRecipe(recipeId, source.ingredients(), source.soupBase(), + source.result(), source.time(), source.carrier()); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/cookery/MillstoneRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/cookery/MillstoneRecipeAdapter.java new file mode 100644 index 00000000..49e31c44 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/cookery/MillstoneRecipeAdapter.java @@ -0,0 +1,36 @@ +package com.sorrowmist.useless.content.recipe.adapters.kaleidoscope.cookery; + +import com.github.ysbbbbbb.kaleidoscopecookery.crafting.output.RandomOutput; +import com.github.ysbbbbbb.kaleidoscopecookery.crafting.recipe.MillstoneRecipe; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; + +import java.util.List; + +public final class MillstoneRecipeAdapter extends AbstractCookeryRecipeAdapter { + private static final ResourceLocation MOLD_ID = + new ResourceLocation("kaleidoscope_cookery", "millstone"); + + @Override + public Class getRecipeClass() { + return MillstoneRecipe.class; + } + + @Override + protected ResourceLocation moldId() { + return MOLD_ID; + } + + @Override + protected AdvancedAlloyFurnaceRecipe convertSource( + ResourceLocation recipeId, MillstoneRecipe source, Level level) { + List outputs = source.results().stream() + .filter(result -> result != null && !result.isEmpty()) + .map(RandomOutput::stack) + .toList(); + return createMultiOutputItemRecipe(recipeId, List.of(source.ingredient()), + outputs, 200); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/cookery/PotRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/cookery/PotRecipeAdapter.java new file mode 100644 index 00000000..fe04ffad --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/cookery/PotRecipeAdapter.java @@ -0,0 +1,28 @@ +package com.sorrowmist.useless.content.recipe.adapters.kaleidoscope.cookery; + +import com.github.ysbbbbbb.kaleidoscopecookery.crafting.recipe.PotRecipe; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.Level; + +public final class PotRecipeAdapter extends AbstractCookeryRecipeAdapter { + private static final ResourceLocation MOLD_ID = + new ResourceLocation("kaleidoscope_cookery", "pot"); + + @Override + public Class getRecipeClass() { + return PotRecipe.class; + } + + @Override + protected ResourceLocation moldId() { + return MOLD_ID; + } + + @Override + protected AdvancedAlloyFurnaceRecipe convertSource( + ResourceLocation recipeId, PotRecipe source, Level level) { + return createItemRecipe(recipeId, source.ingredients(), source.carrier(), + source.result(), source.time()); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/cookery/SteamerRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/cookery/SteamerRecipeAdapter.java new file mode 100644 index 00000000..996473bf --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/cookery/SteamerRecipeAdapter.java @@ -0,0 +1,30 @@ +package com.sorrowmist.useless.content.recipe.adapters.kaleidoscope.cookery; + +import com.github.ysbbbbbb.kaleidoscopecookery.crafting.recipe.SteamerRecipe; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.Level; + +import java.util.List; + +public final class SteamerRecipeAdapter extends AbstractCookeryRecipeAdapter { + private static final ResourceLocation MOLD_ID = + new ResourceLocation("kaleidoscope_cookery", "steamer"); + + @Override + public Class getRecipeClass() { + return SteamerRecipe.class; + } + + @Override + protected ResourceLocation moldId() { + return MOLD_ID; + } + + @Override + protected AdvancedAlloyFurnaceRecipe convertSource( + ResourceLocation recipeId, SteamerRecipe source, Level level) { + return createItemRecipe(recipeId, List.of(source.getIngredient()), + source.getResult(), source.getCookTick()); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/cookery/StockpotRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/cookery/StockpotRecipeAdapter.java new file mode 100644 index 00000000..e5bfcf99 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/cookery/StockpotRecipeAdapter.java @@ -0,0 +1,20 @@ +package com.sorrowmist.useless.content.recipe.adapters.kaleidoscope.cookery; + +import com.github.ysbbbbbb.kaleidoscopecookery.crafting.recipe.StockpotRecipe; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.Level; + +public final class StockpotRecipeAdapter extends AbstractStockpotRecipeAdapter { + @Override + public Class getRecipeClass() { + return StockpotRecipe.class; + } + + @Override + protected AdvancedAlloyFurnaceRecipe convertSource( + ResourceLocation recipeId, StockpotRecipe source, Level level) { + return createStockpotRecipe(recipeId, source.ingredients(), source.soupBase(), + source.result(), source.time(), source.carrier()); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/cookery/TeapotRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/cookery/TeapotRecipeAdapter.java new file mode 100644 index 00000000..80797dec --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/cookery/TeapotRecipeAdapter.java @@ -0,0 +1,68 @@ +package com.sorrowmist.useless.content.recipe.adapters.kaleidoscope.cookery; + +import com.github.ysbbbbbb.kaleidoscopecookery.crafting.recipe.TeapotRecipe; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient; + +import java.util.List; + +public final class TeapotRecipeAdapter extends AbstractCookeryRecipeAdapter { + private static final ResourceLocation MOLD_ID = + new ResourceLocation("kaleidoscope_cookery", "teapot"); + private static final ResourceLocation EMPTY_TEA_FLUID = + new ResourceLocation("minecraft", "empty"); + + @Override + public Class getRecipeClass() { + return TeapotRecipe.class; + } + + @Override + protected ResourceLocation moldId() { + return MOLD_ID; + } + + @Override + protected AdvancedAlloyFurnaceRecipe convertSource( + ResourceLocation recipeId, TeapotRecipe source, Level level) { + if (source.teaFluid() == null || EMPTY_TEA_FLUID.equals(source.teaFluid()) + || source.ingredient() == null || source.ingredient().isEmpty() + || source.ingredientCount() <= 0 || source.result() == null + || source.result().isEmpty()) { + return null; + } + + var fluid = BuiltInRegistries.FLUID.getOptional(source.teaFluid()).orElse(null); + if (fluid == null || fluid == net.minecraft.world.level.material.Fluids.EMPTY) { + return null; + } + + ItemStack output = source.result().copyWithCount(TeapotRecipe.OUTPUT_COUNT); + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(recipeId), + List.of(new CountedIngredient(source.ingredient(), source.ingredientCount())), + List.of(new LongSizedFluidIngredient( + FluidIngredient.single(new FluidStack(fluid, 1_000)), 1_000)), + List.of(), + List.of(output), + List.of(), + List.of(), + energyFor(Math.max(1, source.time())), + Math.max(1, source.time()), + Ingredient.EMPTY, + 0, + moldIngredients(), + AlloyFurnaceMode.NORMAL + ); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/grilling/GrillingRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/grilling/GrillingRecipeAdapter.java new file mode 100644 index 00000000..b2bc87fa --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/grilling/GrillingRecipeAdapter.java @@ -0,0 +1,137 @@ +package com.sorrowmist.useless.content.recipe.adapters.kaleidoscope.grilling; + +import cn.breezeth.kaleidoscope_grilling.skewer.SkewerRecipes; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightRecipeAdapterUtils; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightSyntheticRecipe; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Kaleidoscope Grilling's skewer-cooking stage. */ +public final class GrillingRecipeAdapter implements IRecipeAdapter { + private static final int GRILLING_TIME = 800; + private static final ResourceLocation GRILL_ID = + new ResourceLocation("kaleidoscope_grilling", "grill"); + + @Override + public String sourceId() { + return RecipeSourceIds.KALEIDOSCOPE_GRILLING; + } + + @Override + public Class getRecipeClass() { + return DelightSyntheticRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + Item grill = DelightRecipeAdapterUtils.registeredItem(GRILL_ID); + return grill == null ? ItemStack.EMPTY : grill.getDefaultInstance(); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null + || holder.value().convertedRecipe() == null) { + return List.of(); + } + return List.of(holder.value().convertedRecipe()); + } + + @Override + public List> getGeneratedRecipes(Level level) { + if (level == null) { + return List.of(); + } + + List> generated = new ArrayList<>(); + for (SkewerRecipes.Cooking cooking : SkewerRecipes.cookingRecipes()) { + if (cooking == null || cooking.raw() == null || cooking.raw().isEmpty() + || cooking.cooked() == null || cooking.cooked().isEmpty()) { + continue; + } + + ResourceLocation rawId = BuiltInRegistries.ITEM.getKey(cooking.raw().getItem()); + AdvancedAlloyFurnaceRecipe converted = convertSource(rawId, cooking); + if (converted != null) { + generated.add(new RecipeHolder<>(converted.id(), + new DelightSyntheticRecipe(converted))); + } + } + return List.copyOf(generated); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) { + return List.of(); + } + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : getGeneratedRecipes(level)) { + AdvancedAlloyFurnaceRecipe recipe = holder.value().convertedRecipe(); + if (recipe != null + && DelightRecipeAdapterUtils.matchesItems(recipe.inputs(), mergedInputs, List.of()) + && DelightRecipeAdapterUtils.matchesFluids(recipe.inputFluids(), mergedFluids)) { + matches.add(holder); + } + } + return List.copyOf(matches); + } + + @Nullable + private static AdvancedAlloyFurnaceRecipe convertSource( + ResourceLocation sourceId, SkewerRecipes.Cooking cooking) { + ItemStack raw = cooking.raw(); + ItemStack cooked = cooking.cooked(); + if (sourceId == null || raw == null || raw.isEmpty() + || cooked == null || cooked.isEmpty() || cooked.getCount() <= 0) { + return null; + } + + Item rawItem = BuiltInRegistries.ITEM.getOptional(sourceId).orElse(null); + if (rawItem == null) { + return null; + } + + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(sourceId), + List.of(new CountedIngredient(Ingredient.of(rawItem), 1L)), + List.of(), + List.of(), + List.of(cooked.copy()), + List.of(), + List.of(), + Math.max(1L, (long) GRILLING_TIME * AdapterUtils.DEFAULT_ENERGY / 200L), + GRILLING_TIME, + Ingredient.EMPTY, + 0, + List.of(AdapterUtils.toMoldIngredient(grillStack())), + AlloyFurnaceMode.NORMAL); + } + + @Nullable + private static ItemStack grillStack() { + Item grill = BuiltInRegistries.ITEM.getOptional(GRILL_ID).orElse(null); + return grill == null ? null : grill.getDefaultInstance(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/tavern/AbstractTavernRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/tavern/AbstractTavernRecipeAdapter.java new file mode 100644 index 00000000..27075c30 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/tavern/AbstractTavernRecipeAdapter.java @@ -0,0 +1,105 @@ +package com.sorrowmist.useless.content.recipe.adapters.kaleidoscope.tavern; + +import appeng.api.stacks.AEKey; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.delight.DelightRecipeAdapterUtils; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.Recipe; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +abstract class AbstractTavernRecipeAdapter> implements IRecipeAdapter { + protected abstract ResourceLocation moldId(); + + @Nullable + protected abstract AdvancedAlloyFurnaceRecipe convertSource( + ResourceLocation recipeId, T source, Level level); + + @Override + public final String sourceId() { + return RecipeSourceIds.KALEIDOSCOPE_TAVERN; + } + + @Override + public final ItemStack getMoldItem() { + Item item = DelightRecipeAdapterUtils.registeredItem(moldId()); + return item == null ? ItemStack.EMPTY : item.getDefaultInstance(); + } + + @Override + public final List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + AdvancedAlloyFurnaceRecipe converted = convertSource(holder.id(), holder.value(), level); + return converted == null ? List.of() : List.of(converted); + } + + @Override + public final List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + return findMatchingRecipes(level, mergedInputs, mergedFluids, Map.of(), mold, List.of()); + } + + @Override + public final List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, Map mergedKeys, + @Nullable ItemStack mold, List actualInputs) { + if (level == null || !matchesMold(mold) + || (mergedKeys != null && !mergedKeys.isEmpty())) { + return List.of(); + } + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : DelightRecipeAdapterUtils.allOf( + level.getRecipeManager(), getRecipeClass())) { + AdvancedAlloyFurnaceRecipe converted = convertSource( + holder.id(), holder.value(), level); + if (converted != null + && DelightRecipeAdapterUtils.matchesItems( + converted.inputs(), mergedInputs, actualInputs) + && DelightRecipeAdapterUtils.matchesFluids( + converted.inputFluids(), mergedFluids)) { + matches.add(holder); + } + } + return List.copyOf(matches); + } + + protected final List moldIngredients() { + Ingredient mold = AdapterUtils.toMoldIngredient(getMoldItem()); + return mold.isEmpty() ? List.of() : List.of(mold); + } + + protected static List nonEmptyIngredients(List ingredients) { + if (ingredients == null || ingredients.isEmpty()) { + return List.of(); + } + return ingredients.stream() + .filter(ingredient -> !AdapterUtils.isIngredientEmpty(ingredient)) + .toList(); + } + + protected static int clampProcessTime(long processTime) { + return (int) Math.max(1L, Math.min(Integer.MAX_VALUE, processTime)); + } + + protected static long scaledEnergy(int processTime) { + return Math.max(1L, (long) processTime * AdapterUtils.DEFAULT_ENERGY / 200L); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/tavern/BarrelRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/tavern/BarrelRecipeAdapter.java new file mode 100644 index 00000000..5f2fc364 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/tavern/BarrelRecipeAdapter.java @@ -0,0 +1,79 @@ +package com.sorrowmist.useless.content.recipe.adapters.kaleidoscope.tavern; + +import com.github.ysbbbbbb.kaleidoscopetavern.api.blockentity.IBarrel; +import com.github.ysbbbbbb.kaleidoscopetavern.crafting.recipe.BarrelRecipe; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; + +import java.util.ArrayList; +import java.util.List; + +/** Converts Kaleidoscope Tavern barrel batches into bottled item recipes. */ +public final class BarrelRecipeAdapter extends AbstractTavernRecipeAdapter { + private static final ResourceLocation MOLD_ID = + new ResourceLocation("kaleidoscope_tavern", "barrel"); + private static final int BARREL_OUTPUT_COUNT = 16; + private static final int BREW_LEVEL_TIME_SUM = 15; + + @Override + public Class getRecipeClass() { + return BarrelRecipe.class; + } + + @Override + protected ResourceLocation moldId() { + return MOLD_ID; + } + + @Override + protected AdvancedAlloyFurnaceRecipe convertSource( + ResourceLocation recipeId, BarrelRecipe source, Level level) { + ItemStack result = source.result(); + if (source.fluid() == null || result == null || result.isEmpty()) { + return null; + } + + List batchInputs = new ArrayList<>(); + for (Ingredient ingredient : nonEmptyIngredients(source.ingredients())) { + for (int i = 0; i < BARREL_OUTPUT_COUNT; i++) { + batchInputs.add(ingredient); + } + } + if (!AdapterUtils.isIngredientEmpty(source.carrier())) { + for (int i = 0; i < BARREL_OUTPUT_COUNT; i++) { + batchInputs.add(source.carrier()); + } + } + List inputs = AdapterUtils.mergeIngredients(batchInputs); + if (inputs.isEmpty()) { + return null; + } + + int processTime = clampProcessTime( + (long) Math.max(1, source.unitTime()) * BREW_LEVEL_TIME_SUM); + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(recipeId), + inputs, + List.of(LongSizedFluidIngredient.from( + new FluidStack(source.fluid(), IBarrel.MAX_FLUID_AMOUNT))), + List.of(), + List.of(result.copyWithCount(BARREL_OUTPUT_COUNT)), + List.of(), + List.of(), + scaledEnergy(processTime), + processTime, + Ingredient.EMPTY, + 0, + moldIngredients(), + AlloyFurnaceMode.NORMAL + ); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/tavern/PressingTubRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/tavern/PressingTubRecipeAdapter.java new file mode 100644 index 00000000..55719798 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/tavern/PressingTubRecipeAdapter.java @@ -0,0 +1,56 @@ +package com.sorrowmist.useless.content.recipe.adapters.kaleidoscope.tavern; + +import com.github.ysbbbbbb.kaleidoscopetavern.crafting.recipe.PressingTubRecipe; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; + +import java.util.List; + +/** Converts Kaleidoscope Tavern pressing-tub recipes into fluid outputs. */ +public final class PressingTubRecipeAdapter + extends AbstractTavernRecipeAdapter { + private static final ResourceLocation MOLD_ID = + new ResourceLocation("kaleidoscope_tavern", "pressing_tub"); + + @Override + public Class getRecipeClass() { + return PressingTubRecipe.class; + } + + @Override + protected ResourceLocation moldId() { + return MOLD_ID; + } + + @Override + protected AdvancedAlloyFurnaceRecipe convertSource( + ResourceLocation recipeId, PressingTubRecipe source, Level level) { + Ingredient input = source.getIngredient(); + int fluidAmount = source.getFluidAmount(); + if (input == null || input.isEmpty() || source.getFluid() == null || fluidAmount <= 0) { + return null; + } + + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(recipeId), + List.of(new CountedIngredient(input, 1L)), + List.of(), + List.of(), + List.of(), + List.of(new FluidStack(source.getFluid(), fluidAmount)), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + AdapterUtils.DEFAULT_PROCESS_TIME, + Ingredient.EMPTY, + 0, + moldIngredients(), + AlloyFurnaceMode.NORMAL + ); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/tavern/ShakerRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/tavern/ShakerRecipeAdapter.java new file mode 100644 index 00000000..cfaddc78 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/kaleidoscope/tavern/ShakerRecipeAdapter.java @@ -0,0 +1,56 @@ +package com.sorrowmist.useless.content.recipe.adapters.kaleidoscope.tavern; + +import com.github.ysbbbbbb.kaleidoscopetavern.crafting.recipe.ShakerRecipe; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.Level; + +import java.util.List; + +/** Converts Kaleidoscope Tavern shaker recipes into item recipes. */ +public final class ShakerRecipeAdapter extends AbstractTavernRecipeAdapter { + private static final ResourceLocation MOLD_ID = + new ResourceLocation("kaleidoscope_tavern", "shaker"); + + @Override + public Class getRecipeClass() { + return ShakerRecipe.class; + } + + @Override + protected ResourceLocation moldId() { + return MOLD_ID; + } + + @Override + protected AdvancedAlloyFurnaceRecipe convertSource( + ResourceLocation recipeId, ShakerRecipe source, Level level) { + List inputs = AdapterUtils.mergeIngredients( + nonEmptyIngredients(source.ingredients())); + ItemStack output = source.result(); + if (inputs.isEmpty() || output == null || output.isEmpty()) { + return null; + } + + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(recipeId), + inputs, + List.of(), + List.of(), + List.of(output.copy()), + List.of(), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + AdapterUtils.DEFAULT_PROCESS_TIME, + Ingredient.EMPTY, + 0, + moldIngredients(), + AlloyFurnaceMode.NORMAL + ); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/lychee/LycheeBlockCrushingRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/lychee/LycheeBlockCrushingRecipeAdapter.java new file mode 100644 index 00000000..81e7ca44 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/lychee/LycheeBlockCrushingRecipeAdapter.java @@ -0,0 +1,330 @@ +package com.sorrowmist.useless.content.recipe.adapters.lychee; + +import appeng.api.stacks.AEKey; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.api.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.ExpectedOutputScaler; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.tags.BlockTags; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import snownee.kiwi.recipe.SizedIngredient; +import snownee.lychee.RecipeTypes; +import snownee.lychee.contextual.Chance; +import snownee.lychee.recipes.BlockCrushingRecipe; +import snownee.lychee.util.IngredientCollection; +import snownee.lychee.util.action.PostAction; +import snownee.lychee.util.contextual.ContextualCondition; +import snownee.lychee.util.predicates.BlockPredicateExtensions; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * 将 Lychee 的 {@code lychee:block_crushing} 配方转换为高级合金炉配方。 + * + *

Lychee 的下落方块压碎配方是数据驱动的:配方声明下落方块谓词、落地方块谓词与一组带数量的 + * 输入材料,匹配成功后执行 {@code post} 中的动作列表。KubeJS 通过 + * {@code ServerEvents.recipes} 注册的同类配方同样经由 RecipeManager 加载,因此本适配器统一 + * 枚举 {@link RecipeTypes#BLOCK_CRUSHING},不区分配方来自数据包 JSON 还是脚本。

+ * + *

本适配器只接纳下落方块为铁砧(或谓词为任意方块)的配方,并以铁砧作为合金炉模具,从而与 + * 神化铁砧机制保持一致。

+ * + *

产出语义:Lychee 把每条产出的触发概率表达为 post action 上的 {@code chance} 条件,而非 + * 产出的固有属性。转换时经 {@link ExpectedOutputScaler} 把独立概率产出折算为最小确定性批次, + * 输入、能耗与处理时间按同一批次倍数同步放大,避免把概率产出错误地当作必出。

+ * + *

已知取舍:{@code max_repeats} 控制同一次落地重复执行配方的次数,其随机上界无法在不引入 + * 运行期随机的前提下表达为确定性配方,故此处按单次执行折算。

+ */ +public final class LycheeBlockCrushingRecipeAdapter + implements IRecipeAdapter { + + @Override + public String sourceId() { + return RecipeSourceIds.LYCHEE; + } + + @Override + public Class getRecipeClass() { + return BlockCrushingRecipe.class; + } + + /** 模具统一为铁砧,仅接管铁砧下落压碎配方。 */ + @Override + public ItemStack getMoldItem() { + return new ItemStack(Items.ANVIL); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + Map mergedKeys, @Nullable ItemStack mold, List actualInputs) { + if (level == null || !matchesMold(mold)) { + return List.of(); + } + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(level.getRecipeManager(), RecipeTypes.BLOCK_CRUSHING)) { + AdvancedAlloyFurnaceRecipe converted = convertRecipe(holder); + if (converted == null) { + continue; + } + if (AdapterUtils.matchesRequired(mergedInputs, requiredCounts(converted.inputs()))) { + matches.add(holder); + } + } + return List.copyOf(matches); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + AdvancedAlloyFurnaceRecipe converted = convertRecipe(holder); + return converted == null ? List.of() : List.of(converted); + } + + @Nullable + private AdvancedAlloyFurnaceRecipe convertRecipe(@Nullable RecipeHolder holder) { + if (holder == null || holder.value() == null) { + return null; + } + + BlockCrushingRecipe source = holder.value(); + if (!isAnvilFallingBlock(source)) { + return null; + } + + List inputs = toCountedIngredients(source.ingredientCollection()); + if (inputs.isEmpty()) { + return null; + } + + List deterministic = new ArrayList<>(); + List weighted = new ArrayList<>(); + collectOutputs(source, deterministic, weighted); + if (deterministic.isEmpty() && weighted.isEmpty()) { + return null; + } + + ItemStack mold = getMoldItem(); + if (mold.isEmpty()) { + return null; + } + + Optional scaled = ExpectedOutputScaler.scale(weighted); + if (scaled.isEmpty()) { + return null; + } + int operations = scaled.get().operations(); + + List scaledInputs = scaleInputs(inputs, operations); + if (scaledInputs == null) { + return null; + } + + List outputs = new ArrayList<>(); + for (ItemStack stack : deterministic) { + if (!addOutput(outputs, stack, (long) stack.getCount() * operations)) { + return null; + } + } + for (ItemStack stack : scaled.get().outputs()) { + if (!addOutput(outputs, stack, stack.getCount())) { + return null; + } + } + if (outputs.isEmpty()) { + return null; + } + + final long energy; + final int processTime; + try { + energy = Math.multiplyExact(AdapterUtils.DEFAULT_ENERGY, (long) operations); + long time = Math.multiplyExact((long) AdapterUtils.DEFAULT_PROCESS_TIME, operations); + if (time <= 0 || time > Integer.MAX_VALUE) { + return null; + } + processTime = (int) time; + } catch (ArithmeticException exception) { + return null; + } + + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + scaledInputs, + List.of(), + List.of(), + outputs, + List.of(), + List.of(), + energy, + processTime, + Ingredient.EMPTY, + 0, + AdapterUtils.toMoldIngredient(mold), + AlloyFurnaceMode.NORMAL); + } + + /** + * 判定配方的下落方块谓词是否覆盖铁砧。 + * + *

谓词为“任意方块”时同样接受:这类配方本就不限定下落方块,而合金炉通过铁砧模具 + * 表达触发条件,转换后语义仍然成立。

+ */ + private static boolean isAnvilFallingBlock(BlockCrushingRecipe recipe) { + if (BlockPredicateExtensions.isAny(recipe.blockPredicate())) { + return true; + } + for (Holder holder : BuiltInRegistries.BLOCK.getTagOrEmpty(BlockTags.ANVIL)) { + for (BlockState state : holder.value().getStateDefinition().getPossibleStates()) { + if (recipe.matchesFallingBlock(state, null)) { + return true; + } + } + } + return false; + } + + /** + * 按产出动作收集结果,并按动作自身的概率条件分流。 + * + *

概率为 1 的产出进入确定性列表,其余进入带权列表交由 {@link ExpectedOutputScaler} + * 折算;概率为 0 的产出直接丢弃。

+ */ + private static void collectOutputs( + BlockCrushingRecipe recipe, + List deterministic, + List weighted) { + for (PostAction action : recipe.postActions()) { + if (action == null) { + continue; + } + double chance = chanceOf(action); + if (chance <= 0.0d) { + continue; + } + for (ItemStack stack : action.getOutputItems()) { + if (stack == null || stack.isEmpty()) { + continue; + } + if (chance >= 1.0d) { + deterministic.add(stack.copy()); + } else { + weighted.add(new ExpectedOutputScaler.WeightedItemOutput( + stack.copy(), stack.getCount(), stack.getCount(), chance)); + } + } + } + } + + /** + * 汇总动作条件中的概率约束。 + * + *

同一动作上的多个 {@link Chance} 条件按逻辑与语义叠乘;未声明概率约束时视为必出。

+ */ + private static double chanceOf(PostAction action) { + double chance = 1.0d; + for (ContextualCondition condition : action.conditions()) { + if (condition instanceof Chance chanced) { + chance *= Math.max(0.0d, Math.min(1.0d, chanced.chance())); + } + } + return chance; + } + + private static List toCountedIngredients(@Nullable IngredientCollection collection) { + if (collection == null || collection.isEmpty()) { + return List.of(); + } + + Map merged = new LinkedHashMap<>(); + for (SizedIngredient sized : collection.ingredients()) { + if (sized == null || sized.ingredient() == null || sized.ingredient().isEmpty() + || sized.count() <= 0) { + continue; + } + AdapterUtils.mergeIngredient(merged, sized.ingredient(), sized.count()); + } + return merged.entrySet().stream() + .map(entry -> new CountedIngredient(entry.getKey(), entry.getValue())) + .toList(); + } + + /** 按批次倍数放大输入数量;溢出或结果为空时返回 {@code null}。 */ + @Nullable + private static List scaleInputs(List inputs, int operations) { + if (operations <= 0) { + return null; + } + List scaled = new ArrayList<>(inputs.size()); + for (CountedIngredient input : inputs) { + if (input == null || input.ingredient() == null || input.ingredient().isEmpty() + || input.count() <= 0) { + continue; + } + final long count; + try { + count = Math.multiplyExact(input.count(), (long) operations); + } catch (ArithmeticException exception) { + return null; + } + scaled.add(new CountedIngredient(input.ingredient(), count)); + } + return scaled.isEmpty() ? null : List.copyOf(scaled); + } + + /** 合并同物品产出;数量溢出时返回 {@code false}。 */ + private static boolean addOutput(List outputs, ItemStack stack, long amount) { + if (stack == null || stack.isEmpty() || amount <= 0) { + return true; + } + for (ItemStack existing : outputs) { + if (!ItemStack.isSameItemSameTags(existing, stack)) { + continue; + } + long merged = (long) existing.getCount() + amount; + if (merged > Integer.MAX_VALUE) { + return false; + } + existing.setCount((int) merged); + return true; + } + if (amount > Integer.MAX_VALUE) { + return false; + } + ItemStack copy = stack.copy(); + copy.setCount((int) amount); + outputs.add(copy); + return true; + } + + private static Map requiredCounts(List inputs) { + Map required = new LinkedHashMap<>(); + for (CountedIngredient input : inputs) { + if (input == null || input.ingredient() == null || input.ingredient().isEmpty() + || input.count() <= 0) { + continue; + } + AdapterUtils.mergeIngredient(required, input.ingredient(), input.count()); + } + return required; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/malum/MalumAdapterUtils.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/malum/MalumAdapterUtils.java new file mode 100644 index 00000000..f3988260 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/malum/MalumAdapterUtils.java @@ -0,0 +1,109 @@ +package com.sorrowmist.useless.content.recipe.adapters.malum; + +import com.sammy.malum.core.systems.recipe.SpiritIngredient; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.DataComponentIngredient; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Shared validation and component-aware input helpers for Malum recipe adapters. */ +final class MalumAdapterUtils { + static final String MOD_ID = "malum"; + + private MalumAdapterUtils() { + } + + static ItemStack item(String path) { + Item item = BuiltInRegistries.ITEM.get(new ResourceLocation(MOD_ID, path)); + return item == Items.AIR ? ItemStack.EMPTY : new ItemStack(item); + } + + static Map requirements() { + return new LinkedHashMap<>(); + } + + static boolean addIngredient(Map target, @Nullable Ingredient ingredient, long count) { + if (target == null || ingredient == null || ingredient.isEmpty() || count <= 0L) { + return false; + } + try { + for (Map.Entry entry : target.entrySet()) { + if (AdapterUtils.areIngredientsEqual(entry.getKey(), ingredient)) { + target.put(entry.getKey(), Math.addExact(entry.getValue(), count)); + return true; + } + } + target.put(ingredient, count); + return true; + } catch (ArithmeticException exception) { + return false; + } + } + + static boolean addSpirits(Map target, @Nullable List spirits) { + if (spirits == null) { + return false; + } + for (SpiritIngredient spirit : spirits) { + if (spirit == null || spirit.count() <= 0) { + return false; + } + ItemStack shard; + try { + shard = spirit.asItemStack(); + } catch (RuntimeException exception) { + return false; + } + if (shard == null || shard.isEmpty() || shard.getCount() <= 0) { + return false; + } + Ingredient exactShard = DataComponentIngredient.of(true, shard.copyWithCount(1)); + if (!addIngredient(target, exactShard, shard.getCount())) { + return false; + } + } + return true; + } + + @Nullable + static List counted(Map requirements) { + if (requirements == null) { + return null; + } + List result = new ArrayList<>(requirements.size()); + for (Map.Entry entry : requirements.entrySet()) { + if (entry.getKey() == null || entry.getKey().isEmpty() + || entry.getValue() == null || entry.getValue() <= 0L) { + return null; + } + result.add(new CountedIngredient(entry.getKey(), entry.getValue())); + } + return List.copyOf(result); + } + + static List distinctMatches(@Nullable List stacks, @Nullable Ingredient ingredient) { + List matches = new ArrayList<>(); + if (stacks == null || ingredient == null || ingredient.isEmpty()) { + return matches; + } + for (ItemStack stack : stacks) { + if (stack == null || stack.isEmpty() || !ingredient.test(stack) + || matches.stream().anyMatch(existing -> ItemStack.isSameItemSameTags(existing, stack))) { + continue; + } + matches.add(stack.copyWithCount(1)); + } + return matches; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/malum/SpiritFocusingRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/malum/SpiritFocusingRecipeAdapter.java new file mode 100644 index 00000000..e572a8ed --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/malum/SpiritFocusingRecipeAdapter.java @@ -0,0 +1,119 @@ +package com.sorrowmist.useless.content.recipe.adapters.malum; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.mojang.logging.LogUtils; +import com.sammy.malum.common.recipe.SpiritFocusingRecipe; +import com.sammy.malum.registry.common.recipe.MalumRecipeTypes; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Malum spirit-crucible recipes while intentionally ignoring the reusable Impetus item. */ +public final class SpiritFocusingRecipeAdapter implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + + @Override + public Class getRecipeClass() { + return SpiritFocusingRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return MalumAdapterUtils.item("spirit_crucible"); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + Converted converted = convertData(holder.value()); + if (converted == null) { + RateLimitedLogger.warn(LOGGER, "Skipping invalid Malum spirit focusing recipe: {}", holder.id()); + return List.of(); + } + return List.of(createRecipe(holder, converted)); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold) || mergedInputs == null) { + return List.of(); + } + + List> matches = new ArrayList<>(); + RecipeManager recipeManager = level.getRecipeManager(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + MalumRecipeTypes.SPIRIT_FOCUSING.get())) { + Converted converted = convertData(holder.value()); + if (converted != null && AdapterUtils.matchesRequired(mergedInputs, converted.requirements())) { + matches.add(holder); + } + } + return matches; + } + + @Nullable + private static Converted convertData(@Nullable SpiritFocusingRecipe source) { + if (source == null || source.time <= 0) { + return null; + } + ItemStack output = source.output; + if (output == null || output.isEmpty() || output.getCount() <= 0) { + return null; + } + + // The source input is the reusable, damageable Impetus. The alloy-furnace integration + // deliberately omits it, per the compatibility contract. + Map requirements = MalumAdapterUtils.requirements(); + if (!MalumAdapterUtils.addSpirits(requirements, source.spirits)) { + return null; + } + List inputs = MalumAdapterUtils.counted(requirements); + if (inputs == null) { + return null; + } + return new Converted(inputs, requirements, output.copy(), source.time); + } + + private AdvancedAlloyFurnaceRecipe createRecipe( + RecipeHolder holder, Converted converted) { + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + converted.inputs(), + List.of(), + List.of(converted.output().copy()), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + converted.processTime(), + Ingredient.EMPTY, + 0, + AdapterUtils.toMoldIngredient(getMoldItem()), + AlloyFurnaceMode.NORMAL + ); + } + + private record Converted( + List inputs, + Map requirements, + ItemStack output, + int processTime) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/malum/SpiritInfusionRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/malum/SpiritInfusionRecipeAdapter.java new file mode 100644 index 00000000..4bf026a5 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/malum/SpiritInfusionRecipeAdapter.java @@ -0,0 +1,315 @@ +package com.sorrowmist.useless.content.recipe.adapters.malum; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.GenericStack; +import com.mojang.logging.LogUtils; +import com.sammy.malum.common.recipe.SpiritInfusionRecipe; +import com.sammy.malum.registry.common.recipe.MalumRecipeTypes; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.PatternStackView; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.ItemIngredientAllocator; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import com.sorrowmist.useless.compat.forge1201.recipe.DataComponentIngredient; +import net.minecraftforge.common.crafting.SizedIngredient; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** Converts Malum spirit-altar recipes, including component-carrying infusion results. */ +public final class SpiritInfusionRecipeAdapter implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final int PROCESS_TIME = 300; + + @Override + public Class getRecipeClass() { + return SpiritInfusionRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return MalumAdapterUtils.item("spirit_altar"); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + Converted converted = convertData(holder.value(), null, null); + if (converted == null) { + RateLimitedLogger.warn(LOGGER, "Skipping invalid Malum spirit infusion recipe: {}", holder.id()); + return List.of(); + } + return List.of(createRecipe(AdapterUtils.convertedId(holder.id()), converted)); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level, List actualInputs) { + if (holder == null || holder.value() == null || !holder.value().carryOverComponentData) { + return convertAll(holder, level); + } + + SpiritInfusionRecipe source = holder.value(); + if (!hasValidPrimaryInput(source)) { + return List.of(); + } + ServerLevel serverLevel = level instanceof ServerLevel server ? server : null; + List converted = new ArrayList<>(); + for (ItemStack primary : MalumAdapterUtils.distinctMatches(actualInputs, source.input.ingredient())) { + Converted data = convertData(source, primary, serverLevel); + if (data != null) { + converted.add(createRecipe(variantId(holder.id(), primary), data)); + } + } + return converted; + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold) || mergedInputs == null || mergedInputs.isEmpty()) { + return List.of(); + } + + List> matches = new ArrayList<>(); + RecipeManager recipeManager = level.getRecipeManager(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + MalumRecipeTypes.SPIRIT_INFUSION.get())) { + Converted converted = convertData(holder.value(), null, null); + if (converted != null && AdapterUtils.matchesRequired(mergedInputs, converted.requirements())) { + matches.add(holder); + } + } + return matches; + } + + /** Identifies carry-over altar patterns whose primary input and output are component-dynamic. */ + public static Optional findDynamicPatternProfile( + @Nullable Level level, List patternInputs, List patternOutputs) { + return findDynamicPatternProfileLong(level, PatternStackView.fromLegacy(patternInputs, patternOutputs)); + } + + public static Optional findDynamicPatternProfileLong( + @Nullable Level level, @Nullable PatternStackView pattern) { + if (level == null) { + return Optional.empty(); + } + return findDynamicPatternProfileLong( + com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(level.getRecipeManager(), MalumRecipeTypes.SPIRIT_INFUSION.get()), + pattern); + } + + static Optional findDynamicPatternProfile( + Iterable> recipes, + List patternInputs, List patternOutputs) { + return findDynamicPatternProfileLong(recipes, + PatternStackView.fromLegacy(patternInputs, patternOutputs)); + } + + static Optional findDynamicPatternProfileLong( + Iterable> recipes, @Nullable PatternStackView pattern) { + if (recipes == null || pattern == null || pattern.inputs().isEmpty() + || pattern.outputs().size() != 1 || !validItemStacks(pattern)) { + return Optional.empty(); + } + + List matches = new ArrayList<>(); + for (RecipeHolder holder : recipes) { + SpiritInfusionRecipe source = holder == null ? null : holder.value(); + if (source == null || !source.carryOverComponentData || !hasValidPrimaryInput(source) + || source.input.ingredient().getCustomIngredient() != null) { + continue; + } + + Converted converted = convertData(source, null, null); + if (converted == null || !matchesStaticOutput(converted.output(), pattern.outputs().get(0)) + || !matchesPatternInputs(converted.inputs(), pattern)) { + continue; + } + + int primarySlot = uniquePrimarySlot(source.input.ingredient(), pattern.inputRepresentatives()); + if (primarySlot < 0) { + continue; + } + matches.add(new DynamicPatternProfile(Set.of(primarySlot), Set.of(0))); + if (matches.size() > 1) { + return Optional.empty(); + } + } + return matches.isEmpty() ? Optional.empty() : Optional.of(matches.get(0)); + } + + @Nullable + private static Converted convertData( + @Nullable SpiritInfusionRecipe source, @Nullable ItemStack actualPrimary, + @Nullable ServerLevel serverLevel) { + if (!hasValidPrimaryInput(source) || source.result == null || source.result.isEmpty() + || source.result.getCount() <= 0 || source.extraInputs == null) { + return null; + } + + Map requirements = MalumAdapterUtils.requirements(); + Ingredient primaryRequirement = actualPrimary == null + ? source.input.ingredient() + : DataComponentIngredient.of(true, actualPrimary.copyWithCount(1)); + if (!MalumAdapterUtils.addIngredient(requirements, primaryRequirement, source.input.count())) { + return null; + } + for (SizedIngredient extra : source.extraInputs) { + if (extra == null || extra.ingredient() == null + || !MalumAdapterUtils.addIngredient(requirements, extra.ingredient(), extra.count())) { + return null; + } + } + if (!MalumAdapterUtils.addSpirits(requirements, source.spirits)) { + return null; + } + + ItemStack output = source.result.copy(); + if (source.carryOverComponentData && actualPrimary != null && serverLevel != null) { + try { + output = source.getOutput(serverLevel, actualPrimary.copy()); + } catch (RuntimeException exception) { + RateLimitedLogger.warn(LOGGER, "Failed to create component-carrying Malum infusion output", exception); + return null; + } + } + if (output == null || output.isEmpty() || output.getCount() <= 0) { + return null; + } + + List inputs = MalumAdapterUtils.counted(requirements); + if (inputs == null) { + return null; + } + return new Converted(inputs, requirements, output.copy()); + } + + private AdvancedAlloyFurnaceRecipe createRecipe(ResourceLocation id, Converted converted) { + return new AdvancedAlloyFurnaceRecipe( + id, + converted.inputs(), + List.of(), + List.of(converted.output().copy()), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + PROCESS_TIME, + Ingredient.EMPTY, + 0, + AdapterUtils.toMoldIngredient(getMoldItem()), + AlloyFurnaceMode.NORMAL + ); + } + + private static boolean hasValidPrimaryInput(@Nullable SpiritInfusionRecipe source) { + return source != null && source.input != null && source.input.ingredient() != null + && !source.input.ingredient().isEmpty() && source.input.count() > 0; + } + + private static boolean matchesPatternInputs( + List requirements, PatternStackView pattern) { + return totalRequiredItems(requirements) == totalPatternItems(pattern.inputs()) + && ItemIngredientAllocator.matches(requirements, List.of(), pattern.inputs(), 1L); + } + + private static boolean matchesStaticOutput(ItemStack expected, GenericStack actual) { + if (expected == null || expected.isEmpty() || actual == null || actual.amount() <= 0L + || actual.amount() != expected.getCount() || !(actual.what() instanceof AEItemKey itemKey)) { + return false; + } + return ItemStack.isSameItemSameTags(expected, itemKey.toStack(1)); + } + + private static int uniquePrimarySlot(Ingredient primary, List patternInputs) { + int result = -1; + for (int slot = 0; slot < patternInputs.size(); slot++) { + ItemStack input = patternInputs.get(slot); + if (input == null || input.isEmpty() || !primary.test(input)) { + continue; + } + if (result >= 0) { + return -1; + } + result = slot; + } + return result; + } + + private static long totalRequiredItems(List inputs) { + long result = 0L; + for (CountedIngredient input : inputs) { + if (input == null || input.count() <= 0L) { + continue; + } + if (result > Long.MAX_VALUE - input.count()) { + return Long.MAX_VALUE; + } + result += input.count(); + } + return result; + } + + private static long totalPatternItems(List inputs) { + long result = 0L; + for (GenericStack input : inputs) { + if (input == null || input.amount() <= 0L || !(input.what() instanceof AEItemKey)) { + continue; + } + if (result > Long.MAX_VALUE - input.amount()) { + return Long.MAX_VALUE; + } + result += input.amount(); + } + return result; + } + + private static boolean validItemStacks(PatternStackView pattern) { + return pattern.inputs().stream().allMatch(stack -> stack != null && stack.amount() > 0L + && stack.what() instanceof AEItemKey) + && pattern.outputs().stream().allMatch(stack -> stack != null && stack.amount() > 0L + && stack.what() instanceof AEItemKey); + } + + private static ResourceLocation variantId(ResourceLocation source, ItemStack primary) { + ResourceLocation itemId = BuiltInRegistries.ITEM.getKey(primary.getItem()); + return new ResourceLocation( + source.getNamespace(), + source.getPath() + "_converted_" + itemId.getNamespace() + "_" + + itemId.getPath().replace('/', '_')); + } + + private record Converted( + List inputs, + Map requirements, + ItemStack output) { + } + + public record DynamicPatternProfile(Set idOnlyInputSlots, Set idOnlyOutputSlots) { + public DynamicPatternProfile { + idOnlyInputSlots = Set.copyOf(new LinkedHashSet<>(idOnlyInputSlots)); + idOnlyOutputSlots = Set.copyOf(new LinkedHashSet<>(idOnlyOutputSlots)); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/malum/VoidFavorRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/malum/VoidFavorRecipeAdapter.java new file mode 100644 index 00000000..f4c4b3f1 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/malum/VoidFavorRecipeAdapter.java @@ -0,0 +1,95 @@ +package com.sorrowmist.useless.content.recipe.adapters.malum; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.mojang.logging.LogUtils; +import com.sammy.malum.common.recipe.VoidFavorRecipe; +import com.sammy.malum.registry.common.recipe.MalumRecipeTypes; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Malum's Weeping Well item transmutations. */ +public final class VoidFavorRecipeAdapter implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final int PROCESS_TIME = 80; + + @Override + public Class getRecipeClass() { + return VoidFavorRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return MalumAdapterUtils.item("void_conduit"); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + + VoidFavorRecipe source = holder.value(); + if (source.input == null || source.input.isEmpty() + || source.result == null || source.result.isEmpty() + || source.result.getCount() <= 0) { + RateLimitedLogger.warn(LOGGER, "Skipping invalid Malum void favor recipe: {}", holder.id()); + return List.of(); + } + + AdvancedAlloyFurnaceRecipe converted = new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + List.of(new CountedIngredient(source.input, 1L)), + List.of(), + List.of(source.result.copy()), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + PROCESS_TIME, + Ingredient.EMPTY, + 0, + AdapterUtils.toMoldIngredient(getMoldItem()), + AlloyFurnaceMode.NORMAL + ); + return List.of(converted); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold) || mergedInputs == null || mergedInputs.isEmpty()) { + return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + MalumRecipeTypes.VOID_FAVOR.get())) { + VoidFavorRecipe source = holder.value(); + if (source != null && source.input != null && !source.input.isEmpty() + && source.result != null && !source.result.isEmpty() + && source.result.getCount() > 0 + && AdapterUtils.matchesRequired( + mergedInputs, Map.of(source.input, 1L))) { + matches.add(holder); + } + } + return matches; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/BoilerRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/BoilerRecipeAdapter.java new file mode 100644 index 00000000..0bec73e8 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/BoilerRecipeAdapter.java @@ -0,0 +1,91 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism; + +import appeng.api.stacks.GenericStack; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import mekanism.api.MekanismAPI; +import mekanism.api.chemical.ChemicalStack; +import mekanism.api.chemical.gas.Gas; +import mekanism.api.chemical.gas.GasStack; +import mekanism.api.chemical.gas.attribute.GasAttributes; +import mekanism.api.recipes.ingredients.creator.IngredientCreatorAccess; +import mekanism.common.Mekanism; +import mekanism.common.registries.MekanismBlocks; +import mekanism.common.registries.MekanismGases; +import mekanism.common.util.HeatUtils; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import net.minecraft.tags.FluidTags; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** Dynamic Thermal Evaporative Boiler recipe-viewer adapter. */ +public final class BoilerRecipeAdapter extends MekanismSyntheticRecipeAdapter { + private static final long PROCESS_TICKS = AdapterUtils.MEKANISM_BASE_TICKS_REQUIRED; + + @Override + public @Nullable ItemStack getMoldItem() { + return new ItemStack(MekanismBlocks.BOILER_CASING.getBlock()); + } + + @Override + protected List> createGeneratedRecipes(Level level) { + List> result = new ArrayList<>(); + List water = MekanismChemicalRecipeSupport.fluidIngredients( + IngredientCreatorAccess.fluid().from(FluidTags.WATER, WATER_AMOUNT)); + if (water.isEmpty()) return result; + + // This is the common-side equivalent of BoilerRecipeViewerRecipe.getBoilerRecipes(). + double waterToSteamHeatNecessary = WATER_AMOUNT * HeatUtils.getWaterThermalEnthalpy() + / HeatUtils.getSteamEnergyEfficiency(); + ChemicalStack steam = MekanismGases.STEAM.getStack(WATER_AMOUNT); + ResourceLocation waterId = Mekanism.rl("water"); + addRecipe(result, waterId, water.get(0), null, steam, mekanism.api.chemical.gas.GasStack.EMPTY); + + for (Gas gas : MekanismAPI.gasRegistry()) { + GasAttributes.HeatedCoolant coolant=gas.get(GasAttributes.HeatedCoolant.class); + if(coolant==null) continue; + long amount=Math.round(waterToSteamHeatNecessary/coolant.getThermalEnthalpy()); + if(amount<=0) continue; + GasStack heated=new GasStack(gas,amount); + addRecipe(result,Mekanism.rl("boiler_"+id(heated)),water.get(0),heated,steam, + new GasStack(coolant.getCooledGas(),amount)); + } + return result; + } + + private static final int WATER_AMOUNT = 1; + + private void addRecipe(List> result, ResourceLocation sourceId, + SizedFluidIngredient water, @Nullable ChemicalStack coolant, + ChemicalStack steam, ChemicalStack cooledCoolant) { + GenericStack steamKey = MekanismChemicalRecipeSupport.key(steam); + GenericStack coolantKey = coolant == null ? null : MekanismChemicalRecipeSupport.key(coolant); + GenericStack cooledKey = MekanismChemicalRecipeSupport.key(cooledCoolant); + if (steamKey == null || (coolant != null && coolantKey == null) + || (!cooledCoolant.isEmpty() && cooledKey == null)) return; + + List chemicalInputs = coolantKey == null ? List.of() : List.of(coolantKey); + List chemicalOutputs = new ArrayList<>(); + chemicalOutputs.add(steamKey); + if (cooledKey != null) chemicalOutputs.add(cooledKey); + String suffix = "boiler_" + (coolant == null ? "water" : id(coolant)); + ResourceLocation id = MekanismChemicalRecipeSupport.variantId(sourceId, suffix); + AdvancedAlloyFurnaceRecipe converted = MekanismChemicalRecipeSupport.recipe( + id, List.of(), List.of(water), chemicalInputs, List.of(), List.of(), chemicalOutputs, + 0L, + AdapterUtils.safeInt(PROCESS_TICKS), getMoldItem()); + result.add(MekanismChemicalRecipeSupport.syntheticHolder(id, converted)); + } + + private static String id(ChemicalStack stack) { + var id = stack.getTypeRegistryName(); + return id.getNamespace() + "_" + id.getPath().replace('/', '_'); + } + +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/ChemicalChemicalRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/ChemicalChemicalRecipeAdapter.java new file mode 100644 index 00000000..0faa0ba4 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/ChemicalChemicalRecipeAdapter.java @@ -0,0 +1,140 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism; + +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import mekanism.api.chemical.ChemicalStack; +import mekanism.api.recipes.chemical.ChemicalChemicalToChemicalRecipe; +import mekanism.common.recipe.MekanismRecipeType; +import mekanism.api.recipes.ingredients.ChemicalStackIngredient; +import mekanism.common.registries.MekanismBlocks; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Chemical + chemical -> chemical adapters for the Chemical Infuser and Pigment Mixer. */ +public final class ChemicalChemicalRecipeAdapter implements IRecipeAdapter> { + private static final long PROCESS_TICKS = AdapterUtils.MEKANISM_BASE_TICKS_REQUIRED; + private static final long ENERGY_PER_TICK = 200L; + + private final RecipeType> recipeType; + private final ItemStack mold; + private final String name; + + public ChemicalChemicalRecipeAdapter(RecipeType> recipeType, + ItemStack mold, String name) { + this.recipeType = recipeType; + this.mold = mold == null ? ItemStack.EMPTY : mold.copy(); + this.name = name; + } + + public static ChemicalChemicalRecipeAdapter chemicalInfuser() { + return new ChemicalChemicalRecipeAdapter( + MekanismRecipeType.CHEMICAL_INFUSING.get(), + new ItemStack(MekanismBlocks.CHEMICAL_INFUSER.getBlock()), "chemical_infuser"); + } + + public static ChemicalChemicalRecipeAdapter pigmentMixer() { + return new ChemicalChemicalRecipeAdapter( + MekanismRecipeType.PIGMENT_MIXING.get(), + new ItemStack(MekanismBlocks.PIGMENT_MIXER.getBlock()), "pigment_mixer"); + } + + @Override + public Class> getRecipeClass() { + return (Class) ChemicalChemicalToChemicalRecipe.class; + } + + @Override + public @Nullable ItemStack getMoldItem() { + return mold.isEmpty() ? null : mold.copy(); + } + + @Override + public List convertAll(RecipeHolder> holder, + Level level) { + if (holder != null && NativeMekanismVariants.needsExpansion(holder.value())) + return NativeMekanismVariants.expand(holder.value(), () -> convertAll(holder, level)); + if (holder == null || level == null || holder.value().getType() != recipeType) return List.of(); + ChemicalChemicalToChemicalRecipe original = holder.value(); + ChemicalStackIngredient left = original.getLeftInput(); + ChemicalStackIngredient right = original.getRightInput(); + if (left == null || right == null || left.hasNoMatchingInstances() || right.hasNoMatchingInstances()) { + return List.of(); + } + + List result = new ArrayList<>(); + for (ChemicalStack leftStack : left.getRepresentations()) { + GenericStack leftKey = MekanismChemicalRecipeSupport.key(leftStack); + if (leftKey == null) continue; + for (ChemicalStack rightStack : right.getRepresentations()) { + GenericStack rightKey = MekanismChemicalRecipeSupport.key(rightStack); + if (rightKey == null) continue; + for (ChemicalStack outputStack : original.getOutputDefinition()) { + GenericStack outputKey = MekanismChemicalRecipeSupport.key(outputStack); + if (outputKey == null) continue; + String suffix = name + "_" + id(leftStack) + "_" + id(rightStack) + "_out_" + id(outputStack); + result.add(MekanismChemicalRecipeSupport.recipe( + MekanismChemicalRecipeSupport.variantId(holder.id(), suffix), List.of(), List.of(), + List.of(leftKey, rightKey), List.of(), List.of(), List.of(outputKey), + AdapterUtils.mekanismEnergyCost(ENERGY_PER_TICK, PROCESS_TICKS, 1L), + AdapterUtils.safeInt(PROCESS_TICKS), getMoldItem())); + } + } + } + return result; + } + + @Override + public List>> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + Map mergedKeys, @Nullable ItemStack mold) { + if (level == null || mergedKeys.isEmpty() || !matchesMold(mold)) return List.of(); + RecipeManager manager = level.getRecipeManager(); + List>> result = new ArrayList<>(); + for (RecipeHolder> holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.>getAllRecipesFor(manager, recipeType)) { + ChemicalChemicalToChemicalRecipe recipe = holder.value(); + if (matchesPair(mergedKeys, recipe.getLeftInput(), recipe.getRightInput())) result.add(holder); + } + return result; + } + + private static boolean matchesPair(Map available, ChemicalStackIngredient left, + ChemicalStackIngredient right) { + for (ChemicalStack leftStack : left.getRepresentations()) { + GenericStack leftKey = MekanismChemicalRecipeSupport.key(leftStack); + if (leftKey == null) continue; + for (ChemicalStack rightStack : right.getRepresentations()) { + GenericStack rightKey = MekanismChemicalRecipeSupport.key(rightStack); + if (rightKey == null) continue; + long leftAmount = leftKey.amount(); + long rightAmount = rightKey.amount(); + if (leftKey.what().equals(rightKey.what())) { + if (available.getOrDefault(leftKey.what(), 0L) + >= MekanismChemicalRecipeSupport.saturatingAdd(leftAmount, rightAmount)) return true; + } else if (available.getOrDefault(leftKey.what(), 0L) >= leftAmount + && available.getOrDefault(rightKey.what(), 0L) >= rightAmount) { + return true; + } + } + } + return false; + } + + private static String id(ChemicalStack stack) { + ResourceLocation id = stack.getTypeRegistryName(); + return id.getNamespace() + "_" + id.getPath().replace('/', '_'); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/ChemicalCrystallizerRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/ChemicalCrystallizerRecipeAdapter.java new file mode 100644 index 00000000..3cc9bcbe --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/ChemicalCrystallizerRecipeAdapter.java @@ -0,0 +1,85 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism; + +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import mekanism.api.chemical.ChemicalStack; +import mekanism.api.recipes.ChemicalCrystallizerRecipe; +import mekanism.common.recipe.MekanismRecipeType; +import mekanism.api.recipes.ingredients.ChemicalStackIngredient; +import mekanism.common.registries.MekanismBlocks; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Chemical -> item adapter for the Chemical Crystallizer. */ +public final class ChemicalCrystallizerRecipeAdapter implements IRecipeAdapter { + private static final long PROCESS_TICKS = 10L * 20L; + private static final long ENERGY_PER_TICK = 400L; + + @Override + public Class getRecipeClass() { + return ChemicalCrystallizerRecipe.class; + } + + @Override + public @Nullable ItemStack getMoldItem() { + return new ItemStack(MekanismBlocks.CHEMICAL_CRYSTALLIZER.getBlock()); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder != null && NativeMekanismVariants.needsExpansion(holder.value())) + return NativeMekanismVariants.expand(holder.value(), () -> convertAll(holder, level)); + if (holder == null || level == null || holder.value().getType() != MekanismRecipeType.CRYSTALLIZING.get()) { + return List.of(); + } + ChemicalCrystallizerRecipe original = holder.value(); + ChemicalStackIngredient input = original.getInput(); + if (input == null || input.hasNoMatchingInstances()) return List.of(); + + List result = new ArrayList<>(); + for (ChemicalStack chemical : input.getRepresentations()) { + GenericStack inputKey = MekanismChemicalRecipeSupport.key(chemical); + if (inputKey == null) continue; + for (ItemStack output : original.getOutputDefinition()) { + if (output.isEmpty()) continue; + ResourceLocation chemicalId = chemical.getTypeRegistryName(); + result.add(MekanismChemicalRecipeSupport.recipe( + MekanismChemicalRecipeSupport.variantId(holder.id(), "crystallizing_" + + chemicalId.getNamespace() + "_" + chemicalId.getPath()), + List.of(), List.of(), List.of(inputKey), List.of(output.copy()), List.of(), List.of(), + AdapterUtils.mekanismEnergyCost(ENERGY_PER_TICK, PROCESS_TICKS, 1L), + AdapterUtils.safeInt(PROCESS_TICKS), getMoldItem())); + } + } + return result; + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + Map mergedKeys, @Nullable ItemStack mold) { + if (level == null || mergedKeys.isEmpty() || !matchesMold(mold)) return List.of(); + RecipeManager manager = level.getRecipeManager(); + List> result = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(manager, + MekanismRecipeType.CRYSTALLIZING.get())) { + if (MekanismChemicalRecipeSupport.matchesChemical(mergedKeys, holder.value().getInput())) { + result.add(holder); + } + } + return result; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/ChemicalDissolutionRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/ChemicalDissolutionRecipeAdapter.java new file mode 100644 index 00000000..510187ab --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/ChemicalDissolutionRecipeAdapter.java @@ -0,0 +1,102 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism; + +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import mekanism.api.chemical.ChemicalStack; +import mekanism.api.recipes.ChemicalDissolutionRecipe; +import mekanism.common.recipe.MekanismRecipeType; +import mekanism.api.recipes.ingredients.ChemicalStackIngredient; +import mekanism.api.recipes.ingredients.ItemStackIngredient; +import mekanism.common.registries.MekanismBlocks; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Item + chemical -> chemical adapter for the Chemical Dissolution Chamber. */ +public final class ChemicalDissolutionRecipeAdapter implements IRecipeAdapter { + private static final long PROCESS_TICKS = 5L * 20L; + private static final long ENERGY_PER_TICK = 400L; + + @Override + public Class getRecipeClass() { + return ChemicalDissolutionRecipe.class; + } + + @Override + public @Nullable ItemStack getMoldItem() { + return new ItemStack(MekanismBlocks.CHEMICAL_DISSOLUTION_CHAMBER.getBlock()); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder != null && NativeMekanismVariants.needsExpansion(holder.value())) + return NativeMekanismVariants.expand(holder.value(), () -> convertAll(holder, level)); + if (holder == null || level == null || holder.value().getType() != MekanismRecipeType.DISSOLUTION.get()) { + return List.of(); + } + ChemicalDissolutionRecipe original = holder.value(); + ItemStackIngredient itemInput = original.getItemInput(); + ChemicalStackIngredient chemicalInput = original.getGasInput(); + if (MekanismChemicalRecipeSupport.item(itemInput) == null || chemicalInput == null + || chemicalInput.hasNoMatchingInstances()) return List.of(); + + long multiplier = MekanismChemicalRecipeSupport.perTickUsage(original) ? PROCESS_TICKS : 1L; + List result = new ArrayList<>(); + for (ChemicalStack chemical : chemicalInput.getRepresentations()) { + GenericStack inputKey = MekanismChemicalRecipeSupport.key(mekanism.common.util.ChemicalUtil.copyWithAmount(chemical, + MekanismChemicalRecipeSupport.saturatingMultiply(chemical.getAmount(), multiplier))); + if (inputKey == null) continue; + for (var boxedOutput : original.getOutputDefinition()) { + GenericStack outputKey = MekanismChemicalRecipeSupport.key(boxedOutput.getChemicalStack()); + if (outputKey == null) continue; + ResourceLocation chemicalId = chemical.getTypeRegistryName(); + result.add(MekanismChemicalRecipeSupport.recipe( + MekanismChemicalRecipeSupport.variantId(holder.id(), "dissolution_" + + chemicalId.getNamespace() + "_" + chemicalId.getPath()), + MekanismChemicalRecipeSupport.items(itemInput), List.of(), List.of(inputKey), + List.of(), List.of(), List.of(outputKey), + AdapterUtils.mekanismEnergyCost(ENERGY_PER_TICK, PROCESS_TICKS, 1L), + AdapterUtils.safeInt(PROCESS_TICKS), getMoldItem())); + } + } + return result; + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + Map mergedKeys, @Nullable ItemStack mold) { + if (level == null || mergedInputs.isEmpty() || mergedKeys.isEmpty() || !matchesMold(mold)) return List.of(); + RecipeManager manager = level.getRecipeManager(); + List> result = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(manager, + MekanismRecipeType.DISSOLUTION.get())) { + ChemicalDissolutionRecipe recipe = holder.value(); + if (!MekanismChemicalRecipeSupport.matchesItem(mergedInputs, recipe.getItemInput())) continue; + long multiplier = MekanismChemicalRecipeSupport.perTickUsage(recipe) ? PROCESS_TICKS : 1L; + boolean match = false; + for (ChemicalStack chemical : recipe.getGasInput().getRepresentations()) { + GenericStack key = MekanismChemicalRecipeSupport.key(mekanism.common.util.ChemicalUtil.copyWithAmount(chemical, + MekanismChemicalRecipeSupport.saturatingMultiply(chemical.getAmount(), multiplier))); + if (key != null && mergedKeys.getOrDefault(key.what(), 0L) >= key.amount()) { + match = true; + break; + } + } + if (match) result.add(holder); + } + return result; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/ChemicalInjectionChamberRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/ChemicalInjectionChamberRecipeAdapter.java new file mode 100644 index 00000000..31e0c3cf --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/ChemicalInjectionChamberRecipeAdapter.java @@ -0,0 +1,27 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism; + +import mekanism.api.recipes.chemical.ItemStackChemicalToItemStackRecipe; +import mekanism.common.recipe.MekanismRecipeType; +import mekanism.common.registries.MekanismBlocks; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.RecipeType; +import org.jetbrains.annotations.Nullable; + +/** Direct chemical-key adapter for the Chemical Injection Chamber. */ +public final class ChemicalInjectionChamberRecipeAdapter extends MetallurgicInfuserRecipeAdapter { + @Override + protected long getEnergyPerTick() { + return 400L; + } + + @Override + protected RecipeType> getMekanismRecipeType() { + return MekanismRecipeType.INJECTING.get(); + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return new ItemStack(MekanismBlocks.CHEMICAL_INJECTION_CHAMBER.getBlock()); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/ChemicalToChemicalRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/ChemicalToChemicalRecipeAdapter.java new file mode 100644 index 00000000..b9172949 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/ChemicalToChemicalRecipeAdapter.java @@ -0,0 +1,111 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism; + +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import mekanism.api.chemical.ChemicalStack; +import mekanism.api.recipes.chemical.ChemicalToChemicalRecipe; +import mekanism.common.recipe.MekanismRecipeType; +import mekanism.api.recipes.ingredients.ChemicalStackIngredient; +import mekanism.common.registries.MekanismBlocks; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Chemical -> chemical adapter shared by the isotope centrifuge and solar activator. */ +public final class ChemicalToChemicalRecipeAdapter implements IRecipeAdapter> { + private static final long PROCESS_TICKS = AdapterUtils.MEKANISM_BASE_TICKS_REQUIRED; + private static final long ENERGY_PER_TICK = 200L; + + private final RecipeType> recipeType; + private final ItemStack mold; + private final String name; + + private ChemicalToChemicalRecipeAdapter(RecipeType> recipeType, + ItemStack mold, String name) { + this.recipeType = recipeType; + this.mold = mold.copy(); + this.name = name; + } + + public static ChemicalToChemicalRecipeAdapter isotopicCentrifuge() { + return new ChemicalToChemicalRecipeAdapter( + MekanismRecipeType.CENTRIFUGING.get(), + new ItemStack(MekanismBlocks.ISOTOPIC_CENTRIFUGE.getBlock()), "isotopic_centrifuge"); + } + + public static ChemicalToChemicalRecipeAdapter solarNeutronActivator() { + return new ChemicalToChemicalRecipeAdapter( + MekanismRecipeType.ACTIVATING.get(), + new ItemStack(MekanismBlocks.SOLAR_NEUTRON_ACTIVATOR.getBlock()), "solar_neutron_activator"); + } + + @Override + public Class> getRecipeClass() { + return (Class) ChemicalToChemicalRecipe.class; + } + + @Override + public @Nullable ItemStack getMoldItem() { + return mold.copy(); + } + + @Override + public List convertAll(RecipeHolder> holder, Level level) { + if (holder != null && NativeMekanismVariants.needsExpansion(holder.value())) + return NativeMekanismVariants.expand(holder.value(), () -> convertAll(holder, level)); + if (holder == null || level == null || holder.value().getType() != recipeType) return List.of(); + ChemicalToChemicalRecipe original = holder.value(); + ChemicalStackIngredient input = original.getInput(); + if (input == null || input.hasNoMatchingInstances()) return List.of(); + + List result = new ArrayList<>(); + for (ChemicalStack inputStack : input.getRepresentations()) { + GenericStack inputKey = MekanismChemicalRecipeSupport.key(inputStack); + if (inputKey == null) continue; + for (ChemicalStack outputStack : original.getOutputDefinition()) { + GenericStack outputKey = MekanismChemicalRecipeSupport.key(outputStack); + if (outputKey == null) continue; + result.add(MekanismChemicalRecipeSupport.recipe( + MekanismChemicalRecipeSupport.variantId(holder.id(), name + "_" + + id(inputStack) + "_out_" + id(outputStack)), + List.of(), List.of(), List.of(inputKey), List.of(), List.of(), List.of(outputKey), + AdapterUtils.mekanismEnergyCost(ENERGY_PER_TICK, PROCESS_TICKS, 1L), + AdapterUtils.safeInt(PROCESS_TICKS), getMoldItem())); + } + } + return result; + } + + @Override + public List>> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + Map mergedKeys, @Nullable ItemStack mold) { + if (level == null || mergedKeys.isEmpty() || !matchesMold(mold)) return List.of(); + RecipeManager manager = level.getRecipeManager(); + List>> result = new ArrayList<>(); + for (RecipeHolder> holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.>getAllRecipesFor(manager, recipeType)) { + if (MekanismChemicalRecipeSupport.matchesChemical(mergedKeys, holder.value().getInput())) { + result.add(holder); + } + } + return result; + } + + private static String id(ChemicalStack stack) { + ResourceLocation id = stack.getTypeRegistryName(); + return id.getNamespace() + "_" + id.getPath().replace('/', '_'); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/CombinerRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/CombinerRecipeAdapter.java new file mode 100644 index 00000000..56a400e9 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/CombinerRecipeAdapter.java @@ -0,0 +1,148 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism; + +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import mekanism.api.recipes.CombinerRecipe; +import mekanism.common.recipe.MekanismRecipeType; +import mekanism.api.recipes.ingredients.ItemStackIngredient; +import mekanism.common.config.MekanismConfig; +import mekanism.common.registries.MekanismBlocks; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Converts Mekanism Combiner recipes to alloy-furnace recipes. */ +public final class CombinerRecipeAdapter implements IRecipeAdapter { + private static final int PROCESS_TICKS = 200; + + @Override + public Class getRecipeClass() { + return CombinerRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return new ItemStack(MekanismBlocks.COMBINER.getBlock()); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder != null && NativeMekanismVariants.needsExpansion(holder.value())) + return NativeMekanismVariants.expand(holder.value(), () -> convertAll(holder, level)); + if (holder == null || level == null || holder.value() == null) { + return List.of(); + } + + CombinerRecipe source = holder.value(); + if (source.getType() != MekanismRecipeType.COMBINING.get() + || source.isIncomplete()) { + return List.of(); + } + + List inputs = countedInputs(source); + if (inputs.isEmpty()) { + return List.of(); + } + + List outputs = source.getOutputDefinition().stream() + .filter(stack -> stack != null && !stack.isEmpty()) + .map(ItemStack::copy) + .toList(); + if (outputs.isEmpty()) { + return List.of(); + } + + AdvancedAlloyFurnaceRecipe converted = MekanismChemicalRecipeSupport.recipe( + AdapterUtils.convertedId(holder.id()), + inputs, + List.of(), + List.of(), + outputs, + List.of(), + List.of(), + AdapterUtils.mekanismEnergyCost( + MekanismConfig.usage.combiner.get().longValue(), PROCESS_TICKS, 1L), + PROCESS_TICKS, + getMoldItem()); + return List.of(converted); + } + + @Override + public List> findMatchingRecipes( + Level level, + Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || mergedInputs == null || mergedInputs.isEmpty() + || !matchesMold(mold)) { + return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + MekanismRecipeType.COMBINING.get())) { + CombinerRecipe recipe = holder.value(); + if (recipe == null || recipe.isIncomplete()) { + continue; + } + + List inputs = countedInputs(recipe); + if (inputs.isEmpty()) { + continue; + } + + Map required = new LinkedHashMap<>(); + for (CountedIngredient input : inputs) { + AdapterUtils.mergeIngredient(required, input.ingredient(), input.count()); + } + if (AdapterUtils.matchesRequired(mergedInputs, required)) { + matches.add(holder); + } + } + return matches; + } + + private static List countedInputs(CombinerRecipe recipe) { + CountedIngredient main = countedInput(recipe.getMainInput()); + CountedIngredient extra = countedInput(recipe.getExtraInput()); + if (main == null || extra == null) { + return List.of(); + } + + List inputs = new ArrayList<>(2); + addCountedInput(inputs, main); + addCountedInput(inputs, extra); + return inputs; + } + + @Nullable + private static CountedIngredient countedInput(@Nullable ItemStackIngredient input) { + return MekanismChemicalRecipeSupport.item(input); + } + + private static void addCountedInput(List inputs, CountedIngredient input) { + for (int i = 0; i < inputs.size(); i++) { + CountedIngredient existing = inputs.get(i); + if (AdapterUtils.areIngredientsEqual(existing.ingredient(), input.ingredient())) { + inputs.set(i, new CountedIngredient(existing.ingredient(), + MekanismChemicalRecipeSupport.saturatingAdd(existing.count(), input.count()))); + return; + } + } + inputs.add(input); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/CrusherRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/CrusherRecipeAdapter.java new file mode 100644 index 00000000..c775edc5 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/CrusherRecipeAdapter.java @@ -0,0 +1,133 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import mekanism.api.recipes.ItemStackToItemStackRecipe; +import mekanism.common.recipe.MekanismRecipeType; +import mekanism.api.recipes.ingredients.ItemStackIngredient; +import mekanism.common.registries.MekanismBlocks; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class CrusherRecipeAdapter implements IRecipeAdapter { + + @Override + public Class getRecipeClass() { + return ItemStackToItemStackRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return new ItemStack(MekanismBlocks.CRUSHER.getBlock()); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder != null && NativeMekanismVariants.needsExpansion(holder.value())) + return NativeMekanismVariants.expand(holder.value(), () -> convertAll(holder, level)); + List result = new ArrayList<>(); + + if (holder == null) return result; + + ItemStackToItemStackRecipe originalRecipe = holder.value(); + + if (!originalRecipe.getType().equals(MekanismRecipeType.CRUSHING.get())) { + return result; + } + + List countedIngredients = new ArrayList<>(); + addCountedIngredient(countedIngredients, originalRecipe.getInput(), 1); + + if (countedIngredients.isEmpty()) { + return result; + } + + List outputs = originalRecipe.getOutputDefinition(); + if (outputs.isEmpty()) { + return result; + } + + result.add(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + countedIngredients, + List.of(), + outputs.stream().map(ItemStack::copy).toList(), + List.of(), + AdapterUtils.mekanismEnergyCost(AdapterUtils.MEKANISM_ENRICHMENT_CHAMBER_ENERGY_PER_TICK, 1), + AdapterUtils.mekanismProcessTime(1), + Ingredient.EMPTY, + 0, + AdapterUtils.toMoldIngredient(getMoldItem()), + AlloyFurnaceMode.NORMAL + )); + return result; + } + + @Override + @Nullable + public List> findMatchingRecipes(Level level, Map mergedInputs, Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || mergedInputs.isEmpty()) { + return List.of(); + } + if (mold != null && !mold.isEmpty() && !matchesMold(mold)) { + return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + List> recipes = com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + MekanismRecipeType.CRUSHING.get() + ); + + List> matches = new java.util.ArrayList<>(); + for (RecipeHolder holder : recipes) { + ItemStackToItemStackRecipe recipe = holder.value(); + ItemStackIngredient input = recipe.getInput(); + if (input == null) continue; + + if (matchesIngredient(mergedInputs, input)) { + matches.add(holder); + } + } + return matches; + } + + @Nullable + private static CountedIngredient countedIngredient(ItemStackIngredient input, long multiplier) { + return MekanismChemicalRecipeSupport.countedItem(input, multiplier); + } + + private static boolean matchesIngredient(Map mergedInputs, ItemStackIngredient required) { + if (required == null) { + return false; + } + return MekanismChemicalRecipeSupport.matchesItem(mergedInputs,required); + } + + private static void addCountedIngredient(List countedIngredients, ItemStackIngredient input, long multiplier) { + CountedIngredient counted = countedIngredient(input, multiplier); + if (counted == null) { + return; + } + for (int i = 0; i < countedIngredients.size(); i++) { + CountedIngredient existing = countedIngredients.get(i); + if (AdapterUtils.areIngredientsEqual(existing.ingredient(), counted.ingredient())) { + countedIngredients.set(i, new CountedIngredient(existing.ingredient(), existing.count() + counted.count())); + return; + } + } + countedIngredients.add(counted); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/ElectrolysisRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/ElectrolysisRecipeAdapter.java new file mode 100644 index 00000000..0b96389c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/ElectrolysisRecipeAdapter.java @@ -0,0 +1,80 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism; + +import appeng.api.stacks.GenericStack; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import mekanism.api.recipes.ElectrolysisRecipe; +import mekanism.common.recipe.MekanismRecipeType; +import mekanism.api.recipes.ingredients.FluidStackIngredient; +import mekanism.common.registries.MekanismBlocks; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Fluid -> two chemical outputs adapter for the Electrolytic Separator. */ +public final class ElectrolysisRecipeAdapter implements IRecipeAdapter { + private static final long PROCESS_TICKS = AdapterUtils.MEKANISM_BASE_TICKS_REQUIRED; + private static final long ENERGY_PER_TICK = 200L; + + @Override + public Class getRecipeClass() { + return ElectrolysisRecipe.class; + } + + @Override + public @Nullable ItemStack getMoldItem() { + return new ItemStack(MekanismBlocks.ELECTROLYTIC_SEPARATOR.getBlock()); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder != null && NativeMekanismVariants.needsExpansion(holder.value())) + return NativeMekanismVariants.expand(holder.value(), () -> convertAll(holder, level)); + if (holder == null || level == null || holder.value().getType() != MekanismRecipeType.SEPARATING.get()) { + return List.of(); + } + ElectrolysisRecipe original = holder.value(); + FluidStackIngredient input = original.getInput(); + if (input == null || input.hasNoMatchingInstances()) return List.of(); + + List result = new ArrayList<>(); + List fluidInputs = + MekanismChemicalRecipeSupport.fluidIngredients(input); + for (ElectrolysisRecipe.ElectrolysisRecipeOutput output : original.getOutputDefinition()) { + GenericStack left = MekanismChemicalRecipeSupport.key(output.left()); + GenericStack right = MekanismChemicalRecipeSupport.key(output.right()); + if (left == null || right == null) continue; + result.add(MekanismChemicalRecipeSupport.recipe( + MekanismChemicalRecipeSupport.variantId(holder.id(), "separating"), + List.of(), fluidInputs, List.of(), List.of(), List.of(), List.of(left, right), + MekanismChemicalRecipeSupport.energyCost(ENERGY_PER_TICK,PROCESS_TICKS,original.getEnergyMultiplier()), + AdapterUtils.safeInt(PROCESS_TICKS), getMoldItem())); + } + return result; + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + Map mergedKeys, @Nullable ItemStack mold) { + if (level == null || mergedFluids.isEmpty() || !matchesMold(mold)) return List.of(); + RecipeManager manager = level.getRecipeManager(); + List> result = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(manager, + MekanismRecipeType.SEPARATING.get())) { + if (MekanismChemicalRecipeSupport.matchesFluid(mergedFluids, holder.value().getInput())) { + result.add(holder); + } + } + return result; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/EnrichmentChamberRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/EnrichmentChamberRecipeAdapter.java new file mode 100644 index 00000000..1e1d6e16 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/EnrichmentChamberRecipeAdapter.java @@ -0,0 +1,140 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import mekanism.api.recipes.ItemStackToItemStackRecipe; +import mekanism.common.recipe.MekanismRecipeType; +import mekanism.api.recipes.ingredients.ItemStackIngredient; +import mekanism.common.registries.MekanismBlocks; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Mekanism 富集仓配方适配器 + */ +public class EnrichmentChamberRecipeAdapter implements IRecipeAdapter { + + @Override + public Class getRecipeClass() { + return ItemStackToItemStackRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return new ItemStack(MekanismBlocks.ENRICHMENT_CHAMBER.getBlock()); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder != null && NativeMekanismVariants.needsExpansion(holder.value())) + return NativeMekanismVariants.expand(holder.value(), () -> convertAll(holder, level)); + List result = new ArrayList<>(); + + if (holder == null) return result; + + ItemStackToItemStackRecipe originalRecipe = holder.value(); + + if (!originalRecipe.getType().equals(MekanismRecipeType.ENRICHING.get())) { + return result; + } + + List countedIngredients = new ArrayList<>(); + var input = originalRecipe.getInput(); + addCountedIngredient(countedIngredients, input, 1); + + if (countedIngredients.isEmpty()) { + return result; + } + + List outputs = originalRecipe.getOutputDefinition(); + if (outputs.isEmpty()) { + return result; + } + + result.add(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + countedIngredients, + List.of(), + outputs, + List.of(), + AdapterUtils.mekanismEnrichmentChamberEnergyCost(1), + AdapterUtils.mekanismEnrichmentChamberProcessTime(1), + Ingredient.EMPTY, + 0, + AdapterUtils.toMoldIngredient(getMoldItem()), + AlloyFurnaceMode.NORMAL + )); + return result; + } + + @Override + @Nullable + public List> findMatchingRecipes(Level level, Map mergedInputs, Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || mergedInputs.isEmpty()) { + return List.of(); + } + if (mold != null && !mold.isEmpty() && !matchesMold(mold)) { + return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + List> recipes = com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + MekanismRecipeType.ENRICHING.get() + ); + + List> matches = new java.util.ArrayList<>(); + for (RecipeHolder holder : recipes) { + ItemStackToItemStackRecipe recipe = holder.value(); + + var input = recipe.getInput(); + if (input == null) continue; + + if (matchesIngredient(mergedInputs, input)) { + matches.add(holder); + } + } + return matches; + } + + // ========== Helper Methods ========== + + @Nullable + private static CountedIngredient countedIngredient(ItemStackIngredient input, long multiplier) { + return MekanismChemicalRecipeSupport.countedItem(input, multiplier); + } + + private static boolean matchesIngredient(Map mergedInputs, ItemStackIngredient required) { + if (required == null) { + return false; + } + return MekanismChemicalRecipeSupport.matchesItem(mergedInputs,required); + } + + private static void addCountedIngredient(List countedIngredients, ItemStackIngredient input, long multiplier) { + CountedIngredient counted = countedIngredient(input, multiplier); + if (counted == null) { + return; + } + for (int i = 0; i < countedIngredients.size(); i++) { + CountedIngredient existing = countedIngredients.get(i); + if (AdapterUtils.areIngredientsEqual(existing.ingredient(), counted.ingredient())) { + countedIngredients.set(i, new CountedIngredient(existing.ingredient(), existing.count() + counted.count())); + return; + } + } + countedIngredients.add(counted); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/FluidChemicalRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/FluidChemicalRecipeAdapter.java new file mode 100644 index 00000000..d92bfdd5 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/FluidChemicalRecipeAdapter.java @@ -0,0 +1,98 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism; + +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import mekanism.api.chemical.ChemicalStack; +import mekanism.api.recipes.chemical.FluidChemicalToChemicalRecipe; +import mekanism.common.recipe.MekanismRecipeType; +import mekanism.api.recipes.ingredients.ChemicalStackIngredient; +import mekanism.api.recipes.ingredients.FluidStackIngredient; +import mekanism.common.registries.MekanismBlocks; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Fluid + chemical -> chemical adapter for the Chemical Washer. */ +public final class FluidChemicalRecipeAdapter implements IRecipeAdapter> { + private static final long PROCESS_TICKS = AdapterUtils.MEKANISM_BASE_TICKS_REQUIRED; + private static final long ENERGY_PER_TICK = 200L; + + @Override + public Class> getRecipeClass() { + return (Class) FluidChemicalToChemicalRecipe.class; + } + + @Override + public @Nullable ItemStack getMoldItem() { + return new ItemStack(MekanismBlocks.CHEMICAL_WASHER.getBlock()); + } + + @Override + public List convertAll(RecipeHolder> holder, + Level level) { + if (holder != null && NativeMekanismVariants.needsExpansion(holder.value())) + return NativeMekanismVariants.expand(holder.value(), () -> convertAll(holder, level)); + if (holder == null || level == null || holder.value().getType() != MekanismRecipeType.WASHING.get()) { + return List.of(); + } + FluidChemicalToChemicalRecipe original = holder.value(); + FluidStackIngredient fluidInput = original.getFluidInput(); + ChemicalStackIngredient chemicalInput = original.getChemicalInput(); + if (fluidInput == null || chemicalInput == null || fluidInput.hasNoMatchingInstances() + || chemicalInput.hasNoMatchingInstances()) return List.of(); + + List result = new ArrayList<>(); + List fluidInputs = + MekanismChemicalRecipeSupport.fluidIngredients(fluidInput); + for (ChemicalStack chemical : chemicalInput.getRepresentations()) { + GenericStack chemicalKey = MekanismChemicalRecipeSupport.key(chemical); + if (chemicalKey == null) continue; + for (ChemicalStack output : original.getOutputDefinition()) { + GenericStack outputKey = MekanismChemicalRecipeSupport.key(output); + if (outputKey == null) continue; + result.add(MekanismChemicalRecipeSupport.recipe( + MekanismChemicalRecipeSupport.variantId(holder.id(), "washer_" + id(chemical) + + "_out_" + id(output)), List.of(), fluidInputs, List.of(chemicalKey), + List.of(), List.of(), List.of(outputKey), + AdapterUtils.mekanismEnergyCost(ENERGY_PER_TICK, PROCESS_TICKS, 1L), + AdapterUtils.safeInt(PROCESS_TICKS), getMoldItem())); + } + } + return result; + } + + @Override + public List>> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + Map mergedKeys, @Nullable ItemStack mold) { + if (level == null || mergedFluids.isEmpty() || mergedKeys.isEmpty() || !matchesMold(mold)) return List.of(); + RecipeManager manager = level.getRecipeManager(); + List>> result = new ArrayList<>(); + for (RecipeHolder> holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.>getAllRecipesFor(manager, + MekanismRecipeType.WASHING.get())) { + FluidChemicalToChemicalRecipe recipe = holder.value(); + if (MekanismChemicalRecipeSupport.matchesFluid(mergedFluids, recipe.getFluidInput()) + && MekanismChemicalRecipeSupport.matchesChemical(mergedKeys, recipe.getChemicalInput())) { + result.add(holder); + } + } + return result; + } + + private static String id(ChemicalStack stack) { + ResourceLocation id = stack.getTypeRegistryName(); + return id.getNamespace() + "_" + id.getPath().replace('/', '_'); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/FluidToFluidRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/FluidToFluidRecipeAdapter.java new file mode 100644 index 00000000..33e3d824 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/FluidToFluidRecipeAdapter.java @@ -0,0 +1,81 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism; + +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import mekanism.api.recipes.FluidToFluidRecipe; +import mekanism.common.recipe.MekanismRecipeType; +import mekanism.api.recipes.ingredients.FluidStackIngredient; +import mekanism.common.registries.MekanismBlocks; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Fluid -> fluid adapter for the Thermal Evaporation Controller. */ +public final class FluidToFluidRecipeAdapter implements IRecipeAdapter { + private static final long PROCESS_TICKS = AdapterUtils.MEKANISM_BASE_TICKS_REQUIRED; + private static final long ENERGY_PER_TICK = 50L; + + @Override + public Class getRecipeClass() { + return FluidToFluidRecipe.class; + } + + @Override + public @Nullable ItemStack getMoldItem() { + return new ItemStack(MekanismBlocks.THERMAL_EVAPORATION_CONTROLLER.getBlock()); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder != null && NativeMekanismVariants.needsExpansion(holder.value())) + return NativeMekanismVariants.expand(holder.value(), () -> convertAll(holder, level)); + if (holder == null || level == null || holder.value().getType() != MekanismRecipeType.EVAPORATING.get()) { + return List.of(); + } + FluidStackIngredient input = holder.value().getInput(); + if (input == null || input.hasNoMatchingInstances()) return List.of(); + + List result = new ArrayList<>(); + List fluidInputs = + MekanismChemicalRecipeSupport.fluidIngredients(input); + for (FluidStack output : holder.value().getOutputDefinition()) { + if (output.isEmpty()) continue; + result.add(MekanismChemicalRecipeSupport.recipe( + MekanismChemicalRecipeSupport.variantId(holder.id(), "evaporating_out_" + fluidId(output)), + List.of(), fluidInputs, List.of(), List.of(), List.of(output.copy()), List.of(), + AdapterUtils.mekanismEnergyCost(ENERGY_PER_TICK, PROCESS_TICKS, 1L), + AdapterUtils.safeInt(PROCESS_TICKS), getMoldItem())); + } + return result; + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + Map mergedKeys, @Nullable ItemStack mold) { + if (level == null || mergedFluids.isEmpty() || !matchesMold(mold)) return List.of(); + RecipeManager manager = level.getRecipeManager(); + List> result = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(manager, + MekanismRecipeType.EVAPORATING.get())) { + if (MekanismChemicalRecipeSupport.matchesFluid(mergedFluids, holder.value().getInput())) { + result.add(holder); + } + } + return result; + } + + private static String fluidId(FluidStack stack) { + var id = net.minecraft.core.registries.BuiltInRegistries.FLUID.getKey(stack.getFluid()); + return id.getNamespace() + "_" + id.getPath().replace('/', '_'); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/HeavyWaterRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/HeavyWaterRecipeAdapter.java new file mode 100644 index 00000000..c3893777 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/HeavyWaterRecipeAdapter.java @@ -0,0 +1,43 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism; + +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import mekanism.common.registries.MekanismFluids; +import mekanism.common.registries.MekanismItems; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** Synthetic water -> heavy water recipe using Mekanism's filter upgrade as a mold. */ +public final class HeavyWaterRecipeAdapter extends MekanismSyntheticRecipeAdapter { + private static final ResourceLocation RECIPE_ID = new ResourceLocation( + "useless_mod", "mekanism/heavy_water_filter_upgrade"); + private static final int PROCESS_TICKS = 200; + private static final long ENERGY = 10_000L; + + @Override + public @Nullable ItemStack getMoldItem() { + return new ItemStack(MekanismItems.FILTER_UPGRADE.get()); + } + + @Override + protected List> createGeneratedRecipes(Level level) { + AdvancedAlloyFurnaceRecipe converted = MekanismChemicalRecipeSupport.recipe( + RECIPE_ID, + List.of(), + List.of(new FluidStack(Fluids.WATER, 1)), + List.of(), + List.of(), + List.of(new FluidStack(MekanismFluids.HEAVY_WATER.getStillFluid(), 1)), + List.of(), + ENERGY, + PROCESS_TICKS, + getMoldItem()); + return List.of(MekanismChemicalRecipeSupport.syntheticHolder(RECIPE_ID, converted)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/ItemToChemicalRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/ItemToChemicalRecipeAdapter.java new file mode 100644 index 00000000..6b0fc0f3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/ItemToChemicalRecipeAdapter.java @@ -0,0 +1,115 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism; + +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import mekanism.api.chemical.ChemicalStack; +import mekanism.api.recipes.chemical.ItemStackToChemicalRecipe; +import mekanism.common.recipe.MekanismRecipeType; +import mekanism.api.recipes.ingredients.ItemStackIngredient; +import mekanism.common.registries.MekanismBlocks; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Independent item -> chemical adapters for Mekanism's two conversion types. */ +public final class ItemToChemicalRecipeAdapter implements IRecipeAdapter> { + private static final long PROCESS_TICKS = 100L; + private static final long ENERGY_PER_TICK = 200L; + + private final RecipeType> recipeType; + private final long processTicks; + private final String name; + private final ItemStack mold; + + public ItemToChemicalRecipeAdapter(RecipeType> recipeType, + long processTicks, String name, ItemStack mold) { + this.recipeType = recipeType; + this.processTicks = processTicks; + this.name = name; + this.mold = mold == null ? ItemStack.EMPTY : mold.copy(); + } + + public static ItemToChemicalRecipeAdapter chemicalConversion() { + return new ItemToChemicalRecipeAdapter( + MekanismRecipeType.GAS_CONVERSION.get(), + PROCESS_TICKS, "chemical_conversion", + new ItemStack(MekanismBlocks.CHEMICAL_OXIDIZER.getBlock())); + } + + public static ItemToChemicalRecipeAdapter oxidizing() { + return new ItemToChemicalRecipeAdapter( + MekanismRecipeType.OXIDIZING.get(), + PROCESS_TICKS, "oxidizing", + new ItemStack(MekanismBlocks.CHEMICAL_OXIDIZER.getBlock())); + } + + public static ItemToChemicalRecipeAdapter pigmentExtracting() { + return new ItemToChemicalRecipeAdapter( + MekanismRecipeType.PIGMENT_EXTRACTING.get(), + PROCESS_TICKS, "pigment_extracting", + new ItemStack(MekanismBlocks.PIGMENT_EXTRACTOR.getBlock())); + } + + @Override + public Class> getRecipeClass() { + return (Class) ItemStackToChemicalRecipe.class; + } + + @Override + public @Nullable ItemStack getMoldItem() { + return mold.isEmpty() ? null : mold.copy(); + } + + @Override + public List convertAll(RecipeHolder> holder, Level level) { + if (holder != null && NativeMekanismVariants.needsExpansion(holder.value())) + return NativeMekanismVariants.expand(holder.value(), () -> convertAll(holder, level)); + if (holder == null || level == null || holder.value().getType() != recipeType) return List.of(); + ItemStackToChemicalRecipe original = holder.value(); + ItemStackIngredient itemInput = original.getInput(); + if (MekanismChemicalRecipeSupport.item(itemInput) == null) return List.of(); + + List result = new ArrayList<>(); + for (ChemicalStack output : original.getOutputDefinition()) { + GenericStack key = MekanismChemicalRecipeSupport.key(output); + if (key == null) continue; + result.add(MekanismChemicalRecipeSupport.recipe( + MekanismChemicalRecipeSupport.variantId(holder.id(), name + "_" + id(output)), + MekanismChemicalRecipeSupport.items(itemInput), List.of(), List.of(), List.of(), List.of(), + List.of(key), AdapterUtils.mekanismEnergyCost(ENERGY_PER_TICK, processTicks, 1L), + AdapterUtils.safeInt(processTicks), getMoldItem())); + } + return result; + } + + @Override + public List>> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + Map mergedKeys, @Nullable ItemStack mold) { + if (level == null || mergedInputs.isEmpty() || !matchesMold(mold)) return List.of(); + RecipeManager manager = level.getRecipeManager(); + List>> result = new ArrayList<>(); + for (RecipeHolder> holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.>getAllRecipesFor(manager, recipeType)) { + ItemStackToChemicalRecipe recipe = holder.value(); + if (MekanismChemicalRecipeSupport.matchesItem(mergedInputs, recipe.getInput())) result.add(holder); + } + return result; + } + + private static String id(ChemicalStack stack) { + var id = stack.getTypeRegistryName(); + return id.getNamespace() + "_" + id.getPath().replace('/', '_'); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/MekanismChemicalRecipeSupport.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/MekanismChemicalRecipeSupport.java new file mode 100644 index 00000000..008f4ab7 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/MekanismChemicalRecipeSupport.java @@ -0,0 +1,279 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism; + +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.compat.mekanism.MekanismChemicalStackView; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalKeyProviders; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.FluidIngredientAllocator; +import mekanism.api.chemical.ChemicalStack; +import mekanism.api.recipes.ingredients.ChemicalStackIngredient; +import mekanism.api.recipes.ingredients.FluidStackIngredient; +import mekanism.api.recipes.ingredients.ItemStackIngredient; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.LinkedHashMap; + +/** Shared conversion and matching helpers for AppMek-backed Mekanism recipes. */ +public final class MekanismChemicalRecipeSupport { + public static final long NON_ITEM_QUANTITY_MULTIPLIER = 1_000L; + + private MekanismChemicalRecipeSupport() { + } + + @Nullable + public static GenericStack key(ChemicalStack stack) { + return stack == null || stack.isEmpty() ? null : ChemicalKeyProviders.get().toGenericStack(new MekanismChemicalStackView(stack)); + } + + static List keys(ChemicalStackIngredient ingredient) { + List result = new ArrayList<>(); + if (ingredient == null || ingredient.hasNoMatchingInstances()) return result; + for (ChemicalStack representation : ingredient.getRepresentations()) { + GenericStack key = key(representation); + if (key != null && key.amount() > 0L && result.stream().noneMatch(existing -> existing.what().equals(key.what()))) { + result.add(key); + } + } + return result; + } + + public static long energyCost(long perTick,long ticks,mekanism.api.math.FloatingLong multiplier) { + var cost=new java.math.BigDecimal(multiplier.toString()).multiply(java.math.BigDecimal.valueOf(perTick)) + .multiply(java.math.BigDecimal.valueOf(ticks)).setScale(0,java.math.RoundingMode.CEILING); + if(cost.signum()<0 || cost.compareTo(java.math.BigDecimal.valueOf(Long.MAX_VALUE))>0) + throw new IllegalArgumentException("Mekanism energy cost overflow"); + return cost.longValueExact(); + } + public static boolean perTickUsage(Object recipe) { + // 10.4's gas-consuming item machines and dissolution consume per tick; + // metallurgic infusion and painting consume their ingredient once. + return recipe instanceof mekanism.api.recipes.ItemStackGasToItemStackRecipe + || recipe instanceof mekanism.api.recipes.ChemicalDissolutionRecipe; + } + + static Ingredient itemIngredient(ItemStackIngredient ingredient) { + var item=item(ingredient); return item == null ? Ingredient.EMPTY : item.ingredient(); + } + static CountedIngredient item(ItemStackIngredient ingredient) { return countedItem(ingredient,1L); } + static CountedIngredient countedItem(ItemStackIngredient ingredient,long multiplier) { + if(ingredient == null || multiplier<=0) return null; + var selected=NativeMekanismVariants.selected(ingredient); + if(selected instanceof CountedIngredient item) return new CountedIngredient(item.ingredient(),Math.multiplyExact(item.count(),multiplier)); + return NativeMekanismIngredients.item(ingredient.serialize(),multiplier); + } + + static List items(ItemStackIngredient ingredient) { + CountedIngredient converted = item(ingredient); + return converted == null ? List.of() : List.of(converted); + } + + static boolean matchesItem(Map inputs, ItemStackIngredient required) { + if(required==null) return false; + for(var counted:NativeMekanismIngredients.itemVariants(required.serialize(),1L)) { + Map requirements=new LinkedHashMap<>(); + requirements.put(counted.ingredient(),counted.count()); + if(AdapterUtils.matchesRequired(inputs,requirements)) return true; + } + return false; + } + + static boolean matchesChemical(Map inputs, ChemicalStackIngredient required) { + for (GenericStack key : keys(required)) { + if (inputs.getOrDefault(key.what(), 0L) >= key.amount()) return true; + } + return false; + } + + static boolean matchesFluid(Map inputs, FluidStackIngredient required) { + if (required == null || required.hasNoMatchingInstances()) return false; + return NativeMekanismIngredients.fluidVariants(required.serialize()).stream() + .anyMatch(variant->FluidIngredientAllocator.matches(List.of(variant),inputs,1L)); + } + + /** Keeps Mekanism's fluid tag/compound/component ingredient intact. */ + public static List fluidIngredients(FluidStackIngredient ingredient) { + if(ingredient == null) return List.of(); + var selected=NativeMekanismVariants.selected(ingredient); + SizedFluidIngredient converted=selected instanceof SizedFluidIngredient fluid ? fluid + : NativeMekanismIngredients.fluid(ingredient.serialize()); + return converted == null ? List.of() : List.of(converted); + } + + private static List scaleFluidIngredients( + List ingredients, long batch) { + if (ingredients.isEmpty()) return ingredients; + List scaled = new ArrayList<>(ingredients.size()); + for (SizedFluidIngredient ingredient : ingredients) { + if (ingredient == null || ingredient.ingredient() == null || ingredient.amount() <= 0) { + scaled.add(ingredient); + continue; + } + scaled.add(new SizedFluidIngredient(ingredient.ingredient(), + Math.toIntExact(Math.multiplyExact(ingredient.amount(),batch)))); + } + return scaled; + } + + public static List fluidRepresentations(FluidStackIngredient ingredient) { + if (ingredient == null || ingredient.hasNoMatchingInstances()) return List.of(); + List result = new ArrayList<>(); + for (FluidStack representation : ingredient.getRepresentations()) { + FluidStack copy = representation.copy(); + + result.add(copy); + } + return result; + } + + public static ResourceLocation variantId(ResourceLocation originalId, String suffix) { + String clean = suffix == null || suffix.isBlank() ? "converted" : suffix; + return new ResourceLocation(originalId.getNamespace(), + originalId.getPath() + "_" + clean.replace('/', '_')); + } + + public static AdvancedAlloyFurnaceRecipe recipe(ResourceLocation id, List items, + List fluids, List chemicalInputs, + List outputs, List outputFluids, + List chemicalOutputs, long energy, int processTime, + @Nullable ItemStack mold) { + List safeItems = items == null ? List.of() : items; + List safeFluids = toSizedFluidIngredients(fluids); + List safeChemicalInputs = chemicalInputs == null ? List.of() : chemicalInputs; + List safeOutputs = outputs == null ? List.of() : outputs; + List safeOutputFluids = outputFluids == null ? List.of() : outputFluids; + List safeChemicalOutputs = chemicalOutputs == null ? List.of() : chemicalOutputs; + + // Mekanism's non-item conversion recipes use the same machine operation for a much + // larger resource batch. Keep item-bearing conversions unchanged because their item + // quantities are part of the original recipe's material balance. + boolean itemFree = safeItems.isEmpty() && safeOutputs.isEmpty(); + if (itemFree) { + // Use one representable batch for every material: independent saturation + // would consume a truncated input while retaining a full-size output. + long batch=NON_ITEM_QUANTITY_MULTIPLIER; + for(var fluid:safeFluids) batch=Math.min(batch,Integer.MAX_VALUE/(long)fluid.amount()); + for(var fluid:safeOutputFluids) { + if(fluid==null || fluid.isEmpty()) throw new IllegalArgumentException("Empty required Mekanism fluid output"); + batch=Math.min(batch,Integer.MAX_VALUE/(long)fluid.getAmount()); + } + for(var stack:safeChemicalInputs) batch=boundedBatch(batch,stack); + for(var stack:safeChemicalOutputs) batch=boundedBatch(batch,stack); + if(batch<1) throw new IllegalArgumentException("Unrepresentable Mekanism batch"); + safeFluids = scaleFluidIngredients(safeFluids,batch); + safeChemicalInputs = scaleGenericStacks(safeChemicalInputs,batch); + safeOutputFluids = scaleOutputFluidStacks(safeOutputFluids,batch); + safeChemicalOutputs = scaleGenericStacks(safeChemicalOutputs,batch); + } + + return new AdvancedAlloyFurnaceRecipe( + id, safeItems, safeFluids, safeChemicalInputs, safeOutputs, safeOutputFluids, safeChemicalOutputs, + energy, Math.max(1, processTime), Ingredient.EMPTY, 0, + AdapterUtils.toMoldIngredient(mold), AlloyFurnaceMode.NORMAL); + } + + private static List scaleGenericStacks(List stacks,long batch) { + if (stacks.isEmpty()) return stacks; + List scaled = new ArrayList<>(stacks.size()); + for (GenericStack stack : stacks) { + if (stack == null || stack.what() == null) { + scaled.add(stack); + continue; + } + scaled.add(new GenericStack(stack.what(), + Math.multiplyExact(stack.amount(),batch))); + } + return scaled; + } + + private static List toSizedFluidIngredients(List fluids) { + if (fluids == null || fluids.isEmpty()) return List.of(); + List result = new ArrayList<>(fluids.size()); + for (Object value : fluids) { + if (value instanceof SizedFluidIngredient ingredient) { + if (ingredient.ingredient() != null && !ingredient.ingredient().isEmpty() + && ingredient.amount() > 0) { + result.add(ingredient); + } else throw new IllegalArgumentException("Invalid required Mekanism fluid input"); + } else if (value instanceof FluidStack stack) { + SizedFluidIngredient converted = AdapterUtils.toSizedFluidIngredient(stack); + if (converted == null) throw new IllegalArgumentException("Empty required Mekanism fluid input"); + result.add(converted); + } else throw new IllegalArgumentException("Unknown Mekanism fluid input"); + } + return List.copyOf(result); + } + + private static List scaleOutputFluidStacks(List stacks,long batch) { + if (stacks.isEmpty()) return stacks; + List scaled = new ArrayList<>(stacks.size()); + for (FluidStack stack : stacks) { + if (stack == null || stack.isEmpty()) { + scaled.add(stack); + continue; + } + FluidStack copy = stack.copy(); + copy.setAmount(Math.toIntExact(Math.multiplyExact(copy.getAmount(),batch))); + scaled.add(copy); + } + return scaled; + } + + private static long boundedBatch(long batch,GenericStack stack) { + if(stack==null || stack.what()==null || stack.amount()<=0) + throw new IllegalArgumentException("Invalid required Mekanism chemical"); + return Math.min(batch,Long.MAX_VALUE/stack.amount()); + } + + public static long saturatingAdd(long left, long right) { + return Math.addExact(left,right); + } + + public static long saturatingMultiply(long left, long right) { + if(left<0 || right<0) throw new IllegalArgumentException("Negative Mekanism quantity"); + return Math.multiplyExact(left,right); + } + + public static RecipeHolder syntheticHolder( + ResourceLocation id, AdvancedAlloyFurnaceRecipe recipe) { + return new RecipeHolder<>(id, new MekanismSyntheticRecipe(recipe)); + } + + static boolean matchesConvertedRecipe(AdvancedAlloyFurnaceRecipe recipe, + Map mergedInputs, + Map mergedFluids, + Map mergedKeys) { + if (recipe == null) return false; + + Map requiredItems = new LinkedHashMap<>(); + for (CountedIngredient input : recipe.inputs()) { + AdapterUtils.mergeIngredient(requiredItems, input.ingredient(), input.count()); + } + if (!AdapterUtils.matchesRequired(mergedInputs, requiredItems)) return false; + + if (!FluidIngredientAllocator.matchesLong(recipe.inputFluids(), mergedFluids, 1L)) return false; + + Map requiredKeys = new LinkedHashMap<>(); + for (GenericStack required : recipe.keyInputs()) { + if (required == null || required.what() == null || required.amount() <= 0L) continue; + requiredKeys.merge(required.what(), required.amount(), MekanismChemicalRecipeSupport::saturatingAdd); + } + for (Map.Entry required : requiredKeys.entrySet()) { + if (mergedKeys.getOrDefault(required.getKey(), 0L) < required.getValue()) return false; + } + return true; + } + +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/MekanismSyntheticRecipe.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/MekanismSyntheticRecipe.java new file mode 100644 index 00000000..5435015e --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/MekanismSyntheticRecipe.java @@ -0,0 +1,55 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism; + +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.Container; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; + +/** Recipe holder payload for Mekanism data that is generated outside RecipeManager. */ +public final class MekanismSyntheticRecipe implements Recipe { + private final AdvancedAlloyFurnaceRecipe convertedRecipe; + + public MekanismSyntheticRecipe(AdvancedAlloyFurnaceRecipe convertedRecipe) { + this.convertedRecipe = convertedRecipe; + } + + public AdvancedAlloyFurnaceRecipe convertedRecipe() { + return convertedRecipe; + } + + @Override + public net.minecraft.resources.ResourceLocation getId() { return convertedRecipe.id(); } + + @Override + public boolean matches(Container input, Level level) { + return this.convertedRecipe != null && this.convertedRecipe.matches(input, level); + } + + @Override + public ItemStack assemble(Container input, net.minecraft.core.RegistryAccess registries) { + return this.convertedRecipe == null ? ItemStack.EMPTY : this.convertedRecipe.assemble(input, registries); + } + + @Override + public boolean canCraftInDimensions(int width, int height) { + return this.convertedRecipe != null && this.convertedRecipe.canCraftInDimensions(width, height); + } + + @Override + public ItemStack getResultItem(net.minecraft.core.RegistryAccess registries) { + return this.convertedRecipe == null ? ItemStack.EMPTY : this.convertedRecipe.getResultItem(registries); + } + + @Override + public RecipeSerializer getSerializer() { + return this.convertedRecipe == null ? null : this.convertedRecipe.getSerializer(); + } + + @Override + public RecipeType getType() { + return this.convertedRecipe == null ? null : this.convertedRecipe.getType(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/MekanismSyntheticRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/MekanismSyntheticRecipeAdapter.java new file mode 100644 index 00000000..ee2fd8e3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/MekanismSyntheticRecipeAdapter.java @@ -0,0 +1,56 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism; + +import appeng.api.stacks.AEKey; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Common adapter behavior for Mekanism's recipe-viewer-only recipe sources. */ +public abstract class MekanismSyntheticRecipeAdapter implements IRecipeAdapter { + protected abstract List> createGeneratedRecipes(Level level); + + @Override + public final Class getRecipeClass() { + return MekanismSyntheticRecipe.class; + } + + @Override + public final List> getGeneratedRecipes(Level level) { + return level == null ? List.of() : createGeneratedRecipes(level); + } + + @Override + public final List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null || holder.value().convertedRecipe() == null) { + return List.of(); + } + return List.of(holder.value().convertedRecipe()); + } + + @Override + public final List> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + Map mergedKeys, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) return List.of(); + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : getGeneratedRecipes(level)) { + AdvancedAlloyFurnaceRecipe recipe = holder.value().convertedRecipe(); + if (MekanismChemicalRecipeSupport.matchesConvertedRecipe( + recipe, mergedInputs, mergedFluids, mergedKeys)) { + matches.add(holder); + } + } + return matches; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/MetallurgicInfuserRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/MetallurgicInfuserRecipeAdapter.java new file mode 100644 index 00000000..ff5d1d70 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/MetallurgicInfuserRecipeAdapter.java @@ -0,0 +1,123 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism; + +import appeng.api.stacks.GenericStack; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import mekanism.api.chemical.ChemicalStack; +import mekanism.api.recipes.chemical.ItemStackChemicalToItemStackRecipe; +import mekanism.common.recipe.MekanismRecipeType; +import mekanism.api.recipes.ingredients.ChemicalStackIngredient; +import mekanism.common.registries.MekanismBlocks; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Metallurgic Infuser and Osmium Compressor recipes to direct AE chemical keys. */ +public class MetallurgicInfuserRecipeAdapter implements IRecipeAdapter> { + protected static final long PROCESS_TICKS = AdapterUtils.MEKANISM_METALLURGIC_INFUSER_TICKS_REQUIRED; + protected static final long ENERGY_PER_TICK = AdapterUtils.MEKANISM_METALLURGIC_INFUSER_ENERGY_PER_TICK; + + @Override + public Class> getRecipeClass() { + return (Class) ItemStackChemicalToItemStackRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return new ItemStack(MekanismBlocks.METALLURGIC_INFUSER.getBlock()); + } + + protected RecipeType> getMekanismRecipeType() { + return MekanismRecipeType.METALLURGIC_INFUSING.get(); + } + + protected long getEnergyPerTick() { + return ENERGY_PER_TICK; + } + + @Override + public List convertAll(RecipeHolder> holder, + Level level) { + if (holder != null && NativeMekanismVariants.needsExpansion(holder.value())) + return NativeMekanismVariants.expand(holder.value(), () -> convertAll(holder, level)); + if (holder == null || level == null) return List.of(); + ItemStackChemicalToItemStackRecipe original = holder.value(); + if (original == null || original.getType() != getMekanismRecipeType()) return List.of(); + + var itemInput = original.getItemInput(); + ChemicalStackIngredient chemicalInput = original.getChemicalInput(); + if (MekanismChemicalRecipeSupport.item(itemInput) == null + || chemicalInput == null || chemicalInput.hasNoMatchingInstances() + || original.getOutputDefinition().isEmpty()) { + return List.of(); + } + + long chemicalAmountMultiplier = MekanismChemicalRecipeSupport.perTickUsage(original) ? PROCESS_TICKS : 1L; + List recipes = new ArrayList<>(); + for (ChemicalStack representation : chemicalInput.getRepresentations()) { + GenericStack key = MekanismChemicalRecipeSupport.key( + mekanism.common.util.ChemicalUtil.copyWithAmount(representation,MekanismChemicalRecipeSupport.saturatingMultiply( + representation.getAmount(), chemicalAmountMultiplier))); + if (key == null || key.amount() <= 0L) continue; + + ResourceLocation chemicalId = representation.getTypeRegistryName(); + recipes.add(MekanismChemicalRecipeSupport.recipe( + MekanismChemicalRecipeSupport.variantId(holder.id(), "chemical_" + + chemicalId.getNamespace() + "_" + chemicalId.getPath()), + MekanismChemicalRecipeSupport.items(itemInput), List.of(), List.of(key), + copyItems(original.getOutputDefinition()), List.of(), List.of(), + AdapterUtils.mekanismEnergyCost(getEnergyPerTick(), PROCESS_TICKS, 1L), + AdapterUtils.safeInt(PROCESS_TICKS), getMoldItem())); + } + return recipes; + } + + @Override + @Nullable + public List>> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + Map mergedKeys, @Nullable ItemStack mold) { + if (level == null || mergedInputs.isEmpty() || mergedKeys.isEmpty() || !matchesMold(mold)) { + return List.of(); + } + RecipeManager recipeManager = level.getRecipeManager(); + List>> matches = new ArrayList<>(); + for (RecipeHolder> holder + : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.>getAllRecipesFor(recipeManager, getMekanismRecipeType())) { + ItemStackChemicalToItemStackRecipe recipe = holder.value(); + var itemInput = recipe.getItemInput(); + ChemicalStackIngredient chemicalInput = recipe.getChemicalInput(); + if (itemInput == null || chemicalInput == null + || !MekanismChemicalRecipeSupport.matchesItem(mergedInputs, itemInput)) continue; + + long multiplier = MekanismChemicalRecipeSupport.perTickUsage(recipe) ? PROCESS_TICKS : 1L; + boolean chemicalMatch = false; + for (ChemicalStack representation : chemicalInput.getRepresentations()) { + GenericStack key = MekanismChemicalRecipeSupport.key(mekanism.common.util.ChemicalUtil.copyWithAmount(representation, + MekanismChemicalRecipeSupport.saturatingMultiply(representation.getAmount(), multiplier))); + if (key != null && mergedKeys.getOrDefault(key.what(), 0L) >= key.amount()) { + chemicalMatch = true; + break; + } + } + if (chemicalMatch) matches.add(holder); + } + return matches; + } + + private static List copyItems(List outputs) { + return outputs.stream().map(ItemStack::copy).toList(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/NativeMekanismIngredients.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/NativeMekanismIngredients.java new file mode 100644 index 00000000..ab0dff10 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/NativeMekanismIngredients.java @@ -0,0 +1,76 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism; + +import com.google.gson.*; +import com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import net.minecraft.world.item.crafting.Ingredient; +import java.util.*; + +/** Structural conversion of native 10.4 ingredients, without expanding tags to snapshots. */ +public final class NativeMekanismIngredients { + private NativeMekanismIngredients() {} + public static CountedIngredient item(JsonElement value,long multiplier) { + var variants=itemVariants(value,multiplier); + if(variants.size()>1) throw new JsonSyntaxException("Mekanism item alternatives require quantity expansion"); + return variants.isEmpty()?null:variants.get(0); + } + public static List itemVariants(JsonElement value,long multiplier) { + Map groups=new LinkedHashMap<>(); + collectItem(value,multiplier,groups,0); + List result=new ArrayList<>(); + for(var entry:groups.entrySet()) { + Ingredient ingredient=Ingredient.fromJson(entry.getValue()); + if(!ingredient.isEmpty()) result.add(new CountedIngredient(ingredient,entry.getKey())); + } + return List.copyOf(result); + } + private static void collectItem(JsonElement value,long multiplier,Map groups,int depth) { + if(depth>64) throw new JsonSyntaxException("Mekanism ingredient nesting exceeds 64"); + if(value.isJsonArray()) { + for(var child:value.getAsJsonArray()) collectItem(child,multiplier,groups,depth+1); + return; + } + var object=value.getAsJsonObject(); + long amount=object.has("amount")?object.get("amount").getAsLong():1L; + if(amount<=0 || multiplier<=0) throw new JsonSyntaxException("Nonpositive Mekanism item amount"); + long count; + try { count=Math.multiplyExact(amount,multiplier); } + catch(ArithmeticException error) { throw new JsonSyntaxException("Mekanism item amount overflow",error); } + if(!object.has("ingredient")) throw new JsonSyntaxException("Missing native item predicate"); + var ingredient=object.get("ingredient"); + var group=groups.computeIfAbsent(count,ignored->new JsonArray()); + if(ingredient.isJsonArray()) ingredient.getAsJsonArray().forEach(group::add); + else group.add(ingredient); + } + public static SizedFluidIngredient fluid(JsonElement value) { + var variants=fluidVariants(value); + if(variants.size()>1) throw new JsonSyntaxException("Mekanism fluid alternatives require quantity expansion"); + return variants.isEmpty()?null:variants.get(0); + } + public static List fluidVariants(JsonElement value) { + List alternatives=new ArrayList<>(); + collectFluid(value,alternatives,0); + Map> groups=new LinkedHashMap<>(); + for(var input:alternatives) groups.computeIfAbsent(input.amount(),ignored->new ArrayList<>()).add(input.ingredient()); + return groups.entrySet().stream().map(entry->new SizedFluidIngredient(FluidIngredient.any(entry.getValue()),entry.getKey())).toList(); + } + private static void collectFluid(JsonElement value,List result,int depth) { + if(depth>64) throw new JsonSyntaxException("Mekanism fluid nesting exceeds 64"); + if(value.isJsonArray()) { + for(var child:value.getAsJsonArray()) collectFluid(child,result,depth+1); + return; + } + var json=value.getAsJsonObject().deepCopy(); + long amount=json.get("amount").getAsLong(); + if(amount<=0 || amount>Integer.MAX_VALUE) throw new JsonSyntaxException("Invalid Mekanism fluid amount"); + json.remove("amount"); + if(json.has("fluid")) { + // Native SingleFluidStackIngredient uses FluidStack.isFluidEqual: + // absent NBT is significant too; ordinary fluid-ID ingredients would broaden it. + json.addProperty("type","useless_mod:nbt_fluid"); + json.addProperty("strict",true); + } + result.add(new SizedFluidIngredient(FluidIngredient.fromJson(json),(int)amount)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/NativeMekanismVariants.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/NativeMekanismVariants.java new file mode 100644 index 00000000..3dd42838 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/NativeMekanismVariants.java @@ -0,0 +1,71 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism; + +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import mekanism.api.recipes.ingredients.ItemStackIngredient; +import mekanism.api.recipes.ingredients.FluidStackIngredient; +import java.util.*; +import java.util.function.Supplier; + +/** Expands native OR inputs with different amounts without flattening them to AND requirements. */ +final class NativeMekanismVariants { + private static final ThreadLocal> ACTIVE=new ThreadLocal<>(); + private record Choice(Object ingredient,List alternatives) {} + private NativeMekanismVariants() {} + + static boolean needsExpansion(Object recipe) { + return ACTIVE.get()==null && choices(recipe).stream().anyMatch(choice->choice.alternatives().size()>1); + } + static Object selected(Object ingredient) { + var active=ACTIVE.get(); return active==null?null:active.get(ingredient); + } + private static List choices(Object recipe) { + if(recipe==null) return List.of(); + List choices=new ArrayList<>(); + var seen=Collections.newSetFromMap(new IdentityHashMap()); + var methods=Arrays.stream(recipe.getClass().getMethods()) + .filter(method->!method.isBridge() && !method.isSynthetic() && method.getParameterCount()==0 + && (ItemStackIngredient.class.isAssignableFrom(method.getReturnType()) + || FluidStackIngredient.class.isAssignableFrom(method.getReturnType()))) + .sorted(Comparator.comparing(java.lang.reflect.Method::getName)).toList(); + for(var method:methods) { + try { + Object input=method.invoke(recipe); + if(input==null || !seen.add(input)) continue; + List variants=input instanceof ItemStackIngredient item + ? NativeMekanismIngredients.itemVariants(item.serialize(),1L) + : NativeMekanismIngredients.fluidVariants(((FluidStackIngredient)input).serialize()); + choices.add(new Choice(input,variants)); + } catch(ReflectiveOperationException error) { + throw new IllegalStateException("Cannot inspect native Mekanism ingredient "+method,error); + } + } + return choices; + } + static List expand(Object recipe,Supplier> convert) { + var choices=choices(recipe); + long total=1; + for(var choice:choices) { + total=Math.multiplyExact(total,choice.alternatives().size()); + if(total>4096) throw new IllegalArgumentException("Mekanism recipe exceeds 4096 quantity variants"); + } + List result=new ArrayList<>(); + try { + for(int variant=0;variant(); + int cursor=variant; + for(var choice:choices) { + int size=choice.alternatives().size(); + selected.put(choice.ingredient(),choice.alternatives().get(cursor%size)); cursor/=size; + } + ACTIVE.set(selected); + for(var converted:convert.get()) { + var id=MekanismChemicalRecipeSupport.variantId(converted.id(),"native_quantity_"+variant); + result.add(new AdvancedAlloyFurnaceRecipe(id,converted.inputs(),converted.inputFluids(),converted.keyInputs(), + converted.outputs(),converted.outputFluids(),converted.keyOutputs(),converted.energy(),converted.processTime(), + converted.catalyst(),converted.catalystUses(),converted.molds(),converted.mode(),converted.tier())); + } + } + } finally { ACTIVE.remove(); } + return List.copyOf(result); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/NucleosynthesizingRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/NucleosynthesizingRecipeAdapter.java new file mode 100644 index 00000000..b990a590 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/NucleosynthesizingRecipeAdapter.java @@ -0,0 +1,103 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism; + +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import mekanism.api.chemical.ChemicalStack; +import mekanism.common.recipe.MekanismRecipeType; +import mekanism.api.recipes.NucleosynthesizingRecipe; +import mekanism.api.recipes.ingredients.ChemicalStackIngredient; +import mekanism.api.recipes.ingredients.ItemStackIngredient; +import mekanism.common.registries.MekanismBlocks; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Native-duration adapter for the Antiprotonic Nucleosynthesizer. */ +public final class NucleosynthesizingRecipeAdapter implements IRecipeAdapter { + private static final long ENERGY_PER_TICK = 100_000L; + + @Override + public Class getRecipeClass() { + return NucleosynthesizingRecipe.class; + } + + @Override + public @Nullable ItemStack getMoldItem() { + return new ItemStack(MekanismBlocks.ANTIPROTONIC_NUCLEOSYNTHESIZER.getBlock()); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder != null && NativeMekanismVariants.needsExpansion(holder.value())) + return NativeMekanismVariants.expand(holder.value(), () -> convertAll(holder, level)); + if (holder == null || level == null || holder.value().getType() != MekanismRecipeType.NUCLEOSYNTHESIZING.get()) { + return List.of(); + } + NucleosynthesizingRecipe original = holder.value(); + ItemStackIngredient itemInput = original.getItemInput(); + ChemicalStackIngredient chemicalInput = original.getChemicalInput(); + if (MekanismChemicalRecipeSupport.item(itemInput) == null || chemicalInput == null + || chemicalInput.hasNoMatchingInstances() || original.getOutputDefinition().isEmpty()) return List.of(); + + long duration = Math.max(1L, original.getDuration()); + long multiplier = MekanismChemicalRecipeSupport.perTickUsage(original) ? duration : 1L; + List result = new ArrayList<>(); + for (ChemicalStack chemical : chemicalInput.getRepresentations()) { + GenericStack inputKey = MekanismChemicalRecipeSupport.key(mekanism.common.util.ChemicalUtil.copyWithAmount(chemical, + MekanismChemicalRecipeSupport.saturatingMultiply(chemical.getAmount(), multiplier))); + if (inputKey == null) continue; + for (ItemStack output : original.getOutputDefinition()) { + if (output.isEmpty()) continue; + result.add(MekanismChemicalRecipeSupport.recipe( + MekanismChemicalRecipeSupport.variantId(holder.id(), "nucleosynthesizing_" + + id(chemical)), MekanismChemicalRecipeSupport.items(itemInput), List.of(), + List.of(inputKey), List.of(output.copy()), List.of(), List.of(), + MekanismChemicalRecipeSupport.saturatingMultiply(ENERGY_PER_TICK, duration), + AdapterUtils.safeInt(duration), getMoldItem())); + } + } + return result; + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + Map mergedKeys, @Nullable ItemStack mold) { + if (level == null || mergedInputs.isEmpty() || mergedKeys.isEmpty() || !matchesMold(mold)) return List.of(); + RecipeManager manager = level.getRecipeManager(); + List> result = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(manager, + MekanismRecipeType.NUCLEOSYNTHESIZING.get())) { + NucleosynthesizingRecipe recipe = holder.value(); + if (!MekanismChemicalRecipeSupport.matchesItem(mergedInputs, recipe.getItemInput())) continue; + long multiplier = MekanismChemicalRecipeSupport.perTickUsage(recipe) ? Math.max(1L, recipe.getDuration()) : 1L; + boolean match = false; + for (ChemicalStack chemical : recipe.getChemicalInput().getRepresentations()) { + GenericStack key = MekanismChemicalRecipeSupport.key(mekanism.common.util.ChemicalUtil.copyWithAmount(chemical, + MekanismChemicalRecipeSupport.saturatingMultiply(chemical.getAmount(), multiplier))); + if (key != null && mergedKeys.getOrDefault(key.what(), 0L) >= key.amount()) { + match = true; + break; + } + } + if (match) result.add(holder); + } + return result; + } + + private static String id(ChemicalStack stack) { + var id = stack.getTypeRegistryName(); + return id.getNamespace() + "_" + id.getPath().replace('/', '_'); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/NutritionalLiquifierRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/NutritionalLiquifierRecipeAdapter.java new file mode 100644 index 00000000..7287b55d --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/NutritionalLiquifierRecipeAdapter.java @@ -0,0 +1,56 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism; + +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import mekanism.api.recipes.ItemStackToFluidRecipe; +import mekanism.common.registries.MekanismBlocks; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** Dynamic food -> nutritional paste adapter. */ +public final class NutritionalLiquifierRecipeAdapter extends MekanismSyntheticRecipeAdapter { + private static final long PROCESS_TICKS = 5L * 20L; + private static final long ENERGY_PER_TICK = 200L; + + @Override + public @Nullable ItemStack getMoldItem() { + return new ItemStack(MekanismBlocks.NUTRITIONAL_LIQUIFIER.getBlock()); + } + + @Override + protected List> createGeneratedRecipes(Level level) { + List> result = new ArrayList<>(); + for (var entry : BuiltInRegistries.ITEM.entrySet()) { + ItemStack input = entry.getValue().getDefaultInstance(); + var food=input.getFoodProperties(null); + if(!input.getItem().isEdible() || food == null || food.getNutrition()<=0) continue; + long amount=50L*food.getNutrition(); + if(amount>Integer.MAX_VALUE) continue; + ItemStackToFluidRecipe recipe=new mekanism.common.recipe.impl.NutritionalLiquifierIRecipe( + input.getItem(),mekanism.api.recipes.ingredients.creator.IngredientCreatorAccess.item().from(input,1), + new net.minecraftforge.fluids.FluidStack(mekanism.common.registries.MekanismFluids.NUTRITIONAL_PASTE.getStillFluid(),(int)amount)); + if (recipe == null || recipe.getOutputDefinition().isEmpty()) continue; + + var output=recipe.getOutput(input); + List itemOutputs=List.of(); + ResourceLocation itemId = entry.getKey().location(); + ResourceLocation id = MekanismChemicalRecipeSupport.variantId( + itemId, "nutritional_liquification"); + AdvancedAlloyFurnaceRecipe converted = MekanismChemicalRecipeSupport.recipe( + id, + MekanismChemicalRecipeSupport.items(recipe.getInput()), + List.of(), List.of(), itemOutputs, List.of(output.copy()), List.of(), + AdapterUtils.mekanismEnergyCost(ENERGY_PER_TICK, PROCESS_TICKS, 1L), + AdapterUtils.safeInt(PROCESS_TICKS), getMoldItem()); + result.add(MekanismChemicalRecipeSupport.syntheticHolder(id, converted)); + } + return result; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/OsmiumCompressorRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/OsmiumCompressorRecipeAdapter.java new file mode 100644 index 00000000..eaf14041 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/OsmiumCompressorRecipeAdapter.java @@ -0,0 +1,27 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism; + +import mekanism.api.recipes.chemical.ItemStackChemicalToItemStackRecipe; +import mekanism.common.recipe.MekanismRecipeType; +import mekanism.common.registries.MekanismBlocks; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.RecipeType; +import org.jetbrains.annotations.Nullable; + +public class OsmiumCompressorRecipeAdapter extends MetallurgicInfuserRecipeAdapter { + + @Override + protected long getEnergyPerTick() { + return 100L; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return new ItemStack(MekanismBlocks.OSMIUM_COMPRESSOR.getBlock()); + } + + @Override + protected RecipeType> getMekanismRecipeType() { + return MekanismRecipeType.COMPRESSING.get(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/PaintingRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/PaintingRecipeAdapter.java new file mode 100644 index 00000000..7086c2d0 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/PaintingRecipeAdapter.java @@ -0,0 +1,27 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism; + +import mekanism.api.recipes.chemical.ItemStackChemicalToItemStackRecipe; +import mekanism.common.recipe.MekanismRecipeType; +import mekanism.common.registries.MekanismBlocks; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.RecipeType; +import org.jetbrains.annotations.Nullable; + +/** Direct chemical-key adapter for the Painting Machine. */ +public final class PaintingRecipeAdapter extends MetallurgicInfuserRecipeAdapter { + @Override + protected long getEnergyPerTick() { + return 100L; + } + + @Override + protected RecipeType> getMekanismRecipeType() { + return MekanismRecipeType.PAINTING.get(); + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return new ItemStack(MekanismBlocks.PAINTING_MACHINE.getBlock()); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/PrecisionSawmillRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/PrecisionSawmillRecipeAdapter.java new file mode 100644 index 00000000..d8a4c05f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/PrecisionSawmillRecipeAdapter.java @@ -0,0 +1,181 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import mekanism.common.recipe.MekanismRecipeType; +import mekanism.api.recipes.SawmillRecipe; +import mekanism.api.recipes.ingredients.ItemStackIngredient; +import mekanism.common.registries.MekanismBlocks; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class PrecisionSawmillRecipeAdapter implements IRecipeAdapter { + + @Override + public Class getRecipeClass() { + return SawmillRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return new ItemStack(MekanismBlocks.PRECISION_SAWMILL.getBlock()); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder != null && NativeMekanismVariants.needsExpansion(holder.value())) + return NativeMekanismVariants.expand(holder.value(), () -> convertAll(holder, level)); + List result = new ArrayList<>(); + + if (holder == null) return result; + + SawmillRecipe originalRecipe = holder.value(); + + if (!originalRecipe.getType().equals(MekanismRecipeType.SAWING.get())) { + return result; + } + + ChanceScale chanceScale = scaleChance(originalRecipe.getSecondaryChance()); + long multiplier = chanceScale.denominator(); + + List countedIngredients = new ArrayList<>(); + addCountedIngredient(countedIngredients, originalRecipe.getInput(), multiplier); + + if (countedIngredients.isEmpty()) { + return result; + } + + List outputs = new ArrayList<>(); + for (ItemStack output : originalRecipe.getMainOutputDefinition()) { + if (!output.isEmpty()) { + ItemStack scaled = output.copy(); + scaled.setCount(AdapterUtils.safeInt((long) output.getCount() * multiplier)); + outputs.add(scaled); + } + } + if (chanceScale.numerator() > 0) { + for (ItemStack output : originalRecipe.getSecondaryOutputDefinition()) { + if (!output.isEmpty()) { + ItemStack scaled = output.copy(); + scaled.setCount(AdapterUtils.safeInt((long) output.getCount() * chanceScale.numerator())); + outputs.add(scaled); + } + } + } + + if (outputs.isEmpty()) { + return result; + } + + result.add(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + countedIngredients, + List.of(), + outputs, + List.of(), + AdapterUtils.mekanismEnergyCost(AdapterUtils.MEKANISM_ENRICHMENT_CHAMBER_ENERGY_PER_TICK, multiplier), + AdapterUtils.mekanismProcessTime(multiplier), + Ingredient.EMPTY, + 0, + AdapterUtils.toMoldIngredient(getMoldItem()), + AlloyFurnaceMode.NORMAL + )); + return result; + } + + @Override + @Nullable + public List> findMatchingRecipes(Level level, Map mergedInputs, Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || mergedInputs.isEmpty()) { + return List.of(); + } + if (mold != null && !mold.isEmpty() && !matchesMold(mold)) { + return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + List> recipes = com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + MekanismRecipeType.SAWING.get() + ); + + List> matches = new java.util.ArrayList<>(); + for (RecipeHolder holder : recipes) { + SawmillRecipe recipe = holder.value(); + ItemStackIngredient input = recipe.getInput(); + if (input == null) continue; + + if (matchesIngredient(mergedInputs, input)) { + matches.add(holder); + } + } + return matches; + } + + private static ChanceScale scaleChance(double chance) { + if (chance <= 0) { + return new ChanceScale(0, 1); + } + if (chance >= 1) { + return new ChanceScale(1, 1); + } + int bestDenominator = 1; + int bestNumerator = 0; + double bestError = Double.MAX_VALUE; + for (int denominator = 1; denominator <= 1000; denominator++) { + int numerator = (int) Math.round(chance * denominator); + double error = Math.abs(chance - (double) numerator / denominator); + if (error < bestError) { + bestError = error; + bestDenominator = denominator; + bestNumerator = numerator; + if (error < 1.0E-9) { + break; + } + } + } + long gcd = AdapterUtils.gcd(bestNumerator, bestDenominator); + return new ChanceScale((int) (bestNumerator / gcd), (int) (bestDenominator / gcd)); + } + + @Nullable + private static CountedIngredient countedIngredient(ItemStackIngredient input, long multiplier) { + return MekanismChemicalRecipeSupport.countedItem(input, multiplier); + } + + private static boolean matchesIngredient(Map mergedInputs, ItemStackIngredient required) { + if (required == null) { + return false; + } + return MekanismChemicalRecipeSupport.matchesItem(mergedInputs,required); + } + + private static void addCountedIngredient(List countedIngredients, ItemStackIngredient input, long multiplier) { + CountedIngredient counted = countedIngredient(input, multiplier); + if (counted == null) { + return; + } + for (int i = 0; i < countedIngredients.size(); i++) { + CountedIngredient existing = countedIngredients.get(i); + if (AdapterUtils.areIngredientsEqual(existing.ingredient(), counted.ingredient())) { + countedIngredients.set(i, new CountedIngredient(existing.ingredient(), existing.count() + counted.count())); + return; + } + } + countedIngredients.add(counted); + } + + private record ChanceScale(int numerator, int denominator) {} +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/PressurizedReactionRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/PressurizedReactionRecipeAdapter.java new file mode 100644 index 00000000..71567f5a --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/PressurizedReactionRecipeAdapter.java @@ -0,0 +1,116 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism; + +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import mekanism.api.chemical.ChemicalStack; +import mekanism.common.recipe.MekanismRecipeType; +import mekanism.api.recipes.PressurizedReactionRecipe; +import mekanism.api.recipes.ingredients.ChemicalStackIngredient; +import mekanism.api.recipes.ingredients.FluidStackIngredient; +import mekanism.api.recipes.ingredients.ItemStackIngredient; +import mekanism.common.registries.MekanismBlocks; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Item + fluid + chemical -> item and/or chemical adapter for PRCs. */ +public final class PressurizedReactionRecipeAdapter implements IRecipeAdapter { + private static final long BASE_ENERGY_PER_TICK = 5L; + + @Override + public Class getRecipeClass() { + return PressurizedReactionRecipe.class; + } + + @Override + public @Nullable ItemStack getMoldItem() { + return new ItemStack(MekanismBlocks.PRESSURIZED_REACTION_CHAMBER.getBlock()); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder != null && NativeMekanismVariants.needsExpansion(holder.value())) + return NativeMekanismVariants.expand(holder.value(), () -> convertAll(holder, level)); + if (holder == null || level == null || holder.value().getType() != MekanismRecipeType.REACTION.get()) { + return List.of(); + } + PressurizedReactionRecipe original = holder.value(); + ItemStackIngredient itemInput = original.getInputSolid(); + FluidStackIngredient fluidInput = original.getInputFluid(); + ChemicalStackIngredient chemicalInput = original.getInputGas(); + if (MekanismChemicalRecipeSupport.item(itemInput) == null || fluidInput == null || chemicalInput == null + || fluidInput.hasNoMatchingInstances() + || chemicalInput.hasNoMatchingInstances()) return List.of(); + + List result = new ArrayList<>(); + List fluidInputs = + MekanismChemicalRecipeSupport.fluidIngredients(fluidInput); + for (ChemicalStack chemical : chemicalInput.getRepresentations()) { + GenericStack chemicalKey = MekanismChemicalRecipeSupport.key(chemical); + if (chemicalKey == null) continue; + for (PressurizedReactionRecipe.PressurizedReactionRecipeOutput output + : original.getOutputDefinition()) { + List chemicalOutputs = new ArrayList<>(); + if (!output.gas().isEmpty()) { + GenericStack outputKey = MekanismChemicalRecipeSupport.key(output.gas()); + if (outputKey != null) chemicalOutputs.add(outputKey); + } + if (output.item().isEmpty() && chemicalOutputs.isEmpty()) continue; + + String suffix = "reaction_" + id(chemical) + "_out"; + result.add(MekanismChemicalRecipeSupport.recipe( + MekanismChemicalRecipeSupport.variantId(holder.id(), suffix), + MekanismChemicalRecipeSupport.items(itemInput), fluidInputs, List.of(chemicalKey), + output.item().isEmpty() ? List.of() : List.of(output.item().copy()), List.of(), + chemicalOutputs, reactionEnergy(original), original.getDuration(), getMoldItem())); + } + } + return result; + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + Map mergedKeys, @Nullable ItemStack mold) { + if (level == null || mergedInputs.isEmpty() || mergedFluids.isEmpty() || mergedKeys.isEmpty() + || !matchesMold(mold)) return List.of(); + RecipeManager manager = level.getRecipeManager(); + List> result = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(manager, + MekanismRecipeType.REACTION.get())) { + PressurizedReactionRecipe recipe = holder.value(); + if (MekanismChemicalRecipeSupport.matchesItem(mergedInputs, recipe.getInputSolid()) + && MekanismChemicalRecipeSupport.matchesFluid(mergedFluids, recipe.getInputFluid()) + && MekanismChemicalRecipeSupport.matchesChemical(mergedKeys, recipe.getInputGas())) { + result.add(holder); + } + } + return result; + } + + private static long reactionEnergy(PressurizedReactionRecipe recipe) { + long energyPerTick = MekanismChemicalRecipeSupport.saturatingAdd( + BASE_ENERGY_PER_TICK, recipe.getEnergyRequired().longValue()); + return MekanismChemicalRecipeSupport.saturatingMultiply(energyPerTick, recipe.getDuration()); + } + + private static String id(ChemicalStack stack) { + var id = stack.getTypeRegistryName(); + return id.getNamespace() + "_" + id.getPath().replace('/', '_'); + } + + private static String fluidId(FluidStack stack) { + var id = net.minecraft.core.registries.BuiltInRegistries.FLUID.getKey(stack.getFluid()); + return id.getNamespace() + "_" + id.getPath().replace('/', '_'); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/PurificationChamberRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/PurificationChamberRecipeAdapter.java new file mode 100644 index 00000000..1ca9af57 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/PurificationChamberRecipeAdapter.java @@ -0,0 +1,22 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism; + +import mekanism.api.recipes.chemical.ItemStackChemicalToItemStackRecipe; +import mekanism.common.recipe.MekanismRecipeType; +import mekanism.common.registries.MekanismBlocks; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.RecipeType; +import org.jetbrains.annotations.Nullable; + +/** Direct chemical-key adapter for the Purification Chamber. */ +public final class PurificationChamberRecipeAdapter extends MetallurgicInfuserRecipeAdapter { + @Override + protected RecipeType> getMekanismRecipeType() { + return MekanismRecipeType.PURIFYING.get(); + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return new ItemStack(MekanismBlocks.PURIFICATION_CHAMBER.getBlock()); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/RecyclerRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/RecyclerRecipeAdapter.java new file mode 100644 index 00000000..74cc32c3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/RecyclerRecipeAdapter.java @@ -0,0 +1,115 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism; + +import com.jerry.mekmm.api.recipes.MoreMachineRecipeTypes; +import com.jerry.mekmm.api.recipes.RecyclerRecipe; +import com.jerry.mekmm.common.config.MoreMachineConfig; +import com.jerry.mekmm.common.registries.MoreMachineBlocks; +import com.jerry.mekmm.common.tile.machine.TileEntityRecycler; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeFingerprint; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.item.ItemStack; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** Exposes every possible MoreMachine Recycler output as a water-only recipe. */ +public final class RecyclerRecipeAdapter extends MekanismSyntheticRecipeAdapter { + private static final int WATER_AMOUNT = 1_000; + private static final int PROCESS_TICKS = TileEntityRecycler.BASE_TICKS_REQUIRED; + + @Override + public @Nullable ItemStack getMoldItem() { + return new ItemStack(MoreMachineBlocks.RECYCLER.get()); + } + + @Override + protected List> createGeneratedRecipes(Level level) { + if (level == null) { + return List.of(); + } + + List outputs = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(level.getRecipeManager(), + MoreMachineRecipeTypes.TYPE_RECYCLER.value())) { + RecyclerRecipe source = holder.value(); + if (source == null || source.isIncomplete() || source.getOutputChance() <= 0) { + continue; + } + + for (ItemStack output : source.getChanceOutputDefinition()) { + if (output == null || output.isEmpty() || containsOutput(outputs, output)) { + continue; + } + outputs.add(output.copy()); + } + } + + if (outputs.isEmpty()) { + return List.of(); + } + + List> result = new ArrayList<>(outputs.size()); + for (ItemStack output : outputs) { + ResourceLocation id = recipeId(output, level.registryAccess()); + AdvancedAlloyFurnaceRecipe converted = MekanismChemicalRecipeSupport.recipe( + id, + List.of(), + List.of(new SizedFluidIngredient( + FluidIngredient.tag(FluidTags.WATER), WATER_AMOUNT)), + List.of(), + List.of(output.copy()), + List.of(), + List.of(), + energyCost(), + PROCESS_TICKS, + getMoldItem()); + result.add(MekanismChemicalRecipeSupport.syntheticHolder(id, converted)); + } + return List.copyOf(result); + } + + private static long energyCost() { + return MekanismChemicalRecipeSupport.saturatingMultiply( + MoreMachineConfig.usage.recycler.get(), PROCESS_TICKS); + } + + private static boolean containsOutput(List outputs, ItemStack candidate) { + for (ItemStack output : outputs) { + if (output.getCount() == candidate.getCount() + && ItemStack.isSameItemSameTags(output, candidate)) { + return true; + } + } + return false; + } + + /** + * 由产物内容构造配方 id。 + * + *

id 不得包含产物列表下标:客户端与服务端的回收配方枚举顺序不保证一致,位置派生的 id + * 会让同一个 id 在两端指向不同的产物,导致万象样板在服务端无法解析。判重依据必须与 + * {@link #containsOutput} 完全一致——它以「物品 + 组件 + 数量」判定产物是否相同,因此 id + * 只含物品注册名与数量是不够的:同物品同数量而组件不同的产物会同时存在并产生相同 id。 + * 此处复用指纹模块的物品栈编码,它递归规范化组件映射,只由数据内容决定结果。

+ */ + private static ResourceLocation recipeId(ItemStack output, HolderLookup.Provider registries) { + ResourceLocation itemId = BuiltInRegistries.ITEM.getKey(output.getItem()); + String prefix = itemId == null + ? "unregistered" + : itemId.getNamespace() + "_" + itemId.getPath().replace('/', '_'); + return new ResourceLocation( + "mekmm", "recycler_water_" + prefix + "_" + output.getCount() + "_" + + AdapterUtils.stableHash( + AlloyFurnaceRecipeFingerprint.safeItemStack(output, registries))); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/RotaryRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/RotaryRecipeAdapter.java new file mode 100644 index 00000000..79743635 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/RotaryRecipeAdapter.java @@ -0,0 +1,117 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism; + +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import mekanism.api.chemical.ChemicalStack; +import mekanism.common.recipe.MekanismRecipeType; +import mekanism.api.recipes.RotaryRecipe; +import mekanism.api.recipes.ingredients.ChemicalStackIngredient; +import mekanism.api.recipes.ingredients.FluidStackIngredient; +import mekanism.common.registries.MekanismBlocks; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Splits Rotary Condensentrator's two directions into independent recipes. */ +public final class RotaryRecipeAdapter implements IRecipeAdapter { + private static final long PROCESS_TICKS = AdapterUtils.MEKANISM_BASE_TICKS_REQUIRED; + private static final long ENERGY_PER_TICK = 50L; + + @Override + public Class getRecipeClass() { + return RotaryRecipe.class; + } + + @Override + public @Nullable ItemStack getMoldItem() { + return new ItemStack(MekanismBlocks.ROTARY_CONDENSENTRATOR.getBlock()); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder != null && NativeMekanismVariants.needsExpansion(holder.value())) + return NativeMekanismVariants.expand(holder.value(), () -> convertAll(holder, level)); + if (holder == null || level == null || holder.value().getType() != MekanismRecipeType.ROTARY.get()) { + return List.of(); + } + RotaryRecipe original = holder.value(); + List result = new ArrayList<>(); + + if (original.hasFluidToGas()) { + FluidStackIngredient fluidInput = original.getFluidInput(); + if (fluidInput != null && !fluidInput.hasNoMatchingInstances()) { + List fluidInputs = + MekanismChemicalRecipeSupport.fluidIngredients(fluidInput); + for (ChemicalStack chemical : original.getGasOutputDefinition()) { + GenericStack output = MekanismChemicalRecipeSupport.key(chemical); + if (output == null) continue; + result.add(MekanismChemicalRecipeSupport.recipe( + MekanismChemicalRecipeSupport.variantId(holder.id(), "fluid_to_chemical_" + id(chemical)), + List.of(), fluidInputs, List.of(), List.of(), List.of(), List.of(output), + AdapterUtils.mekanismEnergyCost(ENERGY_PER_TICK, PROCESS_TICKS, 1L), + AdapterUtils.safeInt(PROCESS_TICKS), getMoldItem())); + } + } + } + + if (original.hasGasToFluid()) { + ChemicalStackIngredient chemicalInput = original.getGasInput(); + if (chemicalInput != null && !chemicalInput.hasNoMatchingInstances()) { + for (ChemicalStack chemical : chemicalInput.getRepresentations()) { + GenericStack input = MekanismChemicalRecipeSupport.key(chemical); + if (input == null) continue; + for (FluidStack fluid : original.getFluidOutputDefinition()) { + FluidStack output = fluid.copy(); + result.add(MekanismChemicalRecipeSupport.recipe( + MekanismChemicalRecipeSupport.variantId(holder.id(), "chemical_to_fluid_" + + id(chemical) + "_" + fluidId(output)), List.of(), List.of(), List.of(input), + List.of(), List.of(output), List.of(), + AdapterUtils.mekanismEnergyCost(ENERGY_PER_TICK, PROCESS_TICKS, 1L), + AdapterUtils.safeInt(PROCESS_TICKS), getMoldItem())); + } + } + } + } + return result; + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + Map mergedKeys, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) return List.of(); + RecipeManager manager = level.getRecipeManager(); + List> result = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(manager, MekanismRecipeType.ROTARY.get())) { + RotaryRecipe recipe = holder.value(); + boolean fluidMatch = recipe.hasFluidToGas() && !mergedFluids.isEmpty() + && MekanismChemicalRecipeSupport.matchesFluid(mergedFluids, recipe.getFluidInput()); + boolean chemicalMatch = recipe.hasGasToFluid() && !mergedKeys.isEmpty() + && MekanismChemicalRecipeSupport.matchesChemical(mergedKeys, recipe.getGasInput()); + if (fluidMatch || chemicalMatch) result.add(holder); + } + return result; + } + + private static String id(ChemicalStack stack) { + ResourceLocation id = stack.getTypeRegistryName(); + return id.getNamespace() + "_" + id.getPath().replace('/', '_'); + } + + private static String fluidId(FluidStack stack) { + ResourceLocation id = net.minecraft.core.registries.BuiltInRegistries.FLUID.getKey(stack.getFluid()); + return id.getNamespace() + "_" + id.getPath().replace('/', '_'); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/SpsRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/SpsRecipeAdapter.java new file mode 100644 index 00000000..63c2a7ec --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/SpsRecipeAdapter.java @@ -0,0 +1,59 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism; + +import appeng.api.stacks.GenericStack; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import mekanism.api.recipes.ingredients.ChemicalStackIngredient; +import mekanism.api.recipes.ingredients.creator.IngredientCreatorAccess; +import mekanism.common.Mekanism; +import mekanism.common.config.MekanismConfig; +import mekanism.common.registries.MekanismGases; +import mekanism.common.registries.MekanismBlocks; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** Dynamic SPS recipe-viewer adapter. */ +public final class SpsRecipeAdapter extends MekanismSyntheticRecipeAdapter { + private static final long PROCESS_TICKS = AdapterUtils.MEKANISM_BASE_TICKS_REQUIRED; + + @Override + public @Nullable ItemStack getMoldItem() { + return new ItemStack(MekanismBlocks.SPS_CASING.getBlock()); + } + + @Override + protected List> createGeneratedRecipes(Level level) { + List> result = new ArrayList<>(); + // The SPS has no common recipe type. Mirror Mekanism's recipe-viewer source + // from common classes so dedicated servers never load client-only classes. + ChemicalStackIngredient input = IngredientCreatorAccess.gas().from( + MekanismGases.POLONIUM, MekanismConfig.general.spsInputPerAntimatter.get()); + GenericStack outputKey = MekanismChemicalRecipeSupport.key(MekanismGases.ANTIMATTER.getStack(1)); + if (outputKey == null) return result; + + for (var concreteInput : input.getRepresentations()) { + GenericStack inputKey = MekanismChemicalRecipeSupport.key(concreteInput); + if (inputKey == null) continue; + ResourceLocation id = MekanismChemicalRecipeSupport.variantId( + Mekanism.rl("antimatter"), "sps_" + id(concreteInput)); + AdvancedAlloyFurnaceRecipe converted = MekanismChemicalRecipeSupport.recipe( + id, List.of(), List.of(), List.of(inputKey), List.of(), List.of(), List.of(outputKey), + MekanismChemicalRecipeSupport.energyCost(1L, concreteInput.getAmount(), + MekanismConfig.general.spsEnergyPerInput.get()), + AdapterUtils.safeInt(PROCESS_TICKS), getMoldItem()); + result.add(MekanismChemicalRecipeSupport.syntheticHolder(id, converted)); + } + return result; + } + + private static String id(mekanism.api.chemical.ChemicalStack stack) { + var id = stack.getTypeRegistryName(); + return id.getNamespace() + "_" + id.getPath().replace('/', '_'); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/generators/FissionRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/generators/FissionRecipeAdapter.java new file mode 100644 index 00000000..374fb528 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/generators/FissionRecipeAdapter.java @@ -0,0 +1,116 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism.generators; + +import appeng.api.stacks.GenericStack; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.MekanismChemicalRecipeSupport; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.MekanismSyntheticRecipe; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.MekanismSyntheticRecipeAdapter; +import mekanism.api.MekanismAPI; +import mekanism.api.chemical.ChemicalStack; +import mekanism.api.chemical.gas.Gas; +import mekanism.api.chemical.gas.GasStack; +import mekanism.api.chemical.gas.attribute.GasAttributes; +import mekanism.api.recipes.ingredients.FluidStackIngredient; +import mekanism.api.recipes.ingredients.creator.IngredientCreatorAccess; +import mekanism.common.registries.MekanismGases; +import mekanism.common.util.HeatUtils; +import mekanism.generators.common.registries.GeneratorsBlocks; +import mekanism.generators.common.MekanismGenerators; +import mekanism.generators.common.config.MekanismGeneratorsConfig; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.item.ItemStack; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** Optional Mekanism Generators fission recipe-viewer adapter. */ +public final class FissionRecipeAdapter extends MekanismSyntheticRecipeAdapter { + private static final long PROCESS_TICKS = AdapterUtils.MEKANISM_BASE_TICKS_REQUIRED; + + @Override + public @Nullable ItemStack getMoldItem() { + return new ItemStack(GeneratorsBlocks.FISSION_REACTOR_CASING.getBlock()); + } + + @Override + protected List> createGeneratedRecipes(Level level) { + double energyPerFuel = MekanismGeneratorsConfig.generators.energyPerFissionFuel.get().doubleValue(); + long waterAmount = Math.round(energyPerFuel * HeatUtils.getSteamEnergyEfficiency() + / HeatUtils.getWaterThermalEnthalpy()); + return createGeneratedRecipes(energyPerFuel, waterAmount); + } + + List> createGeneratedRecipes(double energyPerFuel, long waterAmount) { + List> result = new ArrayList<>(); + + ChemicalStack fuel = MekanismGases.FISSILE_FUEL.getStack(1); + ChemicalStack steam = MekanismGases.STEAM.getStack(waterAmount); + var water = IngredientCreatorAccess.fluid().from(FluidTags.WATER, Math.toIntExact(waterAmount)); + result.addAll(createWaterRecipes(water, fuel, steam, MekanismGases.NUCLEAR_WASTE.getStack(1))); + + for(Gas gas:MekanismAPI.gasRegistry()) { + GasAttributes.CooledCoolant coolant=gas.get(GasAttributes.CooledCoolant.class); + if(coolant==null) continue; + long amount=Math.round(energyPerFuel/coolant.getThermalEnthalpy()); + if(amount<=0) continue; + GasStack cooled=new GasStack(gas,amount); + addRecipe(result,MekanismGenerators.rl("fission_"+id(cooled)),fuel,cooled,null, + new GasStack(coolant.getHeatedGas(),amount),MekanismGases.NUCLEAR_WASTE.getStack(1)); + } + return result; + } + + List> createWaterRecipes( + FluidStackIngredient water, ChemicalStack fuel, ChemicalStack steam, ChemicalStack waste) { + List> result = new ArrayList<>(); + List waterInputs = MekanismChemicalRecipeSupport.fluidIngredients(water); + if (!waterInputs.isEmpty()) { + addRecipe(result, MekanismGenerators.rl("fission_water"), fuel, null, + waterInputs.get(0), steam, waste); + } + return result; + } + + private void addRecipe(List> result, ResourceLocation sourceId, + ChemicalStack fuel, @Nullable ChemicalStack coolant, + @Nullable SizedFluidIngredient water, + ChemicalStack outputCoolant, ChemicalStack waste) { + GenericStack fuelKey = MekanismChemicalRecipeSupport.key(fuel); + GenericStack coolantKey = coolant == null ? null : MekanismChemicalRecipeSupport.key(coolant); + GenericStack outputCoolantKey = MekanismChemicalRecipeSupport.key(outputCoolant); + GenericStack wasteKey = MekanismChemicalRecipeSupport.key(waste); + if (fuelKey == null || (coolant != null && coolantKey == null) || outputCoolantKey == null || wasteKey == null) return; + + List inputs = coolantKey == null ? List.of(fuelKey) : List.of(fuelKey, coolantKey); + if (water != null && (water.ingredient() == null || water.ingredient().isEmpty() + || water.amount() <= 0)) return; + + String suffix = "fission_" + id(fuel); + if (water != null) { + suffix += "_water"; + } else if (coolant != null) { + suffix += "_" + id(coolant); + } else { + return; + } + ResourceLocation id = MekanismChemicalRecipeSupport.variantId(sourceId, suffix); + AdvancedAlloyFurnaceRecipe converted = MekanismChemicalRecipeSupport.recipe( + id, List.of(), water == null ? List.of() : List.of(water), inputs, List.of(), List.of(), + List.of(outputCoolantKey, wasteKey), + 0L, + AdapterUtils.safeInt(PROCESS_TICKS), getMoldItem()); + result.add(MekanismChemicalRecipeSupport.syntheticHolder(id, converted)); + } + + private static String id(ChemicalStack stack) { + var id = stack.getTypeRegistryName(); + return id.getNamespace() + "_" + id.getPath().replace('/', '_'); + } + +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/mekanismextra/NaquadahReactorRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/mekanismextra/NaquadahReactorRecipeAdapter.java new file mode 100644 index 00000000..ba0af5d0 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/mekanismextra/NaquadahReactorRecipeAdapter.java @@ -0,0 +1,109 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism.mekanismextra; + +import appeng.api.stacks.GenericStack; +import com.jerry.generator_extras.common.config.GenLoadConfig; +import com.jerry.generator_extras.common.genregistry.ExtraGenBlocks; +import com.jerry.generator_extras.common.genregistry.ExtraGenGases; +import com.jerry.mekanism_extras.common.registry.ExtraGases; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.MekanismChemicalRecipeSupport; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.MekanismSyntheticRecipe; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.MekanismSyntheticRecipeAdapter; +import mekanism.api.chemical.ChemicalStack; +import mekanism.api.recipes.ingredients.creator.IngredientCreatorAccess; +import mekanism.common.util.HeatUtils; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.item.ItemStack; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** Optional Mekanism Extras naquadah reactor recipe-viewer adapter. */ +public final class NaquadahReactorRecipeAdapter extends MekanismSyntheticRecipeAdapter { + private static final long PROCESS_TICKS = AdapterUtils.MEKANISM_BASE_TICKS_REQUIRED; + private static final ResourceLocation SOURCE_ID = + new ResourceLocation("mekanism_extras", "naquadah_reactor"); + + @Override + public @Nullable ItemStack getMoldItem() { + return new ItemStack(ExtraGenBlocks.NAQUADAH_REACTOR_CONTROLLER.getBlock()); + } + + @Override + protected List> createGeneratedRecipes(Level level) { + List> result = new java.util.ArrayList<>(3); + + double energyPerFuel = GenLoadConfig.generatorConfig.energyPerReactorFuel.get().doubleValue(); + long waterPerFuel = Math.round(energyPerFuel * HeatUtils.getSteamEnergyEfficiency() + / HeatUtils.getWaterThermalEnthalpy()); + if (waterPerFuel <= 0L) { + return result; + } + + var singleFuelWater = IngredientCreatorAccess.fluid().from( + FluidTags.WATER, Math.toIntExact(waterPerFuel)); + var singleFuelWaterInputs = MekanismChemicalRecipeSupport.fluidIngredients(singleFuelWater); + var pairedFuelWater = IngredientCreatorAccess.fluid().from( + FluidTags.WATER, Math.toIntExact( + MekanismChemicalRecipeSupport.saturatingMultiply(waterPerFuel, 2L))); + var pairedFuelWaterInputs = MekanismChemicalRecipeSupport.fluidIngredients(pairedFuelWater); + if (singleFuelWaterInputs.isEmpty() || pairedFuelWaterInputs.isEmpty()) { + return result; + } + + addFuelRecipe(result, pairedFuelWaterInputs, + List.of(ExtraGases.RICH_SILICON_LIQUID_FUEL.getStack(1), + ExtraGases.RICH_URANIUM_LIQUID_FUEL.getStack(1)), + MekanismChemicalRecipeSupport.saturatingMultiply(waterPerFuel, 2L), + "rich_fuels_water"); + addFuelRecipe(result, singleFuelWaterInputs, + List.of(ExtraGases.SILICON_URANIUM_FUEL.getStack(1)), + waterPerFuel, "naquadah_uranium_fuel_water"); + addWaterOnlyRecipe(result); + return result; + } + + private void addFuelRecipe(List> result, + List waterInputs, List> fuels, + long steamAmount, String suffix) { + List fuelKeys = new java.util.ArrayList<>(fuels.size()); + for (ChemicalStack fuel : fuels) { + GenericStack fuelKey = MekanismChemicalRecipeSupport.key(fuel); + if (fuelKey == null) { + return; + } + fuelKeys.add(fuelKey); + } + GenericStack steamKey = MekanismChemicalRecipeSupport.key( + ExtraGenGases.POLONIUM_CONTAINING_STEAM.getStack(steamAmount)); + if (steamKey == null) { + return; + } + + ResourceLocation id = MekanismChemicalRecipeSupport.variantId(SOURCE_ID, suffix); + AdvancedAlloyFurnaceRecipe converted = MekanismChemicalRecipeSupport.recipe( + id, List.of(), waterInputs, fuelKeys, List.of(), List.of(), List.of(steamKey), + 0L, AdapterUtils.safeInt(PROCESS_TICKS), getMoldItem()); + result.add(MekanismChemicalRecipeSupport.syntheticHolder(id, converted)); + } + + private void addWaterOnlyRecipe(List> result) { + var water = IngredientCreatorAccess.fluid().from(FluidTags.WATER, 1); + var waterInputs = MekanismChemicalRecipeSupport.fluidIngredients(water); + GenericStack steamKey = MekanismChemicalRecipeSupport.key( + ExtraGenGases.POLONIUM_CONTAINING_STEAM.getStack(1)); + if (waterInputs.isEmpty() || steamKey == null) { + return; + } + + ResourceLocation id = MekanismChemicalRecipeSupport.variantId(SOURCE_ID, "water"); + AdvancedAlloyFurnaceRecipe converted = MekanismChemicalRecipeSupport.recipe( + id, List.of(), waterInputs, List.of(), List.of(), List.of(), List.of(steamKey), + 0L, AdapterUtils.safeInt(PROCESS_TICKS), getMoldItem()); + result.add(MekanismChemicalRecipeSupport.syntheticHolder(id, converted)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/mekanismmoremachine/AmbientGasCollectorRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/mekanismmoremachine/AmbientGasCollectorRecipeAdapter.java new file mode 100644 index 00000000..910ef227 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/mekanismmoremachine/AmbientGasCollectorRecipeAdapter.java @@ -0,0 +1,63 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism.mekanismmoremachine; + +import appeng.api.stacks.GenericStack; +import com.jerry.mekmm.common.config.MoreMachineConfig; +import com.jerry.mekmm.common.registries.MoreMachineBlocks; +import com.jerry.mekmm.common.registries.MoreMachineChemicals; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.MekanismChemicalRecipeSupport; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.MekanismSyntheticRecipe; +import com.sorrowmist.useless.content.recipe.adapters.mekanism.MekanismSyntheticRecipeAdapter; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** Exposes Mekanism More Machine's ambient gas collector as a water-fed recipe. */ +public final class AmbientGasCollectorRecipeAdapter extends MekanismSyntheticRecipeAdapter { + private static final int PROCESS_TICKS = 19; + private static final ResourceLocation RECIPE_ID = + new ResourceLocation("mekmm", "ambient_gas_collector_water"); + + @Override + public @Nullable ItemStack getMoldItem() { + return new ItemStack(MoreMachineBlocks.AMBIENT_GAS_COLLECTOR.get()); + } + + @Override + protected List> createGeneratedRecipes(Level level) { + if (level == null) { + return List.of(); + } + + int gasAmount = MoreMachineConfig.general.gasCollectAmount.get(); + if (gasAmount <= 0) { + return List.of(); + } + + GenericStack output = MekanismChemicalRecipeSupport.key( + MoreMachineChemicals.UNSTABLE_DIMENSIONAL_GAS.asStack(gasAmount)); + if (output == null) { + return List.of(); + } + + AdvancedAlloyFurnaceRecipe converted = MekanismChemicalRecipeSupport.recipe( + RECIPE_ID, + List.of(), + List.of(new FluidStack(Fluids.WATER, gasAmount)), + List.of(), + List.of(), + List.of(), + List.of(output), + MekanismChemicalRecipeSupport.saturatingMultiply( + MoreMachineConfig.usage.ambientGasCollector.get(), PROCESS_TICKS), + PROCESS_TICKS, + getMoldItem()); + return List.of(MekanismChemicalRecipeSupport.syntheticHolder(RECIPE_ID, converted)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/minecraft/BrewingRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/minecraft/BrewingRecipeAdapter.java new file mode 100644 index 00000000..9692b1d0 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/minecraft/BrewingRecipeAdapter.java @@ -0,0 +1,229 @@ +package com.sorrowmist.useless.content.recipe.adapters.minecraft; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.compat.forge1201.recipe.DataComponentIngredient; +import com.sorrowmist.useless.compat.forge1201.recipe.NbtCanonical; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import com.sorrowmist.useless.content.recipe.*; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.alchemy.PotionBrewing; +import net.minecraft.world.item.alchemy.PotionUtils; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.common.brewing.BrewingRecipe; +import net.minecraftforge.common.brewing.BrewingRecipeRegistry; +import net.minecraftforge.common.brewing.IBrewingRecipe; +import net.minecraftforge.event.TagsUpdatedEvent; +import net.minecraftforge.event.OnDatapackSyncEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.*; +import java.util.concurrent.atomic.AtomicLong; + +/** Native Forge brewing, including mod IBrewingRecipe implementations and arbitrary input NBT. + * The catalogue is a finite display of known candidates; the execution path always asks the + * native ordered registry again for the actual input pair, never a flattened Ingredient sample. + */ +@Mod.EventBusSubscriber(modid = "useless_mod") +public final class BrewingRecipeAdapter implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final int INPUT_BOTTLES = 3; + private static final int MAX_BUILD_ATTEMPTS = 32; + private static final AtomicLong RELOAD_EPOCH = new AtomicLong(); + private volatile Cache cache; + private record Cache(RecipeManager manager, List sources, long epoch, + List> recipes) {} + + @SubscribeEvent public static void tagsUpdated(TagsUpdatedEvent event) { RELOAD_EPOCH.incrementAndGet(); } + @SubscribeEvent public static void datapackSync(OnDatapackSyncEvent event) { RELOAD_EPOCH.incrementAndGet(); } + + @Override public Class getRecipeClass() { return BrewingSyntheticRecipe.class; } + @Override public ItemStack getMoldItem() { return new ItemStack(Items.BREWING_STAND); } + + @Override public List> getGeneratedRecipes(Level level) { + if (level == null) return List.of(); + var sources = nativeSnapshot(); + long epoch = RELOAD_EPOCH.get(); + var cached = cache; + if (cached != null && cached.manager() == level.getRecipeManager() + && cached.epoch() == epoch && cached.sources().equals(sources)) return cached.recipes(); + synchronized (this) { + cached = cache; + if (cached != null && cached.manager() == level.getRecipeManager() + && cached.epoch() == epoch && cached.sources().equals(sources)) return cached.recipes(); + for (int attempt = 0; ; attempt++) { + try { + var result = createStaticRecipes(sources, level.registryAccess()); + cache = new Cache(level.getRecipeManager(), sources, epoch, result); + return result; + } catch (ConcurrentModificationException busy) { + if (attempt >= MAX_BUILD_ATTEMPTS - 1 || Thread.currentThread().isInterrupted()) throw busy; + Thread.yield(); + } + } + } + } + + private static List nativeSnapshot() { + for (int attempt = 0; ; attempt++) { + try { return List.copyOf(BrewingRecipeRegistry.getRecipes()); } + catch (ConcurrentModificationException busy) { + if (attempt >= MAX_BUILD_ATTEMPTS - 1 || Thread.currentThread().isInterrupted()) throw busy; + Thread.yield(); + } + } + } + + @Override public List convertAll( + RecipeHolder holder, Level level) { + return holder == null || holder.value() == null || holder.value().convertedRecipe() == null + ? List.of() : List.of(holder.value().convertedRecipe()); + } + + @Override public List> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) return List.of(); + return matchingRecipes(getGeneratedRecipes(level), mergedInputs); + } + + @Override public List> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + Map mergedKeys, @Nullable ItemStack mold, List actualInputs) { + if (level == null || !matchesMold(mold)) return List.of(); + if (actualInputs != null && !actualInputs.isEmpty()) { + // An empty native result must NOT fall back to an old/static recipe for the same + // item id. It may have been disabled, overridden or made NBT-dependent by a mod. + return findRuntimeRecipes(nativeSnapshot(), actualInputs, level.registryAccess()); + } + return matchingRecipes(getGeneratedRecipes(level), mergedInputs); + } + + static List> createStaticRecipes( + List sources, RegistryAccess registries) { + var inputs = new LinkedHashMap(); + var reagents = new LinkedHashMap(); + // Native vanilla potion stacks use PotionUtils / Potion NBT, not a 1.21 component. + for (var potion : BuiltInRegistries.POTION) { + for (var item : List.of(Items.POTION, Items.SPLASH_POTION, Items.LINGERING_POTION)) { + addCandidate(inputs, PotionUtils.setPotion(new ItemStack(item), potion)); + } + } + for (var source : sources) { + if (source instanceof BrewingRecipe simple) { + for (var stack : simple.getInput().getItems()) addCandidate(inputs, stack); + for (var stack : simple.getIngredient().getItems()) addCandidate(reagents, stack); + } + } + for (var item : BuiltInRegistries.ITEM) { + var probe = new ItemStack(item); + if (probe.isEmpty()) continue; + for (var source : sources) { + if (source.isInput(probe)) addCandidate(inputs, probe); + if (source.isIngredient(probe)) addCandidate(reagents, probe); + } + } + return createStaticRecipes(sources, registries, List.copyOf(inputs.values()), List.copyOf(reagents.values())); + } + + static List> createStaticRecipes( + List sources, RegistryAccess registries, + List inputs, List reagents) { + var recipes = new LinkedHashMap>(); + for (var input : inputs) for (var reagent : reagents) { + var converted = convertPair(sources, input, reagent); + if (converted != null) recipes.putIfAbsent(converted.id(), holder(converted)); + } + return List.copyOf(recipes.values()); + } + + private static void addCandidate(Map candidates, ItemStack stack) { + if (stack != null && !stack.isEmpty()) candidates.putIfAbsent(stackKey(stack), stack.copyWithCount(1)); + } + + static List> findRuntimeRecipes( + List sources, List actualInputs, RegistryAccess registries) { + var matches = new LinkedHashMap>(); + if (actualInputs == null) return List.of(); + for (var input : actualInputs) for (var reagent : actualInputs) { + var converted = convertPair(sources, input, reagent); + if (converted != null && ItemIngredientAllocator.matches(converted.inputs(), actualInputs, 1L)) + matches.putIfAbsent(converted.id(), holder(converted)); + } + return List.copyOf(matches.values()); + } + + @Nullable private static AdvancedAlloyFurnaceRecipe convertPair( + List sources, ItemStack input, ItemStack reagent) { + if (input == null || input.isEmpty() || reagent == null || reagent.isEmpty()) return null; + var inputProbe = input.copyWithCount(1); + var reagentProbe = reagent.copyWithCount(1); + ItemStack output = nativeOutput(sources, inputProbe, reagentProbe); + if (output == null || output.isEmpty() || output.getCount() <= 0) return null; + long count = (long) output.getCount() * INPUT_BOTTLES; + if (count > Integer.MAX_VALUE) return null; + var scaled = output.copyWithCount((int) count); + var id = new ResourceLocation("useless_mod", "brewing/native_" + hash( + stackKey(inputProbe) + "\n" + stackKey(reagentProbe) + "\n" + stackKey(scaled) + "|" + count)); + return new AdvancedAlloyFurnaceRecipe(id, + List.of(new CountedIngredient(DataComponentIngredient.of(true, inputProbe), INPUT_BOTTLES), + new CountedIngredient(DataComponentIngredient.of(true, reagentProbe), 1L)), + List.of(), List.of(scaled), List.of(), AdapterUtils.DEFAULT_ENERGY, + PotionBrewing.BREWING_TIME_SECONDS * 20, Ingredient.EMPTY, 0, + AdapterUtils.toMoldIngredient(new ItemStack(Items.BREWING_STAND)), AlloyFurnaceMode.NORMAL); + } + + /** Same first-nonempty-output rule and unit input count as BrewingRecipeRegistry.getOutput. + * A stable snapshot avoids querying a partly modified list on the catalogue worker thread. + */ + private static ItemStack nativeOutput(List sources, ItemStack input, ItemStack reagent) { + for (var source : sources) { + try { + var output = source.getOutput(input.copy(), reagent.copy()); + if (output != null && !output.isEmpty()) return output.copy(); + } catch (ConcurrentModificationException busy) { throw busy; } + catch (RuntimeException invalid) { + RateLimitedLogger.warn(LOGGER, "Cannot evaluate native brewing recipe {} for {} + {}; not falling through to a different recipe", + source.getClass().getName(), input, reagent, invalid); + return ItemStack.EMPTY; + } + } + return ItemStack.EMPTY; + } + + private static List> matchingRecipes( + List> candidates, Map available) { + if (available == null || available.isEmpty()) return List.of(); + return candidates.stream().filter(holder -> { + var required = new LinkedHashMap(); + for (var input : holder.value().convertedRecipe().inputs()) + AdapterUtils.mergeIngredient(required, input.ingredient(), input.count()); + return AdapterUtils.matchesRequired(available, required); + }).toList(); + } + + private static RecipeHolder holder(AdvancedAlloyFurnaceRecipe recipe) { + return new RecipeHolder<>(recipe.id(), new BrewingSyntheticRecipe(recipe)); + } + private static String stackKey(ItemStack stack) { + return BuiltInRegistries.ITEM.getKey(stack.getItem()) + "|" + NbtCanonical.snbt(stack.getTag()); + } + private static String hash(String value) { + try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8))); } + catch (NoSuchAlgorithmException unavailable) { throw new IllegalStateException("SHA-256 is unavailable", unavailable); } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/minecraft/BrewingSyntheticRecipe.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/minecraft/BrewingSyntheticRecipe.java new file mode 100644 index 00000000..2b3893f3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/minecraft/BrewingSyntheticRecipe.java @@ -0,0 +1,55 @@ +package com.sorrowmist.useless.content.recipe.adapters.minecraft; + +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.Container; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; + +/** Recipe holder payload for brewing data owned by PotionBrewing rather than RecipeManager. */ +public final class BrewingSyntheticRecipe implements Recipe { + private final AdvancedAlloyFurnaceRecipe convertedRecipe; + + public BrewingSyntheticRecipe(AdvancedAlloyFurnaceRecipe convertedRecipe) { + this.convertedRecipe = convertedRecipe; + } + + public AdvancedAlloyFurnaceRecipe convertedRecipe() { + return convertedRecipe; + } + + @Override + public net.minecraft.resources.ResourceLocation getId() { return convertedRecipe.id(); } + + @Override + public boolean matches(Container input, Level level) { + return convertedRecipe != null && convertedRecipe.matches(input, level); + } + + @Override + public ItemStack assemble(Container input, net.minecraft.core.RegistryAccess registries) { + return convertedRecipe == null ? ItemStack.EMPTY : convertedRecipe.assemble(input, registries); + } + + @Override + public boolean canCraftInDimensions(int width, int height) { + return convertedRecipe != null && convertedRecipe.canCraftInDimensions(width, height); + } + + @Override + public ItemStack getResultItem(net.minecraft.core.RegistryAccess registries) { + return convertedRecipe == null ? ItemStack.EMPTY : convertedRecipe.getResultItem(registries); + } + + @Override + public RecipeSerializer getSerializer() { + return convertedRecipe == null ? null : convertedRecipe.getSerializer(); + } + + @Override + public RecipeType getType() { + return convertedRecipe == null ? null : convertedRecipe.getType(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/minecraft/CraftingRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/minecraft/CraftingRecipeAdapter.java new file mode 100644 index 00000000..f9630413 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/minecraft/CraftingRecipeAdapter.java @@ -0,0 +1,967 @@ +package com.sorrowmist.useless.content.recipe.adapters.minecraft; + +import appeng.api.behaviors.ContainerItemStrategies; +import appeng.api.stacks.AEFluidKey; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.AEKeyType; +import appeng.api.stacks.GenericStack; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.FluidIngredientAllocator; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.ItemIngredientAllocator; +import com.sorrowmist.useless.content.recipe.adapters.extendedcrafting.ExtendedCraftingAdapterUtils; +import net.minecraft.world.item.BucketItem; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.MilkBucketItem; +import net.minecraft.world.inventory.CraftingContainer; +import net.minecraft.world.item.crafting.CraftingRecipe; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.item.crafting.ShapedRecipe; +import net.minecraft.world.item.crafting.ShapelessRecipe; +import net.minecraft.world.level.Level; +import com.sorrowmist.useless.compat.forge1201.recipe.DataComponentIngredient; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.CompoundFluidIngredient; +import com.sorrowmist.useless.compat.forge1201.fluid.DataComponentFluidIngredient; +import com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** Converts standard shaped and shapeless crafting-table recipes. */ +public final class CraftingRecipeAdapter implements IRecipeAdapter { + private static final int CRAFTING_GRID_SIZE = 3; + private static final int CRAFTING_GRID_SLOTS = CRAFTING_GRID_SIZE * CRAFTING_GRID_SIZE; + + @Override + public Class getRecipeClass() { + return CraftingRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(Items.CRAFTING_TABLE); + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + return mold != null && !mold.isEmpty() && mold.is(Items.CRAFTING_TABLE); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || !isSupported(holder.value())) { + return List.of(); + } + + Converted converted = convertData(holder.value(), level); + if (converted == null) { + return List.of(); + } + + return List.of(createRecipe(holder, converted)); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level, List actualInputs) { + if (holder == null || !isSupported(holder.value())) { + return List.of(); + } + if (actualInputs == null || actualInputs.isEmpty()) { + return convertAll(holder, level); + } + + Converted converted = convertRuntimeData(holder.value(), level, actualInputs); + if (converted == null) { + return List.of(); + } + + return List.of(createRecipe(holder, converted)); + } + + @Override + public List> findMatchingRecipes( + Level level, + Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + return findMatchingRecipes(level, mergedInputs, mergedFluids, Map.of(), mold, List.of()); + } + + @Override + public List> findMatchingRecipes( + Level level, + Map mergedInputs, + Map mergedFluids, + Map mergedKeys, + @Nullable ItemStack mold, + List actualInputs) { + if (level == null || !matchesMold(mold) + || ((mergedInputs == null || mergedInputs.isEmpty()) + && (mergedFluids == null || mergedFluids.isEmpty()) + && (actualInputs == null || actualInputs.isEmpty()) + && (mold == null || mold.isEmpty()))) { + return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, RecipeType.CRAFTING)) { + if (!isSupported(holder.value())) { + continue; + } + if (actualInputs != null && !actualInputs.isEmpty()) { + if (!matchesSourceInputs(holder.value(), actualInputs)) { + continue; + } + Converted converted = convertRuntimeData(holder.value(), level, actualInputs); + if (converted != null + && ItemIngredientAllocator.matches(converted.itemInputs(), actualInputs, 1L) + && matchesFluids(mergedFluids, converted.fluidInputs())) { + matches.add(holder); + } + continue; + } + + // Static lookup is used for recipe discovery and JEI/catalogue views. It must not + // require a result or a deterministic output remainder: those values may only be + // available after the real machine inputs are supplied. Keep only the source input + // predicates here, including a bucket-to-fluid substitution when that substitution + // is independently deterministic. + InputRequirements requirements = staticInputRequirements(holder.value()); + if (requirements != null + && matchesItems(mergedInputs, requirements.itemInputs()) + && matchesFluids(mergedFluids, requirements.fluidInputs())) { + matches.add(holder); + } + } + return matches; + } + + private static boolean isSupported(@Nullable CraftingRecipe recipe) { + if (recipe == null) { + return false; + } + if (recipe instanceof ShapedRecipe || recipe instanceof ShapelessRecipe) { + return true; + } + try { + return !recipe.getIngredients().isEmpty(); + } catch (RuntimeException exception) { + return false; + } + } + + private static AdvancedAlloyFurnaceRecipe createRecipe( + RecipeHolder holder, Converted converted) { + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + converted.itemInputs(), + converted.fluidInputs(), + List.of(), + converted.outputs(), + List.of(), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + AdapterUtils.DEFAULT_PROCESS_TIME, + Ingredient.EMPTY, + 0, + converted.molds(), + AlloyFurnaceMode.NORMAL + ); + } + + @Nullable + private static Converted convertData(CraftingRecipe source, @Nullable Level level) { + List ingredientSlots = ingredientSlots(source); + if (ingredientSlots == null || ingredientSlots.isEmpty()) { + return null; + } + + ItemStack result = ExtendedCraftingAdapterUtils.copyResult(source); + if (result.isEmpty() || result.getCount() <= 0) { + return null; + } + + List canonicalStacks = canonicalStacks(ingredientSlots); + if (canonicalStacks == null) { + return null; + } + + if (!usesBaseCraftingAssembly(source) + && !hasStableAssemblyResult(source, ingredientSlots, result, level)) { + return null; + } + + Optional analysis = analyzeRemainders( + source, ingredientSlots, canonicalStacks); + if (analysis.isEmpty()) { + return null; + } + + return convertData(source, ingredientSlots, canonicalStacks, result, + analysis.get().remainders()); + } + + private static boolean isVanillaCraftingRecipe(CraftingRecipe source) { + return source instanceof ShapedRecipe || source instanceof ShapelessRecipe; + } + + /** + * A subclass may add input-dependent behavior while still inheriting the vanilla fixed-result + * assembly implementation. Those recipes can use the bounded per-slot remainder analysis; + * subclasses which override assemble() must be proven with concrete combinations instead. + */ + private static boolean usesBaseCraftingAssembly(CraftingRecipe source) { + Class baseClass = source instanceof ShapedRecipe + ? ShapedRecipe.class + : source instanceof ShapelessRecipe ? ShapelessRecipe.class : null; + if (baseClass == null) { + return false; + } + try { + return source.getClass() + .getMethod("assemble", CraftingContainer.class, + net.minecraft.core.HolderLookup.Provider.class) + .getDeclaringClass() == baseClass; + } catch (ReflectiveOperationException | RuntimeException exception) { + return false; + } + } + + private static boolean hasStableAssemblyResult( + CraftingRecipe source, List ingredientSlots, + ItemStack expected, @Nullable Level level) { + Optional> assembled = + ExtendedCraftingAdapterUtils.deterministicRemaindersBySlot( + ingredientSlots, + Set.of(), + stacks -> craftingInput(source, stacks), + input -> { + ItemStack result; + try { + result = source.assemble(input, + level == null ? null : level.registryAccess()); + } catch (RuntimeException exception) { + return null; + } + return result == null ? null : List.of(result); + }); + if (assembled.isEmpty() || assembled.get().isEmpty()) { + return false; + } + ItemStack actual = assembled.get().get(0); + return !actual.isEmpty() + && expected.getCount() == actual.getCount() + && ItemStack.isSameItemSameTags(expected, actual); + } + + private static Optional analyzeRemainders( + CraftingRecipe source, List ingredientSlots, + List canonicalStacks) { + if (isVanillaCraftingRecipe(source) && usesBaseCraftingAssembly(source)) { + return analyzeVanillaRemainders(source, ingredientSlots, canonicalStacks); + } + + Optional> variants = + ExtendedCraftingAdapterUtils.remainderVariantsBySlot( + ingredientSlots, + Set.of(), + stacks -> craftingInput(source, stacks), + source::getRemainingItems); + return variants.flatMap(values -> classifyRemainders(values, ingredientSlots)); + } + + /** + * Vanilla crafting recipes only derive remainders from the item in the corresponding slot. + * Probe one slot at a time so a large tag does not create a Cartesian product. + */ + private static Optional analyzeVanillaRemainders( + CraftingRecipe source, List ingredientSlots, + List canonicalStacks) { + List probes = new ArrayList<>(); + for (int slot = 0; slot < ingredientSlots.size(); slot++) { + Ingredient ingredient = ingredientSlots.get(slot); + if (ingredient == null || ingredient.isEmpty()) { + continue; + } + + ItemStack[] candidates; + try { + candidates = ingredient.getItems(); + } catch (RuntimeException exception) { + return Optional.empty(); + } + if (candidates == null || candidates.length == 0) { + candidates = new ItemStack[]{ItemStack.EMPTY}; + } + + for (ItemStack candidate : candidates) { + if (candidate == null || (!candidate.isEmpty() && candidate.getCount() <= 0)) { + return Optional.empty(); + } + List inputStacks = copyStacks(canonicalStacks); + inputStacks.set(slot, candidate.copy()); + List remainder; + try { + CraftingContainer input = craftingInput(source, inputStacks); + if (input == null) { + return Optional.empty(); + } + remainder = source.getRemainingItems(input); + } catch (RuntimeException exception) { + return Optional.empty(); + } + List normalized = normalizeRemainders(remainder, ingredientSlots.size()); + if (normalized == null) { + return Optional.empty(); + } + probes.add(new ExtendedCraftingAdapterUtils.RemainderVariant( + inputStacks, normalized)); + } + } + return classifyRemainders(probes, ingredientSlots); + } + + private static Optional classifyRemainders( + List variants, + List ingredientSlots) { + if (variants == null || variants.isEmpty()) { + return Optional.empty(); + } + + List representative = new ArrayList<>(ingredientSlots.size()); + for (int slot = 0; slot < ingredientSlots.size(); slot++) { + representative.add(ItemStack.EMPTY); + } + Set moldSlots = new LinkedHashSet<>(); + Set fluidSlots = new LinkedHashSet<>(); + + for (int slot = 0; slot < ingredientSlots.size(); slot++) { + Ingredient ingredient = ingredientSlots.get(slot); + if (ingredient == null || ingredient.isEmpty()) { + continue; + } + + RemainderKind kind = null; + ItemStack expectedOutput = null; + ItemStack firstRemainder = ItemStack.EMPTY; + for (ExtendedCraftingAdapterUtils.RemainderVariant variant : variants) { + if (slot >= variant.inputs().size() || slot >= variant.remainders().size()) { + return Optional.empty(); + } + ItemStack original = variant.inputs().get(slot); + ItemStack remainder = variant.remainders().get(slot); + RemainderKind current = classifyRemainder(ingredient, original, remainder); + if (kind == null) { + kind = current; + } else if (kind != current) { + // A tag cannot be represented by one mold/input when only some candidates + // are reusable. The runtime path may still resolve a concrete candidate. + return Optional.empty(); + } + + if (!remainder.isEmpty() && firstRemainder.isEmpty()) { + firstRemainder = remainder.copy(); + } + if (current == RemainderKind.OUTPUT) { + if (expectedOutput == null) { + expectedOutput = remainder.copy(); + } else if (!sameStack(expectedOutput, remainder)) { + return Optional.empty(); + } + } + } + + if (kind == RemainderKind.MOLD) { + moldSlots.add(slot); + } else if (kind == RemainderKind.FLUID) { + fluidSlots.add(slot); + } + if (kind == RemainderKind.MOLD || kind == RemainderKind.FLUID) { + representative.set(slot, firstRemainder); + } else if (kind == RemainderKind.OUTPUT && expectedOutput != null) { + representative.set(slot, expectedOutput); + } + } + return Optional.of(new RemainderAnalysis( + List.copyOf(representative), Set.copyOf(moldSlots), Set.copyOf(fluidSlots))); + } + + private static RemainderKind classifyRemainder( + Ingredient ingredient, @Nullable ItemStack original, @Nullable ItemStack remainder) { + if (remainder == null || remainder.isEmpty()) { + return RemainderKind.CONSUMED; + } + if (fluidSubstitute(ingredient, remainder).isPresent()) { + return RemainderKind.FLUID; + } + if (isReusableRemainder(original, remainder)) { + return RemainderKind.MOLD; + } + return RemainderKind.OUTPUT; + } + + @Nullable + private static List normalizeRemainders( + @Nullable List remainder, int slotCount) { + if (remainder == null) { + return null; + } + List normalized = new ArrayList<>(slotCount); + for (int slot = 0; slot < slotCount; slot++) { + if (slot >= remainder.size()) { + normalized.add(ItemStack.EMPTY); + continue; + } + ItemStack stack = remainder.get(slot); + if (stack == null || stack.isEmpty()) { + normalized.add(ItemStack.EMPTY); + continue; + } + if (stack.getCount() <= 0) { + return null; + } + normalized.add(stack.copy()); + } + for (int slot = slotCount; slot < remainder.size(); slot++) { + ItemStack stack = remainder.get(slot); + if (stack != null && !stack.isEmpty() && stack.getCount() > 0) { + return null; + } + } + return List.copyOf(normalized); + } + + private static List copyStacks(List stacks) { + List result = new ArrayList<>(stacks.size()); + for (ItemStack stack : stacks) { + result.add(stack == null ? ItemStack.EMPTY : stack.copy()); + } + return result; + } + + private static boolean sameStack(ItemStack left, ItemStack right) { + return left != null && right != null + && left.getCount() == right.getCount() + && ItemStack.isSameItemSameTags(left, right); + } + + @Nullable + private static Converted convertRuntimeData( + CraftingRecipe source, @Nullable Level level, List actualInputs) { + List ingredientSlots = ingredientSlots(source); + if (ingredientSlots == null || ingredientSlots.isEmpty()) { + return null; + } + + List craftingStacks = actualCraftingStacks(ingredientSlots, actualInputs); + if (craftingStacks == null) { + return null; + } + + CraftingContainer input = craftingInput(source, craftingStacks); + if (input == null) { + return null; + } + + try { + if (!source.matches(input, level)) { + return null; + } + ItemStack result = source.assemble(input, level == null ? null : level.registryAccess()); + if (result == null || result.isEmpty() || result.getCount() <= 0) { + return null; + } + List remainders = source.getRemainingItems(input); + if (remainders == null || remainders.size() != ingredientSlots.size()) { + return null; + } + return convertData(source, ingredientSlots, craftingStacks, result.copy(), remainders); + } catch (RuntimeException exception) { + return null; + } + } + + private static Converted convertData( + CraftingRecipe source, + List ingredientSlots, + List concreteStacks, + ItemStack result, + List remainders) { + if (result == null || result.isEmpty() || result.getCount() <= 0 + || remainders == null || remainders.size() != ingredientSlots.size()) { + return null; + } + + List outputs = new ArrayList<>(); + if (!ExtendedCraftingAdapterUtils.mergeOutput(outputs, result)) { + return null; + } + + Set fluidSlots = new LinkedHashSet<>(); + List fluidInputs = new ArrayList<>(); + for (int slot = 0; slot < ingredientSlots.size(); slot++) { + Optional fluid = fluidSubstitute( + ingredientSlots.get(slot), remainders.get(slot)); + if (fluid.isEmpty()) { + continue; + } + SizedFluidIngredient stack = fluid.get(); + fluidSlots.add(slot); + mergeFluidIngredient(fluidInputs, stack); + } + + Map itemAmounts = new LinkedHashMap<>(); + List remainderMolds = new ArrayList<>(); + for (int slot = 0; slot < ingredientSlots.size(); slot++) { + Ingredient ingredient = ingredientSlots.get(slot); + ItemStack remainder = remainders.get(slot); + if (remainder == null) { + remainder = ItemStack.EMPTY; + } + if (fluidSlots.contains(slot)) { + // The filled bucket is represented by the fluid input, while the stable empty + // bucket remains a machine mold so the conversion does not create a free item. + remainderMolds.add(stackIngredient(remainder)); + continue; + } + + if (remainder.isEmpty()) { + if (ingredient != null && !ingredient.isEmpty()) { + AdapterUtils.mergeIngredient(itemAmounts, ingredient, 1L); + } + continue; + } + + ItemStack concrete = concreteStacks.get(slot); + if (isReusableRemainder(concrete, remainder)) { + // A source item which survives crafting, either unchanged or with only damage + // consumed, is a reusable mold. Keep the original Ingredient so tags and custom + // ingredient predicates remain available to the multiblock mold hub. + remainderMolds.add(ingredient); + } else { + if (!ExtendedCraftingAdapterUtils.mergeOutput(outputs, remainder)) { + return null; + } + } + + if (!isReusableRemainder(concrete, remainder) + && ingredient != null && !ingredient.isEmpty()) { + AdapterUtils.mergeIngredient(itemAmounts, ingredient, 1L); + } + } + + List molds = new ArrayList<>(); + molds.add(AdapterUtils.toMoldIngredient(new ItemStack(Items.CRAFTING_TABLE))); + molds.addAll(remainderMolds); + if (itemAmounts.isEmpty() && fluidInputs.isEmpty() && remainderMolds.isEmpty()) { + return null; + } + + List itemInputs = itemAmounts.entrySet().stream() + .map(entry -> new CountedIngredient(entry.getKey(), entry.getValue())) + .toList(); + return new Converted(itemInputs, List.copyOf(fluidInputs), List.copyOf(outputs), List.copyOf(molds)); + } + + private static boolean matchesSourceInputs(CraftingRecipe source, List actualInputs) { + List slots = ingredientSlots(source); + return slots != null && actualCraftingStacks(slots, actualInputs) != null; + } + + @Nullable + private static InputRequirements staticInputRequirements(CraftingRecipe source) { + List ingredientSlots = ingredientSlots(source); + if (ingredientSlots == null || ingredientSlots.isEmpty()) { + return null; + } + + List canonicalStacks = canonicalStacks(ingredientSlots); + if (canonicalStacks == null) { + return null; + } + Optional analysis = analyzeRemainders( + source, ingredientSlots, canonicalStacks); + if (analysis.isEmpty()) { + return null; + } + List remainders = analysis.get().remainders(); + + Set fluidSlots = analysis.get().fluidSlots(); + Set moldSlots = analysis.get().moldSlots(); + List fluidInputs = new ArrayList<>(); + for (int slot : fluidSlots) { + Optional fluid = fluidSubstitute( + ingredientSlots.get(slot), remainders.get(slot)); + if (fluid.isEmpty()) { + return null; + } + mergeFluidIngredient(fluidInputs, fluid.get()); + } + + Map itemAmounts = new LinkedHashMap<>(); + for (int slot = 0; slot < ingredientSlots.size(); slot++) { + if (fluidSlots.contains(slot) || moldSlots.contains(slot)) { + continue; + } + Ingredient ingredient = ingredientSlots.get(slot); + if (ingredient != null && !ingredient.isEmpty()) { + AdapterUtils.mergeIngredient(itemAmounts, ingredient, 1L); + } + } + + if (itemAmounts.isEmpty() && fluidInputs.isEmpty()) { + return null; + } + List itemInputs = itemAmounts.entrySet().stream() + .map(entry -> new CountedIngredient(entry.getKey(), entry.getValue())) + .toList(); + return new InputRequirements(itemInputs, List.copyOf(fluidInputs)); + } + + @Nullable + private static List ingredientSlots(CraftingRecipe source) { + List ingredients; + try { + ingredients = new ArrayList<>(source.getIngredients()); + } catch (RuntimeException exception) { + return null; + } + if (ingredients.isEmpty() || ingredients.size() > CRAFTING_GRID_SLOTS) { + return null; + } + + int[] dimensions = craftingDimensions(source, ingredients.size()); + if (dimensions == null) { + return null; + } + int slots = dimensions[0] * dimensions[1]; + if (ingredients.size() > slots) { + return null; + } + while (ingredients.size() < slots) { + ingredients.add(Ingredient.EMPTY); + } + return List.copyOf(ingredients); + } + + @Nullable + private static List canonicalStacks(List ingredients) { + List stacks = new ArrayList<>(ingredients.size()); + boolean foundInput = false; + for (Ingredient ingredient : ingredients) { + if (ingredient == null || ingredient.isEmpty()) { + stacks.add(ItemStack.EMPTY); + continue; + } + ItemStack[] candidates; + try { + candidates = ingredient.getItems(); + } catch (RuntimeException exception) { + return null; + } + if (candidates == null || candidates.length == 0) { + // A tag can be temporarily unresolved while the recipe index is built. The + // original ingredient remains the actual requirement; an empty probe is enough + // for ordinary recipes which do not inspect the concrete input item. + stacks.add(ItemStack.EMPTY); + foundInput = true; + continue; + } + if (candidates[0] == null || candidates[0].isEmpty()) { + return null; + } + stacks.add(candidates[0].copyWithCount(1)); + foundInput = true; + } + return foundInput ? List.copyOf(stacks) : null; + } + + @Nullable + private static List actualCraftingStacks( + List ingredientSlots, List actualInputs) { + if (ingredientSlots == null || actualInputs == null) { + return null; + } + + List requiredSlots = new ArrayList<>(); + for (int slot = 0; slot < ingredientSlots.size(); slot++) { + Ingredient ingredient = ingredientSlots.get(slot); + if (ingredient != null && !ingredient.isEmpty()) { + requiredSlots.add(slot); + } + } + if (requiredSlots.isEmpty()) { + return null; + } + + List units = new ArrayList<>(); + for (ItemStack stack : actualInputs) { + if (stack == null || stack.isEmpty() || stack.getCount() <= 0) { + continue; + } + int copies = Math.min(stack.getCount(), requiredSlots.size()); + for (int copy = 0; copy < copies; copy++) { + units.add(stack.copyWithCount(1)); + } + } + if (units.size() < requiredSlots.size()) { + return null; + } + + List assigned = new ArrayList<>(ingredientSlots.size()); + for (int slot = 0; slot < ingredientSlots.size(); slot++) { + assigned.add(ItemStack.EMPTY); + } + boolean[] used = new boolean[units.size()]; + return assignCraftingStacks(ingredientSlots, requiredSlots, units, 0, used, assigned) + ? List.copyOf(assigned) + : null; + } + + private static boolean assignCraftingStacks( + List ingredientSlots, + List requiredSlots, + List units, + int position, + boolean[] used, + List assigned) { + if (position >= requiredSlots.size()) { + return true; + } + + int slot = requiredSlots.get(position); + Ingredient ingredient = ingredientSlots.get(slot); + for (int unit = 0; unit < units.size(); unit++) { + if (used[unit]) { + continue; + } + try { + if (!ingredient.test(units.get(unit))) { + continue; + } + } catch (RuntimeException exception) { + return false; + } + used[unit] = true; + assigned.set(slot, units.get(unit)); + if (assignCraftingStacks(ingredientSlots, requiredSlots, units, position + 1, used, assigned)) { + return true; + } + assigned.set(slot, ItemStack.EMPTY); + used[unit] = false; + } + return false; + } + + @Nullable + private static CraftingContainer craftingInput(CraftingRecipe source, List stacks) { + if (source == null || stacks == null) { + return null; + } + int[] dimensions = craftingDimensions(source, stacks.size()); + if (dimensions == null) { + return null; + } + int slotCount = dimensions[0] * dimensions[1]; + if (stacks.size() > slotCount) { + return null; + } + List normalized = new ArrayList<>(stacks); + while (normalized.size() < slotCount) { + normalized.add(ItemStack.EMPTY); + } + return com.sorrowmist.useless.compat.forge1201.recipe.CraftingInputs.of(dimensions[0], dimensions[1], normalized); + } + + @Nullable + private static int[] craftingDimensions(CraftingRecipe source, int ingredientCount) { + if (source instanceof ShapedRecipe shaped) { + int width = shaped.getWidth(); + int height = shaped.getHeight(); + if (width <= 0 || height <= 0 || (long) width * height > CRAFTING_GRID_SLOTS) { + return null; + } + return new int[]{width, height}; + } + + int count = Math.max(1, ingredientCount); + int width = Math.min(CRAFTING_GRID_SIZE, count); + int height = (count + width - 1) / width; + return (long) width * height > CRAFTING_GRID_SLOTS ? null : new int[]{width, height}; + } + + private static Optional fluidSubstitute( + Ingredient ingredient, @Nullable ItemStack remainder) { + if (ingredient == null || ingredient.isEmpty() || remainder == null + || remainder.getCount() != 1 || !remainder.is(Items.BUCKET)) { + return Optional.empty(); + } + + ItemStack[] candidates; + try { + candidates = ingredient.getItems(); + } catch (RuntimeException exception) { + return Optional.empty(); + } + if (candidates == null || candidates.length == 0) { + return Optional.empty(); + } + + List ingredients = new ArrayList<>(); + int amount = -1; + for (ItemStack candidateStack : candidates) { + if (candidateStack == null || candidateStack.isEmpty()) { + return Optional.empty(); + } + ItemStack candidate = candidateStack.copyWithCount(1); + if (!(candidate.getItem() instanceof BucketItem) + && !(candidate.getItem() instanceof MilkBucketItem)) { + return Optional.empty(); + } + GenericStack contained = ContainerItemStrategies.getContainedStack(candidate, AEKeyType.fluids()); + if (contained == null || !(contained.what() instanceof AEFluidKey fluidKey) + || contained.amount() <= 0L || contained.amount() > Integer.MAX_VALUE) { + return Optional.empty(); + } + if (amount < 0) { + amount = (int) contained.amount(); + } else if (amount != contained.amount()) { + return Optional.empty(); + } + FluidStack fluid = fluidKey.toStack(amount); + FluidIngredient fluidIngredient = !fluid.hasTag() + ? FluidIngredient.single(fluid) + : DataComponentFluidIngredient.of(true, fluid); + if (ingredients.stream().noneMatch(fluidIngredient::equals)) { + ingredients.add(fluidIngredient); + } + } + if (ingredients.isEmpty() || amount <= 0) { + return Optional.empty(); + } + return Optional.of(new SizedFluidIngredient(CompoundFluidIngredient.of(ingredients), amount)); + } + + private static boolean isReusableRemainder( + @Nullable ItemStack original, @Nullable ItemStack remainder) { + if (original == null || original.isEmpty() || remainder == null || remainder.isEmpty() + || original.getCount() != remainder.getCount()) { + return false; + } + return ItemStack.isSameItemSameTags(original, remainder) + || isDurabilityRemainder(original, remainder); + } + + private static boolean isDurabilityRemainder( + @Nullable ItemStack original, @Nullable ItemStack remainder) { + if (original == null || original.isEmpty() || remainder == null || remainder.isEmpty() + || original.getCount() != remainder.getCount() + || !original.isDamageableItem() || !remainder.isDamageableItem() + || remainder.getDamageValue() <= original.getDamageValue()) { + return false; + } + + ItemStack originalWithoutDamage = original.copy(); + ItemStack remainderWithoutDamage = remainder.copy(); + originalWithoutDamage.setDamageValue(0); + remainderWithoutDamage.setDamageValue(0); + return ItemStack.isSameItemSameTags(originalWithoutDamage, remainderWithoutDamage); + } + + private static Ingredient stackIngredient(ItemStack stack) { + return DataComponentIngredient.of(true, stack.copyWithCount(1)); + } + + private static boolean matchesItems( + @Nullable Map available, List required) { + if (required.isEmpty()) { + return true; + } + if (available == null || available.isEmpty()) { + return false; + } + Map requiredCounts = new LinkedHashMap<>(); + for (CountedIngredient ingredient : required) { + AdapterUtils.mergeIngredient(requiredCounts, ingredient.ingredient(), ingredient.count()); + } + return AdapterUtils.matchesRequired(available, requiredCounts); + } + + private static boolean matchesFluids( + @Nullable Map available, List required) { + if (required.isEmpty()) { + return true; + } + if (available == null || available.isEmpty()) { + return false; + } + return FluidIngredientAllocator.matches(required, available, 1L); + } + + private static long saturatingAdd(long left, long right) { + if (right <= 0L) { + return left; + } + return left >= Long.MAX_VALUE - right ? Long.MAX_VALUE : left + right; + } + + private record Converted( + List itemInputs, + List fluidInputs, + List outputs, + List molds) { + } + + private record InputRequirements( + List itemInputs, + List fluidInputs) { + } + + private enum RemainderKind { + CONSUMED, + MOLD, + FLUID, + OUTPUT + } + + private record RemainderAnalysis( + List remainders, + Set moldSlots, + Set fluidSlots) { + } + + private static void mergeFluidIngredient( + List target, SizedFluidIngredient ingredient) { + for (int i = 0; i < target.size(); i++) { + SizedFluidIngredient existing = target.get(i); + if (!existing.ingredient().equals(ingredient.ingredient())) { + continue; + } + long amount = (long) existing.amount() + ingredient.amount(); + if (amount > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Crafting fluid input amount exceeds integer range"); + } + target.set(i, new SizedFluidIngredient(existing.ingredient(), (int) amount)); + return; + } + target.add(ingredient); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/minecraft/FishingLootRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/minecraft/FishingLootRecipeAdapter.java new file mode 100644 index 00000000..f4b47d1a --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/minecraft/FishingLootRecipeAdapter.java @@ -0,0 +1,678 @@ +package com.sorrowmist.useless.content.recipe.adapters.minecraft; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.FluidIngredientAllocator; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.ItemIngredientAllocator; +import com.sorrowmist.useless.content.recipe.LongSizedFluidIngredient; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.util.GsonHelper; +import net.minecraft.world.item.Equipable; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluids; +import com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** + * 把原版钓鱼战利品表(minecraft:gameplay/fishing)转换成确定性的合金炉配方。 + * + *

原版战利品表使用权重随机、条件与函数,而合金炉配方必须是无随机、无世界状态的静态数据, + * 因此这里只收集「普通物品条目」,忽略带条件或带函数的条目:

+ *
    + *
  • 附魔书({@code minecraft:enchanted_book} + {@code enchant_randomly})被排除;
  • + *
  • 装备(有耐久值的工具/武器/护甲、可穿戴物品)被排除;
  • + *
  • 带 {@code conditions} 的条目被排除,保证客户端与服务端生成完全一致的配方集合。
  • + *
+ * + *

整张表按顶层 pool 拆分:每个 pool 生成一个配方,产物为该 pool 全部可转换条目的合集, + * 消耗水,并以钓鱼竿作为模具。配方 id 只由产物内容决定,不依赖集合迭代顺序。

+ */ +public final class FishingLootRecipeAdapter implements IRecipeAdapter { + /** 原版钓鱼战利品表:data/minecraft/loot_table/gameplay/fishing.json。 */ + private static final ResourceLocation FISHING_TABLE_ID = + new ResourceLocation("gameplay/fishing"); + private static final int WATER_PER_OUTPUT = 1_000; + /** 战利品表可以引用子表,限制递归深度避免异常数据导致栈溢出。 */ + private static final int MAX_TABLE_DEPTH = 8; + + @Override + public String sourceId() { + return RecipeSourceIds.MINECRAFT_FISHING; + } + + @Override + public Class getRecipeClass() { + return FishingLootSyntheticRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(Items.FISHING_ROD); + } + + @Override + public List> getGeneratedRecipes(Level level) { + if (level == null) { + return List.of(); + } + + ResourceManager resourceManager = resourceManager(level); + if (resourceManager == null) { + // 远程客户端读不到战利品表资源,此时不生成任何配方,避免客户端与服务端不一致。 + return List.of(); + } + + // 物品标签在客户端由服务端标签包重绑,这里只用注册表本身,不依赖标签。 + Registry itemRegistry = BuiltInRegistries.ITEM; + List> recipes = new ArrayList<>(); + Set usedIds = new HashSet<>(); + + // 原版自己的钓鱼表。 + generateFromTable(itemRegistry, resourceManager, FISHING_TABLE_ID, "vanilla_", + recipes, usedIds); + // 其他模组放在自己命名空间下的钓鱼表。它们多数通过运行时事件注入原版表, + // 那种注入读不到,但这些表本身是数据包 JSON,可以静态读取。 + generateFromExternalTables(itemRegistry, resourceManager, recipes, usedIds); + + return List.copyOf(recipes); + } + + /** + * 读取一张钓鱼表,按子表分组后生成配方。 + * + * @param idPrefix 配方 id 前缀,用于区分不同模组里同名的子表分组(例如都叫 treasure) + */ + private static void generateFromTable(Registry itemRegistry, + ResourceManager resourceManager, + ResourceLocation tableId, + String idPrefix, + List> recipes, + Set usedIds) { + JsonObject table = readLootTable(resourceManager, tableId); + if (table == null) { + return; + } + + JsonArray pools = asArray(table.get("pools")); + if (pools == null) { + return; + } + + for (JsonElement poolElement : pools) { + JsonObject pool = poolElement != null && poolElement.isJsonObject() + ? poolElement.getAsJsonObject() : null; + if (pool == null) { + continue; + } + + // 原版表以顶层 pool 包含 fish/junk/treasure 三个子表,必须按子表拆分, + // 否则鱼类、杂物与宝藏会合并为单个产物超过二十种的配方。 + Map> grouped = new LinkedHashMap<>(); + // 直接写在顶层 pool 里的物品归入 misc,子表引用会各自带上自己的分组名。 + collectGroupedOutputs(itemRegistry, resourceManager, pool.get("entries"), + grouped, 0, "misc"); + appendGroupedRecipes(grouped, idPrefix, recipes, usedIds); + } + } + + /** 把分组结果逐个转成配方并追加到结果列表。 */ + private static void appendGroupedRecipes(Map> grouped, + String idPrefix, + List> recipes, + Set usedIds) { + for (Map.Entry> group : grouped.entrySet()) { + List outputs = group.getValue(); + if (outputs.isEmpty()) { + continue; + } + + ResourceLocation recipeId = nextRecipeId(idPrefix + group.getKey(), outputs, usedIds); + if (recipeId == null) { + continue; + } + AdvancedAlloyFurnaceRecipe converted = convertOutputs(recipeId, outputs); + if (converted != null) { + recipes.add(new RecipeHolder<>(converted.id(), + new FishingLootSyntheticRecipe(converted))); + } + } + } + + /** + * 读取其他模组放在自己命名空间下的钓鱼表。 + * + *

不少模组通过 {@code LootTableLoadEvent} 在运行时把产物注入原版钓鱼表,那种注入只发生在 + * 逻辑服务端、也没有对应的 JSON,静态读取永远看不到。但这些模组通常会把表本身以数据包 JSON + * 的形式发布(例如 {@code aquaculture:gameplay/fishing/fish}),这部分是可以静态读取的, + * 因此这里把它们一并纳入转换。

+ * + *

只取「根表」:被同一命名空间下其他钓鱼表引用过的表属于子表,根表递归时已经读到, + * 再单独生成一次只会得到重复配方。

+ */ + private static void generateFromExternalTables(Registry itemRegistry, + ResourceManager resourceManager, + List> recipes, + Set usedIds) { + Map candidates = new LinkedHashMap<>(); + try { + Map found = resourceManager.listResources( + "loot_table/gameplay/fishing", + id -> !id.getNamespace().equals("minecraft") + && id.getPath().endsWith(".json")); + for (ResourceLocation resourceId : found.keySet()) { + ResourceLocation tableId = toTableId(resourceId); + if (tableId == null) { + continue; + } + JsonObject table = readLootTable(resourceManager, tableId); + if (table != null) { + candidates.put(tableId, table); + } + } + } catch (RuntimeException ignored) { + return; + } + + if (candidates.isEmpty()) { + return; + } + + // 先扫出所有被引用的子表,剩下的才是根表。 + Set referenced = new HashSet<>(); + for (JsonObject table : candidates.values()) { + collectReferencedTables(table, referenced); + } + + for (ResourceLocation tableId : candidates.keySet()) { + if (referenced.contains(tableId)) { + continue; + } + generateFromTable(itemRegistry, resourceManager, tableId, + tableId.getNamespace() + "_", recipes, usedIds); + } + } + + /** 把 {@code aquaculture:loot_table/gameplay/fishing/fish.json} 还原成表 id。 */ + @Nullable + private static ResourceLocation toTableId(ResourceLocation resourceId) { + String path = resourceId.getPath(); + String prefix = "loot_table/"; + String suffix = ".json"; + if (!path.startsWith(prefix) || !path.endsWith(suffix)) { + return null; + } + return ResourceLocation.tryParse(resourceId.getNamespace() + ":" + + path.substring(prefix.length(), path.length() - suffix.length())); + } + + /** 递归收集一张表里引用的全部子表 id(含嵌套在条目、函数里的引用)。 */ + private static void collectReferencedTables(@Nullable JsonElement element, + Set referenced) { + if (element == null) { + return; + } + if (element.isJsonArray()) { + for (JsonElement child : element.getAsJsonArray()) { + collectReferencedTables(child, referenced); + } + return; + } + if (!element.isJsonObject()) { + return; + } + + JsonObject object = element.getAsJsonObject(); + if ("minecraft:loot_table".equals(stringValue(object.get("type")))) { + ResourceLocation childId = ResourceLocation.tryParse( + firstNonBlank(stringValue(object.get("value")), + stringValue(object.get("name")))); + if (childId != null) { + referenced.add(childId); + } + } + for (Map.Entry field : object.entrySet()) { + collectReferencedTables(field.getValue(), referenced); + } + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null + || holder.value().convertedRecipe() == null) { + return List.of(); + } + return List.of(holder.value().convertedRecipe()); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + return findMatchingRecipes(level, mergedInputs, mergedFluids, Map.of(), mold, List.of()); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, + Map mergedKeys, + @Nullable ItemStack mold, List actualInputs) { + if (level == null || !matchesMold(mold)) { + return List.of(); + } + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : getGeneratedRecipes(level)) { + AdvancedAlloyFurnaceRecipe recipe = holder.value().convertedRecipe(); + if (recipe == null || !recipe.keyInputs().isEmpty()) { + continue; + } + + // 钓鱼转换配方没有物品输入,只消耗水;若机器里放了物品则不匹配。 + if (recipe.inputs().isEmpty() && hasConcreteInputs(actualInputs)) { + continue; + } + if (recipe.inputs().isEmpty() + && mergedInputs != null && !mergedInputs.isEmpty()) { + continue; + } + + boolean itemsMatch = recipe.inputs().isEmpty() + || ItemIngredientAllocator.matches(recipe.inputs(), actualInputs, 1L); + boolean fluidsMatch = FluidIngredientAllocator + .matchesLong(recipe.inputFluids(), mergedFluids == null ? Map.of() : mergedFluids, 1L); + if (itemsMatch && fluidsMatch) { + matches.add(holder); + } + } + return matches; + } + + /** + * 递归收集战利品表条目中的可转换产物,并按「来源子表」分组。 + * + *

原版钓鱼表把鱼类(fish)、杂物(junk)、宝藏(treasure)拆成三张子表, + * 分组键取子表路径的最后一段,这样每个类别各生成一个配方。若条目直接写在顶层, + * 则归入传入的 {@code groupKey}。

+ */ + private static void collectGroupedOutputs(Registry itemRegistry, + ResourceManager resourceManager, + @Nullable JsonElement entriesElement, + Map> grouped, + int depth, String groupKey) { + if (depth > MAX_TABLE_DEPTH) { + return; + } + JsonArray entries = asArray(entriesElement); + if (entries == null) { + return; + } + + for (JsonElement entryElement : entries) { + JsonObject entry = entryElement != null && entryElement.isJsonObject() + ? entryElement.getAsJsonObject() : null; + if (entry == null) { + continue; + } + + String type = stringValue(entry.get("type")); + switch (type) { + case "minecraft:empty" -> { + // 空条目表示该次抽取没有产物,跳过。 + } + case "minecraft:item" -> { + // 条目上的 conditions 只决定「这次能不能抽到它」(例如原版宝藏要求开阔水域、 + // 其他模组要求特定生物群系),不影响产物本身是什么,因此忽略以保证配方的 + // 产物集合完整。合金炉配方本身无随机、无世界状态,忽略这些门槛不会引入不确定性。 + // + // functions 则不同:它会改变产物内容(附魔、药水、随机数量), + // 只有「固定数量」能安全转换,其余一律跳过。 + Integer fixedCount = fixedCountOrNull(entry.get("functions")); + if (fixedCount == null) { + continue; + } + ResourceLocation itemId = ResourceLocation.tryParse(stringValue(entry.get("name"))); + Item item = itemId == null ? null : itemRegistry.getOptional(itemId).orElse(null); + if (item == null || item == Items.AIR) { + continue; + } + ItemStack stack = item.getDefaultInstance(); + if (isEquipment(stack)) { + continue; + } + if (fixedCount > 1) { + stack.setCount(Math.min(fixedCount, stack.getMaxStackSize())); + } + grouped.computeIfAbsent(groupKey, key -> new ArrayList<>()).add(stack); + } + case "minecraft:loot_table" -> { + // 原版钓鱼表用子表拆分鱼类/杂物/宝藏,递归读取子表的 pools。 + // 子表引用上的 conditions 只决定「是否抽取该子表」——例如原版宝藏要求 + // 必须在开阔水域——它不影响子表内产物的确定性,因此这里忽略条件继续递归, + // 否则原版宝藏条目会被整体丢弃。 + ResourceLocation childId = ResourceLocation.tryParse( + firstNonBlank(stringValue(entry.get("value")), + stringValue(entry.get("name")))); + if (childId == null) { + continue; + } + JsonObject childTable = readLootTable(resourceManager, childId); + if (childTable == null || !hasNoEntries(childTable, "conditions")) { + continue; + } + JsonArray childPools = asArray(childTable.get("pools")); + if (childPools == null) { + continue; + } + String childGroup = groupName(childId); + for (JsonElement childPoolElement : childPools) { + JsonObject childPool = childPoolElement != null && childPoolElement.isJsonObject() + ? childPoolElement.getAsJsonObject() : null; + if (childPool == null || !hasNoEntries(childPool, "conditions")) { + continue; + } + collectGroupedOutputs(itemRegistry, resourceManager, childPool.get("entries"), + grouped, depth + 1, childGroup); + } + } + default -> { + // 标签、序列化实体等条目无法静态确定产物,跳过。 + } + } + } + } + + /** 取子表路径的最后一段作为分组名,例如 gameplay/fishing/fish -> fish。 */ + private static String groupName(ResourceLocation tableId) { + String path = tableId.getPath(); + int slash = path.lastIndexOf('/'); + String name = slash >= 0 ? path.substring(slash + 1) : path; + return name.isBlank() ? "fishing" : name; + } + + /** 判断物品是否属于「装备」,钓鱼兼容明确不转换这些产物。 */ + private static boolean isEquipment(ItemStack stack) { + if (stack == null || stack.isEmpty()) { + return false; + } + Item item = stack.getItem(); + // 附魔书虽然不可损坏,但属于明确排除项。 + if (item == Items.ENCHANTED_BOOK) { + return true; + } + // 有耐久值的物品覆盖工具、武器、护甲、钓鱼竿、盾牌、剪刀等全部装备。 + if (stack.isDamageableItem()) { + return true; + } + return item instanceof Equipable; + } + + /** + * 从条目的 {@code functions} 里取出「固定产物数量」,无法确定时返回 null。 + * + *

函数会改变产物内容:附魔、药水、随机数量、写入容器内容等都无法映射成合金炉的 + * 确定性产物,只要出现这类函数就整条跳过。唯一可以安全转换的是固定数量的 + * {@code minecraft:set_count}(常量形式),它只是把产物数量从 1 改成 N。

+ * + * @return 产物数量;无函数时为 1;含无法转换的函数或数量非正时返回 null + */ + @Nullable + private static Integer fixedCountOrNull(@Nullable JsonElement functionsElement) { + JsonArray functions = asArray(functionsElement); + if (functions == null) { + // 没有 functions,产物就是默认的 1 个。 + return 1; + } + + int count = 1; + for (JsonElement functionElement : functions) { + JsonObject function = functionElement != null && functionElement.isJsonObject() + ? functionElement.getAsJsonObject() : null; + if (function == null) { + continue; + } + String name = stringValue(function.get("function")); + if (!"minecraft:set_count".equals(name)) { + // 附魔、药水、耐久、随机数量等函数都会改变产物内容,无法静态确定。 + return null; + } + Integer fixed = constantCountOrNull(function.get("count")); + if (fixed == null) { + // 均匀分布等随机数量同样无法确定。 + return null; + } + count = fixed; + } + return count > 0 ? count : null; + } + + /** 读取 {@code set_count} 的数量字段,只接受常量形式。 */ + @Nullable + private static Integer constantCountOrNull(@Nullable JsonElement countElement) { + if (countElement == null || countElement.isJsonNull()) { + return null; + } + if (countElement.isJsonPrimitive() && countElement.getAsJsonPrimitive().isNumber()) { + return countElement.getAsInt(); + } + if (!countElement.isJsonObject()) { + return null; + } + JsonObject count = countElement.getAsJsonObject(); + // 形如 {"type":"minecraft:constant","value":10},只有 constant 是确定的。 + if (!"minecraft:constant".equals(stringValue(count.get("type")))) { + return null; + } + JsonElement value = count.get("value"); + if (value == null || !value.isJsonPrimitive() || !value.getAsJsonPrimitive().isNumber()) { + return null; + } + return value.getAsInt(); + } + + @Nullable + private static AdvancedAlloyFurnaceRecipe convertOutputs(ResourceLocation recipeId, + List outputs) { + List normalizedOutputs = new ArrayList<>(); + Set seen = new HashSet<>(); + for (ItemStack output : outputs) { + if (output == null || output.isEmpty() || output.getCount() <= 0) { + continue; + } + ResourceLocation id = BuiltInRegistries.ITEM.getKey(output.getItem()); + if (id == null || !seen.add(id)) { + continue; + } + normalizedOutputs.add(output.copy()); + } + if (normalizedOutputs.isEmpty()) { + return null; + } + + int operations = normalizedOutputs.size(); + long waterAmount = saturatingMultiply(operations, WATER_PER_OUTPUT); + long energy = saturatingMultiply(AdapterUtils.DEFAULT_ENERGY, operations); + int processTime = AdapterUtils.safeInt( + saturatingMultiply(AdapterUtils.DEFAULT_PROCESS_TIME, operations)); + + return new AdvancedAlloyFurnaceRecipe( + recipeId, + List.of(), + List.of(new LongSizedFluidIngredient(FluidIngredient.single(Fluids.WATER), waterAmount)), + List.of(), + normalizedOutputs, + List.of(), + List.of(), + energy, + processTime, + Ingredient.EMPTY, + 0, + List.of(Ingredient.of(Items.FISHING_ROD)), + AlloyFurnaceMode.NORMAL); + } + + /** + * 生成与内容绑定的稳定配方 id。 + * + *

id 取自排序后首个产物的注册名,绝不使用集合下标或身份哈希,否则万象样板会在重建索引后失效。

+ */ + @Nullable + private static ResourceLocation nextRecipeId(String groupKey, List outputs, + Set usedIds) { + ResourceLocation anchor = null; + for (ItemStack output : outputs) { + if (output == null || output.isEmpty()) { + continue; + } + ResourceLocation id = BuiltInRegistries.ITEM.getKey(output.getItem()); + if (id == null) { + continue; + } + if (anchor == null || id.compareTo(anchor) < 0) { + anchor = id; + } + } + if (anchor == null) { + return null; + } + + String basePath = "fishing_" + sanitize(groupKey) + "_" + + anchor.getNamespace() + "_" + anchor.getPath().replace('/', '_'); + ResourceLocation candidate = new ResourceLocation( + RecipeSourceIds.MINECRAFT_FISHING, basePath + "_converted"); + int suffix = 2; + while (!usedIds.add(candidate)) { + candidate = new ResourceLocation( + RecipeSourceIds.MINECRAFT_FISHING, basePath + "_converted_" + suffix); + suffix++; + } + return candidate; + } + + /** 把分组名净化为合法的资源路径片段。 */ + private static String sanitize(String groupKey) { + if (groupKey == null || groupKey.isBlank()) { + return "fishing"; + } + StringBuilder builder = new StringBuilder(groupKey.length()); + for (char c : groupKey.toCharArray()) { + // 只保留资源路径允许的字符,其余一律替换为下划线。 + boolean allowed = (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') + || c == '_' || c == '-' || c == '.' || c == '/'; + builder.append(allowed ? c : '_'); + } + return builder.toString(); + } + + @Nullable + private static JsonObject readLootTable(ResourceManager resourceManager, + ResourceLocation tableId) { + ResourceLocation resourceId = new ResourceLocation( + tableId.getNamespace(), "loot_table/" + tableId.getPath() + ".json"); + Optional resource = resourceManager.getResource(resourceId); + if (resource.isEmpty()) { + return null; + } + + try (var reader = resource.get().openAsReader()) { + return GsonHelper.parse(reader); + } catch (IOException | RuntimeException ignored) { + return null; + } + } + + @Nullable + private static ResourceManager resourceManager(Level level) { + MinecraftServer server = level.getServer(); + if (server != null) { + return server.getResourceManager(); + } + + // 单机客户端可以读取集成服务端的数据资源;远程客户端读不到,因此直接放弃生成。 + try { + Class minecraftClass = Class.forName("net.minecraft.client.Minecraft"); + Object minecraft = minecraftClass.getMethod("getInstance").invoke(null); + Object integratedServer = minecraftClass.getMethod("getSingleplayerServer") + .invoke(minecraft); + if (integratedServer instanceof MinecraftServer minecraftServer) { + return minecraftServer.getResourceManager(); + } + } catch (ReflectiveOperationException | LinkageError | ClassCastException ignored) { + } + return null; + } + + @Nullable + private static JsonArray asArray(@Nullable JsonElement element) { + return element != null && element.isJsonArray() ? element.getAsJsonArray() : null; + } + + private static boolean hasNoEntries(JsonObject object, String key) { + JsonElement value = object.get(key); + return value == null || (value.isJsonArray() && value.getAsJsonArray().isEmpty()); + } + + private static String stringValue(@Nullable JsonElement element) { + return element != null && element.isJsonPrimitive() && element.getAsJsonPrimitive().isString() + ? element.getAsString() : ""; + } + + private static String firstNonBlank(String first, String second) { + if (first != null && !first.isBlank()) { + return first; + } + return second == null ? "" : second; + } + + private static boolean hasConcreteInputs(@Nullable List inputs) { + if (inputs == null) { + return false; + } + return inputs.stream().anyMatch(stack -> stack != null && !stack.isEmpty() + && stack.getCount() > 0); + } + + private static long saturatingMultiply(long left, long right) { + if (left <= 0L || right <= 0L) { + return 0L; + } + return left > Long.MAX_VALUE / right ? Long.MAX_VALUE : left * right; + } + + /** 保留给未来扩展:把合并输入映射成需求映射时的工具方法。 */ + static Map mergeRequirements(List requirements) { + Map required = new LinkedHashMap<>(); + for (CountedIngredient requirement : requirements) { + AdapterUtils.mergeIngredient(required, requirement.ingredient(), requirement.count()); + } + return required; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/minecraft/FishingLootSyntheticRecipe.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/minecraft/FishingLootSyntheticRecipe.java new file mode 100644 index 00000000..772670f1 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/minecraft/FishingLootSyntheticRecipe.java @@ -0,0 +1,55 @@ +package com.sorrowmist.useless.content.recipe.adapters.minecraft; + +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.Container; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; + +/** 原版钓鱼战利品表不是 RecipeManager 配方,这里用合成配方承载转换结果。 */ +public final class FishingLootSyntheticRecipe implements Recipe { + private final AdvancedAlloyFurnaceRecipe convertedRecipe; + + public FishingLootSyntheticRecipe(AdvancedAlloyFurnaceRecipe convertedRecipe) { + this.convertedRecipe = convertedRecipe; + } + + public AdvancedAlloyFurnaceRecipe convertedRecipe() { + return convertedRecipe; + } + + @Override + public net.minecraft.resources.ResourceLocation getId() { return convertedRecipe.id(); } + + @Override + public boolean matches(Container input, Level level) { + return convertedRecipe != null && convertedRecipe.matches(input, level); + } + + @Override + public ItemStack assemble(Container input, net.minecraft.core.RegistryAccess registries) { + return convertedRecipe == null ? ItemStack.EMPTY : convertedRecipe.assemble(input, registries); + } + + @Override + public boolean canCraftInDimensions(int width, int height) { + return convertedRecipe != null && convertedRecipe.canCraftInDimensions(width, height); + } + + @Override + public ItemStack getResultItem(net.minecraft.core.RegistryAccess registries) { + return convertedRecipe == null ? ItemStack.EMPTY : convertedRecipe.getResultItem(registries); + } + + @Override + public RecipeSerializer getSerializer() { + return convertedRecipe == null ? null : convertedRecipe.getSerializer(); + } + + @Override + public RecipeType getType() { + return convertedRecipe == null ? null : convertedRecipe.getType(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/minecraft/SmeltingRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/minecraft/SmeltingRecipeAdapter.java new file mode 100644 index 00000000..574a7ff5 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/minecraft/SmeltingRecipeAdapter.java @@ -0,0 +1,151 @@ +package com.sorrowmist.useless.content.recipe.adapters.minecraft; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.core.NonNullList; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.AbstractCookingRecipe; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Map; + +/** + * 原版烹饪配方适配器。 + */ +public class SmeltingRecipeAdapter implements IRecipeAdapter { + + // 处理时间基础值(ticks) + private static final int BASE_PROCESS_TIME = 200; + private final RecipeType recipeType; + private final Item moldItem; + + public SmeltingRecipeAdapter() { + this(RecipeType.SMELTING, Items.FURNACE); + } + + public SmeltingRecipeAdapter( + RecipeType recipeType, + Item moldItem) { + this.recipeType = recipeType; + this.moldItem = moldItem; + } + + @Override + public Class getRecipeClass() { + return AbstractCookingRecipe.class; + } + + @Override + @Nullable + public AdvancedAlloyFurnaceRecipe convert(RecipeHolder holder, Level level) { + if (holder == null) return null; + + AbstractCookingRecipe originalRecipe = holder.value(); + if (originalRecipe.getType() != recipeType) return null; + ResourceLocation originalId = holder.id(); + + // 获取输入材料 + NonNullList ingredients = originalRecipe.getIngredients(); + List countedIngredients = AdapterUtils.mergeIngredients(ingredients); + + if (countedIngredients.isEmpty()) { + return null; + } + + // 获取输出物品 + ItemStack result = originalRecipe.getResultItem(level.registryAccess()); + if (result.isEmpty()) { + return null; + } + List outputs = List.of(result.copy()); + + // 计算能量和处理时间 + int cookingTime = originalRecipe.getCookingTime(); + int processTime = cookingTime > 0 ? cookingTime : BASE_PROCESS_TIME; + + // 能量消耗根据处理时间比例调整 + int energy = AdapterUtils.safeInt((long) processTime * AdapterUtils.DEFAULT_ENERGY / BASE_PROCESS_TIME); + + Ingredient cookingMold = AdapterUtils.toMoldIngredient(getMoldItem()); + + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(originalId), + countedIngredients, + List.of(), + outputs, + List.of(), + energy, + processTime, + Ingredient.EMPTY, + 0, + cookingMold, + AlloyFurnaceMode.NORMAL + ); + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return new ItemStack(moldItem); + } + + @Override + public List> findMatchingRecipes(Level level, Map mergedInputs, Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || mergedInputs.isEmpty() || !matchesMold(mold)) { + return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + + return castHolders(findCookingRecipes(recipeManager, recipeType, mergedInputs)); + } + + /** + * 查找匹配的原版烹饪配方 + */ + private List> findCookingRecipes( + RecipeManager recipeManager, + RecipeType recipeType, + Map mergedInputs + ) { + List> recipes = com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, recipeType); + List> matches = new java.util.ArrayList<>(); + for (RecipeHolder holder : recipes) { + T recipe = holder.value(); + NonNullList ingredients = recipe.getIngredients(); + + if (ingredients.isEmpty()) continue; + + Ingredient mainIngredient = ingredients.get(0); + + if (AdapterUtils.hasMatchingIngredient(mergedInputs, mainIngredient, 1L)) { + matches.add(holder); + } + } + return matches; + } + + @SuppressWarnings("unchecked") + private List> castHolders(List> holders) { + return (List>) (List) holders; + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + return mold != null && !mold.isEmpty() && mold.is(moldItem); + } + +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/minecraft/VillagerTradeRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/minecraft/VillagerTradeRecipeAdapter.java new file mode 100644 index 00000000..55a02edd --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/minecraft/VillagerTradeRecipeAdapter.java @@ -0,0 +1,711 @@ +package com.sorrowmist.useless.content.recipe.adapters.minecraft; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeFingerprint; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.ItemIngredientAllocator; +import com.sorrowmist.useless.init.ModItems; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.npc.Villager; +import net.minecraft.world.entity.npc.VillagerData; +import net.minecraft.world.entity.npc.VillagerProfession; +import net.minecraft.world.entity.npc.VillagerTrades; +import net.minecraft.world.entity.npc.VillagerType; +import net.minecraft.world.entity.npc.WanderingTrader; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.trading.MerchantOffer; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.levelgen.PositionalRandomFactory; +import com.sorrowmist.useless.compat.forge1201.recipe.DataComponentIngredient; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Converts the active vanilla villager trade tables into alloy-furnace recipes. */ +public final class VillagerTradeRecipeAdapter + implements IRecipeAdapter { + private static final String RECIPE_NAMESPACE = "useless_mod"; + private static final int MIN_VILLAGER_LEVEL = 1; + private static final int MAX_VILLAGER_LEVEL = 5; + + private volatile Cached cached; + + @Override + public Class getRecipeClass() { + return VillagerTradeSyntheticRecipe.class; + } + + /** The generated villager recipes use both the spawn egg and the matching job block. */ + @Override + @Nullable + public ItemStack getMoldItem() { + return null; + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + if (mold == null || mold.isEmpty()) { + return false; + } + if (mold.is(Items.VILLAGER_SPAWN_EGG)) { + return true; + } + return registeredProfessions().stream() + .flatMap(profession -> profession.workstations().stream()) + .anyMatch(workstation -> mold.is(workstation)); + } + + /** + * 职业 / 工作站配对在运行期是稳定的:POI 注册表在启动后不再变化。 + */ + private static volatile List professionCache; + + /** + * Builds the profession/workstation pairs from the live registries. This also covers + * professions added by mods whose POI is registered after the vanilla professions. + * + *

结果按注册表内容缓存一次:{@link #matchesMold} 与交易表签名在每次配方查找时都会用到它, + * 每次都重新遍历全部 POI 类型及其匹配状态是纯浪费。

+ */ + private static List registeredProfessions() { + List cachedProfessions = professionCache; + if (cachedProfessions != null) { + return cachedProfessions; + } + synchronized (VillagerTradeRecipeAdapter.class) { + cachedProfessions = professionCache; + if (cachedProfessions == null) { + cachedProfessions = buildProfessions(); + professionCache = cachedProfessions; + } + return cachedProfessions; + } + } + + private static List buildProfessions() { + List professions = new ArrayList<>(); + for (VillagerProfession profession : BuiltInRegistries.VILLAGER_PROFESSION) { + List workstations = BuiltInRegistries.POINT_OF_INTEREST_TYPE.holders() + .filter(profession.heldJobSite()) + .flatMap(holder -> holder.value().matchingStates().stream()) + .map(state -> state.getBlock().asItem()) + .filter(item -> item != Items.AIR) + .distinct() + .sorted(Comparator.comparing(VillagerTradeRecipeAdapter::itemKey)) + .toList(); + professions.add(new ProfessionWorkstation(profession, workstations)); + } + professions.sort(Comparator.comparing( + profession -> professionKey(profession.profession()))); + return List.copyOf(professions); + } + + private static String professionKey(VillagerProfession profession) { + net.minecraft.resources.ResourceLocation id = + BuiltInRegistries.VILLAGER_PROFESSION.getKey(profession); + return id == null ? profession.toString() : id.toString(); + } + + private static String itemKey(Item item) { + net.minecraft.resources.ResourceLocation id = BuiltInRegistries.ITEM.getKey(item); + return id == null ? item.toString() : id.toString(); + } + + /** Keeps the villager spawn egg and all valid workstation blocks as two mold slots. */ + private static List villagerMolds(ProfessionWorkstation profession) { + Ingredient workstation = Ingredient.of(profession.workstations().stream() + .map(Item::getDefaultInstance)); + return List.of( + AdapterUtils.toMoldIngredient(new ItemStack(Items.VILLAGER_SPAWN_EGG)), + workstation); + } + + @Override + public List> getGeneratedRecipes(Level level) { + if (level == null) { + return List.of(); + } + + boolean experimental = false; // Trade rebalance was introduced after 1.20.1. + int tradeTableSignature = tradeTableSignature(experimental); + Cached snapshot = cached; + if (snapshot != null && snapshot.level() == level + && snapshot.experimental() == experimental + && snapshot.tradeTableSignature() == tradeTableSignature) { + return snapshot.recipes(); + } + + synchronized (this) { + snapshot = cached; + if (snapshot == null || snapshot.level() != level + || snapshot.experimental() != experimental + || snapshot.tradeTableSignature() != tradeTableSignature) { + snapshot = new Cached(level, experimental, tradeTableSignature, + buildRecipes(level, experimental)); + cached = snapshot; + } + return snapshot.recipes(); + } + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null + || holder.value().convertedRecipe() == null) { + return List.of(); + } + return List.of(holder.value().convertedRecipe()); + } + + @Override + public List> findMatchingRecipes( + Level level, + Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + return findMatchingRecipes(level, mergedInputs, mergedFluids, Map.of(), mold, List.of()); + } + + @Override + public List> findMatchingRecipes( + Level level, + Map mergedInputs, + Map mergedFluids, + Map mergedKeys, + @Nullable ItemStack mold, + List actualInputs) { + if (level == null || !matchesMold(mold)) { + return List.of(); + } + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : getGeneratedRecipes(level)) { + AdvancedAlloyFurnaceRecipe recipe = holder.value().convertedRecipe(); + // A regular furnace has one mold slot. Dual-mold villager trades are exposed through + // the multiblock mold hub and must not be returned as single-mold matches here. + if (recipe == null || recipe.molds().size() != 1 + || !AdapterUtils.matchesMold(recipe.mold(), mold)) { + continue; + } + + boolean matchesRecipe = actualInputs != null && !actualInputs.isEmpty() + ? ItemIngredientAllocator.matches(recipe.inputs(), actualInputs, 1L) + : matchesMergedInputs(recipe, mergedInputs); + if (matchesRecipe) { + matches.add(holder); + } + } + return List.copyOf(matches); + } + + private static List> buildRecipes( + Level level, boolean experimental) { + Map> recipes = new LinkedHashMap<>(); + + addSupplementalVillagerRecipe(recipes); + + for (ProfessionWorkstation profession : registeredProfessions()) { + if (profession.workstations().isEmpty()) { + continue; + } + for (int villagerLevel = MIN_VILLAGER_LEVEL; + villagerLevel <= MAX_VILLAGER_LEVEL; villagerLevel++) { + VillagerTrades.ItemListing[] listings = listingsFor( + profession.profession(), villagerLevel, experimental); + if (listings == null) { + continue; + } + + for (int listingIndex = 0; listingIndex < listings.length; listingIndex++) { + VillagerTrades.ItemListing listing = listings[listingIndex]; + if (listing == null) { + continue; + } + + for (TradeContext context : contextsFor(listing)) { + try { + MerchantOffer offer = createOffer( + level, profession.profession(), villagerLevel, + listing, context.villagerType()); + if (offer == null) { + continue; + } + + AdvancedAlloyFurnaceRecipe converted = convertOffer( + recipeId(profession.profession(), villagerLevel, + context.villagerType(), experimental, offer, + level.registryAccess()), + offer, villagerMolds(profession)); + if (converted != null) { + RecipeHolder holder = + new RecipeHolder<>(converted.id(), + new VillagerTradeSyntheticRecipe(converted)); + recipes.putIfAbsent(holder.id(), holder); + } + } catch (RuntimeException exception) { + } + } + } + } + } + + addWanderingTraderRecipes(level, experimental, recipes); + return List.copyOf(recipes.values()); + } + + private static void addWanderingTraderRecipes( + Level level, + boolean experimental, + Map> recipes) { + List> groups = + new ArrayList<>(VillagerTrades.WANDERING_TRADER_TRADES.int2ObjectEntrySet()); + groups.sort(Comparator.comparingInt(Int2ObjectMap.Entry::getIntKey)); + for (Int2ObjectMap.Entry group : groups) { + addWanderingTraderListings(level, false, group.getIntKey(), + group.getValue(), recipes); + } + } + + private static void addWanderingTraderListings( + Level level, + boolean experimental, + int groupIndex, + @Nullable VillagerTrades.ItemListing[] listings, + Map> recipes) { + if (listings == null) { + return; + } + for (int listingIndex = 0; listingIndex < listings.length; listingIndex++) { + VillagerTrades.ItemListing listing = listings[listingIndex]; + if (listing == null) { + continue; + } + try { + MerchantOffer offer = createWanderingTraderOffer(level, listing); + if (offer == null) { + continue; + } + AdvancedAlloyFurnaceRecipe converted = convertOffer( + wanderingRecipeId(experimental, offer, level.registryAccess()), offer, + List.of(AdapterUtils.toMoldIngredient( + new ItemStack(Items.WANDERING_TRADER_SPAWN_EGG)))); + if (converted != null) { + RecipeHolder holder = + new RecipeHolder<>(converted.id(), new VillagerTradeSyntheticRecipe(converted)); + recipes.putIfAbsent(holder.id(), holder); + } + } catch (RuntimeException exception) { + } + } + } + + private static void addSupplementalVillagerRecipe( + Map> recipes) { + net.minecraft.resources.ResourceLocation id = + new net.minecraft.resources.ResourceLocation( + RECIPE_NAMESPACE, "villager_trade/supplemental/tier_1_useless_ingot_to_dirt"); + AdvancedAlloyFurnaceRecipe converted = new AdvancedAlloyFurnaceRecipe( + id, + List.of(new CountedIngredient( + Ingredient.of(ModItems.USELESS_INGOT_TIER_1.get()), 1L)), + List.of(), + List.of(), + List.of(new ItemStack(Items.DIRT)), + List.of(), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + AdapterUtils.DEFAULT_PROCESS_TIME, + Ingredient.EMPTY, + 0, + List.of(AdapterUtils.toMoldIngredient(new ItemStack(Items.VILLAGER_SPAWN_EGG))), + AlloyFurnaceMode.NORMAL); + recipes.putIfAbsent(id, + new RecipeHolder<>(id, new VillagerTradeSyntheticRecipe(converted))); + } + + @Nullable + private static VillagerTrades.ItemListing[] listingsFor( + VillagerProfession profession, int villagerLevel, boolean experimental) { + var tradeLevels = VillagerTrades.TRADES.get(profession); + return tradeLevels == null ? null : tradeLevels.get(villagerLevel); + } + + private static List contextsFor(VillagerTrades.ItemListing listing) { + if (listing.getClass().getSimpleName().equals("EmeraldsForVillagerTypeItem")) { + return BuiltInRegistries.VILLAGER_TYPE.stream() + .sorted(Comparator.comparing(VillagerType::toString)) + .map(TradeContext::new) + .toList(); + } + return List.of(new TradeContext(null)); + } + + /** + * 判定交易条目是否属于地图类。 + * + *

地图交易的报价在 {@code getOffer} 内部会执行一次结构定位(原版为 + * {@code VillagerTrades.TreasureMapForEmeralds},模组侧如 Supplementaries 的探险家地图), + * 该操作需要加载区块。目录构建运行在后台线程,而区块加载依赖服务器主线程推进,两者相互等待 + * 会导致服务器 tick 停滞。此外结构定位结果取决于世界种子与执行侧,同一 listing 在客户端与 + * 服务端会产出不同的配方集合,目录无法保持稳定。因此地图类交易一律不参与转换。

+ * + *

除原版类型外,模组侧的地图交易无法通过类型引用判定,只能依据类名。判据刻意放宽为 + * 「类名含有 {@code Map}」:此前按交易条目后缀({@code Listing} / {@code Trade} / + * {@code Offer})逐个列举的写法已被证明不收敛,Supplementaries 的 + * {@code RandomAdventurerMapListing} 与 Goety 的 {@code TreasureMapForEmeralds} 先后漏网, + * 二者内部同样会调用 {@code findNearestMapStructure}。放宽的代价是极少数名称含 + * {@code Map} 却无结构定位的交易被跳过,其后果仅为目录条目缺失,不影响正确性。

+ */ + private static boolean isMapTrade(VillagerTrades.ItemListing listing) { + if (listing.getClass().getSimpleName().equals("TreasureMapForEmeralds")) { + return true; + } + return listing.getClass().getSimpleName().contains("Map"); + } + + @Nullable + private static MerchantOffer createOffer( + Level level, + VillagerProfession profession, + int villagerLevel, + VillagerTrades.ItemListing listing, + @Nullable VillagerType villagerType) { + if (isMapTrade(listing)) { + return null; + } + + Villager villager = EntityType.VILLAGER.create(level); + if (villager == null) { + return null; + } + + VillagerType effectiveType = villagerType == null + ? VillagerType.PLAINS : villagerType; + villager.setVillagerData(new VillagerData(effectiveType, profession, villagerLevel)); + // Vanilla listings generate some base prices from a random range. Feeding them a source + // that always returns the lower bound gives the minimum configured emerald cost. + return listing.getOffer(villager, new MinimumRandomSource()); + } + + @Nullable + private static MerchantOffer createWanderingTraderOffer( + Level level, VillagerTrades.ItemListing listing) { + if (isMapTrade(listing)) { + return null; + } + + WanderingTrader trader = EntityType.WANDERING_TRADER.create(level); + return trader == null ? null : listing.getOffer(trader, new MinimumRandomSource()); + } + + private static int tradeTableSignature(boolean experimental) { + int signature = experimental ? 1 : 0; + for (ProfessionWorkstation profession : registeredProfessions()) { + signature = appendProfessionSignature(signature, profession); + for (int villagerLevel = MIN_VILLAGER_LEVEL; + villagerLevel <= MAX_VILLAGER_LEVEL; villagerLevel++) { + VillagerTrades.ItemListing[] listings = listingsFor( + profession.profession(), villagerLevel, experimental); + signature = 31 * signature + villagerLevel; + signature = appendListingSignature(signature, listings); + } + } + + { + List> groups = + new ArrayList<>(VillagerTrades.WANDERING_TRADER_TRADES.int2ObjectEntrySet()); + groups.sort(Comparator.comparingInt(Int2ObjectMap.Entry::getIntKey)); + for (Int2ObjectMap.Entry group : groups) { + signature = 31 * signature + group.getIntKey(); + signature = appendListingSignature(signature, group.getValue()); + } + } + return signature; + } + + private static int appendProfessionSignature( + int signature, ProfessionWorkstation profession) { + signature = 31 * signature + professionKey(profession.profession()).hashCode(); + for (Item workstation : profession.workstations()) { + signature = 31 * signature + itemKey(workstation).hashCode(); + } + return signature; + } + + private static int appendListingSignature( + int signature, @Nullable VillagerTrades.ItemListing[] listings) { + if (listings == null) { + return 31 * signature; + } + signature = 31 * signature + listings.length; + for (VillagerTrades.ItemListing listing : listings) { + signature = 31 * signature + System.identityHashCode(listing); + } + return signature; + } + + @Nullable + private static AdvancedAlloyFurnaceRecipe convertOffer( + net.minecraft.resources.ResourceLocation id, + MerchantOffer offer, + List moldIngredients) { + if (offer == null || offer.getResult() == null || offer.getResult().isEmpty()) { + return null; + } + + List inputs = offerInputs(offer); + if (inputs.isEmpty()) { + return null; + } + + List molds = new ArrayList<>(); + if (moldIngredients != null) { + for (Ingredient moldIngredient : moldIngredients) { + if (moldIngredient != null && !moldIngredient.isEmpty()) { + molds.add(moldIngredient); + } + } + } + if (molds.isEmpty()) { + return null; + } + + return new AdvancedAlloyFurnaceRecipe( + id, + inputs, + List.of(), + List.of(), + List.of(offer.getResult().copy()), + List.of(), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + AdapterUtils.DEFAULT_PROCESS_TIME, + Ingredient.EMPTY, + 0, + molds, + AlloyFurnaceMode.NORMAL); + } + + private static List offerInputs(MerchantOffer offer) { + Map inputs = new LinkedHashMap<>(); + + // baseCostA is the unmodified, minimum cost. getCostA() includes demand and discounts. + addCost(inputs, offer.getBaseCostA()); + addCost(inputs, offer.getCostB()); + + return inputs.entrySet().stream() + .filter(entry -> entry.getKey() != null && !entry.getKey().isEmpty() + && entry.getValue() != null && entry.getValue() > 0L) + .map(entry -> new CountedIngredient(entry.getKey(), entry.getValue())) + .toList(); + } + + private static void addCost(Map inputs, @Nullable ItemStack cost) { + if (cost == null || cost.isEmpty() || cost.getCount() <= 0) { + return; + } + Ingredient ingredient = DataComponentIngredient.of(true, cost.copyWithCount(1)); + AdapterUtils.mergeIngredient(inputs, ingredient, cost.getCount()); + } + + private static boolean matchesMergedInputs( + AdvancedAlloyFurnaceRecipe recipe, @Nullable Map mergedInputs) { + if (mergedInputs == null || mergedInputs.isEmpty()) { + return false; + } + Map required = new LinkedHashMap<>(); + for (CountedIngredient input : recipe.inputs()) { + if (input != null && input.ingredient() != null && input.count() > 0L) { + AdapterUtils.mergeIngredient(required, input.ingredient(), input.count()); + } + } + return ItemIngredientAllocator.matches(mergedInputs, required); + } + + /** + * 由交易内容构造村民交易配方的 id。 + * + *

id 不得包含交易条目在数组中的下标:客户端与服务端的目录各自独立构建,交易表的枚举 + * 顺序不保证一致,位置派生的 id 会让同一个 id 在两端指向不同的交易,导致万象样板在服务端 + * 无法解析。此处以报价的实际内容(产物与两项成本)派生后缀,职业、等级与村民类型仍作为 + * 可读前缀保留。内容完全相同的两条报价会得到相同 id,由调用方的 {@code putIfAbsent} 合并, + * 与二者配方等价的事实一致。

+ */ + private static net.minecraft.resources.ResourceLocation recipeId( + VillagerProfession profession, + int villagerLevel, + @Nullable VillagerType villagerType, + boolean experimental, + MerchantOffer offer, + HolderLookup.Provider registries) { + net.minecraft.resources.ResourceLocation professionId = + net.minecraft.core.registries.BuiltInRegistries.VILLAGER_PROFESSION.getKey(profession); + String professionPart; + if (professionId == null) { + professionPart = profession.toString(); + } else if ("minecraft".equals(professionId.getNamespace())) { + professionPart = professionId.getPath(); + } else { + // Include the namespace so different mods can use the same profession path safely. + professionPart = professionId.getNamespace() + "_" + professionId.getPath(); + } + String path = "villager_trade/" + + (experimental ? "experimental" : "vanilla") + "/" + + pathPart(professionPart) + "/level_" + villagerLevel + + "/trade_" + offerKey(offer, registries); + if (villagerType != null) { + path += "/type_" + pathPart(villagerType.toString()); + } + path += "/mold_spawn_egg_and_workstation"; + return new net.minecraft.resources.ResourceLocation( + RECIPE_NAMESPACE, path); + } + + private static net.minecraft.resources.ResourceLocation wanderingRecipeId( + boolean experimental, MerchantOffer offer, HolderLookup.Provider registries) { + String path = "villager_trade/" + + (experimental ? "experimental" : "vanilla") + + "/wandering_trader/trade_" + offerKey(offer, registries) + + "/mold_spawn_egg"; + return new net.minecraft.resources.ResourceLocation( + RECIPE_NAMESPACE, path); + } + + /** + * 把报价内容摘要为定长片段,作为配方 id 的内容特征部分。 + * + *

编码复用 {@link AlloyFurnaceRecipeFingerprint#safeItemStack},它会递归规范化组件映射, + * 因此组件内部任何迭代顺序差异都不会影响结果。数量必须计入——成本数量是配方输入的一部分, + * 同物品同组件而数量不同的报价是两条不同配方。

+ */ + private static String offerKey(MerchantOffer offer, HolderLookup.Provider registries) { + StringBuilder value = new StringBuilder(); + value.append(AlloyFurnaceRecipeFingerprint.safeItemStack(offer.getResult(), registries)); + value.append('|').append(AlloyFurnaceRecipeFingerprint.safeItemStack( + offer.getBaseCostA(), registries)); + if (!offer.getCostB().isEmpty()) value.append('|') + .append(AlloyFurnaceRecipeFingerprint.safeItemStack(offer.getCostB(), registries)); + return AdapterUtils.stableHash(value.toString()); + } + + private static String pathPart(String value) { + StringBuilder result = new StringBuilder(value.length()); + for (int index = 0; index < value.length(); index++) { + char character = Character.toLowerCase(value.charAt(index)); + result.append(character >= 'a' && character <= 'z' + || character >= '0' && character <= '9' + || character == '_' || character == '.' || character == '-' + ? character : '_'); + } + return result.isEmpty() ? "unknown" : result.toString(); + } + + private record ProfessionWorkstation(VillagerProfession profession, List workstations) { + } + + private record TradeContext(@Nullable VillagerType villagerType) { + } + + private record Cached( + Level level, + boolean experimental, + int tradeTableSignature, + List> recipes) { + } + + /** Random source used to evaluate the lower endpoint of vanilla random trade prices. */ + private static final class MinimumRandomSource implements RandomSource { + @Override + public RandomSource fork() { + return new MinimumRandomSource(); + } + + @Override + public PositionalRandomFactory forkPositional() { + return new MinimumPositionalRandomFactory(); + } + + @Override + public void setSeed(long seed) { + } + + @Override + public int nextInt() { + return 0; + } + + @Override + public int nextInt(int bound) { + if (bound <= 0) { + throw new IllegalArgumentException("bound must be positive"); + } + return 0; + } + + @Override + public long nextLong() { + return 0L; + } + + @Override + public boolean nextBoolean() { + return false; + } + + @Override + public float nextFloat() { + return 0.0F; + } + + @Override + public double nextDouble() { + return 0.0D; + } + + @Override + public double nextGaussian() { + return 0.0D; + } + } + + private static final class MinimumPositionalRandomFactory implements PositionalRandomFactory { + @Override + public RandomSource fromHashOf(String name) { + return new MinimumRandomSource(); + } + + public RandomSource fromSeed(long seed) { + return new MinimumRandomSource(); + } + + @Override + public RandomSource at(int x, int y, int z) { + return new MinimumRandomSource(); + } + + @Override + public void parityConfigString(StringBuilder builder) { + builder.append("MinimumRandomSource"); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/minecraft/VillagerTradeSyntheticRecipe.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/minecraft/VillagerTradeSyntheticRecipe.java new file mode 100644 index 00000000..cda231a7 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/minecraft/VillagerTradeSyntheticRecipe.java @@ -0,0 +1,55 @@ +package com.sorrowmist.useless.content.recipe.adapters.minecraft; + +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.Container; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; + +/** Recipe holder payload for villager offers generated from the current trade tables. */ +public final class VillagerTradeSyntheticRecipe implements Recipe { + private final AdvancedAlloyFurnaceRecipe convertedRecipe; + + public VillagerTradeSyntheticRecipe(AdvancedAlloyFurnaceRecipe convertedRecipe) { + this.convertedRecipe = convertedRecipe; + } + + public AdvancedAlloyFurnaceRecipe convertedRecipe() { + return convertedRecipe; + } + + @Override + public net.minecraft.resources.ResourceLocation getId() { return convertedRecipe.id(); } + + @Override + public boolean matches(Container input, Level level) { + return convertedRecipe != null && convertedRecipe.matches(input, level); + } + + @Override + public ItemStack assemble(Container input, net.minecraft.core.RegistryAccess registries) { + return convertedRecipe == null ? ItemStack.EMPTY : convertedRecipe.assemble(input, registries); + } + + @Override + public boolean canCraftInDimensions(int width, int height) { + return convertedRecipe != null && convertedRecipe.canCraftInDimensions(width, height); + } + + @Override + public ItemStack getResultItem(net.minecraft.core.RegistryAccess registries) { + return convertedRecipe == null ? ItemStack.EMPTY : convertedRecipe.getResultItem(registries); + } + + @Override + public RecipeSerializer getSerializer() { + return convertedRecipe == null ? null : convertedRecipe.getSerializer(); + } + + @Override + public RecipeType getType() { + return convertedRecipe == null ? null : convertedRecipe.getType(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/modernindustrialization/ForgeHammerRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/modernindustrialization/ForgeHammerRecipeAdapter.java new file mode 100644 index 00000000..3a22cec5 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/modernindustrialization/ForgeHammerRecipeAdapter.java @@ -0,0 +1,105 @@ +package com.sorrowmist.useless.content.recipe.adapters.modernindustrialization; + +import aztech.modern_industrialization.MIRegistries; +import aztech.modern_industrialization.blocks.forgehammer.ForgeHammerRecipe; +import aztech.modern_industrialization.items.ForgeTool; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Modern Industrialization Forge Hammer recipes to alloy-furnace recipes. */ +public final class ForgeHammerRecipeAdapter implements IRecipeAdapter { + private static final ResourceLocation FORGE_HAMMER_ID = + new ResourceLocation("modern_industrialization", "forge_hammer"); + + @Override + public String sourceId() { + return RecipeSourceIds.MODERN_INDUSTRIALIZATION; + } + + @Override + public Class getRecipeClass() { + return ForgeHammerRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + ItemStack mold = new ItemStack(BuiltInRegistries.ITEM.get(FORGE_HAMMER_ID)); + return mold.isEmpty() ? null : mold; + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) return List.of(); + + ForgeHammerRecipe source = holder.value(); + if (source.ingredient() == null || source.ingredient().isEmpty() + || source.count() <= 0 || source.result().isEmpty()) { + return List.of(); + } + + List molds = new ArrayList<>(); + Ingredient machineMold = AdapterUtils.toMoldIngredient(getMoldItem()); + if (machineMold.isEmpty()) return List.of(); + molds.add(machineMold); + if (source.hammerDamage() > 0) { + molds.add(Ingredient.of(ForgeTool.TAG)); + } + + AdvancedAlloyFurnaceRecipe converted = new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + List.of(new CountedIngredient(source.ingredient(), source.count())), + List.of(), + List.of(), + List.of(source.result().copy()), + List.of(), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + AdapterUtils.DEFAULT_PROCESS_TIME, + Ingredient.EMPTY, + 0, + molds, + AlloyFurnaceMode.NORMAL); + return List.of(converted); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold) || mergedInputs == null || mergedInputs.isEmpty()) { + return List.of(); + } + + RecipeManager manager = level.getRecipeManager(); + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(manager, + MIRegistries.FORGE_HAMMER_RECIPE_TYPE.get())) { + ForgeHammerRecipe source = holder.value(); + if (source != null && source.ingredient() != null && !source.ingredient().isEmpty() + && AdapterUtils.hasMatchingIngredient( + mergedInputs, source.ingredient(), source.count())) { + matches.add(holder); + } + } + return matches; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/modernindustrialization/ModernIndustrializationRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/modernindustrialization/ModernIndustrializationRecipeAdapter.java new file mode 100644 index 00000000..890caa3d --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/modernindustrialization/ModernIndustrializationRecipeAdapter.java @@ -0,0 +1,481 @@ +package com.sorrowmist.useless.content.recipe.adapters.modernindustrialization; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import aztech.modern_industrialization.machines.init.MIMachineRecipeTypes; +import aztech.modern_industrialization.machines.recipe.MachineRecipe; +import aztech.modern_industrialization.machines.recipe.MachineRecipeType; +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.FluidIngredientAllocator; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.BucketItem; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.FluidUtil; +import com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; + +/** Converts Modern Industrialization machine recipes to alloy-furnace recipes. */ +public final class ModernIndustrializationRecipeAdapter implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final String MOD_ID = "modern_industrialization"; + private static final Map> MACHINE_MOLDS = createMachineMolds(); + private static final Map TYPE_BY_MACHINE_ITEM = indexMachineItems(); + + @Override + public String sourceId() { + return RecipeSourceIds.MODERN_INDUSTRIALIZATION; + } + + @Override + public Class getRecipeClass() { + return MachineRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return null; + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + return mold != null && !mold.isEmpty() && TYPE_BY_MACHINE_ITEM.containsKey(mold.getItem()); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) return List.of(); + + MachineRecipe source = holder.value(); + MachineRecipeType type = machineRecipeType(source); + if (type == null) return List.of(); + List machineMolds = MACHINE_MOLDS.get(type); + if (machineMolds == null || source.duration <= 0 || source.eu < 0) { + return List.of(); + } + + OptionalInt operations = calculateBatchOperations(source); + if (operations.isEmpty()) { + warnSkipped(holder, "probability batch cannot be represented"); + return List.of(); + } + + Optional machineMold = machineMold(machineMolds); + if (machineMold.isEmpty()) { + warnSkipped(holder, "no listed machine variant is registered"); + return List.of(); + } + + ConversionData data = convertData(source, operations.getAsInt(), machineMold.get()); + if (data == null) { + warnSkipped(holder, "recipe contains an unsupported or overflowing input/output"); + return List.of(); + } + + try { + long energy = Math.multiplyExact(source.getTotalEu(), (long) operations.getAsInt()); + long processTimeLong = Math.multiplyExact((long) source.duration, operations.getAsInt()); + if (processTimeLong <= 0 || processTimeLong > Integer.MAX_VALUE) { + warnSkipped(holder, "scaled process time overflows"); + return List.of(); + } + + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + data.inputs(), + data.inputFluids(), + List.of(), + data.outputs(), + data.outputFluids(), + List.of(), + energy, + (int) processTimeLong, + Ingredient.EMPTY, + 0, + data.molds(), + AlloyFurnaceMode.NORMAL)); + } catch (ArithmeticException exception) { + warnSkipped(holder, "scaled energy overflows"); + return List.of(); + } + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) return List.of(); + + MachineRecipeType type = TYPE_BY_MACHINE_ITEM.get(mold.getItem()); + if (type == null) return List.of(); + + Map safeInputs = mergedInputs == null ? Map.of() : mergedInputs; + Map safeFluids = mergedFluids == null ? Map.of() : mergedFluids; + RecipeManager manager = level.getRecipeManager(); + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(manager, type)) { + List converted = convertAll(holder, level); + if (converted.stream().anyMatch(recipe -> matchesInputs(recipe, safeInputs, safeFluids) + && AdapterUtils.matchesMold(recipe.mold(), mold))) { + matches.add(holder); + } + } + return matches; + } + + private static boolean matchesInputs(AdvancedAlloyFurnaceRecipe recipe, + Map mergedInputs, + Map mergedFluids) { + Map required = new LinkedHashMap<>(); + for (CountedIngredient input : recipe.inputs()) { + AdapterUtils.mergeIngredient(required, input.ingredient(), input.count()); + } + return AdapterUtils.matchesRequired(mergedInputs, required) + && FluidIngredientAllocator.matchesLong(recipe.inputFluids(), mergedFluids, 1L); + } + + @Nullable + private static ConversionData convertData(MachineRecipe source, int operations, + Ingredient machineMold) { + Map inputCounts = new LinkedHashMap<>(); + List inputFluids = new ArrayList<>(); + List outputs = new ArrayList<>(); + List outputFluids = new ArrayList<>(); + List molds = new ArrayList<>(); + molds.add(machineMold); + + for (MachineRecipe.ItemInput input : source.itemInputs) { + Rational probability = probability(input.probability()); + if (probability == null) return null; + if (probability.isZero()) { + if (!addRepeatedMolds(molds, input.ingredient(), input.amount())) return null; + continue; + } + long amount = scaleAmount(input.amount(), probability, operations); + if (amount <= 0) return null; + AdapterUtils.mergeIngredient(inputCounts, input.ingredient(), amount); + } + + for (MachineRecipe.FluidInput input : source.fluidInputs) { + Rational probability = probability(input.probability()); + if (probability == null) return null; + if (probability.isZero()) { + Optional bucketMold = bucketMold(input.fluid()); + bucketMold.ifPresent(molds::add); + continue; + } + long amount = scaleAmount(input.amount(), probability, operations); + if (amount <= 0 || amount > Integer.MAX_VALUE) return null; + if (!mergeFluidInput(inputFluids, input.fluid(), (int) amount)) return null; + } + + if (inputCounts.isEmpty() && inputFluids.isEmpty()) { + inputFluids.add(new SizedFluidIngredient( + FluidIngredient.single(Fluids.WATER), operations)); + } + + for (MachineRecipe.ItemOutput output : source.itemOutputs) { + Rational probability = probability(output.probability()); + if (probability == null) return null; + if (probability.isZero()) continue; + ItemStack stack = output.getStack(); + if (stack == null || stack.isEmpty()) return null; + long amount = scaleAmount(stack.getCount(), probability, operations); + if (amount <= 0 || amount > Integer.MAX_VALUE) return null; + if (!mergeItemOutput(outputs, stack.copyWithCount((int) amount))) return null; + } + + for (MachineRecipe.FluidOutput output : source.fluidOutputs) { + Rational probability = probability(output.probability()); + if (probability == null) return null; + if (probability.isZero()) continue; + long amount = scaleAmount(output.amount(), probability, operations); + if (amount <= 0 || amount > Integer.MAX_VALUE) return null; + if (!mergeFluidOutput(outputFluids, + new FluidStack(output.fluid(), (int) amount))) return null; + } + + if (outputs.isEmpty() && outputFluids.isEmpty()) return null; + + List inputs = inputCounts.entrySet().stream() + .map(entry -> new CountedIngredient(entry.getKey(), entry.getValue())) + .toList(); + return new ConversionData(inputs, List.copyOf(inputFluids), List.copyOf(outputs), + List.copyOf(outputFluids), List.copyOf(molds)); + } + + private static OptionalInt calculateBatchOperations(MachineRecipe source) { + long operations = 1L; + for (MachineRecipe.ItemInput input : source.itemInputs) { + operations = addProbabilityDenominator(operations, input.probability()); + if (operations <= 0) return OptionalInt.empty(); + } + for (MachineRecipe.FluidInput input : source.fluidInputs) { + operations = addProbabilityDenominator(operations, input.probability()); + if (operations <= 0) return OptionalInt.empty(); + } + for (MachineRecipe.ItemOutput output : source.itemOutputs) { + operations = addProbabilityDenominator(operations, output.probability()); + if (operations <= 0) return OptionalInt.empty(); + } + for (MachineRecipe.FluidOutput output : source.fluidOutputs) { + operations = addProbabilityDenominator(operations, output.probability()); + if (operations <= 0) return OptionalInt.empty(); + } + return operations <= Integer.MAX_VALUE ? OptionalInt.of((int) operations) : OptionalInt.empty(); + } + + private static long addProbabilityDenominator(long current, float rawProbability) { + Rational probability = probability(rawProbability); + if (probability == null) return -1L; + if (probability.isZero() || probability.isOne()) return current; + + long divisor = AdapterUtils.gcd(current, probability.denominator()); + long factor = probability.denominator() / divisor; + if (current > Integer.MAX_VALUE / factor) return -1L; + return current * factor; + } + + @Nullable + static Rational probability(float rawProbability) { + return probability(Float.toString(rawProbability), rawProbability); + } + + static Rational probability(double rawProbability) { + return probability(Double.toString(rawProbability), rawProbability); + } + + private static Rational probability(String rawText, double rawProbability) { + if (!Double.isFinite(rawProbability) || rawProbability < 0.0d || rawProbability > 1.0d) { + return null; + } + BigDecimal decimal = new BigDecimal(rawText).stripTrailingZeros(); + int scale = Math.max(0, decimal.scale()); + BigInteger denominator = BigInteger.TEN.pow(scale); + BigInteger numerator = decimal.movePointRight(scale).toBigInteger(); + BigInteger divisor = numerator.gcd(denominator); + denominator = denominator.divide(divisor); + numerator = numerator.divide(divisor); + if (denominator.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0 + || numerator.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0) { + return null; + } + return new Rational(numerator.longValueExact(), denominator.longValueExact()); + } + + static Rational divide(Rational value, long divisor) { + if (value == null || value.numerator() < 0L || value.denominator() <= 0L || divisor <= 0L) { + return null; + } + BigInteger numerator = BigInteger.valueOf(value.numerator()); + BigInteger denominator = BigInteger.valueOf(value.denominator()).multiply(BigInteger.valueOf(divisor)); + BigInteger common = numerator.gcd(denominator); + return new Rational( + numerator.divide(common).longValueExact(), + denominator.divide(common).longValueExact()); + } + + static long scaleAmount(long amount, Rational probability, long operations) { + if (amount <= 0 || probability.isZero()) return 0L; + long factor = operations / probability.denominator(); + try { + return Math.multiplyExact(Math.multiplyExact(amount, probability.numerator()), factor); + } catch (ArithmeticException exception) { + return -1L; + } + } + + private static boolean addRepeatedMolds(List molds, Ingredient mold, int amount) { + if (mold == null || mold.isEmpty() || amount <= 0) return false; + for (int i = 0; i < amount; i++) molds.add(mold); + return true; + } + + private static boolean mergeFluidInput(List target, + FluidIngredient ingredient, int amount) { + if (ingredient == null || ingredient.isEmpty() || amount <= 0) return false; + for (int i = 0; i < target.size(); i++) { + SizedFluidIngredient existing = target.get(i); + if (!existing.ingredient().equals(ingredient)) continue; + long merged = (long) existing.amount() + amount; + if (merged > Integer.MAX_VALUE) return false; + target.set(i, new SizedFluidIngredient(ingredient, (int) merged)); + return true; + } + target.add(new SizedFluidIngredient(ingredient, amount)); + return true; + } + + private static boolean mergeItemOutput(List target, ItemStack output) { + for (ItemStack existing : target) { + if (!ItemStack.isSameItemSameTags(existing, output)) continue; + long merged = (long) existing.getCount() + output.getCount(); + if (merged > Integer.MAX_VALUE) return false; + existing.setCount((int) merged); + return true; + } + target.add(output.copy()); + return true; + } + + private static boolean mergeFluidOutput(List target, FluidStack output) { + for (FluidStack existing : target) { + if (!com.sorrowmist.useless.compat.forge1201.fluid.FluidStacks.same(existing, output)) continue; + long merged = (long) existing.getAmount() + output.getAmount(); + if (merged > Integer.MAX_VALUE) return false; + existing.setAmount((int) merged); + return true; + } + target.add(output.copy()); + return true; + } + + private static Optional bucketMold(FluidIngredient ingredient) { + if (ingredient == null || ingredient.isEmpty()) return Optional.empty(); + Map buckets = new LinkedHashMap<>(); + try { + for (FluidStack stack : ingredient.getStacks()) { + addBucket(buckets, stack == null ? null : stack.getFluid()); + } + } catch (RuntimeException ignored) { + // Registry probing below can still resolve tag-backed fluid ingredients. + } + if (!buckets.isEmpty()) { + return Optional.of(Ingredient.of(buckets.values().toArray(ItemStack[]::new))); + } + for (Fluid fluid : BuiltInRegistries.FLUID) { + FluidStack candidate = new FluidStack(fluid, 1000); + try { + if (ingredient.test(candidate)) addBucket(buckets, fluid); + } catch (RuntimeException ignored) { + // Some custom ingredients do not support arbitrary registry probing. + } + } + if (buckets.isEmpty()) return Optional.empty(); + return Optional.of(Ingredient.of(buckets.values().toArray(ItemStack[]::new))); + } + + private static void addBucket(Map buckets, @Nullable Fluid fluid) { + if (fluid == null) return; + ItemStack bucket = FluidUtil.getFilledBucket(new FluidStack(fluid, 1000)); + if (bucket.isEmpty() || !(bucket.getItem() instanceof BucketItem)) return; + buckets.putIfAbsent(bucket.getItem(), bucket.copyWithCount(1)); + } + + @Nullable + private static MachineRecipeType machineRecipeType(MachineRecipe recipe) { + return recipe.getType() instanceof MachineRecipeType type ? type : null; + } + + private static Optional machineMold(List machineMolds) { + Map stacks = new LinkedHashMap<>(); + for (String machineMold : machineMolds) { + Item item = BuiltInRegistries.ITEM.getOptional(id(machineMold)).orElse(null); + if (item == null || item == Items.AIR) continue; + stacks.putIfAbsent(item, item.getDefaultInstance()); + } + if (stacks.isEmpty()) return Optional.empty(); + return Optional.of(Ingredient.of(stacks.values().toArray(ItemStack[]::new))); + } + + private static Map> createMachineMolds() { + return Map.ofEntries( + Map.entry(MIMachineRecipeTypes.ASSEMBLER, fixed("assembler")), + Map.entry(MIMachineRecipeTypes.CENTRIFUGE, fixed("centrifuge")), + Map.entry(MIMachineRecipeTypes.CHEMICAL_REACTOR, fixed("chemical_reactor")), + Map.entry(MIMachineRecipeTypes.COMPRESSOR, fixed( + "bronze_compressor", "steel_compressor", "electric_compressor")), + Map.entry(MIMachineRecipeTypes.CUTTING_MACHINE, fixed( + "bronze_cutting_machine", "steel_cutting_machine", "electric_cutting_machine")), + Map.entry(MIMachineRecipeTypes.DISTILLERY, fixed("distillery")), + Map.entry(MIMachineRecipeTypes.ELECTROLYZER, fixed("electrolyzer")), + Map.entry(MIMachineRecipeTypes.MACERATOR, fixed( + "bronze_macerator", "steel_macerator", "electric_macerator")), + Map.entry(MIMachineRecipeTypes.MIXER, fixed( + "bronze_mixer", "steel_mixer", "electric_mixer")), + Map.entry(MIMachineRecipeTypes.PACKER, fixed("steel_packer", "electric_packer")), + Map.entry(MIMachineRecipeTypes.POLARIZER, fixed("polarizer")), + Map.entry(MIMachineRecipeTypes.UNPACKER, fixed("steel_unpacker", "electric_unpacker")), + Map.entry(MIMachineRecipeTypes.WIREMILL, fixed("steel_wiremill", "electric_wiremill")), + Map.entry(MIMachineRecipeTypes.BLAST_FURNACE, fixed( + "steam_blast_furnace", "electric_blast_furnace")), + Map.entry(MIMachineRecipeTypes.COKE_OVEN, fixed("coke_oven")), + Map.entry(MIMachineRecipeTypes.DISTILLATION_TOWER, fixed("distillation_tower")), + Map.entry(MIMachineRecipeTypes.FUSION_REACTOR, fixed("fusion_reactor")), + Map.entry(MIMachineRecipeTypes.HEAT_EXCHANGER, fixed("heat_exchanger")), + Map.entry(MIMachineRecipeTypes.IMPLOSION_COMPRESSOR, fixed("implosion_compressor")), + Map.entry(MIMachineRecipeTypes.OIL_DRILLING_RIG, fixed("oil_drilling_rig")), + Map.entry(MIMachineRecipeTypes.PRESSURIZER, fixed("pressurizer")), + Map.entry(MIMachineRecipeTypes.QUARRY, fixed("steam_quarry", "electric_quarry")), + Map.entry(MIMachineRecipeTypes.VACUUM_FREEZER, fixed("vacuum_freezer"))); + } + + private static List fixed(String... ids) { + return List.of(ids); + } + + private static Map indexMachineItems() { + Map result = new LinkedHashMap<>(); + for (Map.Entry> entry : MACHINE_MOLDS.entrySet()) { + for (String machineMold : entry.getValue()) { + Item item = BuiltInRegistries.ITEM.getOptional(id(machineMold)).orElse(null); + if (item != null && item != Items.AIR) result.put(item, entry.getKey()); + } + } + return Map.copyOf(result); + } + + private static ResourceLocation id(String path) { + return new ResourceLocation(MOD_ID, path); + } + + private static void warnSkipped(RecipeHolder holder, String reason) { + RateLimitedLogger.warn(LOGGER, "Skipping Modern Industrialization recipe {}: {}", holder.id(), reason); + } + + record Rational(long numerator, long denominator) { + boolean isZero() { + return numerator == 0L; + } + + boolean isOne() { + return numerator == denominator; + } + } + + private record ConversionData( + List inputs, + List inputFluids, + List outputs, + List outputFluids, + List molds) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/modernindustrialization/NuclearAbsorptionRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/modernindustrialization/NuclearAbsorptionRecipeAdapter.java new file mode 100644 index 00000000..0f994683 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/modernindustrialization/NuclearAbsorptionRecipeAdapter.java @@ -0,0 +1,301 @@ +package com.sorrowmist.useless.content.recipe.adapters.modernindustrialization; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import aztech.modern_industrialization.nuclear.FluidNuclearComponent; +import aztech.modern_industrialization.nuclear.NuclearAbsorbable; +import aztech.modern_industrialization.thirdparty.fabrictransfer.api.fluid.FluidVariant; +import aztech.modern_industrialization.thirdparty.fabrictransfer.api.item.ItemVariant; +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.FluidIngredientAllocator; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.ItemIngredientAllocator; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluid; +import com.sorrowmist.useless.compat.forge1201.recipe.DataComponentIngredient; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Converts Modern Industrialization's dynamic neutron absorption into deterministic recipes. */ +public final class NuclearAbsorptionRecipeAdapter + implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final String MOD_ID = "modern_industrialization"; + private static final long ENERGY_PER_NEUTRON = 1_000L; + private static final int DEFAULT_PROCESS_TIME = AdapterUtils.DEFAULT_PROCESS_TIME; + private static final ResourceLocation NUCLEAR_REACTOR_ID = + new ResourceLocation(MOD_ID, "nuclear_reactor"); + private volatile List> cachedRecipes; + + @Override + public String sourceId() { + return RecipeSourceIds.MODERN_INDUSTRIALIZATION; + } + + @Override + public Class getRecipeClass() { + return NuclearAbsorptionSyntheticRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + Item item = BuiltInRegistries.ITEM.getOptional(NUCLEAR_REACTOR_ID).orElse(null); + return item == null ? null : item.getDefaultInstance(); + } + + @Override + public List> getGeneratedRecipes(Level level) { + List> cached = cachedRecipes; + if (cached != null) return cached; + + synchronized (this) { + if (cachedRecipes == null) { + cachedRecipes = createGeneratedRecipes(); + } + return cachedRecipes; + } + } + + private List> createGeneratedRecipes() { + List> generated = new ArrayList<>(); + for (Item item : BuiltInRegistries.ITEM) { + if (!(item instanceof NuclearAbsorbable absorbable)) continue; + AdvancedAlloyFurnaceRecipe converted; + try { + converted = convertItem(absorbable, item); + } catch (RuntimeException exception) { + RateLimitedLogger.warn(LOGGER, "Skipping Modern Industrialization nuclear item conversion: {}", + BuiltInRegistries.ITEM.getKey(item), exception); + continue; + } + if (converted != null) { + generated.add(new RecipeHolder<>(converted.id(), + new NuclearAbsorptionSyntheticRecipe(converted))); + } + } + for (Fluid fluid : BuiltInRegistries.FLUID) { + AdvancedAlloyFurnaceRecipe converted; + try { + converted = convertFluid(fluid); + } catch (RuntimeException exception) { + RateLimitedLogger.warn(LOGGER, "Skipping Modern Industrialization nuclear fluid conversion: {}", + BuiltInRegistries.FLUID.getKey(fluid), exception); + continue; + } + if (converted != null) { + generated.add(new RecipeHolder<>(converted.id(), + new NuclearAbsorptionSyntheticRecipe(converted))); + } + } + return List.copyOf(generated); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null || holder.value().convertedRecipe() == null) { + return List.of(); + } + return List.of(holder.value().convertedRecipe()); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + return findMatchingRecipes(level, mergedInputs, mergedFluids, Map.of(), mold, List.of()); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, + Map mergedKeys, + @Nullable ItemStack mold, List actualInputs) { + if (level == null || !matchesMold(mold)) return List.of(); + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : getGeneratedRecipes(level)) { + AdvancedAlloyFurnaceRecipe recipe = holder.value().convertedRecipe(); + if (matches(recipe, mergedInputs, mergedFluids, actualInputs)) { + matches.add(holder); + } + } + return matches; + } + + private static boolean matches(AdvancedAlloyFurnaceRecipe recipe, + Map mergedInputs, + Map mergedFluids, + List actualInputs) { + boolean items = actualInputs != null && !actualInputs.isEmpty() + ? ItemIngredientAllocator.matches(recipe.inputs(), actualInputs, 1L) + : matchesItems(recipe.inputs(), mergedInputs); + return items && FluidIngredientAllocator.matchesLong(recipe.inputFluids(), mergedFluids, 1L); + } + + private static boolean matchesItems(List requirements, + Map mergedInputs) { + Map required = new LinkedHashMap<>(); + for (CountedIngredient input : requirements) { + AdapterUtils.mergeIngredient(required, input.ingredient(), input.count()); + } + return AdapterUtils.matchesRequired(mergedInputs == null ? Map.of() : mergedInputs, required); + } + + @Nullable + private static AdvancedAlloyFurnaceRecipe convertItem(NuclearAbsorbable absorbable, Item item) { + int absorptions = absorbable.desintegrationMax; + if (absorptions <= 0) return null; + + if (!(absorbable.getNeutronProduct() instanceof ItemVariant product)) { + return null; + } + long productAmount = absorbable.getNeutronProductAmount(); + if (productAmount <= 0 || productAmount > Integer.MAX_VALUE) return null; + + ItemStack input = item.getDefaultInstance(); + try { + absorbable.setRemainingDesintegrations(input, absorptions); + } catch (RuntimeException exception) { + return null; + } + + ItemStack output = product.toStack((int) productAmount); + if (output.isEmpty()) return null; + + Long energy = multiply(ENERGY_PER_NEUTRON, absorptions); + Integer processTime = multiplyToInt(DEFAULT_PROCESS_TIME, absorptions); + if (energy == null || processTime == null) return null; + + ResourceLocation itemId = BuiltInRegistries.ITEM.getKey(item); + if (itemId == null) return null; + return createRecipe( + new ResourceLocation(MOD_ID, + "nuclear_absorption/item/" + path(itemId)), + List.of(new CountedIngredient(DataComponentIngredient.of(true, input), 1L)), + List.of(), + List.of(output), + List.of(), + energy, + processTime); + } + + @Nullable + private static AdvancedAlloyFurnaceRecipe convertFluid(Fluid fluid) { + FluidNuclearComponent component = FluidNuclearComponent.get(fluid); + if (component == null || component.getNeutronProduct() == null + || component.getNeutronProductAmount() <= 0) { + return null; + } + + if (!(component.getNeutronProduct() instanceof FluidVariant product) + || product.getFluid() == null) return null; + ModernIndustrializationRecipeAdapter.Rational productProbability = + ModernIndustrializationRecipeAdapter.probability( + component.getNeutronProductProbability()); + ModernIndustrializationRecipeAdapter.Rational consumptionProbability = + ModernIndustrializationRecipeAdapter.divide( + productProbability, 81L); + if (consumptionProbability == null || consumptionProbability.isZero()) return null; + + long operations = consumptionProbability.denominator(); + long consumedAmount = ModernIndustrializationRecipeAdapter.scaleAmount( + 1L, consumptionProbability, operations); + long outputAmount = multiply(consumedAmount, component.getNeutronProductAmount()); + Long energy = multiply(ENERGY_PER_NEUTRON, operations); + Integer processTime = multiplyToInt(DEFAULT_PROCESS_TIME, operations); + if (consumedAmount <= 0 || consumedAmount > Integer.MAX_VALUE + || outputAmount <= 0 || outputAmount > Integer.MAX_VALUE + || energy == null || processTime == null) { + return null; + } + + ResourceLocation sourceId = BuiltInRegistries.FLUID.getKey(fluid); + FluidVariant source = component.getVariant(); + if (sourceId == null || source == null || source.getFluid() == null) return null; + List inputFluids = List.of(new SizedFluidIngredient( + FluidIngredient.single(source.toStack(1)), (int) consumedAmount)); + List outputFluids = List.of(product.toStack((int) outputAmount)); + return createRecipe( + new ResourceLocation(MOD_ID, + "nuclear_absorption/fluid/" + path(sourceId)), + List.of(), + inputFluids, + List.of(), + outputFluids, + energy, + processTime); + } + + @Nullable + private static AdvancedAlloyFurnaceRecipe createRecipe( + ResourceLocation id, + List inputs, + List inputFluids, + List outputs, + List outputFluids, + long energy, + int processTime) { + Ingredient mold = AdapterUtils.toMoldIngredient( + BuiltInRegistries.ITEM.getOptional(NUCLEAR_REACTOR_ID) + .map(Item::getDefaultInstance).orElse(ItemStack.EMPTY)); + if (mold.isEmpty()) return null; + + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(id), + inputs, + inputFluids, + List.of(), + outputs, + outputFluids, + List.of(), + energy, + processTime, + Ingredient.EMPTY, + 0, + List.of(mold), + AlloyFurnaceMode.NORMAL); + } + + @Nullable + private static Long multiply(long left, long right) { + try { + return Math.multiplyExact(left, right); + } catch (ArithmeticException exception) { + RateLimitedLogger.warn(LOGGER, "Skipping overflowing Modern Industrialization nuclear conversion"); + return null; + } + } + + @Nullable + private static Integer multiplyToInt(int left, long right) { + Long result = multiply(left, right); + return result == null || result > Integer.MAX_VALUE ? null : result.intValue(); + } + + private static String path(ResourceLocation id) { + return id.getNamespace() + "_" + id.getPath().replace('/', '_'); + } + +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/modernindustrialization/NuclearAbsorptionSyntheticRecipe.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/modernindustrialization/NuclearAbsorptionSyntheticRecipe.java new file mode 100644 index 00000000..37d5125d --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/modernindustrialization/NuclearAbsorptionSyntheticRecipe.java @@ -0,0 +1,55 @@ +package com.sorrowmist.useless.content.recipe.adapters.modernindustrialization; + +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.Container; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; + +/** Recipe holder payload for Modern Industrialization's dynamic nuclear interactions. */ +public final class NuclearAbsorptionSyntheticRecipe implements Recipe { + private final AdvancedAlloyFurnaceRecipe convertedRecipe; + + public NuclearAbsorptionSyntheticRecipe(AdvancedAlloyFurnaceRecipe convertedRecipe) { + this.convertedRecipe = convertedRecipe; + } + + public AdvancedAlloyFurnaceRecipe convertedRecipe() { + return convertedRecipe; + } + + @Override + public net.minecraft.resources.ResourceLocation getId() { return convertedRecipe.id(); } + + @Override + public boolean matches(Container input, Level level) { + return convertedRecipe != null && convertedRecipe.matches(input, level); + } + + @Override + public ItemStack assemble(Container input, net.minecraft.core.RegistryAccess registries) { + return convertedRecipe == null ? ItemStack.EMPTY : convertedRecipe.assemble(input, registries); + } + + @Override + public boolean canCraftInDimensions(int width, int height) { + return convertedRecipe != null && convertedRecipe.canCraftInDimensions(width, height); + } + + @Override + public ItemStack getResultItem(net.minecraft.core.RegistryAccess registries) { + return convertedRecipe == null ? ItemStack.EMPTY : convertedRecipe.getResultItem(registries); + } + + @Override + public RecipeSerializer getSerializer() { + return convertedRecipe == null ? null : convertedRecipe.getSerializer(); + } + + @Override + public RecipeType getType() { + return convertedRecipe == null ? null : convertedRecipe.getType(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mysticalagriculture/AwakeningRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mysticalagriculture/AwakeningRecipeAdapter.java new file mode 100644 index 00000000..09929d59 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mysticalagriculture/AwakeningRecipeAdapter.java @@ -0,0 +1,176 @@ +package com.sorrowmist.useless.content.recipe.adapters.mysticalagriculture; + +import com.blakebr0.mysticalagriculture.api.crafting.IAwakeningRecipe; +import com.blakebr0.mysticalagriculture.init.ModRecipeTypes; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Mystical Agriculture 觉醒祭坛配方适配器 + *

+ * 将觉醒祭坛配方转换为高级合金熔炉配方 + *

+ * 处理逻辑: + * - 祭坛物品(input) + 基座物品(ingredients) + 精华(essences,含数量) → 普通输入(合并相同物品,全部消耗) + * - 产物(result) → 输出 + * - mysticalagriculture:awakening_altar → 模具(不消耗) + */ +public class AwakeningRecipeAdapter implements IRecipeAdapter { + + @Override + public Class getRecipeClass() { + return IAwakeningRecipe.class; + } + + @Nullable + @Override + public ItemStack getMoldItem() { + return new ItemStack( + BuiltInRegistries.ITEM.get( + new ResourceLocation("mysticalagriculture", "awakening_altar"))); + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + return mold != null + && BuiltInRegistries.ITEM.getKey(mold.getItem()).equals( + new ResourceLocation("mysticalagriculture", "awakening_altar")); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder == null) return List.of(); + + IAwakeningRecipe recipe = holder.value(); + ResourceLocation originalId = holder.id(); + + Ingredient altarIngredient = recipe.getAltarIngredient(); + List allIngredients = recipe.getIngredients(); + List essences = recipe.getEssences(); + ItemStack output = recipe.getResultItem(null); + + if (output.isEmpty()) { + return List.of(); + } + + // 合并所有输入(祭坛物品 + 基座物品 + 精华) + Map ingredientCounts = new LinkedHashMap<>(); + + if (!altarIngredient.isEmpty()) { + AdapterUtils.mergeIngredient(ingredientCounts, altarIngredient, 1L); + } + + // 基座物品在 getIngredients() 中位于奇数索引 (1,3,5,7) + for (int i = 1; i < allIngredients.size(); i += 2) { + Ingredient ing = allIngredients.get(i); + if (!ing.isEmpty()) { + AdapterUtils.mergeIngredient(ingredientCounts, ing, 1L); + } + } + + // 精华物品(附带数量) + for (ItemStack essence : essences) { + if (!essence.isEmpty()) { + AdapterUtils.mergeIngredient(ingredientCounts, Ingredient.of(essence), (long) essence.getCount()); + } + } + + if (ingredientCounts.isEmpty()) { + return List.of(); + } + + List countedIngredients = new ArrayList<>(); + for (Map.Entry entry : ingredientCounts.entrySet()) { + countedIngredients.add(new CountedIngredient(entry.getKey(), entry.getValue())); + } + + int processTime = AdapterUtils.DEFAULT_PROCESS_TIME + (allIngredients.size() / 2 + essences.size()) * 15; + + AdvancedAlloyFurnaceRecipe convertedRecipe = new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(originalId), + countedIngredients, + List.of(), + List.of(output.copy()), + List.of(), + 5000, + processTime, + Ingredient.EMPTY, + 0, + AdapterUtils.toMoldIngredient(getMoldItem()), + AlloyFurnaceMode.NORMAL + ); + + return List.of(convertedRecipe); + } + + @Override + @Nullable + @SuppressWarnings("unchecked") + public List> findMatchingRecipes(Level level, Map mergedInputs, Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || mergedInputs.isEmpty()) { + return List.of(); + } + + if (!matchesMold(mold)) { + return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + List> recipes = (List>) (List) com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + ModRecipeTypes.AWAKENING.get() + ); + + List> matches = new java.util.ArrayList<>(); + for (RecipeHolder holder : recipes) { + IAwakeningRecipe recipe = holder.value(); + + Ingredient altarIngredient = recipe.getAltarIngredient(); + List allIngredients = recipe.getIngredients(); + List essences = recipe.getEssences(); + ItemStack output = recipe.getResultItem(null); + + if (output.isEmpty()) continue; + + // 合并所有需要的配料 + Map requiredCounts = new LinkedHashMap<>(); + if (!altarIngredient.isEmpty()) { + AdapterUtils.mergeIngredient(requiredCounts, altarIngredient, 1L); + } + for (int i = 1; i < allIngredients.size(); i += 2) { + Ingredient ing = allIngredients.get(i); + if (!ing.isEmpty()) { + AdapterUtils.mergeIngredient(requiredCounts, ing, 1L); + } + } + for (ItemStack essence : essences) { + if (!essence.isEmpty()) { + AdapterUtils.mergeIngredient(requiredCounts, Ingredient.of(essence), (long) essence.getCount()); + } + } + + if (requiredCounts.isEmpty()) continue; + + if (AdapterUtils.matchesRequired(mergedInputs, requiredCounts)) { + matches.add(holder); + } + } + return matches; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mysticalagriculture/InfusionRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mysticalagriculture/InfusionRecipeAdapter.java new file mode 100644 index 00000000..d3701343 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mysticalagriculture/InfusionRecipeAdapter.java @@ -0,0 +1,150 @@ +package com.sorrowmist.useless.content.recipe.adapters.mysticalagriculture; + +import com.blakebr0.mysticalagriculture.api.crafting.IInfusionRecipe; +import com.blakebr0.mysticalagriculture.init.ModRecipeTypes; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Mystical Agriculture 注魔祭坛配方适配器 + *

+ * 将注魔祭坛配方转换为高级合金熔炉配方 + *

+ * 处理逻辑: + * - 祭坛物品(input) + 基座物品(inputs) → 普通输入(合并相同物品,全部消耗) + * - 产物(result) → 输出 + * - mysticalagriculture:infusion_altar → 模具(不消耗) + */ +public class InfusionRecipeAdapter implements IRecipeAdapter { + + @Override + public Class getRecipeClass() { + return IInfusionRecipe.class; + } + + @Nullable + @Override + public ItemStack getMoldItem() { + return new ItemStack( + BuiltInRegistries.ITEM.get( + new ResourceLocation("mysticalagriculture", "infusion_altar"))); + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + return mold != null + && BuiltInRegistries.ITEM.getKey(mold.getItem()).equals( + new ResourceLocation("mysticalagriculture", "infusion_altar")); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder == null) return List.of(); + + IInfusionRecipe recipe = holder.value(); + ResourceLocation originalId = holder.id(); + + Ingredient altarIngredient = recipe.getAltarIngredient(); + List pedestalItems = recipe.getIngredients(); + ItemStack output = recipe.getResultItem(null); + + if (output.isEmpty()) { + return List.of(); + } + + List countedIngredients = AdapterUtils.mergeIngredients( + new ArrayList<>() {{ + if (!altarIngredient.isEmpty()) add(altarIngredient); + for (Ingredient ing : pedestalItems) { + if (!ing.isEmpty()) add(ing); + } + }} + ); + + if (countedIngredients.isEmpty()) { + return List.of(); + } + + int processTime = AdapterUtils.DEFAULT_PROCESS_TIME + pedestalItems.size() * 15; + + AdvancedAlloyFurnaceRecipe convertedRecipe = new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(originalId), + countedIngredients, + List.of(), + List.of(output.copy()), + List.of(), + 2000, + processTime, + Ingredient.EMPTY, + 0, + AdapterUtils.toMoldIngredient(getMoldItem()), + AlloyFurnaceMode.NORMAL + ); + + return List.of(convertedRecipe); + } + + @Override + @Nullable + @SuppressWarnings("unchecked") + public List> findMatchingRecipes(Level level, Map mergedInputs, Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || mergedInputs.isEmpty()) { + return List.of(); + } + + if (!matchesMold(mold)) { + return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + List> recipes = (List>) (List) com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + ModRecipeTypes.INFUSION.get() + ); + + List> matches = new java.util.ArrayList<>(); + for (RecipeHolder holder : recipes) { + IInfusionRecipe recipe = holder.value(); + + Ingredient altarIngredient = recipe.getAltarIngredient(); + List pedestalItems = recipe.getIngredients(); + ItemStack output = recipe.getResultItem(null); + + if (output.isEmpty()) continue; + + // 合并所有需要的配料 + Map requiredCounts = new LinkedHashMap<>(); + if (!altarIngredient.isEmpty()) { + AdapterUtils.mergeIngredient(requiredCounts, altarIngredient, 1L); + } + for (Ingredient ing : pedestalItems) { + if (!ing.isEmpty()) { + AdapterUtils.mergeIngredient(requiredCounts, ing, 1L); + } + } + + if (requiredCounts.isEmpty()) continue; + + if (AdapterUtils.matchesRequired(mergedInputs, requiredCounts)) { + matches.add(holder); + } + } + return matches; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mysticalagriculture/OmniaUniversalSeedRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mysticalagriculture/OmniaUniversalSeedRecipeAdapter.java new file mode 100644 index 00000000..ed068b9d --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mysticalagriculture/OmniaUniversalSeedRecipeAdapter.java @@ -0,0 +1,165 @@ +package com.sorrowmist.useless.content.recipe.adapters.mysticalagriculture; + +import com.omnia.omnia.registry.ModItems; +import com.omnia.omnia.util.TargetHelper; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.Recipe; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.Container; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluids; +import com.sorrowmist.useless.compat.forge1201.recipe.DataComponentIngredient; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Map; + +/** Converts Omnia's component-backed universal crop seed into universal essence. */ +public final class OmniaUniversalSeedRecipeAdapter + implements IRecipeAdapter { + private static final int WATER_AMOUNT = 1; + private static final int ENERGY = 1000; + private static final int PROCESS_TIME = 60; + private static final net.minecraft.resources.ResourceLocation RECIPE_ID = + new net.minecraft.resources.ResourceLocation( + "omnia", "universal_crop_seed_essence"); + + @Override + public Class getRecipeClass() { + return DynamicRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return null; + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + return isUniversalCropSeed(mold) && TargetHelper.hasTarget(mold); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null || holder.value().convertedRecipe() == null) { + return List.of(); + } + return List.of(holder.value().convertedRecipe()); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level, List actualInputs) { + return convertAll(holder, level); + } + + @Override + public List> findMatchingRecipes( + Level level, + Map mergedInputs, + Map mergedFluids, + Map mergedKeys, + @Nullable ItemStack mold, + List actualInputs) { + if (level == null) return List.of(); + + AdvancedAlloyFurnaceRecipe recipe = createRecipe(mold, level.registryAccess()); + return recipe == null + ? List.of() + : List.of(new RecipeHolder<>(RECIPE_ID, new DynamicRecipe(recipe))); + } + + private static boolean isUniversalCropSeed(@Nullable ItemStack stack) { + return stack != null && !stack.isEmpty() + && stack.is(ModItems.UNIVERSAL_CROP_SEED.get()); + } + + /** Builds the target-specific recipe used by direct processing and AE execution. */ + @Nullable + private static AdvancedAlloyFurnaceRecipe createRecipe( + @Nullable ItemStack mold, @Nullable net.minecraft.core.RegistryAccess registries) { + if (!isUniversalCropSeed(mold) || registries == null || !TargetHelper.hasTarget(mold)) { + return null; + } + + String targetId = TargetHelper.getTargetId(mold); + Item targetItem = TargetHelper.getTargetItem(targetId); + ItemStack targetStack = TargetHelper.getTargetStack(mold, registries); + if (targetId == null || targetId.isEmpty() || targetItem == Items.AIR + || targetStack.isEmpty() || TargetHelper.isStorageLike(targetStack)) { + return null; + } + + ItemStack output = new ItemStack(ModItems.UNIVERSAL_ESSENCE.get()); + TargetHelper.applyTarget(output, targetId, targetStack, registries); + return new AdvancedAlloyFurnaceRecipe( + RECIPE_ID, + List.of(), + List.of(new FluidStack(Fluids.WATER, WATER_AMOUNT)), + List.of(output), + List.of(), + ENERGY, + PROCESS_TIME, + Ingredient.EMPTY, + 0, + DataComponentIngredient.of(true, mold.copyWithCount(1)), + AlloyFurnaceMode.NORMAL); + } + + /** Recipe-manager payload for the generated JEI template and runtime variant. */ + public static final class DynamicRecipe implements Recipe { + private final AdvancedAlloyFurnaceRecipe convertedRecipe; + + private DynamicRecipe(AdvancedAlloyFurnaceRecipe convertedRecipe) { + this.convertedRecipe = convertedRecipe; + } + + public AdvancedAlloyFurnaceRecipe convertedRecipe() { + return convertedRecipe; + } + + @Override + public net.minecraft.resources.ResourceLocation getId() { return convertedRecipe.id(); } + + @Override + public boolean matches(Container input, Level level) { + return false; + } + + @Override + public ItemStack assemble(Container input, net.minecraft.core.RegistryAccess registries) { + return ItemStack.EMPTY; + } + + @Override + public boolean canCraftInDimensions(int width, int height) { + return false; + } + + @Override + public ItemStack getResultItem(net.minecraft.core.RegistryAccess registries) { + return ItemStack.EMPTY; + } + + @Override + public RecipeSerializer getSerializer() { + return null; + } + + @Override + public RecipeType getType() { + return null; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mysticalagriculture/SeedEssenceRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mysticalagriculture/SeedEssenceRecipeAdapter.java new file mode 100644 index 00000000..a5eb2781 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mysticalagriculture/SeedEssenceRecipeAdapter.java @@ -0,0 +1,179 @@ +package com.sorrowmist.useless.content.recipe.adapters.mysticalagriculture; + +import com.blakebr0.mysticalagriculture.api.crop.ICropProvider; +import com.blakebr0.mysticalagriculture.registry.CropRegistry; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.Recipe; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.Container; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 神秘农业种子→精华配方适配器 + *

+ * 根据种子与精华的对应关系自动生成配方: + * - 模具:种子 + * - 输入流体:100mb 水 + * - 输出:2x 对应精华 + */ +public class SeedEssenceRecipeAdapter implements IRecipeAdapter { + + private static final int WATER_AMOUNT = 1; + + private final Map recipeMap = new HashMap<>(); + private final List allRecipes = new ArrayList<>(); + + public SeedEssenceRecipeAdapter() { + buildRecipes(); + } + + private void buildRecipes() { + var crops = CropRegistry.getInstance().getCrops(); + + for (var crop : crops) { + Item seedItem = crop.getSeedsItem(); + Item essenceItem = crop.getEssenceItem(); + + if (seedItem == null || essenceItem == null) continue; + if (seedItem == essenceItem) continue; + + ResourceLocation id = new ResourceLocation( + "mysticalagriculture", + "seed_essence_" + crop.getName() + ); + + FluidStack waterInput = new FluidStack( + BuiltInRegistries.FLUID.get( + new ResourceLocation("water") + ), + WATER_AMOUNT + ); + + AdvancedAlloyFurnaceRecipe recipe = new AdvancedAlloyFurnaceRecipe( + id, + List.of(), // 无物品输入 + List.of(waterInput), // 100mb 水 + List.of(new ItemStack(essenceItem, 2)), // 2x 精华 + List.of(), // 无流体输出 + 1000, // 能量 + 60, // 处理时间 + Ingredient.EMPTY, + 0, + Ingredient.of(seedItem), // 种子作为模具 + AlloyFurnaceMode.NORMAL + ); + + recipeMap.put(seedItem, recipe); + allRecipes.add(recipe); + } + } + + /** + * 获取所有生成的配方(用于 JEI 展示) + */ + public List getAllRecipes() { + return allRecipes; + } + + @Override + public Class getRecipeClass() { + return SeedEssenceDummyRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return null; // 种子作为模具,无固定模具物品 + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + return mold != null && !mold.isEmpty() && mold.getItem() instanceof ICropProvider; + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + return List.of(holder.value().convertedRecipe); + } + + @Override + @Nullable + public AdvancedAlloyFurnaceRecipe convert(RecipeHolder holder, Level level) { + return holder.value().convertedRecipe; + } + + @Override + @Nullable + public List> findMatchingRecipes(Level level, Map mergedInputs, Map mergedFluids, @Nullable ItemStack mold) { + if (mold == null || mold.isEmpty()) return List.of(); + + Item moldItem = mold.getItem(); + if (!(moldItem instanceof ICropProvider)) return List.of(); + + AdvancedAlloyFurnaceRecipe recipe = recipeMap.get(moldItem); + if (recipe == null) return List.of(); + + ResourceLocation holderId = recipe.id(); + return List.of(new RecipeHolder<>(holderId, new SeedEssenceDummyRecipe(recipe))); + } + + /** + * 内部占位配方类,用于满足 RecipeHolder 泛型约束 + */ + public static class SeedEssenceDummyRecipe implements Recipe { + final AdvancedAlloyFurnaceRecipe convertedRecipe; + + SeedEssenceDummyRecipe(AdvancedAlloyFurnaceRecipe convertedRecipe) { + this.convertedRecipe = convertedRecipe; + } + + @Override + public net.minecraft.resources.ResourceLocation getId() { return convertedRecipe.id(); } + + @Override + public boolean matches(Container input, Level level) { + return false; + } + + @Override + public ItemStack assemble(Container input, net.minecraft.core.RegistryAccess registries) { + return ItemStack.EMPTY; + } + + @Override + public boolean canCraftInDimensions(int width, int height) { + return false; + } + + @Override + public ItemStack getResultItem(net.minecraft.core.RegistryAccess registries) { + return ItemStack.EMPTY; + } + + @Override + public RecipeSerializer getSerializer() { + return null; + } + + @Override + public RecipeType getType() { + return null; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mysticalagriculture/SoulJarRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mysticalagriculture/SoulJarRecipeAdapter.java new file mode 100644 index 00000000..9a1e39b7 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/mysticalagriculture/SoulJarRecipeAdapter.java @@ -0,0 +1,205 @@ +package com.sorrowmist.useless.content.recipe.adapters.mysticalagriculture; + +import com.blakebr0.mysticalagriculture.api.soul.MobSoulType; +import com.blakebr0.mysticalagriculture.api.util.MobSoulUtils; +import com.blakebr0.mysticalagriculture.init.ModItems; +import com.blakebr0.mysticalagriculture.registry.MobSoulTypeRegistry; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.SpawnEggItem; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.Recipe; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.Container; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; +import com.sorrowmist.useless.compat.forge1201.recipe.DataComponentIngredient; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** Generates alloy-furnace recipes for Mystical Agriculture soul jars. */ +public class SoulJarRecipeAdapter implements IRecipeAdapter { + + private final Ingredient emptySoulJar; + private final List> generatedRecipes = new ArrayList<>(); + private final Map>> recipesByMold = new HashMap<>(); + + public SoulJarRecipeAdapter() { + Item soulJar = ModItems.SOUL_JAR.get(); + this.emptySoulJar = DataComponentIngredient.of(true, new ItemStack(soulJar)); + buildRecipes(soulJar); + } + + private void buildRecipes(Item soulJar) { + for (MobSoulType soulType : MobSoulTypeRegistry.getInstance().getMobSoulTypes()) { + if (!soulType.isEnabled() || soulType.getSoulRequirement() <= 0D) continue; + + for (ResourceLocation entityId : soulType.getEntityIds()) { + EntityType entityType = BuiltInRegistries.ENTITY_TYPE.getOptional(entityId).orElse(null); + if (entityType == null) continue; + + SpawnEggItem spawnEgg = SpawnEggItem.byId(entityType); + if (spawnEgg == null) continue; + + ItemStack mold = new ItemStack(spawnEgg); + ItemStack output = MobSoulUtils.getFilledSoulJar(soulType, soulJar); + ResourceLocation recipeId = recipeId(soulType.getId(), entityId); + AdvancedAlloyFurnaceRecipe converted = new AdvancedAlloyFurnaceRecipe( + recipeId, + List.of(new CountedIngredient(emptySoulJar, 1L)), + List.of(), + List.of(output), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + AdapterUtils.DEFAULT_PROCESS_TIME, + Ingredient.EMPTY, + 0, + AdapterUtils.toMoldIngredient(mold), + AlloyFurnaceMode.NORMAL); + + RecipeHolder holder = new RecipeHolder<>( + recipeId, new SoulJarDummyRecipe(converted)); + generatedRecipes.add(holder); + recipesByMold.computeIfAbsent(spawnEgg, ignored -> new ArrayList<>()).add(holder); + } + } + } + + private static ResourceLocation recipeId(ResourceLocation soulTypeId, ResourceLocation entityId) { + return new ResourceLocation( + "mysticalagriculture", + "soul_jar_" + idPart(soulTypeId) + "_" + idPart(entityId)); + } + + private static String idPart(ResourceLocation id) { + return (id.getNamespace() + "_" + id.getPath()).replace('/', '_'); + } + + @Override + public Class getRecipeClass() { + return SoulJarDummyRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return null; + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + return mold != null + && !mold.isEmpty() + && mold.getItem() instanceof SpawnEggItem + && recipesByMold.containsKey(mold.getItem()); + } + + @Override + public List> getGeneratedRecipes(Level level) { + return List.copyOf(generatedRecipes); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + return holder == null ? List.of() : List.of(holder.value().convertedRecipe); + } + + @Override + @Nullable + public AdvancedAlloyFurnaceRecipe convert( + RecipeHolder holder, Level level) { + return holder == null ? null : holder.value().convertedRecipe; + } + + @Override + @Nullable + public List> findMatchingRecipes( + Level level, + Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + return findMatchingRecipes(level, mergedInputs, mergedFluids, Map.of(), mold, List.of()); + } + + @Override + @Nullable + public List> findMatchingRecipes( + Level level, + Map mergedInputs, + Map mergedFluids, + Map mergedKeys, + @Nullable ItemStack mold, + List actualInputs) { + if (!matchesMold(mold) || !matchesEmptyJar(mergedInputs, actualInputs)) { + return List.of(); + } + return recipesByMold.getOrDefault(mold.getItem(), List.of()); + } + + private boolean matchesEmptyJar(Map mergedInputs, List actualInputs) { + if (actualInputs != null && !actualInputs.isEmpty()) { + return actualInputs.stream() + .filter(stack -> stack != null && !stack.isEmpty()) + .anyMatch(stack -> emptySoulJar.test(stack)); + } + + return AdapterUtils.matchesRequired( + mergedInputs, Map.of(emptySoulJar, 1L)); + } + + public static class SoulJarDummyRecipe implements Recipe { + private final AdvancedAlloyFurnaceRecipe convertedRecipe; + + SoulJarDummyRecipe(AdvancedAlloyFurnaceRecipe convertedRecipe) { + this.convertedRecipe = convertedRecipe; + } + + @Override + public net.minecraft.resources.ResourceLocation getId() { return convertedRecipe.id(); } + + @Override + public boolean matches(Container input, Level level) { + return false; + } + + @Override + public ItemStack assemble(Container input, net.minecraft.core.RegistryAccess registries) { + return ItemStack.EMPTY; + } + + @Override + public boolean canCraftInDimensions(int width, int height) { + return false; + } + + @Override + public ItemStack getResultItem(net.minecraft.core.RegistryAccess registries) { + return ItemStack.EMPTY; + } + + @Override + public RecipeSerializer getSerializer() { + return null; + } + + @Override + public RecipeType getType() { + return null; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/naturesaura/AnimalSpawnerRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/naturesaura/AnimalSpawnerRecipeAdapter.java new file mode 100644 index 00000000..72c5a9f2 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/naturesaura/AnimalSpawnerRecipeAdapter.java @@ -0,0 +1,123 @@ +package com.sorrowmist.useless.content.recipe.adapters.naturesaura; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import de.ellpeck.naturesaura.Helper; +import de.ellpeck.naturesaura.recipes.AnimalSpawnerRecipe; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.SpawnEggItem; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Nature's Aura animal-spawner recipes into their corresponding spawn eggs. */ +public final class AnimalSpawnerRecipeAdapter implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + + @Override + public Class getRecipeClass() { + return AnimalSpawnerRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return NaturesAuraAdapterUtils.item("animal_spawner"); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + Converted converted = convertData(holder.value()); + if (converted == null) { + RateLimitedLogger.warn(LOGGER, "Skipping unsupported Nature's Aura animal spawner recipe: {}", holder.id()); + return List.of(); + } + return List.of(createRecipe(holder, converted)); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold) || mergedInputs == null || mergedInputs.isEmpty()) { + return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + de.ellpeck.naturesaura.recipes.ModRecipes.ANIMAL_SPAWNER_TYPE)) { + Converted converted = convertData(holder.value()); + if (converted != null && AdapterUtils.matchesRequired(mergedInputs, converted.requirements())) { + matches.add(holder); + } + } + return matches; + } + + @Nullable + private static Converted convertData(@Nullable AnimalSpawnerRecipe source) { + if (source == null || source.aura < 0 || source.time <= 0 || source.entity == null + || source.ingredients == null || source.ingredients.isEmpty()) { + return null; + } + + SpawnEggItem spawnEgg = SpawnEggItem.byId(source.entity); + if (spawnEgg == null) { + return null; + } + + Map requirements = NaturesAuraAdapterUtils.requirements(); + for (Ingredient ingredient : source.ingredients) { + int amount = ingredient == null ? 0 : Helper.getIngredientAmount(ingredient); + if (!NaturesAuraAdapterUtils.addIngredient(requirements, ingredient, amount)) { + return null; + } + } + List inputs = NaturesAuraAdapterUtils.counted(requirements); + if (inputs.isEmpty()) { + return null; + } + return new Converted(inputs, requirements, new ItemStack(spawnEgg), source.aura, source.time); + } + + private AdvancedAlloyFurnaceRecipe createRecipe(RecipeHolder holder, Converted converted) { + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + converted.inputs(), + List.of(), + List.of(converted.output().copy()), + List.of(), + converted.energy(), + converted.time(), + Ingredient.EMPTY, + 0, + AdapterUtils.toMoldIngredient(getMoldItem()), + AlloyFurnaceMode.NORMAL + ); + } + + private record Converted( + List inputs, + Map requirements, + ItemStack output, + long energy, + int time) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/naturesaura/NatureAltarRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/naturesaura/NatureAltarRecipeAdapter.java new file mode 100644 index 00000000..b318ccac --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/naturesaura/NatureAltarRecipeAdapter.java @@ -0,0 +1,131 @@ +package com.sorrowmist.useless.content.recipe.adapters.naturesaura; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import de.ellpeck.naturesaura.recipes.AltarRecipe; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts both standard and crimson Nature's Aura altar recipes. */ +public final class NatureAltarRecipeAdapter implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + + @Override + public Class getRecipeClass() { + return AltarRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + // Altar recipes can use either the altar itself or a source-defined catalyst block. + return null; + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + // The exact source recipe decides whether this is the altar or one of its catalysts. + return mold != null && !mold.isEmpty(); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + Converted converted = convertData(holder.value()); + if (converted == null) { + RateLimitedLogger.warn(LOGGER, "Skipping invalid Nature's Aura altar recipe: {}", holder.id()); + return List.of(); + } + return List.of(createRecipe(holder, converted)); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold) || mergedInputs == null || mergedInputs.isEmpty()) { + return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + de.ellpeck.naturesaura.recipes.ModRecipes.ALTAR_TYPE)) { + Converted converted = convertData(holder.value()); + if (converted != null && converted.mold().test(mold) + && AdapterUtils.matchesRequired(mergedInputs, converted.requirements())) { + matches.add(holder); + } + } + return matches; + } + + @Nullable + private static Converted convertData(@Nullable AltarRecipe source) { + if (source == null || source.aura < 0 || source.time <= 0 || source.input == null + || source.input.isEmpty() || source.output == null || source.output.isEmpty() + || source.output.getCount() <= 0) { + return null; + } + + Ingredient mold = source.catalyst == null || source.catalyst.isEmpty() + ? AdapterUtils.toMoldIngredient(NaturesAuraAdapterUtils.item("nature_altar")) + : source.catalyst; + if (mold.isEmpty()) { + return null; + } + + Map requirements = NaturesAuraAdapterUtils.requirements(); + if (!NaturesAuraAdapterUtils.addIngredient(requirements, source.input, 1L)) { + return null; + } + List inputs = NaturesAuraAdapterUtils.counted(requirements); + if (inputs.isEmpty()) { + return null; + } + return new Converted(inputs, requirements, source.output.copy(), source.aura, source.time, mold); + } + + private static AdvancedAlloyFurnaceRecipe createRecipe(RecipeHolder holder, Converted converted) { + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + converted.inputs(), + List.of(), + List.of(converted.output().copy()), + List.of(), + converted.energy(), + converted.time(), + Ingredient.EMPTY, + 0, + converted.mold(), + AlloyFurnaceMode.NORMAL + ); + } + + private record Converted( + List inputs, + Map requirements, + ItemStack output, + long energy, + int time, + Ingredient mold) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/naturesaura/NaturesAuraAdapterUtils.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/naturesaura/NaturesAuraAdapterUtils.java new file mode 100644 index 00000000..13d139e9 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/naturesaura/NaturesAuraAdapterUtils.java @@ -0,0 +1,79 @@ +package com.sorrowmist.useless.content.recipe.adapters.naturesaura; + +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Common validation and component-preserving helpers for Nature's Aura adapters. */ +final class NaturesAuraAdapterUtils { + private static final String MOD_ID = "naturesaura"; + + private NaturesAuraAdapterUtils() { + } + + static ItemStack item(String path) { + Item item = BuiltInRegistries.ITEM.get(new ResourceLocation(MOD_ID, path)); + return item == Items.AIR ? ItemStack.EMPTY : new ItemStack(item); + } + + static boolean addIngredient(Map target, @Nullable Ingredient ingredient, long count) { + if (target == null || ingredient == null || ingredient.isEmpty() || count <= 0L) { + return false; + } + try { + for (Map.Entry entry : target.entrySet()) { + if (AdapterUtils.areIngredientsEqual(entry.getKey(), ingredient)) { + target.put(entry.getKey(), Math.addExact(entry.getValue(), count)); + return true; + } + } + target.put(ingredient, count); + return true; + } catch (ArithmeticException exception) { + return false; + } + } + + static Map requirements() { + return new LinkedHashMap<>(); + } + + static List counted(Map requirements) { + if (requirements == null || requirements.isEmpty()) { + return List.of(); + } + List counted = new ArrayList<>(requirements.size()); + for (Map.Entry entry : requirements.entrySet()) { + if (entry.getKey() == null || entry.getKey().isEmpty() + || entry.getValue() == null || entry.getValue() <= 0L) { + return List.of(); + } + counted.add(new CountedIngredient(entry.getKey(), entry.getValue())); + } + return List.copyOf(counted); + } + + @Nullable + static ItemStack multipliedOutput(@Nullable ItemStack source, int multiplier) { + if (source == null || source.isEmpty() || source.getCount() <= 0 || multiplier <= 0) { + return null; + } + try { + int count = Math.multiplyExact(source.getCount(), multiplier); + return source.copyWithCount(count); + } catch (ArithmeticException exception) { + return null; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/naturesaura/OfferingRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/naturesaura/OfferingRecipeAdapter.java new file mode 100644 index 00000000..f47ed7aa --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/naturesaura/OfferingRecipeAdapter.java @@ -0,0 +1,127 @@ +package com.sorrowmist.useless.content.recipe.adapters.naturesaura; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import de.ellpeck.naturesaura.Helper; +import de.ellpeck.naturesaura.recipes.OfferingRecipe; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts offerings as one fixed sixteen-operation batch with one calling item. */ +public final class OfferingRecipeAdapter implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final int OFFERING_BATCH_SIZE = 16; + + @Override + public Class getRecipeClass() { + return OfferingRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return NaturesAuraAdapterUtils.item("offering_table"); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + Converted converted = convertData(holder.value()); + if (converted == null) { + RateLimitedLogger.warn(LOGGER, "Skipping invalid Nature's Aura offering recipe: {}", holder.id()); + return List.of(); + } + return List.of(createRecipe(holder, converted)); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold) || mergedInputs == null || mergedInputs.isEmpty()) { + return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + de.ellpeck.naturesaura.recipes.ModRecipes.OFFERING_TYPE)) { + Converted converted = convertData(holder.value()); + if (converted != null && AdapterUtils.matchesRequired(mergedInputs, converted.requirements())) { + matches.add(holder); + } + } + return matches; + } + + @Nullable + private static Converted convertData(@Nullable OfferingRecipe source) { + if (source == null || source.input == null || source.input.isEmpty() || source.startItem == null + || source.startItem.isEmpty()) { + return null; + } + + ItemStack output = NaturesAuraAdapterUtils.multipliedOutput(source.output, OFFERING_BATCH_SIZE); + int sourceInputCount = Helper.getIngredientAmount(source.input); + if (output == null || sourceInputCount <= 0) { + return null; + } + + long inputCount; + try { + inputCount = Math.multiplyExact((long) sourceInputCount, OFFERING_BATCH_SIZE); + } catch (ArithmeticException exception) { + return null; + } + + Map requirements = NaturesAuraAdapterUtils.requirements(); + if (!NaturesAuraAdapterUtils.addIngredient(requirements, source.input, inputCount) + || !NaturesAuraAdapterUtils.addIngredient(requirements, source.startItem, 1L)) { + return null; + } + List inputs = NaturesAuraAdapterUtils.counted(requirements); + if (inputs.isEmpty()) { + return null; + } + return new Converted(inputs, requirements, output); + } + + private AdvancedAlloyFurnaceRecipe createRecipe(RecipeHolder holder, Converted converted) { + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + converted.inputs(), + List.of(), + List.of(converted.output().copy()), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + AdapterUtils.DEFAULT_PROCESS_TIME, + Ingredient.EMPTY, + 0, + AdapterUtils.toMoldIngredient(getMoldItem()), + AlloyFurnaceMode.NORMAL + ); + } + + private record Converted( + List inputs, + Map requirements, + ItemStack output) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/naturesaura/TreeRitualRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/naturesaura/TreeRitualRecipeAdapter.java new file mode 100644 index 00000000..19148cfd --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/naturesaura/TreeRitualRecipeAdapter.java @@ -0,0 +1,126 @@ +package com.sorrowmist.useless.content.recipe.adapters.naturesaura; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import de.ellpeck.naturesaura.recipes.TreeRitualRecipe; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Nature's Aura tree rituals, including their consumed gold-powder ring. */ +public final class TreeRitualRecipeAdapter implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final int GOLD_POWDER_PER_RITUAL = 16; + + @Override + public Class getRecipeClass() { + return TreeRitualRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return NaturesAuraAdapterUtils.item("wood_stand"); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + Converted converted = convertData(holder.value()); + if (converted == null) { + RateLimitedLogger.warn(LOGGER, "Skipping invalid Nature's Aura tree ritual recipe: {}", holder.id()); + return List.of(); + } + return List.of(createRecipe(holder, converted)); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold) || mergedInputs == null || mergedInputs.isEmpty()) { + return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + de.ellpeck.naturesaura.recipes.ModRecipes.TREE_RITUAL_TYPE)) { + Converted converted = convertData(holder.value()); + if (converted != null && AdapterUtils.matchesRequired(mergedInputs, converted.requirements())) { + matches.add(holder); + } + } + return matches; + } + + @Nullable + private static Converted convertData(@Nullable TreeRitualRecipe source) { + if (source == null || source.time <= 0 || source.saplingType == null || source.saplingType.isEmpty() + || source.output == null || source.output.isEmpty() || source.output.getCount() <= 0 + || source.ingredients == null) { + return null; + } + + Map requirements = NaturesAuraAdapterUtils.requirements(); + if (!NaturesAuraAdapterUtils.addIngredient(requirements, source.saplingType, 1L)) { + return null; + } + for (Ingredient ingredient : source.ingredients) { + if (!NaturesAuraAdapterUtils.addIngredient(requirements, ingredient, 1L)) { + return null; + } + } + + ItemStack goldPowder = NaturesAuraAdapterUtils.item("gold_powder"); + if (goldPowder.isEmpty() || !NaturesAuraAdapterUtils.addIngredient( + requirements, Ingredient.of(goldPowder), GOLD_POWDER_PER_RITUAL)) { + return null; + } + + List inputs = NaturesAuraAdapterUtils.counted(requirements); + if (inputs.isEmpty()) { + return null; + } + return new Converted(inputs, requirements, source.output.copy(), source.time); + } + + private AdvancedAlloyFurnaceRecipe createRecipe(RecipeHolder holder, Converted converted) { + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + converted.inputs(), + List.of(), + List.of(converted.output().copy()), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + converted.time(), + Ingredient.EMPTY, + 0, + AdapterUtils.toMoldIngredient(getMoldItem()), + AlloyFurnaceMode.NORMAL + ); + } + + private record Converted( + List inputs, + Map requirements, + ItemStack output, + int time) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/neovitae/AraVitaeRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/neovitae/AraVitaeRecipeAdapter.java new file mode 100644 index 00000000..291a81a0 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/neovitae/AraVitaeRecipeAdapter.java @@ -0,0 +1,112 @@ +package com.sorrowmist.useless.content.recipe.adapters.neovitae; + +import com.breakinblocks.neovitae.common.block.NVBlocks; +import com.breakinblocks.neovitae.common.recipe.NVRecipes; +import com.breakinblocks.neovitae.api.recipe.AraVitaeInput; +import com.breakinblocks.neovitae.api.recipe.AraVitaeRecipe; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts Ara Vitae recipes and preserves copied input components at runtime. */ +public final class AraVitaeRecipeAdapter implements IRecipeAdapter { + private static final ItemStack MOLD = new ItemStack(NVBlocks.ARA_VITAE.asItem()); + + @Override + public Class getRecipeClass() { + return AraVitaeRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return MOLD.copy(); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) return List.of(); + AraVitaeRecipe source = holder.value(); + if (source.getResult().isEmpty() || source.getCraftSpeed() <= 0 + || source.getTotalBlood() < 0) return List.of(); + return List.of(convert(holder.id(), source, source.getResult())); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level, List actualInputs) { + if (holder == null || holder.value() == null || actualInputs == null) return List.of(); + AraVitaeRecipe source = holder.value(); + if (!source.shouldCopyInputComponents()) return convertAll(holder, level); + + List result = new ArrayList<>(); + for (ItemStack input : NeoVitaeAdapterUtils.distinctMatches(actualInputs, source.getInput())) { + ItemStack output; + try { + output = source.assemble(new AraVitaeInput(input, 0), + level == null ? null : level.registryAccess()).copy(); + } catch (RuntimeException exception) { + continue; + } + if (!output.isEmpty()) { + result.add(convert(holder.id(), source, output, input)); + } + } + return result; + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) return List.of(); + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(level.getRecipeManager(), NVRecipes.ARA_VITAE_TYPE.get())) { + AraVitaeRecipe source = holder.value(); + if (source != null && NeoVitaeAdapterUtils.matchesItems( + mergedInputs, List.of(new CountedIngredient(source.getInput(), 1L)))) { + matches.add(holder); + } + } + return matches; + } + + private static AdvancedAlloyFurnaceRecipe convert( + net.minecraft.resources.ResourceLocation id, AraVitaeRecipe source, + ItemStack output) { + return convert(id, source, output, null); + } + + private static AdvancedAlloyFurnaceRecipe convert( + net.minecraft.resources.ResourceLocation id, AraVitaeRecipe source, + ItemStack output, @Nullable ItemStack actualInput) { + List inputs = actualInput == null + ? List.of(new CountedIngredient(source.getInput(), 1L)) + : List.of(new CountedIngredient(NeoVitaeAdapterUtils.exact(actualInput), 1L)); + return new AdvancedAlloyFurnaceRecipe( + id, + inputs, + List.of(), + List.of(), + List.of(output), + List.of(), + List.of(), + NeoVitaeAdapterUtils.energyFor(source.getTotalBlood()), + NeoVitaeAdapterUtils.ceilDivide(source.getTotalBlood(), source.getCraftSpeed()), + Ingredient.EMPTY, + 0, + List.of(Ingredient.of(MOLD)), + AlloyFurnaceMode.NORMAL); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/neovitae/AthanorRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/neovitae/AthanorRecipeAdapter.java new file mode 100644 index 00000000..53613def --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/neovitae/AthanorRecipeAdapter.java @@ -0,0 +1,155 @@ +package com.sorrowmist.useless.content.recipe.adapters.neovitae; + +import appeng.api.stacks.AEKey; +import com.breakinblocks.neovitae.common.block.NVBlocks; +import com.breakinblocks.neovitae.common.recipe.NVRecipes; +import com.breakinblocks.neovitae.common.recipe.athanor.AthanorPotionRecipe; +import com.breakinblocks.neovitae.common.recipe.athanor.AthanorRecipe; +import com.mojang.datafixers.util.Pair; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.FluidIngredientAllocator; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.core.component.DataComponents; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.alchemy.PotionContents; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** Converts Athanor recipes, including fixed chance outputs and potion effects. */ +public final class AthanorRecipeAdapter implements IRecipeAdapter { + private static final ItemStack MOLD = new ItemStack(NVBlocks.ATHANOR_BLOCK.asItem()); + + @Override + public Class getRecipeClass() { + return AthanorRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return MOLD.copy(); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) return List.of(); + AthanorRecipe source = holder.value(); + ItemStack potionInput = source instanceof AthanorPotionRecipe + ? NeoVitaeAdapterUtils.representative(source.getInputs().isEmpty() + ? Ingredient.EMPTY : source.getInputs().get(0)) : null; + return List.of(convert(holder.id(), source, potionInput)); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level, List actualInputs) { + if (holder == null || holder.value() == null || actualInputs == null) return List.of(); + AthanorRecipe source = holder.value(); + if (!(source instanceof AthanorPotionRecipe)) return convertAll(holder, level); + + List result = new ArrayList<>(); + Ingredient potionIngredient = source.getInputs().isEmpty() + ? Ingredient.EMPTY : source.getInputs().get(0); + for (ItemStack input : NeoVitaeAdapterUtils.distinctMatches(actualInputs, potionIngredient)) { + result.add(convert(holder.id(), source, input)); + } + return result; + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + return findMatchingRecipes(level, mergedInputs, mergedFluids, Map.of(), mold, null); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, Map mergedKeys, + @Nullable ItemStack mold, + List actualInputs) { + if (level == null || !matchesMold(mold)) return List.of(); + + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(level.getRecipeManager(), NVRecipes.ATHANOR_TYPE.get())) { + AthanorRecipe source = holder.value(); + if (source == null) continue; + List requirements = NeoVitaeAdapterUtils.counted(source.getInputs()); + boolean itemMatch = actualInputs == null + ? NeoVitaeAdapterUtils.matchesItems(mergedInputs, requirements) + : NeoVitaeAdapterUtils.matchesItems(requirements, actualInputs); + if (!itemMatch) continue; + + Optional inputFluid = source.getInputFluid(); + if (inputFluid.isPresent() && !FluidIngredientAllocator.matches( + List.of(inputFluid.get()), mergedFluids, 1L)) { + continue; + } + matches.add(holder); + } + return matches; + } + + private static AdvancedAlloyFurnaceRecipe convert( + net.minecraft.resources.ResourceLocation id, AthanorRecipe source, + @Nullable ItemStack potionInput) { + List molds = new ArrayList<>(); + molds.add(Ingredient.of(MOLD)); + if (source.getTool() != null && !source.getTool().isEmpty()) { + molds.add(source.getTool()); + } + + return NeoVitaeAdapterUtils.recipe( + id, + NeoVitaeAdapterUtils.counted(source.getInputs()), + NeoVitaeAdapterUtils.inputFluid(source.getInputFluid().orElse(null)), + fixedOutputs(source, potionInput), + NeoVitaeAdapterUtils.outputFluid(source.getOutputFluid().orElse(null)), + NeoVitaeAdapterUtils.energyFor( + NeoVitaeAdapterUtils.sumCosts(source.getSpiritusCosts().values())), + AdapterUtils.DEFAULT_PROCESS_TIME, + molds); + } + + private static List fixedOutputs(AthanorRecipe source, @Nullable ItemStack potionInput) { + List outputs = new ArrayList<>(); + PotionContents potion = potionInput == null + ? null : potionInput.get(DataComponents.POTION_CONTENTS); + for (ItemStack output : source.getGuaranteedOutput()) { + outputs.add(source instanceof AthanorPotionRecipe + ? potionOutput(output, potion) : output.copy()); + } + for (Pair output : source.getChanceOutput()) { + if (output != null && output.get(0) != null) { + outputs.add(source instanceof AthanorPotionRecipe + ? potionOutput(output.get(0), potion) : output.get(0).copy()); + } + } + return outputs; + } + + private static ItemStack potionOutput(ItemStack output, @Nullable PotionContents potion) { + ItemStack result = output.copy(); + if (potion == null || !potion.hasEffects()) return result; + List effects = new ArrayList<>(); + for (MobEffectInstance effect : potion.getAllEffects()) { + effects.add(new MobEffectInstance(effect)); + } + result.set(DataComponents.POTION_CONTENTS, + new PotionContents(Optional.empty(), Optional.empty(), effects)); + return result; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/neovitae/HellfireForgeRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/neovitae/HellfireForgeRecipeAdapter.java new file mode 100644 index 00000000..c44958e3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/neovitae/HellfireForgeRecipeAdapter.java @@ -0,0 +1,384 @@ +package com.sorrowmist.useless.content.recipe.adapters.neovitae; + +import appeng.api.stacks.AEKey; +import com.breakinblocks.neovitae.common.block.NVBlocks; +import com.breakinblocks.neovitae.common.datacomponent.NVDataComponents; +import com.breakinblocks.neovitae.common.recipe.NVRecipes; +import com.breakinblocks.neovitae.common.recipe.forge.ForgeInput; +import com.breakinblocks.neovitae.common.recipe.forge.ForgeRecipe; +import com.breakinblocks.neovitae.common.recipe.forge.ForgeSpiritusInfusionRecipe; +import com.breakinblocks.neovitae.common.recipe.forge.ForgeTransformRecipe; +import com.breakinblocks.neovitae.common.recipe.forge.ForgeUpgradeRecipe; +import com.breakinblocks.neovitae.common.tag.NVTags; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.ItemIngredientAllocator; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.ItemTags; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Converts all four Hellfire Forge recipe variants. */ +public final class HellfireForgeRecipeAdapter implements IRecipeAdapter { + private static final ItemStack MOLD = new ItemStack(NVBlocks.HELLFIRE_FORGE.asItem()); + + @Override + public Class getRecipeClass() { + return ForgeRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return MOLD.copy(); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) return List.of(); + ForgeRecipe source = holder.value(); + if (source instanceof ForgeUpgradeRecipe upgrade) { + return staticUpgradeRecipes(holder.id(), upgrade); + } + if (source instanceof ForgeSpiritusInfusionRecipe infusion) { + return staticInfusionRecipes(holder.id(), infusion); + } + if (source instanceof ForgeTransformRecipe transform) { + return staticTransformRecipes(holder.id(), transform); + } + + ItemStack output = source.getOutput(); + if (output == null || output.isEmpty()) return List.of(); + return List.of(createRecipe( + holder.id(), + NeoVitaeAdapterUtils.counted(source.getCraftingIngredients()), + output, + NeoVitaeAdapterUtils.energyFor(value(source.getDrain())), + AdapterUtils.DEFAULT_PROCESS_TIME)); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level, List actualInputs) { + if (holder == null || holder.value() == null || actualInputs == null) return List.of(); + ForgeRecipe source = holder.value(); + if (source instanceof ForgeUpgradeRecipe upgrade) { + return runtimeUpgradeRecipes(holder.id(), upgrade, actualInputs, level); + } + if (source instanceof ForgeSpiritusInfusionRecipe infusion) { + return runtimeInfusionRecipes(holder.id(), infusion, actualInputs, level); + } + if (source instanceof ForgeTransformRecipe transform) { + return runtimeTransformRecipes(holder.id(), transform, actualInputs, level); + } + return convertAll(holder, level); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + return findMatchingRecipes(level, mergedInputs, mergedFluids, Map.of(), mold, + representativeInputs(mergedInputs)); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, Map mergedKeys, + @Nullable ItemStack mold, + List actualInputs) { + if (level == null || !matchesMold(mold)) return List.of(); + + List> matches = new ArrayList<>(); + RecipeManager manager = level.getRecipeManager(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(manager, NVRecipes.HELLFIRE_FORGE_TYPE.get())) { + ForgeRecipe source = holder.value(); + if (source == null) continue; + + List inputs = actualInputs == null ? List.of() : actualInputs; + if (source instanceof ForgeUpgradeRecipe upgrade) { + if (!upgradeTargets(upgrade, inputs).isEmpty()) matches.add(holder); + continue; + } + if (source instanceof ForgeSpiritusInfusionRecipe infusion) { + if (!infusionMatches(infusion, inputs)) continue; + matches.add(holder); + continue; + } + + List requirements = NeoVitaeAdapterUtils.counted( + source.getCraftingIngredients()); + if (actualInputs != null + ? NeoVitaeAdapterUtils.matchesItems(requirements, actualInputs) + : NeoVitaeAdapterUtils.matchesItems(mergedInputs, requirements)) { + matches.add(holder); + } + } + return matches; + } + + public static boolean isUpgradeTarget(ItemStack stack) { + return stack != null && !stack.isEmpty() && stack.isDamageableItem() + && !stack.is(NVTags.Items.BLOOD_MENDING_BLACKLIST) + && !stack.has(NVDataComponents.BLOOD_MENDING.get()); + } + + public static boolean isInfusionTarget(ItemStack stack) { + return stack != null && !stack.isEmpty() + && stack.is(NVTags.Items.SPIRITUS_CAPABLE) + && !stack.has(NVDataComponents.SPIRITUS_MAX.get()) + && !stack.is(ItemTags.SWORDS) + && !stack.is(ItemTags.AXES) + && !stack.is(ItemTags.PICKAXES) + && !stack.is(ItemTags.SHOVELS) + && !stack.is(ItemTags.HOES); + } + + static List upgradeTargets(ForgeUpgradeRecipe source, List actualInputs) { + List result = new ArrayList<>(); + if (source == null || actualInputs == null) return result; + List catalysts = NeoVitaeAdapterUtils.counted(source.getCraftingIngredients()); + int catalystSlots = source.getCraftingIngredients().size(); + for (int slot = 0; slot < actualInputs.size(); slot++) { + ItemStack target = actualInputs.get(slot); + if (!isUpgradeTarget(target)) continue; + + List remaining = new ArrayList<>(); + for (int index = 0; index < actualInputs.size(); index++) { + if (index != slot && actualInputs.get(index) != null + && !actualInputs.get(index).isEmpty()) { + remaining.add(actualInputs.get(index)); + } + } + if (remaining.size() == catalystSlots + && ItemIngredientAllocator.matches(catalysts, remaining, 1L) + && result.stream().noneMatch(existing -> + ItemStack.isSameItemSameTags(existing, target))) { + result.add(target.copyWithCount(1)); + } + } + return result; + } + + static boolean infusionMatches(ForgeSpiritusInfusionRecipe source, List actualInputs) { + if (source == null || actualInputs == null) return false; + List gems = NeoVitaeAdapterUtils.distinctMatches(actualInputs, source.getGemInput()); + List targets = actualInputs.stream() + .filter(HellfireForgeRecipeAdapter::isInfusionTarget) + .filter(stack -> gems.stream().noneMatch(gem -> ItemStack.isSameItem(gem, stack))) + .map(stack -> stack.copyWithCount(1)) + .distinct() + .toList(); + return !gems.isEmpty() && !targets.isEmpty(); + } + + private static List staticUpgradeRecipes( + ResourceLocation id, ForgeUpgradeRecipe source) { + List representatives = NeoVitaeAdapterUtils.representatives(source.getCraftingIngredients()); + if (representatives.isEmpty()) return List.of(); + + List result = new ArrayList<>(); + for (Item item : BuiltInRegistries.ITEM) { + ItemStack target = item.getDefaultInstance(); + if (!isUpgradeTarget(target)) continue; + List sourceInputs = new ArrayList<>(representatives); + sourceInputs.add(target.copyWithCount(1)); + ItemStack output = assemble(source, sourceInputs, forgeGem(), 4, null); + if (output.isEmpty()) continue; + List inputs = NeoVitaeAdapterUtils.append( + NeoVitaeAdapterUtils.counted(source.getCraftingIngredients()), + // Static variants are generated from default instances. Some third-party + // default component sets cannot be encoded by DataComponentIngredient; + // component-sensitive matching is handled by the dynamic pattern path. + Ingredient.of(target.getItem())); + result.add(createRecipe( + NeoVitaeAdapterUtils.variantId(id, target), + inputs, + output, + NeoVitaeAdapterUtils.energyFor(value(source.getDrain())), + AdapterUtils.DEFAULT_PROCESS_TIME)); + } + return result; + } + + private static List staticInfusionRecipes( + ResourceLocation id, ForgeSpiritusInfusionRecipe source) { + List result = new ArrayList<>(); + for (ItemStack gem : NeoVitaeAdapterUtils.candidates(source.getGemInput())) { + for (Item item : BuiltInRegistries.ITEM) { + ItemStack target = item.getDefaultInstance(); + if (!isInfusionTarget(target)) continue; + + ItemStack output = assemble(source, List.of(gem, target), gem, 0, null); + if (output.isEmpty()) continue; + List inputs = NeoVitaeAdapterUtils.append( + List.of(new CountedIngredient(NeoVitaeAdapterUtils.exact(gem), 1L)), + NeoVitaeAdapterUtils.exact(target)); + result.add(createRecipe( + NeoVitaeAdapterUtils.variantId(id, gem, target), + inputs, + output, + NeoVitaeAdapterUtils.energyFor(value(source.getDrain())), + AdapterUtils.DEFAULT_PROCESS_TIME)); + } + } + return result; + } + + private static List staticTransformRecipes( + ResourceLocation id, ForgeTransformRecipe source) { + List result = new ArrayList<>(); + List catalysts = NeoVitaeAdapterUtils.representatives(source.getCatalysts()); + if (catalysts.size() != source.getCatalysts().size()) return List.of(); + for (ItemStack target : NeoVitaeAdapterUtils.candidates(source.getTransformInput())) { + List sourceInputs = new ArrayList<>(catalysts); + sourceInputs.add(target); + ItemStack output = assemble(source, sourceInputs, forgeGem(), 4, null); + if (output.isEmpty()) continue; + + result.add(createRecipe( + NeoVitaeAdapterUtils.variantId(id, target), + NeoVitaeAdapterUtils.append( + NeoVitaeAdapterUtils.counted(source.getCatalysts()), + NeoVitaeAdapterUtils.exact(target)), + output, + NeoVitaeAdapterUtils.energyFor(value(source.getDrain())), + AdapterUtils.DEFAULT_PROCESS_TIME)); + } + return result; + } + + private static List runtimeUpgradeRecipes( + ResourceLocation id, ForgeUpgradeRecipe source, List actualInputs, + @Nullable Level level) { + List result = new ArrayList<>(); + for (ItemStack target : upgradeTargets(source, actualInputs)) { + ItemStack output = assemble(source, actualInputs, forgeGem(), 4, level); + if (output.isEmpty()) continue; + List inputs = NeoVitaeAdapterUtils.append( + NeoVitaeAdapterUtils.counted(source.getCraftingIngredients()), + NeoVitaeAdapterUtils.exact(target)); + result.add(createRecipe( + NeoVitaeAdapterUtils.variantId(id, target), + inputs, + output, + NeoVitaeAdapterUtils.energyFor(value(source.getDrain())), + AdapterUtils.DEFAULT_PROCESS_TIME)); + } + return result; + } + + private static List runtimeInfusionRecipes( + ResourceLocation id, ForgeSpiritusInfusionRecipe source, List actualInputs, + @Nullable Level level) { + List result = new ArrayList<>(); + List gems = NeoVitaeAdapterUtils.distinctMatches(actualInputs, source.getGemInput()); + for (ItemStack gem : gems) { + for (ItemStack target : actualInputs) { + if (!isInfusionTarget(target) || ItemStack.isSameItem(gem, target)) continue; + ItemStack output = assemble(source, actualInputs, gem, + actualInputs.indexOf(gem), level); + if (output.isEmpty()) continue; + List inputs = NeoVitaeAdapterUtils.append( + List.of(new CountedIngredient(NeoVitaeAdapterUtils.exact(gem), 1L)), + NeoVitaeAdapterUtils.exact(target)); + result.add(createRecipe( + NeoVitaeAdapterUtils.variantId(id, gem, target), + inputs, + output, + NeoVitaeAdapterUtils.energyFor(value(source.getDrain())), + AdapterUtils.DEFAULT_PROCESS_TIME)); + } + } + return result; + } + + private static List runtimeTransformRecipes( + ResourceLocation id, ForgeTransformRecipe source, List actualInputs, + @Nullable Level level) { + List result = new ArrayList<>(); + for (ItemStack target : NeoVitaeAdapterUtils.distinctMatches(actualInputs, source.getTransformInput())) { + ItemStack output = assemble(source, actualInputs, forgeGem(), 4, level); + if (output.isEmpty()) continue; + List inputs = transformInputs(source, target); + result.add(createRecipe( + NeoVitaeAdapterUtils.variantId(id, target), + inputs, + output, + NeoVitaeAdapterUtils.energyFor(value(source.getDrain())), + AdapterUtils.DEFAULT_PROCESS_TIME)); + } + return result; + } + + private static List transformInputs( + ForgeTransformRecipe source, ItemStack target) { + List inputs = new ArrayList<>(source.getCraftingIngredients()); + for (int index = inputs.size() - 1; index >= 0; index--) { + if (inputs.get(index).equals(source.getTransformInput())) { + inputs.set(index, NeoVitaeAdapterUtils.exact(target)); + return NeoVitaeAdapterUtils.counted(inputs); + } + } + return NeoVitaeAdapterUtils.counted(inputs); + } + + private static AdvancedAlloyFurnaceRecipe createRecipe( + ResourceLocation id, List inputs, ItemStack output, + long energy, int processTime) { + return NeoVitaeAdapterUtils.recipe( + id, + inputs, + List.of(), + List.of(output), + List.of(), + energy, + processTime, + List.of(Ingredient.of(MOLD))); + } + + private static ItemStack assemble(ForgeRecipe source, List inputs, + ItemStack gem, int gemIndex, @Nullable Level level) { + try { + return source.assemble(new ForgeInput(inputs, gem, gemIndex), + level == null ? null : level.registryAccess()).copy(); + } catch (RuntimeException ignored) { + return ItemStack.EMPTY; + } + } + + private static ItemStack forgeGem() { + ItemStack gem = new ItemStack(Items.PAPER); + gem.set(NVDataComponents.SPIRITUS_AMOUNT.get(), Double.MAX_VALUE); + return gem; + } + + private static List representativeInputs(Map mergedInputs) { + List result = new ArrayList<>(); + if (mergedInputs == null) return result; + for (Ingredient ingredient : mergedInputs.keySet()) { + ItemStack representative = NeoVitaeAdapterUtils.representative(ingredient); + if (!representative.isEmpty()) result.add(representative); + } + return result; + } + + private static double value(@Nullable Double value) { + return value == null ? 0.0 : value; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/neovitae/NeoVitaeAdapterUtils.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/neovitae/NeoVitaeAdapterUtils.java new file mode 100644 index 00000000..b51208db --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/neovitae/NeoVitaeAdapterUtils.java @@ -0,0 +1,236 @@ +package com.sorrowmist.useless.content.recipe.adapters.neovitae; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.ItemIngredientAllocator; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import com.sorrowmist.useless.compat.forge1201.recipe.DataComponentIngredient; +import org.jetbrains.annotations.Nullable; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +final class NeoVitaeAdapterUtils { + private static final BigDecimal FE_PER_COST = BigDecimal.valueOf(10_000L); + private static final BigDecimal MAX_FE = BigDecimal.valueOf(Integer.MAX_VALUE); + + private NeoVitaeAdapterUtils() { + } + + static long energyFor(double cost) { + if (Double.isNaN(cost) || cost < 0.0) { + return AdapterUtils.DEFAULT_ENERGY; + } + if (cost == 0.0) { + return AdapterUtils.DEFAULT_ENERGY; + } + if (Double.isInfinite(cost)) { + return Integer.MAX_VALUE; + } + + BigDecimal scaled = BigDecimal.valueOf(cost) + .multiply(FE_PER_COST) + .setScale(0, RoundingMode.CEILING); + return scaled.compareTo(MAX_FE) >= 0 ? Integer.MAX_VALUE : Math.max(1L, scaled.longValue()); + } + + static double sumCosts(Iterable costs) { + BigDecimal total = BigDecimal.ZERO; + if (costs != null) { + for (Double cost : costs) { + if (cost == null || Double.isNaN(cost) || cost <= 0.0) continue; + if (Double.isInfinite(cost)) return Double.POSITIVE_INFINITY; + total = total.add(BigDecimal.valueOf(cost)); + } + } + return total.doubleValue(); + } + + static int ceilDivide(long numerator, long denominator) { + if (numerator <= 0L) return 0; + if (denominator <= 0L) return 0; + long result = (numerator - 1L) / denominator + 1L; + return AdapterUtils.safeInt(result); + } + + static List counted(List ingredients) { + return AdapterUtils.mergeIngredients(ingredients == null ? List.of() : ingredients); + } + + static List append(List inputs, Ingredient ingredient) { + List result = new ArrayList<>(inputs == null ? List.of() : inputs); + if (ingredient != null && !ingredient.isEmpty()) { + result.add(new CountedIngredient(ingredient, 1L)); + } + return List.copyOf(result); + } + + static Ingredient exact(ItemStack stack) { + return DataComponentIngredient.of(true, stack.copyWithCount(1)); + } + + static Map requirements(List inputs) { + Map result = new LinkedHashMap<>(); + if (inputs != null) { + for (CountedIngredient input : inputs) { + if (input != null && input.ingredient() != null && input.count() > 0) { + AdapterUtils.mergeIngredient(result, input.ingredient(), input.count()); + } + } + } + return result; + } + + static boolean matchesItems(List requirements, List actualInputs) { + return ItemIngredientAllocator.matches(requirements, actualInputs, 1L); + } + + static boolean matchesItems(Map available, List requirements) { + return AdapterUtils.matchesRequired(available, requirements(requirements)); + } + + static boolean matchesFluids(@Nullable Map available, + @Nullable SizedFluidIngredient requirement) { + if (requirement == null) return true; + return com.sorrowmist.useless.content.recipe.FluidIngredientAllocator.matches( + List.of(requirement), available, 1L); + } + + static List inputFluid(@Nullable SizedFluidIngredient input) { + return input == null ? List.of() : List.of(input); + } + + static List outputFluid(@Nullable FluidStack output) { + return output == null || output.isEmpty() ? List.of() : List.of(output.copy()); + } + + static AdvancedAlloyFurnaceRecipe recipe( + ResourceLocation id, + List inputs, + List inputFluids, + List outputs, + List outputFluids, + long energy, + int processTime, + List molds) { + return new AdvancedAlloyFurnaceRecipe( + id, + inputs, + inputFluids, + List.of(), + copies(outputs), + fluidCopies(outputFluids), + List.of(), + energy, + processTime, + Ingredient.EMPTY, + 0, + molds, + AlloyFurnaceMode.NORMAL); + } + + static List copies(List stacks) { + List result = new ArrayList<>(); + if (stacks != null) { + for (ItemStack stack : stacks) { + if (stack != null && !stack.isEmpty() && stack.getCount() > 0) { + result.add(stack.copy()); + } + } + } + return List.copyOf(result); + } + + static List fluidCopies(List stacks) { + List result = new ArrayList<>(); + if (stacks != null) { + for (FluidStack stack : stacks) { + if (stack != null && !stack.isEmpty() && stack.getAmount() > 0) { + result.add(stack.copy()); + } + } + } + return List.copyOf(result); + } + + static ResourceLocation variantId(ResourceLocation source, ItemStack... stacks) { + StringBuilder suffix = new StringBuilder("_converted"); + if (stacks != null) { + for (ItemStack stack : stacks) { + if (stack == null || stack.isEmpty()) continue; + ResourceLocation itemId = net.minecraft.core.registries.BuiltInRegistries.ITEM.getKey(stack.getItem()); + suffix.append('_').append(itemId.getNamespace()).append('_') + .append(itemId.getPath().replace('/', '_')); + } + } + return new ResourceLocation(source.getNamespace(), source.getPath() + suffix); + } + + static List distinctMatches(List inputs, Ingredient ingredient) { + List result = new ArrayList<>(); + if (inputs == null || ingredient == null || ingredient.isEmpty()) return result; + for (ItemStack input : inputs) { + if (input == null || input.isEmpty() || !ingredient.test(input) + || result.stream().anyMatch(existing -> + ItemStack.isSameItemSameTags(existing, input))) { + continue; + } + result.add(input.copyWithCount(1)); + } + return result; + } + + static ItemStack representative(Ingredient ingredient) { + ItemStack stack = AdapterUtils.itemRepresentative(ingredient); + return stack == null ? ItemStack.EMPTY : stack.copyWithCount(1); + } + + static List representatives(List ingredients) { + List result = new ArrayList<>(); + if (ingredients == null) return result; + for (Ingredient ingredient : ingredients) { + ItemStack stack = representative(ingredient); + if (stack.isEmpty()) return List.of(); + result.add(stack); + } + return result; + } + + static List candidates(Ingredient ingredient) { + if (ingredient == null || ingredient.isEmpty()) return List.of(); + List result = new ArrayList<>(); + try { + for (ItemStack stack : ingredient.getItems()) { + if (stack != null && !stack.isEmpty() + && result.stream().noneMatch(existing -> + ItemStack.isSameItemSameTags(existing, stack))) { + result.add(stack.copyWithCount(1)); + } + } + } catch (RuntimeException ignored) { + // Fall back to the shared representative probe for custom ingredients. + } + if (result.isEmpty()) { + ItemStack representative = representative(ingredient); + if (!representative.isEmpty()) result.add(representative); + } + return List.copyOf(result); + } + + static boolean sameItemAndCount(ItemStack expected, ItemStack actual) { + return expected != null && !expected.isEmpty() + && actual != null && !actual.isEmpty() + && expected.getCount() == actual.getCount() + && expected.is(actual.getItem()); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/neovitae/TabulaVitaeRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/neovitae/TabulaVitaeRecipeAdapter.java new file mode 100644 index 00000000..17b7bed6 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/neovitae/TabulaVitaeRecipeAdapter.java @@ -0,0 +1,74 @@ +package com.sorrowmist.useless.content.recipe.adapters.neovitae; + +import com.breakinblocks.neovitae.common.block.NVBlocks; +import com.breakinblocks.neovitae.common.recipe.NVRecipes; +import com.breakinblocks.neovitae.common.recipe.tabulavitae.TabulaVitaeRecipe; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Map; + +/** Converts Tabula Vitae recipes while preserving their item inputs, output and ticks. */ +public final class TabulaVitaeRecipeAdapter implements IRecipeAdapter { + private static final ItemStack MOLD = new ItemStack(NVBlocks.TABULA_VITAE.asItem()); + + @Override + public Class getRecipeClass() { + return TabulaVitaeRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return MOLD.copy(); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) return List.of(); + TabulaVitaeRecipe source = holder.value(); + ItemStack output = source.getOutput(); + if (output == null || output.isEmpty() || source.getTicks() <= 0) return List.of(); + + return List.of(new AdvancedAlloyFurnaceRecipe( + holder.id(), + NeoVitaeAdapterUtils.counted(source.getInput()), + List.of(), + List.of(), + List.of(output), + List.of(), + List.of(), + NeoVitaeAdapterUtils.energyFor(source.getSyphon()), + source.getTicks(), + Ingredient.EMPTY, + 0, + List.of(Ingredient.of(MOLD)), + AlloyFurnaceMode.NORMAL)); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) return List.of(); + + List> matches = new java.util.ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(level.getRecipeManager(), NVRecipes.TABULA_VITAE_TYPE.get())) { + TabulaVitaeRecipe source = holder.value(); + if (source != null && source.getTicks() > 0 + && NeoVitaeAdapterUtils.matchesItems(mergedInputs, + NeoVitaeAdapterUtils.counted(source.getInput()))) { + matches.add(holder); + } + } + return matches; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/occultism/OccultismRitualRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/occultism/OccultismRitualRecipeAdapter.java new file mode 100644 index 00000000..ec67d2be --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/occultism/OccultismRitualRecipeAdapter.java @@ -0,0 +1,967 @@ +package com.sorrowmist.useless.content.recipe.adapters.occultism; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import appeng.api.stacks.GenericStack; +import com.klikli_dev.occultism.common.entity.spirit.SpiritEntity; +import com.klikli_dev.occultism.common.item.spirit.BookOfBindingBoundItem; +import com.klikli_dev.occultism.common.item.spirit.MinerSpiritItem; +import com.klikli_dev.occultism.crafting.recipe.RitualRecipe; +import com.klikli_dev.occultism.registry.OccultismDataComponents; +import com.klikli_dev.occultism.registry.OccultismEntities; +import com.klikli_dev.occultism.registry.OccultismRecipes; +import com.klikli_dev.occultism.registry.OccultismSpiritJobs; +import com.klikli_dev.occultism.util.ItemNBTUtil; +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.PatternStackView; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.ItemIngredientAllocator; +import com.sorrowmist.useless.core.component.RitualBlueprintPentacles; +import com.sorrowmist.useless.core.component.UComponents; +import com.sorrowmist.useless.init.ModItems; +import net.minecraft.ChatFormatting; +import net.minecraft.core.component.DataComponentMap; +import net.minecraft.core.component.DataComponents; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Rarity; +import net.minecraft.world.item.SpawnEggItem; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.item.component.Unbreakable; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.item.enchantment.EnchantmentHelper; +import net.minecraft.world.item.enchantment.Enchantments; +import net.minecraft.world.level.Level; +import com.sorrowmist.useless.compat.forge1201.recipe.DataComponentIngredient; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** Converts static Occultism ritual semantics into component-aware alloy-furnace recipes. */ +public final class OccultismRitualRecipeAdapter implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final String OCCULTISM = "occultism"; + private static final ResourceLocation REPAIR = id("repair"); + private static final ResourceLocation UPGRADE = id("upgrade"); + private static final ResourceLocation UNBREAKABLE = id("unbreakable"); + private static final ResourceLocation CRAFT_MINER_SPIRIT = id("craft_miner_spirit"); + private static final ResourceLocation RESURRECT_FAMILIAR = id("resurrect_familiar"); + private static final ResourceLocation COMMAND = id("execute_command"); + // Miner-spirit rituals are deliberately NOT matched by a hard-coded recipe id list anymore: + // Occultism's built-in ids are occultism:ritual/craft_miner_* (the old list used the wrong + // occultism:craft_miner_* shape) and modpack authors re-register those rituals through KubeJS + // with both a new id and the schema's default ritual_type (occultism:craft). Detection is now + // semantic, see isMinerSpiritRecipe / isMinerSpiritResult. + public static final String AUTO_TAME_MARKER = "useless_mod_occultism_auto_tame"; + private static final Set WARNED_SKIPPED_RECIPES = ConcurrentHashMap.newKeySet(); + + @Override + public Class getRecipeClass() { + return RitualRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return null; // Blueprint components select the actual pentacle. + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + if (mold == null || mold.isEmpty() || ModItems.RITUAL_BLUEPRINT == null + || !mold.is(ModItems.RITUAL_BLUEPRINT.get())) { + return false; + } + RitualBlueprintPentacles pentacles = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(mold, UComponents.RITUAL_BLUEPRINT_PENTACLE.get()); + return pentacles != null && !pentacles.isEmpty(); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (!isSupported(holder)) { + return List.of(); + } + RitualRecipe source = holder.value(); + if (isRepairLike(source)) { + return staticRepairRecipes(holder, source); + } + if (isUpgrade(source)) { + return staticUpgradeRecipes(holder, source); + } + Converted data = convert(source, null, null, null, level); + return data == null ? List.of() : List.of(createRecipe(holder.id(), source, data)); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level, List actualInputs) { + if (!isSupported(holder)) { + return List.of(); + } + RitualRecipe source = holder.value(); + if (isRepairLike(source)) { + List recipes = new ArrayList<>(); + for (ItemStack activation : distinctMatches(actualInputs, source.getActivationItem())) { + if (!activation.isDamageableItem()) { + continue; + } + Converted data = convert(source, activation, null, null); + if (data != null) { + recipes.add(createRecipe(variantId(holder.id(), activation), source, data)); + } + } + return recipes; + } + if (isUpgrade(source)) { + Ingredient baseIngredient = upgradeBaseIngredient(source); + if (baseIngredient.isEmpty()) { + return List.of(); + } + List recipes = new ArrayList<>(); + for (ItemStack base : distinctMatches(actualInputs, baseIngredient)) { + for (ItemStack activation : distinctMatches(actualInputs, source.getActivationItem())) { + Converted data = convert(source, null, base, activation); + if (data != null) { + recipes.add(createRecipe(variantId(holder.id(), base, activation), source, data)); + } + } + } + return recipes; + } + if (isCraftMinerSpirit(source)) { + List recipes = new ArrayList<>(); + for (ItemStack activation : distinctMatches(actualInputs, source.getActivationItem())) { + Converted data = convert(source, activation, null, null, level); + if (data != null) { + recipes.add(createRecipe(variantId(holder.id(), activation), source, data)); + } + } + return recipes; + } + return convertAll(holder, level); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold) || mergedInputs == null || mergedInputs.isEmpty()) { + return List.of(); + } + RitualBlueprintPentacles pentacles = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(mold, UComponents.RITUAL_BLUEPRINT_PENTACLE.get()); + if (pentacles == null || pentacles.isEmpty()) { + return List.of(); + } + + List> matches = new ArrayList<>(); + RecipeManager manager = level.getRecipeManager(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(manager, OccultismRecipes.RITUAL_TYPE.get())) { + RitualRecipe source = holder.value(); + if (!isSupported(holder) || !pentacles.contains(source.getPentacleId())) { + continue; + } + Map requirements = requirements(source, null, null, null); + if (requirements != null && AdapterUtils.matchesRequired(mergedInputs, requirements)) { + matches.add(holder); + } + } + return matches; + } + + /** Used by the AE pattern resolver to identify outputs generated from actual component-bearing inputs. */ + public static boolean isDynamicComponentRitual(RitualRecipe recipe) { + return recipe != null && (REPAIR.equals(recipe.getRitualType()) + || UPGRADE.equals(recipe.getRitualType()) || UNBREAKABLE.equals(recipe.getRitualType()) + || isCraftMinerSpirit(recipe)); + } + + public static Optional findDynamicPatternProfile( + @Nullable Level level, List patternInputs, List patternOutputs) { + return findDynamicPatternProfileLong(level, PatternStackView.fromLegacy(patternInputs, patternOutputs)); + } + + static Optional findDynamicPatternProfile( + Iterable> recipes, + List patternInputs, + List patternOutputs) { + return findDynamicPatternProfileLong( + recipes, PatternStackView.fromLegacy(patternInputs, patternOutputs)); + } + + public static Optional findDynamicPatternProfileLong( + @Nullable Level level, PatternStackView pattern) { + if (level == null) { + return Optional.empty(); + } + return findDynamicPatternProfileLong( + com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(level.getRecipeManager(), OccultismRecipes.RITUAL_TYPE.get()), pattern); + } + + static Optional findDynamicPatternProfileLong( + Iterable> recipes, PatternStackView pattern) { + if (recipes == null || pattern == null || pattern.inputs().isEmpty() + || pattern.outputs().isEmpty()) { + return Optional.empty(); + } + if (pattern.inputRepresentatives().size() != pattern.inputs().size() + || pattern.outputRepresentatives().size() != pattern.outputs().size()) { + return Optional.empty(); + } + List matches = new ArrayList<>(); + for (RecipeHolder holder : recipes) { + if (!isSupported(holder)) { + continue; + } + RitualRecipe source = holder.value(); + if (isMinerSpiritRecipe(holder)) { + if (pattern.outputs().size() == 1) { + addMinerSpiritPatternMatch(source, pattern, matches); + } + } else if (isRepairLike(source)) { + if (pattern.outputs().size() == 1) { + addRepairPatternMatch(source, pattern, matches); + } + } else if (isUpgrade(source)) { + Set boundBookSlots = boundBookActivationSlots(source, pattern); + if (pattern.outputs().size() == 1) { + addUpgradePatternMatch( + source, pattern, boundBookSlots, matches); + } + } else { + Set boundBookSlots = boundBookActivationSlots(source, pattern); + if (!boundBookSlots.isEmpty()) { + addBoundBookPatternMatch( + source, pattern, boundBookSlots, matches); + } + } + if (matches.size() > 1) { + return Optional.empty(); + } + } + return matches.isEmpty() ? Optional.empty() : Optional.of(matches.get(0)); + } + + private static void addMinerSpiritPatternMatch( + RitualRecipe source, PatternStackView pattern, + List matches) { + Converted data = convert(source, null, null, null); + List patternOutputs = pattern.outputRepresentatives(); + if (data == null || data.outputs().size() != 1) { + return; + } + if (patternOutputs.isEmpty() || pattern.outputs().isEmpty()) { + return; + } + if (!matchesOutputItem(data.outputs().get(0), pattern.outputs().get(0), + patternOutputs.get(0))) { + return; + } + if (!matchesPatternInputs(data.inputs(), pattern)) { + return; + } + + Set activationSlots = new LinkedHashSet<>(); + Ingredient activation = source.getActivationItem(); + List patternInputs = pattern.inputRepresentatives(); + for (int slot = 0; slot < patternInputs.size(); slot++) { + ItemStack input = patternInputs.get(slot); + if (matchesItemId(activation, input)) { + activationSlots.add(slot); + } + } + if (activationSlots.size() == 1) { + matches.add(new DynamicPatternProfile(activationSlots, Set.of(0))); + } + } + + + + private static boolean matchesItemId(@Nullable Ingredient ingredient, @Nullable ItemStack stack) { + if (ingredient == null || ingredient.isEmpty() || stack == null || stack.isEmpty()) { + return false; + } + for (ItemStack option : ingredient.getItems()) { + if (option != null && !option.isEmpty() && option.is(stack.getItem())) { + return true; + } + } + // Custom ingredients do not necessarily expose enumerable item stacks. Their own test + // remains the only safe fallback, while all five built-in miner rituals use simple item + // ingredients and therefore take the component-agnostic branch above. + return ingredient.getCustomIngredient() != null && ingredient.test(stack); + } + + private static void addRepairPatternMatch( + RitualRecipe source, PatternStackView pattern, + List matches) { + List patternInputs = pattern.inputRepresentatives(); + List patternOutputs = pattern.outputRepresentatives(); + for (int slot = 0; slot < patternInputs.size(); slot++) { + ItemStack activation = patternInputs.get(slot); + if (activation == null || activation.isEmpty() || !activation.isDamageableItem() + || !source.getActivationItem().test(activation) + || !activation.is(patternOutputs.get(0).getItem())) { + continue; + } + Converted data = convert(source, activation, null, null); + if (data != null && matchesPatternInputs(data.inputs(), pattern) + && data.outputs().size() == 1 + && matchesOutputItem(data.outputs().get(0), pattern.outputs().get(0), + patternOutputs.get(0))) { + matches.add(new DynamicPatternProfile(Set.of(slot), Set.of(0))); + } + } + } + + private static void addUpgradePatternMatch( + RitualRecipe source, PatternStackView pattern, + Set boundBookSlots, + List matches) { + Ingredient baseIngredient = upgradeBaseIngredient(source); + List patternInputs = pattern.inputRepresentatives(); + List patternOutputs = pattern.outputRepresentatives(); + for (int slot = 0; slot < patternInputs.size(); slot++) { + ItemStack base = patternInputs.get(slot); + if (base == null || base.isEmpty() || !baseIngredient.test(base)) { + continue; + } + Converted data = convert(source, null, base, null); + if (data != null && data.outputs().size() == 1 + && matchesOutputItem(data.outputs().get(0), pattern.outputs().get(0), + patternOutputs.get(0)) + && matchesPatternInputs(data.inputs(), pattern)) { + Set idOnlyInputs = new LinkedHashSet<>(boundBookSlots); + idOnlyInputs.add(slot); + matches.add(new DynamicPatternProfile(idOnlyInputs, Set.of(0))); + } + } + } + + private static void addBoundBookPatternMatch( + RitualRecipe source, PatternStackView pattern, + Set boundBookSlots, List matches) { + Converted data = convert(source, null, null, null); + if (data == null || !matchesStaticOutputs(data.outputs(), pattern.outputs(), + pattern.outputRepresentatives()) || !matchesPatternInputs(data.inputs(), pattern)) { + return; + } + matches.add(new DynamicPatternProfile(boundBookSlots, Set.of())); + } + + private static Set boundBookActivationSlots( + RitualRecipe source, PatternStackView pattern) { + Set slots = new LinkedHashSet<>(); + Ingredient activation = source.getActivationItem(); + if (activation.getCustomIngredient() != null) { + return slots; + } + List patternInputs = pattern.inputRepresentatives(); + for (int slot = 0; slot < patternInputs.size(); slot++) { + ItemStack input = patternInputs.get(slot); + if (input != null && !input.isEmpty() + && input.getItem() instanceof BookOfBindingBoundItem + && activation.test(input)) { + slots.add(slot); + } + } + return slots; + } + + private static boolean matchesPatternInputs( + List requirements, PatternStackView pattern) { + long required = totalRequiredItems(requirements); + long actual = totalPatternItems(pattern.inputs()); + if (required <= 0L || actual <= 0L || actual % required != 0L) { + return false; + } + // A manually multiplied pattern is still the same recipe. AlloyFurnaceRecipeCatalog + // .findPatternScale resolves integer multiples, so the dynamic profiles must use the same + // scaling rule instead of demanding a 1x pattern. + return ItemIngredientAllocator.matches( + requirements, List.of(), pattern.inputs(), actual / required); + } + + private static boolean matchesOutputItem( + ItemStack expected, GenericStack patternOutput, ItemStack patternOutputRepresentative) { + return expected != null && !expected.isEmpty() + && expected.getCount() > 0 + && patternOutput != null && patternOutput.amount() > 0L + && patternOutputRepresentative != null && !patternOutputRepresentative.isEmpty() + && expected.is(patternOutputRepresentative.getItem()) + // Stay proportional rather than exactly equal, mirroring findPatternScale. + && patternOutput.amount() % expected.getCount() == 0L; + } + + private static boolean matchesStaticOutput( + ItemStack expected, GenericStack patternOutput, ItemStack patternOutputRepresentative) { + return matchesOutputItem(expected, patternOutput, patternOutputRepresentative) + && ItemStack.isSameItemSameTags(expected, patternOutputRepresentative); + } + + private static boolean matchesStaticOutputs( + List expected, List patternOutputs, + List patternOutputRepresentatives) { + if (expected.size() != patternOutputs.size()) { + return false; + } + boolean[] matched = new boolean[patternOutputs.size()]; + for (ItemStack expectedOutput : expected) { + boolean found = false; + for (int slot = 0; slot < patternOutputs.size(); slot++) { + if (!matched[slot] && matchesStaticOutput( + expectedOutput, patternOutputs.get(slot), patternOutputRepresentatives.get(slot))) { + matched[slot] = true; + found = true; + break; + } + } + if (!found) { + return false; + } + } + return true; + } + + private static long totalRequiredItems(List requirements) { + long total = 0L; + for (CountedIngredient requirement : requirements) { + if (requirement == null || requirement.count() <= 0) { + continue; + } + if (total > Long.MAX_VALUE - requirement.count()) { + return Long.MAX_VALUE; + } + total += requirement.count(); + } + return total; + } + + private static long totalPatternItems(List patternInputs) { + long total = 0L; + for (GenericStack input : patternInputs) { + if (input == null || input.amount() <= 0L) { + continue; + } + total = total > Long.MAX_VALUE - input.amount() + ? Long.MAX_VALUE + : total + input.amount(); + } + return total; + } + + @Nullable + private static Converted convert( + RitualRecipe source, @Nullable ItemStack repairActivation, @Nullable ItemStack upgradeBase, + @Nullable ItemStack upgradeActivation) { + return convert(source, repairActivation, upgradeBase, upgradeActivation, null); + } + + @Nullable + private static Converted convert( + RitualRecipe source, @Nullable ItemStack repairActivation, @Nullable ItemStack upgradeBase, + @Nullable ItemStack upgradeActivation, @Nullable Level level) { + if (source == null || source.getDuration() < 0 || source.getPentacleId() == null) { + return null; + } + Map requirements = requirements( + source, repairActivation, upgradeBase, upgradeActivation); + if (requirements == null) { + return null; + } + + List outputs; + if (REPAIR.equals(source.getRitualType())) { + ItemStack output = repairActivation == null ? ItemStack.EMPTY : repair(repairActivation); + outputs = output.isEmpty() ? List.of() : List.of(output); + } else if (UNBREAKABLE.equals(source.getRitualType())) { + ItemStack output = repairActivation == null ? ItemStack.EMPTY : makeUnbreakable(repairActivation); + outputs = output.isEmpty() ? List.of() : List.of(output); + } else if (UPGRADE.equals(source.getRitualType())) { + ItemStack output = upgradeBase == null ? ItemStack.EMPTY : upgrade(source, upgradeBase, upgradeActivation); + outputs = output.isEmpty() ? List.of() : List.of(output); + } else if (isCraftMinerSpirit(source)) { + ItemStack output = craftMinerSpirit(source, repairActivation, level); + outputs = output.isEmpty() ? List.of() : List.of(output); + } else if (source.getEntityTagToSummon() != null) { + outputs = randomSummonEggs(source); + } else if (source.getEntityToSummon() != null) { + outputs = fixedSummonEggs(source); + } else { + ItemStack output = source.getResultItem(null).copy(); + outputs = output.isEmpty() ? List.of() : List.of(output); + } + if (outputs.isEmpty()) { + return null; + } + List inputs = requirements.entrySet().stream() + .map(entry -> new CountedIngredient(entry.getKey(), entry.getValue())) + .toList(); + return new Converted(inputs, requirements, outputs); + } + + @Nullable + private static Map requirements( + RitualRecipe source, @Nullable ItemStack repairActivation, @Nullable ItemStack upgradeBase, + @Nullable ItemStack upgradeActivation) { + if (source == null || source.getDuration() < 0 || source.getPentacleId() == null) { + return null; + } + Map requirements = new LinkedHashMap<>(); + Ingredient activation = repairActivation != null + ? itemOnly(repairActivation) + : upgradeActivation != null ? itemOnly(upgradeActivation) : source.getActivationItem(); + if (!add(requirements, activation)) { + return null; + } + List ingredients = source.getIngredients(); + for (int index = 0; index < ingredients.size(); index++) { + Ingredient ingredient = index == 0 && upgradeBase != null + ? itemOnly(upgradeBase) : ingredients.get(index); + if (!add(requirements, ingredient)) { + return null; + } + } + if (source.requiresItemUse() && !add(requirements, source.getItemToUse())) { + return null; + } + return requirements; + } + + private static List staticRepairRecipes( + RecipeHolder holder, RitualRecipe source) { + List recipes = new ArrayList<>(); + for (ItemStack candidate : distinctDamageable(source.getActivationItem())) { + Converted data = convert(source, candidate, null, null); + if (data != null) { + recipes.add(createRecipe(variantId(holder.id(), candidate), source, data)); + } + } + return recipes; + } + + private static List staticUpgradeRecipes( + RecipeHolder holder, RitualRecipe source) { + Ingredient baseIngredient = upgradeBaseIngredient(source); + if (baseIngredient.isEmpty()) { + return List.of(); + } + List recipes = new ArrayList<>(); + for (ItemStack base : distinct(baseIngredient.getItems())) { + Converted data = convert(source, null, base, null); + if (data != null) { + recipes.add(createRecipe(variantId(holder.id(), base), source, data)); + } + } + return recipes; + } + + private static AdvancedAlloyFurnaceRecipe createRecipe( + ResourceLocation id, RitualRecipe source, Converted data) { + return new AdvancedAlloyFurnaceRecipe( + id, + data.inputs(), + List.of(), + data.outputs().stream().map(ItemStack::copy).toList(), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + Math.max(1, source.getDuration()), + Ingredient.EMPTY, + 0, + blueprintMold(source.getPentacleId()), + AlloyFurnaceMode.NORMAL + ); + } + + private static List randomSummonEggs(RitualRecipe source) { + List> entityTypes = new ArrayList<>(); + for (var holder : BuiltInRegistries.ENTITY_TYPE.getTagOrEmpty(source.getEntityTagToSummon())) { + entityTypes.add(holder.value()); + } + entityTypes.sort(Comparator.comparing( + type -> BuiltInRegistries.ENTITY_TYPE.getKey(type).toString())); + return entitySummonOutputs(entityTypes, source); + } + + private static List fixedSummonEggs(RitualRecipe source) { + return entitySummonOutputs(List.of(source.getEntityToSummon()), source); + } + + private static List entitySummonOutputs( + Iterable> entityTypes, RitualRecipe source) { + List outputs = new ArrayList<>(spawnEggOutputs( + entityTypes, source.getEntityNbt(), source.getResultItem(null), + source.getSpiritJobType(), source.getSpiritMaxAge())); + ItemStack originalResult = source.getResultItem(null).copy(); + if (!outputs.isEmpty() && !originalResult.isEmpty() + && outputs.stream().noneMatch(output -> + ItemStack.isSameItemSameTags(output, originalResult))) { + outputs.add(originalResult); + } + return List.copyOf(outputs); + } + + static List spawnEggOutputs( + Iterable> entityTypes, @Nullable CompoundTag sourceEntityData) { + return spawnEggOutputs(entityTypes, sourceEntityData, null, null, -1); + } + + private static List spawnEggOutputs( + Iterable> entityTypes, @Nullable CompoundTag sourceEntityData, + @Nullable ItemStack sourceResult, @Nullable ResourceLocation spiritJobType, + int spiritMaxAge) { + List outputs = new ArrayList<>(); + for (EntityType entityType : entityTypes) { + if (entityType == null) { + continue; + } + ItemStack output = spawnEgg(entityType, sourceEntityData, sourceResult, spiritJobType, spiritMaxAge); + if (output.isEmpty()) { + continue; + } + outputs.add(output); + } + return List.copyOf(outputs); + } + + private static ItemStack spawnEgg( + EntityType entityType, @Nullable CompoundTag sourceEntityData, + @Nullable ItemStack sourceResult, @Nullable ResourceLocation spiritJobType, + int spiritMaxAge) { + SpawnEggItem spawnEgg = SpawnEggItem.byId(entityType); + boolean fallback = false; + if (spawnEgg == null) { + spawnEgg = SpawnEggItem.byId(EntityType.PIG); + fallback = true; + } + if (spawnEgg == null) { + return ItemStack.EMPTY; + } + + ItemStack output = new ItemStack(spawnEgg); + copySpawnEggPresentation(sourceResult, output); + + boolean validSpiritJob = isValidSpiritJob(spiritJobType); + if (fallback || sourceEntityData != null || validSpiritJob) { + CompoundTag entityData = sourceEntityData == null ? new CompoundTag() : sourceEntityData.copy(); + ResourceLocation entityId = BuiltInRegistries.ENTITY_TYPE.getKey(entityType); + if (entityId != null) { + entityData.putString("id", entityId.toString()); + } + if (validSpiritJob) { + entityData.putInt("spiritMaxAge", spiritMaxAge); + CompoundTag job = entityData.getCompound("spiritJob").copy(); + job.putString("factoryId", spiritJobType.toString()); + entityData.put("spiritJob", job); + } + output.set(DataComponents.ENTITY_DATA, CustomData.of(entityData)); + } + + if (isOccultismSpiritType(entityType)) { + CompoundTag marker = new CompoundTag(); + marker.putBoolean(AUTO_TAME_MARKER, true); + output.set(DataComponents.CUSTOM_DATA, CustomData.of(marker)); + } + return output; + } + + private static void copySpawnEggPresentation(@Nullable ItemStack source, ItemStack target) { + if (source == null || !(source.getItem() instanceof SpawnEggItem)) { + return; + } + if (source.has(DataComponents.CUSTOM_NAME)) { + target.set(DataComponents.CUSTOM_NAME, source.get(DataComponents.CUSTOM_NAME)); + } + if (source.has(DataComponents.ITEM_NAME)) { + target.set(DataComponents.ITEM_NAME, source.get(DataComponents.ITEM_NAME)); + } + if (source.has(DataComponents.LORE)) { + target.set(DataComponents.LORE, source.get(DataComponents.LORE)); + } + } + + private static boolean isValidSpiritJob(@Nullable ResourceLocation spiritJobType) { + return spiritJobType != null && OccultismSpiritJobs.REGISTRY.get(spiritJobType) != null; + } + + private static boolean isOccultismSpiritType(EntityType entityType) { + return entityType == OccultismEntities.FOLIOT.get() + || entityType == OccultismEntities.DJINNI.get() + || entityType == OccultismEntities.AFRIT.get() + || entityType == OccultismEntities.MARID.get() + || SpiritEntity.class.isAssignableFrom(entityType.getBaseClass()); + } + + static void applyJobEntityData(RitualRecipe source, ItemStack result) { + CompoundTag entityData = source.getEntityNbt() == null ? new CompoundTag() : source.getEntityNbt().copy(); + ResourceLocation entityId = BuiltInRegistries.ENTITY_TYPE.getKey(source.getEntityToSummon()); + if (entityId != null) { + entityData.putString("id", entityId.toString()); + } + entityData.putInt("spiritMaxAge", source.getSpiritMaxAge()); + CompoundTag job = entityData.getCompound("spiritJob").copy(); + job.putString("factoryId", source.getSpiritJobType().toString()); + entityData.put("spiritJob", job); + result.set(DataComponents.ENTITY_DATA, CustomData.of(entityData)); + } + + private static ItemStack repair(ItemStack source) { + ItemStack output = source.copyWithCount(1); + output.setDamageValue(0); + return output; + } + + private static ItemStack makeUnbreakable(ItemStack source) { + ItemStack output = repair(source); + output.set(DataComponents.UNBREAKABLE, new Unbreakable(true)); + if (output.isEnchanted()) { + EnchantmentHelper.updateEnchantments(output, enchantments -> enchantments.removeIf(entry -> + entry.is(Enchantments.UNBREAKING) || entry.is(Enchantments.MENDING))); + } + output.set(DataComponents.RARITY, Rarity.EPIC); + output.set(DataComponents.CUSTOM_NAME, net.minecraft.network.chat.Component.empty() + .append(ChatFormatting.OBFUSCATED + "nice " + ChatFormatting.RESET) + .append(output.getHoverName()) + .append(ChatFormatting.OBFUSCATED + " ecin" + ChatFormatting.RESET)); + return output; + } + + private static ItemStack upgrade(RitualRecipe source, ItemStack base, @Nullable ItemStack activation) { + ItemStack output = source.getResultItem(null).copy(); + Rarity rarity = output.getRarity(); + Integer targetMaxDamage = output.has(DataComponents.MAX_DAMAGE) ? output.getMaxDamage() : null; + output.applyComponents(base.getComponents()); + if (targetMaxDamage != null) { + output.applyComponents(DataComponentMap.builder() + .set(DataComponents.MAX_DAMAGE, targetMaxDamage) + .build()); + } + if (activation != null && activation.has(OccultismDataComponents.SPIRIT_NAME)) { + ItemNBTUtil.setBoundSpiritName(output, ItemNBTUtil.getBoundSpiritName(activation)); + } + output.set(DataComponents.RARITY, rarity); + return output; + } + + private static ItemStack craftMinerSpirit( + RitualRecipe source, @Nullable ItemStack activation, @Nullable Level level) { + ItemStack output = source.getResultItem(null).copy(); + output.getItem().onCraftedBy(output, level, null); + if (activation == null) { + // The static JEI representative must not contain a generated spirit name. The + // runtime ritual adds the name after configuring the miner from the activation item. + output.remove(OccultismDataComponents.SPIRIT_NAME); + } else { + // CraftMinerSpiritRitual works on a copy of the activation item. Keep the real input + // stack untouched, including the case where Occultism generates a name for an input + // which did not already carry SPIRIT_NAME. + ItemStack activationCopy = activation.copy(); + ItemNBTUtil.setBoundSpiritName(output, ItemNBTUtil.getBoundSpiritName(activationCopy)); + } + return output; + } + + private static boolean isSupported(@Nullable RecipeHolder holder) { + if (holder == null || holder.value() == null) { + return false; + } + ResourceLocation type = holder.value().getRitualType(); + if (RESURRECT_FAMILIAR.equals(type) || COMMAND.equals(type)) { + warnSkipped(holder, "requires source entity or command state"); + return false; + } + RitualRecipe source = holder.value(); + if (source.getActivationItem() == null || source.getActivationItem().isEmpty()) { + warnSkipped(holder, "has no enumerable activation ingredient"); + return false; + } + if (isRepairLike(source) && distinctDamageable(source.getActivationItem()).isEmpty()) { + warnSkipped(holder, "has no enumerable damageable activation item"); + return false; + } + if (isUpgrade(source) && (upgradeBaseIngredient(source).isEmpty() + || distinct(upgradeBaseIngredient(source).getItems()).isEmpty())) { + warnSkipped(holder, "has no enumerable upgrade base item"); + return false; + } + return true; + } + + private static void warnSkipped(RecipeHolder holder, String reason) { + if (WARNED_SKIPPED_RECIPES.add(holder.id())) { + RateLimitedLogger.warn(LOGGER, "Skipping unsupported Occultism ritual recipe {}: {}", holder.id(), reason); + } + } + + private static boolean isRepairLike(RitualRecipe source) { + return REPAIR.equals(source.getRitualType()) || UNBREAKABLE.equals(source.getRitualType()); + } + + private static boolean isUpgrade(RitualRecipe source) { + return UPGRADE.equals(source.getRitualType()); + } + + private static boolean isCraftMinerSpirit(RitualRecipe source) { + return source != null && CRAFT_MINER_SPIRIT.equals(source.getRitualType()); + } + + /** + * Detects a miner-spirit ritual without relying on hard-coded recipe ids. + *

+ * Occultism's built-in miner rituals use {@code ritual_type = occultism:craft_miner_spirit} and live under + * {@code occultism:ritual/...}. Modpacks that re-register a miner ritual through KubeJS (occultism_kubejs) + * end up with a brand new recipe id and the schema's default {@code ritual_type = occultism:craft}, so an + * id- or type-only check silently misses them. A missed miner ritual keeps the strict, component-exact + * output match, which fails because the miner spirit item carries a runtime generated + * {@code occultism:spirit_name} component - that is what made every miner tier uncraftable. Matching on the + * result item class covers both the built-in rituals and any renamed / re-typed re-registration. + */ + private static boolean isMinerSpiritRecipe(@Nullable RecipeHolder holder) { + if (holder == null || holder.value() == null) { + return false; + } + RitualRecipe source = holder.value(); + if (isCraftMinerSpirit(source)) { + return true; + } + return isMinerSpiritResult(resultItem(source)); + } + + /** Static item result of a ritual, or {@link ItemStack#EMPTY} when it has none. */ + private static ItemStack resultItem(RitualRecipe source) { + try { + ItemStack result = source.getResultItem(null); + return result == null ? ItemStack.EMPTY : result; + } catch (RuntimeException ignored) { + // getResultItem may need a registry lookup that is not available at this stage. + return ItemStack.EMPTY; + } + } + + /** True when the given stack is an Occultism miner spirit, whose identity includes generated NBT. */ + private static boolean isMinerSpiritResult(ItemStack result) { + if (result == null || result.isEmpty()) { + return false; + } + try { + return result.getItem() instanceof MinerSpiritItem; + } catch (RuntimeException ignored) { + return false; + } + } + + private static Ingredient upgradeBaseIngredient(RitualRecipe source) { + return source.getIngredients().isEmpty() ? Ingredient.EMPTY : source.getIngredients().get(0); + } + + private static boolean add(Map requirements, @Nullable Ingredient ingredient) { + if (ingredient == null || ingredient.isEmpty()) { + return false; + } + AdapterUtils.mergeIngredient(requirements, ingredient, 1L); + return true; + } + + /** + * Item-id-only requirement for a per-variant source item. + * + *

The repair / upgrade / miner variants exist to bind the output computation to one concrete + * item, not to demand a byte-identical instance. Requiring the components of the catalogued + * representative made these variants unmatchable by the item the player actually owns: a damaged + * pickaxe can never equal a pristine one, and a miner spirit carries its own bound + * {@code occultism:spirit_name}. That is also why the machine crafts them while pattern + * encoding refused them - the machine recomputes the requirement from the real input stack, + * whereas encoding matches the pattern against the catalogued recipe. {@code DynamicPatternProfile} + * already declares these slots id-only, so the recipe now says the same thing. + */ + private static Ingredient itemOnly(ItemStack stack) { + return Ingredient.of(stack.getItem()); + } + + private static Ingredient blueprintMold(ResourceLocation pentacle) { + if (ModItems.RITUAL_BLUEPRINT == null) { + return Ingredient.EMPTY; + } + ItemStack blueprint = new ItemStack(ModItems.RITUAL_BLUEPRINT.get()); + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(blueprint, UComponents.RITUAL_BLUEPRINT_PENTACLE.get(), RitualBlueprintPentacles.of(pentacle)); + return DataComponentIngredient.of(true, blueprint); + } + + private static List distinctDamageable(Ingredient ingredient) { + return distinct(ingredient.getItems()).stream().filter(ItemStack::isDamageableItem).toList(); + } + + private static List distinctMatches(List stacks, Ingredient ingredient) { + List matches = new ArrayList<>(); + if (stacks == null || ingredient == null || ingredient.isEmpty()) { + return matches; + } + for (ItemStack stack : stacks) { + if (stack == null || stack.isEmpty() || !ingredient.test(stack) + || matches.stream().anyMatch(existing -> ItemStack.isSameItemSameTags(existing, stack))) { + continue; + } + matches.add(stack.copyWithCount(1)); + } + return matches; + } + + private static List distinct(ItemStack[] stacks) { + List result = new ArrayList<>(); + for (ItemStack stack : stacks) { + if (stack == null || stack.isEmpty() + || result.stream().anyMatch(existing -> ItemStack.isSameItemSameTags(existing, stack))) { + continue; + } + result.add(stack.copyWithCount(1)); + } + return result; + } + + private static ResourceLocation variantId(ResourceLocation source, ItemStack... stacks) { + StringBuilder suffix = new StringBuilder(); + for (ItemStack stack : stacks) { + ResourceLocation key = net.minecraft.core.registries.BuiltInRegistries.ITEM.getKey(stack.getItem()); + suffix.append('_').append(key.getNamespace()).append('_').append(key.getPath().replace('/', '_')); + } + return new ResourceLocation(source.getNamespace(), source.getPath() + "_converted" + suffix); + } + + private static ResourceLocation id(String path) { + return new ResourceLocation(OCCULTISM, path); + } + + private record Converted( + List inputs, + Map requirements, + List outputs) { + private Converted { + outputs = outputs.stream().map(ItemStack::copy).toList(); + } + } + + public record DynamicPatternProfile(Set idOnlyInputSlots, Set idOnlyOutputSlots) { + public DynamicPatternProfile { + idOnlyInputSlots = Set.copyOf(idOnlyInputSlots); + idOnlyOutputSlots = Set.copyOf(idOnlyOutputSlots); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/occultism/OccultismSpiritEggHandler.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/occultism/OccultismSpiritEggHandler.java new file mode 100644 index 00000000..a21fcaff --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/occultism/OccultismSpiritEggHandler.java @@ -0,0 +1,99 @@ +package com.sorrowmist.useless.content.recipe.adapters.occultism; + +import com.klikli_dev.occultism.common.entity.spirit.SpiritEntity; +import com.klikli_dev.occultism.common.entity.job.SpiritJob; +import com.klikli_dev.occultism.common.entity.job.SpiritJobFactory; +import com.klikli_dev.occultism.registry.OccultismSpiritJobs; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.component.DataComponents; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.TamableAnimal; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.SpawnEggItem; +import net.minecraft.world.item.component.CustomData; +import net.minecraftforge.event.entity.player.PlayerInteractEvent; + +/** Runtime-only handling for the component-bearing Occultism job spirit eggs. */ +public final class OccultismSpiritEggHandler { + private OccultismSpiritEggHandler() { + } + + public static InteractionResult trySpawn(PlayerInteractEvent.RightClickBlock event) { + ItemStack stack = event.getItemStack(); + if (!(stack.getItem() instanceof SpawnEggItem egg)) { + return InteractionResult.PASS; + } + CustomData marker = stack.get(DataComponents.CUSTOM_DATA); + if (marker == null || !marker.copyTag().getBoolean(OccultismRitualRecipeAdapter.AUTO_TAME_MARKER)) { + return InteractionResult.PASS; + } + if (event.getLevel().isClientSide) { + return InteractionResult.SUCCESS; + } + + Entity entity = egg.getType(stack).create(event.getLevel()); + CustomData entityComponent = stack.get(DataComponents.ENTITY_DATA); + if (entity == null) { + return InteractionResult.FAIL; + } + + CompoundTag jobData = new CompoundTag(); + if (entityComponent != null) { + CompoundTag entityData = entityComponent.copyTag(); + if (entityData.contains("spiritJob")) { + jobData = entityData.getCompound("spiritJob").copy(); + entityData.remove("spiritJob"); + } + entity.load(entityData); + } + if (entity instanceof TamableAnimal tamable) { + tamable.tame(event.getEntity()); + } + + BlockPos pos = event.getPos(); + Direction face = event.getFace() == null ? Direction.UP : event.getFace(); + if (!event.getLevel().getBlockState(pos).getCollisionShape(event.getLevel(), pos).isEmpty()) { + pos = pos.relative(face); + } + entity.moveTo(pos.getX() + 0.5D, pos.getY() + 0.0D, pos.getZ() + 0.5D, + event.getEntity().getYRot(), 0.0F); + + if (entity instanceof SpiritEntity spirit) { + restoreSpiritJob(spirit, jobData); + spirit.init(); + } + + if (!event.getLevel().addFreshEntity(entity)) { + return InteractionResult.FAIL; + } + if (!event.getEntity().getAbilities().instabuild) { + stack.shrink(1); + } + return InteractionResult.CONSUME; + } + + private static void restoreSpiritJob(SpiritEntity spirit, CompoundTag jobData) { + String factoryIdValue = jobData.getString("factoryId"); + if (factoryIdValue.isEmpty()) { + return; + } + + SpiritJobFactory factory = OccultismSpiritJobs.REGISTRY.get(new ResourceLocation(factoryIdValue)); + if (factory == null) { + return; + } + + SpiritJob job = factory.create(spirit); + // A generated egg intentionally stores only factoryId. Recreating the job from + // its factory preserves configured defaults such as TraderJob's operation count. + // Full saved job state is still restored when additional fields are present. + if (jobData.size() > 1) { + job.deserializeNBT(spirit.level().registryAccess(), jobData); + } + spirit.setJob(job); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/oritech/OritechRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/oritech/OritechRecipeAdapter.java new file mode 100644 index 00000000..53026e49 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/oritech/OritechRecipeAdapter.java @@ -0,0 +1,251 @@ +package com.sorrowmist.useless.content.recipe.adapters.oritech; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.FluidIngredientAllocator; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import org.jetbrains.annotations.Nullable; +import rearth.oritech.init.BlockContent; +import rearth.oritech.init.recipes.OritechRecipe; +import rearth.oritech.init.recipes.OritechRecipeType; +import rearth.oritech.init.recipes.RecipeContent; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Converts Oritech processing recipes to alloy-furnace recipes. */ +public final class OritechRecipeAdapter implements IRecipeAdapter { + private static final int TAINTED_OUTPUT_MULTIPLIER = 3; + + private static final Map MOLD_BY_TYPE = Map.ofEntries( + Map.entry(RecipeContent.ASSEMBLER, BlockContent.ASSEMBLER_BLOCK.asItem()), + Map.entry(RecipeContent.ATOMIC_FORGE, BlockContent.ATOMIC_FORGE_BLOCK.asItem()), + Map.entry(RecipeContent.CENTRIFUGE, BlockContent.CENTRIFUGE_BLOCK.asItem()), + Map.entry(RecipeContent.CENTRIFUGE_FLUID, BlockContent.CENTRIFUGE_BLOCK.asItem()), + Map.entry(RecipeContent.COOLER, BlockContent.COOLER_BLOCK.asItem()), + Map.entry(RecipeContent.FOUNDRY, BlockContent.FOUNDRY_BLOCK.asItem()), + Map.entry(RecipeContent.GRINDER, BlockContent.FRAGMENT_FORGE_BLOCK.asItem()), + Map.entry(RecipeContent.PULVERIZER, BlockContent.PULVERIZER_BLOCK.asItem()), + Map.entry(RecipeContent.LASER, BlockContent.LASER_ARM_BLOCK.asItem()), + Map.entry(RecipeContent.PARTICLE_COLLISION, BlockContent.ACCELERATOR_CONTROLLER.asItem()), + Map.entry(RecipeContent.REFINERY, BlockContent.REFINERY_BLOCK.asItem()) + ); + + @Override + public Class getRecipeClass() { + return OritechRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return null; + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + return !recipeTypesForMold(mold).isEmpty(); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + + OritechRecipe source = holder.value(); + OritechRecipeType type = source.getOriType(); + if (!MOLD_BY_TYPE.containsKey(type) || source.getTime() <= 0) { + return List.of(); + } + + List inputFluids = convertInputFluid(source.getFluidInput()); + if (inputFluids == null) { + return List.of(); + } + + List converted = new ArrayList<>(); + addConverted(converted, holder, source, type, inputFluids, false); + if (type == RecipeContent.REFINERY) { + addConverted(converted, holder, source, type, inputFluids, true); + } + return converted; + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) { + return List.of(); + } + + Map safeInputs = mergedInputs == null ? Map.of() : mergedInputs; + Map safeFluids = mergedFluids == null ? Map.of() : mergedFluids; + RecipeManager manager = level.getRecipeManager(); + List> matches = new ArrayList<>(); + + for (OritechRecipeType type : recipeTypesForMold(mold)) { + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(manager, type)) { + OritechRecipe source = holder.value(); + if (source == null || source.getOriType() != type) { + continue; + } + + List inputs = AdapterUtils.mergeIngredients(source.getInputs()); + List fluids = convertInputFluid(source.getFluidInput()); + if (fluids == null || (inputs.isEmpty() && fluids.isEmpty())) { + continue; + } + + Map requirements = new LinkedHashMap<>(); + for (CountedIngredient input : inputs) { + AdapterUtils.mergeIngredient(requirements, input.ingredient(), input.count()); + } + if (!AdapterUtils.matchesRequired(safeInputs, requirements) + || !FluidIngredientAllocator.matches(fluids, safeFluids, 1L)) { + continue; + } + matches.add(holder); + } + } + return matches; + } + + private static void addConverted( + List converted, + RecipeHolder holder, + OritechRecipe source, + OritechRecipeType type, + List inputFluids, + boolean tainted) { + Item mold = tainted + ? BlockContent.TAINTED_REFINERY_BLOCK.asItem() + : MOLD_BY_TYPE.get(type); + if (mold == null) { + return; + } + + int outputMultiplier = tainted ? TAINTED_OUTPUT_MULTIPLIER : 1; + List outputs = convertItemOutputs(source, outputMultiplier); + List outputFluids = convertFluidOutputs(source, outputMultiplier); + if (outputs.isEmpty() && outputFluids.isEmpty()) { + return; + } + + String suffix = tainted ? "tainted_converted" : "converted"; + converted.add(new AdvancedAlloyFurnaceRecipe( + variantId(holder.id(), suffix), + AdapterUtils.mergeIngredients(source.getInputs()), + inputFluids, + List.of(), + outputs, + outputFluids, + List.of(), + AdapterUtils.DEFAULT_ENERGY, + source.getTime(), + Ingredient.EMPTY, + 0, + AdapterUtils.toMoldIngredient(new ItemStack(mold)), + AlloyFurnaceMode.NORMAL + )); + } + + @Nullable + private static List convertInputFluid( + @Nullable rearth.oritech.util.FluidIngredient source) { + if (source == null || source.amount() <= 0) { + return List.of(); + } + long amount = source.amount(); + if (amount > Integer.MAX_VALUE) { + return null; + } + + FluidIngredient ingredient; + if (source.hasTag()) { + if (source.getTag() == null) { + return null; + } + ingredient = FluidIngredient.tag(source.getTag()); + } else { + Fluid fluid = source.getFluid(); + if (fluid == null || fluid == Fluids.EMPTY) { + return null; + } + ingredient = FluidIngredient.single(fluid); + } + return List.of(new SizedFluidIngredient(ingredient, (int) amount)); + } + + private static List convertItemOutputs(OritechRecipe source, int multiplier) { + List outputs = new ArrayList<>(); + for (ItemStack output : source.getResults()) { + if (output == null || output.isEmpty()) { + continue; + } + int count = AdapterUtils.safeInt((long) output.getCount() * multiplier); + if (count > 0) { + outputs.add(output.copyWithCount(count)); + } + } + return outputs; + } + + private static List convertFluidOutputs(OritechRecipe source, int multiplier) { + List outputs = new ArrayList<>(); + for (dev.architectury.fluid.FluidStack output : source.getFluidOutputs()) { + if (output == null || output.isEmpty()) { + continue; + } + int amount = AdapterUtils.safeInt(output.getAmount() * multiplier); + if (amount <= 0) { + continue; + } + FluidStack converted = new FluidStack(output.getFluid(), amount); + converted.applyComponents(output.getPatch()); + outputs.add(converted); + } + return outputs; + } + + private static List recipeTypesForMold(@Nullable ItemStack mold) { + if (mold == null || mold.isEmpty()) { + return List.of(); + } + Item item = mold.getItem(); + if (item == BlockContent.TAINTED_REFINERY_BLOCK.asItem()) { + return List.of(RecipeContent.REFINERY); + } + + List types = new ArrayList<>(); + for (Map.Entry entry : MOLD_BY_TYPE.entrySet()) { + if (entry.getValue() == item) { + types.add(entry.getKey()); + } + } + return types; + } + + private static net.minecraft.resources.ResourceLocation variantId( + net.minecraft.resources.ResourceLocation original, String suffix) { + return new net.minecraft.resources.ResourceLocation( + original.getNamespace(), original.getPath() + "_" + suffix); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/pneumaticcraft/PneumaticCraftRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/pneumaticcraft/PneumaticCraftRecipeAdapter.java new file mode 100644 index 00000000..e5f33e8e --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/pneumaticcraft/PneumaticCraftRecipeAdapter.java @@ -0,0 +1,458 @@ +package com.sorrowmist.useless.content.recipe.adapters.pneumaticcraft; + +import appeng.api.stacks.AEKey; +import com.mojang.datafixers.util.Either; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.FluidIngredientAllocator; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import me.desht.pneumaticcraft.api.crafting.AmadronTradeResource; +import me.desht.pneumaticcraft.api.crafting.ingredient.FluidContainerIngredient; +import me.desht.pneumaticcraft.api.crafting.recipe.AmadronRecipe; +import me.desht.pneumaticcraft.api.crafting.recipe.AssemblyRecipe; +import me.desht.pneumaticcraft.api.crafting.recipe.FluidMixerRecipe; +import me.desht.pneumaticcraft.api.crafting.recipe.HeatFrameCoolingRecipe; +import me.desht.pneumaticcraft.api.crafting.recipe.PneumaticCraftRecipe; +import me.desht.pneumaticcraft.api.crafting.recipe.PressureChamberRecipe; +import me.desht.pneumaticcraft.api.crafting.recipe.RefineryRecipe; +import me.desht.pneumaticcraft.api.crafting.recipe.ThermoPlantRecipe; +import me.desht.pneumaticcraft.common.registry.ModBlocks; +import me.desht.pneumaticcraft.common.registry.ModItems; +import me.desht.pneumaticcraft.common.registry.ModRecipeTypes; +import me.desht.pneumaticcraft.common.recipes.machine.PressureDisenchantingRecipe; +import me.desht.pneumaticcraft.common.recipes.machine.PressureEnchantingRecipe; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import com.sorrowmist.useless.compat.forge1201.recipe.DataComponentIngredient; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +/** Converts PneumaticCraft's static machine recipes into alloy-furnace recipes. */ +public final class PneumaticCraftRecipeAdapter + implements IRecipeAdapter { + enum Kind { + ASSEMBLY, + PRESSURE_CHAMBER, + FLUID_MIXER, + HEAT_FRAME_COOLING, + REFINERY, + THERMO_PLANT, + AMADRON + } + + private final Class recipeClass; + private final Kind kind; + private final Function>> recipeProvider; + private final ItemStack moldItem; + + PneumaticCraftRecipeAdapter( + Class recipeClass, + Kind kind, + Function>> recipeProvider, + ItemStack moldItem) { + this.recipeClass = recipeClass; + this.kind = kind; + this.recipeProvider = recipeProvider; + this.moldItem = moldItem; + } + + public static PneumaticCraftRecipeAdapter assembly() { + return new PneumaticCraftRecipeAdapter<>( + AssemblyRecipe.class, Kind.ASSEMBLY, + PneumaticCraftRecipeAdapter::assemblyRecipes, + new ItemStack(ModBlocks.ASSEMBLY_PLATFORM.get())); + } + + public static PneumaticCraftRecipeAdapter fluidMixer() { + return fixed(FluidMixerRecipe.class, Kind.FLUID_MIXER, + level -> ModRecipeTypes.getRecipes(level, ModRecipeTypes.FLUID_MIXER), + new ItemStack(ModBlocks.FLUID_MIXER.get())); + } + + public static PneumaticCraftRecipeAdapter heatFrameCooling() { + return fixed(HeatFrameCoolingRecipe.class, Kind.HEAT_FRAME_COOLING, + level -> ModRecipeTypes.getRecipes(level, ModRecipeTypes.HEAT_FRAME_COOLING), + new ItemStack(ModItems.HEAT_FRAME.get())); + } + + public static PneumaticCraftRecipeAdapter pressureChamber() { + return fixed(PressureChamberRecipe.class, Kind.PRESSURE_CHAMBER, + level -> ModRecipeTypes.getRecipes(level, ModRecipeTypes.PRESSURE_CHAMBER), + new ItemStack(ModBlocks.PRESSURE_CHAMBER_VALVE.get())); + } + + public static PneumaticCraftRecipeAdapter refinery() { + return fixed(RefineryRecipe.class, Kind.REFINERY, + level -> ModRecipeTypes.getRecipes(level, ModRecipeTypes.REFINERY), + new ItemStack(ModBlocks.REFINERY.get())); + } + + public static PneumaticCraftRecipeAdapter thermoPlant() { + return fixed(ThermoPlantRecipe.class, Kind.THERMO_PLANT, + level -> ModRecipeTypes.getRecipes(level, ModRecipeTypes.THERMO_PLANT), + new ItemStack(ModBlocks.THERMOPNEUMATIC_PROCESSING_PLANT.get())); + } + + public static PneumaticCraftRecipeAdapter amadron() { + return fixed(AmadronRecipe.class, Kind.AMADRON, + level -> ModRecipeTypes.getRecipes(level, ModRecipeTypes.AMADRON), + new ItemStack(ModItems.AMADRON_TABLET.get())); + } + + private static PneumaticCraftRecipeAdapter fixed( + Class recipeClass, + Kind kind, + Function>> recipeProvider, + ItemStack moldItem) { + return new PneumaticCraftRecipeAdapter<>(recipeClass, kind, recipeProvider, moldItem); + } + + private static Collection> assemblyRecipes(Level level) { + List> result = new ArrayList<>(); + result.addAll(ModRecipeTypes.getRecipes(level, ModRecipeTypes.ASSEMBLY_DRILL)); + result.addAll(ModRecipeTypes.getRecipes(level, ModRecipeTypes.ASSEMBLY_LASER)); + result.addAll(ModRecipeTypes.getRecipes(level, ModRecipeTypes.ASSEMBLY_DRILL_LASER)); + return result; + } + + @Override + public String sourceId() { + return RecipeSourceIds.PNEUMATICCRAFT; + } + + @Override + public Class getRecipeClass() { + return recipeClass; + } + + @Override + public ItemStack getMoldItem() { + return moldItem.copy(); + } + + @Override + @SuppressWarnings("unchecked") + public List> getGeneratedRecipes(Level level) { + if (level == null || kind != Kind.ASSEMBLY) return List.of(); + + Collection> generated = + ModRecipeTypes.getRecipes(level, ModRecipeTypes.ASSEMBLY_DRILL_LASER); + return (List>) (List) List.copyOf(generated); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) return List.of(); + + AdvancedAlloyFurnaceRecipe converted = switch (kind) { + case ASSEMBLY -> convertAssembly(holder.id(), (AssemblyRecipe) holder.value()); + case PRESSURE_CHAMBER -> convertPressureChamber( + holder.id(), (PressureChamberRecipe) holder.value()); + case FLUID_MIXER -> convertFluidMixer(holder.id(), (FluidMixerRecipe) holder.value()); + case HEAT_FRAME_COOLING -> convertHeatFrameCooling( + holder.id(), (HeatFrameCoolingRecipe) holder.value()); + case REFINERY -> convertRefinery(holder.id(), (RefineryRecipe) holder.value()); + case THERMO_PLANT -> convertThermoPlant(holder.id(), (ThermoPlantRecipe) holder.value()); + case AMADRON -> convertAmadron(holder.id(), (AmadronRecipe) holder.value()); + }; + return converted == null ? List.of() : List.of(converted); + } + + @Override + public List> findMatchingRecipes( + Level level, + Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + return findMatchingRecipes(level, mergedInputs, mergedFluids, Map.of(), mold, List.of()); + } + + @Override + public List> findMatchingRecipes( + Level level, + Map mergedInputs, + Map mergedFluids, + Map mergedKeys, + @Nullable ItemStack mold, + List actualInputs) { + if (level == null || !matchesMold(mold)) return List.of(); + + Map safeInputs = mergedInputs == null ? Map.of() : mergedInputs; + Map safeFluids = mergedFluids == null ? Map.of() : mergedFluids; + List> matches = new ArrayList<>(); + for (RecipeHolder holder : recipeProvider.apply(level)) { + if (kind == Kind.HEAT_FRAME_COOLING + && !matchesHeatFrameInput((HeatFrameCoolingRecipe) holder.value(), actualInputs)) { + continue; + } + + List converted = convertAll(holder, level); + if (converted.stream().anyMatch(recipe -> matchesConverted(recipe, safeInputs, safeFluids))) { + matches.add(holder); + } + } + return matches; + } + + static boolean matchesConverted( + AdvancedAlloyFurnaceRecipe recipe, + Map mergedInputs, + Map mergedFluids) { + Map requiredInputs = new LinkedHashMap<>(); + for (CountedIngredient input : recipe.inputs()) { + AdapterUtils.mergeIngredient(requiredInputs, input.ingredient(), input.count()); + } + return AdapterUtils.matchesRequired(mergedInputs, requiredInputs) + && FluidIngredientAllocator.matchesLong(recipe.inputFluids(), mergedFluids, 1L); + } + + private boolean matchesHeatFrameInput( + HeatFrameCoolingRecipe recipe, List actualInputs) { + if (actualInputs == null || actualInputs.isEmpty()) return true; + Either input = recipe.getInput(); + if (input == null) return false; + if (input.left().isPresent()) { + Ingredient ingredient = input.left().get(); + return actualInputs.stream().anyMatch(ingredient::test); + } + return input.right().map(container -> + actualInputs.stream().anyMatch(container::test)).orElse(false); + } + + @Nullable + private static AdvancedAlloyFurnaceRecipe convertAssembly( + ResourceLocation id, AssemblyRecipe source) { + if (source.getInput() == null || source.getInput().ingredient().isEmpty() + || source.getInput().count() <= 0 || source.getOutput().isEmpty()) { + return null; + } + + ItemStack program = switch (source.getProgramType()) { + case DRILL -> new ItemStack(ModItems.ASSEMBLY_PROGRAM_DRILL.get()); + case LASER -> new ItemStack(ModItems.ASSEMBLY_PROGRAM_LASER.get()); + case DRILL_LASER -> new ItemStack(ModItems.ASSEMBLY_PROGRAM_DRILL_LASER.get()); + }; + return build(id, + List.of(new CountedIngredient(source.getInput().ingredient(), source.getInput().count())), + List.of(), + List.of(source.getOutput().copy()), + List.of(Ingredient.of(new ItemStack(ModBlocks.ASSEMBLY_PLATFORM.get())), + Ingredient.of(program)), + AdapterUtils.DEFAULT_PROCESS_TIME); + } + + @Nullable + private static AdvancedAlloyFurnaceRecipe convertPressureChamber( + ResourceLocation id, PressureChamberRecipe source) { + if (source instanceof PressureEnchantingRecipe + || source instanceof PressureDisenchantingRecipe) { + return null; + } + List inputs = countedInputs(source.getInputs()); + List outputs = copiedItems(source.getOutputs()); + if (inputs.isEmpty() || outputs.isEmpty()) return null; + return build(id, inputs, List.of(), outputs, + List.of(Ingredient.of(new ItemStack(ModBlocks.PRESSURE_CHAMBER_VALVE.get()))), + AdapterUtils.DEFAULT_PROCESS_TIME); + } + + @Nullable + private static AdvancedAlloyFurnaceRecipe convertFluidMixer( + ResourceLocation id, FluidMixerRecipe source) { + List inputs = sizedFluids(source.getInput1(), source.getInput2()); + List outputs = source.getOutputItem().isEmpty() + ? List.of() : List.of(source.getOutputItem().copy()); + List outputFluids = source.getOutputFluid().isEmpty() + ? List.of() : List.of(source.getOutputFluid().copy()); + if (inputs.isEmpty() || outputs.isEmpty() && outputFluids.isEmpty()) return null; + int time = source.getProcessingTime() > 0 + ? source.getProcessingTime() : AdapterUtils.DEFAULT_PROCESS_TIME; + return build(id, List.of(), inputs, outputs, + List.of(Ingredient.of(new ItemStack(ModBlocks.FLUID_MIXER.get()))), time, outputFluids); + } + + @Nullable + private static AdvancedAlloyFurnaceRecipe convertHeatFrameCooling( + ResourceLocation id, HeatFrameCoolingRecipe source) { + Ingredient input = source.getInput().map( + ingredient -> ingredient, + container -> Ingredient.of(container.getItems())); + if (input == null || input.isEmpty() || source.getOutput().isEmpty()) return null; + + int outputCount = maximumHeatFrameOutput(source); + ItemStack output = source.getOutput().copyWithCount(outputCount); + return build(id, + List.of(new CountedIngredient(input, 1L)), + List.of(), + List.of(output), + List.of(Ingredient.of(new ItemStack(ModItems.HEAT_FRAME.get()))), + AdapterUtils.DEFAULT_PROCESS_TIME); + } + + @Nullable + private static AdvancedAlloyFurnaceRecipe convertRefinery( + ResourceLocation id, RefineryRecipe source) { + List inputs = sizedFluids(source.getInput()); + List outputs = copiedFluids(source.getOutputs()); + if (inputs.isEmpty() || outputs.isEmpty()) return null; + return build(id, List.of(), inputs, List.of(), + List.of(Ingredient.of(new ItemStack(ModBlocks.REFINERY.get()))), + AdapterUtils.DEFAULT_PROCESS_TIME, outputs); + } + + @Nullable + private static AdvancedAlloyFurnaceRecipe convertThermoPlant( + ResourceLocation id, ThermoPlantRecipe source) { + List inputs = new ArrayList<>(); + source.getInputItem().filter(ingredient -> !ingredient.isEmpty()) + .ifPresent(ingredient -> inputs.add(new CountedIngredient(ingredient, 1L))); + List fluids = source.getInputFluid() + .filter(PneumaticCraftRecipeAdapter::validFluid) + .map(List::of).orElse(List.of()); + + List outputs = source.getOutputItem().isEmpty() + ? List.of() : List.of(source.getOutputItem().copy()); + List outputFluids = source.getOutputFluid().isEmpty() + ? List.of() : List.of(source.getOutputFluid().copy()); + if (inputs.isEmpty() && fluids.isEmpty() || outputs.isEmpty() && outputFluids.isEmpty()) { + return null; + } + return build(id, inputs, fluids, outputs, + List.of(Ingredient.of(new ItemStack(ModBlocks.THERMOPNEUMATIC_PROCESSING_PLANT.get()))), + AdapterUtils.DEFAULT_PROCESS_TIME, outputFluids); + } + + @Nullable + private static AdvancedAlloyFurnaceRecipe convertAmadron( + ResourceLocation id, AmadronRecipe source) { + AmadronTradeResource input = source.getInput(); + AmadronTradeResource output = source.getOutput(); + if (input == null || output == null || input.isEmpty() || output.isEmpty()) return null; + + List inputs = new ArrayList<>(); + List fluids = new ArrayList<>(); + ItemStack inputItem = input.getItem(); + FluidStack inputFluid = input.getFluid(); + if (inputItem != null && !inputItem.isEmpty()) { + inputs.add(new CountedIngredient(itemIngredient(inputItem), inputItem.getCount())); + } else if (inputFluid != null && !inputFluid.isEmpty()) { + SizedFluidIngredient sized = AdapterUtils.toSizedFluidIngredient(inputFluid); + if (sized != null) fluids.add(sized); + } + + List outputs = new ArrayList<>(); + List outputFluids = new ArrayList<>(); + ItemStack outputItem = output.getItem(); + FluidStack outputFluid = output.getFluid(); + if (outputItem != null && !outputItem.isEmpty()) outputs.add(outputItem.copy()); + else if (outputFluid != null && !outputFluid.isEmpty()) outputFluids.add(outputFluid.copy()); + + if (inputs.isEmpty() && fluids.isEmpty() || outputs.isEmpty() && outputFluids.isEmpty()) { + return null; + } + return build(id, inputs, fluids, outputs, + List.of(Ingredient.of(new ItemStack(ModItems.AMADRON_TABLET.get()))), + AdapterUtils.DEFAULT_PROCESS_TIME, outputFluids); + } + + private static AdvancedAlloyFurnaceRecipe build( + ResourceLocation id, + List inputs, + List inputFluids, + List outputs, + List molds, + int processTime) { + return build(id, inputs, inputFluids, outputs, molds, processTime, List.of()); + } + + private static AdvancedAlloyFurnaceRecipe build( + ResourceLocation id, + List inputs, + List inputFluids, + List outputs, + List molds, + int processTime, + List outputFluids) { + return new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(id), inputs, inputFluids, List.of(), outputs, + outputFluids, List.of(), AdapterUtils.DEFAULT_ENERGY, + Math.max(1, processTime), Ingredient.EMPTY, 0, molds, AlloyFurnaceMode.NORMAL); + } + + private static List countedInputs( + List inputs) { + List result = new ArrayList<>(); + if (inputs == null) return result; + for (net.minecraftforge.common.crafting.SizedIngredient input : inputs) { + if (input != null && input.ingredient() != null && !input.ingredient().isEmpty() + && input.count() > 0) { + result.add(new CountedIngredient(input.ingredient(), input.count())); + } + } + return List.copyOf(result); + } + + private static List sizedFluids(SizedFluidIngredient... inputs) { + List result = new ArrayList<>(); + if (inputs != null) { + for (SizedFluidIngredient input : inputs) { + if (validFluid(input)) result.add(input); + } + } + return List.copyOf(result); + } + + private static boolean validFluid(SizedFluidIngredient input) { + return input != null && input.ingredient() != null && !input.ingredient().isEmpty() + && input.amount() > 0; + } + + private static List copiedItems(List stacks) { + List result = new ArrayList<>(); + if (stacks != null) { + for (ItemStack stack : stacks) { + if (stack != null && !stack.isEmpty() && stack.getCount() > 0) result.add(stack.copy()); + } + } + return List.copyOf(result); + } + + private static List copiedFluids(List stacks) { + List result = new ArrayList<>(); + if (stacks != null) { + for (FluidStack stack : stacks) { + if (stack != null && !stack.isEmpty() && stack.getAmount() > 0) result.add(stack.copy()); + } + } + return List.copyOf(result); + } + + private static Ingredient itemIngredient(ItemStack stack) { + ItemStack representative = stack.copyWithCount(1); + return representative.getComponents().isEmpty() + ? Ingredient.of(representative) + : DataComponentIngredient.of(true, representative); + } + + private static int maximumHeatFrameOutput(HeatFrameCoolingRecipe recipe) { + if (recipe.getBonusMultiplier() <= 0.0f) return 1; + double maximum = 1.0 + Math.max(0.0, recipe.getBonusLimit()); + if (maximum >= Integer.MAX_VALUE) return Integer.MAX_VALUE; + return Math.max(1, (int) Math.ceil(maximum)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/pneumaticcraft/PneumaticCraftSyntheticRecipe.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/pneumaticcraft/PneumaticCraftSyntheticRecipe.java new file mode 100644 index 00000000..6ab930ed --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/pneumaticcraft/PneumaticCraftSyntheticRecipe.java @@ -0,0 +1,55 @@ +package com.sorrowmist.useless.content.recipe.adapters.pneumaticcraft; + +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.Container; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; + +/** Recipe holder payload for PneumaticCraft machine data generated outside RecipeManager. */ +public final class PneumaticCraftSyntheticRecipe implements Recipe { + private final AdvancedAlloyFurnaceRecipe convertedRecipe; + + public PneumaticCraftSyntheticRecipe(AdvancedAlloyFurnaceRecipe convertedRecipe) { + this.convertedRecipe = convertedRecipe; + } + + public AdvancedAlloyFurnaceRecipe convertedRecipe() { + return convertedRecipe; + } + + @Override + public net.minecraft.resources.ResourceLocation getId() { return convertedRecipe.id(); } + + @Override + public boolean matches(Container input, Level level) { + return convertedRecipe != null && convertedRecipe.matches(input, level); + } + + @Override + public ItemStack assemble(Container input, net.minecraft.core.RegistryAccess registries) { + return convertedRecipe == null ? ItemStack.EMPTY : convertedRecipe.assemble(input, registries); + } + + @Override + public boolean canCraftInDimensions(int width, int height) { + return convertedRecipe != null && convertedRecipe.canCraftInDimensions(width, height); + } + + @Override + public ItemStack getResultItem(net.minecraft.core.RegistryAccess registries) { + return convertedRecipe == null ? ItemStack.EMPTY : convertedRecipe.getResultItem(registries); + } + + @Override + public RecipeSerializer getSerializer() { + return convertedRecipe == null ? null : convertedRecipe.getSerializer(); + } + + @Override + public RecipeType getType() { + return convertedRecipe == null ? null : convertedRecipe.getType(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/pneumaticcraft/PneumaticCraftSyntheticRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/pneumaticcraft/PneumaticCraftSyntheticRecipeAdapter.java new file mode 100644 index 00000000..dfc7297c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/pneumaticcraft/PneumaticCraftSyntheticRecipeAdapter.java @@ -0,0 +1,157 @@ +package com.sorrowmist.useless.content.recipe.adapters.pneumaticcraft; + +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import me.desht.pneumaticcraft.common.block.entity.processing.UVLightBoxBlockEntity; +import me.desht.pneumaticcraft.common.item.EmptyPCBItem; +import me.desht.pneumaticcraft.common.registry.ModBlocks; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Exposes PneumaticCraft's UV and etching data, which is generated from registered PCB items. */ +public final class PneumaticCraftSyntheticRecipeAdapter + implements IRecipeAdapter { + private enum Kind { + ETCHING, + UV_LIGHT_BOX + } + + private final Kind kind; + + private PneumaticCraftSyntheticRecipeAdapter(Kind kind) { + this.kind = kind; + } + + public static PneumaticCraftSyntheticRecipeAdapter etching() { + return new PneumaticCraftSyntheticRecipeAdapter(Kind.ETCHING); + } + + public static PneumaticCraftSyntheticRecipeAdapter uvLightBox() { + return new PneumaticCraftSyntheticRecipeAdapter(Kind.UV_LIGHT_BOX); + } + + @Override + public String sourceId() { + return RecipeSourceIds.PNEUMATICCRAFT; + } + + @Override + public Class getRecipeClass() { + return PneumaticCraftSyntheticRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(kind == Kind.ETCHING + ? ModBlocks.ETCHING_TANK.get() + : ModBlocks.UV_LIGHT_BOX.get()); + } + + @Override + public List> getGeneratedRecipes(Level level) { + List> result = new ArrayList<>(); + for (Item item : BuiltInRegistries.ITEM) { + if (!(item instanceof EmptyPCBItem pcb)) continue; + + AdvancedAlloyFurnaceRecipe converted = kind == Kind.ETCHING + ? etchingRecipe(pcb, item) + : uvLightBoxRecipe(pcb, item); + if (converted != null) { + result.add(new RecipeHolder<>(converted.id(), + new PneumaticCraftSyntheticRecipe(converted))); + } + } + return List.copyOf(result); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null || holder.value().convertedRecipe() == null) { + return List.of(); + } + return List.of(holder.value().convertedRecipe()); + } + + @Override + public List> findMatchingRecipes( + Level level, + Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) return List.of(); + + Map safeInputs = mergedInputs == null ? Map.of() : mergedInputs; + Map safeFluids = mergedFluids == null ? Map.of() : mergedFluids; + List> matches = new ArrayList<>(); + for (RecipeHolder holder : getGeneratedRecipes(level)) { + AdvancedAlloyFurnaceRecipe recipe = holder.value().convertedRecipe(); + if (recipe != null && PneumaticCraftRecipeAdapter.matchesConverted( + recipe, safeInputs, safeFluids)) { + matches.add(holder); + } + } + return matches; + } + + @Nullable + private AdvancedAlloyFurnaceRecipe etchingRecipe(EmptyPCBItem pcb, Item item) { + ItemStack input = new ItemStack(item); + FluidStack acid = EmptyPCBItem.getEtchingFluid(); + SizedFluidIngredient fluid = AdapterUtils.toSizedFluidIngredient(acid); + if (fluid == null) return null; + + ResourceLocation id = itemId(item, "etching"); + return recipe(id, input, List.of(fluid), pcb.getSuccessItem().copy()); + } + + private AdvancedAlloyFurnaceRecipe uvLightBoxRecipe(EmptyPCBItem pcb, Item item) { + ItemStack input = new ItemStack(item); + ItemStack output = UVLightBoxBlockEntity.setExposureProgress(input.copy(), 100); + return recipe(itemId(item, "uv_light_box"), input, List.of(), output); + } + + private AdvancedAlloyFurnaceRecipe recipe( + ResourceLocation id, + ItemStack input, + List fluids, + ItemStack output) { + return new AdvancedAlloyFurnaceRecipe( + id, + List.of(new CountedIngredient(Ingredient.of(input.copyWithCount(1)), 1L)), + fluids, + List.of(), + List.of(output), + List.of(), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + AdapterUtils.DEFAULT_PROCESS_TIME, + Ingredient.EMPTY, + 0, + List.of(Ingredient.of(getMoldItem())), + AlloyFurnaceMode.NORMAL); + } + + private static ResourceLocation itemId(Item item, String prefix) { + ResourceLocation itemId = BuiltInRegistries.ITEM.getKey(item); + String path = itemId == null ? "unknown" : itemId.getNamespace() + "_" + itemId.getPath(); + return new ResourceLocation( + RecipeSourceIds.PNEUMATICCRAFT, prefix + "_" + path.replace('/', '_')); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/powah/EnergizingRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/powah/EnergizingRecipeAdapter.java new file mode 100644 index 00000000..063ba6c0 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/powah/EnergizingRecipeAdapter.java @@ -0,0 +1,150 @@ +package com.sorrowmist.useless.content.recipe.adapters.powah; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import owmii.powah.block.Blcks; +import owmii.powah.block.energizing.EnergizingRecipe; +import owmii.powah.recipe.Recipes; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Converts Powah energizing recipes to alloy-furnace processing recipes. */ +public class EnergizingRecipeAdapter implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final int ENERGIZING_ORB_INPUT_CAPACITY = 6; + + @Override + public Class getRecipeClass() { + return EnergizingRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(Blcks.ENERGIZING_ORB.get()); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + Converted converted = convertData(holder.value()); + if (converted == null) { + RateLimitedLogger.warn(LOGGER, "Skipping invalid Powah energizing recipe: {}", holder.id()); + return List.of(); + } + + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + converted.inputs(), + List.of(), + converted.outputs(), + List.of(), + converted.energy(), + AdapterUtils.DEFAULT_PROCESS_TIME, + Ingredient.EMPTY, + 0, + AdapterUtils.toMoldIngredient(getMoldItem()), + AlloyFurnaceMode.NORMAL + )); + } + + @Override + public List> findMatchingRecipes( + Level level, + Map mergedInputs, + Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold) || mergedInputs == null || mergedInputs.isEmpty()) { + return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, Recipes.ENERGIZING.get())) { + Converted converted = convertData(holder.value()); + if (converted != null && AdapterUtils.matchesRequired(mergedInputs, converted.requirements())) { + matches.add(holder); + } + } + return matches; + } + + @Nullable + private static Converted convertData(@Nullable EnergizingRecipe source) { + if (source == null || source.getEnergy() < 0L) { + return null; + } + + Map requirements = new LinkedHashMap<>(); + List sourceIngredients = source.getIngredients(); + if (sourceIngredients == null || sourceIngredients.isEmpty() + || sourceIngredients.size() > ENERGIZING_ORB_INPUT_CAPACITY) { + return null; + } + for (Ingredient ingredient : sourceIngredients) { + if (ingredient == null || ingredient.isEmpty()) { + return null; + } + AdapterUtils.mergeIngredient(requirements, ingredient, 1L); + } + if (requirements.isEmpty()) { + return null; + } + + ItemStack result = source.getResultItem(); + if (result == null || result.isEmpty() || result.getCount() <= 0) { + return null; + } + List outputs = new ArrayList<>(); + if (!mergeOutput(outputs, result)) { + return null; + } + + List counted = requirements.entrySet().stream() + .map(entry -> new CountedIngredient(entry.getKey(), entry.getValue())) + .toList(); + return new Converted(counted, outputs, requirements, source.getEnergy()); + } + + private static boolean mergeOutput(List outputs, ItemStack output) { + for (ItemStack existing : outputs) { + if (!ItemStack.isSameItemSameTags(existing, output)) { + continue; + } + long count = (long) existing.getCount() + output.getCount(); + if (count > Integer.MAX_VALUE) { + return false; + } + existing.setCount((int) count); + return true; + } + outputs.add(output.copy()); + return true; + } + + private record Converted( + List inputs, + List outputs, + Map requirements, + long energy) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/productivebees/BeeBreedingRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/productivebees/BeeBreedingRecipeAdapter.java new file mode 100644 index 00000000..f5dabde4 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/productivebees/BeeBreedingRecipeAdapter.java @@ -0,0 +1,195 @@ +package com.sorrowmist.useless.content.recipe.adapters.productivebees; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import cy.jdkdigital.productivebees.ProductiveBees; +import cy.jdkdigital.productivebees.ProductiveBeesConfig; +import cy.jdkdigital.productivebees.common.crafting.ingredient.BeeIngredient; +import cy.jdkdigital.productivebees.common.crafting.ingredient.BeeIngredientFactory; +import cy.jdkdigital.productivebees.common.entity.bee.ProductiveBee; +import cy.jdkdigital.productivebees.common.recipe.BeeBreedingRecipe; +import cy.jdkdigital.productivebees.init.ModBlocks; +import cy.jdkdigital.productivebees.init.ModRecipeTypes; +import cy.jdkdigital.productivebees.setup.BeeReloadListener; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Converts Productive Bees breeding-chamber recipes into multi-mold alloy-furnace recipes. */ +public final class BeeBreedingRecipeAdapter implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + + @Override + public Class getRecipeClass() { + return BeeBreedingRecipe.class; + } + + @Override + public List> getGeneratedRecipes(Level level) { + if (level == null || !isBeeBreedingEnabled()) return List.of(); + + List> generated = new ArrayList<>(); + for (Map.Entry entry : BeeIngredientFactory.getOrCreateList().entrySet()) { + BeeIngredient bee = entry.getValue(); + if (bee == null || !canSelfBreed(entry.getKey(), bee, level)) continue; + + try { + ResourceLocation beeId = new ResourceLocation(entry.getKey()); + ResourceLocation recipeId = new ResourceLocation( + ProductiveBees.MODID, "bee_breeding_" + beeId.getPath() + "_self"); + generated.add(new RecipeHolder<>(recipeId, + new BeeBreedingRecipe(() -> bee, () -> bee, () -> bee, 0f))); + } catch (RuntimeException exception) { + } + } + return List.copyOf(generated); + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(ModBlocks.BREEDING_CHAMBER.get()); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (!isBeeBreedingEnabled() || holder == null || holder.value() == null) return List.of(); + + BeeBreedingRecipe source = holder.value(); + BeeIngredient parent1 = ProductiveBeesAdapterUtils.resolveBee(source.parent1); + BeeIngredient parent2 = ProductiveBeesAdapterUtils.resolveBee(source.parent2); + BeeIngredient offspring = ProductiveBeesAdapterUtils.resolveBee(source.offspring); + if (parent1 == null || parent2 == null || offspring == null + || parent1.getBeeType() == null || parent2.getBeeType() == null + || offspring.getBeeType() == null) { + RateLimitedLogger.warn(LOGGER, "Skipping Productive Bees breeding recipe with unresolved bee ingredient: {}", + holder.id()); + return List.of(); + } + + ItemStack parentEgg1 = ProductiveBeesAdapterUtils.spawnEgg(parent1); + ItemStack parentEgg2 = ProductiveBeesAdapterUtils.spawnEgg(parent2); + ItemStack offspringEgg = ProductiveBeesAdapterUtils.spawnEgg(offspring); + if (parentEgg1.isEmpty() || parentEgg2.isEmpty() || offspringEgg.isEmpty()) { + RateLimitedLogger.warn(LOGGER, "Skipping Productive Bees breeding recipe with unresolved spawn egg: {}", + holder.id()); + return List.of(); + } + + int parent1ItemCount = ProductiveBeesAdapterUtils.breedingItemCount(parent1.getBeeType()); + int parent2ItemCount = ProductiveBeesAdapterUtils.breedingItemCount(parent2.getBeeType()); + if (parent1ItemCount < 0 || parent2ItemCount < 0) { + RateLimitedLogger.warn(LOGGER, "Skipping Productive Bees breeding recipe with negative breeding item count: {}", + holder.id()); + return List.of(); + } + + Map requiredFlowers = new LinkedHashMap<>(); + AdapterUtils.mergeIngredient(requiredFlowers, + ProductiveBeesAdapterUtils.breedingIngredient(parent1.getBeeType()), + parent1ItemCount); + AdapterUtils.mergeIngredient(requiredFlowers, + ProductiveBeesAdapterUtils.breedingIngredient(parent2.getBeeType()), + parent2ItemCount); + + List inputs = requiredFlowers.entrySet().stream() + .map(entry -> new CountedIngredient(entry.getKey(), entry.getValue())) + .toList(); + if (inputs.isEmpty()) return List.of(); + + List molds = List.of( + AdapterUtils.toMoldIngredient(getMoldItem()), + cy.jdkdigital.productivebees.common.crafting.ingredient.ComponentIngredient.of(parentEgg1), + cy.jdkdigital.productivebees.common.crafting.ingredient.ComponentIngredient.of(parentEgg2)); + int processTime = Math.max(1, source.getProcessingTime()); + + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + inputs, + List.of(), + List.of(), + List.of(offspringEgg), + List.of(), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + processTime, + Ingredient.EMPTY, + 0, + molds, + AlloyFurnaceMode.NORMAL, + AdvancedAlloyFurnaceRecipe.NO_EXPLICIT_TIER)); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (!isBeeBreedingEnabled() || level == null || !matchesMold(mold) + || mergedInputs == null || mergedInputs.isEmpty()) { + return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + List> matches = new ArrayList<>(); + List> recipes = new ArrayList<>(com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + ModRecipeTypes.BEE_BREEDING_TYPE.get())); + recipes.addAll(getGeneratedRecipes(level)); + for (RecipeHolder holder : recipes) { + List converted = convertAll(holder, level); + if (!converted.isEmpty() && matchesInputs(converted.get(0), mergedInputs)) { + matches.add(holder); + } + } + return matches; + } + + private static boolean matchesInputs( + AdvancedAlloyFurnaceRecipe recipe, Map mergedInputs) { + Map required = new LinkedHashMap<>(); + for (CountedIngredient input : recipe.inputs()) { + AdapterUtils.mergeIngredient(required, input.ingredient(), input.count()); + } + return AdapterUtils.matchesRequired(mergedInputs, required); + } + + private static boolean canSelfBreed(String identifier, BeeIngredient bee, Level level) { + ResourceLocation beeId; + try { + beeId = new ResourceLocation(identifier); + } catch (RuntimeException exception) { + return false; + } + + var data = BeeReloadListener.INSTANCE.getData(beeId); + boolean canSelfBreed = !ProductiveBees.MODID.equals(beeId.getNamespace()) + || data == null + || data.getBoolean("selfbreed"); + if (canSelfBreed && data == null) { + var entity = bee.getCachedEntity(level); + canSelfBreed = !(entity instanceof ProductiveBee productiveBee) + || productiveBee.canSelfBreed(); + } + return canSelfBreed; + } + + private static boolean isBeeBreedingEnabled() { + return ProductiveBeesConfig.BEES.allowBeeBreeding.get(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/productivebees/BeeConversionRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/productivebees/BeeConversionRecipeAdapter.java new file mode 100644 index 00000000..907b766e --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/productivebees/BeeConversionRecipeAdapter.java @@ -0,0 +1,130 @@ +package com.sorrowmist.useless.content.recipe.adapters.productivebees; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.ExpectedOutputScaler; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import cy.jdkdigital.productivebees.common.crafting.ingredient.BeeIngredient; +import cy.jdkdigital.productivebees.common.crafting.ingredient.ComponentIngredient; +import cy.jdkdigital.productivebees.common.recipe.BeeConversionRecipe; +import cy.jdkdigital.productivebees.init.ModRecipeTypes; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; + +/** Converts Productive Bees bee-conversion recipes into item-input alloy-furnace recipes. */ +public final class BeeConversionRecipeAdapter implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + + @Override + public Class getRecipeClass() { + return BeeConversionRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return null; + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) return List.of(); + + BeeConversionRecipe source = holder.value(); + BeeIngredient sourceBee = ProductiveBeesAdapterUtils.resolveBee(source.source); + BeeIngredient resultBee = ProductiveBeesAdapterUtils.resolveBee(source.result); + if (sourceBee == null || resultBee == null || source.item == null || source.item.isEmpty()) { + RateLimitedLogger.warn(LOGGER, "Skipping Productive Bees conversion recipe with unresolved ingredient: {}", + holder.id()); + return List.of(); + } + + ItemStack sourceEgg = ProductiveBeesAdapterUtils.spawnEgg(sourceBee); + ItemStack resultEgg = ProductiveBeesAdapterUtils.spawnEgg(resultBee); + if (sourceEgg.isEmpty() || resultEgg.isEmpty()) { + RateLimitedLogger.warn(LOGGER, "Skipping Productive Bees conversion recipe with unresolved spawn egg: {}", + holder.id()); + return List.of(); + } + + Optional scaled = ExpectedOutputScaler.scale(List.of( + new ExpectedOutputScaler.WeightedItemOutput( + resultEgg, 1, 1, source.chance))); + if (scaled.isEmpty() || scaled.get().outputs().isEmpty()) { + RateLimitedLogger.warn(LOGGER, "Skipping Productive Bees conversion recipe with unsupported chance: {}", + holder.id()); + return List.of(); + } + + int operations = scaled.get().operations(); + OptionalInt energy = ExpectedOutputScaler.multiplyToInt(AdapterUtils.DEFAULT_ENERGY, operations); + OptionalInt processTime = ExpectedOutputScaler.multiplyToInt(AdapterUtils.DEFAULT_PROCESS_TIME, operations); + if (energy.isEmpty() || processTime.isEmpty()) { + RateLimitedLogger.warn(LOGGER, "Skipping overflowing Productive Bees conversion recipe: {}", holder.id()); + return List.of(); + } + + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + List.of( + new CountedIngredient(source.item, operations), + new CountedIngredient(ComponentIngredient.of(sourceEgg), operations)), + List.of(), + List.of(), + scaled.get().outputs(), + List.of(), + List.of(), + energy.getAsInt(), + processTime.getAsInt(), + Ingredient.EMPTY, + 0, + List.of(), + AlloyFurnaceMode.NORMAL, + AdvancedAlloyFurnaceRecipe.NO_EXPLICIT_TIER)); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, @Nullable ItemStack mold) { + if (level == null || mergedInputs == null || mergedInputs.isEmpty()) return List.of(); + + RecipeManager recipeManager = level.getRecipeManager(); + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + ModRecipeTypes.BEE_CONVERSION_TYPE.get())) { + List converted = convertAll(holder, level); + if (!converted.isEmpty() && matchesInputs(converted.get(0), mergedInputs)) { + matches.add(holder); + } + } + return matches; + } + + private static boolean matchesInputs( + AdvancedAlloyFurnaceRecipe recipe, Map mergedInputs) { + java.util.LinkedHashMap required = new java.util.LinkedHashMap<>(); + for (CountedIngredient input : recipe.inputs()) { + AdapterUtils.mergeIngredient(required, input.ingredient(), input.count()); + } + return AdapterUtils.matchesRequired(mergedInputs, required); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/productivebees/BeeFishingRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/productivebees/BeeFishingRecipeAdapter.java new file mode 100644 index 00000000..2594c0d3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/productivebees/BeeFishingRecipeAdapter.java @@ -0,0 +1,153 @@ +package com.sorrowmist.useless.content.recipe.adapters.productivebees; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import cy.jdkdigital.productivebees.common.crafting.ingredient.BeeIngredient; +import cy.jdkdigital.productivebees.common.recipe.BeeFishingRecipe; +import cy.jdkdigital.productivebees.init.ModRecipeTypes; +import cy.jdkdigital.productivebees.util.BeeCreator; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * 将资源蜜蜂的钓鱼配方({@code productivebees:bee_fishing})转换为合金炉配方。 + * + *

资源蜜蜂钓鱼不会向原版 {@code minecraft:fishing} 战利品表添加条目,而是监听 + * {@code ItemFishedEvent} 后按生物群系抽取独立配方并直接生成蜜蜂实体。因此此处单独读取 + * 该配方类型,并将产物由「蜜蜂实体」改为对应的蜜蜂刷怪蛋物品。

+ * + *

配方按 {@link ResourceLocation} 稳定 id 转换:id 仅取自数据包中的配方 id, + * 不依赖生物群系集合的迭代顺序,避免万象样板在重建索引后失效。

+ * + *

原版钓鱼适配器({@code FishingLootRecipeAdapter})不涉及该配方类型,两者不会重复转换。

+ */ +public final class BeeFishingRecipeAdapter implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + /** 每次转换消耗的水量,与其它钓鱼类转换保持一致。 */ + private static final int WATER_PER_OPERATION = 1_000; + + @Override + public String sourceId() { + return RecipeSourceIds.PRODUCTIVE_BEES; + } + + @Override + public Class getRecipeClass() { + return BeeFishingRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return new ItemStack(Items.FISHING_ROD); + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + return mold != null && !mold.isEmpty() && mold.is(Items.FISHING_ROD); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + + BeeFishingRecipe source = holder.value(); + ItemStack spawnEgg = resolveSpawnEgg(source); + if (spawnEgg.isEmpty()) { + RateLimitedLogger.warn(LOGGER, "Skipping unsupported Productive Bees fishing recipe: {}", holder.id()); + return List.of(); + } + + AdvancedAlloyFurnaceRecipe converted = new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + List.of(), + List.of(new FluidStack(Fluids.WATER, WATER_PER_OPERATION)), + List.of(spawnEgg), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + AdapterUtils.DEFAULT_PROCESS_TIME, + Ingredient.EMPTY, + 0, + Ingredient.of(Items.FISHING_ROD), + AlloyFurnaceMode.NORMAL); + return List.of(converted); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) { + return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + ModRecipeTypes.BEE_FISHING_TYPE.get())) { + BeeFishingRecipe source = holder.value(); + if (source == null || resolveSpawnEgg(source).isEmpty()) { + continue; + } + if (availableWater(mergedFluids) >= WATER_PER_OPERATION) { + matches.add(holder); + } + } + return matches; + } + + /** 将配方产物(蜜蜂)解析为对应的刷怪蛋物品;无法解析时返回空堆。 */ + private static ItemStack resolveSpawnEgg(BeeFishingRecipe source) { + BeeIngredient bee = source.output == null ? null : source.output.get(); + if (bee == null) { + return ItemStack.EMPTY; + } + ResourceLocation beeType = bee.getBeeType(); + if (beeType == null) { + return ItemStack.EMPTY; + } + // 资源蜜蜂的刷怪蛋可能不是原版 SpawnEggItem,因此仅判空,避免误排除可配置蜂种。 + // 该 API 在蜂种未注册刷怪蛋时可能返回 null,统一兜底为空气: + // 否则空指针将发生在配方生成阶段,导致整个配方目录构建失败。 + ItemStack egg = BeeCreator.getSpawnEgg(beeType); + return egg == null ? ItemStack.EMPTY : egg; + } + + private static long availableWater(Map fluids) { + if (fluids == null || fluids.isEmpty()) { + return 0L; + } + long amount = 0L; + for (Map.Entry entry : fluids.entrySet()) { + if (entry.getValue() == null || entry.getValue() <= 0L) { + continue; + } + if (entry.getKey() != null && entry.getKey().getFluid() == Fluids.WATER) { + amount += entry.getValue(); + } + } + return amount; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/productivebees/BeeProduceRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/productivebees/BeeProduceRecipeAdapter.java new file mode 100644 index 00000000..54993880 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/productivebees/BeeProduceRecipeAdapter.java @@ -0,0 +1,141 @@ +package com.sorrowmist.useless.content.recipe.adapters.productivebees; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.ExpectedOutputScaler; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import cy.jdkdigital.productivebees.common.crafting.ingredient.BeeIngredient; +import cy.jdkdigital.productivebees.common.crafting.ingredient.ComponentIngredient; +import cy.jdkdigital.productivebees.common.recipe.AdvancedBeehiveRecipe; +import cy.jdkdigital.productivebees.util.BeeCreator; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.SpawnEggItem; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; + +public final class BeeProduceRecipeAdapter implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + + @Override + public Class getRecipeClass() { + return AdvancedBeehiveRecipe.class; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return null; + } + + @Override + public boolean matchesMold(@Nullable ItemStack mold) { + return mold != null && !mold.isEmpty() && mold.getItem() instanceof SpawnEggItem; + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder == null) { + return List.of(); + } + + AdvancedBeehiveRecipe source = holder.value(); + BeeIngredient bee = source.ingredient.get(); + if (bee == null) { + return List.of(); + } + + ItemStack spawnEgg = BeeCreator.getSpawnEgg(bee.getBeeType()); + Optional scaled = ProductiveBeesAdapterUtils.scaleOutputs( + source.getRecipeOutputs() + ); + if (spawnEgg.isEmpty() || scaled.isEmpty() || scaled.get().outputs().isEmpty()) { + RateLimitedLogger.warn(LOGGER, "Skipping unsupported Productive Bees produce recipe: {}", holder.id()); + return List.of(); + } + + int operations = scaled.get().operations(); + OptionalInt energy = ExpectedOutputScaler.multiplyToInt(AdapterUtils.DEFAULT_ENERGY, operations); + OptionalInt processTime = ExpectedOutputScaler.multiplyToInt(AdapterUtils.DEFAULT_PROCESS_TIME, operations); + if (energy.isEmpty() || processTime.isEmpty()) { + RateLimitedLogger.warn(LOGGER, "Skipping overflowing Productive Bees produce recipe: {}", holder.id()); + return List.of(); + } + + AdvancedAlloyFurnaceRecipe converted = new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + List.of(), + List.of(new FluidStack(Fluids.WATER, operations)), + scaled.get().outputs(), + List.of(), + energy.getAsInt(), + processTime.getAsInt(), + Ingredient.EMPTY, + 0, + ComponentIngredient.of(spawnEgg), + AlloyFurnaceMode.NORMAL + ); + return List.of(converted); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold) || mergedFluids.isEmpty()) { + return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + cy.jdkdigital.productivebees.init.ModRecipeTypes.ADVANCED_BEEHIVE_TYPE.get())) { + AdvancedBeehiveRecipe source = holder.value(); + BeeIngredient bee = source.ingredient.get(); + if (bee == null) { + continue; + } + + ItemStack expectedEgg = BeeCreator.getSpawnEgg(bee.getBeeType()); + if (expectedEgg.isEmpty() || !ComponentIngredient.of(expectedEgg).test(mold)) { + continue; + } + + Optional scaled = ProductiveBeesAdapterUtils.scaleOutputs( + source.getRecipeOutputs() + ); + if (scaled.isEmpty() || scaled.get().outputs().isEmpty()) { + continue; + } + if (availableFluid(mergedFluids, new FluidStack(Fluids.WATER, 1)) >= scaled.get().operations()) { + matches.add(holder); + } + } + return matches; + } + + private static long availableFluid(Map fluids, FluidStack required) { + long amount = 0; + for (Map.Entry entry : fluids.entrySet()) { + if (com.sorrowmist.useless.compat.forge1201.fluid.FluidStacks.same(entry.getKey(), required)) { + amount += entry.getValue(); + } + } + return amount; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/productivebees/CentrifugeRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/productivebees/CentrifugeRecipeAdapter.java new file mode 100644 index 00000000..b7baf682 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/productivebees/CentrifugeRecipeAdapter.java @@ -0,0 +1,124 @@ +package com.sorrowmist.useless.content.recipe.adapters.productivebees; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.ExpectedOutputScaler; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import cy.jdkdigital.productivebees.common.recipe.CentrifugeRecipe; +import cy.jdkdigital.productivebees.init.ModBlocks; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; + +public final class CentrifugeRecipeAdapter implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + + @Override + public Class getRecipeClass() { + return CentrifugeRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return new ItemStack(ModBlocks.CENTRIFUGE.get()); + } + + @Override + public List convertAll(RecipeHolder holder, Level level) { + if (holder == null) { + return List.of(); + } + + CentrifugeRecipe source = holder.value(); + if (source.ingredient == null || source.ingredient.isEmpty()) { + return List.of(); + } + + Optional scaled = ProductiveBeesAdapterUtils.scaleOutputs( + source.getRecipeOutputs() + ); + if (scaled.isEmpty()) { + RateLimitedLogger.warn(LOGGER, "Skipping unsupported Productive Bees centrifuge recipe: {}", holder.id()); + return List.of(); + } + + int operations = scaled.get().operations(); + FluidStack fluidOutput = source.getFluidOutputs().copy(); + if (!fluidOutput.isEmpty()) { + OptionalInt fluidAmount = ExpectedOutputScaler.multiplyToInt(fluidOutput.getAmount(), operations); + if (fluidAmount.isEmpty()) { + RateLimitedLogger.warn(LOGGER, "Skipping overflowing Productive Bees centrifuge fluid output: {}", holder.id()); + return List.of(); + } + fluidOutput.setAmount(fluidAmount.getAsInt()); + } + if (scaled.get().outputs().isEmpty() && fluidOutput.isEmpty()) { + return List.of(); + } + + OptionalInt energy = ExpectedOutputScaler.multiplyToInt(AdapterUtils.DEFAULT_ENERGY, operations); + OptionalInt processTime = ExpectedOutputScaler.multiplyToInt(Math.max(1, source.getProcessingTime()), operations); + if (energy.isEmpty() || processTime.isEmpty()) { + RateLimitedLogger.warn(LOGGER, "Skipping overflowing Productive Bees centrifuge recipe: {}", holder.id()); + return List.of(); + } + + AdvancedAlloyFurnaceRecipe converted = new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + List.of(new CountedIngredient(source.ingredient, operations)), + List.of(), + scaled.get().outputs(), + fluidOutput.isEmpty() ? List.of() : List.of(fluidOutput), + energy.getAsInt(), + processTime.getAsInt(), + Ingredient.EMPTY, + 0, + AdapterUtils.toMoldIngredient(getMoldItem()), + AlloyFurnaceMode.NORMAL + ); + return List.of(converted); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || mergedInputs.isEmpty() || !matchesMold(mold)) { + return List.of(); + } + + RecipeManager recipeManager = level.getRecipeManager(); + List> matches = new ArrayList<>(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + cy.jdkdigital.productivebees.init.ModRecipeTypes.CENTRIFUGE_TYPE.get())) { + CentrifugeRecipe source = holder.value(); + if (source.ingredient == null || source.ingredient.isEmpty()) { + continue; + } + Optional scaled = ProductiveBeesAdapterUtils.scaleOutputs( + source.getRecipeOutputs() + ); + if (scaled.isPresent() + && AdapterUtils.hasMatchingIngredient(mergedInputs, source.ingredient, scaled.get().operations())) { + matches.add(holder); + } + } + return matches; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/productivebees/ProductiveBeesAdapterUtils.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/productivebees/ProductiveBeesAdapterUtils.java new file mode 100644 index 00000000..a52cabee --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/productivebees/ProductiveBeesAdapterUtils.java @@ -0,0 +1,68 @@ +package com.sorrowmist.useless.content.recipe.adapters.productivebees; + +import com.sorrowmist.useless.content.recipe.ExpectedOutputScaler; +import cy.jdkdigital.productivebees.common.crafting.ingredient.BeeIngredient; +import cy.jdkdigital.productivebees.common.entity.bee.ConfigurableBee; +import cy.jdkdigital.productivebees.init.ModTags; +import cy.jdkdigital.productivebees.setup.BeeReloadListener; +import cy.jdkdigital.productivebees.util.BeeCreator; +import cy.jdkdigital.productivelib.common.recipe.TagOutputRecipe; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Supplier; + +final class ProductiveBeesAdapterUtils { + private ProductiveBeesAdapterUtils() { + } + + static Optional scaleOutputs( + Map outputs) { + List weightedOutputs = new ArrayList<>(); + for (Map.Entry entry : outputs.entrySet()) { + TagOutputRecipe.ChancedOutput value = entry.getValue(); + weightedOutputs.add(new ExpectedOutputScaler.WeightedItemOutput( + entry.getKey(), value.min(), value.max(), value.chance() + )); + } + return ExpectedOutputScaler.scale(weightedOutputs); + } + + @Nullable + static BeeIngredient resolveBee(@Nullable Supplier supplier) { + if (supplier == null) return null; + try { + return supplier.get(); + } catch (RuntimeException ignored) { + return null; + } + } + + static ItemStack spawnEgg(@Nullable BeeIngredient bee) { + if (bee == null || bee.getBeeType() == null) return ItemStack.EMPTY; + try { + return BeeCreator.getSpawnEgg(bee.getBeeType()); + } catch (RuntimeException ignored) { + return ItemStack.EMPTY; + } + } + + static Ingredient breedingIngredient(ResourceLocation beeType) { + CompoundTag data = BeeReloadListener.INSTANCE.getData(beeType); + return data == null + ? Ingredient.of(ModTags.DEFAULT_BREEDING) + : ConfigurableBee.getBreedingIngredientFromString(data.getString("breedingItem")); + } + + static int breedingItemCount(ResourceLocation beeType) { + CompoundTag data = BeeReloadListener.INSTANCE.getData(beeType); + return data == null ? 1 : data.getInt("breedingItemCount"); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/summoningrituals/SummoningRitualsAltarRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/summoningrituals/SummoningRitualsAltarRecipeAdapter.java new file mode 100644 index 00000000..9930d14b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/summoningrituals/SummoningRitualsAltarRecipeAdapter.java @@ -0,0 +1,176 @@ +package com.sorrowmist.useless.content.recipe.adapters.summoningrituals; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.almostreliable.summoningrituals.Registration; +import com.almostreliable.summoningrituals.recipe.AltarRecipe; +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import com.almostreliable.summoningrituals.recipe.component.IngredientStack; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Converts Summoning Rituals item-output altar recipes into alloy-furnace recipes. */ +public final class SummoningRitualsAltarRecipeAdapter implements IRecipeAdapter { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final ResourceLocation ALTAR_ID = new ResourceLocation( + "summoningrituals", "altar"); + + @Override + public Class getRecipeClass() { + return AltarRecipe.class; + } + + @Override + public ItemStack getMoldItem() { + return BuiltInRegistries.ITEM.getOptional(ALTAR_ID) + .map(ItemStack::new) + .orElse(ItemStack.EMPTY); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + + Converted converted = convertData(holder.value()); + if (converted == null) { + RateLimitedLogger.warn(LOGGER, "Skipping unsupported Summoning Rituals altar recipe: {}", holder.id()); + return List.of(); + } + + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), + converted.inputs(), + List.of(), + converted.outputs(), + List.of(), + AdapterUtils.DEFAULT_ENERGY, + converted.processTime(), + Ingredient.EMPTY, + 0, + AdapterUtils.toMoldIngredient(getMoldItem()), + AlloyFurnaceMode.NORMAL + )); + } + + @Override + public List> findMatchingRecipes( + Level level, Map mergedInputs, Map mergedFluids, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold) || mergedInputs == null || mergedInputs.isEmpty()) { + return List.of(); + } + + List> matches = new ArrayList<>(); + RecipeManager recipeManager = level.getRecipeManager(); + for (RecipeHolder holder : com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(recipeManager, + Registration.ALTAR_RECIPE.type().get())) { + AltarRecipe source = holder.value(); + Converted converted = source == null ? null : convertData(source); + if (converted != null && AdapterUtils.matchesRequired(mergedInputs, converted.requirements())) { + matches.add(holder); + } + } + return matches; + } + + @Nullable + private static Converted convertData(@Nullable AltarRecipe source) { + if (source == null || source.getRecipeTime() <= 0 + || source.getCatalyst() == null || source.getCatalyst().isEmpty()) { + return null; + } + + var sourceInputs = source.getInputs(); + if (sourceInputs == null || !source.getSacrifices().isEmpty()) return null; + var sourceOutputs = source.getOutputs(); + if (sourceOutputs == null || sourceOutputs.size() == 0) return null; + List outputs = new ArrayList<>(); + boolean[] unsupported = {false}; + sourceOutputs.forEach((type, output, index) -> { + if (type != com.almostreliable.summoningrituals.recipe.component.RecipeOutputs.OutputType.ITEM + || !(output.getOutput() instanceof ItemStack stack) || stack.isEmpty()) unsupported[0]=true; + else { + ItemStack result = nativeItemOutput(stack, output.getData()); + if (result == null || result.isEmpty()) unsupported[0] = true; + else outputs.add(result); + } + }); + if (unsupported[0]) return null; + + Map requirements = new LinkedHashMap<>(); + if (!addRequirement(requirements, source.getCatalyst(), 1L)) { + return null; + } + for (IngredientStack input : sourceInputs) { + if (input == null || !addRequirement(requirements, input.ingredient(), input.count())) { + return null; + } + } + + if (requirements.isEmpty() || outputs.isEmpty()) { + return null; + } + + List inputs = requirements.entrySet().stream() + .map(entry -> new CountedIngredient(entry.getKey(), entry.getValue())) + .toList(); + return new Converted(inputs, requirements, List.copyOf(outputs), Math.max(1, source.getRecipeTime())); + } + + /** Summoning Rituals applies data to each spawned ItemEntity, not to the item tag. + * It splits the template into batches of at most four before replacing entity NBT. + * Reject other entity-only semantics instead of silently changing ownership/lifetime. + */ + static ItemStack nativeItemOutput(ItemStack template, net.minecraft.nbt.CompoundTag data) { + if (data == null || data.isEmpty()) return template.copy(); + if (!data.getAllKeys().equals(java.util.Set.of("Item")) + || !data.contains("Item", net.minecraft.nbt.Tag.TAG_COMPOUND)) return null; + ItemStack replaced = ItemStack.of(data.getCompound("Item")); + if (replaced.isEmpty()) return null; + long total = ((template.getCount() + 3L) / 4L) * replaced.getCount(); + if (total <= 0 || total > Integer.MAX_VALUE) return null; + replaced.setCount((int)total); + return replaced; + } + + private static boolean addRequirement( + Map requirements, @Nullable Ingredient ingredient, long count) { + if (ingredient == null || ingredient.isEmpty() || count <= 0L) { + return false; + } + try { + AdapterUtils.mergeIngredient(requirements, ingredient, count); + return true; + } catch (ArithmeticException exception) { + return false; + } + } + + private record Converted( + List inputs, + Map requirements, + List outputs, + int processTime) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ufo/DimensionalMatterAssemblerRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ufo/DimensionalMatterAssemblerRecipeAdapter.java new file mode 100644 index 00000000..a3975aa9 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ufo/DimensionalMatterAssemblerRecipeAdapter.java @@ -0,0 +1,76 @@ +package com.sorrowmist.useless.content.recipe.adapters.ufo; + +import appeng.api.stacks.GenericStack; +import com.raishxn.ufo.block.ModBlocks; +import com.raishxn.ufo.init.ModRecipes; +import com.raishxn.ufo.recipe.DimensionalMatterAssemblerRecipe; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import org.jetbrains.annotations.Nullable; +import net.pedroksl.ae2addonlib.recipes.IngredientStack; + +import java.util.ArrayList; +import java.util.List; + +/** Converts UFO Dimensional Matter Assembler recipes. */ +public final class DimensionalMatterAssemblerRecipeAdapter + extends UfoRecipeAdapter { + @Override + public Class getRecipeClass() { + return DimensionalMatterAssemblerRecipe.class; + } + + @Override + protected RecipeType recipeType() { + return ModRecipes.DMA_RECIPE_TYPE.get(); + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return new ItemStack(ModBlocks.DIMENSIONAL_MATTER_ASSEMBLER_BLOCK.get()); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + DimensionalMatterAssemblerRecipe source = holder.value(); + if (source.getTime() <= 0) { + return List.of(); + } + + List items = UfoRecipeAdapterSupport.itemInputs( + source.getItemInputs(), IngredientStack.Item::getIngredient, + value -> value.getAmount()); + List fluids = UfoRecipeAdapterSupport.fluidInputs( + source.getFluidInputs(), IngredientStack.Fluid::getIngredient, + value -> value.getAmount()); + if (fluids == null) { + return List.of(); + } + List keyOutputs = new ArrayList<>(); + UfoRecipeAdapterSupport.addGenericOutputs(keyOutputs, source.getItemOutputs()); + UfoRecipeAdapterSupport.addGenericOutputs(keyOutputs, source.getFluidOutputs()); + if ((items.isEmpty() && fluids.isEmpty()) || keyOutputs.isEmpty()) { + return List.of(); + } + + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), items, fluids, List.of(), + List.of(), List.of(), keyOutputs, + UfoRecipeAdapterSupport.energy(source.getEnergy()), source.getTime(), + Ingredient.EMPTY, 0, AdapterUtils.toMoldIngredient(getMoldItem()), + AlloyFurnaceMode.NORMAL)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ufo/QMFRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ufo/QMFRecipeAdapter.java new file mode 100644 index 00000000..e44045d6 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ufo/QMFRecipeAdapter.java @@ -0,0 +1,85 @@ +package com.sorrowmist.useless.content.recipe.adapters.ufo; + +import com.raishxn.ufo.init.ModRecipes; +import com.raishxn.ufo.recipe.QMFRecipe; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.raishxn.ufo.block.MultiblockBlocks; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** Converts UFO's standalone Quantum Matter Fabricator recipes. */ +public final class QMFRecipeAdapter extends UfoRecipeAdapter { + @Override + public Class getRecipeClass() { + return QMFRecipe.class; + } + + @Override + protected RecipeType recipeType() { + return ModRecipes.QMF_TYPE.get(); + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return new ItemStack(MultiblockBlocks.QUANTUM_MATTER_FABRICATOR_CONTROLLER.get()); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + QMFRecipe source = holder.value(); + if (source.getTime() <= 0) { + return List.of(); + } + + List items = UfoRecipeAdapterSupport.itemInputs( + source.getItemInputs(), QMFRecipe.QMFRecipeIngredient::ingredient, + QMFRecipe.QMFRecipeIngredient::amount); + List fluids = + UfoRecipeAdapterSupport.concreteFluidInputs( + source.getFluidInputs(), QMFRecipe.QMFFluidIngredient::fluid, + QMFRecipe.QMFFluidIngredient::amount); + if (fluids == null) { + return List.of(); + } + List chemicals = UfoRecipeAdapterSupport.chemicalInputs( + source.getChemicalInputs(), QMFRecipe.QMFChemicalIngredient::chemicalId, + QMFRecipe.QMFChemicalIngredient::amount); + if (chemicals == null) { + return List.of(); + } + + List outputs = new ArrayList<>(); + List outputFluids = new ArrayList<>(); + List keyOutputs = new ArrayList<>(); + ItemStack output = source.getResultItem(); + UfoRecipeAdapterSupport.addItemOutput(outputs, keyOutputs, output, + output == null ? 0L : output.getCount()); + if ((items.isEmpty() && fluids.isEmpty() && chemicals.isEmpty()) + || (outputs.isEmpty() && outputFluids.isEmpty() && keyOutputs.isEmpty())) { + return List.of(); + } + + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), items, fluids, chemicals, + outputs, outputFluids, keyOutputs, + UfoRecipeAdapterSupport.energy(source.getEnergy()), source.getTime(), + Ingredient.EMPTY, 0, AdapterUtils.toMoldIngredient(getMoldItem()), + AlloyFurnaceMode.NORMAL)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ufo/StellarSimulationRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ufo/StellarSimulationRecipeAdapter.java new file mode 100644 index 00000000..c3fbdf4f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ufo/StellarSimulationRecipeAdapter.java @@ -0,0 +1,98 @@ +package com.sorrowmist.useless.content.recipe.adapters.ufo; + +import appeng.api.stacks.GenericStack; +import com.raishxn.ufo.block.MultiblockBlocks; +import com.raishxn.ufo.init.ModRecipes; +import com.raishxn.ufo.recipe.StellarSimulationRecipe; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import org.jetbrains.annotations.Nullable; +import net.pedroksl.ae2addonlib.recipes.IngredientStack; + +import java.util.ArrayList; +import java.util.List; + +/** Converts UFO Stellar Simulation recipes, including fuel and coolant fluids. */ +public final class StellarSimulationRecipeAdapter + extends UfoRecipeAdapter { + @Override + public Class getRecipeClass() { + return StellarSimulationRecipe.class; + } + + @Override + protected RecipeType recipeType() { + return ModRecipes.STELLAR_SIMULATION_TYPE.get(); + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return new ItemStack(MultiblockBlocks.STELLAR_NEXUS_CONTROLLER.get()); + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null) { + return List.of(); + } + StellarSimulationRecipe source = holder.value(); + if (source.getTime() <= 0) { + return List.of(); + } + + List items = UfoRecipeAdapterSupport.itemInputs( + source.getItemInputs(), IngredientStack.Item::getIngredient, + value -> value.getAmount()); + List fluids = UfoRecipeAdapterSupport.fluidInputs( + source.getFluidInputs(), IngredientStack.Fluid::getIngredient, + value -> value.getAmount()); + if (fluids == null) { + return List.of(); + } + SizedFluidIngredient fuel = UfoRecipeAdapterSupport.namedFluid( + source.getFuelFluid(), source.getFuelAmount()); + if (source.getFuelAmount() > 0L && fuel == null) { + return List.of(); + } + if (fuel != null) { + fluids = append(fluids, fuel); + } + if (source.getCoolantAmount() > 0L) { + if (source.getCoolantAmount() > Integer.MAX_VALUE) { + return List.of(); + } + fluids = append(fluids, UfoRecipeAdapterSupport.coolant(source.getCoolantAmount())); + } + + List keyOutputs = new ArrayList<>(); + UfoRecipeAdapterSupport.addGenericOutputs(keyOutputs, source.getItemOutputs()); + UfoRecipeAdapterSupport.addGenericOutputs(keyOutputs, source.getFluidOutputs()); + if ((items.isEmpty() && fluids.isEmpty()) || keyOutputs.isEmpty()) { + return List.of(); + } + + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), items, fluids, List.of(), + List.of(), List.of(), keyOutputs, + UfoRecipeAdapterSupport.energy(source.getEnergyCost()), source.getTime(), + Ingredient.EMPTY, 0, AdapterUtils.toMoldIngredient(getMoldItem()), + AlloyFurnaceMode.NORMAL)); + } + + private static List append( + List fluids, SizedFluidIngredient additional) { + List result = new ArrayList<>(fluids); + result.add(additional); + return List.copyOf(result); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ufo/UfoChemicalCompat.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ufo/UfoChemicalCompat.java new file mode 100644 index 00000000..822365ab --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ufo/UfoChemicalCompat.java @@ -0,0 +1,46 @@ +package com.sorrowmist.useless.content.recipe.adapters.ufo; + +import appeng.api.stacks.GenericStack; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Method; + +/** Optional UFO/Mekanism/AppMek bridge kept out of the normal recipe path. */ +final class UfoChemicalCompat { + private static final String UFO_COMPAT_CLASS = + "com.raishxn.ufo.compat.mekanism.MekanismChemicalCompat"; + private static final String MEKANISM_STACK_CLASS = "mekanism.api.chemical.ChemicalStack"; + private static final String FURNACE_CHEMICAL_COMPAT_CLASS = + "com.sorrowmist.useless.compat.mekanism.MekanismChemicalCompat"; + + private UfoChemicalCompat() { + } + + @Nullable + static GenericStack toGenericStack(ResourceLocation chemicalId, long amount) { + if (chemicalId == null || amount <= 0L) { + return null; + } + + try { + ClassLoader loader = UfoChemicalCompat.class.getClassLoader(); + Class ufoCompat = Class.forName(UFO_COMPAT_CLASS, true, loader); + Method createStack = ufoCompat.getMethod("createStack", ResourceLocation.class, long.class); + Object chemical = createStack.invoke(null, chemicalId, amount); + if (chemical == null) { + return null; + } + + Class chemicalStack = Class.forName(MEKANISM_STACK_CLASS, true, loader); + Class furnaceCompat = Class.forName(FURNACE_CHEMICAL_COMPAT_CLASS, true, loader); + Method toGenericStack = furnaceCompat.getMethod("toGenericStack", chemicalStack); + Object generic = toGenericStack.invoke(null, chemical); + return generic instanceof GenericStack stack && stack.what() != null && stack.amount() > 0L + ? stack + : null; + } catch (ReflectiveOperationException | LinkageError | RuntimeException ignored) { + return null; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ufo/UfoRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ufo/UfoRecipeAdapter.java new file mode 100644 index 00000000..49615518 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ufo/UfoRecipeAdapter.java @@ -0,0 +1,55 @@ +package com.sorrowmist.useless.content.recipe.adapters.ufo; + +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.IRecipeAdapter; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.Recipe; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +abstract class UfoRecipeAdapter> implements IRecipeAdapter { + protected abstract RecipeType recipeType(); + + protected boolean accepts(T recipe) { + return recipe != null; + } + + @Override + @SuppressWarnings({"unchecked", "rawtypes"}) + public List> findMatchingRecipes( + Level level, Map mergedInputs, + Map mergedFluids, Map mergedKeys, + @Nullable ItemStack mold) { + if (level == null || !matchesMold(mold)) { + return List.of(); + } + RecipeManager manager = level.getRecipeManager(); + List> result = new ArrayList<>(); + for (RecipeHolder holder : (List>) (List) + com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getAllRecipesFor(manager, (RecipeType) recipeType())) { + if (!accepts(holder.value())) { + continue; + } + boolean matched = false; + for (AdvancedAlloyFurnaceRecipe converted : convertAll(holder, level)) { + if (UfoRecipeAdapterSupport.matches(converted, mergedInputs, mergedFluids, mergedKeys)) { + matched = true; + break; + } + } + if (matched) { + result.add(holder); + } + } + return result; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ufo/UfoRecipeAdapterSupport.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ufo/UfoRecipeAdapterSupport.java new file mode 100644 index 00000000..7c959ac1 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ufo/UfoRecipeAdapterSupport.java @@ -0,0 +1,253 @@ +package com.sorrowmist.useless.content.recipe.adapters.ufo; + +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import com.sorrowmist.useless.content.recipe.FluidIngredientAllocator; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.ToLongFunction; + +final class UfoRecipeAdapterSupport { + private static final ResourceLocation COOLANTS_ID = + new ResourceLocation("c", "coolants"); + private static final TagKey COOLANTS = + TagKey.create(Registries.FLUID, COOLANTS_ID); + + private UfoRecipeAdapterSupport() { + } + + static long energy(long aeEnergy) { + return saturatingMultiply(aeEnergy, 2L); + } + + static long saturatingMultiply(long left, long right) { + if (left <= 0L || right <= 0L) { + return 0L; + } + return left > Long.MAX_VALUE / right ? Long.MAX_VALUE : left * right; + } + + static List itemInputs( + @Nullable Iterable source, + Function ingredientGetter, + ToLongFunction amountGetter) { + if (source == null) { + return List.of(); + } + List result = new ArrayList<>(); + for (T value : source) { + if (value == null) { + continue; + } + Ingredient ingredient = ingredientGetter.apply(value); + long amount = amountGetter.applyAsLong(value); + if (ingredient != null && !ingredient.isEmpty() && amount > 0L) { + result.add(new CountedIngredient(ingredient, amount)); + } + } + return List.copyOf(result); + } + + @Nullable + static List fluidInputs( + @Nullable Iterable source, + Function ingredientGetter, + ToLongFunction amountGetter) { + if (source == null) { + return List.of(); + } + List result = new ArrayList<>(); + for (T value : source) { + if (value == null) { + continue; + } + FluidIngredient ingredient = ingredientGetter.apply(value); + long amount = amountGetter.applyAsLong(value); + if (amount > Integer.MAX_VALUE) { + return null; + } + if (ingredient != null && !ingredient.isEmpty() && amount > 0L) { + result.add(new SizedFluidIngredient(ingredient, (int) amount)); + } + } + return List.copyOf(result); + } + + @Nullable + static List concreteFluidInputs( + @Nullable Iterable source, + Function fluidGetter, + ToLongFunction amountGetter) { + if (source == null) { + return List.of(); + } + List result = new ArrayList<>(); + for (T value : source) { + if (value == null) { + continue; + } + FluidStack fluid = fluidGetter.apply(value); + long amount = amountGetter.applyAsLong(value); + if (amount > Integer.MAX_VALUE) { + return null; + } + if (fluid == null || fluid.isEmpty() || amount <= 0L) { + continue; + } + FluidStack representative = new net.minecraftforge.fluids.FluidStack(fluid, (int) amount); + SizedFluidIngredient ingredient = AdapterUtils.toSizedFluidIngredient(representative); + if (ingredient != null) { + result.add(ingredient); + } + } + return List.copyOf(result); + } + + @Nullable + static SizedFluidIngredient namedFluid(@Nullable String id, long amount) { + if (id == null || id.isBlank() || amount <= 0L) { + return null; + } + if (amount > Integer.MAX_VALUE) { + return null; + } + final ResourceLocation location; + try { + location = new ResourceLocation(id); + } catch (IllegalArgumentException exception) { + return null; + } + var fluid = BuiltInRegistries.FLUID.getOptional(location).orElse(null); + if (fluid == null || fluid == net.minecraft.world.level.material.Fluids.EMPTY) { + return null; + } + return new SizedFluidIngredient(FluidIngredient.single(fluid), (int) amount); + } + + static SizedFluidIngredient coolant(long amount) { + return new SizedFluidIngredient(FluidIngredient.tag(COOLANTS), AdapterUtils.safeInt(amount)); + } + + static List chemicalInputs( + @Nullable Iterable source, + Function idGetter, + ToLongFunction amountGetter) { + if (source == null) { + return List.of(); + } + List result = new ArrayList<>(); + for (T value : source) { + if (value == null) { + continue; + } + long amount = amountGetter.applyAsLong(value); + if (amount <= 0L) { + continue; + } + GenericStack converted = UfoChemicalCompat.toGenericStack(idGetter.apply(value), amount); + if (converted == null) { + return null; + } + result.add(converted); + } + return List.copyOf(result); + } + + static void addItemOutput(List outputs, List keyOutputs, + @Nullable ItemStack source, long amount) { + if (source == null || source.isEmpty() || amount <= 0L) { + return; + } + if (amount <= Integer.MAX_VALUE) { + outputs.add(source.copyWithCount((int) amount)); + return; + } + GenericStack key = GenericStack.fromItemStack(source.copyWithCount(1)); + if (key != null && key.what() != null) { + keyOutputs.add(new GenericStack(key.what(), amount)); + } + } + + static void addFluidOutput(List outputs, List keyOutputs, + @Nullable FluidStack source, long amount) { + if (source == null || source.isEmpty() || amount <= 0L) { + return; + } + if (amount <= Integer.MAX_VALUE) { + outputs.add(new net.minecraftforge.fluids.FluidStack(source, (int) amount)); + return; + } + GenericStack key = GenericStack.fromFluidStack(new net.minecraftforge.fluids.FluidStack(source, 1)); + if (key != null && key.what() != null) { + keyOutputs.add(new GenericStack(key.what(), amount)); + } + } + + static void addGenericOutputs(List target, @Nullable Iterable source) { + if (source == null) { + return; + } + for (GenericStack output : source) { + if (output != null && output.what() != null && output.amount() > 0L) { + target.add(output); + } + } + } + + static boolean matches(AdvancedAlloyFurnaceRecipe recipe, + @Nullable Map mergedInputs, + @Nullable Map mergedFluids, + @Nullable Map mergedKeys) { + if (recipe == null) { + return false; + } + Map requiredItems = new LinkedHashMap<>(); + for (CountedIngredient input : recipe.inputs()) { + if (input != null && input.ingredient() != null && input.count() > 0L) { + requiredItems.merge(input.ingredient(), input.count(), UfoRecipeAdapterSupport::saturatingAdd); + } + } + if (!AdapterUtils.matchesRequired(mergedInputs == null ? Map.of() : mergedInputs, requiredItems)) { + return false; + } + if (!FluidIngredientAllocator.matchesLong(recipe.inputFluids(), + mergedFluids == null ? Map.of() : mergedFluids, 1L)) { + return false; + } + + Map requiredKeys = new LinkedHashMap<>(); + for (GenericStack input : recipe.keyInputs()) { + if (input != null && input.what() != null && input.amount() > 0L) { + requiredKeys.merge(input.what(), input.amount(), UfoRecipeAdapterSupport::saturatingAdd); + } + } + Map availableKeys = mergedKeys == null ? Map.of() : mergedKeys; + for (Map.Entry required : requiredKeys.entrySet()) { + if (availableKeys.getOrDefault(required.getKey(), 0L) < required.getValue()) { + return false; + } + } + return true; + } + + private static long saturatingAdd(long left, long right) { + return right > 0L && left > Long.MAX_VALUE - right ? Long.MAX_VALUE : left + right; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ufo/UniversalMultiblockRecipeAdapter.java b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ufo/UniversalMultiblockRecipeAdapter.java new file mode 100644 index 00000000..034934e1 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/recipe/adapters/ufo/UniversalMultiblockRecipeAdapter.java @@ -0,0 +1,103 @@ +package com.sorrowmist.useless.content.recipe.adapters.ufo; + +import com.raishxn.ufo.block.MultiblockBlocks; +import com.raishxn.ufo.init.ModRecipes; +import com.raishxn.ufo.recipe.UniversalMultiblockMachineKind; +import com.raishxn.ufo.recipe.UniversalMultiblockRecipe; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.content.recipe.AdapterUtils; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import com.sorrowmist.useless.content.recipe.CountedIngredient; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** Converts one machine kind from UFO's universal multiblock recipe type. */ +public final class UniversalMultiblockRecipeAdapter + extends UfoRecipeAdapter { + private final UniversalMultiblockMachineKind machine; + + public UniversalMultiblockRecipeAdapter(UniversalMultiblockMachineKind machine) { + this.machine = machine; + } + + @Override + public Class getRecipeClass() { + return UniversalMultiblockRecipe.class; + } + + @Override + protected RecipeType recipeType() { + return ModRecipes.UNIVERSAL_MULTIBLOCK_TYPE.get(); + } + + @Override + protected boolean accepts(UniversalMultiblockRecipe recipe) { + return recipe != null && recipe.getMachine() == this.machine; + } + + @Override + @Nullable + public ItemStack getMoldItem() { + return switch (this.machine) { + case QMF -> new ItemStack(MultiblockBlocks.QUANTUM_MATTER_FABRICATOR_CONTROLLER.get()); + case QUANTUM_CRYOFORGE -> new ItemStack(MultiblockBlocks.QUANTUM_CRYOFORGE_CONTROLLER.get()); + default -> null; + }; + } + + @Override + public List convertAll( + RecipeHolder holder, Level level) { + if (holder == null || holder.value() == null || !accepts(holder.value())) { + return List.of(); + } + UniversalMultiblockRecipe source = holder.value(); + if (source.getTime() <= 0) { + return List.of(); + } + + List items = UfoRecipeAdapterSupport.itemInputs( + source.getItemInputs(), UniversalMultiblockRecipe.ItemRequirement::ingredient, + UniversalMultiblockRecipe.ItemRequirement::amount); + List fluids = + UfoRecipeAdapterSupport.concreteFluidInputs( + source.getFluidInputs(), UniversalMultiblockRecipe.FluidRequirement::fluid, + UniversalMultiblockRecipe.FluidRequirement::amount); + if (fluids == null) { + return List.of(); + } + List chemicals = UfoRecipeAdapterSupport.chemicalInputs( + source.getChemicalInputs(), UniversalMultiblockRecipe.ChemicalRequirement::chemicalId, + UniversalMultiblockRecipe.ChemicalRequirement::amount); + if (chemicals == null) { + return List.of(); + } + + List outputs = new ArrayList<>(); + List outputFluids = new ArrayList<>(); + List keyOutputs = new ArrayList<>(); + UfoRecipeAdapterSupport.addItemOutput(outputs, keyOutputs, source.getItemOutput(), + source.getItemOutputAmount()); + UfoRecipeAdapterSupport.addFluidOutput(outputFluids, keyOutputs, source.getFluidOutput(), + source.getFluidOutputAmount()); + if ((items.isEmpty() && fluids.isEmpty() && chemicals.isEmpty()) + || (outputs.isEmpty() && outputFluids.isEmpty() && keyOutputs.isEmpty())) { + return List.of(); + } + + return List.of(new AdvancedAlloyFurnaceRecipe( + AdapterUtils.convertedId(holder.id()), items, fluids, chemicals, + outputs, outputFluids, keyOutputs, + UfoRecipeAdapterSupport.energy(source.getEnergy()), source.getTime(), + Ingredient.EMPTY, 0, AdapterUtils.toMoldIngredient(getMoldItem()), + AlloyFurnaceMode.NORMAL)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/stafflink/LinkFilterCondition.java b/src/main/java/com/sorrowmist/useless/content/stafflink/LinkFilterCondition.java new file mode 100644 index 00000000..1c1a411b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/stafflink/LinkFilterCondition.java @@ -0,0 +1,106 @@ +package com.sorrowmist.useless.content.stafflink; + +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.fluids.FluidStack; + +/** + * 过滤格里的一条「控制条件」。 + * + *

它把原来那两个纯数字(源端保留 / 接收端上限)推广成一句完整的话:

+ * + *
当 [某个容器] 里的 [控制材料 B] [≥ / ≤] [N] 时,才允许搬运。
+ * + *
    + *
  • {@link #op()}:比较方向,{@link Op#OFF} 表示这条条件不启用(默认)。
  • + *
  • {@link #item()}/{@link #fluid()}:控制材料 B。两者互斥,流体优先。 + * 都为空 = B 就是被搬运的那种材料 A 自身(即改动前的行为)。
  • + *
  • {@link #value()}:阈值 N,非负。
  • + *
+ * + *

测量在哪一侧由它挂在 {@link LinkFilterSlot#outCond()} 还是 + * {@link LinkFilterSlot#inCond()} 上决定:前者在输出端(源容器)上量,后者在 + * 输入端(目标容器)上量。

+ * + *

上限折算(由引擎负责,见 {@code StaffLinkTargets}):只有当 B 就是 A 自身、 + * 且方向是「输出端 ≥ N」或「输入端 ≤ N」时,这条条件除了门控之外还顺带限定搬运量 + * (等价于旧的「保留 / 上限」)。B 是别的材料时它只是纯门控。

+ * + *

为什么不支持模式({@code #tag} / 通配符):控制材料要按类型去查容器存量 + * ({@code amountOf}),模式匹配的是一整类,没有单一模板可查;而且量一整类也没有明确的语义。

+ */ +public record LinkFilterCondition(Op op, ItemStack item, FluidStack fluid, long value) { + + /** 比较方向。 */ + public enum Op { + /** 不启用这条条件。 */ + OFF, + /** 容器里该材料 ≥ N 才允许搬运。 */ + AT_LEAST, + /** 容器里该材料 ≤ N 才允许搬运。 */ + AT_MOST + } + + /** 不启用的条件。 */ + public static final LinkFilterCondition OFF = + new LinkFilterCondition(Op.OFF, ItemStack.EMPTY, FluidStack.EMPTY, 0L); + + public LinkFilterCondition { + op = op == null ? Op.OFF : op; + item = item == null || item.isEmpty() ? ItemStack.EMPTY : item.copyWithCount(1); + fluid = fluid == null || fluid.isEmpty() ? FluidStack.EMPTY : new net.minecraftforge.fluids.FluidStack(fluid, 1); + // 控制材料二选一,流体优先(与 LinkFilterSlot 的标记同一套收敛规则)。 + if (!fluid.isEmpty()) { + item = ItemStack.EMPTY; + } + value = Math.max(0L, value); + // 不启用时不留无意义的控制材料与数值,让 equals / 存档 / 同步都干净。 + if (op == Op.OFF) { + item = ItemStack.EMPTY; + fluid = FluidStack.EMPTY; + value = 0L; + } + } + + /** 造一条「测 A 自身」的条件(B 留空)。 */ + public static LinkFilterCondition self(Op op, long value) { + return new LinkFilterCondition(op, ItemStack.EMPTY, FluidStack.EMPTY, value); + } + + public boolean isOff() { + return op == Op.OFF; + } + + /** 控制材料留空 = 测被搬运的那种材料 A 自身。 */ + public boolean isSelf() { + return item.isEmpty() && fluid.isEmpty(); + } + + public boolean isItemControl() { + return !item.isEmpty(); + } + + public boolean isFluidControl() { + return !fluid.isEmpty(); + } + + public LinkFilterCondition withOp(Op newOp) { + return new LinkFilterCondition(newOp, item, fluid, value); + } + + public LinkFilterCondition withValue(long newValue) { + return new LinkFilterCondition(op, item, fluid, newValue); + } + + public LinkFilterCondition withControl(ItemStack control) { + return new LinkFilterCondition(op, control, FluidStack.EMPTY, value); + } + + public LinkFilterCondition withControl(FluidStack control) { + return new LinkFilterCondition(op, ItemStack.EMPTY, control, value); + } + + /** 清掉控制材料(回到「测 A 自身」),保留方向与数值。 */ + public LinkFilterCondition withoutControl() { + return new LinkFilterCondition(op, ItemStack.EMPTY, FluidStack.EMPTY, value); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/stafflink/LinkFilterPattern.java b/src/main/java/com/sorrowmist/useless/content/stafflink/LinkFilterPattern.java new file mode 100644 index 00000000..7e1be947 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/stafflink/LinkFilterPattern.java @@ -0,0 +1,307 @@ +package com.sorrowmist.useless.content.stafflink; + +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.material.Fluid; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * 过滤器里的一格「模式」标记:{@code #tag} 或含通配符的字面量。 + * + *

为什么需要它。 具体标记(一个 {@link ItemStack} / {@link FluidStack})只能表达 + * 「就是这一种」。可玩家常常想说的是「所有锭」「所有带 {@code iron} 字样的东西」——那要靠 + * 标签或通配符。这一格因此有三种形态:物品、流体、模式。

+ * + *

两种模式

+ * + *
    + *
  • {@link Kind#TAG}:{@code #namespace:path},按标签匹配。用的是静态注册表的 + * 标签查询({@code BuiltInRegistries.ITEM.getTagOrEmpty}),因此不需要 + * {@code HolderLookup.Provider},也就能在服务端搬运路径里直接用。
  • + *
  • {@link Kind#GLOB}:含 {@code *} 或 {@code ?} 的字面量,按注册表 id 与显示名 + * 两边都试。{@code *} 匹配任意长、{@code ?} 匹配恰好一个字符。
  • + *
+ * + *

为什么「纯字面量」会被拒绝

+ * + *

{@code minecraft:iron_ingot} 这种既没 {@code #} 也没通配符的输入解析结果为 + * {@code null}(调用方当作非法输入退回)。理由:一个不带组件的裸 id 无法表达 + * 「铁锭(带某某附魔)」,它到底匹不匹配永远说不清;接受了它只会让玩家以为「打上去了」, + * 而实际永远匹配不上。想精确指定就用具体标记(拿物品点格子 / 从 JEI 拖入)。

+ * + *

未知标签 = 匹配不上任何东西

+ * + *

刻意不把「标签没注册」当成「不限制」。若当不限制,玩家把 {@code #c:ingots} 打错 + * 一个字,整张网络就会全灌进目标容器——破坏性后果不可逆。宁可「什么都不搬」(界面上能看见、 + * 玩家能自己改),也不要「全搬」。

+ * + *

不可变

+ * + *

解析即归一化,之后不再改动;{@link #equals}/{@link #hashCode} 由 record 语义给出, + * 因此可以安全地放进 {@link LinkFilterSlot} 并参与 NBT / 网络编解码的比较。

+ */ +public record LinkFilterPattern(Kind kind, String text, @Nullable TagKey itemTag, + @Nullable TagKey fluidTag) { + + /** 输入的形态。 */ + public enum Kind { + /** {@code #namespace:path},按标签匹配。 */ + TAG, + /** 含通配符的字面量,按 id / 显示名匹配。 */ + GLOB + } + + /** 模式原文长度上限;超出直接判非法(存档被改坏时也不必去匹配一个巨型字符串)。 */ + public static final int MAX_LENGTH = 128; + + /** 通配符:匹配任意长(含空)。 */ + private static final char WILDCARD_ANY = '*'; + /** 通配符:匹配恰好一个字符。 */ + private static final char WILDCARD_ONE = '?'; + + public LinkFilterPattern { + kind = kind == null ? Kind.GLOB : kind; + text = text == null ? "" : text; + } + + // ------------------------------------------------------------------ 解析 + + /** + * 把玩家的输入解析成一个模式;非法输入返回 {@code null}。 + * + *

判定顺序:

+ *
    + *
  1. 空白 / 超长 → {@code null}
  2. + *
  3. 以 {@code #} 开头 → 按标签解析(id 非法同样返回 {@code null})
  4. + *
  5. 含 {@code *} 或 {@code ?} → {@link Kind#GLOB}
  6. + *
  7. 其余 → {@code null}(见类注释「为什么纯字面量会被拒绝」)
  8. + *
+ */ + @Nullable + public static LinkFilterPattern parse(@Nullable String raw) { + String trimmed = raw == null ? "" : raw.trim(); + if (trimmed.isEmpty() || trimmed.length() > MAX_LENGTH) { + return null; + } + if (trimmed.charAt(0) == '#') { + return parseTag(trimmed); + } + if (trimmed.indexOf(WILDCARD_ANY) < 0 && trimmed.indexOf(WILDCARD_ONE) < 0) { + // 没有 # 也没有通配符:一个裸 id 表达不了组件,接受它只会误导玩家。 + return null; + } + return new LinkFilterPattern(Kind.GLOB, trimmed, null, null); + } + + @Nullable + private static LinkFilterPattern parseTag(String raw) { + String body = raw.substring(1).trim(); + if (body.isEmpty() || body.length() > MAX_LENGTH) { + return null; + } + ResourceLocation id = parseId(body); + if (id == null) { + return null; + } + // 物品与流体两个标签都挂上:输入时并不知道玩家这条线路搬的是什么,匹配时按需取用。 + // 两个 TagKey 由同一个 id 派生,因此「同一份文本 ⇒ 同一个 record」成立。 + return new LinkFilterPattern(Kind.TAG, raw, + TagKey.create(Registries.ITEM, id), TagKey.create(Registries.FLUID, id)); + } + + /** + * 宽松解析一个资源 id:没写 namespace 时补 {@code minecraft}。 + * + *

{@code ResourceLocation.tryParse} 本身要求写全 {@code namespace:path},而玩家输入 + * {@code #c:ingots} 时那条 {@code c} 路径也是常见的(通用标签约定),所以两条都试一遍。

+ */ + @Nullable + private static ResourceLocation parseId(String body) { + ResourceLocation withNamespace = ResourceLocation.tryParse(body); + if (withNamespace != null) { + return withNamespace; + } + return ResourceLocation.tryParse("minecraft:" + body); + } + + // ------------------------------------------------------------------ 匹配 + + /** + * 是否匹配这个物品。 + * + *

{@link Kind#TAG} 走静态注册表标签查询;{@link Kind#GLOB} 先比 id,id 不中才构造显示名 + * 再比一次(显示名要新建 {@code Component},能短路就短路)。

+ * + *

{@code tag} 不存在时 {@link #matchesTag} 返回 {@code false}(见类注释)。

+ */ + public boolean matchesItem(ItemStack stack) { + if (stack == null || stack.isEmpty()) { + return false; + } + if (kind == Kind.TAG) { + return matchesTag(BuiltInRegistries.ITEM, itemTag, stack.getItem()); + } + ResourceLocation id = BuiltInRegistries.ITEM.getKey(stack.getItem()); + if (matchesGlob(id == null ? "" : id.toString())) { + return true; + } + // id 未命中才付显示名的代价(语言相关,但这是玩家自己选的匹配方式)。 + return matchesGlob(stack.getHoverName().getString()); + } + + /** 是否匹配这种流体;语义与 {@link #matchesItem(ItemStack)} 完全对称。 */ + public boolean matchesFluid(FluidStack stack) { + if (stack == null || stack.isEmpty()) { + return false; + } + if (kind == Kind.TAG) { + return matchesTag(BuiltInRegistries.FLUID, fluidTag, stack.getFluid()); + } + ResourceLocation id = BuiltInRegistries.FLUID.getKey(stack.getFluid()); + if (matchesGlob(id == null ? "" : id.toString())) { + return true; + } + return matchesGlob(stack.getDisplayName().getString()); + } + + /** + * 是否匹配某个化学品。 + * + *

只支持 {@link Kind#GLOB},且只按 id。 化学品没有稳定的标签体系(Mekanism 的 + * 化学品注册表挂在自己的注册表上,跨模组没有通用 TagKey),所以 {@code #} 形式对化学品 + * 永远不成立;{@code typeKey()} 又是个不透明的对象,界面上也拿不到显示名,因此只比 id。

+ * + *

{@code chemicalId} 为 {@code null} 表示这一端的化学品集成给不出 id + * (见 {@code ChemicalCompatProvider#chemicalIdOf} 的默认实现);此时 {@link Kind#TAG} + * 恒 {@code false},{@link Kind#GLOB} 也恒 {@code false}——宁可「匹配不上」(可观察、 + * 可修正),也不要在拿不到 id 时放行一切。

+ */ + public boolean matchesChemicalId(@Nullable ResourceLocation chemicalId) { + if (kind == Kind.TAG || chemicalId == null) { + return false; + } + return matchesGlob(chemicalId.toString()); + } + + /** + * 这个模式在当前环境里是否「可能匹配到东西」。 + * + *

界面用它把打错的标签标红:{@link Kind#TAG} 查一下标签注册表里有没有这一条; + * {@link Kind#GLOB} 恒为真(通配符能不能匹配到要看具体资源,静态判不了)。

+ */ + public boolean isResolvable() { + if (kind == Kind.GLOB) { + return true; + } + if (itemTag != null && BuiltInRegistries.ITEM.getTag(itemTag).isPresent()) { + return true; + } + return fluidTag != null && BuiltInRegistries.FLUID.getTag(fluidTag).isPresent(); + } + + /** 这一格是 {@code #tag} 形式。*/ + public boolean isTag() { + return kind == Kind.TAG; + } + + /** 界面上显示的字样({@code #tag} 原样,glob 原样)。 */ + public String displayName() { + return text; + } + + // ------------------------------------------------------------------ 内部 + + private static boolean matchesTag(net.minecraft.core.Registry registry, + @Nullable TagKey tag, T value) { + if (tag == null || value == null) { + return false; + } + // 遍历标签内容而不是 wrapAsHolder(...).is(tag):后者要先把对象包成 Holder 再查, + // 这里已经在注册表上,直接取条目列表更直白,且对「标签不存在」天然返回空。 + for (var holder : registry.getTagOrEmpty(tag)) { + if (holder.value() == value) { + return true; + } + } + return false; + } + + /** + * 通配符匹配:{@code *} 任意长、{@code ?} 恰好一个。 + * + *

手写两指针回溯(不用正则):一是不必在搬运热路径上编译 {@code Pattern},二是这里的 + * 语义只有两个通配符,正则那套转义反而容易出错。回溯版本最坏 O(n·m),但输入上限 128、 + * 被匹配的 id 通常只有几十字符,代价可忽略。

+ */ + private boolean matchesGlob(String candidate) { + return globMatches(text, candidate); + } + + /** + * {@code *}/{@code ?} 通配符匹配。包级可见仅为方便测试,实现细节不属于公开契约。 + * + * @param pattern 模式(含通配符) + * @param candidate 被匹配的字符串 + */ + static boolean globMatches(String pattern, String candidate) { + int p = 0; + int c = 0; + int starP = -1; + int starC = 0; + int patternLength = pattern.length(); + int candidateLength = candidate.length(); + while (c < candidateLength) { + if (p < patternLength + && (pattern.charAt(p) == WILDCARD_ONE || pattern.charAt(p) == candidate.charAt(c))) { + p++; + c++; + } else if (p < patternLength && pattern.charAt(p) == WILDCARD_ANY) { + // 记下「从这里开始可以吞任意长」,先试着不吞。 + starP = p; + starC = c; + p++; + } else if (starP >= 0) { + // 回退:上一个 * 多吞一个字符,模式指针回到 * 之后。 + starC++; + c = starC; + p = starP + 1; + } else { + return false; + } + } + // 候选串走完了,模式剩下的必须全是 *。 + while (p < patternLength && pattern.charAt(p) == WILDCARD_ANY) { + p++; + } + return p == patternLength; + } + + /** 供界面列举「这一格大概匹配什么」用:把所有候选 id 里命中的挑出来(上限 {@code max})。 */ + public List sampleItemIds(int max) { + List result = new ArrayList<>(); + if (max <= 0) { + return result; + } + for (Item item : BuiltInRegistries.ITEM) { + ResourceLocation id = BuiltInRegistries.ITEM.getKey(item); + if (id == null) { + continue; + } + if (kind == Kind.TAG ? matchesTag(BuiltInRegistries.ITEM, itemTag, item) : matchesGlob(id.toString())) { + result.add(id); + if (result.size() >= max) { + break; + } + } + } + return result; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/stafflink/LinkFilterSlot.java b/src/main/java/com/sorrowmist/useless/content/stafflink/LinkFilterSlot.java new file mode 100644 index 00000000..09fd322f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/stafflink/LinkFilterSlot.java @@ -0,0 +1,184 @@ +package com.sorrowmist.useless.content.stafflink; + +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +/** + * 过滤器里的一格。 + * + *

一格只装一种标记资源 A,有三种形态:

+ * + *
    + *
  • 物品:装物品本身(化学品线路装的是化学品集成自己的「储罐物品」表示)。
  • + *
  • 流体:装流体本身(不是装它的桶)——没有桶的流体也能标记上了。
  • + *
  • 模式:{@link LinkFilterPattern},即 {@code #tag} 或含通配符的字面量, + * 能一次覆盖一整类资源。
  • + *
+ * + *

早先这一格统一是 {@link ItemStack},流体得先换成「装它的桶」才能存进去,于是 + * 「流体过滤」实际是在比物品:没有桶的流体根本标记不上,把流体拖到物品线路上还会变成 + * 一个水桶物品。现在流体以 {@link FluidStack} 直接存,与物品彻底分开。

+ * + *

三种附加信息

+ * + *
    + *
  • {@link #exclude()}:这一格是「黑名单」而不是「白名单」。标记命中的资源不搬, + * 其余照搬。同一格上「白名单」与「黑名单」互斥——它就是这一格的方向。
  • + *
  • {@link #outCond()}:输出端条件,在源容器上测量(旧「源端保留」的推广)。
  • + *
  • {@link #inCond()}:输入端条件,在目标容器上测量(旧「接收端上限」的推广)。
  • + *
+ * + *

两条条件同时成立才允许搬运这一格标记的资源;任一为 {@link LinkFilterCondition#OFF} + * 即视为不限制。控制材料留空时条件测的就是被搬的资源 A 自身,行为与改动前完全一致。

+ * + *

空槽不变式:标记为空 ⇒ {@code exclude=false} 且两条条件都是 {@code OFF}, + * 让「空槽」与「带信息的空槽」不可能共存,也让 {@code equals} 干净。

+ */ +public record LinkFilterSlot(ItemStack item, FluidStack fluid, + @Nullable String pattern, + boolean exclude, + LinkFilterCondition outCond, + LinkFilterCondition inCond) { + public static final LinkFilterSlot EMPTY = new LinkFilterSlot( + ItemStack.EMPTY, FluidStack.EMPTY, null, false, + LinkFilterCondition.OFF, LinkFilterCondition.OFF); + + public LinkFilterSlot { + item = item == null || item.isEmpty() ? ItemStack.EMPTY : item.copyWithCount(1); + fluid = fluid == null || fluid.isEmpty() ? FluidStack.EMPTY : new net.minecraftforge.fluids.FluidStack(fluid, 1); + pattern = pattern == null || pattern.isBlank() ? null : pattern.trim(); + + // 三态互斥,优先级:模式 > 流体 > 物品。调用方本来也不该一次给多个, + // 但存档被改坏 / 网络包构造异常时这里必须收敛到一个确定状态。 + if (pattern != null) { + fluid = FluidStack.EMPTY; + item = ItemStack.EMPTY; + } else if (!fluid.isEmpty()) { + item = ItemStack.EMPTY; + } + + outCond = outCond == null ? LinkFilterCondition.OFF : outCond; + inCond = inCond == null ? LinkFilterCondition.OFF : inCond; + + // 空槽不保留任何无意义的附加信息:让「空槽」与「带信息的空槽」不可能共存, + // 也让 equals 干净。 + if (pattern == null && fluid.isEmpty() && item.isEmpty()) { + exclude = false; + outCond = LinkFilterCondition.OFF; + inCond = LinkFilterCondition.OFF; + } + } + + public static LinkFilterSlot ofItem(ItemStack stack) { + return stack == null || stack.isEmpty() + ? EMPTY + : new LinkFilterSlot(stack, FluidStack.EMPTY, null, false, + LinkFilterCondition.OFF, LinkFilterCondition.OFF); + } + + public static LinkFilterSlot ofFluid(FluidStack stack) { + return stack == null || stack.isEmpty() + ? EMPTY + : new LinkFilterSlot(ItemStack.EMPTY, stack, null, false, + LinkFilterCondition.OFF, LinkFilterCondition.OFF); + } + + /** + * 用一段模式文本造一格;文本非法(见 {@link LinkFilterPattern#parse})时返回 + * {@link #EMPTY}——调用方若想区分「非法输入」应当自己先 {@code parse} 一次。 + */ + public static LinkFilterSlot ofPattern(String raw) { + LinkFilterPattern parsed = LinkFilterPattern.parse(raw); + return parsed == null + ? EMPTY + : new LinkFilterSlot(ItemStack.EMPTY, FluidStack.EMPTY, parsed.text(), false, + LinkFilterCondition.OFF, LinkFilterCondition.OFF); + } + + /** 用已解析好的模式造一格。 */ + public static LinkFilterSlot ofPattern(LinkFilterPattern parsed) { + return parsed == null + ? EMPTY + : new LinkFilterSlot(ItemStack.EMPTY, FluidStack.EMPTY, parsed.text(), false, + LinkFilterCondition.OFF, LinkFilterCondition.OFF); + } + + public boolean isEmpty() { + return item.isEmpty() && fluid.isEmpty() && pattern == null; + } + + /** 这一格装的是流体(否则是物品或模式)。 */ + public boolean isFluid() { + return pattern == null && !fluid.isEmpty(); + } + + /** 这一格装的是物品。 */ + public boolean isItem() { + return pattern == null && !item.isEmpty(); + } + + /** 这一格是 {@code #tag} / 通配符模式。 */ + public boolean isPattern() { + return pattern != null; + } + + /** 这一格是「黑名单」而不是「白名单」。 */ + public boolean isExcluded() { + return exclude; + } + + /** 这一格有没有启用任何一条控制条件(给界面角标用)。 */ + public boolean hasConditions() { + return !outCond.isOff() || !inCond.isOff(); + } + + /** 换掉「白名单 / 黑名单」,其余不变。 */ + public LinkFilterSlot withExclude(boolean newExclude) { + return new LinkFilterSlot(item, fluid, pattern, newExclude, outCond, inCond); + } + + /** 换掉两条控制条件,其余不变。 */ + public LinkFilterSlot withConditions(LinkFilterCondition newOut, LinkFilterCondition newIn) { + return new LinkFilterSlot(item, fluid, pattern, exclude, newOut, newIn); + } + + /** + * 换掉标记 A,保留白名单/黑名单与两条条件。 + * + *

标记换成空时构造器会把附加信息一起清掉(空槽不变式)。

+ */ + public LinkFilterSlot withMarker(LinkFilterSlot marker) { + if (marker == null) { + return EMPTY; + } + return new LinkFilterSlot(marker.item(), marker.fluid(), marker.pattern(), + exclude, outCond, inCond); + } + + /** + * 兼容旧语义的便捷映射:{@code keep} → 输出端 {@code ≥ self}, + * {@code max} → 输入端 {@code ≤ self};{@code 0} 表示该侧条件关闭。 + */ + public LinkFilterSlot withLimits(long keepAtSource, long maxInto) { + LinkFilterCondition out = keepAtSource > 0L + ? LinkFilterCondition.self(LinkFilterCondition.Op.AT_LEAST, keepAtSource) + : LinkFilterCondition.OFF; + LinkFilterCondition in = maxInto > 0L + ? LinkFilterCondition.self(LinkFilterCondition.Op.AT_MOST, maxInto) + : LinkFilterCondition.OFF; + return new LinkFilterSlot(item, fluid, pattern, exclude, out, in); + } + + /** 旧「源端保留」值(= 输出端 ≥ self 的阈值);没有这类条件时返回 0。 */ + public long keepAtSource() { + return outCond.op() == LinkFilterCondition.Op.AT_LEAST && outCond.isSelf() + ? outCond.value() : 0L; + } + + /** 旧「接收端上限」值(= 输入端 ≤ self 的阈值);没有这类条件时返回 0。 */ + public long maxInto() { + return inCond.op() == LinkFilterCondition.Op.AT_MOST && inCond.isSelf() + ? inCond.value() : 0L; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/stafflink/LinkFlow.java b/src/main/java/com/sorrowmist/useless/content/stafflink/LinkFlow.java new file mode 100644 index 00000000..f3c8ed36 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/stafflink/LinkFlow.java @@ -0,0 +1,30 @@ +package com.sorrowmist.useless.content.stafflink; + +import net.minecraft.network.chat.Component; + +/** + * 一条线路的流向。 + * + *

{@link #RELEASE} 端负责把资源推出去,{@link #ABSORB} 端负责接住。 + * 引擎只在「同一张网络 + 同一线路号 + RELEASE → ABSORB + 同一资源类型」之间搬运。

+ */ +public enum LinkFlow { + /** 吸收:接收同线路 RELEASE 端推来的资源。 */ + ABSORB("gui.useless_mod.wireless_logistics.flow.absorb"), + /** 释放:把自身库存推给同线路的 ABSORB 端。 */ + RELEASE("gui.useless_mod.wireless_logistics.flow.release"); + + private final String translationKey; + + LinkFlow(String translationKey) { + this.translationKey = translationKey; + } + + public Component displayName() { + return Component.translatable(translationKey); + } + + public LinkFlow next() { + return this == ABSORB ? RELEASE : ABSORB; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/stafflink/LinkMedium.java b/src/main/java/com/sorrowmist/useless/content/stafflink/LinkMedium.java new file mode 100644 index 00000000..adb56909 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/stafflink/LinkMedium.java @@ -0,0 +1,151 @@ +package com.sorrowmist.useless.content.stafflink; + +import net.minecraft.network.chat.Component; +import net.minecraftforge.fml.ModList; + +import java.util.ArrayList; +import java.util.List; + +/** + * 一条线路搬运的资源类型。 + * + *

带 {@code AE_} 前缀的项表示这一端直接接在 AE 网络上:不解析方块自身的容器能力, + * 而是从网格的 ME 存储里取/存。它们与对应的方块版本属于同一个 {@link ResourceFamily}, + * 因此「AE 网络 ↔ 箱子」可以正常配对。

+ * + *

每种资源能否走 AE 取决于「谁把这种资源带进了 ME 网络」:物品与流体是 AE2 自带, + * 化学品需要 Applied Mekanistics,魔源需要 Ars Énergistique,能量需要 AppliedFlux + * (FE 以通量元件的形式存进网络)。因此 {@code AE_CHEMICAL} 的可用条件比 {@link #CHEMICAL} + * 更严(还要 AE2 与 appmek)。

+ */ +public enum LinkMedium { + ITEM("gui.useless_mod.wireless_logistics.medium.item", ResourceFamily.ITEM), + AE_ITEM("gui.useless_mod.wireless_logistics.medium.ae_item", ResourceFamily.ITEM), + FLUID("gui.useless_mod.wireless_logistics.medium.fluid", ResourceFamily.FLUID), + AE_FLUID("gui.useless_mod.wireless_logistics.medium.ae_fluid", ResourceFamily.FLUID), + ENERGY("gui.useless_mod.wireless_logistics.medium.energy", ResourceFamily.ENERGY), + CHEMICAL("gui.useless_mod.wireless_logistics.medium.chemical", ResourceFamily.CHEMICAL), + AE_CHEMICAL("gui.useless_mod.wireless_logistics.medium.ae_chemical", ResourceFamily.CHEMICAL), + SOURCE("gui.useless_mod.wireless_logistics.medium.source", ResourceFamily.SOURCE), + AE_SOURCE("gui.useless_mod.wireless_logistics.medium.ae_source", ResourceFamily.SOURCE), + /** + * 接在 ME 网络上的 FE(AppliedFlux 通量元件)。 + * + *

必须追加在末尾:{@code StaffLinkRoute.STREAM_CODEC} 用 {@code writeEnum} 按 + * ordinal 编码,往中间插值会让新旧版本之间把「能量」读成别的资源。

+ */ + AE_ENERGY("gui.useless_mod.wireless_logistics.medium.ae_energy", ResourceFamily.ENERGY), + /** + * 机械动力的转速与应力容量。 + * + *

与能量、魔源一样,是「源头出来只有一种」的连续量,因此同样不参与过滤 + * ({@link StaffLinkRoute#filterApplies()} 恒 {@code false},界面不给填标记)。

+ * + *

同样必须追加在末尾,理由见 {@link #AE_ENERGY}。

+ */ + STRESS("gui.useless_mod.wireless_logistics.medium.stress", ResourceFamily.STRESS), + /** + * 气动工艺的气压(空气)。 + * + *

与能量、魔源、应力一样是「源头出来只有一种」的连续量,因此同样不参与过滤 + * ({@link StaffLinkRoute#filterApplies()} 恒 {@code false},界面不给填标记)。

+ * + *

它是状态量:搬运的目标是让接收端达到某个气压,而不是每轮固定搬一批, + * 因此走 {@link StaffLinkEngine} 的专用分支,不经过逐对 {@code transfer}。

+ * + *

同样必须追加在末尾,理由见 {@link #AE_ENERGY}。

+ */ + PRESSURE("gui.useless_mod.wireless_logistics.medium.pressure", ResourceFamily.PRESSURE), + /** + * 存进 ME 网络的空气(Applied Pneumatics 的 {@code AirKey})。 + * + *

网络里只有一堆空气、没有体积,因此没有压力概念;目标气压对它无意义, + * 它只作为「气源 / 气库」参与搬运。

+ * + *

同样必须追加在末尾,理由见 {@link #AE_ENERGY}。

+ */ + AE_PRESSURE("gui.useless_mod.wireless_logistics.medium.ae_pressure", ResourceFamily.PRESSURE); + + private static final String MEKANISM = "mekanism"; + private static final String ARS_NOUVEAU = "ars_nouveau"; + private static final String AE2 = "ae2"; + private static final String APPLIED_MEKANISTICS = "appmek"; + private static final String ARS_ENERGISTIQUE = "arseng"; + private static final String APPLIED_FLUX = "appflux"; + private static final String CREATE = "create"; + private static final String PNEUMATICRAFT = "pneumaticcraft"; + private static final String APPLIED_PNEUMATICS = "appliedpneumatics"; + + private final String translationKey; + private final ResourceFamily family; + + LinkMedium(String translationKey, ResourceFamily family) { + this.translationKey = translationKey; + this.family = family; + } + + public Component displayName() { + return Component.translatable(translationKey); + } + + /** 搬运语义上的粗分类;配对时比较它,见 {@link ResourceFamily}。 */ + public ResourceFamily family() { + return family; + } + + /** + * 这一端是否走 AE 网络。 + * + *

走网络的一端有一处特殊规则:从 AE 取出时只认接收端的白名单。AE 网络里动辄上千种 + * 物品,让玩家在源端(网络侧)列白名单既繁琐又容易漏;「要从网里拿什么」由接收端说了算更自然。

+ */ + public boolean isAe() { + return this == AE_ITEM || this == AE_FLUID || this == AE_CHEMICAL || this == AE_SOURCE + || this == AE_ENERGY || this == AE_PRESSURE; + } + + /** 当前环境是否支持这种资源。可选集成缺失时界面跳过、服务端拒绝。 */ + public boolean isSupported() { + return switch (this) { + case CHEMICAL -> ModList.get().isLoaded(MEKANISM); + case SOURCE -> ModList.get().isLoaded(ARS_NOUVEAU); + case AE_ITEM, AE_FLUID -> ModList.get().isLoaded(AE2); + // 化学品/魔源/能量要进 ME 网络,除了 AE2 还得有把它们带进网络的那个附属。 + case AE_CHEMICAL -> ModList.get().isLoaded(AE2) && ModList.get().isLoaded(APPLIED_MEKANISTICS); + case AE_SOURCE -> ModList.get().isLoaded(AE2) && ModList.get().isLoaded(ARS_ENERGISTIQUE); + case AE_ENERGY -> ModList.get().isLoaded(AE2) && ModList.get().isLoaded(APPLIED_FLUX); + case STRESS -> ModList.get().isLoaded(CREATE); + case PRESSURE -> ModList.get().isLoaded(PNEUMATICRAFT); + // 空气要进 ME 网络得靠 Applied Pneumatics,只有 AE2 是不够的。 + case AE_PRESSURE -> ModList.get().isLoaded(AE2) && ModList.get().isLoaded(APPLIED_PNEUMATICS); + default -> true; + }; + } + + /** + * 下拉菜单里的排列次序:同一个资源族的普通形态与 AE 形态相邻。 + * + *

刻意不直接用 {@code values()}:{@link #AE_ENERGY} 是为了保持网络编解码的 ordinal + * 稳定才追加在末尾的,直接枚举会把它排到「AE 魔源」后面,读起来莫名其妙。

+ */ + private static final LinkMedium[] MENU_ORDER = { + ITEM, AE_ITEM, FLUID, AE_FLUID, ENERGY, AE_ENERGY, PRESSURE, AE_PRESSURE, + CHEMICAL, AE_CHEMICAL, SOURCE, AE_SOURCE, STRESS + }; + + /** + * 当前环境支持的类型,按下拉菜单的次序。 + * + *

界面拿它铺下拉列表:装了化学品 / 魔源 / 通量之后类型能到十种,靠「点一下换一个」 + * 轮换太费手。

+ */ + public static List supported() { + List result = new ArrayList<>(MENU_ORDER.length); + for (LinkMedium medium : MENU_ORDER) { + if (medium.isSupported()) { + result.add(medium); + } + } + return List.copyOf(result); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/stafflink/LongResourceAdapters.java b/src/main/java/com/sorrowmist/useless/content/stafflink/LongResourceAdapters.java new file mode 100644 index 00000000..6047607f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/stafflink/LongResourceAdapters.java @@ -0,0 +1,441 @@ +package com.sorrowmist.useless.content.stafflink; + +import com.sorrowmist.useless.api.logistics.LongEnergyHandler; +import com.sorrowmist.useless.api.logistics.LongFluidHandler; +import com.sorrowmist.useless.api.logistics.LongItemHandler; +import com.sorrowmist.useless.energy.IEnergyManager; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.energy.IEnergyStorage; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.capability.IFluidHandler; +import net.minecraftforge.items.IItemHandler; + +/** + * 把 MinecraftForge 原生的 int 级能力包装成 {@code long} 级接口。 + * + *

本模组自己的 long 能力不走分片。 {@link IEnergyManager} 从设计之初就是 long 契约, + * 因此 {@link #energy(IEnergyManager)} 是原生直通:一次调用搬多少就是多少,不存在任何 + * int 中转,也没有补循环。只有外部模组的 int 能力才需要 {@link #energy(IEnergyStorage)} + * 那种分片包装。

+ * + *

模拟必须精确。 原生接口的 {@code simulate} 调用不改动状态,因此重复调用会一直返回 + * 同一个上限——拿它循环累加会凭空放大可搬量。所以每个模拟路径要么只探一次(物品按槽独立, + * 可以跨槽累加;同槽只探一次),要么像流体那样直接按容量/存量算出精确值。

+ */ +public final class LongResourceAdapters { + + private LongResourceAdapters() { + } + + // ------------------------------------------------------------------ 物品 + + /** + * 单次 extract / drain / fill 调用内允许的最大分片步数。 + * + *

存在的理由是一次真实的卡死事故:某些外部实现(例如 SophisticatedCore 的背包) + * 每次只让渡 1 个物品,而每个槽位的存量可能是几十万。原来那种「循环到凑满 amount 为止」 + * 的写法会把一次调用拖成几十万轮,而每一轮它都要重建槽位索引、对 DataComponent 做 + * hashCode —— 主线程就此卡死,日志停在 tick 中间,游戏表现为完全无响应。

+ * + *

更隐蔽的一种:实现返回了非空、但 {@code getCount() == 0} 的栈。 + * 此时 {@code moved} 永远不增加,是真正的死循环。所有循环都必须在 + * 「本步没有实际进展」时立刻退出。

+ * + *

截断是安全的:搬运循环每个 tick 都会重新进来,剩下的量自然顺延到下一 tick, + * 只影响速率,不影响正确性。

+ * + *

{@code insert} 不再用它。那边改成了「每个槽位只问一次」—— + * {@code IItemHandler#insertItem} 的契约是「尽量收」,收到的比给出去的少就说明槽满了, + * 再问同一个槽必然被拒,所以原本的多步重试有一半是白问。理由见 + * {@code LongResourceAdapters.items().insert} 的注释。

+ */ + private static final int MAX_CHUNK_STEPS = 64; + + /** 把 MinecraftForge 物品处理器包装成 long 级。 */ + public static LongItemHandler items(IItemHandler handler) { + return new LongItemHandler() { + /** + * 复用的暂存栈,见 {@link #insert} 的说明。 + * + *

适配器实例是每次解析端点时新建的,所以这只栈只在「一次搬运」里的两次 + * {@code insert}(模拟 + 执行)之间复用,生命周期很短。

+ */ + private ItemStack scratch; + + @Override + public int getSlots() { + return handler.getSlots(); + } + + @Override + public ItemStack getStackInSlot(int slot) { + // 必须返回副本:原生的 getStackInSlot 交出的是容器内部栈的引用, + // 一旦执行 extract 把槽位清空,外部持有的这个对象会跟着变成空栈, + // 后续据此 insert 就等于塞了个空气,搬运量凭空蒸发。 + return handler.getStackInSlot(slot).copy(); + } + + @Override + public ItemStack peekStack(int slot) { + // 扫描路径专用,故意不 copy:省掉我们自己叠加的那次 copy(实测占无线物流总耗时的 13%)。 + // + // ⚠️ 交出的就是容器槽里那只对象本身,所以调用方在**抽取之后**再拿它用是不安全的: + // 原生容器的 extractItem 抽空一个槽时常把这只栈就地 shrink 成空。扫描路径因此 + // 在「这一抽会把槽抽空」时先固化一份(StaffLinkTargets#stabilize),而不是 + // 依赖这里返回副本。流体侧没有这个问题,是因为 getFluidInTank 一律返回副本。 + return handler.getStackInSlot(slot); + } + + @Override + public long amountIn(int slot) { + if (slot < 0 || slot >= handler.getSlots()) { + return 0L; + } + return handler.getStackInSlot(slot).getCount(); + } + + @Override + public long amountIn(int slot, ItemStack stackFromSlot) { + // 无论模板是 getStackInSlot 交出的副本,还是 peekStack 交出的底层对象, + // 它的 count 都必然等于 amountIn(slot)(原生实现里槽内栈的 count 就是真实数量)。 + // 扫描路径已经付过那次读取了,直接复用 —— 对底层容器这是一次多余的 + // getStackInSlot,对 AE 这种底层是资源键的实现则是一次完整的 ItemStack 物化。 + return stackFromSlot == null ? 0L : stackFromSlot.getCount(); + } + + @Override + public long amountOf(ItemStack template) { + if (template == null || template.isEmpty()) { + return 0L; + } + // 必须跨槽累加,不能只答「第一个同类型槽」:原版容器(含本模组的大容量容器) + // 经常把同一种物品分散在好几个槽里(每个槽各自封顶)。只答一个槽会让玩家设的 + // 「源端保留 8 个」算少存量,于是多搬 —— 那是真的把不该出去的搬走了。 + // + // 用固定槽位遍历 + peekStack:这里只判类型,不需要副本。本模组的容器为了 + // 「同种物品先填满旧槽」而故意把同类型的槽相邻,所以不必提前剪枝。 + long total = 0L; + for (int slot = 0; slot < handler.getSlots(); slot++) { + ItemStack inSlot = handler.getStackInSlot(slot); + if (!inSlot.isEmpty() && ItemStack.isSameItemSameTags(inSlot, template)) { + total += inSlot.getCount(); + } + } + return total; + } + + @Override + public long extract(int slot, long amount, boolean simulate) { + if (amount <= 0L || slot < 0 || slot >= handler.getSlots()) { + return 0L; + } + if (simulate) { + // 模拟不改状态,只能探一次;栈内数量本身就是精确上限。 + ItemStack probe = handler.extractItem( + slot, (int) Math.min(amount, Integer.MAX_VALUE), true); + return probe.isEmpty() ? 0L : probe.getCount(); + } + long moved = 0L; + for (int step = 0; step < MAX_CHUNK_STEPS && moved < amount; step++) { + int chunk = (int) Math.min(amount - moved, Integer.MAX_VALUE); + ItemStack got = handler.extractItem(slot, chunk, false); + // 必须同时判空与判零:有的实现会返回「非空但 count 为 0」的栈, + // 只判 isEmpty 的话 moved 永远不增加,这里就是死循环。 + if (got.isEmpty() || got.getCount() <= 0) { + break; + } + moved += got.getCount(); + } + return moved; + } + + @Override + public long insert(ItemStack template, long amount, boolean simulate) { + if (template.isEmpty() || amount <= 0L) { + return 0L; + } + // 暂存栈:insertItem 每个槽都要一只「count = chunk」的栈,原实现每次都 + // copyWithCount 造一只新的。1.21 里造 ItemStack 不只是 new 个对象——它要连带 + // 复制整份 DataComponent 映射,是这条路上最贵的一笔。同一次调用内 template 不变, + // 所以只造一次,之后只改 count。 + // + // 这么做依赖 MinecraftForge 的契约:insertItem「不得修改传入的栈」。我们改的是自己的 + // 副本,且复用前比对过物品与组件,所以对合规实现完全等价。 + ItemStack offer = scratch; + if (offer == null || !ItemStack.isSameItemSameTags(offer, template)) { + offer = template.copy(); + scratch = offer; + } + long inserted = 0L; + // 每个槽位只问一次,理由是 IItemHandler#insertItem 的契约: + // + // 「尽量往这个槽里塞,塞不下的退回来」——所以**收到的比给出去的少,就说明这个槽满了**, + // 拿剩下的量再问同一个槽必然被拒。原实现用 while 反复问同一个槽(上限 + // MAX_CHUNK_STEPS 次),实测对 ME 接口这种 9 格容器,每个槽问 2 次、其中 1 次白问, + // 提交路径一半的 insertItem 调用都是浪费(见内部无线物流性能分析报告 11.7)。 + // + // 反过来,收到的正好等于给出去的,说明 inserted == amount,外层循环条件自然不成立, + // 也不需要再问。所以「每槽一次」在两种情况下都够用,且与旧行为等价。 + // + // 唯一的例外是「单次调用有数量上限、但槽还没满」的实现(例如一次最多收 1 组): + // 那种实现下本循环会换到下一个槽,导致单 tick 搬得比旧版少(只是吞吐,不会丢东西)。 + // 真遇到这种容器,把这里改回带 MAX_CHUNK_STEPS 的 while 即可。 + for (int slot = 0; slot < handler.getSlots() && inserted < amount; slot++) { + int chunk = (int) Math.min(amount - inserted, Integer.MAX_VALUE); + offer.setCount(chunk); + ItemStack leftover = handler.insertItem(slot, offer, simulate); + long accepted = chunk - leftover.getCount(); + if (accepted <= 0L) { + continue; + } + inserted += accepted; + } + return inserted; + } + }; + } + + // ------------------------------------------------------------------ 流体 + + /** 把 MinecraftForge 流体处理器包装成 long 级。 */ + public static LongFluidHandler fluids(IFluidHandler handler) { + return new LongFluidHandler() { + @Override + public int getTanks() { + return handler.getTanks(); + } + + @Override + public FluidStack getFluidInTank(int tank) { + // 同 getStackInSlot:必须返回副本。drain 执行后容器内部栈会被就地清空, + // 引用型返回值会让外层手里那个「待注入的类型」也一起变成空, + // 结果是抽走了、却既注入不了也回填不了——流体凭空消失。 + return handler.getFluidInTank(tank).copy(); + } + + @Override + public long amountIn(int tank) { + if (tank < 0 || tank >= handler.getTanks()) { + return 0L; + } + return handler.getFluidInTank(tank).getAmount(); + } + + @Override + public long capacityOf(int tank) { + if (tank < 0 || tank >= handler.getTanks()) { + return 0L; + } + return Math.max(0L, handler.getTankCapacity(tank)); + } + + @Override + public long amountOf(FluidStack type) { + if (type == null || type.isEmpty()) { + return 0L; + } + // 同物品侧:跨罐累加。流体容器把同一种流体分散在多个罐里同样常见 + // (机器自己的输入/输出罐、多方块的储罐阵列)。 + long total = 0L; + for (int tank = 0; tank < handler.getTanks(); tank++) { + FluidStack inTank = handler.getFluidInTank(tank); + if (!inTank.isEmpty() && com.sorrowmist.useless.compat.forge1201.fluid.FluidStacks.same(inTank, type)) { + total += inTank.getAmount(); + } + } + return total; + } + + @Override + public long drain(int tank, long amount, boolean simulate) { + if (amount <= 0L || tank < 0 || tank >= handler.getTanks()) { + return 0L; + } + FluidStack type = handler.getFluidInTank(tank).copy(); + if (type.isEmpty()) { + return 0L; + } + // 模拟与执行必须走同一组调用:过去模拟用「同类储罐存量求和」的公式推算, + // 而执行走原生 drain,两者一旦分叉(模拟高报),外层就会多抽、多抽的部分 + // 再也塞不回去,直接变成死账。现在两侧共用 drainExact。 + return drainExact(handler, type, amount, simulate); + } + + @Override + public long fill(FluidStack type, long amount, boolean simulate) { + if (type.isEmpty() || amount <= 0L) { + return 0L; + } + // 同上:模拟必须就是执行会得到的那个数,不能靠容量公式另算一套。 + return fillExact(handler, type, amount, simulate); + } + }; + } + + /** + * 流体抽取:模拟与执行共用同一组原生调用。 + * + *

原生 {@code FluidStack} 的 amount 是 int,所以单次调用天然以 {@link Integer#MAX_VALUE} + * 为界分片。模拟不改状态,同一个分片重复调用只会一直返回同一个上限,因此模拟只探一片 + * ——对真实储罐(单罐存量本就 ≤ int)这就是精确值;即便入参超 int,模拟也只会少报, + * 少报不会造成多抽,因而是安全的。

+ */ + private static long drainExact(IFluidHandler handler, FluidStack type, long amount, + boolean simulate) { + IFluidHandler.FluidAction action = simulate + ? IFluidHandler.FluidAction.SIMULATE + : IFluidHandler.FluidAction.EXECUTE; + long drained = 0L; + for (int step = 0; step < MAX_CHUNK_STEPS && drained < amount; step++) { + int chunk = (int) Math.min(amount - drained, Integer.MAX_VALUE); + FluidStack got = handler.drain(new net.minecraftforge.fluids.FluidStack(type, chunk), action); + // 同物品侧:非空但量为 0 的返回值会让 drained 停滞,必须一并判掉。 + if (got.isEmpty() || got.getAmount() <= 0) { + break; + } + drained += got.getAmount(); + if (simulate) { + break; + } + } + return drained; + } + + /** + * 流体注入:模拟与执行共用同一组原生调用,理由同 {@link #drainExact}。 + * + *

模拟只探一片,超 int 的入参会被少报;少报只会让外层少搬,不会多搬。

+ */ + private static long fillExact(IFluidHandler handler, FluidStack type, long amount, + boolean simulate) { + IFluidHandler.FluidAction action = simulate + ? IFluidHandler.FluidAction.SIMULATE + : IFluidHandler.FluidAction.EXECUTE; + long filled = 0L; + for (int step = 0; step < MAX_CHUNK_STEPS && filled < amount; step++) { + int chunk = (int) Math.min(amount - filled, Integer.MAX_VALUE); + int accepted = handler.fill(new net.minecraftforge.fluids.FluidStack(type, chunk), action); + if (accepted <= 0) { + break; + } + filled += accepted; + if (simulate) { + break; + } + } + return filled; + } + + // ------------------------------------------------------------------ 能量 + + /** + * 本模组 long 能量能力的原生直通。 + * + *

没有任何分片循环:{@code IEnergyManager} 的收发本来就是 long 语义, + * 一次 {@code transfer} 能搬多少就搬多少。

+ */ + public static LongEnergyHandler energy(IEnergyManager manager) { + return new LongEnergyHandler() { + @Override + public long extract(long amount, boolean simulate) { + return amount <= 0L ? 0L : manager.extractEnergy(amount, simulate); + } + + @Override + public long receive(long amount, boolean simulate) { + return amount <= 0L ? 0L : manager.receiveEnergy(amount, simulate); + } + + @Override + public long stored() { + return manager.getEnergyStoredLong(); + } + + @Override + public long capacity() { + return manager.getMaxEnergyStoredLong(); + } + + @Override + public boolean canExtract() { + return manager.canExtract(); + } + + @Override + public boolean canReceive() { + return manager.canReceive(); + } + }; + } + + /** + * 外部模组 int 能量的 long 级包装。 + * + *

能量没有槽位,执行时可以安全地按 int 分片循环;模拟路径只探一次上限。

+ */ + public static LongEnergyHandler energy(IEnergyStorage storage) { + return new LongEnergyHandler() { + @Override + public long extract(long amount, boolean simulate) { + if (amount <= 0L || !storage.canExtract()) { + return 0L; + } + int first = storage.extractEnergy((int) Math.min(amount, Integer.MAX_VALUE), simulate); + if (first <= 0 || simulate) { + return Math.max(0L, first); + } + long moved = first; + for (int step = 0; step < MAX_CHUNK_STEPS && moved < amount; step++) { + int chunk = (int) Math.min(amount - moved, Integer.MAX_VALUE); + int got = storage.extractEnergy(chunk, false); + if (got <= 0) { + break; + } + moved += got; + } + return moved; + } + + @Override + public long receive(long amount, boolean simulate) { + if (amount <= 0L || !storage.canReceive()) { + return 0L; + } + int first = storage.receiveEnergy((int) Math.min(amount, Integer.MAX_VALUE), simulate); + if (first <= 0 || simulate) { + return Math.max(0L, first); + } + long moved = first; + for (int step = 0; step < MAX_CHUNK_STEPS && moved < amount; step++) { + int chunk = (int) Math.min(amount - moved, Integer.MAX_VALUE); + int got = storage.receiveEnergy(chunk, false); + if (got <= 0) { + break; + } + moved += got; + } + return moved; + } + + @Override + public long stored() { + return storage.getEnergyStored(); + } + + @Override + public long capacity() { + return storage.getMaxEnergyStored(); + } + + @Override + public boolean canExtract() { + return storage.canExtract(); + } + + @Override + public boolean canReceive() { + return storage.canReceive(); + } + }; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/stafflink/PressureSealRegistry.java b/src/main/java/com/sorrowmist/useless/content/stafflink/PressureSealRegistry.java new file mode 100644 index 00000000..fde06ba5 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/stafflink/PressureSealRegistry.java @@ -0,0 +1,116 @@ +package com.sorrowmist.useless.content.stafflink; + +import com.sorrowmist.useless.world.stafflink.StaffLinkManager; +import com.sorrowmist.useless.world.stafflink.StaffLinkNetwork; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.Level; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 「被无线物流绑定的气压位置」集合。 + * + *

为什么需要它

+ * + *

气动工艺的方块在「某个面没接空气处理器」时会持续向环境漏气 + * ({@code MachineAirHandler#handleAirLeak} 每 tick 销毁 {@code 压力 × 40 + 20} mL)。 + * 典型受害者是压力室气阀:它只在朝外那一面真的挂着 {@code AIR_HANDLER_MACHINE} 能力时才认为自己是密封的。 + * 无线物流的注入只是 {@code addAir(...)},不会建立这样一个邻居,所以方块永远认为自己漏气。

+ * + *

本集合登记「哪些坐标被无线物流的气压线路绑着」,由 + * {@code mixin/pneumaticcraft/MachineAirHandlerMixin} 在空气处理器 tick 的开头查询, + * 命中就把漏气状态清掉 —— 即被无线连接的方块视同"已经接上了空气处理器"。

+ * + *

线程与生命周期

+ * + *

只在服务端线程读写:重建由 {@link StaffLinkEngine#tick} 驱动,查询来自服务端方块实体 tick。 + * mixin 侧先判 {@code level.isClientSide} 再碰本类,保证客户端线程(单人存档下与服务端不同线程) + * 永远不会读到正在被重建的集合。

+ */ +public final class PressureSealRegistry { + + /** 重建间隔;绑定 / 解绑会通过 {@link #invalidate()} 立即触发一次。 */ + private static final int REBUILD_INTERVAL = 20; + + /** 按维度分组的位置集合;用 long 打包坐标避免装箱。 */ + private static final Map, LongOpenHashSet> SEALED = new HashMap<>(); + + /** 是否有待生效的改动(绑定 / 解绑 / 改配置)。 */ + private static boolean dirty = true; + + private static long lastRebuildTick = Long.MIN_VALUE; + + private PressureSealRegistry() { + } + + /** + * 当前有没有任何位置被封堵。 + * + *

这是给 mixin 的快路径:绝大多数存档里一条气压线路都没有,此时每个空气处理器的 + * tick 只需付一次 {@code Map#isEmpty()}。

+ */ + public static boolean isEmpty() { + return SEALED.isEmpty(); + } + + /** 该坐标是否被无线物流的气压线路绑着。 */ + public static boolean isSealed(Level level, BlockPos pos) { + LongOpenHashSet positions = SEALED.get(level.dimension()); + return positions != null && positions.contains(pos.asLong()); + } + + /** + * 重建封堵集合,必要时才真做。 + * + *

全量重建而不是增量维护:增量要和绑定 / 解绑 / 启停 / 换方块四条路径保持一致, + * 漏一条就会留下"幽灵封堵";而全量重建只有 O(总路由数),代价可以忽略。

+ * + *

判据跟随引擎:归属者本局没人在线的孤儿网络不跑搬运,因此也不封 + * (见 {@link StaffLinkManager#isLive})。

+ */ + public static void rebuild(List networks, long now) { + if (!dirty && now - lastRebuildTick < REBUILD_INTERVAL) { + return; + } + dirty = false; + lastRebuildTick = now; + SEALED.clear(); + for (StaffLinkNetwork network : networks) { + if (!StaffLinkManager.isLive(network.id())) { + continue; + } + for (StaffLinkRoute route : network.routes()) { + if (!route.enabled()) { + continue; + } + // 只要方块形态的 PRESSURE:AE_PRESSURE 的锚点必然是 AE 方块 / 访问点, + // 不可能是气动方块,收进来只会多几个永远命不中的位置。 + if (route.medium() != LinkMedium.PRESSURE) { + continue; + } + if (!route.medium().isSupported()) { + continue; + } + // 释放端与吸收端都算「被无线连接」。 + SEALED.computeIfAbsent(route.anchor().dimension(), key -> new LongOpenHashSet()) + .add(route.anchor().pos().asLong()); + } + } + } + + /** 绑定 / 解绑 / 改配置后置脏,让下一次 tick 立刻重建(而不是等满一个重建周期)。 */ + public static void invalidate() { + dirty = true; + } + + /** 服务器停止时清空,别把上一局的封堵带进下一局。 */ + public static void clear() { + SEALED.clear(); + dirty = true; + lastRebuildTick = Long.MIN_VALUE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/stafflink/ResourceFamily.java b/src/main/java/com/sorrowmist/useless/content/stafflink/ResourceFamily.java new file mode 100644 index 00000000..909d8ce1 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/stafflink/ResourceFamily.java @@ -0,0 +1,21 @@ +package com.sorrowmist.useless.content.stafflink; + +/** + * 资源族:一条线路「搬的是什么」的粗分类。 + * + *

引入它是因为同一种资源出现了不同的承载方式:物品既可能是方块容器里的槽位, + * 也可能是 AE 网络里的一条 {@code AEItemKey} 记录。两者在搬运语义上完全等价(都是「物品」), + * 所以配对时按 {@link #family()} 比较而不是按 {@link LinkMedium} 比较——否则 + * 「AE 网络 → 箱子」这种最常见的用法会因为在两个不同的 medium 上而永远配不上。

+ */ +public enum ResourceFamily { + ITEM, + FLUID, + ENERGY, + CHEMICAL, + SOURCE, + /** 转速 + 应力容量;只有方块形态,没有 AE 形态。 */ + STRESS, + /** 气压(空气);方块形态(气动工艺)与 AE 形态(Applied Pneumatics)都有。 */ + PRESSURE +} diff --git a/src/main/java/com/sorrowmist/useless/content/stafflink/SourceHandlerView.java b/src/main/java/com/sorrowmist/useless/content/stafflink/SourceHandlerView.java new file mode 100644 index 00000000..435c86bb --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/stafflink/SourceHandlerView.java @@ -0,0 +1,33 @@ +package com.sorrowmist.useless.content.stafflink; + +/** + * 魔源端点契约。 + * + *

魔源刻意不追求 long:Ars Nouveau 自身的 {@code ISourceTile} 全程就是 int, + * 单个魔源罐的容量与传输速率都离 {@link Integer#MAX_VALUE} 极远,包一层 long 只会多一次 + * 无意义的转换。因此这里沿用 int,与化学品那边「本来就是 long」的情况不同。

+ * + *

引入它的原因是搬运路径要统一:方块魔源容器与 AE 网络里的魔源(Ars Énergistique + * 提供的 {@code SourceKey})在搬运语义上完全等价,只有先抽象成同一个端点, + * 「魔源罐 ↔ ME 网络」才配得起来。此前的位置对位置接口({@code moveSource})无法表达 + * 「没有方块坐标」的 ME 网络那一端。

+ */ +public interface SourceHandlerView { + + /** 当前魔源量。 */ + int amount(); + + /** 容量。 */ + int capacity(); + + /** 抽出最多 {@code amount},返回实际抽出量。 */ + int extract(int amount, boolean simulate); + + /** 接收最多 {@code amount},返回实际接收量。 */ + int receive(int amount, boolean simulate); + + /** 单次可搬运上限:两端都取各自速率与请求量的最小值。 */ + default int transferRate() { + return Integer.MAX_VALUE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/stafflink/StaffLinkBinding.java b/src/main/java/com/sorrowmist/useless/content/stafflink/StaffLinkBinding.java new file mode 100644 index 00000000..844ecc5b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/stafflink/StaffLinkBinding.java @@ -0,0 +1,231 @@ +package com.sorrowmist.useless.content.stafflink; + +import com.sorrowmist.useless.network.StaffLinkSyncPacket; +import com.sorrowmist.useless.world.stafflink.StaffLinkManager; +import com.sorrowmist.useless.world.stafflink.StaffLinkNetwork; +import com.sorrowmist.useless.world.stafflink.StaffLinkSavedData; +import net.minecraft.ChatFormatting; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.GlobalPos; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.level.block.state.BlockState; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +/** + * 潜行右键容器 → 绑定 / 解绑。 + * + *

绑定只登记「这个坐标属于这张网络」并给一条默认线路;具体怎么搬由玩家在界面里改。

+ * + *

按住 Ctrl 时走批量:把和点击位置连在一起的同种方块一次性全绑上, + * 不用一格一格点。判断「同种」用的是方块类型({@code BlockState#getBlock()}), + * 所以一排同型号的机器算一批,中间夹了一台别的机器就断开。

+ */ +public final class StaffLinkBinding { + /** 新锚点的默认单次搬运量。 */ + private static final int DEFAULT_AMOUNT = 16; + /** 新锚点的默认搬运周期(tick)。取小值,绑完就能明显看到东西在动。 */ + private static final int DEFAULT_INTERVAL = 5; + /** + * 一次批量操作最多处理多少台机器。 + * + *

玩家有可能圈到一大片(比如整面墙的储罐),不设上限的话一次点击要跑几百次 + * 能力探测,会卡住服务端 tick。512 台已经远超正常用量。

+ */ + private static final int BATCH_LIMIT = 512; + + private StaffLinkBinding() { + } + + public static void toggle(ServerLevel level, ServerPlayer player, UUID ownerId, BlockPos pos) { + StaffLinkNetwork network = StaffLinkManager.activeNetwork(level.getServer(), ownerId, true); + if (network == null) { + return; + } + StaffLinkSavedData data = StaffLinkSavedData.get(level.getServer()); + GlobalPos anchor = GlobalPos.of(level.dimension(), pos.immutable()); + + if (network.isBound(anchor)) { + network.detach(anchor); + data.markDirty(); + display(player, "gui.useless_mod.wireless_logistics.unbound", anchor, ChatFormatting.GREEN); + chime(level, pos, false); + } else { + if (!bindOne(level, player, network, pos)) { + return; + } + data.markDirty(); + display(player, "gui.useless_mod.wireless_logistics.bound", anchor, ChatFormatting.GREEN); + chime(level, pos, true); + } + + finish(level, player, ownerId, network, data); + } + + /** + * 批量绑定 / 解绑:把与 {@code origin} 连成一片的同种方块一起处理。 + * + *

方向由起点决定——起点已经绑过了就当「整批解绑」,否则整批绑定。这样玩家 + * 不用先想清楚「我现在是在加还是在减」,看一眼起点状态就知道结果。

+ */ + public static void toggleBatch(ServerLevel level, ServerPlayer player, UUID ownerId, BlockPos origin) { + StaffLinkNetwork network = StaffLinkManager.activeNetwork(level.getServer(), ownerId, true); + if (network == null) { + return; + } + if (!StaffLinkTargets.isBindable(level, origin)) { + notify(player, "gui.useless_mod.wireless_logistics.not_bindable", ChatFormatting.YELLOW); + return; + } + StaffLinkSavedData data = StaffLinkSavedData.get(level.getServer()); + GlobalPos originAnchor = GlobalPos.of(level.dimension(), origin.immutable()); + boolean unbinding = network.isBound(originAnchor); + + List group = collectGroup(level, origin); + int changed = 0; + for (BlockPos pos : group) { + if (unbinding) { + GlobalPos anchor = GlobalPos.of(level.dimension(), pos.immutable()); + if (network.isBound(anchor)) { + network.detach(anchor); + changed++; + } + } else { + if (bindOne(level, player, network, pos)) { + changed++; + } + } + } + + if (changed == 0) { + // 起点可绑定、但整批里没有一台产生变化(例如批量绑定但全都早就绑过了)。 + notify(player, unbinding + ? "gui.useless_mod.wireless_logistics.batch_none_unbound" + : "gui.useless_mod.wireless_logistics.batch_none_bound", ChatFormatting.YELLOW); + return; + } + + data.markDirty(); + player.displayClientMessage(Component.translatable(unbinding + ? "gui.useless_mod.wireless_logistics.batch_unbound" + : "gui.useless_mod.wireless_logistics.batch_bound", changed) + .withStyle(ChatFormatting.GREEN), true); + chime(level, origin, !unbinding); + + finish(level, player, ownerId, network, data); + } + + /** + * 绑定单台机器,成功返回 true。 + * + *

不在这里发包 / 存盘:批量时几十台一起改,中途同步只会让界面闪。 + * 调用方改完统一走 {@link #finish}。

+ */ + private static boolean bindOne(ServerLevel level, ServerPlayer player, StaffLinkNetwork network, BlockPos pos) { + if (!StaffLinkTargets.isBindable(level, pos)) { + return false; + } + GlobalPos anchor = GlobalPos.of(level.dimension(), pos.immutable()); + if (network.isBound(anchor)) { + return false; + } + LinkMedium medium = StaffLinkTargets.defaultMediumFor(level, pos); + if (medium == null) { + return false; + } + // 网络里还没有释放端时,第一个绑定的容器就当释放端——绑两个容器即可跑起来。 + LinkFlow flow = network.hasReleaseRoute() ? LinkFlow.ABSORB : LinkFlow.RELEASE; + // 应力线路的两个数值是「目标转速 / 旋转方向」,语义与别的介质不同,默认值也要跟着换。 + boolean stress = medium.family() == ResourceFamily.STRESS; + // 新锚点默认关闭:要搬什么由玩家显式打开,免得一绑上就开始动别人的库存。 + network.putRoute(new StaffLinkRoute(anchor, 0, false, flow, medium, + stress ? StaffLinkRoute.STRESS_DEFAULT_RPM : DEFAULT_AMOUNT, + stress ? StaffLinkRoute.STRESS_CLOCKWISE : DEFAULT_INTERVAL, + null, 0, List.of())); + return true; + } + + /** + * 从 {@code origin} 出发广度优先收集连成一片的同种方块。 + * + *

只走 6 个正交方向、只接受方块类型相同的位置,因此斜角相邻不算连通。 + * 结果按坐标排序,保证同一批机器每次绑定顺序一致(谁当释放端是可预期的)。

+ */ + private static List collectGroup(ServerLevel level, BlockPos origin) { + BlockState originState = level.getBlockState(origin); + Set visited = new HashSet<>(); + List found = new ArrayList<>(); + Deque queue = new ArrayDeque<>(); + + BlockPos start = origin.immutable(); + visited.add(start); + queue.add(start); + + while (!queue.isEmpty() && found.size() < BATCH_LIMIT) { + BlockPos current = queue.poll(); + // 起点本身已经校验过了;其余位置只比较方块类型,可绑定与否留给 bindOne 判定, + // 这样「中间有个同种方块但没能力」不会把整条链切断。 + if (level.getBlockState(current).getBlock() != originState.getBlock()) { + continue; + } + found.add(current); + for (Direction direction : Direction.values()) { + BlockPos next = current.relative(direction); + if (!level.isLoaded(next) || !visited.add(next)) { + continue; + } + queue.add(next); + } + } + found.sort((a, b) -> { + int byY = Integer.compare(a.getY(), b.getY()); + if (byY != 0) return byY; + int byX = Integer.compare(a.getX(), b.getX()); + if (byX != 0) return byX; + return Integer.compare(a.getZ(), b.getZ()); + }); + return found; + } + + /** 收尾:同步界面、唤醒引擎。所有绑定路径都必须走这里。 */ + private static void finish(ServerLevel level, ServerPlayer player, UUID ownerId, + StaffLinkNetwork network, StaffLinkSavedData data) { + // 界面开着时同步刷新,避免显示的还是旧锚点列表。 + PacketDistributor.sendToPlayer(player, + StaffLinkSyncPacket.of(level.getServer(), ownerId, network)); + // 立刻让引擎重跑一遍:新加进来的配对不该等上一次排定的退避。 + StaffLinkEngine.wake(network.id()); + } + + private static void display(ServerPlayer player, String key, GlobalPos anchor, ChatFormatting style) { + player.displayClientMessage( + Component.translatable(key, describe(anchor)).withStyle(style), true); + } + + private static void notify(ServerPlayer player, String key, ChatFormatting style) { + player.displayClientMessage(Component.translatable(key).withStyle(style), true); + } + + /** 「维度:坐标」的可读描述。 */ + public static String describe(GlobalPos anchor) { + return anchor.dimension().location() + " " + anchor.pos().toShortString(); + } + + private static void chime(ServerLevel level, BlockPos pos, boolean bound) { + level.playSound(null, pos, + bound ? SoundEvents.AMETHYST_BLOCK_CHIME : SoundEvents.AMETHYST_BLOCK_BREAK, + SoundSource.BLOCKS, 0.7F, bound ? 1.4F : 0.8F); + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/stafflink/StaffLinkEngine.java b/src/main/java/com/sorrowmist/useless/content/stafflink/StaffLinkEngine.java new file mode 100644 index 00000000..f5bde8c2 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/stafflink/StaffLinkEngine.java @@ -0,0 +1,764 @@ +package com.sorrowmist.useless.content.stafflink; + +import com.sorrowmist.useless.api.logistics.LongPressureHandler; +import com.sorrowmist.useless.compat.create.CreateStressCompatLoader; +import com.sorrowmist.useless.core.config.ConfigManager; +import com.sorrowmist.useless.world.stafflink.StaffLinkManager; +import com.sorrowmist.useless.world.stafflink.StaffLinkNetwork; +import com.sorrowmist.useless.world.stafflink.StaffLinkSavedData; +import net.minecraft.core.GlobalPos; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.BiPredicate; + +/** + * 无线物流的搬运引擎。 + * + *

每个服务端 tick 遍历所有网络:按线路号分组,把「释放端」的资源搬给同线路同类型的 + * 「吸收端」。搬运完全在服务端进行,与玩家是否手持造化杖无关——网络存在存档里, + * 所以杖放在箱子里、所在区块卸载都不影响。

+ * + *

三条自我保护:

+ *
    + *
  • 不强制加载:锚点所在区块未加载就跳过本轮;
  • + *
  • 预算:单 tick 最多处理 {@link #NETWORK_BUDGET} 张网络;
  • + *
  • 退避:解析失败或空转的网络按 {@link #BACKOFF_TICKS} 推迟,避免每 tick 空跑。
  • + *
+ */ +public final class StaffLinkEngine { + private static final int NETWORK_BUDGET = 32; + /** 锚点解析失败(区块没加载 / 方块没了)时的退避。 */ + private static final int BACKOFF_TICKS = 20; + /** 自愈周期:清理锚点方块已消失的线路配置。 */ + private static final int SELF_HEAL_INTERVAL = 100; + /** 刷新「本局活跃网络」集合的间隔。 */ + private static final int LIVE_REFRESH_INTERVAL = 20; + /** 每次自愈最多检查多少张网络,避免一次扫全部。 */ + private static final int PRUNE_BUDGET = 16; + + /** + * 空转退避的倍率上限:下一轮间隔最多放大到配置周期的这个倍数。 + * + *

空转(这一轮搬运量为 0)时把间隔翻倍,直到 {@code interval × 倍率上限} 封顶。 + * 一旦搬动就立刻恢复成配置的 {@code interval}。

+ * + *

为什么需要它。此前「搬不动」也按配置周期原样重排下一轮,而周期最小是 1 —— + * 于是一个空的、卡住的通道会每 tick 白跑一次完整流程(解析端点 + 判断 + 读容器)。 + * 在有大量空通道的装置上,这部分开销实测独占 {@code onStaffLinkTick} 的 90.9%, + * 真正的搬运反而微不足道。参考实现(P2P Channel Network)用指数退避解决同一问题, + * 这里对齐它的做法。

+ * + *

倍率上限走配置。{@code stafflink_idle_backoff_enabled} 控制开关, + * {@code stafflink_idle_backoff_cap} 给出倍率(默认 32)。倍率按 2 的幂取, + * 因为间隔按 {@code interval << 退避次数} 增长,「翻几次」到的倍数上限就是 log2(倍率)。 + * 关掉开关时本类退化成「严格按配置周期」,不再有翻倍。

+ */ + private static int backoffCap() { + return ConfigManager.isStaffLinkIdleBackoffEnabled() + ? ConfigManager.getStaffLinkIdleBackoffCap() + : 1; + } + + /** + * 退避的指数上限 = {@code log2(倍率上限)}。 + * + *

间隔按 {@code interval << 退避} 增长,所以「翻几次」到的倍数上限就是 log2。 + * 到顶之后不再增加,避免长期空转的通道把间隔推到无限。倍率非 2 的幂时向下取整 + * ({@code 5 → 翻 2 次 → 最多 ×4}),若倍率本身就小于 2 则退化为 0 次翻倍。

+ */ + private static int backoffCapExponent() { + return Integer.numberOfTrailingZeros(backoffCap()); + } + + /** + * 每张网络的调度表:锚点 → 各线路号的下一次可运行 tick。 + * + *

计时粒度必须细到单个容器:同一条线路上两个输入端的「周期」可以完全不同(1 和 1200), + * 若只按线路记一个时间、取最小值,周期 1200 的那个也会跟着周期 1 的每 tick 跑, + * 「周期」就形同虚设了。

+ * + *

用「锚点 → {@code long[]}」而不是「线路号 + 锚点」当 map 的键:锚点本来就是调用方手里 + * 现成的 {@link GlobalPos},线路号只是数组下标,于是查询与写入都不必再构造临时对象。

+ * + *

运行时状态,服务器停止时清空;解绑的节点会在自愈周期里清掉。

+ */ + private static final Map NODE_NEXT_RUN = new HashMap<>(); + + private static final class NetworkSchedule { + /** 该线路号还没排过队。 */ + private static final long UNSCHEDULED = Long.MIN_VALUE; + + private final Map byAnchor = new HashMap<>(); + /** + * 每条「锚点 × 线路」当前的退避次数(空转了几轮)。 + * + *

刻意与 {@link #byAnchor} 平行存放,而不是塞进同一个数组:下一个可运行 tick 是 + * {@code long}(绝对值),退避次数是 {@code int}(相对值),语义不同,混在一个 + * {@code long[]} 里要靠位运算拆包,不值得。两者共用同一把锚点键、同一套 prune。

+ * + *

只对「非 0 退避」的条目建行:稳态下绝大多数线路都是 0(正常搬运), + * 为它们各留一个数组纯属浪费。

+ */ + private final Map backoffByAnchor = new HashMap<>(); + /** 表内最小的「已排定」tick;{@code null} 表示需要重算。 */ + private Long earliest; + + long nextRun(int route, GlobalPos anchor) { + long[] slots = byAnchor.get(anchor); + if (slots == null) { + return 0L; + } + long value = slots[route]; + return value == UNSCHEDULED ? 0L : value; + } + + void setNextRun(int route, GlobalPos anchor, long tick) { + byAnchor.computeIfAbsent(anchor, key -> unscheduledSlots())[route] = tick; + // 排进去的值只会把「最快到点」往后推,缓存必须作废。 + earliest = null; + } + + /** 记录这一轮的退避次数;0 表示恢复正常节奏,顺手把行删掉不留垃圾。 */ + void setBackoff(int route, GlobalPos anchor, int backoff) { + if (backoff <= 0) { + int[] slots = backoffByAnchor.get(anchor); + if (slots != null) { + slots[route] = 0; + } + return; + } + int[] slots = backoffByAnchor.get(anchor); + if (slots == null) { + slots = new int[StaffLinkNetwork.ROUTE_COUNT]; + backoffByAnchor.put(anchor, slots); + } + slots[route] = backoff; + } + + /** 这条「锚点 × 线路」当前空转了几轮;没有记录时是 0。 */ + int backoffOf(int route, GlobalPos anchor) { + int[] slots = backoffByAnchor.get(anchor); + return slots == null ? 0 : slots[route]; + } + + /** 清掉已经没有对应线路配置的条目;整条锚点都空了就把锚点也删掉。 */ + void pruneAgainst(BiPredicate stillConfigured) { + boolean changed = false; + Iterator> iterator = byAnchor.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + long[] slots = entry.getValue(); + boolean empty = true; + for (int route = 0; route < slots.length; route++) { + if (slots[route] != UNSCHEDULED && !stillConfigured.test(entry.getKey(), route)) { + slots[route] = UNSCHEDULED; + changed = true; + } + empty &= slots[route] == UNSCHEDULED; + } + if (empty) { + iterator.remove(); + changed = true; + } + } + // 退避表跟着一起清:行只在「有退避次数」时存在,线路没了就该丢。 + // 判据用 stillConfigured 而不是 byAnchor —— 有的线路还在配置里、只是这一轮 + // 恰好没排上队(byAnchor 里是 UNSCHEDULED),它的退避状态必须留着。 + backoffByAnchor.entrySet().removeIf(entry -> { + for (int route = 0; route < entry.getValue().length; route++) { + if (entry.getValue()[route] > 0 + && stillConfigured.test(entry.getKey(), route)) { + return false; + } + } + return true; + }); + if (changed) { + earliest = null; + } + } + + /** + * 这张表里有没有已经到点的条目;还没有任何条目时算到点(首次)。 + * + *

「没有条目就算到点」是原语义,别改:刚绑定、还没跑过的网络必须能立刻跑一次。

+ */ + boolean anyDue(long now) { + if (byAnchor.isEmpty()) { + return true; + } + Long cached = earliest; + if (cached != null && now < cached) { + // 快路径:连最快的那个都还没到点,其余的更不用看。稳态下走的就是这一条。 + return false; + } + long earliestSeen = Long.MAX_VALUE; + boolean due = false; + for (long[] slots : byAnchor.values()) { + for (long value : slots) { + if (value == UNSCHEDULED) { + continue; + } + if (now >= value) { + due = true; + } else if (value < earliestSeen) { + earliestSeen = value; + } + } + } + if (earliestSeen == Long.MAX_VALUE) { + // 一个已排定的条目都没有(pruneAgainst 之后理论上不会出现)。当作首次,别让网络饿死。 + earliest = null; + return true; + } + // 有到点的条目时缓存作废:它下一刻就可能被重排,不能拿旧的最小值去挡。 + earliest = due ? null : earliestSeen; + return due; + } + + private static long[] unscheduledSlots() { + long[] slots = new long[StaffLinkNetwork.ROUTE_COUNT]; + Arrays.fill(slots, UNSCHEDULED); + return slots; + } + } + + private static long nextRunAt(UUID networkId, int route, GlobalPos anchor) { + NetworkSchedule schedule = NODE_NEXT_RUN.get(networkId); + return schedule == null ? 0L : schedule.nextRun(route, anchor); + } + + private static void setNextRun(UUID networkId, int route, GlobalPos anchor, long tick) { + NODE_NEXT_RUN.computeIfAbsent(networkId, id -> new NetworkSchedule()) + .setNextRun(route, anchor, tick); + } + + /** + * 按「本轮搬动了没有」排下一轮,并维护退避次数。这是释放端的唯一排程入口。 + * + *

搬动了 → 退避清零,下一轮按配置的 {@code interval};空转 → 退避 +1, + * 下一轮间隔为 {@code interval << min(退避, log2(退避倍率上限))}(即每次翻倍, + * 到 {@code interval × 倍率上限} 封顶)。开关与倍率见 {@link #backoffCap()}。

+ * + *

为什么不是「一空转就跳过一个周期」。那样对「周期 1」的通道等于没退避 + * (跳过 1 tick 还是每 tick 跑);而「周期 1200」的通道本来就难得空转,翻倍也无所谓。 + * 指数退避对两种都成立:短周期通道迅速被推远,长周期通道几乎不受影响。

+ * + *

它还承担着第二个职责:把「扫了一整遍但一个也没搬动」的代价压掉。 + * 第九轮删掉了扫描端的 {@code MAX_SCAN_SLOTS} 预算(那是个语义 bug,会让排在后面的资源 + * 永远轮不到,见 {@code StaffLinkTargets#sweepItems}),改由「循环退出条件 = 搬够了」 + * 与这里的退避共同兜底。所以 {@code moved == 0} 必须记为一次空转 + * ——这是删掉那个预算的前置条件。调用点的判据就是 {@code result.moved() > 0L}: + * 搬运完全没进展(含「扫完一遍但目标全满」)时 {@code moved} 为 0, + * 计入空转,大网络上就不会反复白扫。

+ */ + private static void scheduleNextRun(UUID networkId, int route, GlobalPos anchor, + long now, int interval, boolean moved) { + NetworkSchedule schedule = NODE_NEXT_RUN.computeIfAbsent(networkId, id -> new NetworkSchedule()); + int backoff; + if (moved) { + backoff = 0; + } else { + // 上限按「翻几倍」算,避免 32 次翻倍之后 long 溢出(实际 interval 最大 1200)。 + backoff = Math.min(schedule.backoffOf(route, anchor) + 1, backoffCapExponent()); + } + schedule.setBackoff(route, anchor, backoff); + long wait = backoff == 0L + ? Math.max(1, interval) + : Math.max(1, (long) interval << backoff); + schedule.setNextRun(route, anchor, now + wait); + } + + /** 这张网络有没有哪个容器到了该跑的时候。还没排过(首次)时算到点。 */ + private static boolean anyNodeDue(UUID networkId, long now) { + NetworkSchedule schedule = NODE_NEXT_RUN.get(networkId); + return schedule == null || schedule.anyDue(now); + } + + /** 输出端的优先级:权重降序;同权重保持列表次序({@code sort} 是稳定的)。 */ + private static final Comparator BY_WEIGHT = + Comparator.comparingInt(StaffLinkRoute::weight).reversed(); + + // ------------------------------------------------------------------ 主循环 + + public static void tick(MinecraftServer server) { + StaffLinkSavedData data = StaffLinkSavedData.get(server); + // all() 本身就返回一份快照副本,不必再复制一次。 + List networks = data.all(); + long now = server.getTickCount(); + // 气压封堵集合必须先于下面的早退更新:网络被删空时它要能跟着清空, + // 否则已解绑的方块会被永久误封(mixin 只看这个集合,不看线路还在不在)。 + PressureSealRegistry.rebuild(networks, now); + if (networks.isEmpty()) { + return; + } + + if (now % LIVE_REFRESH_INTERVAL == 0) { + // 刷新「归属者本局有成员在线的网络」;没有归属者认领的孤儿网络一律不跑。 + StaffLinkManager.refreshLiveNetworks(server); + } + + int visited = 0; + for (StaffLinkNetwork network : networks) { + if (!StaffLinkManager.isLive(network.id())) { + continue; + } + if (!anyNodeDue(network.id(), now) && !network.hasStressRoute()) { + continue; + } + // 预算按「真正跑过的网络」计:跑过的会写下一次可运行时间,因此本 tick 没轮到的 + // 会在下一 tick 补上,不会饿死。 + if (visited >= NETWORK_BUDGET) { + break; + } + visited++; + runNetwork(server, network, now); + } + + if (now % SELF_HEAL_INTERVAL == 0) { + pruneStaleRoutes(server, data, networks); + // 网络被解散 / 自愈删除、或节点被解绑后,调度表里的条目也要跟着走。 + NODE_NEXT_RUN.keySet().removeIf(id -> data.get(id) == null); + for (StaffLinkNetwork network : networks) { + NetworkSchedule schedule = NODE_NEXT_RUN.get(network.id()); + if (schedule != null) { + schedule.pruneAgainst((anchor, route) -> network.routeAt(anchor, route) != null); + } + } + // 「上次命中的面」提示失手只会退回完整扫描、不会读错,清它纯粹是防止无限增长。 + StaffLinkTargets.clearCapabilityHints(); + } + } + + /** 服务器停止时清掉运行时状态,别把上一局的 tick 数带进下一局。 */ + public static void clearRuntimeState() { + NODE_NEXT_RUN.clear(); + StaffLinkManager.clearLive(); + StaffLinkTargets.clearCapabilityHints(); + PressureSealRegistry.clear(); + } + + /** + * 网络刚被改动(绑定 / 解绑 / 改线路配置)时调用。 + * + *

把它的「下一次可运行时间」清掉,让引擎在本 tick 就重跑一遍——否则新加进来的配对要等 + * 上一次排定的退避走完才被发现,表现就是「刚绑定没反应,过一会儿才动」。

+ */ + public static void wake(UUID networkId) { + if (networkId != null) { + NODE_NEXT_RUN.remove(networkId); + StaffLinkManager.markLive(networkId); + // 绑定 / 解绑 / 改配置都会走到这里:让气压封堵集合下一次 tick 就重建, + // 而不是等满一个重建周期(否则新绑定的气阀最多还要多漏 1 秒)。 + PressureSealRegistry.invalidate(); + } + } + + // ------------------------------------------------------------------ 单张网络 + + private static void runNetwork(MinecraftServer server, StaffLinkNetwork network, long now) { + // 按线路号分桶。用数组 + 惰性建桶:ROUTE_COUNT 个列表里绝大多数是空的, + // 每 tick 每个网络白建 9 个对象不划算。 + @SuppressWarnings("unchecked") + List[] byRoute = new List[StaffLinkNetwork.ROUTE_COUNT]; + for (StaffLinkRoute route : network.routes()) { + int index = route.route(); + List bucket = byRoute[index]; + if (bucket == null) { + bucket = new ArrayList<>(); + byRoute[index] = bucket; + } + bucket.add(route); + } + + for (int routeIndex = 0; routeIndex < StaffLinkNetwork.ROUTE_COUNT; routeIndex++) { + List onRoute = byRoute[routeIndex]; + if (onRoute == null) { + continue; + } + + // ---- 应力:整条线路一起处理,每 tick 一次,不吃周期也不吃退避 ---- + // + // 转速与应力容量是「网络的状态」而不是「一轮搬多少」:同一个网络上所有机器共享一个 + // 转速,输出端能拿到多少容量又取决于输入端网络的富余量,还要在多个输出之间分配。 + // 这些都得拿到「同线路上全部输入与全部输出」才算得出来,因此它走的是「跑一整条线路」 + // 而不是逐对搬运。 + // + // 也正因为是状态量,它不能进 scheduleNextRun:一旦按周期退避,源端停转之后 + // 目标端要等几十秒才会跟着停,而玩家的预期是立刻停。 + // + // `enabled()` 必须在这里判。新绑定的线路默认是关闭的(由玩家显式打开), + // 漏掉这一条就会「一绑上目标端就转起来」—— 开关形同虚设。 + // 关掉的线路不进 releases / absorbs,桥那边拿到空表就会释放认领、把转速摘掉。 + List stressReleases = null; + List stressAbsorbs = null; + for (StaffLinkRoute candidate : onRoute) { + if (!candidate.enabled() + || candidate.medium().family() != ResourceFamily.STRESS + || !candidate.medium().isSupported() + || !passesGate(server, candidate)) { + continue; + } + if (candidate.flow() == LinkFlow.RELEASE) { + if (stressReleases == null) { + stressReleases = new ArrayList<>(2); + } + stressReleases.add(candidate); + } else { + if (stressAbsorbs == null) { + stressAbsorbs = new ArrayList<>(2); + } + stressAbsorbs.add(candidate); + } + } + if (stressReleases != null || stressAbsorbs != null) { + CreateStressCompatLoader.applyRoute(server, network.id(), routeIndex, + stressReleases == null ? List.of() : stressReleases, + stressAbsorbs == null ? List.of() : stressAbsorbs); + } + + // ---- 气压:双向伺服 ---- + // + // 气压与应力一样是状态量,目标不是「一轮搬多少」而是「把接收端调到某个气压」。 + // 逐对搬运模型只有「源 → 目标」一个方向,表达不了「把接收端的空气抽掉」, + // 所以同样走「跑一整条线路」。 + // + // 与应力不同的是它吃 interval 与退避:伺服是周期性的(多久调一次), + // 不像转速那样必须每 tick 对齐。 + List pressureReleases = null; + List pressureAbsorbs = null; + for (StaffLinkRoute candidate : onRoute) { + if (!candidate.enabled() + || candidate.medium().family() != ResourceFamily.PRESSURE + || !candidate.medium().isSupported() + || !passesGate(server, candidate)) { + continue; + } + if (candidate.flow() == LinkFlow.RELEASE) { + if (pressureReleases == null) { + pressureReleases = new ArrayList<>(2); + } + pressureReleases.add(candidate); + } else { + if (pressureAbsorbs == null) { + pressureAbsorbs = new ArrayList<>(2); + } + pressureAbsorbs.add(candidate); + } + } + if (pressureAbsorbs != null) { + applyPressureRoute(server, network.id(), routeIndex, now, pressureAbsorbs, + pressureReleases == null ? List.of() : pressureReleases); + } + + // ---- 第一遍:只挑「这一 tick 真的到点」的释放端 ---- + // + // 顺序很要紧:周期判定必须排在解析世界与读红石之前。绝大多数 tick 里一条线路都 + // 没到点,先判周期就能整段跳过;反过来写等于每 tick 对每条线路都做一次 + // levelOf + 6 个方向的方块查询,白耗。吸收端的桶也是惰性建的:走到这里就返回的 + // 情况(稳态下的多数 tick)连那个列表都不必建。 + // + // 只有「自己这个容器」到点的输入端才搬:同一条线路上两个输入的周期可以完全不同, + // 不能因为其中一个到点了就把另一个也捎上。 + List dueReleases = null; + for (StaffLinkRoute candidate : onRoute) { + if (!candidate.enabled() || !candidate.medium().isSupported() + || candidate.medium().family() == ResourceFamily.STRESS + || candidate.medium().family() == ResourceFamily.PRESSURE + || candidate.flow() != LinkFlow.RELEASE) { + continue; + } + if (now < nextRunAt(network.id(), routeIndex, candidate.anchor())) { + continue; + } + if (dueReleases == null) { + dueReleases = new ArrayList<>(2); + } + dueReleases.add(candidate); + } + if (dueReleases == null) { + continue; + } + + // ---- 确认有活要干,才去碰方块 ---- + // + // 「世界是否加载」到这里才判:没有释放端要跑时,吸收端的状态读了也没用。 + // 没通过的释放端不进 dueReleases,因此不会排程 —— 保持原语义:条件恢复后 + // 下一 tick 立刻再试,而不是等一个周期。 + dueReleases.removeIf(release -> !passesGate(server, release)); + if (dueReleases.isEmpty()) { + continue; + } + List absorbs = new ArrayList<>(2); + for (StaffLinkRoute candidate : onRoute) { + if (candidate.enabled() && candidate.medium().isSupported() + && candidate.medium().family() != ResourceFamily.STRESS + && candidate.medium().family() != ResourceFamily.PRESSURE + && candidate.flow() != LinkFlow.RELEASE + && passesGate(server, candidate)) { + absorbs.add(candidate); + } + } + + dueReleases.sort(BY_WEIGHT); + absorbs.sort(BY_WEIGHT); + + for (StaffLinkRoute release : dueReleases) { + ServerLevel releaseLevel = levelOf(server, release.anchor()); + if (releaseLevel == null) { + // 世界没加载:保持原语义,下一 tick 立刻重试(不排程)。 + continue; + } + + List targets = new ArrayList<>(absorbs.size()); + for (StaffLinkRoute absorb : absorbs) { + // 按 family 配对而不是按 medium:箱子(ITEM)与 AE 网络(AE_ITEM) + // 都是「物品」,能互相搬运;同一种资源类型自己配自己也照常成立。 + if (absorb.medium().family() == release.medium().family()) { + targets.add(absorb); + } + } + if (targets.isEmpty()) { + // 配不上接收端:也算空转,走退避,免得这条线路每 tick 白跑。 + scheduleNextRun(network.id(), routeIndex, release.anchor(), + now, release.interval(), false); + continue; + } + + Distribution result = distribute(releaseLevel, release, targets, server); + // 按「这一轮到底搬动了没有」排下一轮:空转走指数退避,见 scheduleNextRun。 + scheduleNextRun(network.id(), routeIndex, release.anchor(), + now, release.interval(), result.moved() > 0L); + } + } + } + + // ------------------------------------------------------------------ 分配 + + /** 一次分配的结果:搬走多少。 */ + private record Distribution(long moved) { + } + + /** + * 把这次搬运分给同线路的多个输出。 + * + *

「数量」是每个输出各搬多少,不是这次搬运的总量:每个输出最多搬 {@code amount}, + * 相互之间不瓜分。所以一个输入接 N 个输出时,这一轮的搬运上限是 {@code N × 数量}。

+ * + *

这样改是因为接收端各自设了不同的过滤时,它们本来就不争抢同一批货: + * 一台只收铁、一台只收金,按总量平分等于让它们互相「占额度」,各自只能拿到 1/N。

+ * + *

代价要讲清楚:源端的库存仍然是共享的,前面的输出先取、取完为止,后面的只能拿到 + * 剩下的。源不够分时表现就是「东西全进了靠前的那个」(也就是权重高的那个)。要避免它, + * 要么把数量调小,要么给接收端设过滤让它们各取所需。

+ */ + private static Distribution distribute(ServerLevel releaseLevel, StaffLinkRoute release, + List targets, MinecraftServer server) { + // 源端点解析一次,所有接收端共用。AE 端点每次解析都要「取方块实体 → 取能力」并新建 + // 一份端点实例,按接收端个数重复付这笔钱没有意义;顺带也让端点内部的整网快照 + // (只在无过滤器的扫描路径上才会用到)在多个接收端之间保持一致。 + Object from = StaffLinkTargets.resolveSource(releaseLevel, release); + if (from == null) { + return new Distribution(0L); + } + long amount = release.amount(); + long moved = 0; + + for (StaffLinkRoute target : targets) { + ServerLevel targetLevel = levelOf(server, target.anchor()); + if (targetLevel == null) { + continue; + } + moved += StaffLinkTargets.transfer(from, releaseLevel, release, targetLevel, target, amount); + } + return new Distribution(moved); + } + + // ------------------------------------------------------------------ 气压伺服 + + /** + * 气压的双向伺服:把每个「接收端」调到它设定的目标气压。 + * + *

规则:

+ *
    + *
  • 高于目标 ⇒ 把多余的空气 {@code setAirTo} 排到环境(按用户决定,不回收)。
  • + *
  • 低于目标 ⇒ 从同线路的释放端补气,最多补到目标。
  • + *
  • 接收端是 AE 网络({@code volume() == 0})⇒ 没有压力概念,退化为「把源端能给的 + * 空气尽量搬进网络」。
  • + *
+ * + *

搬运量来自「目标 − 当前」而不是 {@link StaffLinkRoute#amount()}:气压线路的 {@code amount} + * 已被复用为目标气压(毫巴)。

+ */ + private static void applyPressureRoute(MinecraftServer server, UUID networkId, int routeIndex, long now, + List absorbs, List releases) { + for (StaffLinkRoute absorb : absorbs) { + if (now < nextRunAt(networkId, routeIndex, absorb.anchor())) { + continue; + } + ServerLevel targetLevel = levelOf(server, absorb.anchor()); + if (targetLevel == null) { + continue; + } + Object endpoint = StaffLinkTargets.resolve(targetLevel, absorb.anchor().pos(), + absorb.side(), absorb.medium()); + if (!(endpoint instanceof LongPressureHandler target)) { + // 解析不出端点(方块没了 / 介质装错):按空转退避,别每 tick 白跑。 + scheduleNextRun(networkId, routeIndex, absorb.anchor(), now, absorb.interval(), false); + continue; + } + + boolean moved; + if (target.volume() > 0L) { + long targetAir = Math.round((double) absorb.amount() / 1000.0D * (double) target.volume()); + long current = target.stored(); + if (current > targetAir) { + // 高于目标:把多余空气排到环境(可为负压:setAirTo 允许越过 0 继续降)。 + moved = target.setAirTo(targetAir, false) != 0L; + } else if (current < targetAir) { + moved = fillPressure(server, target, targetAir - current, releases); + } else { + moved = false; + } + } else { + // AE 网络:没有压力概念,把源端能给的空气尽量搬进来。 + moved = fillPressure(server, target, Long.MAX_VALUE, releases); + } + scheduleNextRun(networkId, routeIndex, absorb.anchor(), now, absorb.interval(), moved); + } + } + + /** + * 从同线路的释放端把空气搬进 {@code target},最多 {@code need} mL。 + * + *

先模拟后提交:先从源端试算能抽多少,再真抽,最后写进目标;目标没全收下的部分 + * 退回源端,保证空气不凭空消失(「排到环境」是另一条明确的路,不走这里)。

+ * + * @return 是否真的搬动了(决定下一轮走正常周期还是退避) + */ + private static boolean fillPressure(MinecraftServer server, LongPressureHandler target, long need, + List releases) { + if (need <= 0L || releases.isEmpty()) { + return false; + } + long remaining = need; + boolean moved = false; + for (StaffLinkRoute release : releases) { + if (remaining <= 0L) { + break; + } + ServerLevel releaseLevel = levelOf(server, release.anchor()); + if (releaseLevel == null) { + continue; + } + Object endpoint = StaffLinkTargets.resolve(releaseLevel, release.anchor().pos(), + release.side(), release.medium()); + if (!(endpoint instanceof LongPressureHandler source)) { + continue; + } + long available = source.extract(remaining, true); + if (available <= 0L) { + continue; + } + long taken = source.extract(available, false); + if (taken <= 0L) { + continue; + } + long accepted = target.receive(taken, false); + if (accepted < taken) { + // 目标没收下:把剩下的退回源端,别让它凭空消失。 + source.receive(taken - accepted, false); + } + if (accepted > 0L) { + moved = true; + remaining -= accepted; + } + } + return moved; + } + + // ------------------------------------------------------------------ 自愈 + + /** + * 清理「锚点方块已经不在」的线路配置。 + * + *

只在区块已加载时下结论:未加载的坐标一律保留,否则玩家跑远一点就会把整张网清空。

+ * + *

判据是「方块本身没了」,不是「当前介质解析不出」。 这一点必须说清楚,因为早先 + * 正是拿 {@code resolve(...) == null} 当判据,才出现了「过几秒容器被自动删掉」:解析失败 + * 的原因太多了——方块确实被挖掉只是其中之一,此外还有「这一格本来就不是该介质的容器」 + * 「ME 接口选了 AE 化学品而附属没装」「AE 网络暂时离线」等等。这些情况下方块都还立在那里, + * 玩家配的线不该被抹掉。搬运失败会自己失败并在界面显示卡在哪一步,条件恢复又自动继续, + * 不需要自愈来「帮忙」。资源类型当前不受支持时也跳过(不能因为没装 Mekanism 就删化学品线路)。

+ */ + private static void pruneStaleRoutes(MinecraftServer server, StaffLinkSavedData data, + List networks) { + int visited = 0; + boolean changed = false; + for (StaffLinkNetwork network : networks) { + if (visited++ >= PRUNE_BUDGET) { + break; + } + List stale = new ArrayList<>(); + for (StaffLinkRoute route : network.routes()) { + if (!route.medium().isSupported()) { + continue; + } + ServerLevel level = server.getLevel(route.anchor().dimension()); + if (level == null || !level.isLoaded(route.anchor().pos())) { + continue; + } + if (!StaffLinkTargets.anchorPresent(level, route.anchor().pos())) { + stale.add(route); + } + } + if (stale.isEmpty()) { + continue; + } + for (StaffLinkRoute route : stale) { + network.detach(route.anchor()); + } + changed = true; + if (network.isEmpty()) { + data.remove(network.id()); + NODE_NEXT_RUN.remove(network.id()); + } + } + if (changed) { + data.markDirty(); + } + } + + // ------------------------------------------------------------------ 工具 + + /** + * 该线路这一 tick 是否放行:只要求锚点所在世界已加载。 + * + *

红石触发已整体移除。早先这里还会按 {@code LinkTrigger} 读一次 + * {@code getBestNeighborSignal},实测在「大量通道空转」的装置里这一项独占 + * {@code onStaffLinkTick} 的 90.9%(1672 ms)——因为每个锚点每 tick 都要走 6 个方向的 + * 方块查询,而绝大多数通道当轮根本没有东西可搬。无线物流本身就是「持续搬运」语义, + * 红石闸门在这个场景里既边缘又昂贵,因此连同 {@code LinkTrigger} 枚举一起删除。 + * 需要「什么时候搬」的玩家改用 {@code interval}(周期)表达。

+ */ + private static boolean passesGate(MinecraftServer server, StaffLinkRoute route) { + return levelOf(server, route.anchor()) != null; + } + + /** 锚点所在的服务端世界;未加载或维度不存在时返回 {@code null}。 */ + @Nullable + public static ServerLevel levelOf(MinecraftServer server, GlobalPos anchor) { + ServerLevel level = server.getLevel(anchor.dimension()); + return level != null && level.isLoaded(anchor.pos()) ? level : null; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/stafflink/StaffLinkFilters.java b/src/main/java/com/sorrowmist/useless/content/stafflink/StaffLinkFilters.java new file mode 100644 index 00000000..a08c8fca --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/stafflink/StaffLinkFilters.java @@ -0,0 +1,108 @@ +package com.sorrowmist.useless.content.stafflink; + +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalCompatProvider; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalCompatProviders; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalStackView; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.common.capabilities.ForgeCapabilities; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.capability.IFluidHandlerItem; +import org.jetbrains.annotations.Nullable; + +/** + * 把「玩家手上的物品」或「JEI 拖来的原料」按线路的资源类型解释成一格过滤标记。 + * + *

关键在于按类型分流:流体线路只认流体,物品线路只认物品,化学品线路只认能读出 + * 化学品的那种容器。这样就不会出现「往物品线路的过滤器里拖水、结果存了个水桶」, + * 也不会有「没有桶的流体根本标记不上」。

+ * + *

解释不了时一律返回 {@code null},由调用方保持原样——绝不悄悄把格子清空或塞进错的东西。

+ */ +public final class StaffLinkFilters { + + private StaffLinkFilters() { + } + + /** + * 按线路资源类型解释一个物品(手上拿的,或 JEI 里的物品原料)。 + * + *

流体线路上,容器里装什么就标记什么:水桶 → 水,流体罐 → 罐里那种流体。

+ * + * @return 解释不了时返回 {@code null} + */ + @Nullable + public static LinkFilterSlot fromItem(LinkMedium medium, ItemStack stack) { + if (medium == null || stack == null || stack.isEmpty()) { + return null; + } + if (medium.family() == ResourceFamily.FLUID) { + FluidStack fluid = fluidInItem(stack); + return fluid.isEmpty() ? null : LinkFilterSlot.ofFluid(fluid); + } + if (medium.family() == ResourceFamily.CHEMICAL) { + // 化学品线路只认「读得出化学品」的容器;一块石头放进来什么也匹配不到,不如不接。 + ChemicalStackView chemical = chemicalInItem(stack); + return chemical == null || chemical.isEmpty() ? null : LinkFilterSlot.ofItem(stack); + } + return LinkFilterSlot.ofItem(stack); + } + + /** + * 按线路资源类型解释一个 JEI 原料。 + * + * @return 解释不了(类型对不上、或转换失败)时返回 {@code null} + */ + @Nullable + public static LinkFilterSlot fromIngredient(LinkMedium medium, Object raw) { + if (medium == null || raw == null) { + return null; + } + if (raw instanceof FluidStack fluid) { + // 流体原料只进流体线路:拖到物品线路上不接,免得又变成「桶」。 + return medium.family() == ResourceFamily.FLUID && !fluid.isEmpty() + ? LinkFilterSlot.ofFluid(fluid) : null; + } + if (raw instanceof ItemStack stack) { + return fromItem(medium, stack); + } + if (medium.family() == ResourceFamily.CHEMICAL) { + // 化学品原料只有化学品集成自己认得;由它造一只装满该化学品的储罐当标记。 + ChemicalCompatProvider provider = ChemicalCompatProviders.get(); + if (!provider.isAvailable()) { + return null; + } + ItemStack marker = provider.markerForChemical(raw); + return marker.isEmpty() ? null : LinkFilterSlot.ofItem(marker); + } + return null; + } + + /** + * 容器物品里装的第一种流体;不是流体容器、或是空罐时返回空栈。 + * + *

老存档迁移也要用,所以是 public。

+ */ + public static FluidStack fluidInItem(ItemStack stack) { + if (stack == null || stack.isEmpty()) { + return FluidStack.EMPTY; + } + IFluidHandlerItem handler = com.sorrowmist.useless.compat.forge1201.capability.CapabilityAccess.get(stack, ForgeCapabilities.FLUID_HANDLER_ITEM); + if (handler == null) { + return FluidStack.EMPTY; + } + for (int tank = 0; tank < handler.getTanks(); tank++) { + FluidStack contained = handler.getFluidInTank(tank); + if (!contained.isEmpty()) { + return contained; + } + } + return FluidStack.EMPTY; + } + + /** 物品里装的化学品;化学品集成缺失或读不出来时返回 {@code null}。 */ + @Nullable + private static ChemicalStackView chemicalInItem(ItemStack stack) { + ChemicalCompatProvider provider = ChemicalCompatProviders.get(); + return provider.isAvailable() ? provider.chemicalInItem(stack) : null; + } +} diff --git a/src/main/java/com/sorrowmist/useless/content/stafflink/StaffLinkRoute.java b/src/main/java/com/sorrowmist/useless/content/stafflink/StaffLinkRoute.java new file mode 100644 index 00000000..7ca88258 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/stafflink/StaffLinkRoute.java @@ -0,0 +1,658 @@ +package com.sorrowmist.useless.content.stafflink; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.GlobalPos; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.ByteBufCodecs; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * 一条「锚点 × 线路」的搬运配置。 + * + *

{@code anchor} 是被绑定的容器坐标;{@code route} 是线路号,只有同一张网络里 + * 线路号相同、且一个 {@link LinkFlow#RELEASE} 一个 {@link LinkFlow#ABSORB}、 + * 资源类型相同的两条配置之间才会发生搬运。

+ * + *

构造器即校验点:所有数值都在此处夹取到合法区间,因此从存档、网络或界面传入的 + * 任何越界值都不会流进引擎。

+ */ +public record StaffLinkRoute( + GlobalPos anchor, + int route, + boolean enabled, + LinkFlow flow, + LinkMedium medium, + long amount, + int interval, + @Nullable Direction side, + int weight, + List filter +) { + /** 线路总数;线路号取值 {@code 0 .. ROUTE_COUNT - 1}。 */ + public static final int ROUTE_COUNT = 9; + /** + * 过滤器槽位数(18 格,3 行 × 6 列)。 + * + *

18 格不画在主界面上:主界面只留一个「过滤」按钮,点开才在覆盖层里编辑 + * (见 {@code StaffLinkScreen} 的过滤面板)。每格周围要留出「源端保留 / 接收端上限」 + * 两个输入框的位置,铺在主界面上会把面板撑得很高。

+ */ + public static final int FILTER_LIMIT = 18; + + /** + * 单次搬运量的下限。 + * + *

没有上限:一次要搬多少由玩家决定,大数值可以用 {@code K / M / G / T / P / E} + * 输入(界面走 {@code ScaledEnergyAmount})。真要去到容器的极限,那也是玩家自己的选择。

+ */ + public static final long MIN_AMOUNT = 1L; + + public static final int MIN_INTERVAL = 1; + public static final int MAX_INTERVAL = 1_200; + public static final int MIN_WEIGHT = -99; + public static final int MAX_WEIGHT = 99; + + /** + * 应力线路上 {@link #interval} 字段承载「方向」,取值 {@link #STRESS_CLOCKWISE} / + * {@link #STRESS_COUNTER_CLOCKWISE}。 + * + *

为什么复用而不是新加一个布尔字段:{@code amount} 与 {@code interval} 在别的介质上 + * 分别表示「一轮搬多少」与「多久一轮」,而转速与方向正是应力这种连续量对应的两样东西, + * 语义刚好对得上;复用可以让存档、网络编解码与界面控件都不必新增字段。 + * {@code interval} 的既有夹取区间 {@code [1, 1200]} 对这两个取值是安全的。

+ */ + public static final int STRESS_CLOCKWISE = 1; + + /** 应力线路上 {@link #interval} 字段表示逆时针。 */ + public static final int STRESS_COUNTER_CLOCKWISE = 2; + + /** + * 应力线路上 {@link #amount} 字段承载「目标转速(RPM)」时的默认值。 + * + *

取 256 是因为它是常见的最高档转速;真正可用的上限由桥在运行时按动力学配置夹取。

+ */ + public static final long STRESS_DEFAULT_RPM = 256L; + + /** + * 应力线路目标转速的**粗**上限,只用于服务端清洗客户端提交的值。 + * + *

真正的上限是动力学配置里的最高转速,那是可选集成侧才知道的事,因此这里先给一个 + * 绝对上界挡住离谱数值,再由桥按实际配置夹一次。

+ */ + public static final long STRESS_RPM_HARD_LIMIT = 1_000_000L; + + /** + * 气压线路上 {@link #amount} 字段承载「目标气压(毫巴)」。 + * + *

与应力复用 {@code amount} 承载「目标转速」同一个思路:{@code amount} 在别的介质上表示 + * 「一轮搬多少」,而气压的搬运目标本身就是「让接收端达到某个压力」,语义刚好对得上; + * 复用可以让存档、网络编解码都不必新增字段。

+ * + *

注意与 {@link #MIN_AMOUNT} 的冲突:真空是负压,因此气压线路的 {@code amount} + * 必须允许负数,构造器里对气压族单独夹取(见 {@link #StaffLinkRoute})。

+ */ + public static final long PRESSURE_MIN_MBAR = -1_000L; + + /** 气压线路上 {@link #amount} 的粗上限:20 bar。真正的物理上限由气动方块自身决定。 */ + public static final long PRESSURE_MAX_MBAR = 20_000L; + + /** 气压线路的默认目标气压:2 bar(气动工艺常见的起步工作压力)。 */ + public static final long PRESSURE_DEFAULT_MBAR = 2_000L; + + private static final String TAG_DIMENSION = "Dimension"; + private static final String TAG_POS = "Pos"; + private static final String TAG_ROUTE = "Route"; + private static final String TAG_ENABLED = "Enabled"; + private static final String TAG_FLOW = "Flow"; + private static final String TAG_MEDIUM = "Medium"; + private static final String TAG_AMOUNT = "Amount"; + private static final String TAG_INTERVAL = "Interval"; + private static final String TAG_SIDE = "Side"; + private static final String TAG_WEIGHT = "Weight"; + private static final String TAG_FILTER = "Filter"; + /** 一格标记里的物品(化学品线路的储罐也走这里)。 */ + private static final String TAG_MARKER_ITEM = "MarkerItem"; + /** 一格标记里的流体。 */ + private static final String TAG_MARKER_FLUID = "MarkerFluid"; + /** 一格标记里的模式文本({@code #tag} 或通配符)。 */ + private static final String TAG_MARKER_PATTERN = "MarkerPattern"; + /** 源端保留量;缺省 / 0 = 不保留。 */ + private static final String TAG_KEEP_AT_SOURCE = "KeepAtSource"; + /** 接收端上限;缺省 / 0 = 不限。 */ + private static final String TAG_MAX_INTO = "MaxInto"; + /** 这一格是「黑名单」;缺省 / false = 白名单。 */ + private static final String TAG_EXCLUDE = "Exclude"; + /** 输出端控制条件(compound,仅非 OFF 时写)。 */ + private static final String TAG_OUT_COND = "OutCond"; + /** 输入端控制条件(compound,仅非 OFF 时写)。 */ + private static final String TAG_IN_COND = "InCond"; + /** 条件内部:比较方向。 */ + private static final String TAG_COND_OP = "Op"; + /** 条件内部:控制材料物品。 */ + private static final String TAG_COND_ITEM = "Item"; + /** 条件内部:控制材料流体。 */ + private static final String TAG_COND_FLUID = "Fluid"; + /** 条件内部:阈值。 */ + private static final String TAG_COND_VALUE = "Value"; + + public StaffLinkRoute { + Objects.requireNonNull(anchor, "anchor"); + Objects.requireNonNull(flow, "flow"); + Objects.requireNonNull(medium, "medium"); + + route = Mth.clamp(route, 0, ROUTE_COUNT - 1); + // 气压的 amount 是「目标气压(毫巴)」,真空是负值,因此不能套用 MIN_AMOUNT 的下限。 + amount = medium.family() == ResourceFamily.PRESSURE + ? Math.max(PRESSURE_MIN_MBAR, Math.min(PRESSURE_MAX_MBAR, amount)) + : Math.max(MIN_AMOUNT, amount); + interval = Mth.clamp(interval, MIN_INTERVAL, MAX_INTERVAL); + weight = Mth.clamp(weight, MIN_WEIGHT, MAX_WEIGHT); + + List normalized = new ArrayList<>(FILTER_LIMIT); + for (int slot = 0; slot < FILTER_LIMIT; slot++) { + LinkFilterSlot value = filter != null && slot < filter.size() ? filter.get(slot) : null; + normalized.add(value == null ? LinkFilterSlot.EMPTY : value); + } + filter = List.copyOf(normalized); + } + + /** + * 过滤器适用的资源类型。 + * + *

标记物按线路类型分开存:物品线路放物品本身,流体线路放流体本身(不是装它的桶), + * 化学品线路放一只装有该化学品的储罐。能量、魔源与应力都没有合适的标记物, + * 因此不参与过滤。

+ */ + public boolean filterApplies() { + return switch (medium) { + case ITEM, AE_ITEM, FLUID, AE_FLUID, CHEMICAL, AE_CHEMICAL -> true; + // 能量、魔源、应力、气压没有合适的标记物,因此不参与过滤。 + case ENERGY, AE_ENERGY, SOURCE, AE_SOURCE, STRESS, PRESSURE, AE_PRESSURE -> false; + }; + } + + /** 非空的过滤标记;为空表示「不限制」。 */ + public List activeFilters() { + if (!filterApplies()) { + return List.of(); + } + List active = new ArrayList<>(filter.size()); + for (LinkFilterSlot slot : filter) { + if (!slot.isEmpty()) { + active.add(slot); + } + } + return active; + } + + /** 换一份过滤器,其余不变。 */ + public StaffLinkRoute withFilter(List newFilter) { + return new StaffLinkRoute(anchor, route, enabled, flow, medium, amount, interval, + side, weight, newFilter); + } + + /** + * 这条线路的过滤器里有没有「可用于当前资源类型」的白名单条目。 + * + *

注意与 {@link #activeFilters()} 的区别:那个只判「非空」,这个还要判「类型对得上」 + * ——一个只填了流体标记的物品线路,在 {@code activeFilters()} 眼里是「有过滤」, + * 在这里却是「没有有效白名单」(它一个物品也匹配不上)。

+ * + *

只为「源端是 ME 网络」那条前置判定服务(见 + * {@code StaffLinkTargets#transfer}):从网络里拿什么完全由接收端白名单决定, + * 没有白名单就什么都不拿。所以这里的判据必须严格,否则「填了张不相干的格子」 + * 会被当成「配过了」而放行全网络。

+ * + *
    + *
  • 模式({@code #tag} / 通配符)恒算命中——它本来就按类型无关的方式匹配, + * 能不能匹配到具体资源静态判不了,交给运行时的谓词。
  • + *
  • 物品族看 {@link LinkFilterSlot#isItem()},流体族看 {@link LinkFilterSlot#isFluid()}, + * 化学品看物品标记(化学品只有「储罐物品」这一种表示)。
  • + *
  • 能量 / 魔源 / 应力不参与过滤({@link #filterApplies()} 为 {@code false}),恒 {@code false}。
  • + *
+ */ + public boolean hasApplicableWhitelist() { + if (!filterApplies()) { + return false; + } + // 「白名单」只数**非黑名单**条目:黑名单格不构成「允许清单」。 + return switch (medium.family()) { + case ITEM -> hasMarker(slot -> !slot.exclude() && (slot.isItem() || slot.isPattern())); + case FLUID -> hasMarker(slot -> !slot.exclude() && (slot.isFluid() || slot.isPattern())); + case CHEMICAL -> hasMarker(slot -> !slot.exclude() && (slot.isItem() || slot.isPattern())); + case ENERGY, SOURCE, STRESS, PRESSURE -> false; + }; + } + + /** + * 这条线路的过滤器里有没有任何「可用于当前资源类型」的条目——白名单或黑名单都算。 + * + *

与 {@link #hasApplicableWhitelist()} 的差别就在「纯黑名单」这种情况:只填了一个黑名单格时 + * 白名单判定为 false,但这里为 true(玩家确实配置了「除了 X 都搬」)。

+ */ + public boolean hasApplicableFilter() { + if (!filterApplies()) { + return false; + } + return switch (medium.family()) { + case ITEM -> hasMarker(slot -> slot.isItem() || slot.isPattern()); + case FLUID -> hasMarker(slot -> slot.isFluid() || slot.isPattern()); + case CHEMICAL -> hasMarker(slot -> slot.isItem() || slot.isPattern()); + case ENERGY, SOURCE, STRESS, PRESSURE -> false; + }; + } + + /** + * 「源端是 ME 网络、这条线路又拿不出白名单」是否应当整条线路一个也不搬。 + * + *

{@link #hasApplicableWhitelist()} 判的是「有没有白名单」,这里判的是「需不需要 + * 白名单」——两者的差就在能量与魔源上。

+ * + *

「没有白名单就一个也不搬」这条规则的由来是:源端是 ME 网络时,拿什么完全由接收端白名单 + * 决定,白名单为空就退化成「把整张网络灌进这个容器」——而网络里可能有几千种东西。 + * 但这条理由只对「网络里能装多种资源」的族成立:

+ * + *
    + *
  • 物品 / 流体 / 化学品:网络里能有上千种 ⇒ 需要白名单。
  • + *
  • 能量 / 魔源:从网络里出来就一种(FE / 魔源),「灌满」根本无从谈起; + * 而且它们没有合适的标记物({@link #filterApplies()} 恒 {@code false}, + * 界面压根不给填)。对它们套这条规则等于把整条线路锁死—— + * 玩家既搬不动、也无处可填来解锁。
  • + *
+ * + *

因此判据是「本族吃过滤器」∧「没填出有效白名单」。界面那条同样的警告也用它, + * 免得能量 / 魔源线路一边能搬一边报错。

+ */ + public boolean blocksAeSourceWithoutWhitelist() { + // 判据用 hasApplicableFilter(白名单或黑名单都算):黑名单(纯黑名单)在 AE 源上是 + // 「除了列出的都搬」,语义明确,不该被当成「没填过滤」而整条锁死。 + // 只有「完全没填任何本族条目」才按老规矩挡下(防误灌整网)。 + return filterApplies() && !hasApplicableFilter(); + } + + private boolean hasMarker(java.util.function.Predicate predicate) { + for (LinkFilterSlot slot : filter) { + if (!slot.isEmpty() && predicate.test(slot)) { + return true; + } + } + return false; + } + + // ------------------------------------------------------------------ 持久化 + + public CompoundTag save(HolderLookup.Provider registries) { + CompoundTag tag = new CompoundTag(); + tag.putString(TAG_DIMENSION, anchor.dimension().location().toString()); + tag.putLong(TAG_POS, anchor.pos().asLong()); + tag.putInt(TAG_ROUTE, route); + tag.putBoolean(TAG_ENABLED, enabled); + tag.putString(TAG_FLOW, flow.name()); + tag.putString(TAG_MEDIUM, medium.name()); + tag.putLong(TAG_AMOUNT, amount); + tag.putInt(TAG_INTERVAL, interval); + tag.putString(TAG_SIDE, side == null ? "" : side.getName()); + tag.putInt(TAG_WEIGHT, weight); + + ListTag filterTag = new ListTag(); + for (LinkFilterSlot slot : filter) { + CompoundTag entry = new CompoundTag(); + // 空槽位写空 tag:saveOptional 对空栈返回空 CompoundTag,parseOptional 会还原成 EMPTY。 + entry.put(TAG_MARKER_ITEM, com.sorrowmist.useless.compat.forge1201.nbt.NativeStackNbt.saveItem(slot.item(), registries)); + if (!slot.fluid().isEmpty()) { + entry.put(TAG_MARKER_FLUID, com.sorrowmist.useless.compat.forge1201.nbt.NativeStackNbt.saveFluid(slot.fluid(), registries)); + } + if (slot.pattern() != null) { + entry.putString(TAG_MARKER_PATTERN, slot.pattern()); + } + // 只在非 0(= 有限制)时写,让「老格式读出来是 0」与「没限制」天然同构。 + // 这是**向后投影**:只覆盖「输出端 ≥ self」/「输入端 ≤ self」这两种最普通的条件, + // 好让退回旧版本时那两种仍能读出来。 + if (slot.keepAtSource() > 0L) { + entry.putLong(TAG_KEEP_AT_SOURCE, slot.keepAtSource()); + } + if (slot.maxInto() > 0L) { + entry.putLong(TAG_MAX_INTO, slot.maxInto()); + } + if (slot.exclude()) { + entry.putBoolean(TAG_EXCLUDE, true); + } + // 完整条件:仅非 OFF 时写(OFF 是默认,老存档与新存档的空条件都不会有这两个键)。 + if (!slot.outCond().isOff()) { + entry.put(TAG_OUT_COND, saveCondition(slot.outCond(), registries)); + } + if (!slot.inCond().isOff()) { + entry.put(TAG_IN_COND, saveCondition(slot.inCond(), registries)); + } + filterTag.add(entry); + } + tag.put(TAG_FILTER, filterTag); + return tag; + } + + /** 读回一条配置;维度或坐标不合法时返回 {@code null}(调用方跳过该条)。 */ + @Nullable + public static StaffLinkRoute load(CompoundTag tag, HolderLookup.Provider registries) { + ResourceLocation dimensionId = ResourceLocation.tryParse(tag.getString(TAG_DIMENSION)); + if (dimensionId == null) { + return null; + } + GlobalPos anchor = GlobalPos.of( + ResourceKey.create(Registries.DIMENSION, dimensionId), + BlockPos.of(tag.getLong(TAG_POS))); + + // 资源类型要先读:旧格式的过滤器迁移要知道这条线路搬的是什么。 + LinkMedium medium = readEnum(LinkMedium.class, tag.getString(TAG_MEDIUM), LinkMedium.ITEM); + + List filter = new ArrayList<>(FILTER_LIMIT); + ListTag filterTag = tag.getList(TAG_FILTER, Tag.TAG_COMPOUND); + for (int slot = 0; slot < FILTER_LIMIT; slot++) { + filter.add(slot < filterTag.size() + ? readFilterSlot(filterTag.getCompound(slot), medium, registries) + : LinkFilterSlot.EMPTY); + } + + return new StaffLinkRoute( + anchor, + tag.getInt(TAG_ROUTE), + tag.getBoolean(TAG_ENABLED), + readEnum(LinkFlow.class, tag.getString(TAG_FLOW), LinkFlow.ABSORB), + medium, + readAmount(tag), + tag.getInt(TAG_INTERVAL), + readSide(tag.getString(TAG_SIDE)), + tag.getInt(TAG_WEIGHT), + filter); + } + + /** + * 读一格过滤标记。 + * + *

兼容旧格式。早先这一格就是一个 ItemStack(流体线路存的是「装它的桶」), + * 整个 CompoundTag 只有 {@code id}/{@code count}/{@code components} 这几个键。这里用 + * 「有没有新格式的标记键」来区分:没有就按旧格式读,并且对流体线路顺手把桶里的流体取出来 + * ——否则升级之后玩家配好的流体过滤会整片失效(标记全成了物品,一个流体也匹配不上)。

+ * + *

条件的两级回退:优先读 {@code OutCond}/{@code InCond};没有时用旧键 + * {@code KeepAtSource}/{@code MaxInto} 合成 {@code 输出端 ≥ self} / {@code 输入端 ≤ self}, + * 于是老存档读出来与改动前逐字节一致。

+ */ + private static LinkFilterSlot readFilterSlot(CompoundTag entry, LinkMedium medium, + HolderLookup.Provider registries) { + LinkFilterSlot base = readFilterMarker(entry, medium, registries); + if (base.isEmpty()) { + return LinkFilterSlot.EMPTY; + } + boolean exclude = entry.getBoolean(TAG_EXCLUDE); + LinkFilterCondition out = entry.contains(TAG_OUT_COND) + ? readCondition(entry.getCompound(TAG_OUT_COND), registries) + : legacyCondition(entry.getLong(TAG_KEEP_AT_SOURCE), LinkFilterCondition.Op.AT_LEAST); + LinkFilterCondition in = entry.contains(TAG_IN_COND) + ? readCondition(entry.getCompound(TAG_IN_COND), registries) + : legacyCondition(entry.getLong(TAG_MAX_INTO), LinkFilterCondition.Op.AT_MOST); + return base.withExclude(exclude).withConditions(out, in); + } + + /** 老键({@code KeepAtSource} / {@code MaxInto})到条件的映射;{@code 0} 表示没有这条条件。 */ + private static LinkFilterCondition legacyCondition(long value, LinkFilterCondition.Op op) { + return value > 0L ? LinkFilterCondition.self(op, value) : LinkFilterCondition.OFF; + } + + /** + * 只读标记(物品 / 流体 / 模式),不含白名单/黑名单方向与条件。 + * + *

读不出标记(空键 / 非法模式 / 空栈)时返回 {@link LinkFilterSlot#EMPTY}。

+ */ + private static LinkFilterSlot readFilterMarker(CompoundTag entry, LinkMedium medium, + HolderLookup.Provider registries) { + if (entry.contains(TAG_MARKER_ITEM) || entry.contains(TAG_MARKER_FLUID) + || entry.contains(TAG_MARKER_PATTERN)) { + // 模式优先:三态互斥,构造器会收敛。 + if (entry.contains(TAG_MARKER_PATTERN)) { + LinkFilterPattern pattern = LinkFilterPattern.parse(entry.getString(TAG_MARKER_PATTERN)); + // 存档里是个非法模式(改坏了 / 旧版本写进来的):当作空槽,不猜。 + return pattern == null ? LinkFilterSlot.EMPTY : LinkFilterSlot.ofPattern(pattern); + } + ItemStack item = com.sorrowmist.useless.compat.forge1201.nbt.NativeStackNbt.readItem(registries, entry.getCompound(TAG_MARKER_ITEM)); + FluidStack fluid = entry.contains(TAG_MARKER_FLUID) + ? com.sorrowmist.useless.compat.forge1201.nbt.NativeStackNbt.readFluid(registries, entry.getCompound(TAG_MARKER_FLUID)) + : FluidStack.EMPTY; + return fluid.isEmpty() ? LinkFilterSlot.ofItem(item) : LinkFilterSlot.ofFluid(fluid); + } + + ItemStack legacy = com.sorrowmist.useless.compat.forge1201.nbt.NativeStackNbt.readItem(registries, entry); + if (legacy.isEmpty()) { + return LinkFilterSlot.EMPTY; + } + if (medium.family() == ResourceFamily.FLUID) { + FluidStack fluid = StaffLinkFilters.fluidInItem(legacy); + return fluid.isEmpty() ? LinkFilterSlot.EMPTY : LinkFilterSlot.ofFluid(fluid); + } + return LinkFilterSlot.ofItem(legacy); + } + + /** 写一条控制条件;只对非 OFF 调用。 */ + private static CompoundTag saveCondition(LinkFilterCondition cond, HolderLookup.Provider registries) { + CompoundTag tag = new CompoundTag(); + tag.putString(TAG_COND_OP, cond.op().name()); + if (!cond.item().isEmpty()) { + tag.put(TAG_COND_ITEM, com.sorrowmist.useless.compat.forge1201.nbt.NativeStackNbt.saveItem(cond.item(), registries)); + } + if (!cond.fluid().isEmpty()) { + tag.put(TAG_COND_FLUID, com.sorrowmist.useless.compat.forge1201.nbt.NativeStackNbt.saveFluid(cond.fluid(), registries)); + } + tag.putLong(TAG_COND_VALUE, cond.value()); + return tag; + } + + /** 读一条控制条件;方向未知 / 缺失一律当 OFF(延续「未知 → 不抛」的约定)。 */ + private static LinkFilterCondition readCondition(CompoundTag tag, HolderLookup.Provider registries) { + LinkFilterCondition.Op op = readEnum(LinkFilterCondition.Op.class, + tag.getString(TAG_COND_OP), LinkFilterCondition.Op.OFF); + if (op == LinkFilterCondition.Op.OFF) { + return LinkFilterCondition.OFF; + } + ItemStack item = tag.contains(TAG_COND_ITEM) + ? com.sorrowmist.useless.compat.forge1201.nbt.NativeStackNbt.readItem(registries, tag.getCompound(TAG_COND_ITEM)) : ItemStack.EMPTY; + FluidStack fluid = tag.contains(TAG_COND_FLUID) + ? com.sorrowmist.useless.compat.forge1201.nbt.NativeStackNbt.readFluid(registries, tag.getCompound(TAG_COND_FLUID)) : FluidStack.EMPTY; + return new LinkFilterCondition(op, item, fluid, tag.getLong(TAG_COND_VALUE)); + } + + /** 读单次搬运量;老存档里存的是 TAG_Int,两种都要认,否则会读成 0。 */ + private static long readAmount(CompoundTag tag) { + return tag.contains(TAG_AMOUNT, Tag.TAG_LONG) + ? tag.getLong(TAG_AMOUNT) : tag.getInt(TAG_AMOUNT); + } + + private static > E readEnum(Class type, String name, E fallback) { + for (E value : type.getEnumConstants()) { + if (value.name().equals(name)) { + return value; + } + } + return fallback; + } + + @Nullable + private static Direction readSide(String name) { + return name == null || name.isEmpty() ? null : Direction.byName(name); + } + + // ------------------------------------------------------------------ 网络同步 + + /** + * 一格过滤标记的编解码。 + * + *

先用一个状态字节说清这格是哪一种形态,再按对应类型写。状态值手写 + * ({@code 0=物品 / 1=流体 / 2=模式})而刻意不用 {@code writeEnum}:枚举按 ordinal + * 编码,将来往 {@link LinkFilterSlot} 里插入一个形态就会把旧包解成别的意思。 + * 手写常量则新形态只可能是「未知值」,能显式处理。

+ * + *

旧投影的两个数量限制({@code keep} / {@code max})仍然写一遍,让读端能对齐; + * 随后才是真正的 {@code exclude} 与两条控制条件。条件里 {@code op}/{@code control} + * 同样手写字节,且无论 OFF 与否都写满三个字段——这样未知 op 也能安全跳过, + * 不会把后面的流读错位。

+ */ + private static final byte SLOT_STATE_ITEM = 0; + private static final byte SLOT_STATE_FLUID = 1; + private static final byte SLOT_STATE_PATTERN = 2; + + private static final byte COND_OP_OFF = 0; + private static final byte COND_OP_AT_LEAST = 1; + private static final byte COND_OP_AT_MOST = 2; + + private static final byte COND_CONTROL_SELF = 0; + private static final byte COND_CONTROL_ITEM = 1; + private static final byte COND_CONTROL_FLUID = 2; + + private static final StreamCodec FILTER_SLOT_CODEC = + StreamCodec.of( + (buf, slot) -> { + if (slot.isPattern()) { + buf.writeByte(SLOT_STATE_PATTERN); + buf.writeUtf(slot.pattern(), LinkFilterPattern.MAX_LENGTH); + } else if (slot.isFluid()) { + buf.writeByte(SLOT_STATE_FLUID); + com.sorrowmist.useless.compat.forge1201.network.PortCodecs.FLUID_STACK.encode(buf, slot.fluid()); + } else { + buf.writeByte(SLOT_STATE_ITEM); + com.sorrowmist.useless.compat.forge1201.network.PortCodecs.ITEM_STACK_OPTIONAL.encode(buf, slot.item()); + } + // 旧投影:仅用于与旧版对齐读取,读端不采用。 + buf.writeVarLong(slot.keepAtSource()); + buf.writeVarLong(slot.maxInto()); + buf.writeBoolean(slot.exclude()); + writeCondition(buf, slot.outCond()); + writeCondition(buf, slot.inCond()); + }, + buf -> { + byte state = buf.readByte(); + LinkFilterSlot slot = switch (state) { + case SLOT_STATE_PATTERN -> LinkFilterSlot.ofPattern(buf.readUtf(LinkFilterPattern.MAX_LENGTH)); + case SLOT_STATE_FLUID -> LinkFilterSlot.ofFluid(com.sorrowmist.useless.compat.forge1201.network.PortCodecs.FLUID_STACK.decode(buf)); + case SLOT_STATE_ITEM -> LinkFilterSlot.ofItem(com.sorrowmist.useless.compat.forge1201.network.PortCodecs.ITEM_STACK_OPTIONAL.decode(buf)); + // 未知状态(装了带新形态的版本又退回旧版本):读掉后面的字段当空槽, + // 而不是抛异常——一个解不开的过滤器不该让整个界面同步失败。 + default -> LinkFilterSlot.EMPTY; + }; + buf.readVarLong(); + buf.readVarLong(); + boolean exclude = buf.readBoolean(); + LinkFilterCondition out = readCondition(buf); + LinkFilterCondition in = readCondition(buf); + return slot.isEmpty() + ? LinkFilterSlot.EMPTY + : slot.withExclude(exclude).withConditions(out, in); + }); + + /** 写一条控制条件;OFF 也写满(op + control + value),保证读端能安全对齐。 */ + private static void writeCondition(FriendlyByteBuf buf, LinkFilterCondition cond) { + switch (cond.op()) { + case OFF -> buf.writeByte(COND_OP_OFF); + case AT_LEAST -> buf.writeByte(COND_OP_AT_LEAST); + case AT_MOST -> buf.writeByte(COND_OP_AT_MOST); + } + if (cond.isFluidControl()) { + buf.writeByte(COND_CONTROL_FLUID); + com.sorrowmist.useless.compat.forge1201.network.PortCodecs.FLUID_STACK.encode(buf, cond.fluid()); + } else if (cond.isItemControl()) { + buf.writeByte(COND_CONTROL_ITEM); + com.sorrowmist.useless.compat.forge1201.network.PortCodecs.ITEM_STACK_OPTIONAL.encode(buf, cond.item()); + } else { + buf.writeByte(COND_CONTROL_SELF); + } + buf.writeVarLong(cond.value()); + } + + /** 读一条控制条件;未知 op / control 一律收敛成「不启用」,不抛。 */ + private static LinkFilterCondition readCondition(FriendlyByteBuf buf) { + byte opByte = buf.readByte(); + byte control = buf.readByte(); + ItemStack item = ItemStack.EMPTY; + FluidStack fluid = FluidStack.EMPTY; + if (control == COND_CONTROL_ITEM) { + item = com.sorrowmist.useless.compat.forge1201.network.PortCodecs.ITEM_STACK_OPTIONAL.decode(buf); + } else if (control == COND_CONTROL_FLUID) { + fluid = com.sorrowmist.useless.compat.forge1201.network.PortCodecs.FLUID_STACK.decode(buf); + } + long value = buf.readVarLong(); + LinkFilterCondition.Op op = switch (opByte) { + case COND_OP_AT_LEAST -> LinkFilterCondition.Op.AT_LEAST; + case COND_OP_AT_MOST -> LinkFilterCondition.Op.AT_MOST; + default -> LinkFilterCondition.Op.OFF; + }; + return op == LinkFilterCondition.Op.OFF + ? LinkFilterCondition.OFF + : new LinkFilterCondition(op, item, fluid, value); + } + + private static final StreamCodec> FILTER_CODEC = + FILTER_SLOT_CODEC.apply(ByteBufCodecs.list(FILTER_LIMIT)); + + /** 锚点(维度 + 坐标)的网络编解码;手写以避开 {@code com.sorrowmist.useless.compat.forge1201.network.PortCodecs.GLOBAL_POS} 的泛型歧义。 */ + public static void writeAnchor(FriendlyByteBuf buf, GlobalPos anchor) { + buf.writeResourceLocation(anchor.dimension().location()); + buf.writeBlockPos(anchor.pos()); + } + + public static GlobalPos readAnchor(FriendlyByteBuf buf) { + return GlobalPos.of( + ResourceKey.create(Registries.DIMENSION, buf.readResourceLocation()), + buf.readBlockPos()); + } + + /** + * 网络编解码。 + * + *

过滤器里的物品与流体都依赖 {@link FriendlyByteBuf} 携带的注册表 + * ({@code OPTIONAL_STREAM_CODEC} / {@code STREAM_CODEC}),所以整条编解码器也按该类型声明。 + * 存档侧则走 {@code saveOptional}/{@code parseOptional} 与 {@code HolderLookup.Provider}。

+ */ + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, route) -> { + writeAnchor(buf, route.anchor()); + buf.writeVarInt(route.route()); + buf.writeBoolean(route.enabled()); + buf.writeEnum(route.flow()); + buf.writeEnum(route.medium()); + buf.writeVarLong(route.amount()); + buf.writeVarInt(route.interval()); + buf.writeBoolean(route.side() != null); + if (route.side() != null) { + buf.writeEnum(route.side()); + } + buf.writeVarInt(route.weight()); + FILTER_CODEC.encode(buf, route.filter()); + }, + buf -> { + GlobalPos anchor = readAnchor(buf); + int route = buf.readVarInt(); + boolean enabled = buf.readBoolean(); + LinkFlow flow = buf.readEnum(LinkFlow.class); + LinkMedium medium = buf.readEnum(LinkMedium.class); + long amount = buf.readVarLong(); + int interval = buf.readVarInt(); + Direction side = buf.readBoolean() ? buf.readEnum(Direction.class) : null; + int weight = buf.readVarInt(); + List filter = FILTER_CODEC.decode(buf); + return new StaffLinkRoute(anchor, route, enabled, flow, medium, amount, interval, + side, weight, filter); + }); +} diff --git a/src/main/java/com/sorrowmist/useless/content/stafflink/StaffLinkStressBridge.java b/src/main/java/com/sorrowmist/useless/content/stafflink/StaffLinkStressBridge.java new file mode 100644 index 00000000..5fc339ed --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/stafflink/StaffLinkStressBridge.java @@ -0,0 +1,63 @@ +package com.sorrowmist.useless.content.stafflink; + +import net.minecraft.core.BlockPos; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.UUID; + +/** + * 应力搬运桥。 + * + *

刻意不出现任何外部模组的类型。{@link StaffLinkTargets} 与 + * {@code StaffLinkEngine} 只依赖这个接口,真正引用动力学方块实体类型的实现类由 + * {@code CreateStressCompatLoader} 在检测到对应模组后才反射加载。

+ * + *

为什么它不像别的介质那样只交出「端点」。物品、流体、能量都是「一轮搬多少」的 + * 流量语义:一端抽出、另一端收下,逐对搬运就够。转速与应力容量却是整张网络的连续状态量—— + * 同一个网络上所有机器共享一个转速,输出端能拿到多少容量取决于输入端网络的富余量, + * 还要在多个输出之间按权重分配。这些都必须拿到「同一条线路上全部输入与全部输出」才算得出来, + * 因此这里暴露的是「跑一整条线路」而不是「搬一对端点」。

+ */ +public interface StaffLinkStressBridge { + + /** + * 把该坐标的方块包成应力端点。 + * + *

返回的对象对常驻代码是不透明的:调用方只拿它做 {@code null} 判断 + * (决定这个坐标能不能绑成应力锚点)。

+ * + * @return 该坐标不是动力学方块时返回 {@code null} + */ + @Nullable + Object resolveEndpoint(Level level, BlockPos pos); + + /** + * 跑一整条线路:读输入端的转速与富余应力,驱动输出端,并把负载回馈给输入端网络。 + * + *

由引擎每 tick 调用一次(不走周期与退避):驱动关系必须持续维持, + * 一旦停手,输出端的转速会随时间失效。

+ * + * @param releases 该线路上全部启用的释放端(输入) + * @param absorbs 该线路上全部启用的吸收端(输出) + */ + void applyRoute(MinecraftServer server, UUID networkId, int routeIndex, + List releases, List absorbs); + + /** + * 清掉已经过期的「驱动认领」。 + * + *

必须每 tick 调用,不能并进低频任务:桥停止驱动之后,输出端方块身上还留着 + * 上一轮的虚拟转速,只有这里的过期判定能把它摘掉。迟一个周期,玩家就会看到 + * 「源已经停了,目标还在转」。

+ */ + void sweep(MinecraftServer server); + + /** 把运行状态(可用应力 / 需求 / 每个端点的状态与原因)下发给正在看界面的玩家。 */ + void syncStatus(MinecraftServer server); + + /** 服务器停止时清掉运行时状态,别把上一局的 tick 数带进下一局。 */ + void clearRuntimeState(); +} diff --git a/src/main/java/com/sorrowmist/useless/content/stafflink/StaffLinkTargets.java b/src/main/java/com/sorrowmist/useless/content/stafflink/StaffLinkTargets.java new file mode 100644 index 00000000..3091929c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/content/stafflink/StaffLinkTargets.java @@ -0,0 +1,1423 @@ +package com.sorrowmist.useless.content.stafflink; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.api.logistics.LongEnergyHandler; +import com.sorrowmist.useless.api.logistics.LongFluidHandler; +import com.sorrowmist.useless.api.logistics.LongItemHandler; +import com.sorrowmist.useless.compat.ae.AeChemicalCompatLoader; +import com.sorrowmist.useless.compat.ae.AeEnergyCompatLoader; +import com.sorrowmist.useless.compat.ae.AeGenericInvCompatLoader; +import com.sorrowmist.useless.compat.ae.AeLogisticsCompatLoader; +import com.sorrowmist.useless.compat.ae.AePressureCompatLoader; +import com.sorrowmist.useless.compat.ae.AeSourceCompatLoader; + +import com.sorrowmist.useless.compat.create.CreateStressCompatLoader; + + +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalCompatProvider; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalCompatProviders; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalHandlerView; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.chemical.ChemicalStackView; +import com.sorrowmist.useless.energy.IEnergyManager; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +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.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraftforge.common.capabilities.ForgeCapabilities; +import net.minecraftforge.energy.IEnergyStorage; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +/** + * 造化杖无线物流的「容器能力」解析与搬运实现。 + * + *

全链路 long 语义。 搬运不再经过 MinecraftForge 原生的 int 槽位接口,而是统一走 + * {@code com.sorrowmist.useless.api.logistics} 的 long 级契约:一次请求要搬多少就是多少, + * 由处理器内部决定怎么把它凑出来。超 {@link Integer#MAX_VALUE} 的运输因此不再需要外层 + * 反复循环补足。

+ * + *

本模组自己的 long 能力原生长途直通。 能量优先识别 {@link IEnergyManager}:它是 + * long 契约,一次调用直接搬走全部请求量,不存在任何 int 中转,也没有补循环。

+ * + *

能量还有两个额外来源:方块侧兼容 Modern Industrialization 的 EU(按 MI 自己的 + * {@code forgeEnergyPerEu} 换算成 FE,且不查电压等级),网络侧兼容 AppliedFlux 存进 ME 网络的 + * 通量 FE(即 {@link LinkMedium#AE_ENERGY})。两者都被归一成同一个 {@link LongEnergyHandler} + * 契约,所以搬运与诊断对它们一视同仁。

+ * + *

物品 / 流体的底层容器多数仍是 int 槽位,因此由 {@link LongResourceAdapters} 在内部 + * 分片——分片细节封在适配器里,调用方(也就是本类)只看得到 long。

+ * + *

化学品经本模组已有的 {@link ChemicalCompatProviders} 抽象层(本就是 long 计数,因此 + * 无需包装);魔源经 {@link ArsSourceCompatLoader} 提供的端点(Ars Nouveau 自身是 int 契约, + * 上限处夹取,魔源的量级远到不了 int 边界)。

+ * + *

每种资源都有「方块容器」与「AE 网络」两种承载。 两者在解析阶段被归一成同一个 + * 端点契约,于是搬运与诊断对它们一视同仁,配对时也只需比较 {@link ResourceFamily}。

+ * + *

所有解析都会先确认区块已加载,绝不触发强制加载。

+ */ +public final class StaffLinkTargets { + private static final Direction[] DIRECTIONS = Direction.values(); + + /** + * 绑定新锚点时挑选默认资源类型的优先级。 + * + *

AE 类型排在最后:它们只有在方块本身不是容器时才会被选中,因此把一个 + * 无线访问点当作普通箱子绑定也没问题——普通容器先命中,访问点则落到 AE 上。

+ */ + private static final LinkMedium[] DEFAULT_ORDER = { + LinkMedium.ITEM, LinkMedium.FLUID, LinkMedium.ENERGY, LinkMedium.CHEMICAL, + LinkMedium.SOURCE, LinkMedium.AE_ITEM, LinkMedium.AE_FLUID, + LinkMedium.AE_CHEMICAL, LinkMedium.AE_SOURCE, LinkMedium.AE_ENERGY, + // 应力排在最后:它只对动力学方块解析得出,而动力学方块永远不是容器, + // 所以放在哪一位都不会抢走别的介质;放最后纯粹是让「默认介质」的语义保持好读。 + LinkMedium.STRESS, + // 气压同样排最后:它只对气动方块 / AE 网络解析得出,而气动方块通常也带物品能力 + // (升级槽),排最后才不会把「绑定一台气动机器」默认判成物品线路。 + LinkMedium.PRESSURE, LinkMedium.AE_PRESSURE + }; + + private StaffLinkTargets() { + } + + // ------------------------------------------------------------------ 解析 + + /** 该坐标是不是「有东西可搬」的容器——决定潜行右键要不要接管。 */ + public static boolean isBindable(Level level, BlockPos pos) { + if (level == null || pos == null || !level.isLoaded(pos)) { + return false; + } + for (LinkMedium medium : DEFAULT_ORDER) { + if (medium.isSupported() && resolve(level, pos, null, medium) != null) { + return true; + } + } + return false; + } + + /** + * 该坐标上的锚点方块是否还在——自愈逻辑唯一的删除判据。 + * + *

刻意不碰资源能力。 「解析不出当前介质」不等于「方块没了」:一个 ME 接口在 + * 资源类型选成流体、化学品或魔源时照样解析不出端点,可它明明还在那里。若拿解析结果 + * 当存活判据,玩家刚配好的线会在下一个自愈周期被整条删掉。搬运失败有界面提示, + * 条件恢复后自动继续,不需要删配置。

+ * + *

因此这里只看方块本身:空气才算「没了」。被换成别的方块时保留配置——玩家可能只是 + * 中途替换,重新放回去就能继续用;要彻底解绑有手动解绑。

+ */ + public static boolean anchorPresent(Level level, BlockPos pos) { + if (level == null || pos == null || !level.isLoaded(pos)) { + return false; + } + return !level.getBlockState(pos).isAir(); + } + + /** 该坐标最适合的默认资源类型;什么都解析不出来时返回 {@code null}。 */ + @Nullable + public static LinkMedium defaultMediumFor(Level level, BlockPos pos) { + for (LinkMedium medium : DEFAULT_ORDER) { + if (medium.isSupported() && resolve(level, pos, null, medium) != null) { + return medium; + } + } + return null; + } + + /** + * 解析目标容器在指定资源类型下的处理器。 + * + * @return {@link LongItemHandler} / {@link LongFluidHandler} / {@link LongEnergyHandler} / + * {@code ChemicalHandlerView} / {@link SourceBridge};不可用返回 {@code null} + */ + @Nullable + public static Object resolve(Level level, BlockPos pos, @Nullable Direction side, LinkMedium medium) { + if (level == null || pos == null || !level.isLoaded(pos)) { + return null; + } + return switch (medium) { + case ITEM -> genericItems(level, pos, side); + case FLUID -> genericFluids(level, pos, side); + case ENERGY -> energyHandler(level, pos, side, medium); + case CHEMICAL -> chemicalHandler(level, pos, side); + case SOURCE -> com.sorrowmist.useless.compat.forge1201.OptionalIntegration.call("ars_nouveau","com.sorrowmist.useless.compat.ars.ArsSourceCompatLoader","sourceHandler",Object.class,null,new Class[]{Level.class,BlockPos.class},level, pos); + // AE 这一端交给桥去解析:常驻代码只拿到一个 long 契约的端点, + // 不出现任何 AE2 类型,因此没装 AE2 时这段代码照常加载、只是永远解析不出结果。 + case AE_ITEM -> AeLogisticsCompatLoader.itemEndpoint(level, pos); + case AE_FLUID -> AeLogisticsCompatLoader.fluidEndpoint(level, pos); + // 化学品/魔源/能量要进 ME 网络得靠各自的附属(appmek / arseng / appflux), + // 所以走独立的桥。 + case AE_CHEMICAL -> AeChemicalCompatLoader.chemicalEndpoint(level, pos); + case AE_SOURCE -> AeSourceCompatLoader.sourceEndpoint(level, pos); + case AE_ENERGY -> AeEnergyCompatLoader.energyEndpoint(level, pos); + // 应力没有「能力」可查:它的端点是动力学方块实体本身,交给桥去判定。 + // 常驻代码只拿到一个不透明的对象做 null 判断,因此没装对应模组时这段照常加载。 + case STRESS -> CreateStressCompatLoader.resolveEndpoint(level, pos); + // 气压:方块侧走气动工艺的空气处理器,网络侧走 Applied Pneumatics 存进 ME 网络的空气。 + case PRESSURE -> com.sorrowmist.useless.compat.forge1201.OptionalIntegration.call("pneumaticcraft","com.sorrowmist.useless.compat.pneumaticcraft.PneumaticCraftPressureCompatLoader","pressureEndpoint",Object.class,null,new Class[]{Level.class,BlockPos.class,Direction.class},level, pos, side); + case AE_PRESSURE -> AePressureCompatLoader.pressureEndpoint(level, pos); + }; + } + + @Nullable + private static LongItemHandler items(@Nullable net.minecraftforge.items.IItemHandler handler) { + return handler == null ? null : LongResourceAdapters.items(handler); + } + + @Nullable + private static LongFluidHandler fluids(@Nullable net.minecraftforge.fluids.capability.IFluidHandler handler) { + return handler == null ? null : LongResourceAdapters.fluids(handler); + } + + /** + * 物品端点:优先走 AE2 的「局部通用库存」,探不到再退回原生 {@code IItemHandler}。 + * + *

ME 接口这类方块同时有两条路可达:AE2 为它们额外注册了 {@code ItemHandler.BLOCK} + * (把 generic 库存投影成 {@code GenericStackItemStorage}),于是原先走的是投影。直接走 + * generic 能省掉「每次读数量 / 抽取都要 {@code AEKey → ItemStack} 物化」的那一大笔 + * (见内部无线物流性能分析报告第十三节)。

+ * + *

两条路谁赢结果一样:投影本就是 generic 的纯视图,且没有方块会同时是「真容器」 + * 和「generic 库存」({@code AEBaseInvBlockEntity} 的子类如驱动器/箱子只注册后者中的 + * {@code ItemHandler.BLOCK},不注册 generic)。

+ * + *

不复用 {@link #capability}:generic 的注册 lambda 忽略 context,任何面都返回同一个 + * 库存对象,一次 {@code getCapability} 就够;复用那套「先试记忆面、再试 6 个方向」是白付, + * 还会污染 {@link #CAPABILITY_HINTS}。

+ */ + @Nullable + private static LongItemHandler genericItems(Level level, BlockPos pos, @Nullable Direction side) { + LongItemHandler generic = AeGenericInvCompatLoader.itemEndpoint(level, pos, side); + return generic != null + ? generic + : items(capability(level, ForgeCapabilities.ITEM_HANDLER, pos, side, LinkMedium.ITEM)); + } + + /** 流体端点:优先 AE2 局部通用库存,退回原生 {@code IFluidHandler}。理由同上。 */ + @Nullable + private static LongFluidHandler genericFluids(Level level, BlockPos pos, @Nullable Direction side) { + LongFluidHandler generic = AeGenericInvCompatLoader.fluidEndpoint(level, pos, side); + return generic != null + ? generic + : fluids(capability(level, ForgeCapabilities.FLUID_HANDLER, pos, side, LinkMedium.FLUID)); + } + + /** + * 能量解析。按优先级依次尝试: + * + *
    + *
  1. 本模组的 {@link IEnergyManager}:long 契约,原生直通,一次调用搬走全部请求量。 + * 本模组机器的能量能力注册时交出的就是 {@code EnergyManager},所以在自家机器上必然命中。
  2. + *
  3. 外部模组 int 级的 {@link IEnergyStorage}:由 {@link LongResourceAdapters} 包装成分片的 + * long 契约。
  4. + *
  5. Modern Industrialization 的 EU:MI 的机器只注册自己的 {@code EnergyApi.SIDED}, + * 不提供 MinecraftForge 的 FE 能力,所以上面两步都拿不到。交给 MI 的桥,由它按 + * MI 自己的 {@code forgeEnergyPerEu} 换算成 FE,并且不查电压等级。
  6. + *
+ */ + @Nullable + private static LongEnergyHandler energyHandler(Level level, BlockPos pos, @Nullable Direction side, + LinkMedium medium) { + IEnergyStorage storage = capability(level, ForgeCapabilities.ENERGY, pos, side, medium); + if (storage instanceof IEnergyManager manager) { + return LongResourceAdapters.energy(manager); + } + if (storage != null) { + return LongResourceAdapters.energy(storage); + } + return com.sorrowmist.useless.compat.forge1201.OptionalIntegration.call("modern_industrialization","com.sorrowmist.useless.compat.modernindustrialization.MiEnergyCompatLoader","energyEndpoint",LongEnergyHandler.class,null,new Class[]{Level.class,BlockPos.class,Direction.class},level, pos, side); + } + + @Nullable + private static T capability(Level level, net.minecraftforge.common.capabilities.Capability capability, + BlockPos pos, @Nullable Direction side, LinkMedium medium) { + if (side != null) { + return com.sorrowmist.useless.compat.forge1201.capability.CapabilityAccess.get(level, capability, pos, side); + } + CapabilityHint hint = new CapabilityHint(level.dimension(), pos.immutable(), medium); + if (CAPABILITY_HINTS.containsKey(hint)) { + // 上次命中的那个面:先试它,命中就省下最多 6 次 getCapability。 + Direction remembered = CAPABILITY_HINTS.get(hint); + T found = remembered == null + ? com.sorrowmist.useless.compat.forge1201.capability.CapabilityAccess.get(level, capability, pos, null) + : com.sorrowmist.useless.compat.forge1201.capability.CapabilityAccess.get(level, capability, pos, remembered); + if (found != null) { + return found; + } + // 提示失手:方块多半被换过了。退回下面的完整扫描并重新记,所以绝不会读错, + // 最多多付一次查找。 + CAPABILITY_HINTS.remove(hint); + } + T unspecified = com.sorrowmist.useless.compat.forge1201.capability.CapabilityAccess.get(level, capability, pos, null); + if (unspecified != null) { + CAPABILITY_HINTS.put(hint, null); + return unspecified; + } + for (Direction direction : DIRECTIONS) { + T found = com.sorrowmist.useless.compat.forge1201.capability.CapabilityAccess.get(level, capability, pos, direction); + if (found != null) { + CAPABILITY_HINTS.put(hint, direction); + return found; + } + } + return null; + } + + /** + * 「上次哪个面解析成功」的提示表。 + * + *

{@link #capability} 在 {@code side == null} 时要依次试「不限面 + 6 个方向」,最坏 7 次 + * {@code getCapability};而一个锚点的可用面在方块没被换掉之前是稳定的。这里只记「上次命中 + * 的那个面」,下次先试它。

+ * + *

提示失手一律退回完整扫描,所以方块被换掉、能力被撤回都不会读错,最多多付一次 + * 查找。值写 {@code null} 表示「不限面那次就命中了」——用 {@code containsKey} 区分 + * 「没记过」与「记的是 null」。

+ * + *

键里带 {@code medium}:不同资源类型可能落在不同的面上。只在服务端线程访问 + * (右键判定、绑定包、引擎 tick 都在服务端线程)。

+ */ + private static final Map CAPABILITY_HINTS = new HashMap<>(); + + private record CapabilityHint(ResourceKey dimension, BlockPos pos, LinkMedium medium) { + } + + /** + * 清空「上次命中的面」提示表。 + * + *

提示本身不会读错(失手就退回完整扫描),这里只是防止它随着历史锚点无限增长。 + * 引擎在自愈周期与服务器停止时调用。

+ */ + public static void clearCapabilityHints() { + CAPABILITY_HINTS.clear(); + } + + @Nullable + private static ChemicalHandlerView chemicalHandler(Level level, BlockPos pos, @Nullable Direction side) { + ChemicalCompatProvider provider = ChemicalCompatProviders.get(); + if (!provider.isAvailable()) { + return null; + } + BlockEntity entity = level.getBlockEntity(pos); + if (entity == null) { + return null; + } + BlockState state = level.getBlockState(pos); + return provider.getAdjacentHandler(level, pos, state, entity, side); + } + + // ------------------------------------------------------------------ 搬运 + + /** + * 解析释放端(源)的端点。解析不出来返回 {@code null}。 + * + *

单独暴露是因为一次分配里源端要发给多个接收端:源端点只需解析一次, + * 由 {@link StaffLinkEngine#distribute} 拿到后交给 + * {@link #transfer(Object, ServerLevel, StaffLinkRoute, ServerLevel, StaffLinkRoute, long)} 复用。 + * AE 端点尤其要紧 —— 它每次解析都要走「取方块实体 → 取能力」,还会新建一份端点实例。

+ */ + @Nullable + public static Object resolveSource(ServerLevel sourceLevel, StaffLinkRoute source) { + return resolve(sourceLevel, source.anchor().pos(), source.side(), source.medium()); + } + + /** + * 把资源从 {@code source} 搬到 {@code target}。 + * + *

默认「先模拟后提交」:模拟阶段算出目标能接多少,提交阶段只搬那个数量;万一提交时 + * 目标仍然吐回余量(同 tick 内不该发生),余量会退回源,源也塞不回时掉落到源脚边, + * 保证不凭空消失。

+ * + *

源端是 ME 网络时跳过「先问目标能收多少」这一步({@code skipTargetProbe})。 + * 理由有两条:

+ * + *
    + *
  1. 模拟本来就是纯开销。它存在的意义是「别从源里抽出超过目标能收的量」, + * 而那要靠「抽出来的东西能塞回源」兜底。ME 网络的 {@code insert} 一定收得下刚刚 + * 从它里面抽出来的东西(同一 tick 内),所以这条底线本来就成立。
  2. + *
  3. 模拟很贵。目标端的 {@code insert(..., true)} 会完整走一遍 + * {@code AEItemKey.of} + 槽位试算;实测一次搬运里「源试算 + 目标试算 + 源抽 + + * 目标提交」四次调用中,目标那两次占了目标端总耗时的全部。去掉模拟等于把 + * 「每次搬运问两遍」变成「问一遍」。
  4. + *
+ * + *

为什么只按源端判断,不按目标端判断。余量退回的是源,所以风险只取决于源端 + * 收不收得回:源是 ME 网络 ⇒ 稳;源是普通容器 ⇒ 有可能塞不回(例如机器的输出槽只出不进), + * 那时东西会掉在源脚边。目标端是不是 AE 与这条底线无关,所以不看它。

+ * + *

数量全程 long。 {@code limit} 多大就请求多大,不存在「先截断成 int、剩下的 + * 交给外层循环」这一步。

+ * + *

源端由调用方先解析好再传进来(见 {@link #resolveSource})。一个释放端往往要发给 + * 多个接收端,源端点解析一次就够;AE 端点尤其要紧 —— 它每次解析都要走「取方块实体 → 取能力」 + * 并新建一份端点实例,按接收端个数重复付这笔钱没有意义。

+ * + * @param from 源端点,{@code null} 视为解析失败、直接不搬 + * @return 实际搬运量(0 表示没搬) + */ + public static long transfer(@Nullable Object from, ServerLevel sourceLevel, StaffLinkRoute source, + ServerLevel targetLevel, StaffLinkRoute target, long limit) { + // 配对按 family 而不是 medium:ITEM 与 AE_ITEM 都是「物品」,箱子接 AE 网络才配得起来。 + if (from == null || limit <= 0L || source.medium().family() != target.medium().family()) { + return 0L; + } + // 过滤器两端都算数:释放端决定「允许抽出什么」,接收端决定「允许收下什么」, + // 一个资源必须同时通过两边才允许搬运。只看一端的话,接收端设的过滤器等于没设。 + // + // 唯一的例外是「从 AE 取出」:源端是 ME 网络时源端过滤器不参与判定,拿什么完全由 + // 接收端的白名单决定(见 LinkMedium#isAe)。网络里动辄上千种物品,让玩家在源端列 + // 白名单既繁琐又容易漏,而「我要往这个箱子拿什么」本来就该由接收端说。 + boolean sourceIsAe = source.medium().isAe(); + List sourceFilter = sourceIsAe ? List.of() : source.activeFilters(); + List targetFilter = target.activeFilters(); + + // ---- Req3:源端是 ME 网络 + 接收端没有任何「类型对得上」的白名单 ⇒ 一个也不搬 ---- + // + // 理由:上面那条例外把「拿什么」的决定权整条交给了接收端。如果接收端什么都没写, + // 那条例外就退化成「把整张网络灌进这个容器」——而网络里可能有几千种东西, + // 这是个破坏性后果。所以「没有白名单」在 AE 源这一侧必须解释为「不搬」, + // 而不是「不限制」(后者是非 AE 源的语义,保持不变)。 + // + // ⚠️ 这条规则只对「网络里能装多种资源」的族成立:能量与魔源从网络里出来只有一种, + // 既没有灌满容器的风险,也根本没有可标记物(界面不给填)。对它们套这条规则 = 把线路锁死, + // 玩家搬不动又无处解锁。判据封装在 StaffLinkRoute#blocksAeSourceWithoutWhitelist。 + // + // 放在 resolve(target) 之前:省一次目标端解析。 + if (sourceIsAe && target.blocksAeSourceWithoutWhitelist()) { + return 0L; + } + + Object to = resolve(targetLevel, target.anchor().pos(), target.side(), target.medium()); + if (to == null) { + return 0L; + } + // 源端是 ME 网络 ⇒ 抽出来的东西一定塞得回去 ⇒ 不必先问目标能收多少。见方法说明。 + boolean skipTargetProbe = sourceIsAe; + return switch (source.medium()) { + case ITEM, AE_ITEM -> moveItems(sourceLevel, source.anchor().pos(), + (LongItemHandler) from, (LongItemHandler) to, limit, sourceFilter, targetFilter, + skipTargetProbe); + case FLUID, AE_FLUID -> moveFluid((LongFluidHandler) from, (LongFluidHandler) to, limit, + sourceFilter, targetFilter, skipTargetProbe); + case ENERGY, AE_ENERGY -> moveEnergy((LongEnergyHandler) from, (LongEnergyHandler) to, limit); + case CHEMICAL, AE_CHEMICAL -> moveChemical((ChemicalHandlerView) from, + (ChemicalHandlerView) to, limit, sourceFilter, targetFilter); + // 魔源那边的接口是 int,超出的部分只能夹掉——魔源本身也到不了那么大的量。 + case SOURCE, AE_SOURCE -> moveSource((SourceHandlerView) from, (SourceHandlerView) to, limit); + // 应力不走逐对搬运:转速与应力容量是整张网络的连续状态量,不是「一轮搬多少」, + // 必须拿到同线路上全部输入与输出才能分配。它由 StaffLinkEngine#runNetwork 里的 + // 专用分支整条处理,永远走不到这里。 + case STRESS -> 0L; + // 气压也不走逐对搬运:它是「把接收端伺服到目标气压」的状态量,可能要把接收端的空气 + // 抽掉(排到环境),且搬运量来自压力差而不是 amount(amount 已被复用为目标气压)。 + // 它由 StaffLinkEngine#runNetwork 里的专用分支整条处理,永远走不到这里。 + case PRESSURE, AE_PRESSURE -> 0L; + }; + } + + private static long moveItems(ServerLevel sourceLevel, BlockPos sourcePos, + LongItemHandler from, LongItemHandler to, long limit, + List sourceFilter, List targetFilter, + boolean skipTargetProbe) { + // ---- 路径三:有模式标记(#tag / 通配符)⇒ 只能扫描源端 + 谓词 ---- + // + // 模式匹配的是一个「类」,没法变成单一模板去调 extractMatching,所以必须逐种试。 + // 这条路的顺序是「先模式、后具体标记」:模式那部分本来就不可能有零枚举的走法。 + if (hasPatternMarker(sourceFilter) || hasPatternMarker(targetFilter)) { + return sweepItems(sourceLevel, sourcePos, from, to, limit, + sourceFilter, targetFilter, skipTargetProbe); + } + + // ---- 路径一:两侧都只有「具体标记」(或完全没标记)⇒ 按类型直接抽,绝不扫描 ---- + // + // 「要搬什么」已经被过滤器完全确定了,直接问源端「有没有这一种」即可 —— + // 这一步合并进了 moveOneItemStackByType(源端试算一次)。 + // + // 不能改成「扫源端槽位、挑匹配的」:AE 端点的 getSlots() 是整个 ME 网络的资源种类数 + // (成熟网络上千种很常见),扫描会退化成「把整张表走一遍」;而 extractMatching 是 + // 按资源键的哈希查找。 + // + // 同样不能改成「先 findSlot 定位槽位、再 extract(slot)」:AE 端点的 findSlot 需要先抓 + // 一份整网快照(MEStorage.getAvailableStacks()),等于每次搬运遍历一遍网络里每一种资源。 + // 实测这一条就占了无线物流总耗时的 32%(见内部无线物流性能分析报告第十节)。 + List wanted = filteredItemMarkers(sourceFilter, targetFilter); + if (!wanted.isEmpty()) { + long moved = 0L; + for (ItemStack template : wanted) { + if (moved >= limit) { + break; + } + // 两端一起判定(白名单/黑名单 + 两条控制条件),顺带算出这一种能搬多少。 + Gate gate = evaluateItemGate(sourceFilter, targetFilter, from, to, template, limit - moved); + if (!gate.allowed) { + continue; + } + moved += moveOneItemStackByType(sourceLevel, sourcePos, from, to, + limit - moved, template, gate.movableLimit, gate.roomLimit, skipTargetProbe); + } + return moved; + } + + // ---- 路径二:完全没有标记 ⇒ 沿用扫描,命中即搬 ---- + return sweepItems(sourceLevel, sourcePos, from, to, limit, + sourceFilter, targetFilter, skipTargetProbe); + } + + /** + * 完整的一轮扫描:把源端所有槽走一遍,能搬的都搬,最多搬 {@code limit}。 + * + *

刻意没有「扫描预算」这个上限。早先这里有个 {@code MAX_SCAN_SLOTS = 256}, + * 当时写的理由是「防止每 tick 把上千条目全走一遍」。但那是语义错误而不是性能折衷: + * 循环每轮都从 slot 0 开始,一旦排在前面的条目总是把 {@code limit} 用满, + * 排在后面的资源就永远轮不到——表现是「接收端设了过滤的机器一直收不到东西」。

+ * + *

去掉预算为什么反而安全:

+ * + *
    + *
  • 循环的退出条件本来就是 {@code moved < limit}。源端有东西可搬时,通常扫几种就够 + * {@code limit} 退出了;遍历长度由「实际搬没搬到」决定,不是「把整张表走完」。
  • + *
  • 对 AE 源端点,{@code getSlots()} 会先抓一份整网快照(一次 + * {@code MEStorage.getAvailableStacks()})。这份枚举本来就已付——现在的做法 + * 只是「枚举一次 + 逐条试」,预算省的只是「枚举之后」的比较,枚举本身一分没省。
  • + *
  • 真正担心的「源端一直搬不动、一路扫到底」场景,由 {@link StaffLinkEngine} 的 + * 指数退避兜底:连续空转({@code moved == 0})时下一轮间隔按 + * {@code interval << n} 翻倍、封顶 32 倍。这是删掉预算的前置条件, + * 已在引擎侧确认成立。
  • + *
+ */ + private static long sweepItems(ServerLevel sourceLevel, BlockPos sourcePos, + LongItemHandler from, LongItemHandler to, long limit, + List sourceFilter, List targetFilter, + boolean skipTargetProbe) { + long moved = 0L; + int slots = from.getSlots(); + for (int slot = 0; slot < slots && moved < limit; slot++) { + // peekStack 而不是 getStackInSlot:扫描只需判类型,不必付一次 copy。 + // 这个模板在 settleItemLeftover 里若真有余量退回,那边会先固化一份。 + ItemStack template = from.peekStack(slot); + if (template.isEmpty()) { + continue; + } + // 数量顺手从刚拿到的模板上取(见 LongItemHandler#amountIn(int, ItemStack)): + // 通用实现里模板的 count 就是槽内真实数量,不必再把同一个槽位读第二遍。 + // 它同时充当「源端自身存量」查询失败时的回退值,所以先取。 + long available = from.amountIn(slot, template); + Gate gate = evaluateItemGate(sourceFilter, targetFilter, from, to, template, available); + if (!gate.allowed) { + continue; + } + moved += moveOneItemStack(sourceLevel, sourcePos, from, to, slot, + limit - moved, template, available, + gate.movableLimit, gate.roomLimit, skipTargetProbe); + } + return moved; + } + + private static final long NOT_COMPUTED = Long.MIN_VALUE; + + /** + * 一条线路的「门控结果」:允不允许搬,以及能搬多少。 + * + *

把旧的 {@code keepFor}/{@code maxFor} 合并成一次求值:遍历过滤格时既判定 + * 白名单/黑名单与两条控制条件,又把「测 A 自身」的条件折算成两个搬运量上限。

+ * + *
    + *
  • {@link #movableLimit}:最多能搬走多少(来自「输出端 ≥ self」,= 源存量 − N)。
  • + *
  • {@link #roomLimit}:目标最多还能收多少(来自「输入端 ≤ self」,= N − 目标存量)。
  • + *
  • 两者默认 {@link Long#MAX_VALUE},表示不限制——此时一次 {@code amountOf} 都不查。
  • + *
+ */ + private static final class Gate { + boolean allowed = true; + long movableLimit = Long.MAX_VALUE; + long roomLimit = Long.MAX_VALUE; + /** 源端 A 自身存量;{@link #NOT_COMPUTED} 表示还没查。 */ + long sourceSelf = NOT_COMPUTED; + /** 目标端 A 自身存量;{@link #NOT_COMPUTED} 表示还没查。 */ + long targetSelf = NOT_COMPUTED; + /** 源端存量查询失败(返回 ≤0)时的回退值。 */ + long sourceFallback; + } + + /** + * 物品门控:两端过滤器都判定一遍,结果合进一个 {@link Gate}。 + * + * @param sourceFallback 源端自身存量查询失败时的回退值(扫描路径传槽内存量、按类型路径传请求量) + */ + private static Gate evaluateItemGate(List sourceFilter, List targetFilter, + LongItemHandler from, LongItemHandler to, ItemStack template, + long sourceFallback) { + Gate gate = new Gate(); + gate.sourceFallback = sourceFallback; + gate.allowed = evalItemFilter(sourceFilter, gate, from, to, template) + && evalItemFilter(targetFilter, gate, from, to, template); + return gate; + } + + /** + * 单侧判定:返回「这一端允不允许这种物品」。 + * + *

规则(兼容旧的「无标记 = 不限制」白名单语义,并加上黑名单与条件):

+ *
    + *
  1. 命中任一黑名单格(标记匹配且条件成立)⇒ 直接不允许。
  2. + *
  3. 否则若这一端存在白名单格 ⇒ 必须至少命中一个(标记匹配且条件成立)。
  4. + *
  5. 否则(没有白名单格)⇒ 不限制。
  6. + *
+ */ + private static boolean evalItemFilter(List filter, Gate gate, + LongItemHandler from, LongItemHandler to, ItemStack template) { + boolean hasInclude = false; + boolean includeApplies = false; + for (LinkFilterSlot slot : filter) { + if (!slot.isItem() && !slot.isPattern()) { + continue; + } + if (!slot.exclude()) { + hasInclude = true; + } + if (!matchesSlot(slot, template)) { + continue; + } + if (!itemConditionsHold(slot, gate, from, to, template)) { + continue; + } + if (slot.exclude()) { + return false; + } + includeApplies = true; + } + return !hasInclude || includeApplies; + } + + /** 这一格的两条控制条件是否都成立;成立时把「测 A 自身」的条件折算进 gate 的两个上限。 */ + private static boolean itemConditionsHold(LinkFilterSlot slot, Gate gate, + LongItemHandler from, LongItemHandler to, ItemStack template) { + LinkFilterCondition out = slot.outCond(); + if (!out.isOff()) { + if (out.isSelf()) { + long source = itemAmountAtSource(gate, from, template); + if (out.op() == LinkFilterCondition.Op.AT_LEAST) { + if (source < out.value()) { + return false; + } + // 保留 N 在输出端:最多搬走 源存量 − N(= 旧的「源端保留」)。 + gate.movableLimit = Math.min(gate.movableLimit, source - out.value()); + } else if (source > out.value()) { + return false; + } + } else if (!itemConditionHoldsOn(from, out)) { + return false; + } + } + LinkFilterCondition in = slot.inCond(); + if (!in.isOff()) { + if (in.isSelf()) { + long target = itemAmountAtTarget(gate, to, template); + if (in.op() == LinkFilterCondition.Op.AT_MOST) { + if (target > in.value()) { + return false; + } + // 目标封顶 N:最多再收 N − 目标存量(= 旧的「接收端上限」)。 + gate.roomLimit = Math.min(gate.roomLimit, in.value() - target); + } else if (target < in.value()) { + return false; + } + } else if (!itemConditionHoldsOn(to, in)) { + return false; + } + } + return true; + } + + /** 源端 A 自身存量,惰性查询 + 缓存;查询失败时回退到调用方给的值。 */ + private static long itemAmountAtSource(Gate gate, LongItemHandler from, ItemStack template) { + if (gate.sourceSelf == NOT_COMPUTED) { + long total = from.amountOf(template); + gate.sourceSelf = total > 0L ? total : gate.sourceFallback; + } + return gate.sourceSelf; + } + + /** 目标端 A 自身存量,惰性查询 + 缓存。 */ + private static long itemAmountAtTarget(Gate gate, LongItemHandler to, ItemStack template) { + if (gate.targetSelf == NOT_COMPUTED) { + gate.targetSelf = to.amountOf(template); + } + return gate.targetSelf; + } + + /** 控制材料是「别的物品」时,在给定端点上量它的存量再比较。 */ + private static boolean itemConditionHoldsOn(LongItemHandler handler, LinkFilterCondition cond) { + if (!cond.isItemControl()) { + // 控制材料不是物品(异族):物品端点上量不了,忽略这条(服务端清洗会把它清掉)。 + return true; + } + long amount = handler.amountOf(cond.item()); + return cond.op() == LinkFilterCondition.Op.AT_LEAST + ? amount >= cond.value() + : amount <= cond.value(); + } + + /** + * 处理「已经抽出来了、但目标没全收下」的余量:退回源,退不回就掉在源脚边。返回实际搬走的量。 + * + *

抽出来之后才发现目标不收,是跳过目标试算那条路的正常分支(见 + * {@link #transfer(Object, ServerLevel, StaffLinkRoute, ServerLevel, StaffLinkRoute, long)})。

+ * + *

{@code template} 可能是源容器持有的活对象(无过滤器的扫描路径用的是 + * {@link LongItemHandler#peekStack(int)},它不 copy)。被抽空时那个对象会就地变成空栈, + * 拿它去退回等于塞空气、物品凭空蒸发 —— 所以退回前这里再固化一份。 + * 「写进目标端」那一步同样依赖模板,而它发生在调用方那一侧,所以那边必须在抽取 + * 之前就固化(见 {@code stabilize});这里的 copy 是余量路径的最后一道防线。

+ * + *

这次 copy 只在真有余量时才会付,正常路径(抽多少收多少)完全不付。

+ * + * @param extracted 从源里实际抽出的量 + * @param accepted 目标实际收下的量(调用方已经拿到,不必再问一次) + */ + private static long settleItemLeftover(ServerLevel sourceLevel, BlockPos sourcePos, + LongItemHandler from, ItemStack template, + long extracted, long accepted) { + long leftover = extracted - accepted; + long actuallyMoved = extracted; + if (leftover > 0L) { + // 固化模板:template 可能正被源容器持有,抽取已经改动了它。 + ItemStack stable = template.copy(); + long unreturned = leftover - from.insert(stable, leftover, false); + if (unreturned > 0L) { + // 退回不了就落地。这条以前是静默的,而「东西抽出来了、目标没收、源也塞不回」 + // 正是最容易被误当成「凭空消失」的场景(其实是在源脚边),必须留痕。 + RateLimitedLogger.warn(UselessMod.LOGGER, + "无线物流:{} 个 {} 未能写入目标端、也退不回源端,已掉落在源容器脚边", + unreturned, stable.getItem()); + dropItems(sourceLevel, sourcePos, stable, unreturned); + actuallyMoved -= unreturned; + } + } + return Math.max(0L, actuallyMoved); + } + + /** + * 从指定槽位搬走最多 {@code want} 个 {@code template};返回实际搬走的数量。 + * + *

只给无过滤器 / 模式过滤器的扫描路径用(只有具体标记的走 + * {@link #moveOneItemStackByType})。

+ * + * @param available 该槽位的存量;扫描路径已经连同模板一起取过,避免重复读同一个槽位 + * @param movableLimit 输出端条件折算出的搬运量上限({@link Long#MAX_VALUE} = 不限) + * @param roomLimit 输入端条件折算出的目标可收量上限({@link Long#MAX_VALUE} = 不限) + */ + private static long moveOneItemStack(ServerLevel sourceLevel, BlockPos sourcePos, + LongItemHandler from, LongItemHandler to, int slot, + long want, ItemStack template, long available, + long movableLimit, long roomLimit, + boolean skipTargetProbe) { + if (want <= 0L || available <= 0L) { + return 0L; + } + long request = requestAmount(want, available, movableLimit, roomLimit); + if (request <= 0L) { + return 0L; + } + if (skipTargetProbe) { + long extracted = from.extract(slot, request, false); + if (extracted <= 0L) { + return 0L; + } + // 这条分支只在源端是 AE 时才会走到(skipTargetProbe 就是 sourceIsAe), + // 而 AE 端点交出的模板一律是新对象({@code toStack(1)} / {@code key.toStack(1)}), + // 抽取不会动它 —— 所以这里不需要下面那次固化。 + return settleItemLeftover(sourceLevel, sourcePos, from, template, extracted, + insertInto(to, template, extracted)); + } + // 模拟:目标最多能接多少(long,跨槽累加精确)。 + long accepted = to.insert(template, request, true); + if (accepted <= 0L) { + return 0L; + } + // ⚠️ 抽取前必须先固化模板(见 stabilize 的说明)。 + ItemStack stable = accepted >= available ? stabilize(template) : template; + long extracted = from.extract(slot, accepted, false); + if (extracted <= 0L) { + return 0L; + } + return settleItemLeftover(sourceLevel, sourcePos, from, stable, extracted, + insertInto(to, stable, extracted)); + } + + /** + * 抽取前把源容器持有的那只模板栈固化一份。 + * + *

为什么必须固化。扫描路径的模板来自 {@link LongItemHandler#peekStack(int)},它 + * 故意不 copy(省掉实测占无线物流总耗时 13% 的一次拷贝),交出的可能就是源容器 + * 槽里那只对象本身。而容器抽空一个槽时就地改写这只栈是家常便饭 —— 最典型的就是 + * 原版箱子 / 木桶:{@code InvWrapper.extractItem} 走 {@code Container#removeItem}, + * 即 {@code ContainerHelper.removeItem} 里的 {@code stack.split(amount)},而 + * {@code split} 的语义就是就地 shrink 自己。于是我们手里那只模板跟着变成空栈。

+ * + *

模板在抽取之后还要用两次:写进目标端、以及余量退回源端。变成空栈之后这两步都会 + * 静默失败({@code insert} 见到空栈直接返回 0,连日志都不会打),结果就是 + * 「东西从源里抽走了,却既没进目标也没退回,凭空蒸发」。流体侧没有这个问题,是因为 + * {@code getFluidInTank} 一律返回副本,注释里写明了同样的理由。

+ * + *

只在真会被抽空时才付这次拷贝。抽取请求量恒 ≤ 该槽存量,所以 + * {@code accepted >= available}(等价于「目标全要、这一抽正好把槽清空」)是「模板会被就地 + * 抽空」的充要条件;其余情况槽里必然还剩东西,模板对象依旧可用(数量在这条路上不参与任何 + * 判定:写入方一律自己设 count,退回/掉落也走 {@code copyWithCount})。

+ * + *

另一个方向的保守性:容器若每次只让渡 1 个(SophisticatedCore 那类), + * {@code extract} 会因 {@code MAX_CHUNK_STEPS} 提前收手、槽其实没被抽空,这时多付的这次 + * 拷贝只是浪费,不会改变行为。

+ */ + private static ItemStack stabilize(ItemStack template) { + return template.copy(); + } + + /** + * 写进目标端,并盯住「模板在抽取途中被抽坏」这种本该不可能发生的死账。 + * + *

正常路径只是转发一次调用、零开销。模板真被抽空时({@link #stabilize} 的判据没覆盖到 + * 的异常容器:报了部分抽取却把槽清空了),{@code insert} 会静默返回 0,随后余量退回同样 + * 静默失败 —— 物品就此消失且不留任何痕迹。这里补一条限流警告,让这种损失至少可见。

+ */ + private static long insertInto(LongItemHandler to, ItemStack template, long amount) { + if (template.isEmpty()) { + warnTemplateVoided(amount); + return 0L; + } + return to.insert(template, amount, false); + } + + /** 「模板被抽空」告警的限流:每 {@link #TEMPLATE_WARN_INTERVAL_MS} 毫秒最多一条。 */ + private static final long TEMPLATE_WARN_INTERVAL_MS = 5000L; + private static final AtomicLong LAST_TEMPLATE_WARN_AT = new AtomicLong(); + + private static void warnTemplateVoided(long amount) { + long now = System.currentTimeMillis(); + long last = LAST_TEMPLATE_WARN_AT.get(); + if (now - last < TEMPLATE_WARN_INTERVAL_MS || !LAST_TEMPLATE_WARN_AT.compareAndSet(last, now)) { + return; + } + RateLimitedLogger.warn(UselessMod.LOGGER, + "无线物流:源容器抽取时就地清空了模板栈,{} 个物品无法写入目标端(模板固化判据未覆盖)", + amount); + } + + /** + * 把「想要搬多少 / 这一槽有多少 / 两个条件折算出的上限」收敛成一个请求量。 + * + *

两个上限由 {@link Gate} 在门控阶段算好:{@code movableLimit}(输出端保留折算)与 + * {@code roomLimit}(输入端封顶折算)。两者默认 {@link Long#MAX_VALUE} = 不限制, + * 此时这里不做任何查询——AE 端点上的 {@code amountOf} 会抓整网快照,绝不能被当成免费查询。

+ */ + private static long requestAmount(long want, long available, long movableLimit, long roomLimit) { + long request = Math.min(want, available); + if (movableLimit != Long.MAX_VALUE) { + request = Math.min(request, movableLimit); + } + if (roomLimit != Long.MAX_VALUE) { + request = Math.min(request, roomLimit); + } + return Math.max(0L, request); + } + + /** + * 从源端按类型搬走最多 {@code want} 个 {@code template};返回实际搬走的数量。 + * + *

与 {@link #moveOneItemStack} 的区别是不经过「第几号槽」:定位与试算合并成 + * {@link LongItemHandler#extractMatching(ItemStack, long, boolean)} 一次调用。对 AE 端点 + * 这一步是一次按资源键的哈希查找;走槽位的话得先抓一份整网快照,也就是把 ME 网络里的 + * 每一种资源遍历一遍(实测占无线物流总耗时的 32%)。

+ * + *

{@code skipTargetProbe} 为真时只问两次(源抽一次、目标收一次),不为真的话 + * 是四次(源试算 → 目标试算 → 源抽 → 目标收)。判据见 + * {@link #transfer(Object, ServerLevel, StaffLinkRoute, ServerLevel, StaffLinkRoute, long)}。

+ */ + private static long moveOneItemStackByType(ServerLevel sourceLevel, BlockPos sourcePos, + LongItemHandler from, LongItemHandler to, + long want, ItemStack template, + long movableLimit, long roomLimit, + boolean skipTargetProbe) { + if (want <= 0L) { + return 0L; + } + // 上限已在门控阶段算好;这里 available 未知(= want),只做一次取小。 + long request = requestAmount(want, want, movableLimit, roomLimit); + if (request <= 0L) { + return 0L; + } + if (skipTargetProbe) { + // 源端一次调用完成「定位 + 抽取」:抽到多少就是多少,目标收不下的余量退回源。 + long extracted = from.extractMatching(template, request, false); + if (extracted <= 0L) { + return 0L; + } + return settleItemLeftover(sourceLevel, sourcePos, from, template, extracted, + to.insert(template, extracted, false)); + } + // 源端试算:既回答「有没有这一种」,也给出实际可抽量。 + long available = from.extractMatching(template, request, true); + if (available <= 0L) { + return 0L; + } + long accepted = to.insert(template, available, true); + if (accepted <= 0L) { + return 0L; + } + long extracted = from.extractMatching(template, accepted, false); + if (extracted <= 0L) { + return 0L; + } + return settleItemLeftover(sourceLevel, sourcePos, from, template, extracted, + to.insert(template, extracted, false)); + } + + /** + * 收集两端过滤器里列出的物品种类(按类型去重)。 + * + *

次序是「先源端、后接收端」,同权重下靠前的先拿,这个顺序要可预期。

+ */ + private static List filteredItemMarkers(List sourceFilter, + List targetFilter) { + List result = new ArrayList<>(sourceFilter.size() + targetFilter.size()); + collectItemMarkers(result, sourceFilter); + collectItemMarkers(result, targetFilter); + return result; + } + + private static void collectItemMarkers(List out, List filter) { + for (LinkFilterSlot slot : filter) { + // 黑名单格不是「候选搬运对象」:它只负责挡,不负责搬。 + if (!slot.isItem() || slot.exclude()) { + continue; + } + boolean duplicate = false; + for (ItemStack existing : out) { + if (ItemStack.isSameItemSameTags(existing, slot.item())) { + duplicate = true; + break; + } + } + if (!duplicate) { + out.add(slot.item()); + } + } + } + + /** 塞不回去的余量掉在源脚边;按堆叠上限分片,避免做出超过栈上限的非法栈。 */ + private static void dropItems(ServerLevel level, BlockPos pos, ItemStack template, long amount) { + int stackLimit = Math.max(1, template.getMaxStackSize()); + long remaining = amount; + while (remaining > 0L) { + int chunk = (int) Math.min(remaining, stackLimit); + Block.popResource(level, pos, template.copyWithCount(chunk)); + remaining -= chunk; + } + } + + /** + * 流体搬运。 + * + *

模拟阶段两边都给的是 long 精确上限,因此 {@code limit} 能一次性用满。 + * 与物品侧一样有三条路径(模式 / 具体标记 / 无标记),且同样没有扫描预算 + * (理由见 {@link #sweepItems})。

+ */ + private static long moveFluid(LongFluidHandler from, LongFluidHandler to, long limit, + List sourceFilter, List targetFilter, + boolean skipTargetProbe) { + if (hasPatternMarker(sourceFilter) || hasPatternMarker(targetFilter)) { + return sweepFluids(from, to, limit, sourceFilter, targetFilter, skipTargetProbe); + } + + // 有过滤器时按类型直接抽,理由同 moveItems:AE 端点的 getTanks() 是网络里的流体 + // 种类数,靠扫描要遍历整张表;而 findTank 又要先抓整网快照。 + List wanted = filteredFluidMarkers(sourceFilter, targetFilter); + if (!wanted.isEmpty()) { + for (FluidStack type : wanted) { + Gate gate = evaluateFluidGate(sourceFilter, targetFilter, from, to, type, limit); + if (!gate.allowed) { + continue; + } + long moved = moveOneFluidByType(from, to, limit, type, + gate.movableLimit, gate.roomLimit, skipTargetProbe); + if (moved > 0L) { + // 一次只搬一种:搬动了就收工。 + return moved; + } + } + return 0L; + } + + return sweepFluids(from, to, limit, sourceFilter, targetFilter, skipTargetProbe); + } + + /** + * 流体侧的一轮完整扫描(无预算,理由同 {@link #sweepItems})。 + * + *

与原实现一致:一次只搬一种——搬动了就返回,不等搬满 {@code limit}。 + * 这是流体侧一直以来的语义(一次搬运给一种流体),保持不动。

+ */ + private static long sweepFluids(LongFluidHandler from, LongFluidHandler to, long limit, + List sourceFilter, List targetFilter, + boolean skipTargetProbe) { + int tanks = from.getTanks(); + for (int tank = 0; tank < tanks; tank++) { + FluidStack type = from.getFluidInTank(tank); + if (type.isEmpty()) { + continue; + } + Gate gate = evaluateFluidGate(sourceFilter, targetFilter, from, to, type, + from.amountIn(tank)); + if (!gate.allowed) { + continue; + } + long moved = moveOneFluid(from, to, tank, limit, type, + gate.movableLimit, gate.roomLimit, skipTargetProbe); + if (moved > 0L) { + return moved; + } + } + return 0L; + } + + /** 流体门控:与 {@link #evaluateItemGate} 同构。 */ + private static Gate evaluateFluidGate(List sourceFilter, List targetFilter, + LongFluidHandler from, LongFluidHandler to, FluidStack type, + long sourceFallback) { + Gate gate = new Gate(); + gate.sourceFallback = sourceFallback; + gate.allowed = evalFluidFilter(sourceFilter, gate, from, to, type) + && evalFluidFilter(targetFilter, gate, from, to, type); + return gate; + } + + /** 单侧流体判定:规则与 {@link #evalItemFilter} 相同。 */ + private static boolean evalFluidFilter(List filter, Gate gate, + LongFluidHandler from, LongFluidHandler to, FluidStack type) { + boolean hasInclude = false; + boolean includeApplies = false; + for (LinkFilterSlot slot : filter) { + if (!slot.isFluid() && !slot.isPattern()) { + continue; + } + if (!slot.exclude()) { + hasInclude = true; + } + if (!matchesSlotFluid(slot, type)) { + continue; + } + if (!fluidConditionsHold(slot, gate, from, to, type)) { + continue; + } + if (slot.exclude()) { + return false; + } + includeApplies = true; + } + return !hasInclude || includeApplies; + } + + /** 流体版的两条控制条件判定;折算规则同 {@link #itemConditionsHold}。 */ + private static boolean fluidConditionsHold(LinkFilterSlot slot, Gate gate, + LongFluidHandler from, LongFluidHandler to, FluidStack type) { + LinkFilterCondition out = slot.outCond(); + if (!out.isOff()) { + if (out.isSelf()) { + long source = fluidAmountAtSource(gate, from, type); + if (out.op() == LinkFilterCondition.Op.AT_LEAST) { + if (source < out.value()) { + return false; + } + gate.movableLimit = Math.min(gate.movableLimit, source - out.value()); + } else if (source > out.value()) { + return false; + } + } else if (!fluidConditionHoldsOn(from, out)) { + return false; + } + } + LinkFilterCondition in = slot.inCond(); + if (!in.isOff()) { + if (in.isSelf()) { + long target = fluidAmountAtTarget(gate, to, type); + if (in.op() == LinkFilterCondition.Op.AT_MOST) { + if (target > in.value()) { + return false; + } + gate.roomLimit = Math.min(gate.roomLimit, in.value() - target); + } else if (target < in.value()) { + return false; + } + } else if (!fluidConditionHoldsOn(to, in)) { + return false; + } + } + return true; + } + + private static long fluidAmountAtSource(Gate gate, LongFluidHandler from, FluidStack type) { + if (gate.sourceSelf == NOT_COMPUTED) { + long total = from.amountOf(type); + gate.sourceSelf = total > 0L ? total : gate.sourceFallback; + } + return gate.sourceSelf; + } + + private static long fluidAmountAtTarget(Gate gate, LongFluidHandler to, FluidStack type) { + if (gate.targetSelf == NOT_COMPUTED) { + gate.targetSelf = to.amountOf(type); + } + return gate.targetSelf; + } + + private static boolean fluidConditionHoldsOn(LongFluidHandler handler, LinkFilterCondition cond) { + if (!cond.isFluidControl()) { + // 控制材料不是流体(异族):流体端点上量不了,忽略这条。 + return true; + } + long amount = handler.amountOf(cond.fluid()); + return cond.op() == LinkFilterCondition.Op.AT_LEAST + ? amount >= cond.value() + : amount <= cond.value(); + } + + /** + * 处理「已经抽出来了、但目标没全收下」的余量:退回源,退不回就记一条警告。返回实际搬走的量。 + * + * @param drained 从源里实际抽出的量 + * @param filled 目标实际收下的量 + */ + private static long settleFluidLeftover(LongFluidHandler from, LongFluidHandler to, + FluidStack type, long drained, long filled) { + if (filled < drained) { + long leftover = drained - filled; + long returned = from.fill(type, leftover, false); + if (returned < leftover) { + RateLimitedLogger.warn(UselessMod.LOGGER, + "无线物流:流体回填失败,{} mB {} 无法回收(源 {},目标 {})", + leftover - returned, type.getFluid(), + from.getClass().getSimpleName(), to.getClass().getSimpleName()); + } + } + return Math.max(0L, filled); + } + + /** 从指定储罐搬走最多 {@code limit} 的 {@code type};返回实际搬走的量(0 = 没搬动)。 */ + private static long moveOneFluid(LongFluidHandler from, LongFluidHandler to, int tank, + long limit, FluidStack type, + long movableLimit, long roomLimit, boolean skipTargetProbe) { + long available = from.amountIn(tank); + if (available <= 0L) { + return 0L; + } + long request = fluidRequest(Math.min(limit, available), movableLimit, roomLimit); + if (request <= 0L) { + return 0L; + } + if (skipTargetProbe) { + long drained = from.drain(tank, request, false); + if (drained <= 0L) { + return 0L; + } + return settleFluidLeftover(from, to, type, drained, to.fill(type, drained, false)); + } + long drainable = from.drain(tank, request, true); + if (drainable <= 0L) { + return 0L; + } + long fillable = to.fill(type, drainable, true); + long target = Math.min(drainable, fillable); + if (target <= 0L) { + return 0L; + } + + long drained = from.drain(tank, target, false); + if (drained <= 0L) { + return 0L; + } + return settleFluidLeftover(from, to, type, drained, to.fill(type, drained, false)); + } + + /** + * 把「想要搬多少 / 两个条件折算出的上限」收敛成一个流体请求量。 + * + *

与物品侧的 {@link #requestAmount} 完全对应;上限已在门控阶段算好, + * {@link Long#MAX_VALUE} 表示不限制。

+ */ + private static long fluidRequest(long want, long movableLimit, long roomLimit) { + long request = Math.max(0L, want); + if (movableLimit != Long.MAX_VALUE) { + request = Math.min(request, movableLimit); + } + if (roomLimit != Long.MAX_VALUE) { + request = Math.min(request, roomLimit); + } + return Math.max(0L, request); + } + + /** + * 从源端按类型搬走最多 {@code limit} 的 {@code type};返回实际搬走的量。 + * + *

理由同 {@link #moveOneItemStackByType}:不经过「第几号罐」,定位与试算合并成 + * {@link LongFluidHandler#drainMatching(FluidStack, long, boolean)} 一次调用。

+ */ + private static long moveOneFluidByType(LongFluidHandler from, LongFluidHandler to, + long limit, FluidStack type, + long movableLimit, long roomLimit, boolean skipTargetProbe) { + long request = fluidRequest(limit, movableLimit, roomLimit); + if (request <= 0L) { + return 0L; + } + if (skipTargetProbe) { + long drained = from.drainMatching(type, request, false); + if (drained <= 0L) { + return 0L; + } + return settleFluidLeftover(from, to, type, drained, to.fill(type, drained, false)); + } + long drainable = from.drainMatching(type, request, true); + if (drainable <= 0L) { + return 0L; + } + long fillable = to.fill(type, drainable, true); + long target = Math.min(drainable, fillable); + if (target <= 0L) { + return 0L; + } + long drained = from.drainMatching(type, target, false); + if (drained <= 0L) { + return 0L; + } + return settleFluidLeftover(from, to, type, drained, to.fill(type, drained, false)); + } + + /** 收集两端过滤器里列出的流体种类(按类型去重)。 */ + private static List filteredFluidMarkers(List sourceFilter, + List targetFilter) { + List result = new ArrayList<>(sourceFilter.size() + targetFilter.size()); + collectFluidMarkers(result, sourceFilter); + collectFluidMarkers(result, targetFilter); + return result; + } + + private static void collectFluidMarkers(List out, List filter) { + for (LinkFilterSlot slot : filter) { + // 黑名单格不是「候选搬运对象」:它只负责挡,不负责搬。 + if (!slot.isFluid() || slot.exclude()) { + continue; + } + boolean duplicate = false; + for (FluidStack existing : out) { + if (com.sorrowmist.useless.compat.forge1201.fluid.FluidStacks.same(existing, slot.fluid())) { + duplicate = true; + break; + } + } + if (!duplicate) { + out.add(slot.fluid()); + } + } + } + + /** + * 能量搬运。 + * + *

两端都已经是 long 契约(本模组 {@link IEnergyManager} 原生直通、MI 的 EU 端点按配置 + * 换算、AppliedFlux 的 ME 通量走 AE 的 long 接口),所以这里一次到位,没有分片循环。

+ */ + private static long moveEnergy(LongEnergyHandler from, LongEnergyHandler to, long limit) { + long available = from.extract(limit, true); + if (available <= 0L) { + return 0L; + } + long accepted = to.receive(available, true); + long target = Math.min(available, accepted); + if (target <= 0L) { + return 0L; + } + + long extracted = from.extract(target, false); + if (extracted <= 0L) { + return 0L; + } + long received = to.receive(extracted, false); + if (received < extracted) { + from.receive(extracted - received, false); + } + return received; + } + + /** + * 魔源搬运。 + * + *

接口是 int:Ars Nouveau 自身的 {@code ISourceTile} 就是 int 契约,魔源的量级离 + * {@link Integer#MAX_VALUE} 极远,所以这里把 long 的请求夹到 int 再进端点,端点在内部 + * 还会按各自的 {@code transferRate()} 再夹一次。两端的速率因此都得到尊重。

+ * + *

时序与其它资源一致:先模拟出「源能出多少 ∩ 目标能收多少」,再按这个数量提交, + * 提交时若目标吐回余量则退回源,保证不凭空消失。

+ */ + private static long moveSource(SourceHandlerView from, SourceHandlerView to, long limit) { + int request = clampToInt(limit); + if (request <= 0) { + return 0L; + } + int available = from.extract(request, true); + if (available <= 0) { + return 0L; + } + int accepted = to.receive(available, true); + int target = Math.min(available, accepted); + if (target <= 0) { + return 0L; + } + + int extracted = from.extract(target, false); + if (extracted <= 0) { + return 0L; + } + int received = to.receive(extracted, false); + if (received < extracted) { + from.receive(extracted - received, false); + } + return received; + } + + /** 把 long 请求夹进 int。魔源侧只可能是 int,超出的部分本来就搬不动。 */ + private static int clampToInt(long value) { + if (value <= 0L) { + return 0; + } + return (int) Math.min(value, Integer.MAX_VALUE); + } + + private static long moveChemical(ChemicalHandlerView from, ChemicalHandlerView to, long limit, + List sourceFilter, List targetFilter) { + ChemicalStackView simulated = from.extractChemical(limit, true); + if (simulated == null || simulated.isEmpty() || !matchesChemical(sourceFilter, simulated) + || !matchesChemical(targetFilter, simulated)) { + return 0L; + } + ChemicalStackView rejected = to.insertChemical(simulated, true); + long accepted = simulated.amount() - (rejected == null ? 0L : rejected.amount()); + if (accepted <= 0L) { + return 0L; + } + + ChemicalStackView taken = from.extractChemical(accepted, false); + if (taken == null || taken.isEmpty()) { + return 0L; + } + ChemicalStackView leftover = to.insertChemical(taken, false); + long moved = taken.amount() - (leftover == null ? 0L : leftover.amount()); + if (leftover != null && !leftover.isEmpty()) { + from.insertChemical(leftover, false); + } + return moved; + } + + /** 这一格(具体标记或模式)是否匹配这个物品。 */ + private static boolean matchesSlot(LinkFilterSlot slot, ItemStack stack) { + // 具体标记优先:模式只在「不是物品/流体标记」时才用(三态互斥,这里只是保险)。 + if (slot.isItem()) { + return ItemStack.isSameItemSameTags(slot.item(), stack); + } + if (slot.isPattern()) { + LinkFilterPattern pattern = LinkFilterPattern.parse(slot.pattern()); + return pattern != null && pattern.matchesItem(stack); + } + return false; + } + + /** 这一格是否匹配这种流体。 */ + private static boolean matchesSlotFluid(LinkFilterSlot slot, FluidStack fluid) { + if (slot.isFluid()) { + return com.sorrowmist.useless.compat.forge1201.fluid.FluidStacks.same(slot.fluid(), fluid); + } + if (slot.isPattern()) { + LinkFilterPattern pattern = LinkFilterPattern.parse(slot.pattern()); + return pattern != null && pattern.matchesFluid(fluid); + } + return false; + } + + /** + * 这一格是否匹配这个化学品。 + * + *

具体标记走「罐里装的化学品」;模式走 id 通配(化学品没有标签体系, + * 见 {@link LinkFilterPattern#matchesChemicalId})。

+ */ + private static boolean matchesSlotChemical(LinkFilterSlot slot, ChemicalStackView chemical) { + ChemicalCompatProvider provider = ChemicalCompatProviders.get(); + if (!provider.isAvailable()) { + return false; + } + if (slot.isItem()) { + ChemicalStackView contained = provider.chemicalInItem(slot.item()); + return contained != null && chemical.isSameType(contained); + } + if (slot.isPattern()) { + LinkFilterPattern pattern = LinkFilterPattern.parse(slot.pattern()); + // 给不出 id 时通配符恒不匹配:宁可什么都不搬(可观察、玩家能改), + // 也不要因为拿不到 id 就当「不限制」而把整张网络灌出去。 + return pattern != null && pattern.matchesChemicalId(provider.chemicalIdOf(chemical.typeKey())); + } + return false; + } + + /** 某一端有没有「模式」标记(只要有模式,这一端的搬运就必须走扫描 + 谓词)。 */ + private static boolean hasPatternMarker(List filter) { + for (LinkFilterSlot slot : filter) { + if (!slot.isEmpty() && slot.isPattern()) { + return true; + } + } + return false; + } + + /** + * 化学品过滤:具体标记是「装有该化学品的储罐物品」,比对的是罐里装的化学品种类; + * 模式标记按 id 通配。 + * + *

白名单/黑名单规则与物品侧一致:命中黑名单格 ⇒ 不允许;否则这一端有白名单格就必须命中一个; + * 没有白名单格 ⇒ 不限制。化学品不支持控制条件({@code ChemicalHandlerView} 没有存量查询), + * 所以这里只看标记。

+ */ + private static boolean matchesChemical(List filter, ChemicalStackView chemical) { + ChemicalCompatProvider provider = ChemicalCompatProviders.get(); + if (!provider.isAvailable()) { + return true; + } + boolean hasInclude = false; + boolean includeApplies = false; + for (LinkFilterSlot slot : filter) { + if (!slot.isItem() && !slot.isPattern()) { + continue; + } + if (!slot.exclude()) { + hasInclude = true; + } + if (!matchesSlotChemical(slot, chemical)) { + continue; + } + if (slot.exclude()) { + return false; + } + includeApplies = true; + } + return !hasInclude || includeApplies; + } +} diff --git a/src/main/java/com/sorrowmist/useless/core/common/FlyEffectedHolder.java b/src/main/java/com/sorrowmist/useless/core/common/FlyEffectedHolder.java new file mode 100644 index 00000000..2e76b9ad --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/core/common/FlyEffectedHolder.java @@ -0,0 +1,69 @@ +package com.sorrowmist.useless.core.common; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.world.entity.player.Player; + +/** + * 造化杖飞行授予标记。 + *

+ * 标记写在玩家的持久化数据里,而不是内存集合里。原因:{@code Abilities.mayfly} 本身会被写进 + * 玩家 NBT。若「该飞行由模组授予」只记录在内存中,则服务器重启或单人存档重载后该标记即丢失, + * 玩家离线期间保留的 mayfly 无法回收,表现为未持有造化杖仍可永久创造飞行。 + */ +public final class FlyEffectedHolder { + private static final String ROOT_TAG = "useless_mod:beef_tool_flight"; + private static final String GRANTED_TAG = "granted"; + + private FlyEffectedHolder() {} // 防止实例化 + + /** 标记该玩家的飞行由造化杖授予,返回是否为新标记。 */ + public static boolean add(Player player) { + CompoundTag data = getOrCreate(player); + if (data.getBoolean(GRANTED_TAG)) { + return false; + } + data.putBoolean(GRANTED_TAG, true); + return true; + } + + /** 该玩家的飞行当前是否由造化杖授予(跨存档重载有效)。 */ + public static boolean contains(Player player) { + return get(player).getBoolean(GRANTED_TAG); + } + + /** 清除授予标记,返回此前是否处于「由造化杖授予」状态。 */ + public static boolean remove(Player player) { + CompoundTag data = get(player); + if (!data.getBoolean(GRANTED_TAG)) { + return false; + } + data.remove(GRANTED_TAG); + return true; + } + + private static CompoundTag get(Player player) { + CompoundTag persistentData = player.getPersistentData(); + if (!persistentData.contains(Player.PERSISTED_NBT_TAG, Tag.TAG_COMPOUND)) { + return new CompoundTag(); + } + + CompoundTag playerData = persistentData.getCompound(Player.PERSISTED_NBT_TAG); + return playerData.contains(ROOT_TAG, Tag.TAG_COMPOUND) + ? playerData.getCompound(ROOT_TAG) + : new CompoundTag(); + } + + private static CompoundTag getOrCreate(Player player) { + CompoundTag persistentData = player.getPersistentData(); + if (!persistentData.contains(Player.PERSISTED_NBT_TAG, Tag.TAG_COMPOUND)) { + persistentData.put(Player.PERSISTED_NBT_TAG, new CompoundTag()); + } + + CompoundTag playerData = persistentData.getCompound(Player.PERSISTED_NBT_TAG); + if (!playerData.contains(ROOT_TAG, Tag.TAG_COMPOUND)) { + playerData.put(ROOT_TAG, new CompoundTag()); + } + return playerData.getCompound(ROOT_TAG); + } +} diff --git a/src/main/java/com/sorrowmist/useless/core/common/KeyBindings.java b/src/main/java/com/sorrowmist/useless/core/common/KeyBindings.java new file mode 100644 index 00000000..96c0844f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/core/common/KeyBindings.java @@ -0,0 +1,210 @@ +package com.sorrowmist.useless.core.common; + +import com.mojang.blaze3d.platform.InputConstants; +import com.sorrowmist.useless.compat.ftbultimine.FtbUltimineChainKeyCompat; +import net.minecraft.client.KeyMapping; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.client.settings.IKeyConflictContext; +import net.minecraftforge.client.settings.KeyConflictContext; +import net.minecraftforge.client.settings.KeyModifier; +import net.minecraftforge.common.util.Lazy; +import org.lwjgl.glfw.GLFW; + +/** + * Useless Mod 的所有按键绑定定义 + */ +public class KeyBindings { + + // ==================== 按键分类和翻译键 ==================== + private static final String CATEGORY = "key.category.useless_mod.useless"; + + // 精准 (Silk Touch) + private static final String SWITCH_SILK_TOUCH = "key.useless_mod.switch_silk_touch"; + // ==================== 懒加载的 KeyMapping ==================== + public static final Lazy SWITCH_SILK_TOUCH_KEY = Lazy.of(() -> new KeyMapping( + SWITCH_SILK_TOUCH, + KeyConflictContext.IN_GAME, + InputConstants.Type.KEYSYM, + GLFW.GLFW_KEY_PAGE_DOWN, + CATEGORY + )); + // 时运(Fortune) + private static final String SWITCH_FORTUNE = "key.useless_mod.switch_fortune"; + public static final Lazy SWITCH_FORTUNE_KEY = Lazy.of(() -> new KeyMapping( + SWITCH_FORTUNE, + KeyConflictContext.IN_GAME, + InputConstants.Type.KEYSYM, + GLFW.GLFW_KEY_PAGE_UP, + CATEGORY + )); + // 连锁模式切换(普通 ↔ 增强) + private static final String TOGGLE_CHAIN_MODE = "key.useless_mod.toggle_chain_mode"; + public static final Lazy TOGGLE_CHAIN_MODE_KEY = Lazy.of(() -> new KeyMapping( + TOGGLE_CHAIN_MODE, + KeyConflictContext.IN_GAME, + InputConstants.Type.KEYSYM, + GLFW.GLFW_KEY_V, + CATEGORY + )); + // 模式轮盘 + private static final String SWITCH_MODE_WHEEL = "key.useless_mod.switch_mode_wheel"; + public static final Lazy SWITCH_MODE_WHEEL_KEY = Lazy.of(() -> new KeyMapping( + SWITCH_MODE_WHEEL, + KeyConflictContext.IN_GAME, + InputConstants.Type.KEYSYM, + GLFW.GLFW_KEY_G, + CATEGORY + )); + // 强制挖掘功能 + private static final String SWITCH_FORCE_MINING = "key.useless_mod.force_mining"; + public static final Lazy SWITCH_FORCE_MINING_KEY = Lazy.of(() -> new KeyMapping( + SWITCH_FORCE_MINING, + KeyConflictContext.IN_GAME, + InputConstants.Type.KEYSYM, + GLFW.GLFW_KEY_KP_9, + CATEGORY + )); + // 触发强制挖掘 + private static final String TRIGGER_FORCE_MINING = "key.useless_mod.trigger_force_mining"; + public static final Lazy TRIGGER_FORCE_MINING_KEY = Lazy.of(() -> new KeyMapping( + TRIGGER_FORCE_MINING, + KeyConflictContext.IN_GAME, + InputConstants.Type.KEYSYM, + GLFW.GLFW_KEY_R, + CATEGORY + )); + // Tab键触发连锁挖掘 + private static final String TRIGGER_CHAIN_MINING = "key.useless_mod.trigger_chain_mining"; + public static final Lazy TRIGGER_CHAIN_MINING_KEY = Lazy.of(() -> new KeyMapping( + TRIGGER_CHAIN_MINING, + KeyConflictContext.IN_GAME, + InputConstants.Type.KEYSYM, + GLFW.GLFW_KEY_TAB, + CATEGORY + )); + /** + * 连锁键是否按下。 + * + *

连锁可由本模组的 Tab 键或 FTB Ultimine 的连锁键触发,两者语义等价。 + * + *

FTB 的键位不在此处自行注册,而是读取它自己的绑定:造化杖虽已被 + * {@code #ftbultimine:excluded_tools} 排除出 FTB 的连锁,其按键绑定依然存在且仍由玩家配置。 + * 自行注册一个同键位的绑定会在按键设置界面产生冲突提示,且玩家改动 FTB 键位后必须同步 + * 改动两处,读取则天然跟随。 + * + *

所有连锁相关判定——发送按键包、高亮预览、状态面板、Shift 滚轮切换形状、R 键连锁—— + * 都必须经由此方法,否则会出现「能挖掘但无高亮」这类两端不一致的表现。 + */ + public static boolean isChainMiningKeyDown() { + return TRIGGER_CHAIN_MINING_KEY.get().isDown() + || (ModList.get().isLoaded("ftbultimine") + && FtbUltimineChainKeyCompat.isChainKeyDown()); + } + + // 耕地/草径模式切换(右键泥土时的优先行为) + private static final String SWITCH_FARMLAND_MODE = "key.useless_mod.switch_farmland_mode"; + public static final Lazy SWITCH_FARMLAND_MODE_KEY = Lazy.of(() -> new KeyMapping( + SWITCH_FARMLAND_MODE, + KeyConflictContext.IN_GAME, + InputConstants.Type.KEYSYM, + GLFW.GLFW_KEY_H, + CATEGORY + )); + // 顺手收菜开关 + private static final String TOGGLE_CROP_HARVEST = "key.useless_mod.toggle_crop_harvest"; + public static final Lazy TOGGLE_CROP_HARVEST_KEY = Lazy.of(() -> new KeyMapping( + TOGGLE_CROP_HARVEST, + KeyConflictContext.IN_GAME, + InputConstants.Type.KEYSYM, + GLFW.GLFW_KEY_J, + CATEGORY + )); + // 剪刀功能开关 + private static final String TOGGLE_SHEARS = "key.useless_mod.toggle_shears"; + public static final Lazy TOGGLE_SHEARS_KEY = Lazy.of(() -> new KeyMapping( + TOGGLE_SHEARS, + KeyConflictContext.IN_GAME, + InputConstants.Type.KEYSYM, + GLFW.GLFW_KEY_K, + CATEGORY + )); + // 打火石功能开关 + private static final String TOGGLE_FLINT_AND_STEEL = "key.useless_mod.toggle_flint_and_steel"; + public static final Lazy TOGGLE_FLINT_AND_STEEL_KEY = Lazy.of(() -> new KeyMapping( + TOGGLE_FLINT_AND_STEEL, + KeyConflictContext.IN_GAME, + InputConstants.Type.KEYSYM, + GLFW.GLFW_KEY_L, + CATEGORY + )); + /** + * 恒为非激活的冲突上下文。 + * + *

用于保留组合键在按键设置中的可配置性,同时使其不参与运行时分发。 + * 若改用 {@code KeyConflictContext.IN_GAME},按住 Shift 时同键位的原版 + * {@code keyUse} 会被该修饰键桶遮蔽,方块交互将整体失效。

+ */ + private static final IKeyConflictContext ALWAYS_INACTIVE = new IKeyConflictContext() { + @Override + public boolean isActive() { + return false; + } + + @Override + public boolean conflicts(IKeyConflictContext other) { + return false; + } + }; + + // 短距传送(造化杖) + private static final String SHORT_TELEPORT = "key.useless_mod.short_teleport"; + + /** + * 短距传送绑定。 + * + *

该绑定挂在恒为非激活的冲突上下文上:它在按键设置界面中可见、可改键, + * 但无法通过 {@code KeyMappingLookup} 的激活过滤。按住 Shift 时其所在修饰键桶 + * 过滤后为空,查找逻辑转而回退到无修饰键桶,原版 {@code keyUse} 因而仍能取得 + * 该次点击,方块交互不受影响。触发判定不经过按键分发,改由 + * {@code ClientEventBusSubscriber} 在 {@code InputEvent.MouseButton.Pre} 与 + * {@code InputEvent.Key} 中按物理按下边沿完成,使鼠标主键与键盘主键均可触发。

+ */ + public static final Lazy SHORT_TELEPORT_KEY = Lazy.of(() -> new KeyMapping( + SHORT_TELEPORT, + ALWAYS_INACTIVE, + KeyModifier.SHIFT, + InputConstants.Type.MOUSE, + GLFW.GLFW_MOUSE_BUTTON_RIGHT, + CATEGORY + )); + + // 连点模式开关(默认未绑定,由玩家在「按键设置」里自行指定) + private static final String TOGGLE_AUTO_CLICK = "key.useless_mod.toggle_auto_click"; + public static final Lazy TOGGLE_AUTO_CLICK_KEY = Lazy.of(() -> new KeyMapping( + TOGGLE_AUTO_CLICK, + KeyConflictContext.IN_GAME, + InputConstants.Type.KEYSYM, + GLFW.GLFW_KEY_UNKNOWN, + CATEGORY + )); + + // 打开无线物流配置界面(仅手持造化杖时生效) + private static final String OPEN_WIRELESS_LOGISTICS = "key.useless_mod.open_wireless_logistics"; + public static final Lazy OPEN_WIRELESS_LOGISTICS_KEY = Lazy.of(() -> new KeyMapping( + OPEN_WIRELESS_LOGISTICS, + KeyConflictContext.IN_GAME, + InputConstants.Type.KEYSYM, + GLFW.GLFW_KEY_B, + CATEGORY + )); + + // 杀戮光环开关(默认未绑定,由玩家在「按键设置」里自行指定) + private static final String TOGGLE_KILL_AURA = "key.useless_mod.toggle_kill_aura"; + public static final Lazy TOGGLE_KILL_AURA_KEY = Lazy.of(() -> new KeyMapping( + TOGGLE_KILL_AURA, + KeyConflictContext.IN_GAME, + InputConstants.Type.KEYSYM, + GLFW.GLFW_KEY_UNKNOWN, + CATEGORY + )); +} diff --git a/src/main/java/com/sorrowmist/useless/core/component/ExternalInventoryKind.java b/src/main/java/com/sorrowmist/useless/core/component/ExternalInventoryKind.java new file mode 100644 index 00000000..bb554df9 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/core/component/ExternalInventoryKind.java @@ -0,0 +1,53 @@ +package com.sorrowmist.useless.core.component; + +import com.mojang.serialization.Codec; +import com.sorrowmist.useless.compat.forge1201.network.ByteBufCodecs; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; + +public enum ExternalInventoryKind { + FURNACE(0, "furnace"), + PATTERN_ASSEMBLY(1, "pattern_assembly"), + MOLD_HUB(2, "mold_hub"), + PASSIVE_HATCH(3, "passive_hatch"), + /** + * 紧凑 F9 内部 176 条影子样板总线的整份持久化数据(不是物品栏,而是一段原始 NBT)。 + * 它跟着主机身上的 UUID 引用走,方块被拆掉时数据留在外置存储里等掉落物把它带回来。 + */ + COMPACT_PATTERN_BUS(4, "compact_pattern_bus"); + + public static final Codec CODEC = Codec.STRING.xmap( + ExternalInventoryKind::fromName, + ExternalInventoryKind::serializedName); + public static final StreamCodec STREAM_CODEC = + ByteBufCodecs.VAR_INT.map(ExternalInventoryKind::fromId, ExternalInventoryKind::networkId); + + private final int networkId; + private final String serializedName; + + ExternalInventoryKind(int networkId, String serializedName) { + this.networkId = networkId; + this.serializedName = serializedName; + } + + public int networkId() { + return networkId; + } + + public String serializedName() { + return serializedName; + } + + private static ExternalInventoryKind fromId(int id) { + for (ExternalInventoryKind kind : values()) { + if (kind.networkId == id) return kind; + } + throw new IllegalArgumentException("Unknown external inventory kind id: " + id); + } + + private static ExternalInventoryKind fromName(String name) { + for (ExternalInventoryKind kind : values()) { + if (kind.serializedName.equals(name)) return kind; + } + throw new IllegalArgumentException("Unknown external inventory kind: " + name); + } +} diff --git a/src/main/java/com/sorrowmist/useless/core/component/ExternalInventoryReference.java b/src/main/java/com/sorrowmist/useless/core/component/ExternalInventoryReference.java new file mode 100644 index 00000000..c69e33d0 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/core/component/ExternalInventoryReference.java @@ -0,0 +1,29 @@ +package com.sorrowmist.useless.core.component; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.UUIDUtil; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; + +import java.util.Objects; +import java.util.UUID; + +public record ExternalInventoryReference(ExternalInventoryKind kind, UUID id) { + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + ExternalInventoryKind.CODEC.fieldOf("kind").forGetter(ExternalInventoryReference::kind), + UUIDUtil.CODEC.fieldOf("id").forGetter(ExternalInventoryReference::id) + ).apply(instance, ExternalInventoryReference::new)); + + public static final StreamCodec STREAM_CODEC = + StreamCodec.composite( + ExternalInventoryKind.STREAM_CODEC, + ExternalInventoryReference::kind, + com.sorrowmist.useless.compat.forge1201.network.PortCodecs.UUID_CODEC, + ExternalInventoryReference::id, + ExternalInventoryReference::new); + + public ExternalInventoryReference { + Objects.requireNonNull(kind, "kind"); + Objects.requireNonNull(id, "id"); + } +} diff --git a/src/main/java/com/sorrowmist/useless/core/component/FurnaceDataComponent.java b/src/main/java/com/sorrowmist/useless/core/component/FurnaceDataComponent.java new file mode 100644 index 00000000..586548fb --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/core/component/FurnaceDataComponent.java @@ -0,0 +1,46 @@ +package com.sorrowmist.useless.core.component; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import com.sorrowmist.useless.compat.forge1201.component.DataComponentType; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.ByteBufCodecs; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; + +import java.util.function.UnaryOperator; + +/** + * 熔炉数据组件 - 用于存储高级合金炉的完整状态到物品中 + * 包含阶级、物品栏、流体、能量等所有数据 + */ +public record FurnaceDataComponent(int tier, CompoundTag data) { + + // Codec 用于持久化存储 + public static final Codec CODEC = RecordCodecBuilder.create(instance -> + instance.group( + Codec.INT.fieldOf("tier").forGetter(FurnaceDataComponent::tier), + CompoundTag.CODEC.fieldOf("data").forGetter(FurnaceDataComponent::data) + ).apply(instance, FurnaceDataComponent::new) + ); + + // StreamCodec 用于网络同步 + public static final StreamCodec STREAM_CODEC = + StreamCodec.composite( + ByteBufCodecs.INT, + FurnaceDataComponent::tier, + ByteBufCodecs.COMPOUND_TAG, + FurnaceDataComponent::data, + FurnaceDataComponent::new + ); + + public static UnaryOperator> builder() { + return builder -> builder + .persistent(CODEC) + .networkSynchronized(STREAM_CODEC); + } + + public static FurnaceDataComponent empty() { + return new FurnaceDataComponent(0, new CompoundTag()); + } +} diff --git a/src/main/java/com/sorrowmist/useless/core/component/MultiblockPartData.java b/src/main/java/com/sorrowmist/useless/core/component/MultiblockPartData.java new file mode 100644 index 00000000..b164ea61 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/core/component/MultiblockPartData.java @@ -0,0 +1,65 @@ +package com.sorrowmist.useless.core.component; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.ByteBufCodecs; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import net.minecraftforge.items.ItemStackHandler; + +/** Portable inventory and user settings for multiblock furnace functional parts. */ +public record MultiblockPartData(int version, CompoundTag inventory, int intervalTicks, long multiplier) { + public static final int CURRENT_VERSION = 1; + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + Codec.INT.optionalFieldOf("version", CURRENT_VERSION).forGetter(MultiblockPartData::version), + CompoundTag.CODEC.fieldOf("inventory").forGetter(MultiblockPartData::inventory), + Codec.INT.optionalFieldOf("interval_ticks", 0).forGetter(MultiblockPartData::intervalTicks), + Codec.LONG.optionalFieldOf("multiplier", 0L).forGetter(MultiblockPartData::multiplier) + ).apply(instance, MultiblockPartData::new)); + public static final StreamCodec STREAM_CODEC = + StreamCodec.composite( + ByteBufCodecs.VAR_INT, MultiblockPartData::version, + ByteBufCodecs.COMPOUND_TAG, MultiblockPartData::inventory, + ByteBufCodecs.VAR_INT, MultiblockPartData::intervalTicks, + ByteBufCodecs.VAR_LONG, MultiblockPartData::multiplier, + MultiblockPartData::new); + + public MultiblockPartData { + inventory = inventory == null ? new CompoundTag() : inventory.copy(); + intervalTicks = Math.max(0, intervalTicks); + multiplier = Math.max(0L, multiplier); + } + + public static MultiblockPartData inventory( + ItemStackHandler inventory, HolderLookup.Provider registries) { + return new MultiblockPartData( + CURRENT_VERSION, inventory.serializeNBT(), 0, 0L); + } + + public static MultiblockPartData passiveHatch( + ItemStackHandler inventory, HolderLookup.Provider registries, + int intervalTicks, long multiplier) { + return new MultiblockPartData( + CURRENT_VERSION, inventory.serializeNBT(), intervalTicks, multiplier); + } + + @Override + public CompoundTag inventory() { + return inventory.copy(); + } + + public void restoreInventory(ItemStackHandler target, HolderLookup.Provider registries) { + target.deserializeNBT(inventory.copy()); + } + + public boolean hasInventoryContents() { + return !inventory.getList("Items", Tag.TAG_COMPOUND).isEmpty(); + } + + public boolean isEmpty() { + return !hasInventoryContents() && intervalTicks == 0 && multiplier == 0L; + } +} diff --git a/src/main/java/com/sorrowmist/useless/core/component/MultiblockRecoveryData.java b/src/main/java/com/sorrowmist/useless/core/component/MultiblockRecoveryData.java new file mode 100644 index 00000000..b310541f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/core/component/MultiblockRecoveryData.java @@ -0,0 +1,44 @@ +package com.sorrowmist.useless.core.component; + +import appeng.api.stacks.GenericStack; +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.ByteBufCodecs; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; + +import java.util.List; + +public record MultiblockRecoveryData( + int version, long energy, List contents, long automaticEnergyLimit) { + private static final int LEGACY_DEFAULT_VERSION = 1; + public static final int CURRENT_VERSION = 2; + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + Codec.INT.optionalFieldOf("version", LEGACY_DEFAULT_VERSION).forGetter(MultiblockRecoveryData::version), + Codec.LONG.optionalFieldOf("energy", 0L).forGetter(MultiblockRecoveryData::energy), + com.sorrowmist.useless.compat.forge1201.recipe.AE2Codecs.GENERIC_STACK.listOf().optionalFieldOf("contents", List.of()).forGetter(MultiblockRecoveryData::contents), + Codec.LONG.optionalFieldOf("automatic_energy_limit", Long.MAX_VALUE) + .forGetter(MultiblockRecoveryData::automaticEnergyLimit) + ).apply(instance, MultiblockRecoveryData::new)); + public static final StreamCodec STREAM_CODEC = + StreamCodec.composite( + ByteBufCodecs.VAR_INT, MultiblockRecoveryData::version, + ByteBufCodecs.VAR_LONG, MultiblockRecoveryData::energy, + com.sorrowmist.useless.compat.forge1201.recipe.AE2Codecs.GENERIC_STACK_STREAM.apply(ByteBufCodecs.list()), MultiblockRecoveryData::contents, + ByteBufCodecs.VAR_LONG, MultiblockRecoveryData::automaticEnergyLimit, + MultiblockRecoveryData::new); + + public MultiblockRecoveryData { + energy = Math.max(0L, energy); + contents = List.copyOf(contents == null ? List.of() : contents); + automaticEnergyLimit = Math.max(0L, automaticEnergyLimit); + } + + public MultiblockRecoveryData(int version, long energy, List contents) { + this(version, energy, contents, Long.MAX_VALUE); + } + + public boolean isEmpty() { + return energy <= 0L && contents.isEmpty() && automaticEnergyLimit == Long.MAX_VALUE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/core/component/OmniversalPatternData.java b/src/main/java/com/sorrowmist/useless/core/component/OmniversalPatternData.java new file mode 100644 index 00000000..9b289221 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/core/component/OmniversalPatternData.java @@ -0,0 +1,342 @@ +package com.sorrowmist.useless.core.component; + +import appeng.api.stacks.AEItemKey; +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeIdentity; +import io.netty.handler.codec.DecoderException; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.ByteBufCodecs; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.item.Item; + +import java.util.List; +import java.util.Optional; + +public record OmniversalPatternData( + int version, + ResourceLocation recipeId, + String recipeFingerprint, + String sourceId, + boolean requiresMold, + Optional displayMold, + List displayMolds, + List tagInputSlots, + List fluidTagInputSlots, + List moldTagInputSlots, + List itemIdInputSlots, + List itemIdOutputSlots) { + private static final int LEGACY_DEFAULT_VERSION = 1; + public static final int SEMANTIC_FINGERPRINT_VERSION = 2; + public static final int MULTI_MOLD_VERSION = 3; + public static final int TAG_INPUT_VERSION = 4; + public static final int FLUID_TAG_INPUT_VERSION = 5; + public static final int MOLD_TAG_INPUT_VERSION = 6; + public static final int SOURCE_ID_VERSION = 7; + public static final int CURRENT_VERSION = SOURCE_ID_VERSION; + + public record TagInputSlot(int slot, TagKey tag) { + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + Codec.INT.fieldOf("slot").forGetter(TagInputSlot::slot), + TagKey.codec(Registries.ITEM).fieldOf("tag").forGetter(TagInputSlot::tag) + ).apply(instance, TagInputSlot::new)); + + public static final StreamCodec STREAM_CODEC = + StreamCodec.composite( + ByteBufCodecs.VAR_INT, TagInputSlot::slot, + ByteBufCodecs.fromCodecWithRegistries(TagKey.codec(Registries.ITEM)), TagInputSlot::tag, + TagInputSlot::new); + + public TagInputSlot { + if (slot < 0) { + throw new IllegalArgumentException("Tag input slot cannot be negative: " + slot); + } + if (tag == null) { + throw new NullPointerException("tag"); + } + } + } + + public record FluidTagInputSlot(int slot, TagKey tag) { + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + Codec.INT.fieldOf("slot").forGetter(FluidTagInputSlot::slot), + TagKey.codec(Registries.FLUID).fieldOf("tag").forGetter(FluidTagInputSlot::tag) + ).apply(instance, FluidTagInputSlot::new)); + + public static final StreamCodec STREAM_CODEC = + StreamCodec.composite( + ByteBufCodecs.VAR_INT, FluidTagInputSlot::slot, + ByteBufCodecs.fromCodecWithRegistries(TagKey.codec(Registries.FLUID)), FluidTagInputSlot::tag, + FluidTagInputSlot::new); + + public FluidTagInputSlot { + if (slot < 0) { + throw new IllegalArgumentException("Fluid tag input slot cannot be negative: " + slot); + } + if (tag == null) { + throw new NullPointerException("tag"); + } + } + } + + /** A tag-backed mold requirement, indexed by the independent mold slot in the recipe. */ + public record MoldTagInputSlot(int moldSlot, TagKey tag) { + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + Codec.INT.fieldOf("mold_slot").forGetter(MoldTagInputSlot::moldSlot), + TagKey.codec(Registries.ITEM).fieldOf("tag").forGetter(MoldTagInputSlot::tag) + ).apply(instance, MoldTagInputSlot::new)); + + public static final StreamCodec STREAM_CODEC = + StreamCodec.composite( + ByteBufCodecs.VAR_INT, MoldTagInputSlot::moldSlot, + ByteBufCodecs.fromCodecWithRegistries(TagKey.codec(Registries.ITEM)), MoldTagInputSlot::tag, + MoldTagInputSlot::new); + + public MoldTagInputSlot { + if (moldSlot < 0) { + throw new IllegalArgumentException("Mold tag input slot cannot be negative: " + moldSlot); + } + if (tag == null) { + throw new NullPointerException("tag"); + } + } + } + + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + // Version one was omitted by its original codec, so its decode + // default must remain stable after introducing newer versions. + Codec.INT.optionalFieldOf("version", LEGACY_DEFAULT_VERSION).forGetter(OmniversalPatternData::version), + ResourceLocation.CODEC.fieldOf("recipe_id").forGetter(OmniversalPatternData::recipeId), + Codec.STRING.fieldOf("recipe_fingerprint").forGetter(OmniversalPatternData::recipeFingerprint), + Codec.STRING.optionalFieldOf("source_id", "").forGetter(OmniversalPatternData::sourceId), + Codec.BOOL.optionalFieldOf("requires_mold", false).forGetter(OmniversalPatternData::requiresMold), + com.sorrowmist.useless.compat.forge1201.recipe.AE2Codecs.ITEM_KEY.optionalFieldOf("display_mold").forGetter(OmniversalPatternData::displayMold), + com.sorrowmist.useless.compat.forge1201.recipe.AE2Codecs.ITEM_KEY.listOf().optionalFieldOf("display_molds", List.of()).forGetter(OmniversalPatternData::displayMolds), + TagInputSlot.CODEC.listOf().optionalFieldOf("tag_inputs", List.of()).forGetter(OmniversalPatternData::tagInputSlots), + FluidTagInputSlot.CODEC.listOf().optionalFieldOf("fluid_tag_inputs", List.of()).forGetter(OmniversalPatternData::fluidTagInputSlots), + MoldTagInputSlot.CODEC.listOf().optionalFieldOf("mold_tag_inputs", List.of()).forGetter(OmniversalPatternData::moldTagInputSlots), + Codec.INT.listOf().optionalFieldOf("item_id_inputs", List.of()).forGetter(OmniversalPatternData::itemIdInputSlots), + Codec.INT.listOf().optionalFieldOf("item_id_outputs", List.of()).forGetter(OmniversalPatternData::itemIdOutputSlots) + ).apply(instance, OmniversalPatternData::new)); + + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + OmniversalPatternData::write, + OmniversalPatternData::read); + + public OmniversalPatternData { + sourceId = sourceId == null ? "" : sourceId; + displayMold = displayMold == null ? Optional.empty() : displayMold; + displayMolds = List.copyOf(displayMolds == null ? List.of() : displayMolds); + tagInputSlots = List.copyOf(tagInputSlots == null ? List.of() : tagInputSlots); + fluidTagInputSlots = List.copyOf(fluidTagInputSlots == null ? List.of() : fluidTagInputSlots); + moldTagInputSlots = List.copyOf(moldTagInputSlots == null ? List.of() : moldTagInputSlots); + itemIdInputSlots = List.copyOf(itemIdInputSlots == null ? List.of() : itemIdInputSlots); + itemIdOutputSlots = List.copyOf(itemIdOutputSlots == null ? List.of() : itemIdOutputSlots); + } + + /** Compatibility constructor for metadata written before tag input slots were introduced. */ + public OmniversalPatternData( + int version, + ResourceLocation recipeId, + String recipeFingerprint, + boolean requiresMold, + Optional displayMold, + List displayMolds, + List itemIdInputSlots, + List itemIdOutputSlots) { + this(version, recipeId, recipeFingerprint, "", requiresMold, displayMold, displayMolds, + List.of(), List.of(), List.of(), itemIdInputSlots, itemIdOutputSlots); + } + + /** Compatibility constructor for metadata written after item tags but before fluid tags. */ + public OmniversalPatternData( + int version, + ResourceLocation recipeId, + String recipeFingerprint, + boolean requiresMold, + Optional displayMold, + List displayMolds, + List tagInputSlots, + List itemIdInputSlots, + List itemIdOutputSlots) { + this(version, recipeId, recipeFingerprint, "", requiresMold, displayMold, displayMolds, + tagInputSlots, List.of(), List.of(), itemIdInputSlots, itemIdOutputSlots); + } + + /** Compatibility constructor for metadata written after fluid tags but before mold tags. */ + public OmniversalPatternData( + int version, + ResourceLocation recipeId, + String recipeFingerprint, + boolean requiresMold, + Optional displayMold, + List displayMolds, + List tagInputSlots, + List fluidTagInputSlots, + List itemIdInputSlots, + List itemIdOutputSlots) { + this(version, recipeId, recipeFingerprint, "", requiresMold, displayMold, displayMolds, + tagInputSlots, fluidTagInputSlots, List.of(), itemIdInputSlots, itemIdOutputSlots); + } + + /** Compatibility constructor for the complete v6 shape. */ + public OmniversalPatternData( + int version, + ResourceLocation recipeId, + String recipeFingerprint, + boolean requiresMold, + Optional displayMold, + List displayMolds, + List tagInputSlots, + List fluidTagInputSlots, + List moldTagInputSlots, + List itemIdInputSlots, + List itemIdOutputSlots) { + this(version, recipeId, recipeFingerprint, "", requiresMold, displayMold, displayMolds, + tagInputSlots, fluidTagInputSlots, moldTagInputSlots, + itemIdInputSlots, itemIdOutputSlots); + } + + /** Compatibility constructor for metadata written before display_molds was introduced. */ + public OmniversalPatternData( + int version, + ResourceLocation recipeId, + String recipeFingerprint, + boolean requiresMold, + Optional displayMold, + List itemIdInputSlots, + List itemIdOutputSlots) { + this(version, recipeId, recipeFingerprint, "", requiresMold, displayMold, + List.of(), List.of(), List.of(), List.of(), + itemIdInputSlots, itemIdOutputSlots); + } + + public AlloyFurnaceRecipeIdentity identity() { + return new AlloyFurnaceRecipeIdentity(recipeId, recipeFingerprint); + } + + private static void write(FriendlyByteBuf buffer, OmniversalPatternData data) { + buffer.writeVarInt(data.version); + com.sorrowmist.useless.compat.forge1201.network.PortCodecs.RESOURCE_LOCATION.encode(buffer, data.recipeId); + buffer.writeUtf(data.recipeFingerprint); + if (data.version >= SOURCE_ID_VERSION) buffer.writeUtf(data.sourceId); + buffer.writeBoolean(data.requiresMold); + buffer.writeBoolean(data.displayMold.isPresent()); + data.displayMold.ifPresent(mold -> mold.writeToPacket(buffer)); + if (data.version >= MULTI_MOLD_VERSION) { + writeMolds(buffer, data.displayMolds); + } + if (data.version >= TAG_INPUT_VERSION) { + writeTagInputs(buffer, data.tagInputSlots); + } + if (data.version >= FLUID_TAG_INPUT_VERSION) { + writeFluidTagInputs(buffer, data.fluidTagInputSlots); + } + if (data.version >= MOLD_TAG_INPUT_VERSION) { + writeMoldTagInputs(buffer, data.moldTagInputSlots); + } + writeInts(buffer, data.itemIdInputSlots); + writeInts(buffer, data.itemIdOutputSlots); + } + + private static OmniversalPatternData read(FriendlyByteBuf buffer) { + int version = buffer.readVarInt(); + ResourceLocation recipeId = com.sorrowmist.useless.compat.forge1201.network.PortCodecs.RESOURCE_LOCATION.decode(buffer); + String fingerprint = buffer.readUtf(); + String sourceId = version >= SOURCE_ID_VERSION ? buffer.readUtf() : ""; + boolean requiresMold = buffer.readBoolean(); + Optional displayMold = buffer.readBoolean() + ? Optional.of(AEItemKey.fromPacket(buffer)) + : Optional.empty(); + List displayMolds = version >= MULTI_MOLD_VERSION + ? readMolds(buffer) : List.of(); + List tagInputSlots = version >= TAG_INPUT_VERSION + ? readTagInputs(buffer) : List.of(); + List fluidTagInputSlots = version >= FLUID_TAG_INPUT_VERSION + ? readFluidTagInputs(buffer) : List.of(); + List moldTagInputSlots = version >= MOLD_TAG_INPUT_VERSION + ? readMoldTagInputs(buffer) : List.of(); + return new OmniversalPatternData( + version, recipeId, fingerprint, sourceId, requiresMold, displayMold, displayMolds, + tagInputSlots, fluidTagInputSlots, moldTagInputSlots, readInts(buffer), readInts(buffer)); + } + + private static void writeMolds(FriendlyByteBuf buffer, List molds) { + buffer.writeVarInt(molds.size()); + for (AEItemKey mold : molds) mold.writeToPacket(buffer); + } + + private static List readMolds(FriendlyByteBuf buffer) { + int size = buffer.readVarInt(); + if (size < 0 || size > 256) { + throw new DecoderException("Omniversal pattern mold list exceeds 256 entries"); + } + java.util.ArrayList result = new java.util.ArrayList<>(size); + for (int i = 0; i < size; i++) result.add(AEItemKey.fromPacket(buffer)); + return List.copyOf(result); + } + + private static void writeTagInputs(FriendlyByteBuf buffer, List slots) { + buffer.writeVarInt(slots.size()); + for (TagInputSlot slot : slots) TagInputSlot.STREAM_CODEC.encode(buffer, slot); + } + + private static List readTagInputs(FriendlyByteBuf buffer) { + int size = buffer.readVarInt(); + if (size < 0 || size > 256) { + throw new DecoderException("Omniversal pattern tag input list exceeds 256 entries"); + } + java.util.ArrayList result = new java.util.ArrayList<>(size); + for (int i = 0; i < size; i++) result.add(TagInputSlot.STREAM_CODEC.decode(buffer)); + return List.copyOf(result); + } + + private static void writeFluidTagInputs(FriendlyByteBuf buffer, List slots) { + buffer.writeVarInt(slots.size()); + for (FluidTagInputSlot slot : slots) FluidTagInputSlot.STREAM_CODEC.encode(buffer, slot); + } + + private static List readFluidTagInputs(FriendlyByteBuf buffer) { + int size = buffer.readVarInt(); + if (size < 0 || size > 256) { + throw new DecoderException("Omniversal pattern fluid tag input list exceeds 256 entries"); + } + java.util.ArrayList result = new java.util.ArrayList<>(size); + for (int i = 0; i < size; i++) result.add(FluidTagInputSlot.STREAM_CODEC.decode(buffer)); + return List.copyOf(result); + } + + private static void writeMoldTagInputs(FriendlyByteBuf buffer, List slots) { + buffer.writeVarInt(slots.size()); + for (MoldTagInputSlot slot : slots) MoldTagInputSlot.STREAM_CODEC.encode(buffer, slot); + } + + private static List readMoldTagInputs(FriendlyByteBuf buffer) { + int size = buffer.readVarInt(); + if (size < 0 || size > 256) { + throw new DecoderException("Omniversal pattern mold tag list exceeds 256 entries"); + } + java.util.ArrayList result = new java.util.ArrayList<>(size); + for (int i = 0; i < size; i++) result.add(MoldTagInputSlot.STREAM_CODEC.decode(buffer)); + return List.copyOf(result); + } + + private static void writeInts(FriendlyByteBuf buffer, List values) { + buffer.writeVarInt(values.size()); + for (int value : values) buffer.writeVarInt(value); + } + + private static List readInts(FriendlyByteBuf buffer) { + int size = buffer.readVarInt(); + if (size < 0 || size > 256) { + throw new DecoderException("Omniversal pattern slot list exceeds 256 entries"); + } + java.util.ArrayList result = new java.util.ArrayList<>(size); + for (int i = 0; i < size; i++) result.add(buffer.readVarInt()); + return List.copyOf(result); + } +} diff --git a/src/main/java/com/sorrowmist/useless/core/component/PassiveHatchSettings.java b/src/main/java/com/sorrowmist/useless/core/component/PassiveHatchSettings.java new file mode 100644 index 00000000..9681bfd6 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/core/component/PassiveHatchSettings.java @@ -0,0 +1,99 @@ +package com.sorrowmist.useless.core.component; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.ByteBufCodecs; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; + +import java.util.ArrayList; +import java.util.List; + +/** + * Portable settings of a passive crafting hatch: the global interval, the global default + * multiplier, and the sparse per-slot multipliers that override that default. + */ +public record PassiveHatchSettings( + int intervalTicks, long multiplier, List slotMultipliers) { + /** Upper bound on the number of stored overrides; the hatch never reports more than this. */ + public static final int MAX_SLOT_ENTRIES = 4096; + + /** + * A single slot pinned to its own batch size. Absent slots follow the global multiplier, + * which is why clearing an override is expressed by removing the entry, not by storing 1. + */ + public record SlotMultiplier(int slot, long multiplier) { + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + Codec.INT.fieldOf("slot").forGetter(SlotMultiplier::slot), + Codec.LONG.fieldOf("multiplier").forGetter(SlotMultiplier::multiplier) + ).apply(instance, SlotMultiplier::new)); + public static final StreamCodec STREAM_CODEC = + StreamCodec.composite( + ByteBufCodecs.VAR_INT, SlotMultiplier::slot, + ByteBufCodecs.VAR_LONG, SlotMultiplier::multiplier, + SlotMultiplier::new); + + public SlotMultiplier { + slot = Math.max(0, slot); + multiplier = Math.max(0L, multiplier); + } + + public boolean valid() { + return slot < MAX_SLOT_ENTRIES && multiplier > 0L; + } + } + + private static final StreamCodec> MULTIPLIER_LIST_CODEC = + StreamCodec.of(PassiveHatchSettings::encodeSlotMultipliers, + PassiveHatchSettings::decodeSlotMultipliers); + + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + Codec.INT.fieldOf("interval_ticks").forGetter(PassiveHatchSettings::intervalTicks), + Codec.LONG.fieldOf("multiplier").forGetter(PassiveHatchSettings::multiplier), + SlotMultiplier.CODEC.listOf().optionalFieldOf("slot_multipliers", List.of()) + .forGetter(PassiveHatchSettings::slotMultipliers) + ).apply(instance, PassiveHatchSettings::new)); + public static final StreamCodec STREAM_CODEC = + StreamCodec.composite( + ByteBufCodecs.VAR_INT, PassiveHatchSettings::intervalTicks, + ByteBufCodecs.VAR_LONG, PassiveHatchSettings::multiplier, + MULTIPLIER_LIST_CODEC, PassiveHatchSettings::slotMultipliers, + PassiveHatchSettings::new); + + public PassiveHatchSettings { + intervalTicks = Math.max(0, intervalTicks); + multiplier = Math.max(0L, multiplier); + slotMultipliers = slotMultipliers == null + ? List.of() + : slotMultipliers.stream().filter(entry -> entry != null && entry.valid()) + .limit(MAX_SLOT_ENTRIES).toList(); + } + + /** Convenience constructor for hatches that only carry the global settings. */ + public PassiveHatchSettings(int intervalTicks, long multiplier) { + this(intervalTicks, multiplier, List.of()); + } + + public boolean hasSlotMultipliers() { + return !slotMultipliers.isEmpty(); + } + + private static void encodeSlotMultipliers(FriendlyByteBuf buffer, List entries) { + buffer.writeVarInt(entries.size()); + for (SlotMultiplier entry : entries) { + SlotMultiplier.STREAM_CODEC.encode(buffer, entry); + } + } + + private static List decodeSlotMultipliers(FriendlyByteBuf buffer) { + int size = buffer.readVarInt(); + if (size < 0 || size > MAX_SLOT_ENTRIES) { + throw new IllegalArgumentException("Invalid passive hatch slot multiplier count: " + size); + } + List entries = new ArrayList<>(size); + for (int index = 0; index < size; index++) { + entries.add(SlotMultiplier.STREAM_CODEC.decode(buffer)); + } + return List.copyOf(entries); + } +} diff --git a/src/main/java/com/sorrowmist/useless/core/component/PatternConverterData.java b/src/main/java/com/sorrowmist/useless/core/component/PatternConverterData.java new file mode 100644 index 00000000..627663d2 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/core/component/PatternConverterData.java @@ -0,0 +1,29 @@ +package com.sorrowmist.useless.core.component; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.ByteBufCodecs; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; + +/** Persistent inventory data for the omniversal pattern converter. */ +public record PatternConverterData(CompoundTag data) { + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + CompoundTag.CODEC.fieldOf("data").forGetter(PatternConverterData::data) + ).apply(instance, PatternConverterData::new)); + + public static final StreamCodec STREAM_CODEC = + StreamCodec.composite( + ByteBufCodecs.COMPOUND_TAG, PatternConverterData::data, + PatternConverterData::new); + + public PatternConverterData { + data = data == null ? new CompoundTag() : data.copy(); + } + + @Override + public CompoundTag data() { + return data.copy(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/core/component/RitualBlueprintPentacles.java b/src/main/java/com/sorrowmist/useless/core/component/RitualBlueprintPentacles.java new file mode 100644 index 00000000..24a14ce5 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/core/component/RitualBlueprintPentacles.java @@ -0,0 +1,68 @@ +package com.sorrowmist.useless.core.component; + +import com.mojang.serialization.Codec; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import net.minecraft.resources.ResourceLocation; + +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +/** Canonical set of Occultism pentacles imprinted on a ritual blueprint. */ +public record RitualBlueprintPentacles(List pentacles) { + private static final int MAX_PENTACLES = 256; + + public static final Codec CODEC = ResourceLocation.CODEC.listOf() + .xmap(RitualBlueprintPentacles::new, RitualBlueprintPentacles::pentacles); + + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + RitualBlueprintPentacles::write, RitualBlueprintPentacles::read); + + public RitualBlueprintPentacles { + pentacles = pentacles == null ? List.of() : pentacles.stream() + .filter(Objects::nonNull) + .distinct() + .sorted(Comparator.comparing(ResourceLocation::toString)) + .limit(MAX_PENTACLES) + .toList(); + } + + public static RitualBlueprintPentacles of(ResourceLocation pentacle) { + return new RitualBlueprintPentacles(List.of(pentacle)); + } + + public static RitualBlueprintPentacles of(Collection pentacles) { + return new RitualBlueprintPentacles(pentacles == null ? List.of() : List.copyOf(pentacles)); + } + + public boolean isEmpty() { + return pentacles.isEmpty(); + } + + public boolean contains(ResourceLocation pentacle) { + return pentacles.contains(pentacle); + } + + public boolean containsAll(RitualBlueprintPentacles required) { + return required != null && pentacles.containsAll(required.pentacles); + } + + private static void write(FriendlyByteBuf buffer, RitualBlueprintPentacles value) { + buffer.writeVarInt(value.pentacles.size()); + value.pentacles.forEach(buffer::writeResourceLocation); + } + + private static RitualBlueprintPentacles read(FriendlyByteBuf buffer) { + int size = buffer.readVarInt(); + if (size < 0 || size > MAX_PENTACLES) { + throw new IllegalArgumentException("Invalid ritual blueprint pentacle count: " + size); + } + java.util.ArrayList pentacles = new java.util.ArrayList<>(size); + for (int i = 0; i < size; i++) { + pentacles.add(buffer.readResourceLocation()); + } + return new RitualBlueprintPentacles(pentacles); + } +} diff --git a/src/main/java/com/sorrowmist/useless/core/component/UComponents.java b/src/main/java/com/sorrowmist/useless/core/component/UComponents.java new file mode 100644 index 00000000..7aff5f68 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/core/component/UComponents.java @@ -0,0 +1,588 @@ +package com.sorrowmist.useless.core.component; + +import com.mojang.serialization.Codec; +import com.sorrowmist.useless.api.enums.tool.EnchantMode; +import com.sorrowmist.useless.api.enums.tool.ConstructionWandCoreMode; +import com.sorrowmist.useless.api.enums.tool.ToolTypeMode; +import net.minecraft.core.GlobalPos; +import net.minecraft.core.UUIDUtil; +import com.sorrowmist.useless.compat.forge1201.component.DataComponentType; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.ByteBufCodecs; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import net.minecraftforge.eventbus.api.IEventBus; +import com.sorrowmist.useless.compat.forge1201.registry.DeferredHolder; +import com.sorrowmist.useless.compat.forge1201.registry.DeferredRegister; + +import java.util.List; +import java.util.UUID; +import java.util.function.UnaryOperator; + +public final class UComponents { + private static final DeferredRegister> DATA_COMPONENTS = + DeferredRegister.createLocal("useless_mod"); + + /** + * 附魔模式组件(EnchantMode) + * 用于在物品上存储当前的附魔模式(枚举类型 EnchantMode) + */ + public static final DeferredHolder, DataComponentType> EnchantModeComponent = + register("enchant_mode", builder -> + builder + // 持久化存储:使用字符串 Codec,将枚举转换为字符串保存(保存到 NBT/文件时) + // valueOf 用于从字符串转为枚举,Enum::name 用于从枚举转为字符串 + .persistent(Codec.STRING.xmap(EnchantMode::valueOf, Enum::name)) + // 网络同步:客户端与服务端同步时使用枚举的原生读写方式 + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeEnum, // 写入枚举 + buf -> buf.readEnum(EnchantMode.class) // 读取枚举 + )) + ); + + /** + * 当前工具类型组件(CurrentToolType) + * 用于在物品上存储当前选中的工具类型(枚举类型 ToolTypeMode) + * 例如多功能工具在不同模式(镐、斧、铲等)之间切换时使用 + */ + public static final DeferredHolder, DataComponentType> CurrentToolTypeComponent = + register("tool_type", builder -> + builder.persistent(Codec.STRING.xmap(ToolTypeMode::valueOf, Enum::name)) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeEnum, + buf -> buf.readEnum(ToolTypeMode.class) + )) + ); + + public static final DeferredHolder, DataComponentType> WrenchTagEnabledComponent = + register("wrench_tag_enabled", builder -> + builder.persistent(Codec.BOOL) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeBoolean, + FriendlyByteBuf::readBoolean + )) + ); + + public static final DeferredHolder, DataComponentType> ConstructionWandEnabledComponent = + register("construction_wand_enabled", builder -> + builder.persistent(Codec.BOOL) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeBoolean, + FriendlyByteBuf::readBoolean + )) + ); + + public static final DeferredHolder, DataComponentType> ConstructionWandCoreComponent = + register("construction_wand_core", builder -> + builder.persistent(Codec.STRING.xmap(ConstructionWandCoreMode::valueOf, Enum::name)) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeEnum, + buf -> buf.readEnum(ConstructionWandCoreMode.class) + )) + ); + + /** + * 土壤右键模式组件(BeefFarmlandMode) + * 造化杖同时具备铲子与锄头能力,右键泥土/草方块时谁先生效由此决定: + * false = 铲子优先(变为草径),true = 锄头优先(变为耕地)。 + */ + public static final DeferredHolder, DataComponentType> BeefFarmlandModeComponent = + register("beef_farmland_mode", builder -> + builder.persistent(Codec.BOOL) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeBoolean, + FriendlyByteBuf::readBoolean + )) + ); + + /** + * 「顺手收菜」组件(BeefCropHarvest) + * true = 右键成熟作物时由造化杖自身执行收获,并将种子保留于耕地(作物重置为 0 龄), + * 以避免整合包的右键收菜功能将作物连根拔起。 + */ + public static final DeferredHolder, DataComponentType> BeefCropHarvestComponent = + register("beef_crop_harvest", builder -> + builder.persistent(Codec.BOOL) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeBoolean, + FriendlyByteBuf::readBoolean + )) + ); + + /** + * 剪刀功能组件(BeefShears) + * true = 造化杖具备剪刀能力:可对羊、哞菇等 IShearable 实体剪毛/剪掉落, + * 也向依赖 ToolAction 的模组暴露 DEFAULT_SHEARS_ACTIONS。 + */ + public static final DeferredHolder, DataComponentType> BeefShearsComponent = + register("beef_shears", builder -> + builder.persistent(Codec.BOOL) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeBoolean, + FriendlyByteBuf::readBoolean + )) + ); + + /** + * 打火石功能组件(BeefFlintAndSteel) + * true = 造化杖具备打火石能力:右键可点燃营火/蜡烛,或在可放置火的位置点火。 + */ + public static final DeferredHolder, DataComponentType> BeefFlintAndSteelComponent = + register("beef_flint_and_steel", builder -> + builder.persistent(Codec.BOOL) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeBoolean, + FriendlyByteBuf::readBoolean + )) + ); + + /** + * 自动熔炼组件(AutoSmelt) + * true = 挖掘产生的掉落物先按原版烹饪配方(熔炉/高炉/烟熏炉)炼一遍再入包。 + * 只影响挖掘掉落,不改变方块本身的行为。 + */ + public static final DeferredHolder, DataComponentType> AutoSmeltComponent = + register("auto_smelt", builder -> + builder.persistent(Codec.BOOL) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeBoolean, + FriendlyByteBuf::readBoolean + )) + ); + + /** + * 增强连锁挖矿模式组件(EnhancedChainMiningMode) + * 用于在物品上存储是否启用增强连锁挖掘(布尔类型) + * true = 启用增强连锁挖掘,false = 使用普通连锁挖掘 + */ + public static final DeferredHolder, DataComponentType> EnhancedChainMiningComponent = + register("chain_mining", builder -> + builder.persistent(Codec.BOOL) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeBoolean, + FriendlyByteBuf::readBoolean + )) + ); + + /** + * 强制挖掘组件(ForceMining) + * 用于在物品上存储是否启用强制挖掘功能(布尔类型) + */ + public static final DeferredHolder, DataComponentType> ForceMiningComponent = + register("force_mining", builder -> + builder.persistent(Codec.BOOL) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeBoolean, + FriendlyByteBuf::readBoolean + )) + ); + + /** + * AE存储优先组件(AEStoragePriority) + * 用于在物品上存储是否启用AE存储优先功能(布尔类型) + */ + public static final DeferredHolder, DataComponentType> AEStoragePriorityComponent = + register("ae_storage_priority", builder -> + builder.persistent(Codec.BOOL) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeBoolean, + FriendlyByteBuf::readBoolean + )) + ); + + /** + * AE 连接模式组件(AeNetworkConnect) + * 开启后右键一台「拥有 AE 网格节点」的机器,把它的节点接入工具绑定无线访问点所在的那张网。 + * 与「顺手收菜 / 时间加速 / 建筑魔杖」互斥(同一时刻仅允许一项占用右键)。 + */ + public static final DeferredHolder, DataComponentType> AeNetworkConnectComponent = + register("ae_network_connect", builder -> + builder.persistent(Codec.BOOL) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeBoolean, + FriendlyByteBuf::readBoolean + )) + ); + + /** + * 匠心仪式挎包模式(RitualSatchel) + * 开启后右键已用魔典预览过的五芒星,可直接从绑定的 AE 网络取用方块摆放整座仪式。 + * 仅在安装了 occultism 时才会出现在模式轮盘里。 + */ + public static final DeferredHolder, DataComponentType> BeefRitualSatchelComponent = + register("beef_ritual_satchel", builder -> + builder.persistent(Codec.BOOL) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeBoolean, + FriendlyByteBuf::readBoolean + )) + ); + + /** + * Ex Deorum 钩子模式(ExDeorumCrook) + * true = 破坏方块时按 Ex Deorum 的钩子配方额外产出掉落(如树叶的蚕、线)。 + * 仅在安装了 exdeorum 时该模式才会出现在模式轮盘里。 + */ + public static final DeferredHolder, DataComponentType> ExDeorumCrookComponent = + register("exdeorum_crook", builder -> + builder.persistent(Codec.BOOL) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeBoolean, + FriendlyByteBuf::readBoolean + )) + ); + + /** + * Ex Deorum 锤子模式(ExDeorumHammer) + * true = 破坏方块时按 Ex Deorum 的锤子配方改写掉落,替代方块原有掉落。 + * 仅在安装了 exdeorum 时该模式才会出现在模式轮盘里。 + */ + public static final DeferredHolder, DataComponentType> ExDeorumHammerComponent = + register("exdeorum_hammer", builder -> + builder.persistent(Codec.BOOL) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeBoolean, + FriendlyByteBuf::readBoolean + )) + ); + + /** + * Ex Deorum 压缩锤模式(ExDeorumCompressedHammer) + * true = 破坏方块时按 Ex Deorum 的压缩锤配方改写掉落。压缩锤配方与普通锤配方是两套 + * 独立数据,因此单列为一项开关。仅在安装了 exdeorum 时该模式才会出现在模式轮盘里。 + */ + public static final DeferredHolder, DataComponentType> ExDeorumCompressedHammerComponent = + register("exdeorum_compressed_hammer", builder -> + builder.persistent(Codec.BOOL) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeBoolean, + FriendlyByteBuf::readBoolean + )) + ); + + public static final DeferredHolder, DataComponentType> ForceKillEnabledComponent = + register("force_kill_enabled", builder -> + builder.persistent(Codec.BOOL) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeBoolean, + FriendlyByteBuf::readBoolean + )) + ); + + public static final DeferredHolder, DataComponentType> BeefTimeAccelerationEnabledComponent = + register("beef_time_acceleration_enabled", builder -> + builder.persistent(Codec.BOOL) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeBoolean, + FriendlyByteBuf::readBoolean + )) + ); + + public static final DeferredHolder, DataComponentType> BeefInvulnerabilityEnabledComponent = + register("beef_invulnerability_enabled", builder -> + builder.persistent(Codec.BOOL) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeBoolean, + FriendlyByteBuf::readBoolean + )) + ); + + public static final DeferredHolder, DataComponentType> BeefAdvancedStealthEnabledComponent = + register("beef_advanced_stealth_enabled", builder -> + builder.persistent(Codec.BOOL) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeBoolean, + FriendlyByteBuf::readBoolean + )) + ); + + public static final DeferredHolder, DataComponentType> BeefCaptureEnabledComponent = + register("beef_capture_enabled", builder -> + builder.persistent(Codec.BOOL) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeBoolean, + FriendlyByteBuf::readBoolean + )) + ); + + public static final DeferredHolder, DataComponentType> BeefMalumSpiritEnabledComponent = + register("beef_malum_spirit_enabled", builder -> + builder.persistent(Codec.BOOL) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeBoolean, + FriendlyByteBuf::readBoolean + )) + ); + + public static final DeferredHolder, DataComponentType> BeefMysticalAgricultureEnabledComponent = + register("beef_mystical_agriculture_enabled", builder -> + builder.persistent(Codec.BOOL) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeBoolean, + FriendlyByteBuf::readBoolean + )) + ); + + public static final DeferredHolder, DataComponentType> BeefBeheadingEnabledComponent = + register("beef_beheading_enabled", builder -> + builder.persistent(Codec.BOOL) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeBoolean, + FriendlyByteBuf::readBoolean + )) + ); + + public static final DeferredHolder, DataComponentType> BeefTeleportEnabledComponent = + register("beef_teleport_enabled", builder -> + builder.persistent(Codec.BOOL) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeBoolean, + FriendlyByteBuf::readBoolean + )) + ); + + /** + * 范围伤害组件(BeefAoeDamage) + * 用于在物品上存储是否启用范围伤害(布尔类型) + */ + public static final DeferredHolder, DataComponentType> BeefAoeDamageEnabledComponent = + register("beef_aoe_damage_enabled", builder -> + builder.persistent(Codec.BOOL) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeBoolean, + FriendlyByteBuf::readBoolean + )) + ); + + /** + * 范围磁力吸附组件(BeefMagnet) + * 用于在物品上存储是否启用击杀后的范围磁力吸附(布尔类型) + */ + public static final DeferredHolder, DataComponentType> BeefMagnetEnabledComponent = + register("beef_magnet_enabled", builder -> + builder.persistent(Codec.BOOL) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeBoolean, + FriendlyByteBuf::readBoolean + )) + ); + + /** + * 杀戮光环组件(BeefKillAura) + * 用于在物品上存储是否启用杀戮光环(布尔类型) + * true = 手持造化杖时每 20 tick 对「范围伤害」配置范围内生物结算一次攻击伤害 + */ + public static final DeferredHolder, DataComponentType> BeefKillAuraComponent = + register("beef_kill_aura", builder -> + builder.persistent(Codec.BOOL) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeBoolean, + FriendlyByteBuf::readBoolean + )) + ); + + /** + * 保护名单模式组件(BeefProtectMode) + * true = 开启:暂停杀戮光环与范围伤害的结算,方便玩家从容地 Ctrl/Shift 右键添加保护名单, + * 不会出现「怪先被光环打死、来不及加名单」的情况。 + */ + public static final DeferredHolder, DataComponentType> BeefProtectModeComponent = + register("beef_protect_mode", builder -> + builder.persistent(Codec.BOOL) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeBoolean, + FriendlyByteBuf::readBoolean + )) + ); + + public static final DeferredHolder, DataComponentType> WIRELESS_LINK_TARGET = register( + "wireless_link_target", + builder -> + builder.persistent(GlobalPos.CODEC) + .networkSynchronized(com.sorrowmist.useless.compat.forge1201.network.PortCodecs.GLOBAL_POS) + ); + + /** + * 熔炉数据组件(FurnaceData) + * 用于存储高级合金炉的完整状态到物品中 + * 包含阶级、物品栏、流体、能量等所有数据 + */ + public static final DeferredHolder, DataComponentType> FURNACE_DATA = + register("furnace_data", builder -> builder + .persistent(FurnaceDataComponent.CODEC) + .networkSynchronized(FurnaceDataComponent.STREAM_CODEC)); + + public static final DeferredHolder, DataComponentType> OMNIVERSAL_PATTERN_DATA = + register("omniversal_pattern_data", builder -> builder + .persistent(OmniversalPatternData.CODEC) + .networkSynchronized(OmniversalPatternData.STREAM_CODEC)); + public static final DeferredHolder, DataComponentType> PATTERN_CONVERTER_DATA = + register("pattern_converter_data", builder -> builder + .persistent(PatternConverterData.CODEC) + .networkSynchronized(PatternConverterData.STREAM_CODEC)); + public static final DeferredHolder, DataComponentType> PATTERN_CONVERTER_LINK_TARGET = register( + // Keep the serialized ID so existing converter links remain valid. + "mold_hub_link_target", + builder -> builder + .persistent(GlobalPos.CODEC) + .networkSynchronized(com.sorrowmist.useless.compat.forge1201.network.PortCodecs.GLOBAL_POS)); + public static final DeferredHolder, DataComponentType> MULTIBLOCK_RECOVERY_DATA = + register("multiblock_recovery_data", builder -> builder + .persistent(MultiblockRecoveryData.CODEC) + .networkSynchronized(MultiblockRecoveryData.STREAM_CODEC)); + public static final DeferredHolder, DataComponentType> MULTIBLOCK_PART_DATA = + register("multiblock_part_data", builder -> builder + .persistent(MultiblockPartData.CODEC) + .networkSynchronized(MultiblockPartData.STREAM_CODEC)); + public static final DeferredHolder, DataComponentType> EXTERNAL_INVENTORY_REFERENCE = + register("external_inventory_reference", builder -> builder + .persistent(ExternalInventoryReference.CODEC) + .networkSynchronized(ExternalInventoryReference.STREAM_CODEC)); + public static final DeferredHolder, DataComponentType> PASSIVE_HATCH_SETTINGS = + register("passive_hatch_settings", builder -> builder + .persistent(PassiveHatchSettings.CODEC) + .networkSynchronized(PassiveHatchSettings.STREAM_CODEC)); + public static final DeferredHolder, DataComponentType> SMART_DOUBLING_OPERATIONS = + register("smart_doubling_operations", builder -> builder + .persistent(Codec.LONG) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeLong, + FriendlyByteBuf::readLong))); + /** The Occultism pentacles recorded on an imprinted ritual blueprint. */ + public static final DeferredHolder, DataComponentType> RITUAL_BLUEPRINT_PENTACLE = + register("ritual_blueprint_pentacle", builder -> builder + .persistent(RitualBlueprintPentacles.CODEC) + .networkSynchronized(RitualBlueprintPentacles.STREAM_CODEC)); + + /** + * 催熟模式组件(BeefRipen) + * true = 右键时一键催熟目标:可骨粉方块循环施加骨粉直到长满, + * 幼年动物直接催至成年;潜行右键把这次交互让给其它模组。 + */ + public static final DeferredHolder, DataComponentType> BeefRipenComponent = + register("beef_ripen", builder -> + builder.persistent(Codec.BOOL) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeBoolean, + FriendlyByteBuf::readBoolean + )) + ); + + /** + * 连点模式组件(BeefAutoClick)—— 历史遗留,已不再读写。 + * + *

连点开关现为客户端会话级状态({@code BeefAutoClicker}),不绑定物品。 + * 这里保留注册仅为了让旧存档里已带该组件的物品能正常反序列化(注销会刷未知组件警告), + * 任何新代码都不应再读它。

+ */ + public static final DeferredHolder, DataComponentType> BeefAutoClickComponent = + register("beef_auto_click", builder -> + builder.persistent(Codec.BOOL) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeBoolean, + FriendlyByteBuf::readBoolean + )) + ); + + /** + * 强制生长组件(BeefForceGrow) + * true = 催熟时额外用「随机刻」推进方块,可作用于甘蔗、仙人掌、竹子等 + * 原版骨粉无效的方块。代价是它不挑方块——树叶枯萎、火蔓延、雪冰融化、 + * 耕地退化这些随机刻行为同样会被推,是否开启由玩家自行抉择。 + */ + public static final DeferredHolder, DataComponentType> BeefForceGrowComponent = + register("beef_force_grow", builder -> + builder.persistent(Codec.BOOL) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeBoolean, + FriendlyByteBuf::readBoolean + )) + ); + + /** + * 生物加速模式组件(BeefEntityTimeAcceleration) + * true = 潜行右键生物时加速其「计时器」类数值:幼年成长 / 繁殖后冷却、 + * 鸡下蛋间隔、羊剪毛后长毛、村民补货、蜜蜂采蜜、海龟下蛋。 + * 只改数值,不跑 AI / 寻路 / 移动 / 物理。 + * + *

该模式与「保护名单」的 Shift 手势互斥:开启后 Shift+右键生物被解释为 + * 加速,而不是切换保护名单(Ctrl 按种类的手势不受影响)。

+ */ + public static final DeferredHolder, DataComponentType> BeefEntityTimeAccelerationComponent = + register("beef_entity_time_acceleration", builder -> + builder.persistent(Codec.BOOL) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeBoolean, + FriendlyByteBuf::readBoolean + )) + ); + + /** + * 无线物流模式组件(StaffLink) + * true = 开启「无线物流」:潜行右键容器方块可把它绑进/解绑出造化杖的物流网络, + * 网络内的搬运规则在独立界面里配置。真正的搬运由服务端引擎执行。 + */ + public static final DeferredHolder, DataComponentType> StaffLinkEnabledComponent = + register("beef_wireless_logistics", builder -> + builder.persistent(Codec.BOOL) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeBoolean, + FriendlyByteBuf::readBoolean + )) + ); + + /** + * 旧格式:造化杖曾经把物流网络列表挂在自己身上。 + * + *

网络归属已经改成跟着玩家/队伍走(存在服务端的 {@code StaffLinkSavedData} 归属 + * 登记里),所以这两个组件现在只用于迁移:玩家登录(或每 20 tick 的活跃刷新)时把 + * 杖上残留的列表迁到归属者名下,然后就地清空。新代码不要再读写它们。

+ */ + public static final DeferredHolder, DataComponentType>> STAFF_LINK_NETWORKS = + register("staff_link_networks", builder -> + builder.persistent(UUIDUtil.CODEC.listOf()) + .networkSynchronized(com.sorrowmist.useless.compat.forge1201.network.PortCodecs.UUID_CODEC.apply(ByteBufCodecs.list())) + ); + + /** 旧格式:{@link #STAFF_LINK_NETWORKS} 里当前生效的下标。只用于迁移。 */ + public static final DeferredHolder, DataComponentType> STAFF_LINK_ACTIVE = + register("staff_link_active", builder -> + builder.persistent(Codec.INT) + .networkSynchronized(StreamCodec.of( + FriendlyByteBuf::writeVarInt, + FriendlyByteBuf::readVarInt + )) + ); + + /** Preserve both native NBT/capabilities and prototype state when changing tool item IDs. */ + public static void copyState(net.minecraft.world.item.ItemStack source,net.minecraft.world.item.ItemStack target) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.copyState( + source,target,DATA_COMPONENTS.getEntries()); + } + + // 私有构造器,防止外部实例化(该类仅用于注册静态组件) + private UComponents() {} + + /** + * 通用注册方法 + * + * @param name 组件的注册名称(资源位置路径部分) + * @param builderOperator 对 DataComponentType.Builder 的操作函数 + * @param 组件存储的数据类型 + * @return DeferredHolder,用于延迟获取已注册的 DataComponentType + */ + private static DeferredHolder, DataComponentType> register( + String name, + UnaryOperator> builderOperator) { + // 通过 DeferredRegister 注册,实际构建在模组加载时进行 + return DATA_COMPONENTS.register(name, () -> builderOperator.apply(DataComponentType.builder()).build()); + } + + /** + * 初始化方法:在模组事件总线上注册 DeferredRegister + * 通常在模组主类的构造函数中调用:UComponents.init(modEventBus); + * @param modEventBus 模组的事件总线(通常是 MOD 事件总线) + */ + public static void init(IEventBus modEventBus) { + DATA_COMPONENTS.register(modEventBus); + } +} diff --git a/src/main/java/com/sorrowmist/useless/core/config/AlloyFurnaceTierRules.java b/src/main/java/com/sorrowmist/useless/core/config/AlloyFurnaceTierRules.java new file mode 100644 index 00000000..65602295 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/core/config/AlloyFurnaceTierRules.java @@ -0,0 +1,178 @@ +package com.sorrowmist.useless.core.config; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.mojang.logging.LogUtils; +import net.minecraft.resources.ResourceLocation; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.regex.Pattern; + +/** Resolves the configured minimum tier for an alloy-furnace recipe id. */ +public final class AlloyFurnaceTierRules { + public static final int NO_MACHINE_TIER = -1; + public static final int MIN_TIER = 0; + public static final int MAX_TIER = 10; + + private static final Logger LOGGER = LogUtils.getLogger(); + private static volatile List cachedConfiguration = List.of(); + private static volatile List cachedRules = List.of(); + private static volatile long configurationVersion; + + private AlloyFurnaceTierRules() { + } + + public static int requiredTier(ResourceLocation recipeId) { + return requiredTier(recipeId, -1); + } + + public static int requiredTier(ResourceLocation recipeId, int explicitTier) { + if (explicitTier >= MIN_TIER && explicitTier <= MAX_TIER) { + return explicitTier; + } + if (recipeId == null) return 0; + + List rules = rules(); + String id = recipeId.toString(); + Rule best = null; + for (Rule rule : rules) { + if (rule.matches(id) && (best == null || rule.isMoreSpecificThan(best))) { + best = rule; + } + } + return best == null ? 0 : best.tier(); + } + + public static boolean allows(ResourceLocation recipeId, int machineTier) { + return allows(recipeId, -1, machineTier); + } + + public static boolean allows(ResourceLocation recipeId, int explicitTier, int machineTier) { + return machineTier == NO_MACHINE_TIER + || machineTier >= requiredTier(recipeId, explicitTier); + } + + /** Changes whenever the raw config list changes, so recipe caches can include it. */ + public static long configurationVersion() { + rules(); + return configurationVersion; + } + + public static boolean isValidEntry(Object entry) { + if (!(entry instanceof String value)) return false; + + String[] parts = value.split(",", -1); + String pattern = parts.length == 2 + ? parts[0].trim().toLowerCase(Locale.ROOT) : ""; + if (parts.length != 2 || !isValidPattern(pattern)) return false; + + try { + int tier = Integer.parseInt(parts[1].trim()); + return tier >= MIN_TIER && tier <= MAX_TIER; + } catch (NumberFormatException ignored) { + return false; + } + } + + private static List rules() { + List configured = ConfigManager.getFurnaceRecipeTierRules(); + if (configured.equals(cachedConfiguration)) return cachedRules; + + synchronized (AlloyFurnaceTierRules.class) { + if (!configured.equals(cachedConfiguration)) { + List parsed = new ArrayList<>(); + for (String entry : configured) { + Rule rule = parse(entry); + if (rule != null) parsed.add(rule); + } + cachedRules = List.copyOf(parsed); + cachedConfiguration = configured; + configurationVersion++; + } + return cachedRules; + } + } + + private static Rule parse(String entry) { + String[] parts = entry.split(",", -1); + if (parts.length != 2) { + RateLimitedLogger.warn(LOGGER, "Ignoring invalid alloy-furnace tier rule '{}': expected pattern,tier", entry); + return null; + } + + String pattern = parts[0].trim().toLowerCase(Locale.ROOT); + int tier; + try { + tier = Integer.parseInt(parts[1].trim()); + } catch (NumberFormatException exception) { + RateLimitedLogger.warn(LOGGER, "Ignoring invalid alloy-furnace tier rule '{}': tier is not an integer", entry); + return null; + } + if (!isValidPattern(pattern)) { + RateLimitedLogger.warn(LOGGER, "Ignoring invalid alloy-furnace tier rule '{}': invalid recipe id pattern", entry); + return null; + } + if (tier < MIN_TIER || tier > MAX_TIER) { + RateLimitedLogger.warn(LOGGER, "Ignoring invalid alloy-furnace tier rule '{}': tier must be between {} and {}", + entry, MIN_TIER, MAX_TIER); + return null; + } + + StringBuilder regex = new StringBuilder("^"); + int literalLength = 0; + boolean wildcard = false; + for (int index = 0; index < pattern.length(); index++) { + char character = pattern.charAt(index); + if (character == '*') { + regex.append(".*"); + wildcard = true; + } else { + regex.append(Pattern.quote(String.valueOf(character))); + literalLength++; + } + } + regex.append('$'); + return new Rule(pattern, tier, wildcard, literalLength, Pattern.compile(regex.toString())); + } + + private static boolean isValidPattern(String pattern) { + if (pattern == null || pattern.isEmpty()) return false; + + int namespaceSeparators = 0; + for (int index = 0; index < pattern.length(); index++) { + char character = pattern.charAt(index); + if (character == '*') continue; + if (character == ':') { + if (++namespaceSeparators > 1) return false; + continue; + } + if ((character >= 'a' && character <= 'z') + || (character >= '0' && character <= '9') + || character == '_' || character == '-' || character == '.' || character == '/') { + continue; + } + return false; + } + return true; + } + + private record Rule( + String pattern, + int tier, + boolean wildcard, + int literalLength, + Pattern matcher) { + private boolean matches(String recipeId) { + return matcher.matcher(recipeId).matches(); + } + + private boolean isMoreSpecificThan(Rule other) { + if (wildcard != other.wildcard) return !wildcard; + if (literalLength != other.literalLength) return literalLength > other.literalLength; + return true; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/core/config/BeefToolProtectionManager.java b/src/main/java/com/sorrowmist/useless/core/config/BeefToolProtectionManager.java new file mode 100644 index 00000000..2769fd56 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/core/config/BeefToolProtectionManager.java @@ -0,0 +1,148 @@ +package com.sorrowmist.useless.core.config; + +import com.sorrowmist.useless.data.BeefToolLayout; +import com.sorrowmist.useless.data.BeefToolLayoutManager; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; +import org.jetbrains.annotations.Nullable; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 生物保护名单的读取入口。 + * + *

保护名单是玩家个人设置,与连锁挖掘等价组、造化杖布局存在同一份 persistent data 里 + * (见 {@link BeefToolLayout#protectedTypes()} / {@link BeefToolLayout#protectedEntities()}), + * 因此随布局一起校验、同步、导入导出。

+ * + *

本类同时承担两端的一致性职责:服务端从玩家存档读(按 UUID 缓存解析结果), + * 客户端从 {@code BeefToolLayoutSyncPacket} 下发的快照读({@link #setClientMirror}), + * 后者只用于 tooltip 里的「已保护 N 种 / N 只」计数。

+ */ +public final class BeefToolProtectionManager { + + /** 解析后的名单:生物种类 ID 集合 + 生物个体 UUID 集合。 */ + public record Protection(Set types, Set entities) { + public static final Protection EMPTY = new Protection(Set.of(), Set.of()); + + public boolean isEmpty() { + return types.isEmpty() && entities.isEmpty(); + } + } + + private static final Map SERVER_CACHE = new ConcurrentHashMap<>(); + private static volatile Protection clientMirror = Protection.EMPTY; + + private BeefToolProtectionManager() { + } + + /** + * 判定用入口:客户端返回镜像,服务端返回按玩家 UUID 缓存的解析结果。 + * + *

存档缺失或解析失败一律返回空名单(等价于「没有任何手动保护」), + * 与 {@link BeefToolLayoutManager#load} 的只读语义一致,不在读路径上产生写副作用。

+ */ + public static Protection protectionFor(@Nullable Player player) { + if (player == null) { + return Protection.EMPTY; + } + if (player.level().isClientSide()) { + return clientMirror; + } + if (player instanceof ServerPlayer serverPlayer) { + return SERVER_CACHE.computeIfAbsent(serverPlayer.getUUID(), id -> { + BeefToolLayout layout = BeefToolLayoutManager.load(serverPlayer); + return layout == null + ? Protection.EMPTY + : ofLists(layout.protectedTypes(), layout.protectedEntities()); + }); + } + return Protection.EMPTY; + } + + /** tooltip 用:客户端镜像(只读)。 */ + public static Protection clientProtection() { + return clientMirror; + } + + /** 玩家改过名单或改过布局后调用,丢弃其解析缓存。 */ + public static void invalidate(UUID playerId) { + SERVER_CACHE.remove(playerId); + } + + /** 客户端收到服务端下发的布局快照后刷新镜像。 */ + public static void setClientMirror(List types, List entities) { + clientMirror = ofLists(types, entities); + } + + /** 服务端停止 / 客户端登出时清空,避免跨存档残留。 */ + public static void clearAll() { + SERVER_CACHE.clear(); + clientMirror = Protection.EMPTY; + } + + private static Protection ofLists(List types, List entities) { + Set typeSet = new HashSet<>(); + if (types != null) { + for (String type : types) { + if (type != null && !type.isBlank()) { + typeSet.add(type); + } + } + } + Set entitySet = new HashSet<>(); + if (entities != null) { + for (String raw : entities) { + if (raw == null) { + continue; + } + try { + entitySet.add(UUID.fromString(raw)); + } catch (IllegalArgumentException ignored) { + // 存档里出现不可解析的 UUID 时静默忽略,不影响其它条目 + } + } + } + return new Protection(Set.copyOf(typeSet), Set.copyOf(entitySet)); + } + + /** + * 保存前语义校验:生物种类 ID 需是合法的 {@link ResourceLocation} 语法 + * (不查实体注册表,与 {@link ChainGroupManager#validateEntries} 的宽容语义一致), + * 个体条目需是可解析的 UUID。 + */ + public static void validateEntries(List types, List entities) + throws BeefToolLayout.LayoutException { + if (types != null) { + if (types.size() > BeefToolLayout.MAX_PROTECTED_TYPES) { + throw new BeefToolLayout.LayoutException(BeefToolLayout.Error.LIMIT); + } + for (String type : types) { + if (type == null || ResourceLocation.tryParse(type) == null) { + throw new BeefToolLayout.LayoutException(BeefToolLayout.Error.INVALID_STRUCTURE); + } + } + } + if (entities != null) { + if (entities.size() > BeefToolLayout.MAX_PROTECTED_ENTITIES) { + throw new BeefToolLayout.LayoutException(BeefToolLayout.Error.LIMIT); + } + for (String raw : entities) { + if (raw == null) { + throw new BeefToolLayout.LayoutException(BeefToolLayout.Error.INVALID_STRUCTURE); + } + try { + UUID.fromString(raw); + } catch (IllegalArgumentException exception) { + throw new BeefToolLayout.LayoutException(BeefToolLayout.Error.INVALID_STRUCTURE); + } + } + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/core/config/BlockBlacklistMatcher.java b/src/main/java/com/sorrowmist/useless/core/config/BlockBlacklistMatcher.java new file mode 100644 index 00000000..75e54d54 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/core/config/BlockBlacklistMatcher.java @@ -0,0 +1,135 @@ +package com.sorrowmist.useless.core.config; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.sorrowmist.useless.UselessMod; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; +import net.minecraft.world.level.block.Block; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.BiPredicate; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +final class BlockBlacklistMatcher { + private final Set exactIds; + private final List wildcardPatterns; + private final List> tags; + private final BiPredicate> tagMatcher; + private final String listName; + + static BlockBlacklistMatcher empty() { + return empty("blacklist"); + } + + static BlockBlacklistMatcher empty(String listName) { + return new BlockBlacklistMatcher(List.of(), listName); + } + + BlockBlacklistMatcher(List entries) { + this(entries, (block, tag) -> block.defaultBlockState().is(tag)); + } + + BlockBlacklistMatcher(List entries, String listName) { + this(entries, listName, (block, tag) -> block.defaultBlockState().is(tag)); + } + + BlockBlacklistMatcher(List entries, + BiPredicate> tagMatcher) { + this(entries, "blacklist", tagMatcher); + } + + BlockBlacklistMatcher(List entries, + String listName, + BiPredicate> tagMatcher) { + Set exact = new HashSet<>(); + List wildcards = new ArrayList<>(); + List> parsedTags = new ArrayList<>(); + this.listName = listName; + this.tagMatcher = tagMatcher; + + if (entries != null) { + for (String entry : entries) { + if (entry == null || entry.isBlank()) { + warn(entry, "empty entry"); + continue; + } + + if (entry.startsWith("#")) { + ResourceLocation tagId = ResourceLocation.tryParse(entry.substring(1)); + if (tagId == null) { + warn(entry, "invalid block tag id"); + continue; + } + TagKey tag = TagKey.create(Registries.BLOCK, tagId); + if (BuiltInRegistries.BLOCK.getTag(tag).isEmpty()) { + warn(entry, "unknown block tag"); + } + parsedTags.add(tag); + continue; + } + + if (entry.indexOf('*') >= 0) { + if (!isValidWildcard(entry)) { + warn(entry, "invalid wildcard pattern"); + continue; + } + wildcards.add(compileWildcard(entry)); + continue; + } + + ResourceLocation blockId = ResourceLocation.tryParse(entry); + if (blockId == null) { + warn(entry, "invalid block id"); + } else if (!BuiltInRegistries.BLOCK.containsKey(blockId)) { + warn(entry, "unknown block id"); + } else { + exact.add(blockId); + } + } + } + + this.exactIds = Set.copyOf(exact); + this.wildcardPatterns = List.copyOf(wildcards); + this.tags = List.copyOf(parsedTags); + } + + boolean isEmpty() { + return exactIds.isEmpty() && wildcardPatterns.isEmpty() && tags.isEmpty(); + } + + boolean matches(ResourceLocation blockId) { + if (blockId == null) return false; + if (exactIds.contains(blockId)) return true; + + String id = blockId.toString(); + if (wildcardPatterns.stream().anyMatch(pattern -> pattern.matcher(id).matches())) { + return true; + } + + Block block = BuiltInRegistries.BLOCK.get(blockId); + return tags.stream().anyMatch(tag -> tagMatcher.test(block, tag)); + } + + private static boolean isValidWildcard(String value) { + return ResourceLocation.tryParse(value.replace("*", "wildcard")) != null; + } + + private static Pattern compileWildcard(String value) { + String regex = Arrays.stream(value.split("\\*", -1)) + .map(Pattern::quote) + .collect(Collectors.joining(".*")); + return Pattern.compile(regex); + } + + private void warn(String entry, String reason) { + RateLimitedLogger.warn(UselessMod.LOGGER, "Ignoring {} entry '{}': {}", listName, entry, reason); + } +} diff --git a/src/main/java/com/sorrowmist/useless/core/config/ChainEquivalence.java b/src/main/java/com/sorrowmist/useless/core/config/ChainEquivalence.java new file mode 100644 index 00000000..009e373b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/core/config/ChainEquivalence.java @@ -0,0 +1,51 @@ +package com.sorrowmist.useless.core.config; + +import it.unimi.dsi.fastutil.objects.Object2BooleanMap; +import it.unimi.dsi.fastutil.objects.Object2BooleanOpenHashMap; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; + +import java.util.List; + +/** + * 连锁挖掘的"同类方块"判定。 + * 由 {@link ChainGroupManager#equivalenceFor(net.minecraft.world.entity.player.Player, Block)} + * 在每次扫描前构造一次,扫描结束即丢弃。 + * + *

原点方块命中的等价组决定判定范围:命中多条取并集,一条都不命中时退回严格同方块匹配。 + * 单次扫描内用 memo 缓存每种方块的判定结果——增强连锁一次可扫十几万格, + * 但其中不同的方块种类只有几十个,热循环因此仍是一次哈希查找。 + * memo 只活在一次扫描内,数据包重载标签后下一次扫描立刻生效。 + */ +public final class ChainEquivalence { + private final Block origin; + private final List hitGroups; + private final Object2BooleanMap memo; + + ChainEquivalence(Block origin, List hitGroups) { + this.origin = origin; + this.hitGroups = hitGroups; + this.memo = hitGroups.isEmpty() ? null : new Object2BooleanOpenHashMap<>(); + } + + /** + * 判断目标方块状态是否与原点方块属于同类,可被一起连锁。 + */ + public boolean matches(BlockState state) { + Block block = state.getBlock(); + if (block == this.origin) return true; + if (this.hitGroups.isEmpty()) return false; + if (this.memo.containsKey(block)) { + return this.memo.getBoolean(block); + } + boolean result = testGroups(block); + this.memo.put(block, result); + return result; + } + + private boolean testGroups(Block block) { + var blockId = BuiltInRegistries.BLOCK.getKey(block); + return this.hitGroups.stream().anyMatch(group -> group.matches(blockId)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/core/config/ChainGroupManager.java b/src/main/java/com/sorrowmist/useless/core/config/ChainGroupManager.java new file mode 100644 index 00000000..35a0c765 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/core/config/ChainGroupManager.java @@ -0,0 +1,130 @@ +package com.sorrowmist.useless.core.config; + +import com.sorrowmist.useless.data.BeefToolLayout; +import com.sorrowmist.useless.data.BeefToolLayoutManager; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.block.Block; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 连锁挖掘等价组的读取入口。 + * + *

等价组是玩家个人设置,与造化杖布局存在同一份 persistent data 里 + * (见 {@link BeefToolLayout#chainGroups()}),因此随布局一起校验、同步、导入导出。

+ * + *

本类同时承担两端的一致性职责:服务端从玩家存档读,客户端从 + * {@code BeefToolLayoutSyncPacket} 下发的快照读({@link #setClientMirror})。 + * 右键连锁(剥皮/刮铜/去蜡/耕地)走的是 {@code Item#useOn},两端都会执行并各自预测, + * 若客户端拿不到等价组,客户端预测的范围会小于服务端实际破坏的范围。

+ * + *

解析结果(正则已编译、TagKey 已建)按玩家缓存,连锁扫描的热路径只做一次哈希查找。

+ */ +public final class ChainGroupManager { + + private static final Map SERVER_CACHE = new ConcurrentHashMap<>(); + private static volatile ChainMatchGroups clientMirror = ChainMatchGroups.empty(); + + private ChainGroupManager() { + } + + /** + * 取原点方块对应的等价判定。 + * + *

{@code player} 为 null(或非服务端玩家的服务端上下文)时退回严格同方块匹配, + * 与「没有任何等价组」的行为一致。

+ */ + public static ChainEquivalence equivalenceFor(@Nullable Player player, Block origin) { + if (player == null) { + return ChainMatchGroups.empty().forOrigin(origin); + } + if (player.level().isClientSide()) { + return clientMirror.forOrigin(origin); + } + if (player instanceof ServerPlayer serverPlayer) { + ChainMatchGroups groups = SERVER_CACHE.computeIfAbsent(serverPlayer.getUUID(), + id -> ChainMatchGroups.ofGroups(BeefToolLayoutManager.chainGroups(serverPlayer))); + return groups.forOrigin(origin); + } + return ChainMatchGroups.empty().forOrigin(origin); + } + + /** 玩家改过等价组后调用,丢弃其解析缓存。 */ + public static void invalidate(UUID playerId) { + SERVER_CACHE.remove(playerId); + } + + /** 客户端收到服务端下发的布局快照后刷新镜像。 */ + public static void setClientMirror(List> chainGroups) { + clientMirror = ChainMatchGroups.ofGroups(chainGroups); + } + + /** 服务端停止(含单人存档退出)时清空,避免跨存档残留。 */ + public static void clearAll() { + SERVER_CACHE.clear(); + clientMirror = ChainMatchGroups.empty(); + } + + /** + * 校验等价组:数量上限 + 条目语法。 + * + *

只校验形式是否合法。语法合法但当前解析不到的条目(未知方块 ID、 + * 尚未加载的标签、暂无匹配的通配符)允许保存——与 {@link BlockBlacklistMatcher} + * 「warn 并忽略」的宽容语义一致,也不会因为某个模组没装就写不进去。

+ */ + public static void validateEntries(List> chainGroups) throws BeefToolLayout.LayoutException { + if (chainGroups == null) { + return; + } + if (chainGroups.size() > BeefToolLayout.MAX_CHAIN_GROUPS) { + throw new BeefToolLayout.LayoutException(BeefToolLayout.Error.LIMIT); + } + + int total = 0; + for (List group : chainGroups) { + if (group == null || group.size() > BeefToolLayout.MAX_CHAIN_ENTRIES_PER_GROUP) { + throw new BeefToolLayout.LayoutException(BeefToolLayout.Error.LIMIT); + } + for (String entry : group) { + if (!isValidEntrySyntax(entry)) { + throw new BeefToolLayout.LayoutException(BeefToolLayout.Error.INVALID_STRUCTURE); + } + total++; + } + } + if (total > BeefToolLayout.MAX_TOTAL_CHAIN_ENTRIES) { + throw new BeefToolLayout.LayoutException(BeefToolLayout.Error.LIMIT); + } + } + + /** + * 条目语法是否合法。 + * + *

与 {@link BlockBlacklistMatcher} 的解析规则保持一致(精确 ID / {@code #标签} / + * {@code *} 通配符),但不查方块注册表,因此未知方块 ID 也算语法合法。

+ */ + public static boolean isValidEntrySyntax(@Nullable String entry) { + if (entry == null) { + return false; + } + String trimmed = entry.trim(); + if (trimmed.isEmpty() + || trimmed.length() > BeefToolLayout.MAX_CHAIN_ENTRY_LENGTH + || trimmed.indexOf('\n') >= 0 || trimmed.indexOf('\r') >= 0) { + return false; + } + if (trimmed.startsWith("#")) { + return ResourceLocation.tryParse(trimmed.substring(1)) != null; + } + if (trimmed.indexOf('*') >= 0) { + return ResourceLocation.tryParse(trimmed.replace("*", "wildcard")) != null; + } + return ResourceLocation.tryParse(trimmed) != null; + } +} diff --git a/src/main/java/com/sorrowmist/useless/core/config/ChainMatchGroups.java b/src/main/java/com/sorrowmist/useless/core/config/ChainMatchGroups.java new file mode 100644 index 00000000..9ca45ce9 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/core/config/ChainMatchGroups.java @@ -0,0 +1,69 @@ +package com.sorrowmist.useless.core.config; + +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.Block; + +import java.util.ArrayList; +import java.util.List; + +/** + * 连锁挖掘等价组集合。 + * 每个「组」是一个条目列表,组内条目复用 {@link BlockBlacklistMatcher} 解析 + * 精确方块ID、{@code #方块标签} 和 {@code *} 通配符;命中同一个组的方块互相连锁。 + * + *

组由玩家在「模式轮盘 → 连锁挖掘 → 齿轮」界面里编辑,存在玩家个人的 + * {@code BeefToolLayout} 里,见 {@link ChainGroupManager}。

+ */ +final class ChainMatchGroups { + private static final ChainMatchGroups EMPTY = new ChainMatchGroups(List.of()); + + private final List groups; + + static ChainMatchGroups empty() { + return EMPTY; + } + + private ChainMatchGroups(List groups) { + this.groups = groups; + } + + /** + * 由「组 → 条目」的嵌套列表构造。空组与解析后为空的组直接丢弃。 + */ + static ChainMatchGroups ofGroups(List> groupEntries) { + if (groupEntries == null || groupEntries.isEmpty()) { + return EMPTY; + } + + List parsed = new ArrayList<>(); + for (int i = 0; i < groupEntries.size(); i++) { + List entries = groupEntries.get(i); + BlockBlacklistMatcher matcher = new BlockBlacklistMatcher( + entries == null ? List.of() : entries, + "chain mining equivalent group #" + (i + 1)); + if (!matcher.isEmpty()) { + parsed.add(matcher); + } + } + return parsed.isEmpty() ? EMPTY : new ChainMatchGroups(List.copyOf(parsed)); + } + + /** + * 挑出原点方块命中的等价组。一条都不命中时返回的判定退回严格同方块匹配。 + */ + ChainEquivalence forOrigin(Block origin) { + if (this.groups.isEmpty()) { + return new ChainEquivalence(origin, List.of()); + } + + ResourceLocation originId = BuiltInRegistries.BLOCK.getKey(origin); + List hit = new ArrayList<>(); + for (BlockBlacklistMatcher group : this.groups) { + if (group.matches(originId)) { + hit.add(group); + } + } + return new ChainEquivalence(origin, List.copyOf(hit)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/core/config/ConfigManager.java b/src/main/java/com/sorrowmist/useless/core/config/ConfigManager.java new file mode 100644 index 00000000..d4dd3d64 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/core/config/ConfigManager.java @@ -0,0 +1,1516 @@ +package com.sorrowmist.useless.core.config; + +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.common.ForgeConfigSpec; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class ConfigManager { + public static final ForgeConfigSpec COMMON_SPEC; + public static final ForgeConfigSpec CLIENT_SPEC; + public static final ForgeConfigSpec SERVER_SPEC; + private static final ForgeConfigSpec.Builder COMMON_BUILDER = new ForgeConfigSpec.Builder(); + private static final ForgeConfigSpec.Builder CLIENT_BUILDER = new ForgeConfigSpec.Builder(); + private static final ForgeConfigSpec.Builder SERVER_BUILDER = new ForgeConfigSpec.Builder(); + // 植物盆生长速度配置 + private static final ForgeConfigSpec.IntValue BOTANY_POT_GROWTH_MULTIPLIER; + // 植物盆渲染配置 + private static final ForgeConfigSpec.BooleanValue ENABLE_BOTANY_POT_RENDERING; + // 造化杖连点模式:每个客户端 tick 触发多少次右键(客户端行为,放 CLIENT 配置) + private static final ForgeConfigSpec.IntValue BEEF_AUTOCLICK_CLICKS_PER_TICK; + + // 矩阵样板数量配置 + private static final ForgeConfigSpec.IntValue MATRIX_PATTERN_COUNT; + + // 药水效果配置 + private static final ForgeConfigSpec.BooleanValue ENABLE_POTION_EFFECTS; + private static final ForgeConfigSpec.BooleanValue ENABLE_FLIGHT_EFFECT; + + // 自定义药水效果配置 - 每个列表条目格式: "modid:effect_name,amplifier" + // 持续时间固定为20000 tick, 等级从1开始计算 + private static final ForgeConfigSpec.ConfigValue> CUSTOM_POTION_EFFECTS; + + // 牛排工具连锁挖掘配置 + private static final ForgeConfigSpec.IntValue CHAIN_MINING_RANGE_X; + private static final ForgeConfigSpec.IntValue CHAIN_MINING_RANGE_Y; + private static final ForgeConfigSpec.IntValue CHAIN_MINING_RANGE_Z; + private static final ForgeConfigSpec.IntValue CHAIN_MINING_MAX_BLOCKS; + private static final ForgeConfigSpec.DoubleValue BEEF_TOOL_FLIGHT_SPEED; + // 牛排工具附魔等级配置 + private static final ForgeConfigSpec.IntValue FORTUNE_LEVEL; + private static final ForgeConfigSpec.IntValue LOOTING_LEVEL; + // 牛排工具挖掘速度配置 + private static final ForgeConfigSpec.DoubleValue BEEF_TOOL_MINING_SPEED; + private static final ForgeConfigSpec.BooleanValue BEEF_TOOL_IGNORES_TOOL_TIER; + private static final ForgeConfigSpec.DoubleValue BEEF_TOOL_PSEUDO_HARDNESS; + private static final ForgeConfigSpec.DoubleValue BEEF_TOOL_ENTITY_INTERACTION_RANGE; + private static final ForgeConfigSpec.DoubleValue BEEF_TOOL_BLOCK_INTERACTION_RANGE; + // 造化杖短距闪现的最大距离 + private static final ForgeConfigSpec.IntValue BEEF_SHORT_TELEPORT_RANGE; + private static final ForgeConfigSpec.IntValue BEEF_CONSTRUCTION_WAND_BUILD_LIMIT; + private static final ForgeConfigSpec.IntValue BEEF_CONSTRUCTION_WAND_ANGEL_LIMIT; + private static final ForgeConfigSpec.IntValue BEEF_CONSTRUCTION_WAND_DESTRUCTION_LIMIT; + private static final ForgeConfigSpec.ConfigValue> BEEF_TOOL_FORCE_MINING_BLACKLIST; + private static final ForgeConfigSpec.ConfigValue BEEF_TOOL_FORCE_KILL_BLACKLIST; + private static final ForgeConfigSpec.ConfigValue BEEF_TOOL_FORCE_KILL_NON_LIVING_WHITELIST; + // 生物捕捉:写进刷怪蛋前额外丢弃的 NBT 键路径(`容器.子键`)与数据附件命名空间。 + // 默认只列「已知每只都不同的运行时数值」;未知条目一律保留,避免误伤其它整合包。 + private static final ForgeConfigSpec.ConfigValue> BEEF_CAPTURE_STRIP_KEYS; + private static final ForgeConfigSpec.ConfigValue> BEEF_CAPTURE_STRIP_ATTACHMENT_NAMESPACES; + // 牛排工具范围伤害配置(半径) + private static final ForgeConfigSpec.IntValue BEEF_AOE_DAMAGE_RANGE_X; + private static final ForgeConfigSpec.IntValue BEEF_AOE_DAMAGE_RANGE_Y; + private static final ForgeConfigSpec.IntValue BEEF_AOE_DAMAGE_RANGE_Z; + private static final ForgeConfigSpec.IntValue BEEF_AOE_DAMAGE_MAX_TARGETS; + // 杀戮光环:每隔多少 tick 结算一次(服务端玩法行为) + private static final ForgeConfigSpec.IntValue BEEF_KILL_AURA_INTERVAL; + // 牛排工具范围磁力吸附配置(半径) + private static final ForgeConfigSpec.IntValue BEEF_MAGNET_RANGE_X; + private static final ForgeConfigSpec.IntValue BEEF_MAGNET_RANGE_Y; + private static final ForgeConfigSpec.IntValue BEEF_MAGNET_RANGE_Z; + + // 战利品大爆发触发概率配置 (百分比) + private static final ForgeConfigSpec.IntValue FESTIVE_DROP_CHANCE; + + // 打草掉落造化杖彩蛋配置 + private static final ForgeConfigSpec.BooleanValue ENABLE_GRASS_WAND_DROP; + private static final ForgeConfigSpec.DoubleValue GRASS_WAND_DROP_PROBABILITY; + + // Mekanism 升级配置 + private static final ForgeConfigSpec.IntValue TIME_MULTIPLIER; + private static final ForgeConfigSpec.IntValue ELECTRICITY_MULTIPLIER; + private static final ForgeConfigSpec.IntValue CAPACITY_MULTIPLIER; + private static final ForgeConfigSpec.IntValue MAX_UPGRADE; + + // 万象炉从AE网络抽取能量配置 + private static final ForgeConfigSpec.BooleanValue FURNACE_DRAW_APPFLUX_ENERGY; + private static final ForgeConfigSpec.BooleanValue FURNACE_DRAW_AE_ENERGY; + private static final ForgeConfigSpec.ConfigValue> FURNACE_RECIPE_TIER_RULES; + // 万象炉 AE 批次成熟等待窗口 + private static final ForgeConfigSpec.IntValue FURNACE_AE_BATCH_RIPE_TICKS; + // 万象炉产物回网每 tick 时间预算 + private static final ForgeConfigSpec.IntValue FURNACE_AE_OUTPUT_RETURN_BUDGET_MILLIS; + // 万象炉是否解除「材料窗口」对单批规模的限制 + private static final ForgeConfigSpec.BooleanValue FURNACE_AE_UNLIMITED_BIGINT_PARALLELISM; + private static final ForgeConfigSpec.IntValue[] FURNACE_TIER_THREADS = + new ForgeConfigSpec.IntValue[11]; + private static final ForgeConfigSpec.IntValue[] CATALYST_TIER_PARALLEL = + new ForgeConfigSpec.IntValue[10]; + private static final ForgeConfigSpec.IntValue[] CATALYST_TIER_ENERGY_DIVISOR = + new ForgeConfigSpec.IntValue[10]; + private static final ForgeConfigSpec.DoubleValue[] CATALYST_TIER_TIME_MULTIPLIER = + new ForgeConfigSpec.DoubleValue[10]; + + // 无线物流:空闲退避开关与倍率上限 + private static final ForgeConfigSpec.BooleanValue STAFFLINK_IDLE_BACKOFF_ENABLED; + private static final ForgeConfigSpec.IntValue STAFFLINK_IDLE_BACKOFF_CAP; + + // 万象炉配方转换配置 + private static final ForgeConfigSpec.BooleanValue ENABLE_CRAFTING_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_SMELTING_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_BREWING_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_FISHING_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_VILLAGER_TRADE_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_FARMERS_DELIGHT_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_EXTRA_DELIGHT_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_CRABBERS_DELIGHT_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_CASUALNESS_DELIGHT_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_EXPANDED_DELIGHT_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_BREWIN_AND_CHEWIN_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_NOMADS_DELIGHT_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_UBES_DELIGHT_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_BARBEQUES_DELIGHT_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_YOUKAI_HOMECOMING_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_KALEIDOSCOPE_COOKERY_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_KALEIDOSCOPE_GRILLING_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_KALEIDOSCOPE_TAVERN_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_EXTENDED_AE_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_ADVANCED_AE_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_MEKANISM_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_MEKANISM_GENERATORS_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_APP_MEK_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_AE2_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_AE2CS_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_INDUSTRIAL_FOREGOING_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_ACTUALLY_ADDITIONS_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_ARS_NOUVEAU_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_MYSTICAL_AGRICULTURE_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_AE2LT_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_DATA_ENERGISTICS_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_PRODUCTIVE_BEES_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_DRACONIC_EVOLUTION_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_POWAH_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_EXTENDED_CRAFTING_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_AVARITIA_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_NEO_ECO_AE_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_NATURES_AURA_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_FORBIDDEN_ARCANUS_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_OCCULTISM_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_SUMMONING_RITUALS_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_MALUM_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_ENDER_IO_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_CREATE_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_ORITECH_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_NEOVITAE_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_UFO_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_MODERN_INDUSTRIALIZATION_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_IMMERSIVE_ENGINEERING_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_PNEUMATICCRAFT_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_BIG_REACTORS_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_IRONS_SPELLBOOKS_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_HOSTILE_NETWORKS_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_APOTHIC_FLUX_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_FLUX_NETWORKS_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_APOTHEOSIS_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_LYCHEE_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_ASTRAL_SORCERY_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_BOTANY_POTS_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_EX_DEORUM_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_JUSTDIRETHINGS_RECIPE_CONVERSION; + private static final ForgeConfigSpec.BooleanValue ENABLE_JDTE_RECIPE_CONVERSION; + private static final Map RECIPE_CONVERSION_OPTIONS; + + private static final ForgeConfigSpec.IntValue OMNIVERSAL_PATTERN_SLOTS; + private static final ForgeConfigSpec.IntValue OMNIVERSAL_MOLD_SLOTS; + private static final ForgeConfigSpec.IntValue OMNIVERSAL_PASSIVE_PATTERN_SLOTS; + private static final ForgeConfigSpec.IntValue OMNIVERSAL_DECODE_CACHE_CAPACITY; + private static final ForgeConfigSpec.BooleanValue OMNIVERSAL_PATTERN_AUTO_UPLOAD; + private static final ForgeConfigSpec.IntValue[] OMNIVERSAL_COIL_TIER_THREADS = + new ForgeConfigSpec.IntValue[9]; + private static final ForgeConfigSpec.LongValue[] OMNIVERSAL_COIL_TIER_PARALLEL = + new ForgeConfigSpec.LongValue[9]; + private static final ForgeConfigSpec.IntValue[] OMNIVERSAL_COIL_TIER_ENERGY_DIVISOR = + new ForgeConfigSpec.IntValue[9]; + private static final ForgeConfigSpec.DoubleValue[] OMNIVERSAL_COIL_TIER_TIME_MULTIPLIER = + new ForgeConfigSpec.DoubleValue[9]; + private static final ForgeConfigSpec.IntValue OMNIVERSAL_USEFUL_TIER_THREADS; + private static final ForgeConfigSpec.IntValue ORE_GENERATOR_SLOTS; + private static final ForgeConfigSpec.ConfigValue> + USELESS_DIMENSION_FLOOR_BLOCK_BLACKLIST; + private static final ForgeConfigSpec.ConfigValue> + USELESS_DIMENSION_FLOOR_BLOCK_WHITELIST; + private static final ForgeConfigSpec.ConfigValue> + AE2_GIFT_PACKAGE_ITEMS; + private static final String DIMENSION_FLOOR_BLACKLIST_NAME = "Useless Dimension floor block blacklist"; + private static final String DIMENSION_FLOOR_WHITELIST_NAME = "Useless Dimension floor block whitelist"; + private static volatile List cachedUselessDimensionFloorBlockBlacklist = List.of(); + private static volatile BlockBlacklistMatcher uselessDimensionFloorBlockBlacklistMatcher = + BlockBlacklistMatcher.empty(DIMENSION_FLOOR_BLACKLIST_NAME); + private static volatile List cachedUselessDimensionFloorBlockWhitelist = List.of(); + private static volatile BlockBlacklistMatcher uselessDimensionFloorBlockWhitelistMatcher = + BlockBlacklistMatcher.empty(DIMENSION_FLOOR_WHITELIST_NAME); + private static volatile List cachedBeefToolForceMiningBlacklist = List.of(); + private static volatile BlockBlacklistMatcher beefToolForceMiningBlacklistMatcher = + BlockBlacklistMatcher.empty("beef tool force mining blacklist"); + + static { + // Server config: these values change world or machine behavior. + SERVER_BUILDER.push("game_mechanics"); + BOTANY_POT_GROWTH_MULTIPLIER = SERVER_BUILDER + .comment("植物盆生长倍率 - 1.0为原版速度, 2.0为2倍速度") + .defineInRange("botany_pot_growth_multiplier", 1, 1, Integer.MAX_VALUE); + + MATRIX_PATTERN_COUNT = SERVER_BUILDER + .comment("矩阵样板槽位倍数 - 减少数量时请保持槽位空!否则可能会造成样板丢失") + .defineInRange("matrix_pattern_count", 1, 1, 100); + SERVER_BUILDER.pop(); + + // Client config: rendering-only options must never affect server behavior. + CLIENT_BUILDER.push("game_mechanics"); + ENABLE_BOTANY_POT_RENDERING = CLIENT_BUILDER + .comment("是否启用植物盆作物渲染") + .define("enable_botany_pot_rendering", true); + CLIENT_BUILDER.pop(); + + CLIENT_BUILDER.push("beef_tool"); + BEEF_AUTOCLICK_CLICKS_PER_TICK = CLIENT_BUILDER + .comment("造化杖连点模式每个客户端 tick 触发多少次右键", + "调大 = 更快(默认 4 约等于 80 次/秒),调小 = 降低服务端压力", + "仅在客户端生效,无需服务端同意") + .translation("useless_mod.configuration.beef_autoclick_clicks_per_tick") + .defineInRange("beef_autoclick_clicks_per_tick", 4, 1, 20); + CLIENT_BUILDER.pop(); + + SERVER_BUILDER.translation("useless_mod.configuration.omniversal_multiblock_alloy_furnace") + .push("omniversal_multiblock_alloy_furnace"); + OMNIVERSAL_PATTERN_SLOTS = SERVER_BUILDER + .comment("ME pattern assembly slots. Values are normalized to pages of 27.") + .translation("useless_mod.configuration.pattern_slots") + .defineInRange("pattern_slots", 108, 27, 4096); + OMNIVERSAL_MOLD_SLOTS = SERVER_BUILDER + .comment("Omniversal mold hub slots. Values are normalized to pages of 27.") + .translation("useless_mod.configuration.mold_slots") + .defineInRange("mold_slots", 108, 27, 4096); + OMNIVERSAL_PASSIVE_PATTERN_SLOTS = SERVER_BUILDER + .comment("Passive crafting hatch slots. Higher coil tiers unlock this capacity gradually.") + .translation("useless_mod.configuration.passive_pattern_slots") + .defineInRange("passive_pattern_slots", 30, 1, 4096); + OMNIVERSAL_DECODE_CACHE_CAPACITY = SERVER_BUILDER + .comment("Maximum decoded omniversal pattern entries kept per level. Takes effect after restart.") + .translation("useless_mod.configuration.decode_cache_capacity") + .defineInRange("decode_cache_capacity", 2048, 64, 16384); + for (int tier = 1; tier <= 9; tier++) { + int index = tier - 1; + OMNIVERSAL_COIL_TIER_THREADS[index] = SERVER_BUILDER + .comment("普通线圈 " + tier + " 阶的最大AE任务数") + .defineInRange("coil_tier_" + tier + "_threads", tier + 1, 1, Integer.MAX_VALUE); + OMNIVERSAL_COIL_TIER_PARALLEL[index] = SERVER_BUILDER + .comment("普通线圈 " + tier + " 阶的单任务最大并行数") + .defineInRange("coil_tier_" + tier + "_single_task_parallel", + 1L << (tier * 2), 1L, Long.MAX_VALUE); + OMNIVERSAL_COIL_TIER_ENERGY_DIVISOR[index] = SERVER_BUILDER + .comment("普通线圈 " + tier + " 阶的能耗除数") + .defineInRange("coil_tier_" + tier + "_energy_divisor", 1 << tier, 1, Integer.MAX_VALUE); + OMNIVERSAL_COIL_TIER_TIME_MULTIPLIER[index] = SERVER_BUILDER + .comment("普通线圈 " + tier + " 阶的处理时间倍率") + .defineInRange("coil_tier_" + tier + "_time_multiplier", + 1.0 / (1L << tier), 0.0, 1.0); + } + OMNIVERSAL_USEFUL_TIER_THREADS = SERVER_BUILDER + .comment("有用级线圈的最大AE任务数") + .defineInRange("useful_tier_threads", 11, 1, Integer.MAX_VALUE); + SERVER_BUILDER.pop(); + + // 万象样板自动上传:单独成组,便于只开关上传行为, + // 而不必连带改动多方块结构自身的容量与线圈参数。 + SERVER_BUILDER.translation("useless_mod.configuration.omniversal_pattern_upload") + .push("omniversal_pattern_upload"); + OMNIVERSAL_PATTERN_AUTO_UPLOAD = SERVER_BUILDER + .comment("Whether a freshly encoded omniversal pattern is uploaded to a pattern assembly", + "on the same AE grid. When disabled, the pattern stays in the encoding terminal", + "and the player moves it manually, as before.") + .translation("useless_mod.configuration.omniversal_pattern_auto_upload") + .define("omniversal_pattern_auto_upload", true); + SERVER_BUILDER.pop(); + + SERVER_BUILDER.translation("useless_mod.configuration.ore_generator") + .push("ore_generator"); + ORE_GENERATOR_SLOTS = SERVER_BUILDER + .comment("Ore generator sample slots. Slots above this value remain recovery-only.") + .translation("useless_mod.configuration.ore_generator_slots") + .defineInRange("ore_generator_slots", 9, 1, 540); + SERVER_BUILDER.pop(); + + SERVER_BUILDER.translation("useless_mod.configuration.ae2_gift_package") + .push("ae2_gift_package"); + AE2_GIFT_PACKAGE_ITEMS = SERVER_BUILDER + .comment("Items granted by the AE2 gift package. Format: modid:item_id,count.", + "Missing items and entries with invalid quantities are skipped.") + .translation("useless_mod.configuration.ae2_gift_package.items") + .defineListAllowEmpty("items", defaultAE2GiftPackageItems(), + ConfigManager::isValidAE2GiftPackageEntry); + SERVER_BUILDER.pop(); + + SERVER_BUILDER.translation("useless_mod.configuration.beef_tool") + .push("beef_tool"); + CUSTOM_POTION_EFFECTS = SERVER_BUILDER + .comment("Custom potion effects. Format: modid:effect_id,amplifier.", + "Use one effect per list entry. Missing effects and invalid levels are skipped.") + .translation("useless_mod.configuration.custom_potion_effects") + .defineListAllowEmpty("custom_potion_effects", defaultCustomPotionEffects(), + ConfigManager::isValidCustomPotionEffectEntry); + SERVER_BUILDER.pop(); + + SERVER_BUILDER.translation("useless_mod.configuration.useless_dimension") + .push("useless_dimension"); + USELESS_DIMENSION_FLOOR_BLOCK_BLACKLIST = SERVER_BUILDER + .comment("Blocks that cannot be used for Useless Dimension layers, borders, fills, roads, or center markers", + "A block matching this list remains blocked even if it matches the whitelist", + "Use exact block IDs, #block tags, or * wildcard patterns") + .translation("useless_mod.configuration.useless_dimension_floor_block_blacklist") + .defineListAllowEmpty("floor_block_blacklist", List.of(), + entry -> entry instanceof String); + USELESS_DIMENSION_FLOOR_BLOCK_WHITELIST = SERVER_BUILDER + .comment("When non-empty, only matching blocks can be used for Useless Dimension layers, borders, fills, roads, or center markers", + "Leave empty to allow every block that is not on the blacklist", + "Use exact block IDs, #block tags, or * wildcard patterns") + .translation("useless_mod.configuration.useless_dimension_floor_block_whitelist") + .defineListAllowEmpty("floor_block_whitelist", List.of(), + entry -> entry instanceof String); + SERVER_BUILDER.pop(); + + SERVER_BUILDER.translation("useless_mod.configuration.stafflink") + .push("stafflink"); + STAFFLINK_IDLE_BACKOFF_ENABLED = SERVER_BUILDER + .comment("无线物流:空转的线路是否启用指数退避", + "开启(默认):某一轮搬运量为 0 时把下一轮间隔逐次翻倍,最多放大到「周期 × stafflink_idle_backoff_cap」,", + "搬动一次立即恢复。这样可以避免大量空通道每 tick 白跑一遍(实测能占引擎开销的九成以上)", + "关闭:无论搬动与否都严格按线路配置的周期运行,搬运响应最及时,代价是空通道持续产生开销") + .translation("useless_mod.configuration.stafflink_idle_backoff_enabled") + .define("stafflink_idle_backoff_enabled", true); + + STAFFLINK_IDLE_BACKOFF_CAP = SERVER_BUILDER + .comment("无线物流:空转退避的倍率上限(下一轮间隔最多放大到 配置周期 × 本值)", + "本值取 2 的幂(如 1/2/4/8/16/32/64):内部按 log2 换算成翻倍次数,非 2 的幂会向下取整", + "设为 1 表示不放大(等同关闭退避)。默认 32 折合约 32 分钟封顶(周期上限 1200 tick)", + "仅在 stafflink_idle_backoff_enabled 开启时生效") + .translation("useless_mod.configuration.stafflink_idle_backoff_cap") + .defineInRange("stafflink_idle_backoff_cap", 32, 1, 1024); + SERVER_BUILDER.pop(); + + // 牛排工具连锁挖掘配置 + // Server config: gameplay options are authoritative on the logical server. + SERVER_BUILDER.translation("useless_mod.configuration.beef_tool").push("beef_tool"); + ENABLE_POTION_EFFECTS = SERVER_BUILDER + .comment("是否启用药水效果") + .translation("useless_mod.configuration.enable_potion_effects") + .define("enable_potion_effects", true); + + ENABLE_FLIGHT_EFFECT = SERVER_BUILDER + .comment("是否启用飞行效果") + .translation("useless_mod.configuration.enable_flight_effect") + .define("enable_flight_effect", true); + + BEEF_TOOL_FLIGHT_SPEED = SERVER_BUILDER + .comment("牛排工具飞行速度") + .translation("useless_mod.configuration.beef_tool_flight_speed") + .defineInRange("beef_tool_flight_speed", 0.05, 0.01, 1.0); + + CHAIN_MINING_RANGE_X = SERVER_BUILDER + .comment("连锁挖掘的X轴范围半径") + .translation("useless_mod.configuration.chain_mining_range_x") + .defineInRange("chain_mining_range_x", 8, 1, 32); + + CHAIN_MINING_RANGE_Y = SERVER_BUILDER + .comment("连锁挖掘的Y轴范围半径") + .translation("useless_mod.configuration.chain_mining_range_y") + .defineInRange("chain_mining_range_y", 8, 1, 255); + + CHAIN_MINING_RANGE_Z = SERVER_BUILDER + .comment("连锁挖掘的Z轴范围半径") + .translation("useless_mod.configuration.chain_mining_range_z") + .defineInRange("chain_mining_range_z", 8, 1, 32); + + BEEF_AOE_DAMAGE_RANGE_X = SERVER_BUILDER + .comment("范围伤害的X轴范围半径") + .translation("useless_mod.configuration.beef_aoe_damage_range_x") + .defineInRange("beef_aoe_damage_range_x", 5, 1, 64); + + BEEF_AOE_DAMAGE_RANGE_Y = SERVER_BUILDER + .comment("范围伤害的Y轴范围半径") + .translation("useless_mod.configuration.beef_aoe_damage_range_y") + .defineInRange("beef_aoe_damage_range_y", 3, 1, 64); + + BEEF_AOE_DAMAGE_RANGE_Z = SERVER_BUILDER + .comment("范围伤害的Z轴范围半径") + .translation("useless_mod.configuration.beef_aoe_damage_range_z") + .defineInRange("beef_aoe_damage_range_z", 5, 1, 64); + + BEEF_AOE_DAMAGE_MAX_TARGETS = SERVER_BUILDER + .comment("范围伤害单次最多命中的实体数量") + .translation("useless_mod.configuration.beef_aoe_damage_max_targets") + .defineInRange("beef_aoe_damage_max_targets", 64, 1, 1024); + + BEEF_KILL_AURA_INTERVAL = SERVER_BUILDER + .comment("杀戮光环每隔多少 tick 结算一次(20 tick = 1 秒)", + "调小 = 更快,但服务端实体查询压力会线性上升") + .translation("useless_mod.configuration.beef_kill_aura_interval") + .defineInRange("beef_kill_aura_interval", 20, 1, 1200); + + BEEF_MAGNET_RANGE_X = SERVER_BUILDER + .comment("击杀后范围磁力吸附的X轴范围半径") + .translation("useless_mod.configuration.beef_magnet_range_x") + .defineInRange("beef_magnet_range_x", 5, 1, 64); + + BEEF_MAGNET_RANGE_Y = SERVER_BUILDER + .comment("击杀后范围磁力吸附的Y轴范围半径") + .translation("useless_mod.configuration.beef_magnet_range_y") + .defineInRange("beef_magnet_range_y", 3, 1, 64); + + BEEF_MAGNET_RANGE_Z = SERVER_BUILDER + .comment("击杀后范围磁力吸附的Z轴范围半径") + .translation("useless_mod.configuration.beef_magnet_range_z") + .defineInRange("beef_magnet_range_z", 5, 1, 64); + + CHAIN_MINING_MAX_BLOCKS = SERVER_BUILDER + .comment("连锁挖掘的最大方块数量") + .translation("useless_mod.configuration.chain_mining_max_blocks") + .defineInRange("chain_mining_max_blocks", 1000, 1, 1000000); + + // 牛排工具附魔等级配置 + FORTUNE_LEVEL = SERVER_BUILDER + .comment("牛排工具时运附魔等级") + .translation("useless_mod.configuration.fortune_level") + .defineInRange("fortune_level", 10, 1, 127); + + LOOTING_LEVEL = SERVER_BUILDER + .comment("牛排工具抢夺附魔等级") + .translation("useless_mod.configuration.looting_level") + .defineInRange("looting_level", 10, 1, 127); + + // 战利品大爆发触发概率配置 + FESTIVE_DROP_CHANCE = SERVER_BUILDER + .comment("战利品大爆发触发概率 (百分比, 1-100%)") + .translation("useless_mod.configuration.festive_drop_chance") + .defineInRange("festive_drop_chance", 5, 1, 100); + + ENABLE_GRASS_WAND_DROP = SERVER_BUILDER + .comment("是否启用打草掉落造化杖彩蛋") + .translation("useless_mod.configuration.enable_grass_wand_drop") + .define("enable_grass_wand_drop", true); + + GRASS_WAND_DROP_PROBABILITY = SERVER_BUILDER + .comment("打草掉落造化杖的概率,0.00001为十万分之一") + .translation("useless_mod.configuration.grass_wand_drop_probability") + .defineInRange("grass_wand_drop_probability", 0.00001, 0.0, 1.0); + + // 牛排工具挖掘速度配置 + BEEF_TOOL_MINING_SPEED = SERVER_BUILDER + .comment("牛排工具基础挖掘速度") + .translation("useless_mod.configuration.beef_tool_mining_speed") + .defineInRange("beef_tool_mining_speed", 10.0, 1.0, 1000.0); + + // 造化杖是否无视工具挖掘等级(含其它模组自定义的等级) + BEEF_TOOL_IGNORES_TOOL_TIER = SERVER_BUILDER + .comment("造化杖是否无视所有工具挖掘等级限制(含其它模组自定义的等级)", + "开启:可正常挖掘任何需要特定工具等级的方块;关闭:回到下界合金层级的原版语义", + "无论开关如何,useless_mod:beef_tool_tier_locked 标签内的方块始终按等级判定") + .translation("useless_mod.configuration.beef_tool_ignores_tool_tier") + .define("beef_tool_ignores_tool_tier", true); + + // 「伪不可破坏」方块(hardness = -1)的名义硬度:speed = baseSpeed × 本值 + BEEF_TOOL_PSEUDO_HARDNESS = SERVER_BUILDER + .comment("牛排工具挖掘「伪不可破坏」方块(hardness = -1)时使用的名义硬度", + "这类方块原版层面不可破坏,靠自写 getDestroyProgress 决定进度,其进度除数远大于普通方块", + "(如 AllTheModium 的三矿与远古石用 250,而普通方块相当于硬度x30),", + "所以需要更大的挖掘速度才能手感一致:speed = beef_tool_mining_speed × 本值", + "15 约等于挖普通方块的手感;调大 = 更快") + .translation("useless_mod.configuration.beef_tool_pseudo_hardness") + .defineInRange("beef_tool_pseudo_hardness", 15.0, 1.0, 1000.0); + + BEEF_TOOL_ENTITY_INTERACTION_RANGE = SERVER_BUILDER + .comment("牛排工具实体触及范围加成, 重启游戏生效") + .translation("useless_mod.configuration.beef_tool_entity_interaction_range") + .defineInRange("beef_tool_entity_interaction_range", 8.0, 0.0, 1024.0); + + BEEF_TOOL_BLOCK_INTERACTION_RANGE = SERVER_BUILDER + .comment("牛排工具方块触及范围加成, 重启游戏生效") + .translation("useless_mod.configuration.beef_tool_block_interaction_range") + .defineInRange("beef_tool_block_interaction_range", 8.0, 0.0, 1024.0); + + BEEF_SHORT_TELEPORT_RANGE = SERVER_BUILDER + .comment("造化杖短距闪现的最大距离(格)", + "该值为上限而非保证距离:准星方向若在中途命中方块或缺少可站立空间,落点会相应前移", + "未命中任何方块时按该距离取最远落点") + .translation("useless_mod.configuration.beef_short_teleport_range") + .defineInRange("beef_short_teleport_range", 24, 4, 512); + + BEEF_CONSTRUCTION_WAND_BUILD_LIMIT = SERVER_BUILDER + .comment("牛排工具建筑手杖普通建造单次最大方块数") + .translation("useless_mod.configuration.beef_construction_wand_build_limit") + .defineInRange("construction_wand_build_limit", 1024, 1, 1_000_000); + + BEEF_CONSTRUCTION_WAND_ANGEL_LIMIT = SERVER_BUILDER + .comment("牛排工具建筑手杖天使核心单次最大方块数") + .translation("useless_mod.configuration.beef_construction_wand_angel_limit") + .defineInRange("construction_wand_angel_limit", 16, 1, 1_000_000); + + BEEF_CONSTRUCTION_WAND_DESTRUCTION_LIMIT = SERVER_BUILDER + .comment("牛排工具建筑手杖破坏核心单次最大方块数") + .translation("useless_mod.configuration.beef_construction_wand_destruction_limit") + .defineInRange("construction_wand_destruction_limit", 81, 1, 1_000_000); + + BEEF_TOOL_FORCE_MINING_BLACKLIST = SERVER_BUILDER + .comment("牛排工具强制挖掘黑名单,不会被强制挖掘的方块", + "支持精确方块ID、#方块标签和*通配符") + .translation("useless_mod.configuration.beef_tool_force_mining_blacklist") + .defineListAllowEmpty("beef_tool_force_mining_blacklist", List.of(), + entry -> entry instanceof String); + + BEEF_TOOL_FORCE_KILL_BLACKLIST = SERVER_BUILDER + .comment("牛排工具强制击杀黑名单, 多个实体ID用分号分隔, 例如 minecraft:wither;modid:boss") + .translation("useless_mod.configuration.beef_tool_force_kill_blacklist") + .define("beef_tool_force_kill_blacklist", ""); + + BEEF_TOOL_FORCE_KILL_NON_LIVING_WHITELIST = SERVER_BUILDER + .comment("牛排工具非生物实体强制击杀白名单, 多个实体ID用分号分隔") + .translation("useless_mod.configuration.beef_tool_force_kill_non_living_whitelist") + .define("beef_tool_force_kill_non_living_whitelist", "draconicevolution:guardian_crystal"); + + BEEF_CAPTURE_STRIP_KEYS = SERVER_BUILDER + .comment("生物捕捉:写进刷怪蛋前额外丢弃的 NBT 键路径,格式为 容器.子键(不含点则视为顶层键)", + "默认只列「每只都不同」的运行时数值;未知键一律保留,避免误伤其它整合包", + "例:NeoForgeData.naturesaura:time_alive KubeJSPersistentData.senescence") + .translation("useless_mod.configuration.beef_capture_strip_keys") + .defineListAllowEmpty("beef_capture_strip_keys", defaultBeefCaptureStripKeys(), + entry -> entry instanceof String); + + BEEF_CAPTURE_STRIP_ATTACHMENT_NAMESPACES = SERVER_BUILDER + .comment("生物捕捉:写进刷怪蛋前额外丢弃的数据附件命名空间(该命名空间下所有附件都丢)", + "默认只列「已知只装运行时数值」的模组;其余(含未知模组)一律保留", + "例:cold_sweat malum") + .translation("useless_mod.configuration.beef_capture_strip_attachment_namespaces") + .defineListAllowEmpty("beef_capture_strip_attachment_namespaces", + defaultBeefCaptureStripAttachmentNamespaces(), + entry -> entry instanceof String); + SERVER_BUILDER.pop(); + + SERVER_BUILDER.push("mekanism_upgrade"); + TIME_MULTIPLIER = SERVER_BUILDER + .comment("速度升级增强倍率") + .defineInRange("time_multiplier", 1, 1, Integer.MAX_VALUE); + + ELECTRICITY_MULTIPLIER = SERVER_BUILDER + .comment("能量升级节电增强倍率") + .defineInRange("electricity_multiplier", 1, 1, Integer.MAX_VALUE); + + CAPACITY_MULTIPLIER = SERVER_BUILDER + .comment("能量升级储电增强倍率") + .defineInRange("capacity_multiplier", 1, 1, Integer.MAX_VALUE); + + MAX_UPGRADE = SERVER_BUILDER + .comment("机器可接受的最大速度/能量升级数量, 重启游戏生效") + .defineInRange("max_upgrade", 16, 1, 64); + SERVER_BUILDER.pop(); + + SERVER_BUILDER.translation("useless_mod.configuration.advanced_alloy_furnace") + .push("advanced_alloy_furnace"); + FURNACE_DRAW_APPFLUX_ENERGY = SERVER_BUILDER + .comment("万象炉是否自动从所在AE网络抽取AppliedFlux(应用通量)存储的FE能量", + "需要安装AppliedFlux且网络中有通量元件, 每tick抽取量受熔炉最大输入速率限制") + .define("draw_appflux_energy", true); + + FURNACE_DRAW_AE_ENERGY = SERVER_BUILDER + .comment("万象炉是否直接抽取AE网络自身的能量(按 1 AE = 2 FE 折算)", + "警告: 会与网络中其他设备争抢供电, 网络储能不足时可能导致设备频繁掉线", + "在AppliedFlux抽取之后作为补充, 每tick总抽取量受熔炉最大输入速率限制") + .define("draw_ae_energy", false); + + FURNACE_AE_BATCH_RIPE_TICKS = SERVER_BUILDER + .comment("万象炉收到 AE 推送的批次后,先等待多少个 tick 再投入执行", + "这个窗口用于把连续推送合并成一个任务,等待期间 GUI 显示为「排队」", + "设为 0 表示推送即刻投入执行;机器完全空闲(无任何运行中任务)时会跳过该窗口") + .translation("useless_mod.configuration.ae_batch_ripe_ticks") + .defineInRange("ae_batch_ripe_ticks", 10, 0, 200); + + FURNACE_AE_UNLIMITED_BIGINT_PARALLELISM = SERVER_BUILDER + .comment("是否解除「材料窗口」对单批规模的限制(即去掉 线程数 × Long.MAX 这道闸)", + "开启后单批规模只由「产物交付能力」决定,可以一次吃下任意大的份数,", + "不必再靠堆线程数来抬高单批上限", + "代价:单次准入可能吃下极大量材料(由调用方自己的 BigInteger 账本扣除);", + "低档线圈会转而受「能量」闸限制,有用线圈无能量闸、不受影响") + .translation("useless_mod.configuration.ae_unlimited_bigint_parallelism") + .define("ae_unlimited_bigint_parallelism", false); + + FURNACE_AE_OUTPUT_RETURN_BUDGET_MILLIS = SERVER_BUILDER + .comment("万象炉每 tick 最多花多少毫秒把产物写回 ME 网络", + "这个值直接决定「可持续合成速度」:AE2 存储接口单次只能写一个 long 分段,", + "产物必须逐段插入,所以每 tick 能插多少次就决定了能跑多快", + "调大 = 合成更快,但单 tick 更重(可能掉 TPS);调小 = 更省 tick,但合成变慢", + "机器或服务端过载时,本预算还会被全局降频系数按比例收窄(下限 250 微秒)", + "全局降频基准会跟随本值放大(= 本值×2,下限 10 毫秒),所以调大本值确实能生效") + .translation("useless_mod.configuration.ae_output_return_budget_millis") + .defineInRange("ae_output_return_budget_millis", 8, 1, 200); + + for (int tier = 0; tier <= 10; tier++) { + FURNACE_TIER_THREADS[tier] = SERVER_BUILDER + .comment("单方块熔炉 " + tier + " 阶的最大AE任务数") + .defineInRange("furnace_tier_" + tier + "_threads", tier + 1, 1, Integer.MAX_VALUE); + } + for (int tier = 0; tier <= 9; tier++) { + CATALYST_TIER_PARALLEL[tier] = SERVER_BUILDER + .comment("催化剂 " + tier + " 阶的普通配方并行数") + .defineInRange("catalyst_tier_" + tier + "_parallel", 1 << tier, 1, Integer.MAX_VALUE); + CATALYST_TIER_ENERGY_DIVISOR[tier] = SERVER_BUILDER + .comment("催化剂 " + tier + " 阶的能耗除数") + .defineInRange("catalyst_tier_" + tier + "_energy_divisor", 1, 1, Integer.MAX_VALUE); + CATALYST_TIER_TIME_MULTIPLIER[tier] = SERVER_BUILDER + .comment("催化剂 " + tier + " 阶的处理时间倍率") + .defineInRange("catalyst_tier_" + tier + "_time_multiplier", + Math.max(0.1, 1.0 - tier * 0.1), 0.0, 1.0); + } + + FURNACE_RECIPE_TIER_RULES = SERVER_BUILDER + .comment("万象炉配方等级限制,格式为 配方ID通配符,等级", + "*可匹配任意字符;精确配方ID优先于通配符,匹配具体度相同时后面的规则覆盖前面的规则", + "等级范围为0-10;未匹配规则的配方不受限制") + .translation("useless_mod.configuration.advanced_alloy_furnace.recipe_tier_rules") + .defineListAllowEmpty("recipe_tier_rules", List.of(), + ConfigManager::isValidFurnaceRecipeTierRuleEntry); + SERVER_BUILDER.pop(); + + // Common config: adapter registration happens during common setup on both physical sides. + COMMON_BUILDER.translation("useless_mod.configuration.advanced_alloy_furnace") + .push("advanced_alloy_furnace"); + COMMON_BUILDER.translation("useless_mod.configuration.advanced_alloy_furnace.recipe_conversion") + .push("recipe_conversion"); + COMMON_BUILDER.translation("useless_mod.configuration.advanced_alloy_furnace.recipe_conversion.ae") + .push("ae"); + ENABLE_ADVANCED_AE_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_advanced_ae_recipe_conversion", true); + ENABLE_AE2_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_ae2_recipe_conversion", true); + ENABLE_AE2CS_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_ae2cs_recipe_conversion", true); + ENABLE_AE2LT_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_ae2lt_recipe_conversion", true); + ENABLE_DATA_ENERGISTICS_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_data_energistics_recipe_conversion", true); + ENABLE_EXTENDED_AE_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_extendedae_recipe_conversion", true); + ENABLE_NEO_ECO_AE_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_neo_eco_ae_recipe_conversion", true); + COMMON_BUILDER.pop(); + + COMMON_BUILDER.translation("useless_mod.configuration.advanced_alloy_furnace.recipe_conversion.farmers_delight") + .push("farmers_delight"); + ENABLE_BARBEQUES_DELIGHT_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_barbequesdelight_recipe_conversion", true); + ENABLE_BREWIN_AND_CHEWIN_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_brewinandchewin_recipe_conversion", true); + ENABLE_CASUALNESS_DELIGHT_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_casualnessdelight_recipe_conversion", true); + ENABLE_CRABBERS_DELIGHT_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_crabbersdelight_recipe_conversion", true); + ENABLE_EXPANDED_DELIGHT_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_expandeddelight_recipe_conversion", true); + ENABLE_EXTRA_DELIGHT_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_extradelight_recipe_conversion", true); + ENABLE_FARMERS_DELIGHT_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_farmersdelight_recipe_conversion", true); + ENABLE_NOMADS_DELIGHT_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_nomadsdelight_recipe_conversion", true); + ENABLE_UBES_DELIGHT_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_ubesdelight_recipe_conversion", true); + ENABLE_YOUKAI_HOMECOMING_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_youkaishomecoming_recipe_conversion", true); + COMMON_BUILDER.pop(); + + // Just Dire Things 及其附属独立成组,与 AE、Mekanism 的分组方式保持一致 + COMMON_BUILDER.translation("useless_mod.configuration.advanced_alloy_furnace.recipe_conversion.justdirethings") + .push("justdirethings"); + ENABLE_JDTE_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_jdte_recipe_conversion", true); + ENABLE_JUSTDIRETHINGS_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_justdirethings_recipe_conversion", true); + COMMON_BUILDER.pop(); + + COMMON_BUILDER.translation("useless_mod.configuration.advanced_alloy_furnace.recipe_conversion.kaleidoscope") + .push("kaleidoscope"); + ENABLE_KALEIDOSCOPE_COOKERY_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_kaleidoscope_cookery_recipe_conversion", true); + ENABLE_KALEIDOSCOPE_GRILLING_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_kaleidoscope_grilling_recipe_conversion", true); + ENABLE_KALEIDOSCOPE_TAVERN_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_kaleidoscope_tavern_recipe_conversion", true); + COMMON_BUILDER.pop(); + + COMMON_BUILDER.translation("useless_mod.configuration.advanced_alloy_furnace.recipe_conversion.mekanism") + .push("mekanism"); + // AppMek 属 Mekanism 家族:其配方转换开关归入本分组,避免落在 AE 分组下造成误导 + ENABLE_APP_MEK_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_appmek_recipe_conversion", true); + ENABLE_MEKANISM_GENERATORS_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_mekanism_generators_recipe_conversion", true); + ENABLE_MEKANISM_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_mekanism_recipe_conversion", true); + COMMON_BUILDER.pop(); + + COMMON_BUILDER.translation("useless_mod.configuration.advanced_alloy_furnace.recipe_conversion.minecraft") + .push("minecraft"); + ENABLE_BREWING_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_brewing_recipe_conversion", true); + ENABLE_CRAFTING_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_crafting_recipe_conversion", false); + ENABLE_FISHING_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_fishing_recipe_conversion", true); + ENABLE_SMELTING_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_smelting_recipe_conversion", true); + ENABLE_VILLAGER_TRADE_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_villager_trade_recipe_conversion", true); + COMMON_BUILDER.pop(); + + COMMON_BUILDER.translation("useless_mod.configuration.advanced_alloy_furnace.recipe_conversion.other") + .push("other"); + ENABLE_ACTUALLY_ADDITIONS_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_actually_additions_recipe_conversion", true); + ENABLE_APOTHIC_FLUX_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_apothic_flux_recipe_conversion", true); + ENABLE_FLUX_NETWORKS_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_fluxnetworks_recipe_conversion", true); + ENABLE_APOTHEOSIS_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_apotheosis_recipe_conversion", true); + ENABLE_LYCHEE_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_lychee_recipe_conversion", true); + ENABLE_ASTRAL_SORCERY_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_astralsorcery_recipe_conversion", true); + ENABLE_ARS_NOUVEAU_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_ars_nouveau_recipe_conversion", true); + ENABLE_BOTANY_POTS_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_botany_pots_recipe_conversion", true); + ENABLE_EX_DEORUM_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_exdeorum_recipe_conversion", true); + ENABLE_AVARITIA_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_avaritia_recipe_conversion", true); + ENABLE_BIG_REACTORS_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_bigreactors_recipe_conversion", true); + ENABLE_CREATE_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_create_recipe_conversion", true); + ENABLE_DRACONIC_EVOLUTION_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_draconic_evolution_recipe_conversion", true); + ENABLE_ENDER_IO_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_ender_io_recipe_conversion", true); + ENABLE_EXTENDED_CRAFTING_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_extended_crafting_recipe_conversion", true); + ENABLE_FORBIDDEN_ARCANUS_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_forbidden_arcanus_recipe_conversion", true); + ENABLE_HOSTILE_NETWORKS_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_hostilenetworks_recipe_conversion", true); + ENABLE_IMMERSIVE_ENGINEERING_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_immersiveengineering_recipe_conversion", true); + ENABLE_INDUSTRIAL_FOREGOING_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_industrial_foregoing_recipe_conversion", true); + ENABLE_IRONS_SPELLBOOKS_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_irons_spellbooks_recipe_conversion", true); + ENABLE_MALUM_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_malum_recipe_conversion", true); + ENABLE_MODERN_INDUSTRIALIZATION_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_modern_industrialization_recipe_conversion", true); + ENABLE_MYSTICAL_AGRICULTURE_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_mystical_agriculture_recipe_conversion", true); + ENABLE_NATURES_AURA_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_natures_aura_recipe_conversion", true); + ENABLE_NEOVITAE_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_neovitae_recipe_conversion", true); + ENABLE_OCCULTISM_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_occultism_recipe_conversion", true); + ENABLE_ORITECH_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_oritech_recipe_conversion", true); + ENABLE_PNEUMATICCRAFT_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_pneumaticcraft_recipe_conversion", true); + ENABLE_POWAH_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_powah_recipe_conversion", true); + ENABLE_PRODUCTIVE_BEES_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_productive_bees_recipe_conversion", true); + ENABLE_SUMMONING_RITUALS_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_summoningrituals_recipe_conversion", true); + ENABLE_UFO_RECIPE_CONVERSION = defineRecipeConversionOption( + "enable_ufo_recipe_conversion", true); + COMMON_BUILDER.pop(); + + RECIPE_CONVERSION_OPTIONS = Map.ofEntries( + Map.entry("actuallyadditions", ENABLE_ACTUALLY_ADDITIONS_RECIPE_CONVERSION), + Map.entry("advanced_ae", ENABLE_ADVANCED_AE_RECIPE_CONVERSION), + Map.entry("ae2", ENABLE_AE2_RECIPE_CONVERSION), + Map.entry("ae2cs", ENABLE_AE2CS_RECIPE_CONVERSION), + Map.entry("ae2lt", ENABLE_AE2LT_RECIPE_CONVERSION), + Map.entry("apotheosis", ENABLE_APOTHEOSIS_RECIPE_CONVERSION), + Map.entry("apothic_flux", ENABLE_APOTHIC_FLUX_RECIPE_CONVERSION), + Map.entry("appmek", ENABLE_APP_MEK_RECIPE_CONVERSION), + Map.entry("ars_nouveau", ENABLE_ARS_NOUVEAU_RECIPE_CONVERSION), + Map.entry("astralsorcery", ENABLE_ASTRAL_SORCERY_RECIPE_CONVERSION), + Map.entry("avaritia", ENABLE_AVARITIA_RECIPE_CONVERSION), + Map.entry("barbequesdelight", ENABLE_BARBEQUES_DELIGHT_RECIPE_CONVERSION), + Map.entry("bigreactors", ENABLE_BIG_REACTORS_RECIPE_CONVERSION), + Map.entry("botanypots", ENABLE_BOTANY_POTS_RECIPE_CONVERSION), + Map.entry("exdeorum", ENABLE_EX_DEORUM_RECIPE_CONVERSION), + Map.entry("brewinandchewin", ENABLE_BREWIN_AND_CHEWIN_RECIPE_CONVERSION), + Map.entry("casualnessdelight", ENABLE_CASUALNESS_DELIGHT_RECIPE_CONVERSION), + Map.entry("crabbersdelight", ENABLE_CRABBERS_DELIGHT_RECIPE_CONVERSION), + Map.entry("create", ENABLE_CREATE_RECIPE_CONVERSION), + Map.entry("data_energistics", ENABLE_DATA_ENERGISTICS_RECIPE_CONVERSION), + Map.entry("draconicevolution", ENABLE_DRACONIC_EVOLUTION_RECIPE_CONVERSION), + Map.entry("enderio", ENABLE_ENDER_IO_RECIPE_CONVERSION), + Map.entry("expandeddelight", ENABLE_EXPANDED_DELIGHT_RECIPE_CONVERSION), + Map.entry("expatternprovider", ENABLE_EXTENDED_AE_RECIPE_CONVERSION), + Map.entry("extendedcrafting", ENABLE_EXTENDED_CRAFTING_RECIPE_CONVERSION), + Map.entry("extradelight", ENABLE_EXTRA_DELIGHT_RECIPE_CONVERSION), + Map.entry("farmersdelight", ENABLE_FARMERS_DELIGHT_RECIPE_CONVERSION), + Map.entry("forbidden_arcanus", ENABLE_FORBIDDEN_ARCANUS_RECIPE_CONVERSION), + Map.entry("fluxnetworks", ENABLE_FLUX_NETWORKS_RECIPE_CONVERSION), + Map.entry("hostilenetworks", ENABLE_HOSTILE_NETWORKS_RECIPE_CONVERSION), + Map.entry("immersiveengineering", ENABLE_IMMERSIVE_ENGINEERING_RECIPE_CONVERSION), + Map.entry("industrialforegoing", ENABLE_INDUSTRIAL_FOREGOING_RECIPE_CONVERSION), + Map.entry("irons_spellbooks", ENABLE_IRONS_SPELLBOOKS_RECIPE_CONVERSION), + Map.entry("jdte", ENABLE_JDTE_RECIPE_CONVERSION), + Map.entry("justdirethings", ENABLE_JUSTDIRETHINGS_RECIPE_CONVERSION), + Map.entry("kaleidoscope_cookery", ENABLE_KALEIDOSCOPE_COOKERY_RECIPE_CONVERSION), + Map.entry("kaleidoscope_grilling", ENABLE_KALEIDOSCOPE_GRILLING_RECIPE_CONVERSION), + Map.entry("kaleidoscope_tavern", ENABLE_KALEIDOSCOPE_TAVERN_RECIPE_CONVERSION), + Map.entry("lychee", ENABLE_LYCHEE_RECIPE_CONVERSION), + Map.entry("malum", ENABLE_MALUM_RECIPE_CONVERSION), + Map.entry("mekanism", ENABLE_MEKANISM_RECIPE_CONVERSION), + Map.entry("mekanismgenerators", ENABLE_MEKANISM_GENERATORS_RECIPE_CONVERSION), + Map.entry("minecraft_fishing", ENABLE_FISHING_RECIPE_CONVERSION), + Map.entry("modern_industrialization", ENABLE_MODERN_INDUSTRIALIZATION_RECIPE_CONVERSION), + Map.entry("mysticalagriculture", ENABLE_MYSTICAL_AGRICULTURE_RECIPE_CONVERSION), + Map.entry("naturesaura", ENABLE_NATURES_AURA_RECIPE_CONVERSION), + Map.entry("neoecoae", ENABLE_NEO_ECO_AE_RECIPE_CONVERSION), + Map.entry("neovitae", ENABLE_NEOVITAE_RECIPE_CONVERSION), + Map.entry("nomads_delight", ENABLE_NOMADS_DELIGHT_RECIPE_CONVERSION), + Map.entry("nomadsdelight", ENABLE_NOMADS_DELIGHT_RECIPE_CONVERSION), + Map.entry("occultism", ENABLE_OCCULTISM_RECIPE_CONVERSION), + Map.entry("oritech", ENABLE_ORITECH_RECIPE_CONVERSION), + Map.entry("pneumaticcraft", ENABLE_PNEUMATICCRAFT_RECIPE_CONVERSION), + Map.entry("powah", ENABLE_POWAH_RECIPE_CONVERSION), + Map.entry("productivebees", ENABLE_PRODUCTIVE_BEES_RECIPE_CONVERSION), + Map.entry("summoningrituals", ENABLE_SUMMONING_RITUALS_RECIPE_CONVERSION), + Map.entry("ubesdelight", ENABLE_UBES_DELIGHT_RECIPE_CONVERSION), + Map.entry("ufo", ENABLE_UFO_RECIPE_CONVERSION), + Map.entry("youkaisfeasts", ENABLE_YOUKAI_HOMECOMING_RECIPE_CONVERSION), + Map.entry("youkaishomecoming", ENABLE_YOUKAI_HOMECOMING_RECIPE_CONVERSION)); + COMMON_BUILDER.pop(); + COMMON_BUILDER.pop(); + + COMMON_SPEC = COMMON_BUILDER.build(); + CLIENT_SPEC = CLIENT_BUILDER.build(); + SERVER_SPEC = SERVER_BUILDER.build(); + } + + private static ForgeConfigSpec.BooleanValue defineRecipeConversionOption( + String key, boolean defaultValue) { + return COMMON_BUILDER + .comment("是否启用该配方来源的配方转换", "修改后重启游戏生效") + .translation("useless_mod.configuration." + key) + .define(key, defaultValue); + } + + // 获取配置值方法 + private static List defaultCustomPotionEffects() { + return List.of( + "minecraft:saturation,1", + "minecraft:regeneration,6", + "minecraft:night_vision,1", + "minecraft:fire_resistance,1", + "minecraft:water_breathing,1", + "minecraft:resistance,6" + ); + } + + private static List defaultBeefCaptureStripKeys() { + return List.of( + "NeoForgeData.Temperature", + "NeoForgeData.naturesaura:time_alive", + "NeoForgeData.BalmData", + "KubeJSPersistentData.senescence", + "KubeJSPersistentData.isSenescent", + "KubeJSPersistentData.original_max_health" + ); + } + + private static List defaultBeefCaptureStripAttachmentNamespaces() { + return List.of("cold_sweat", "malum", "blueflame"); + } + + private static boolean isValidCustomPotionEffectEntry(Object entry) { + if (!(entry instanceof String value)) { + return false; + } + + String[] parts = value.split(",", -1); + if (parts.length != 2 || ResourceLocation.tryParse(parts[0].trim()) == null) { + return false; + } + + try { + return Integer.parseInt(parts[1].trim()) > 0; + } catch (NumberFormatException ignored) { + return false; + } + } + + private static boolean isValidFurnaceRecipeTierRuleEntry(Object entry) { + return AlloyFurnaceTierRules.isValidEntry(entry); + } + + private static List defaultAE2GiftPackageItems() { + List items = new ArrayList<>(); + items.add("ae2:creative_energy_cell,1"); + items.add("ae2:fluix_covered_cable,64"); + items.add("ae2:wireless_access_point,1"); + items.add("ae2:wireless_booster,64"); + items.add("ae2:wireless_crafting_terminal,1"); + items.add("ae2:crafting_terminal,1"); + + if (ModList.get().isLoaded("extendedae_plus")) { + items.add("extendedae_plus:infinity_biginteger_cell,1"); + } else { + items.add("ae2:item_storage_cell_256k,8"); + } + + if (ModList.get().isLoaded("expatternprovider")) { + items.add("expatternprovider:ex_drive,1"); + } else { + items.add("ae2:drive,1"); + } + + if (ModList.get().isLoaded("ae2wtlib")) { + items.add("ae2wtlib:quantum_bridge_card,1"); + items.add("ae2:quantum_ring,8"); + items.add("ae2:quantum_link,1"); + items.add("ae2:quantum_entangled_singularity,2"); + } + + return List.copyOf(items); + } + + private static boolean isValidAE2GiftPackageEntry(Object entry) { + if (!(entry instanceof String value)) { + return false; + } + + String[] parts = value.split(",", -1); + if (parts.length != 2 || ResourceLocation.tryParse(parts[0].trim()) == null) { + return false; + } + + try { + return Integer.parseInt(parts[1].trim()) > 0; + } catch (NumberFormatException ignored) { + return false; + } + } + + public static int getBotanyPotGrowthMultiplier() { + return getConfigValue(BOTANY_POT_GROWTH_MULTIPLIER); + } + + public static boolean shouldEnableBotanyPotRendering() { + return getConfigValue(ENABLE_BOTANY_POT_RENDERING); + } + + public static int getMatrixPatternCount() { + return getConfigValue(MATRIX_PATTERN_COUNT); + } + + public static int getTimeMultiplier() { + return getConfigValue(TIME_MULTIPLIER); + } + + public static int getElectricityMultiplier() { + return getConfigValue(ELECTRICITY_MULTIPLIER); + } + + public static int getChainMiningMaxBlocks() { + return getConfigValue(CHAIN_MINING_MAX_BLOCKS); + } + + public static int getCapacityMultiplier() { + return getConfigValue(CAPACITY_MULTIPLIER); + } + + public static int getMaxUpgrade() { + return getConfigValue(MAX_UPGRADE); + } + + // 万象炉AE网络抽电配置 + public static boolean isFurnaceDrawAppfluxEnergyEnabled() { + return getConfigValue(FURNACE_DRAW_APPFLUX_ENERGY); + } + + public static boolean isFurnaceDrawAeEnergyEnabled() { + return getConfigValue(FURNACE_DRAW_AE_ENERGY); + } + + public static List getFurnaceRecipeTierRules() { + return readConfigList(FURNACE_RECIPE_TIER_RULES); + } + + public static int getAdvancedAlloyFurnaceTierThreads(int tier) { + return getConfigValue(FURNACE_TIER_THREADS[Math.max(0, Math.min(10, tier))]); + } + + /** 万象炉 AE 批次成熟等待窗口(tick);0 表示推送即刻投入执行 */ + public static int getAdvancedAlloyFurnaceAeBatchRipeTicks() { + return Math.max(0, getConfigValue(FURNACE_AE_BATCH_RIPE_TICKS)); + } + + /** + * 万象炉每 tick 的产物回网时间预算(毫秒)。 + * + *

它直接决定可持续合成速度:AE2 存储接口单次只能写一个 long 分段,产物必须逐段插入, + * 所以每 tick 能插多少次就决定了能跑多快。

+ */ + public static int getAdvancedAlloyFurnaceAeOutputReturnBudgetMillis() { + return Math.max(1, getConfigValue(FURNACE_AE_OUTPUT_RETURN_BUDGET_MILLIS)); + } + + /** + * 是否解除「材料窗口」对单批规模的限制。 + * + *

开启后 {@code maximumWindowedCount} 返回哨兵值,单批规模只受产物交付能力约束。

+ */ + public static boolean isFurnaceAeUnlimitedBigintParallelism() { + return getConfigValue(FURNACE_AE_UNLIMITED_BIGINT_PARALLELISM); + } + + public static int getAdvancedAlloyFurnaceCatalystParallel(int tier) { + return getConfigValue(CATALYST_TIER_PARALLEL[Math.max(0, Math.min(9, tier))]); + } + + public static int getAdvancedAlloyFurnaceCatalystEnergyDivisor(int tier) { + return getConfigValue(CATALYST_TIER_ENERGY_DIVISOR[Math.max(0, Math.min(9, tier))]); + } + + public static double getAdvancedAlloyFurnaceCatalystTimeMultiplier(int tier) { + return getConfigValue(CATALYST_TIER_TIME_MULTIPLIER[Math.max(0, Math.min(9, tier))]); + } + + public static int getOmniversalCoilThreads(int tier) { + return getConfigValue(OMNIVERSAL_COIL_TIER_THREADS[Math.max(1, Math.min(9, tier)) - 1]); + } + + public static long getOmniversalCoilSingleTaskParallel(int tier) { + return getConfigValue(OMNIVERSAL_COIL_TIER_PARALLEL[Math.max(1, Math.min(9, tier)) - 1]); + } + + public static int getOmniversalCoilEnergyDivisor(int tier) { + return getConfigValue(OMNIVERSAL_COIL_TIER_ENERGY_DIVISOR[Math.max(1, Math.min(9, tier)) - 1]); + } + + public static double getOmniversalCoilTimeMultiplier(int tier) { + return getConfigValue(OMNIVERSAL_COIL_TIER_TIME_MULTIPLIER[Math.max(1, Math.min(9, tier)) - 1]); + } + + public static int getOmniversalUsefulTierThreads() { + return getConfigValue(OMNIVERSAL_USEFUL_TIER_THREADS); + } + + public static boolean isCraftingRecipeConversionEnabled() { + return getConfigValue(ENABLE_CRAFTING_RECIPE_CONVERSION); + } + + public static boolean isSmeltingRecipeConversionEnabled() { + return getConfigValue(ENABLE_SMELTING_RECIPE_CONVERSION); + } + + public static boolean isBrewingRecipeConversionEnabled() { + return getConfigValue(ENABLE_BREWING_RECIPE_CONVERSION); + } + + public static boolean isVillagerTradeRecipeConversionEnabled() { + return getConfigValue(ENABLE_VILLAGER_TRADE_RECIPE_CONVERSION); + } + + public static boolean isFishingRecipeConversionEnabled() { + return getConfigValue(ENABLE_FISHING_RECIPE_CONVERSION); + } + + public static boolean isRecipeConversionEnabled(String sourceId) { + ForgeConfigSpec.BooleanValue option = RECIPE_CONVERSION_OPTIONS.get(sourceId); + return option == null || getConfigValue(option); + } + + public static int getOmniversalPatternSlots() { + return normalizeInventorySlots(getConfigValue(OMNIVERSAL_PATTERN_SLOTS)); + } + + public static int getOmniversalMoldSlots() { + return normalizeInventorySlots(getConfigValue(OMNIVERSAL_MOLD_SLOTS)); + } + + public static int getOmniversalPassivePatternSlots() { + return Math.max(1, Math.min(4096, getConfigValue(OMNIVERSAL_PASSIVE_PATTERN_SLOTS))); + } + + public static int getOmniversalDecodeCacheCapacity() { + return Math.max(64, Math.min(16384, getConfigValue(OMNIVERSAL_DECODE_CACHE_CAPACITY))); + } + + /** + * 万象样板编码后是否自动上传到同网格的样板总成。 + * + *

关闭时样板保留在编码终端内,由玩家自行取出,行为与引入上传功能之前一致。

+ */ + public static boolean isOmniversalPatternAutoUploadEnabled() { + return getConfigValue(OMNIVERSAL_PATTERN_AUTO_UPLOAD); + } + + public static int getOreGeneratorSlots() { + return Math.max(1, Math.min(540, getConfigValue(ORE_GENERATOR_SLOTS))); + } + + /** + * 无线物流:空转的线路是否启用指数退避。 + * + *

关闭后线路严格按配置周期运行,空通道也会持续产生开销。

+ */ + public static boolean isStaffLinkIdleBackoffEnabled() { + return getConfigValue(STAFFLINK_IDLE_BACKOFF_ENABLED); + } + + /** + * 无线物流:空转退避的倍率上限(下一轮间隔最多放大到 {@code 周期 × 本值})。 + * + *

返回的是原始倍率(已夹取到 [1, 1024]);调用方按 {@code log2} 换算成翻倍次数。 + * 非 2 的幂由调用方向下取整,不会报错。

+ */ + public static int getStaffLinkIdleBackoffCap() { + return Math.max(1, Math.min(1024, getConfigValue(STAFFLINK_IDLE_BACKOFF_CAP))); + } + + private static int normalizeInventorySlots(int value) { + int clamped = Math.max(27, Math.min(4096, value)); + return Math.max(27, clamped / 27 * 27); + } + + // 获取连锁挖掘范围配置 + public static int getChainMiningRangeX() { + return getConfigValue(CHAIN_MINING_RANGE_X); + } + + public static int getChainMiningRangeY() { + return getConfigValue(CHAIN_MINING_RANGE_Y); + } + + public static int getChainMiningRangeZ() { + return getConfigValue(CHAIN_MINING_RANGE_Z); + } + + // 获取牛排工具范围伤害配置 + public static int getBeefAoeDamageRangeX() { + return getConfigValue(BEEF_AOE_DAMAGE_RANGE_X); + } + + public static int getBeefAoeDamageRangeY() { + return getConfigValue(BEEF_AOE_DAMAGE_RANGE_Y); + } + + public static int getBeefAoeDamageRangeZ() { + return getConfigValue(BEEF_AOE_DAMAGE_RANGE_Z); + } + + public static int getBeefAoeDamageMaxTargets() { + return getConfigValue(BEEF_AOE_DAMAGE_MAX_TARGETS); + } + + // 杀戮光环结算间隔(tick) + public static int getBeefKillAuraInterval() { + return getConfigValue(BEEF_KILL_AURA_INTERVAL); + } + + // 获取牛排工具范围磁力吸附配置 + public static int getBeefMagnetRangeX() { + return getConfigValue(BEEF_MAGNET_RANGE_X); + } + + public static int getBeefMagnetRangeY() { + return getConfigValue(BEEF_MAGNET_RANGE_Y); + } + + public static int getBeefMagnetRangeZ() { + return getConfigValue(BEEF_MAGNET_RANGE_Z); + } + + public static double getBeefToolFlightSpeed() { + return getConfigValue(BEEF_TOOL_FLIGHT_SPEED); + } + + // 获取牛排工具附魔等级配置 + public static int getFortuneLevel() { + return getConfigValue(FORTUNE_LEVEL); + } + + public static int getLootingLevel() { + return getConfigValue(LOOTING_LEVEL); + } + + // 获取节日掉落触发概率 + public static int getFestiveDropChance() { + return getConfigValue(FESTIVE_DROP_CHANCE); + } + + public static boolean shouldEnableGrassWandDrop() { + return getConfigValue(ENABLE_GRASS_WAND_DROP); + } + + public static double getGrassWandDropProbability() { + return getConfigValue(GRASS_WAND_DROP_PROBABILITY); + } + + // 获取牛排工具基础挖掘速度 + public static double getBeefToolMiningSpeed() { + return getConfigValue(BEEF_TOOL_MINING_SPEED); + } + + // 造化杖是否无视工具挖掘等级 + public static boolean isBeefToolIgnoresToolTier() { + return getConfigValue(BEEF_TOOL_IGNORES_TOOL_TIER); + } + + // 造化杖挖掘「伪不可破坏」方块时使用的名义硬度 + public static double getBeefToolPseudoHardness() { + return getConfigValue(BEEF_TOOL_PSEUDO_HARDNESS); + } + + // 造化杖连点模式:每个客户端 tick 触发多少次右键 + public static int getBeefAutoClickClicksPerTick() { + return getConfigValue(BEEF_AUTOCLICK_CLICKS_PER_TICK); + } + + public static double getBeefToolEntityInteractionRange() { + return getConfigValue(BEEF_TOOL_ENTITY_INTERACTION_RANGE); + } + + public static double getBeefToolBlockInteractionRange() { + return getConfigValue(BEEF_TOOL_BLOCK_INTERACTION_RANGE); + } + + // 造化杖短距闪现的最大距离(格) + public static int getBeefShortTeleportRange() { + return getConfigValue(BEEF_SHORT_TELEPORT_RANGE); + } + + public static int getBeefConstructionWandBuildLimit() { + return getConfigValue(BEEF_CONSTRUCTION_WAND_BUILD_LIMIT); + } + + public static int getBeefConstructionWandAngelLimit() { + return getConfigValue(BEEF_CONSTRUCTION_WAND_ANGEL_LIMIT); + } + + public static int getBeefConstructionWandDestructionLimit() { + return getConfigValue(BEEF_CONSTRUCTION_WAND_DESTRUCTION_LIMIT); + } + + public static List getBeefToolForceMiningBlacklist() { + return readConfigList(BEEF_TOOL_FORCE_MINING_BLACKLIST); + } + + public static List getAE2GiftPackageItems() { + return resolveAE2GiftPackageItems(readConfigList(AE2_GIFT_PACKAGE_ITEMS)); + } + + /** + * Upgrades the automatic fallback entries from older server configs when an + * AE2 addon is installed after the config was first created. Explicit custom + * entries are left untouched. + */ + private static List resolveAE2GiftPackageItems(List configured) { + if (configured.isEmpty()) { + return configured; + } + + List resolved = new ArrayList<>(configured); + if (ModList.get().isLoaded("extendedae_plus")) { + replaceLegacyAE2GiftStorageCells(resolved); + } + if (ModList.get().isLoaded("expatternprovider")) { + replaceLegacyAE2GiftEntry(resolved, + "ae2:drive", 1, + "expatternprovider:ex_drive,1"); + } + + return List.copyOf(resolved); + } + + private static void replaceLegacyAE2GiftStorageCells(List entries) { + String replacement = "extendedae_plus:infinity_biginteger_cell,1"; + if (containsAE2GiftItem(entries, "extendedae_plus:infinity_biginteger_cell")) { + return; + } + + int firstLegacyEntry = -1; + for (int index = 0; index < entries.size(); index++) { + String entry = entries.get(index); + if (entry == null) { + continue; + } + + String[] parts = entry.split(",", -1); + if (parts.length != 2 || !"ae2:item_storage_cell_256k".equals(parts[0].trim())) { + continue; + } + + if (parsePositiveInteger(parts[1].trim()) != 8) { + continue; + } + if (firstLegacyEntry < 0) { + firstLegacyEntry = index; + } + } + + if (firstLegacyEntry < 0) { + return; + } + + for (int index = entries.size() - 1; index >= 0; index--) { + String entry = entries.get(index); + if (entry == null) { + continue; + } + + String[] parts = entry.split(",", -1); + if (parts.length == 2 + && "ae2:item_storage_cell_256k".equals(parts[0].trim()) + && parsePositiveInteger(parts[1].trim()) == 8) { + entries.remove(index); + } + } + entries.add(firstLegacyEntry, replacement); + } + + private static void replaceLegacyAE2GiftEntry( + List entries, String legacyItemId, int legacyCount, String replacementEntry) { + String replacementItemId = replacementEntry.substring(0, replacementEntry.indexOf(',')); + if (containsAE2GiftItem(entries, replacementItemId)) { + return; + } + + for (int index = 0; index < entries.size(); index++) { + String entry = entries.get(index); + if (entry == null) { + continue; + } + + String[] parts = entry.split(",", -1); + if (parts.length == 2 + && legacyItemId.equals(parts[0].trim()) + && parsePositiveInteger(parts[1].trim()) == legacyCount) { + entries.set(index, replacementEntry); + return; + } + } + } + + private static int parsePositiveInteger(String value) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException ignored) { + return -1; + } + } + + private static boolean containsAE2GiftItem(List entries, String itemId) { + for (String entry : entries) { + if (entry == null) { + continue; + } + + int separator = entry.indexOf(','); + if (separator >= 0 && itemId.equals(entry.substring(0, separator).trim())) { + return true; + } + } + return false; + } + + public static boolean isBeefToolForceMiningBlockBlacklisted(ResourceLocation blockId) { + return beefToolForceMiningBlacklistMatcher().matches(blockId); + } + + public static List getUselessDimensionFloorBlockBlacklist() { + return readConfigList(USELESS_DIMENSION_FLOOR_BLOCK_BLACKLIST); + } + + public static List getUselessDimensionFloorBlockWhitelist() { + return readConfigList(USELESS_DIMENSION_FLOOR_BLOCK_WHITELIST); + } + + public static boolean isUselessDimensionFloorBlockBlacklisted(ResourceLocation blockId) { + return uselessDimensionFloorBlockBlacklistMatcher().matches(blockId); + } + + public static boolean isUselessDimensionFloorBlockAllowed(ResourceLocation blockId) { + return isUselessDimensionFloorBlockAllowed(blockId, + uselessDimensionFloorBlockBlacklistMatcher(), + uselessDimensionFloorBlockWhitelistMatcher()); + } + + static boolean isUselessDimensionFloorBlockAllowed( + ResourceLocation blockId, + BlockBlacklistMatcher blacklist, + BlockBlacklistMatcher whitelist) { + if (blockId == null || blacklist.matches(blockId)) return false; + return whitelist.isEmpty() || whitelist.matches(blockId); + } + + public static List getBeefToolForceKillBlacklist() { + return splitEntityIdList(getConfigValue(BEEF_TOOL_FORCE_KILL_BLACKLIST)); + } + + public static List getBeefToolForceKillNonLivingWhitelist() { + return splitEntityIdList(getConfigValue(BEEF_TOOL_FORCE_KILL_NON_LIVING_WHITELIST)); + } + + // 获取药水效果配置 + public static boolean shouldEnablePotionEffects() { + return getConfigValue(ENABLE_POTION_EFFECTS); + } + + public static boolean shouldEnableFlightEffect() { + return getConfigValue(ENABLE_FLIGHT_EFFECT); + } + + // 获取自定义药水效果配置列表 + public static List getCustomPotionEffects() { + return readConfigList(CUSTOM_POTION_EFFECTS); + } + + /** 生物捕捉:额外丢弃的 NBT 键路径(`容器.子键`;不含 `.` 时视为顶层键)。 */ + public static List getBeefCaptureStripKeys() { + return readConfigList(BEEF_CAPTURE_STRIP_KEYS); + } + + /** 生物捕捉:额外丢弃的数据附件命名空间。 */ + public static List getBeefCaptureStripAttachmentNamespaces() { + return readConfigList(BEEF_CAPTURE_STRIP_ATTACHMENT_NAMESPACES); + } + + private static List splitEntityIdList(String value) { + if (value == null || value.isBlank()) { + return List.of(); + } + return List.of(value.split(";")); + } + + private static List readConfigList( + ForgeConfigSpec.ConfigValue> value) { + List configured = getConfigValue(value); + return configured == null ? List.of() : List.copyOf(configured); + } + + private static T getConfigValue(ForgeConfigSpec.ConfigValue value) { + try { + return value.get(); + } catch (IllegalStateException ignored) { + return value.getDefault(); + } + } + + private static BlockBlacklistMatcher uselessDimensionFloorBlockBlacklistMatcher() { + List configured = getUselessDimensionFloorBlockBlacklist(); + if (!configured.equals(cachedUselessDimensionFloorBlockBlacklist)) { + synchronized (ConfigManager.class) { + if (!configured.equals(cachedUselessDimensionFloorBlockBlacklist)) { + uselessDimensionFloorBlockBlacklistMatcher = + new BlockBlacklistMatcher(configured, DIMENSION_FLOOR_BLACKLIST_NAME); + cachedUselessDimensionFloorBlockBlacklist = configured; + } + } + } + return uselessDimensionFloorBlockBlacklistMatcher; + } + + private static BlockBlacklistMatcher uselessDimensionFloorBlockWhitelistMatcher() { + List configured = getUselessDimensionFloorBlockWhitelist(); + if (!configured.equals(cachedUselessDimensionFloorBlockWhitelist)) { + synchronized (ConfigManager.class) { + if (!configured.equals(cachedUselessDimensionFloorBlockWhitelist)) { + uselessDimensionFloorBlockWhitelistMatcher = + new BlockBlacklistMatcher(configured, DIMENSION_FLOOR_WHITELIST_NAME); + cachedUselessDimensionFloorBlockWhitelist = configured; + } + } + } + return uselessDimensionFloorBlockWhitelistMatcher; + } + + private static BlockBlacklistMatcher beefToolForceMiningBlacklistMatcher() { + List configured = getBeefToolForceMiningBlacklist(); + if (!configured.equals(cachedBeefToolForceMiningBlacklist)) { + synchronized (ConfigManager.class) { + if (!configured.equals(cachedBeefToolForceMiningBlacklist)) { + beefToolForceMiningBlacklistMatcher = + new BlockBlacklistMatcher(configured, "beef tool force mining blacklist"); + cachedBeefToolForceMiningBlacklist = configured; + } + } + } + return beefToolForceMiningBlacklistMatcher; + } + +} diff --git a/src/main/java/com/sorrowmist/useless/core/constants/NBTConstants.java b/src/main/java/com/sorrowmist/useless/core/constants/NBTConstants.java new file mode 100644 index 00000000..3af6174d --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/core/constants/NBTConstants.java @@ -0,0 +1,37 @@ +package com.sorrowmist.useless.core.constants; + +public final class NBTConstants { + + public static final String INVENTORY = "Inventory"; + public static final String ENERGY = "Energy"; + public static final String PROGRESS = "Progress"; + public static final String MAX_PROGRESS = "MaxProgress"; + public static final String CURRENT_PARALLEL = "CurrentParallel"; + public static final String HAS_MOLD = "HasMold"; + public static final String CACHED_PARALLEL = "CachedParallel"; + public static final String IS_USELESS_INGOT_RECIPE = "IsUselessIngotRecipe"; + public static final String TARGET_USELESS_INGOT_TIER = "TargetUselessIngotTier"; + public static final String ACCUMULATED_ENERGY = "AccumulatedEnergy"; + + // 熔炉升级相关 + public static final String FURNACE_TIER = "FurnaceTier"; + + private NBTConstants() { + } + + public static String getInputFluidTag(int index) { + return "InputFluid" + index; + } + + public static String getOutputFluidTag(int index) { + return "OutputFluid" + index; + } + + public static String getInputChemicalTag(int index) { + return "InputChemical" + index; + } + + public static String getOutputChemicalTag(int index) { + return "OutputChemical" + index; + } +} diff --git a/src/main/java/com/sorrowmist/useless/data/BeefToolLayout.java b/src/main/java/com/sorrowmist/useless/data/BeefToolLayout.java new file mode 100644 index 00000000..849a03e7 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/data/BeefToolLayout.java @@ -0,0 +1,640 @@ +package com.sorrowmist.useless.data; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * The player-owned layout of the beef tool mode screen. + * + *

本类同时承载「连锁挖掘等价组」——两者都是造化杖的玩家个人设置,存在同一份 + * persistent data 里,因此共用一套校验、同步与导入导出通道。

+ */ +public final class BeefToolLayout { + /** 当前写出的格式版本。v3 新增「生物保护名单」。 */ + public static final int FORMAT_VERSION = 3; + /** 仍然接受读取的最低版本(v1 没有 {@code chainGroups}、v3 之前没有保护名单,读进来都是空)。 */ + public static final int MIN_FORMAT_VERSION = 1; + public static final int MAX_PAGES = 32; + public static final int MAX_GROUPS_PER_PAGE = 16; + public static final int MAX_MODULES_PER_GROUP = 32; + public static final int MAX_TOTAL_MODULES = 128; + public static final int MAX_NAME_LENGTH = 32; + public static final int MAX_CHAIN_GROUPS = 16; + public static final int MAX_CHAIN_ENTRIES_PER_GROUP = 16; + public static final int MAX_CHAIN_ENTRY_LENGTH = 128; + public static final int MAX_TOTAL_CHAIN_ENTRIES = 128; + public static final int MAX_PROTECTED_TYPES = 64; + public static final int MAX_PROTECTED_ENTITIES = 128; + public static final int MAX_PROTECTED_ENTRY_LENGTH = 128; + public static final int MAX_TEXT_LENGTH = 64 * 1024; + + private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().create(); + + private int selectedPage; + private final List pages; + private final List unassignedModules; + private final List> chainGroups; + /** 受保护的生物「种类」:{@code EntityType} 注册名,如 {@code minecraft:villager}。 */ + private final List protectedTypes; + /** 受保护的生物「个体」:UUID 字符串。 */ + private final List protectedEntities; + + public BeefToolLayout(int selectedPage, List pages, List unassignedModules) { + this(selectedPage, pages, unassignedModules, List.of(), List.of(), List.of()); + } + + public BeefToolLayout(int selectedPage, List pages, List unassignedModules, + List> chainGroups) { + this(selectedPage, pages, unassignedModules, chainGroups, List.of(), List.of()); + } + + public BeefToolLayout(int selectedPage, List pages, List unassignedModules, + List> chainGroups, + List protectedTypes, List protectedEntities) { + this.selectedPage = selectedPage; + this.pages = new ArrayList<>(); + if (pages != null) { + for (Page page : pages) { + this.pages.add(page.copy()); + } + } + this.unassignedModules = unassignedModules == null + ? new ArrayList<>() + : new ArrayList<>(unassignedModules); + this.chainGroups = new ArrayList<>(); + if (chainGroups != null) { + for (List group : chainGroups) { + this.chainGroups.add(group == null ? new ArrayList<>() : new ArrayList<>(group)); + } + } + this.protectedTypes = protectedTypes == null + ? new ArrayList<>() + : new ArrayList<>(protectedTypes); + this.protectedEntities = protectedEntities == null + ? new ArrayList<>() + : new ArrayList<>(protectedEntities); + } + + public int selectedPage() { + return selectedPage; + } + + public void setSelectedPage(int selectedPage) { + this.selectedPage = selectedPage; + } + + public List pages() { + return pages; + } + + public List unassignedModules() { + return unassignedModules; + } + + /** + * 连锁挖掘等价组。外层列表的每一项是一个「组」,内层是该组的条目 + * (精确方块 ID、{@code #方块标签} 或 {@code *} 通配符),组内条目互相连锁。 + */ + public List> chainGroups() { + return chainGroups; + } + + /** + * 受保护的生物「种类」:{@code EntityType} 注册名(如 {@code minecraft:villager})。 + * 光环、范围伤害与强制击杀都会跳过这些种类。 + */ + public List protectedTypes() { + return protectedTypes; + } + + /** 受保护的生物「个体」:UUID 字符串。 */ + public List protectedEntities() { + return protectedEntities; + } + + public BeefToolLayout copy() { + return new BeefToolLayout(selectedPage, pages, unassignedModules, chainGroups, + protectedTypes, protectedEntities); + } + + public boolean containsModule(String moduleId) { + if (unassignedModules.contains(moduleId)) { + return true; + } + for (Page page : pages) { + for (Group group : page.groups()) { + if (group.modules().contains(moduleId)) { + return true; + } + } + } + return false; + } + + public void validate() throws LayoutException { + if (pages.isEmpty()) { + throw new LayoutException(Error.INVALID_STRUCTURE); + } + if (pages.size() > MAX_PAGES) { + throw new LayoutException(Error.LIMIT); + } + if (selectedPage < 0) { + throw new LayoutException(Error.INVALID_STRUCTURE); + } + + Set seenModules = new HashSet<>(); + int moduleCount = 0; + for (Page page : pages) { + validateName(page.name()); + if (page.groups().size() > MAX_GROUPS_PER_PAGE) { + throw new LayoutException(Error.LIMIT); + } + for (Group group : page.groups()) { + validateName(group.name()); + if (group.modules().size() > MAX_MODULES_PER_GROUP) { + throw new LayoutException(Error.LIMIT); + } + for (String module : group.modules()) { + validateModuleId(module); + if (!seenModules.add(module)) { + throw new LayoutException(Error.DUPLICATE_MODULE); + } + moduleCount++; + } + } + } + for (String module : unassignedModules) { + validateModuleId(module); + if (!seenModules.add(module)) { + throw new LayoutException(Error.DUPLICATE_MODULE); + } + moduleCount++; + } + if (moduleCount > MAX_TOTAL_MODULES) { + throw new LayoutException(Error.LIMIT); + } + + // 等价组只做结构校验;条目语法与方块注册表相关的判定交给 ChainGroupManager。 + if (chainGroups.size() > MAX_CHAIN_GROUPS) { + throw new LayoutException(Error.LIMIT); + } + int chainEntryCount = 0; + for (List group : chainGroups) { + if (group == null || group.size() > MAX_CHAIN_ENTRIES_PER_GROUP) { + throw new LayoutException(Error.LIMIT); + } + for (String entry : group) { + if (entry == null || entry.isBlank() + || entry.length() > MAX_CHAIN_ENTRY_LENGTH + || entry.indexOf('\n') >= 0 || entry.indexOf('\r') >= 0) { + throw new LayoutException(Error.INVALID_STRUCTURE); + } + chainEntryCount++; + } + } + if (chainEntryCount > MAX_TOTAL_CHAIN_ENTRIES) { + throw new LayoutException(Error.LIMIT); + } + + // 保护名单同样只做结构校验;实体 ID 语法与 UUID 可解析性交给 BeefToolProtectionManager。 + validateProtectionList(protectedTypes, MAX_PROTECTED_TYPES); + validateProtectionList(protectedEntities, MAX_PROTECTED_ENTITIES); + } + + public CompoundTag toNbt() { + CompoundTag root = new CompoundTag(); + root.putInt("version", FORMAT_VERSION); + root.putInt("selected_page", selectedPage); + + ListTag pageList = new ListTag(); + for (Page page : pages) { + CompoundTag pageTag = new CompoundTag(); + pageTag.putString("name", page.name()); + ListTag groupList = new ListTag(); + for (Group group : page.groups()) { + CompoundTag groupTag = new CompoundTag(); + groupTag.putString("name", group.name()); + ListTag modules = new ListTag(); + for (String module : group.modules()) { + modules.add(StringTag.valueOf(module)); + } + groupTag.put("modules", modules); + groupList.add(groupTag); + } + pageTag.put("groups", groupList); + pageList.add(pageTag); + } + root.put("pages", pageList); + + ListTag unassigned = new ListTag(); + for (String module : unassignedModules) { + unassigned.add(StringTag.valueOf(module)); + } + root.put("unassigned", unassigned); + + ListTag chainGroupList = new ListTag(); + for (List group : chainGroups) { + ListTag entryList = new ListTag(); + for (String entry : group) { + entryList.add(StringTag.valueOf(entry)); + } + chainGroupList.add(entryList); + } + root.put("chain_groups", chainGroupList); + + ListTag protectedTypeList = new ListTag(); + for (String type : protectedTypes) { + protectedTypeList.add(StringTag.valueOf(type)); + } + root.put("protected_types", protectedTypeList); + + ListTag protectedEntityList = new ListTag(); + for (String uuid : protectedEntities) { + protectedEntityList.add(StringTag.valueOf(uuid)); + } + root.put("protected_entities", protectedEntityList); + return root; + } + + public static BeefToolLayout fromNbt(CompoundTag root) throws LayoutException { + if (!root.contains("version", Tag.TAG_INT) + || !root.contains("selected_page", Tag.TAG_INT) + || !root.contains("pages", Tag.TAG_LIST) + || !root.contains("unassigned", Tag.TAG_LIST)) { + throw new LayoutException(Error.UNSUPPORTED_VERSION); + } + int version = root.getInt("version"); + if (version < MIN_FORMAT_VERSION || version > FORMAT_VERSION) { + throw new LayoutException(Error.UNSUPPORTED_VERSION); + } + + List pages = new ArrayList<>(); + ListTag pageList = root.getList("pages", Tag.TAG_COMPOUND); + for (int i = 0; i < pageList.size(); i++) { + CompoundTag pageTag = pageList.getCompound(i); + if (!pageTag.contains("name", Tag.TAG_STRING) + || !pageTag.contains("groups", Tag.TAG_LIST)) { + throw new LayoutException(Error.INVALID_STRUCTURE); + } + List groups = new ArrayList<>(); + ListTag groupList = pageTag.getList("groups", Tag.TAG_COMPOUND); + for (int j = 0; j < groupList.size(); j++) { + CompoundTag groupTag = groupList.getCompound(j); + if (!groupTag.contains("name", Tag.TAG_STRING) + || !groupTag.contains("modules", Tag.TAG_LIST)) { + throw new LayoutException(Error.INVALID_STRUCTURE); + } + List modules = readStringList(groupTag.getList("modules", Tag.TAG_STRING)); + groups.add(new Group(groupTag.getString("name"), modules)); + } + pages.add(new Page(pageTag.getString("name"), groups)); + } + + List unassigned = readStringList(root.getList("unassigned", Tag.TAG_STRING)); + List> chainGroups = version >= 2 && root.contains("chain_groups", Tag.TAG_LIST) + ? readNestedStringList(root.getList("chain_groups", Tag.TAG_LIST)) + : List.of(); + List protectedTypes = version >= 3 && root.contains("protected_types", Tag.TAG_LIST) + ? readStringList(root.getList("protected_types", Tag.TAG_STRING)) + : List.of(); + List protectedEntities = version >= 3 && root.contains("protected_entities", Tag.TAG_LIST) + ? readStringList(root.getList("protected_entities", Tag.TAG_STRING)) + : List.of(); + BeefToolLayout layout = new BeefToolLayout( + root.getInt("selected_page"), pages, unassigned, chainGroups, + protectedTypes, protectedEntities); + layout.validate(); + return layout; + } + + public String toJson() { + JsonObject root = new JsonObject(); + root.addProperty("version", FORMAT_VERSION); + root.addProperty("selectedPage", selectedPage); + + JsonArray pageArray = new JsonArray(); + for (Page page : pages) { + JsonObject pageObject = new JsonObject(); + pageObject.addProperty("name", page.name()); + JsonArray groupArray = new JsonArray(); + for (Group group : page.groups()) { + JsonObject groupObject = new JsonObject(); + groupObject.addProperty("name", group.name()); + JsonArray moduleArray = new JsonArray(); + for (String module : group.modules()) { + moduleArray.add(module); + } + groupObject.add("modules", moduleArray); + groupArray.add(groupObject); + } + pageObject.add("groups", groupArray); + pageArray.add(pageObject); + } + root.add("pages", pageArray); + + JsonArray unassigned = new JsonArray(); + for (String module : unassignedModules) { + unassigned.add(module); + } + root.add("unassigned", unassigned); + + JsonArray chainGroupArray = new JsonArray(); + for (List group : chainGroups) { + JsonArray entryArray = new JsonArray(); + for (String entry : group) { + entryArray.add(entry); + } + chainGroupArray.add(entryArray); + } + root.add("chainGroups", chainGroupArray); + + JsonArray protectedTypeArray = new JsonArray(); + for (String type : protectedTypes) { + protectedTypeArray.add(type); + } + root.add("protectedTypes", protectedTypeArray); + + JsonArray protectedEntityArray = new JsonArray(); + for (String uuid : protectedEntities) { + protectedEntityArray.add(uuid); + } + root.add("protectedEntities", protectedEntityArray); + return GSON.toJson(root); + } + + public static BeefToolLayout fromJson(String text) throws LayoutException { + if (text == null || text.getBytes(StandardCharsets.UTF_8).length > MAX_TEXT_LENGTH) { + throw new LayoutException(Error.LIMIT); + } + + try { + JsonElement parsed = JsonParser.parseString(text); + if (!parsed.isJsonObject()) { + throw new LayoutException(Error.INVALID_TEXT); + } + JsonObject root = parsed.getAsJsonObject(); + if (!root.has("version") || !root.has("selectedPage") + || !root.has("pages") || !root.has("unassigned")) { + throw new LayoutException(Error.UNSUPPORTED_VERSION); + } + int version = root.get("version").getAsInt(); + if (version < MIN_FORMAT_VERSION || version > FORMAT_VERSION) { + throw new LayoutException(Error.UNSUPPORTED_VERSION); + } + + JsonArray pageArray = requireArray(root, "pages"); + List pages = new ArrayList<>(); + for (JsonElement pageElement : pageArray) { + JsonObject pageObject = requireObject(pageElement); + String name = requireString(pageObject, "name"); + JsonArray groupArray = requireArray(pageObject, "groups"); + List groups = new ArrayList<>(); + for (JsonElement groupElement : groupArray) { + JsonObject groupObject = requireObject(groupElement); + String groupName = requireString(groupObject, "name"); + JsonArray moduleArray = requireArray(groupObject, "modules"); + List modules = new ArrayList<>(); + for (JsonElement module : moduleArray) { + modules.add(requireString(module)); + } + groups.add(new Group(groupName, modules)); + } + pages.add(new Page(name, groups)); + } + + List unassigned = new ArrayList<>(); + for (JsonElement module : requireArray(root, "unassigned")) { + unassigned.add(requireString(module)); + } + + List> chainGroups = new ArrayList<>(); + if (version >= 2 && root.has("chainGroups")) { + for (JsonElement groupElement : requireArray(root, "chainGroups")) { + if (groupElement == null || !groupElement.isJsonArray()) { + throw new LayoutException(Error.INVALID_STRUCTURE); + } + List entries = new ArrayList<>(); + for (JsonElement entry : groupElement.getAsJsonArray()) { + entries.add(requireString(entry)); + } + chainGroups.add(entries); + } + } + + List protectedTypes = new ArrayList<>(); + if (version >= 3 && root.has("protectedTypes")) { + for (JsonElement entry : requireArray(root, "protectedTypes")) { + protectedTypes.add(requireString(entry)); + } + } + + List protectedEntities = new ArrayList<>(); + if (version >= 3 && root.has("protectedEntities")) { + for (JsonElement entry : requireArray(root, "protectedEntities")) { + protectedEntities.add(requireString(entry)); + } + } + + BeefToolLayout layout = new BeefToolLayout( + root.get("selectedPage").getAsInt(), pages, unassigned, chainGroups, + protectedTypes, protectedEntities); + layout.validate(); + return layout; + } catch (LayoutException exception) { + throw exception; + } catch (JsonParseException | IllegalStateException | UnsupportedOperationException + | ClassCastException | NumberFormatException exception) { + throw new LayoutException(Error.INVALID_TEXT); + } + } + + private static JsonArray requireArray(JsonObject object, String key) throws LayoutException { + JsonElement value = object.get(key); + if (value == null || !value.isJsonArray()) { + throw new LayoutException(Error.INVALID_STRUCTURE); + } + return value.getAsJsonArray(); + } + + private static JsonObject requireObject(JsonElement element) throws LayoutException { + if (element == null || !element.isJsonObject()) { + throw new LayoutException(Error.INVALID_STRUCTURE); + } + return element.getAsJsonObject(); + } + + private static String requireString(JsonObject object, String key) throws LayoutException { + JsonElement value = object.get(key); + if (value == null) { + throw new LayoutException(Error.INVALID_STRUCTURE); + } + return requireString(value); + } + + private static String requireString(JsonElement element) throws LayoutException { + if (element == null || !element.isJsonPrimitive() || !element.getAsJsonPrimitive().isString()) { + throw new LayoutException(Error.INVALID_STRUCTURE); + } + return element.getAsString(); + } + + private static List readStringList(ListTag list) throws LayoutException { + List values = new ArrayList<>(); + for (int i = 0; i < list.size(); i++) { + if (!(list.get(i) instanceof net.minecraft.nbt.StringTag)) { + throw new LayoutException(Error.INVALID_STRUCTURE); + } + values.add(list.getString(i)); + } + return values; + } + + /** 读取「组 → 条目」的嵌套字符串列表(外层与内层都是 TAG_LIST)。 */ + private static List> readNestedStringList(ListTag list) throws LayoutException { + List> groups = new ArrayList<>(); + for (int i = 0; i < list.size(); i++) { + if (!(list.get(i) instanceof ListTag entryList)) { + throw new LayoutException(Error.INVALID_STRUCTURE); + } + groups.add(readStringList(entryList)); + } + return groups; + } + + private static void validateName(String name) throws LayoutException { + if (name == null || name.isBlank() + || name.codePointCount(0, name.length()) > MAX_NAME_LENGTH + || name.indexOf('\n') >= 0 || name.indexOf('\r') >= 0) { + throw new LayoutException(Error.INVALID_NAME); + } + } + + private static void validateModuleId(String module) throws LayoutException { + if (module == null || module.isBlank() || module.length() > 64 + || module.indexOf('\n') >= 0 || module.indexOf('\r') >= 0) { + throw new LayoutException(Error.INVALID_STRUCTURE); + } + } + + /** + * 保护名单的纯结构校验:数量上限 + 条目非空、长度、无换行。 + * + *

刻意不查实体注册表、不解析 UUID —— 与连锁等价组「语法合法但当前解析不到的条目 + * 允许保存」的宽容语义一致,也不会因为某个模组没装就写不进去。

+ */ + private static void validateProtectionList(List list, int max) throws LayoutException { + if (list == null || list.size() > max) { + throw new LayoutException(Error.LIMIT); + } + for (String entry : list) { + if (entry == null || entry.isBlank() + || entry.length() > MAX_PROTECTED_ENTRY_LENGTH + || entry.indexOf('\n') >= 0 || entry.indexOf('\r') >= 0) { + throw new LayoutException(Error.INVALID_STRUCTURE); + } + } + } + + public static final class Page { + private String name; + private final List groups; + + public Page(String name, List groups) { + this.name = name; + this.groups = new ArrayList<>(); + if (groups != null) { + for (Group group : groups) { + this.groups.add(group.copy()); + } + } + } + + public Page(String name) { + this(name, List.of()); + } + + public String name() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List groups() { + return groups; + } + + private Page copy() { + return new Page(name, groups); + } + } + + public static final class Group { + private String name; + private final List modules; + + public Group(String name, List modules) { + this.name = name; + this.modules = modules == null ? new ArrayList<>() : new ArrayList<>(modules); + } + + public Group(String name) { + this(name, List.of()); + } + + public String name() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List modules() { + return modules; + } + + private Group copy() { + return new Group(name, modules); + } + } + + public enum Error { + INVALID_TEXT, + UNSUPPORTED_VERSION, + INVALID_STRUCTURE, + INVALID_NAME, + DUPLICATE_MODULE, + UNKNOWN_MODULE, + LIMIT + } + + public static final class LayoutException extends Exception { + private final Error error; + + public LayoutException(Error error) { + super(error.name()); + this.error = error; + } + + public Error error() { + return error; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/data/BeefToolLayoutManager.java b/src/main/java/com/sorrowmist/useless/data/BeefToolLayoutManager.java new file mode 100644 index 00000000..e3535723 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/data/BeefToolLayoutManager.java @@ -0,0 +1,180 @@ +package com.sorrowmist.useless.data; + +import com.sorrowmist.useless.core.config.BeefToolProtectionManager; +import com.sorrowmist.useless.core.config.ChainGroupManager; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; + +import java.util.List; + +/** Loads and stores the beef tool layout in a player's persistent save data. */ +public final class BeefToolLayoutManager { + private static final String ROOT_TAG = "useless_mod:beef_tool_layout"; + private static final String VANILLA_GROUP_MIGRATION_TAG = "useless_mod:beef_tool_vanilla_group"; + private static final String EXDEORUM_GROUP_MIGRATION_TAG = "useless_mod:beef_tool_exdeorum_group"; + + private BeefToolLayoutManager() { + } + + public static BeefToolLayout loadOrCreate(ServerPlayer player) { + CompoundTag root = get(player); + if (root == null) { + BeefToolLayout layout = BeefToolModuleRegistry.defaultLayout(); + normalizeForPlayer(player, layout); + save(player, layout); + return layout; + } + + try { + BeefToolLayout layout = BeefToolLayout.fromNbt(root); + validateKnownModules(layout); + normalizeForPlayer(player, layout); + save(player, layout); + return layout; + } catch (BeefToolLayout.LayoutException exception) { + BeefToolLayout layout = BeefToolModuleRegistry.defaultLayout(); + normalizeForPlayer(player, layout); + save(player, layout); + return layout; + } + } + + public static BeefToolLayout normalizeForPlayer(ServerPlayer player, BeefToolLayout layout) { + migrateVanillaModulesOnce(player, layout); + migrateExDeorumModulesOnce(player, layout); + BeefToolModuleRegistry.addMissingAvailableModules(layout, findTarget(player)); + if (!layout.pages().isEmpty()) { + layout.setSelectedPage(Math.max(0, + Math.min(layout.selectedPage(), layout.pages().size() - 1))); + } + return layout; + } + + /** + * 首次加载既有存档时,把归在其它分组的原版工具动作迁入「原版」分组。 + * + *

迁移结果写入持久标记,每个玩家只执行一次, + * 以免覆盖玩家此后在模式配置界面中对分组的调整。 + */ + private static void migrateVanillaModulesOnce(ServerPlayer player, BeefToolLayout layout) { + CompoundTag playerData = playerData(player); + if (playerData != null && playerData.getBoolean(VANILLA_GROUP_MIGRATION_TAG)) { + return; + } + BeefToolModuleRegistry.migrateVanillaModules(layout); + setFlag(player, VANILLA_GROUP_MIGRATION_TAG); + } + + /** + * 首次加载既有存档时,把 Ex Deorum 工具模式迁入其专属分组。 + * + *

三项在旧版本中归入「挖掘」分组并已随存档落盘,补缺逻辑只处理布局中 + * 尚不存在的模块,因此需要显式迁移;迁移结果同样写入持久标记,每个玩家只执行一次。

+ */ + private static void migrateExDeorumModulesOnce(ServerPlayer player, BeefToolLayout layout) { + CompoundTag playerData = playerData(player); + if (playerData != null && playerData.getBoolean(EXDEORUM_GROUP_MIGRATION_TAG)) { + return; + } + BeefToolModuleRegistry.migrateExDeorumModules(layout); + setFlag(player, EXDEORUM_GROUP_MIGRATION_TAG); + } + + public static void validateForSave(BeefToolLayout layout) throws BeefToolLayout.LayoutException { + layout.validate(); + validateKnownModules(layout); + ChainGroupManager.validateEntries(layout.chainGroups()); + BeefToolProtectionManager.validateEntries(layout.protectedTypes(), layout.protectedEntities()); + } + + /** + * 读取玩家已有的布局;不存在或解析失败返回 {@code null}(不创建、不写回)。 + */ + public static BeefToolLayout load(ServerPlayer player) { + CompoundTag root = get(player); + if (root == null) { + return null; + } + try { + return BeefToolLayout.fromNbt(root); + } catch (BeefToolLayout.LayoutException exception) { + return null; + } + } + + /** + * 只读地取出玩家的连锁等价组:不 normalize、不写回。 + * + *

供连锁判定的缓存未命中路径使用,避免在读路径上产生写副作用。 + * 存档缺失或解析失败一律返回空列表(等价于「没有任何等价组」)。

+ */ + public static List> chainGroups(ServerPlayer player) { + BeefToolLayout layout = load(player); + return layout == null ? List.of() : layout.chainGroups(); + } + + public static void save(ServerPlayer player, BeefToolLayout layout) { + try { + validateForSave(layout); + } catch (BeefToolLayout.LayoutException exception) { + return; + } + + CompoundTag persistentData = player.getPersistentData(); + if (!persistentData.contains(Player.PERSISTED_NBT_TAG, Tag.TAG_COMPOUND)) { + persistentData.put(Player.PERSISTED_NBT_TAG, new CompoundTag()); + } + persistentData.getCompound(Player.PERSISTED_NBT_TAG).put(ROOT_TAG, layout.toNbt()); + } + + private static CompoundTag playerData(ServerPlayer player) { + CompoundTag persistentData = player.getPersistentData(); + if (!persistentData.contains(Player.PERSISTED_NBT_TAG, Tag.TAG_COMPOUND)) { + return null; + } + return persistentData.getCompound(Player.PERSISTED_NBT_TAG); + } + + private static CompoundTag get(ServerPlayer player) { + CompoundTag playerData = playerData(player); + return playerData != null && playerData.contains(ROOT_TAG, Tag.TAG_COMPOUND) + ? playerData.getCompound(ROOT_TAG) + : null; + } + + private static void setFlag(ServerPlayer player, String key) { + CompoundTag persistentData = player.getPersistentData(); + if (!persistentData.contains(Player.PERSISTED_NBT_TAG, Tag.TAG_COMPOUND)) { + persistentData.put(Player.PERSISTED_NBT_TAG, new CompoundTag()); + } + persistentData.getCompound(Player.PERSISTED_NBT_TAG).putBoolean(key, true); + } + + private static void validateKnownModules(BeefToolLayout layout) throws BeefToolLayout.LayoutException { + for (BeefToolLayout.Page page : layout.pages()) { + for (BeefToolLayout.Group group : page.groups()) { + for (String module : group.modules()) { + validateKnownModule(module); + } + } + } + for (String module : layout.unassignedModules()) { + validateKnownModule(module); + } + } + + private static void validateKnownModule(String module) throws BeefToolLayout.LayoutException { + if (!BeefToolModuleRegistry.isKnown(module)) { + throw new BeefToolLayout.LayoutException(BeefToolLayout.Error.UNKNOWN_MODULE); + } + } + + private static ItemStack findTarget(ServerPlayer player) { + return com.sorrowmist.useless.utils.UselessItemUtils.findTargetToolInHands(player) + .map(java.util.AbstractMap.SimpleImmutableEntry::getKey) + .orElse(ItemStack.EMPTY); + } +} diff --git a/src/main/java/com/sorrowmist/useless/data/BeefToolModuleRegistry.java b/src/main/java/com/sorrowmist/useless/data/BeefToolModuleRegistry.java new file mode 100644 index 00000000..e8d71dc6 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/data/BeefToolModuleRegistry.java @@ -0,0 +1,667 @@ +package com.sorrowmist.useless.data; + +import com.sorrowmist.useless.api.enums.tool.EnchantMode; +import com.sorrowmist.useless.api.enums.tool.ModeTypeEnum; +import com.sorrowmist.useless.api.enums.tool.ToolTypeMode; +import com.sorrowmist.useless.content.items.BeefToolVariants; +import com.sorrowmist.useless.content.items.EndlessBeafItem; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.fml.ModList; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** Stable IDs and availability rules for every button on the beef tool screen. */ +public final class BeefToolModuleRegistry { + public static final String ENCHANT_SILK_TOUCH = "enchant.silk_touch"; + public static final String ENCHANT_FORTUNE = "enchant.fortune"; + public static final String TOOL_NONE = "tool.none"; + public static final String TOOL_WRENCH = "tool.wrench"; + public static final String TOOL_SCREWDRIVER = "tool.screwdriver"; + public static final String TOOL_MALLET = "tool.mallet"; + public static final String TOOL_CROWBAR = "tool.crowbar"; + public static final String TOOL_HAMMER = "tool.hammer"; + public static final String TOOL_OMNITOOL = "tool.omnitool"; + public static final String CONSTRUCTION_WAND = "mode.construction_wand"; + public static final String CONSTRUCTION_WAND_ANGEL = "mode.construction_wand_angel"; + public static final String CONSTRUCTION_WAND_DESTRUCTION = "mode.construction_wand_destruction"; + public static final String ENHANCED_CHAIN_MINING = "mode.enhanced_chain_mining"; + public static final String FORCE_MINING = "mode.force_mining"; + public static final String AUTO_SMELT = "mode.auto_smelt"; + public static final String AE_STORAGE_PRIORITY = "mode.ae_storage_priority"; + public static final String AE_NETWORK_CONNECT = "mode.ae_network_connect"; + public static final String WRENCH_TAG = "mode.wrench_tag"; + public static final String FORCE_KILL = "mode.force_kill"; + public static final String BEEF_MALUM_SPIRIT = "mode.beef_malum_spirit"; + public static final String BEEF_MYSTICAL_AGRICULTURE = "mode.beef_mystical_agriculture"; + public static final String BEEF_BEHEADING = "mode.beef_beheading"; + public static final String BEEF_TIME_ACCELERATION = "mode.beef_time_acceleration"; + public static final String BEEF_INVULNERABILITY = "mode.beef_invulnerability"; + public static final String BEEF_ADVANCED_STEALTH = "mode.beef_advanced_stealth"; + public static final String BEEF_CAPTURE = "mode.beef_capture"; + public static final String BEEF_TELEPORT = "mode.beef_teleport"; + public static final String BEEF_AOE_DAMAGE = "mode.beef_aoe_damage"; + public static final String BEEF_MAGNET = "mode.beef_magnet"; + public static final String BEEF_FARMLAND_MODE = "mode.beef_farmland"; + public static final String BEEF_CROP_HARVEST = "mode.beef_crop_harvest"; + public static final String BEEF_SHEARS = "mode.beef_shears"; + public static final String BEEF_FLINT_AND_STEEL = "mode.beef_flint_and_steel"; + public static final String BEEF_RITUAL_SATCHEL = "mode.beef_ritual_satchel"; + public static final String BEEF_RIPEN = "mode.beef_ripen"; + public static final String BEEF_FORCE_GROW = "mode.beef_force_grow"; + public static final String BEEF_AUTO_CLICK = "mode.beef_auto_click"; + public static final String BEEF_WIRELESS_LOGISTICS = "mode.beef_wireless_logistics"; + public static final String BEEF_KILL_AURA = "mode.beef_kill_aura"; + public static final String BEEF_PROTECT_MODE = "mode.beef_protect_mode"; + public static final String BEEF_ENTITY_TIME_ACCELERATION = "mode.beef_entity_time_acceleration"; + public static final String EXDEORUM_CROOK = "mode.exdeorum_crook"; + public static final String EXDEORUM_HAMMER = "mode.exdeorum_hammer"; + public static final String EXDEORUM_COMPRESSED_HAMMER = "mode.exdeorum_compressed_hammer"; + + private static final List DEFINITIONS = List.of( + new Definition(ENCHANT_SILK_TOUCH, EnchantMode.SILK_TOUCH.getTooltip(), GroupKind.TOOLS, + Availability.ALWAYS, true), + new Definition(ENCHANT_FORTUNE, EnchantMode.FORTUNE.getTooltip(), GroupKind.TOOLS, + Availability.ALWAYS, true), + new Definition(TOOL_NONE, ToolTypeMode.NONE_MODE.getTooltip(), GroupKind.TOOLS, + Availability.REMOVED, true), + new Definition(TOOL_WRENCH, ToolTypeMode.WRENCH_MODE.getTooltip(), GroupKind.TOOLS, + Availability.REMOVED, true), + new Definition(TOOL_SCREWDRIVER, ToolTypeMode.SCREWDRIVER_MODE.getTooltip(), GroupKind.TOOLS, + Availability.REMOVED, true), + new Definition(TOOL_MALLET, ToolTypeMode.MALLET_MODE.getTooltip(), GroupKind.TOOLS, + Availability.REMOVED, true), + new Definition(TOOL_CROWBAR, ToolTypeMode.CROWBAR_MODE.getTooltip(), GroupKind.TOOLS, + Availability.REMOVED, true), + new Definition(TOOL_HAMMER, ToolTypeMode.HAMMER_MODE.getTooltip(), GroupKind.TOOLS, + Availability.REMOVED, true), + new Definition(TOOL_OMNITOOL, ToolTypeMode.OMNITOOL_MODE.getTooltip(), GroupKind.TOOLS, + Availability.OMNITOOLS, false), + new Definition(CONSTRUCTION_WAND, ModeTypeEnum.CONSTRUCTION_WAND_ENABLED.getTooltip(), GroupKind.MINING, + Availability.ENDLESS, false), + new Definition(CONSTRUCTION_WAND_ANGEL, ModeTypeEnum.CONSTRUCTION_WAND_ANGEL_CORE.getTooltip(), GroupKind.MINING, + Availability.ENDLESS, true), + new Definition(CONSTRUCTION_WAND_DESTRUCTION, ModeTypeEnum.CONSTRUCTION_WAND_DESTRUCTION_CORE.getTooltip(), GroupKind.MINING, + Availability.ENDLESS, true), + new Definition(ENHANCED_CHAIN_MINING, ModeTypeEnum.ENHANCED_CHAIN_MINING_ENABLED.getTooltip(), GroupKind.MINING, + Availability.ALWAYS, false), + new Definition(FORCE_MINING, ModeTypeEnum.FORCE_MINING_ENABLED.getTooltip(), GroupKind.MINING, + Availability.ALWAYS, false), + new Definition(AUTO_SMELT, ModeTypeEnum.AUTO_SMELT_ENABLED.getTooltip(), GroupKind.MINING, + Availability.ALWAYS, false), + new Definition(EXDEORUM_CROOK, ModeTypeEnum.EXDEORUM_CROOK_ENABLED.getTooltip(), GroupKind.EXDEORUM, + Availability.EXDEORUM, false), + new Definition(EXDEORUM_HAMMER, ModeTypeEnum.EXDEORUM_HAMMER_ENABLED.getTooltip(), GroupKind.EXDEORUM, + Availability.EXDEORUM, false), + new Definition(EXDEORUM_COMPRESSED_HAMMER, ModeTypeEnum.EXDEORUM_COMPRESSED_HAMMER_ENABLED.getTooltip(), + GroupKind.EXDEORUM, Availability.EXDEORUM, false), + new Definition(AE_STORAGE_PRIORITY, ModeTypeEnum.AE_STORAGE_PRIORITY_ENABLED.getTooltip(), GroupKind.MINING, + Availability.AE2, false), + new Definition(AE_NETWORK_CONNECT, ModeTypeEnum.AE_NETWORK_CONNECT_ENABLED.getTooltip(), GroupKind.MINING, + Availability.AE2, false), + new Definition(WRENCH_TAG, ModeTypeEnum.WRENCH_TAG_ENABLED.getTooltip(), GroupKind.MINING, + Availability.BASE, false), + new Definition(FORCE_KILL, ModeTypeEnum.FORCE_KILL.getTooltip(), GroupKind.COMBAT, + Availability.ALWAYS, false), + new Definition(BEEF_MALUM_SPIRIT, ModeTypeEnum.BEEF_MALUM_SPIRIT_ENABLED.getTooltip(), GroupKind.COMBAT, + Availability.MALUM, false), + new Definition(BEEF_MYSTICAL_AGRICULTURE, + ModeTypeEnum.BEEF_MYSTICAL_AGRICULTURE_ENABLED.getTooltip(), GroupKind.COMBAT, + Availability.MYSTICAL_AGRICULTURE, false), + new Definition(BEEF_BEHEADING, ModeTypeEnum.BEEF_BEHEADING_ENABLED.getTooltip(), GroupKind.COMBAT, + Availability.ENDLESS, false), + new Definition(BEEF_CAPTURE, ModeTypeEnum.BEEF_CAPTURE_ENABLED.getTooltip(), GroupKind.COMBAT, + Availability.ENDLESS, false), + new Definition(BEEF_AOE_DAMAGE, ModeTypeEnum.BEEF_AOE_DAMAGE_ENABLED.getTooltip(), GroupKind.COMBAT, + Availability.ENDLESS, false), + new Definition(BEEF_TIME_ACCELERATION, ModeTypeEnum.BEEF_TIME_ACCELERATION_ENABLED.getTooltip(), GroupKind.AUXILIARY, + Availability.ENDLESS, false), + new Definition(BEEF_ENTITY_TIME_ACCELERATION, + ModeTypeEnum.BEEF_ENTITY_TIME_ACCELERATION_ENABLED.getTooltip(), GroupKind.AUXILIARY, + Availability.ENDLESS, false), + new Definition(BEEF_INVULNERABILITY, ModeTypeEnum.BEEF_INVULNERABILITY_ENABLED.getTooltip(), GroupKind.AUXILIARY, + Availability.ALWAYS, false), + new Definition(BEEF_ADVANCED_STEALTH, ModeTypeEnum.BEEF_ADVANCED_STEALTH_ENABLED.getTooltip(), GroupKind.AUXILIARY, + Availability.ALWAYS, false), + new Definition(BEEF_TELEPORT, ModeTypeEnum.BEEF_TELEPORT_ENABLED.getTooltip(), GroupKind.AUXILIARY, + Availability.ENDLESS, false), + new Definition(BEEF_MAGNET, ModeTypeEnum.BEEF_MAGNET_ENABLED.getTooltip(), GroupKind.AUXILIARY, + Availability.ENDLESS, false), + new Definition(BEEF_FARMLAND_MODE, ModeTypeEnum.BEEF_FARMLAND_MODE_ENABLED.getTooltip(), + GroupKind.VANILLA, Availability.ALWAYS, false), + new Definition(BEEF_CROP_HARVEST, ModeTypeEnum.BEEF_CROP_HARVEST_ENABLED.getTooltip(), + GroupKind.VANILLA, Availability.ALWAYS, false), + new Definition(BEEF_SHEARS, ModeTypeEnum.BEEF_SHEARS_ENABLED.getTooltip(), + GroupKind.VANILLA, Availability.ALWAYS, false), + new Definition(BEEF_FLINT_AND_STEEL, ModeTypeEnum.BEEF_FLINT_AND_STEEL_ENABLED.getTooltip(), + GroupKind.VANILLA, Availability.ALWAYS, false), + new Definition(BEEF_RITUAL_SATCHEL, ModeTypeEnum.BEEF_RITUAL_SATCHEL_ENABLED.getTooltip(), + GroupKind.MINING, Availability.OCCULTISM, false), + new Definition(BEEF_RIPEN, ModeTypeEnum.BEEF_RIPEN_ENABLED.getTooltip(), + GroupKind.VANILLA, Availability.ALWAYS, false), + new Definition(BEEF_FORCE_GROW, ModeTypeEnum.BEEF_FORCE_GROW_ENABLED.getTooltip(), + GroupKind.VANILLA, Availability.ALWAYS, false), + new Definition(BEEF_AUTO_CLICK, ModeTypeEnum.BEEF_AUTO_CLICK_ENABLED.getTooltip(), + GroupKind.AUXILIARY, Availability.ALWAYS, false), + new Definition(BEEF_WIRELESS_LOGISTICS, ModeTypeEnum.BEEF_WIRELESS_LOGISTICS_ENABLED.getTooltip(), + GroupKind.AUXILIARY, Availability.ENDLESS, false), + new Definition(BEEF_KILL_AURA, ModeTypeEnum.BEEF_KILL_AURA_ENABLED.getTooltip(), + GroupKind.COMBAT, Availability.ENDLESS, false), + new Definition(BEEF_PROTECT_MODE, ModeTypeEnum.BEEF_PROTECT_MODE_ENABLED.getTooltip(), + GroupKind.COMBAT, Availability.ENDLESS, false) + ); + private static final List AUTO_COMBAT_MODULES = List.of( + BEEF_MALUM_SPIRIT, + BEEF_MYSTICAL_AGRICULTURE, + BEEF_BEHEADING, + BEEF_KILL_AURA, + BEEF_PROTECT_MODE); + /** 新增的辅助类模块:老存档的布局里没有它们,进游戏时自动补进「辅助」分组。 */ + private static final List AUTO_AUXILIARY_MODULES = List.of( + BEEF_AUTO_CLICK, + BEEF_WIRELESS_LOGISTICS, + BEEF_ENTITY_TIME_ACCELERATION); + /** 「原版」分组的模块:承载原版工具动作(右键、剪羊毛、点火、催熟等),老存档自动补组。 */ + private static final List AUTO_VANILLA_MODULES = List.of( + BEEF_FARMLAND_MODE, + BEEF_CROP_HARVEST, + BEEF_SHEARS, + BEEF_FLINT_AND_STEEL, + BEEF_RIPEN, + BEEF_FORCE_GROW); + /** 新增的挖掘类模块:老存档的布局里没有它们,进游戏时自动补进「挖掘」分组。 */ + private static final List AUTO_MINING_MODULES = List.of( + AE_NETWORK_CONNECT, + AUTO_SMELT, + BEEF_RITUAL_SATCHEL); + /** + * Ex Deorum 专属分组的模块:三者共用同一套配方体系,单独成组承载, + * 既不必在按钮上标注所属模组,也避免其较长的功能名撑宽通用分组的按钮。 + */ + private static final List AUTO_EXDEORUM_MODULES = List.of( + EXDEORUM_CROOK, + EXDEORUM_HAMMER, + EXDEORUM_COMPRESSED_HAMMER); + + private static final Map BY_ID; + + static { + Map definitions = new HashMap<>(); + for (Definition definition : DEFINITIONS) { + definitions.put(definition.id(), definition); + } + BY_ID = Collections.unmodifiableMap(definitions); + } + + private BeefToolModuleRegistry() { + } + + public static List definitions() { + return DEFINITIONS; + } + + public static Definition get(String id) { + return BY_ID.get(id); + } + + public static boolean isKnown(String id) { + return BY_ID.containsKey(id); + } + + public static boolean isAvailable(String id, ItemStack target) { + Definition definition = get(id); + return definition != null && definition.availability().isAvailable(target); + } + + public static Component name(String id) { + Definition definition = get(id); + return definition == null ? Component.literal(id) : definition.name(); + } + + public static boolean isExclusive(String id) { + Definition definition = get(id); + return definition != null && definition.exclusive(); + } + + public static BeefToolLayout defaultLayout() { + List pages = new ArrayList<>(); + BeefToolLayout.Page page = new BeefToolLayout.Page("Page 1"); + for (GroupKind kind : GroupKind.values()) { + if (!kind.isLoaded()) { + continue; + } + BeefToolLayout.Group group = new BeefToolLayout.Group(kind.defaultName()); + for (Definition definition : DEFINITIONS) { + if (definition.group() == kind && definition.availability() != Availability.REMOVED) { + group.modules().add(definition.id()); + } + } + page.groups().add(group); + } + pages.add(page); + return new BeefToolLayout(0, pages, List.of()); + } + + /** Adds newly available modules to their default group without disturbing any existing order. */ + public static void addMissingAvailableModules(BeefToolLayout layout, ItemStack target) { + for (String moduleId : AUTO_COMBAT_MODULES) { + if (!isAvailable(moduleId, target)) { + continue; + } + + boolean inUnassigned = layout.unassignedModules().contains(moduleId); + if (!inUnassigned && layout.containsModule(moduleId)) { + continue; + } + + BeefToolLayout.Group combat = findOrCreateCombatGroup(layout); + if (combat == null || combat.modules().size() >= BeefToolLayout.MAX_MODULES_PER_GROUP) { + continue; + } + + if (inUnassigned) { + layout.unassignedModules().remove(moduleId); + } + combat.modules().add(moduleId); + } + + addMissingModules(layout, target, AUTO_AUXILIARY_MODULES, GroupKind.AUXILIARY); + addMissingModules(layout, target, AUTO_VANILLA_MODULES, GroupKind.VANILLA); + addMissingModules(layout, target, AUTO_MINING_MODULES, GroupKind.MINING); + addMissingModules(layout, target, AUTO_EXDEORUM_MODULES, GroupKind.EXDEORUM); + } + + /** + * 把布局中尚不存在的模块补进对应类别的分组;分组无法创建或已满时放入未分配区, + * 玩家可以在模式配置界面里自行拖拽。 + */ + private static void addMissingModules(BeefToolLayout layout, ItemStack target, + List moduleIds, GroupKind kind) { + for (String moduleId : moduleIds) { + if (!isAvailable(moduleId, target) || layout.containsModule(moduleId)) { + continue; + } + + BeefToolLayout.Group group = findOrCreateGroup(layout, kind); + if (group != null && group.modules().size() < BeefToolLayout.MAX_MODULES_PER_GROUP) { + group.modules().add(moduleId); + } else if (layout.unassignedModules().size() < BeefToolLayout.MAX_TOTAL_MODULES) { + layout.unassignedModules().add(moduleId); + } + } + } + + /** + * 找到可以接收某一类模块、并且还有余量的分组。 + * + *

判定分两轮:先按默认分组名匹配,使分组归属不因其中模块构成变化而漂移; + * 名称都不匹配时,再按分组内已有模块的类别推断,以兼容被玩家改名的分组。 + * 第二轮会跳过使用其它类别默认名的分组,否则模块被重新归类后, + * 承载它的旧分组会被误判为仍属于新类别。 + */ + private static BeefToolLayout.Group findGroupContainingKind(BeefToolLayout layout, GroupKind kind) { + for (BeefToolLayout.Page page : layout.pages()) { + for (BeefToolLayout.Group group : page.groups()) { + if (group.modules().size() >= BeefToolLayout.MAX_MODULES_PER_GROUP) continue; + if (matchesDefaultName(group.name(), kind)) { + return group; + } + } + } + + for (BeefToolLayout.Page page : layout.pages()) { + for (BeefToolLayout.Group group : page.groups()) { + if (group.modules().size() >= BeefToolLayout.MAX_MODULES_PER_GROUP) continue; + if (hasKnownDefaultName(group.name())) continue; + for (String moduleId : group.modules()) { + Definition definition = get(moduleId); + if (definition != null && definition.group() == kind) { + return group; + } + } + } + } + return null; + } + + /** 分组名是否等于该类别的默认名,或为其追加序号后的形式。 */ + private static boolean matchesDefaultName(String name, GroupKind kind) { + String defaultName = kind.defaultName(); + return defaultName.equals(name) || name.startsWith(defaultName + " "); + } + + /** + * 该分组在当前环境下是否应当隐藏。 + * + *

模组专属分组绑定前置模组。前置模组未加载时组内模块全部不可用, + * 分组会退化为一张没有任何按钮的空卡片;这类分组一旦随存档落盘 + * (加载过该模组后又被卸载),布局计算与绘制都必须跳过它。

+ */ + public static boolean isGroupHidden(BeefToolLayout.Group group) { + GroupKind kind = groupKindOf(group); + return kind != null && !kind.isLoaded(); + } + + /** + * 推断分组所属的类别。 + * + *

先按分组名匹配默认名,使归属判定不因组内模块构成变化而漂移; + * 名称与任何默认名都不符时,退化为按组内首个可识别模块的类别判定, + * 以兼容被玩家改名的分组。

+ */ + private static GroupKind groupKindOf(BeefToolLayout.Group group) { + for (GroupKind kind : GroupKind.values()) { + if (matchesDefaultName(group.name(), kind)) { + return kind; + } + } + for (String moduleId : group.modules()) { + Definition definition = get(moduleId); + if (definition != null) { + return definition.group(); + } + } + return null; + } + + /** 分组名是否已被某个类别占用为默认名。 */ + private static boolean hasKnownDefaultName(String name) { + for (GroupKind kind : GroupKind.values()) { + if (matchesDefaultName(name, kind)) { + return true; + } + } + return false; + } + + /** + * 把老存档中归在其它分组的原版工具动作迁入「原版」分组。 + * + *

补缺逻辑只处理布局中尚不存在的模块:旧版本把这些模块归在「辅助」分组, + * 它们因此不会自行出现在新增的原版分组中,需要在首次加载时显式迁移。 + */ + public static void migrateVanillaModules(BeefToolLayout layout) { + migrateModulesToGroup(layout, AUTO_VANILLA_MODULES, GroupKind.VANILLA); + } + + /** + * 把 Ex Deorum 工具模式迁入其专属分组。 + * + *

三项在旧版本中归入「挖掘」分组,且已随存档落盘,因此不能只靠补缺逻辑 + * 让它们进入新建的专属分组,需要在首次加载时显式迁移。

+ */ + public static void migrateExDeorumModules(BeefToolLayout layout) { + if (!GroupKind.EXDEORUM.isLoaded()) { + return; + } + migrateModulesToGroup(layout, AUTO_EXDEORUM_MODULES, GroupKind.EXDEORUM); + } + + /** + * 把指定类别的模块统一搬入该类别的分组。 + * + *

迁移只搬动模块本身,不改动分组顺序、分组名称与分组数量; + * 目标分组已满或无法创建时保留原有归属,不做部分迁移。 + */ + private static void migrateModulesToGroup(BeefToolLayout layout, List moduleIds, GroupKind kind) { + List pending = new ArrayList<>(); + for (String moduleId : moduleIds) { + Definition definition = get(moduleId); + if (definition == null || definition.group() != kind) continue; + if (isInGroupOfKind(layout, moduleId, kind)) continue; + pending.add(moduleId); + } + if (pending.isEmpty()) return; + + BeefToolLayout.Group target = findOrCreateGroup(layout, kind); + if (target == null) return; + if (target.modules().size() + pending.size() > BeefToolLayout.MAX_MODULES_PER_GROUP) return; + + for (String moduleId : pending) { + removeModuleFromLayout(layout, moduleId); + target.modules().add(moduleId); + } + } + + /** 模块是否已经位于名称与该类别默认名一致的分组中。 */ + private static boolean isInGroupOfKind(BeefToolLayout layout, String moduleId, GroupKind kind) { + for (BeefToolLayout.Page page : layout.pages()) { + for (BeefToolLayout.Group group : page.groups()) { + if (matchesDefaultName(group.name(), kind) && group.modules().contains(moduleId)) { + return true; + } + } + } + return false; + } + + /** 把模块从布局中的任意分组与未分配区移除。 */ + private static void removeModuleFromLayout(BeefToolLayout layout, String moduleId) { + for (BeefToolLayout.Page page : layout.pages()) { + for (BeefToolLayout.Group group : page.groups()) { + group.modules().remove(moduleId); + } + } + layout.unassignedModules().remove(moduleId); + } + + /** + * 取得可以接收指定类别模块的分组:优先复用已承载该类别的分组, + * 不存在时新建一个以默认名命名的分组。 + * + *

既有存档由旧版本写入,其中不存在「原版」这类新增分组, + * 若仅查找复用会把这些模块推进未分配区,因此需要在此补建。 + */ + private static BeefToolLayout.Group findOrCreateGroup(BeefToolLayout layout, GroupKind kind) { + BeefToolLayout.Group existing = findGroupContainingKind(layout, kind); + if (existing != null) { + return existing; + } + return createGroup(layout, kind); + } + + private static BeefToolLayout.Group findOrCreateCombatGroup(BeefToolLayout layout) { + BeefToolLayout.Group combat = findNamedCombatGroup(layout); + if (combat != null) return combat; + + combat = findRenamedCombatGroup(layout); + if (combat != null) return combat; + + return createGroup(layout, GroupKind.COMBAT); + } + + private static BeefToolLayout.Group findNamedCombatGroup(BeefToolLayout layout) { + for (BeefToolLayout.Page page : layout.pages()) { + for (BeefToolLayout.Group group : page.groups()) { + if (GroupKind.COMBAT.defaultName().equals(group.name()) + && group.modules().size() < BeefToolLayout.MAX_MODULES_PER_GROUP) { + return group; + } + } + } + return null; + } + + private static BeefToolLayout.Group findRenamedCombatGroup(BeefToolLayout layout) { + for (BeefToolLayout.Page page : layout.pages()) { + for (BeefToolLayout.Group group : page.groups()) { + if (group.modules().size() >= BeefToolLayout.MAX_MODULES_PER_GROUP) continue; + for (String moduleId : group.modules()) { + Definition definition = get(moduleId); + if (definition != null && definition.group() == GroupKind.COMBAT + && !AUTO_COMBAT_MODULES.contains(moduleId)) { + return group; + } + } + } + } + return null; + } + + /** + * 新建一个指定类别的分组:优先放入尚未占满的既有页面,全部占满时另开新页; + * 默认名已被占用时追加序号,避免同页出现重名分组。 + * + *

「原版」分组插入到同页战斗分组之后,使它在界面上的位置紧随战斗分组, + * 而非按创建顺序落到页面末尾。 + */ + private static BeefToolLayout.Group createGroup(BeefToolLayout layout, GroupKind kind) { + BeefToolLayout.Page page = layout.pages().stream() + .filter(candidate -> candidate.groups().size() < BeefToolLayout.MAX_GROUPS_PER_PAGE) + .findFirst() + .orElse(null); + + if (page == null) { + if (layout.pages().size() >= BeefToolLayout.MAX_PAGES) return null; + page = new BeefToolLayout.Page("Page " + (layout.pages().size() + 1)); + layout.pages().add(page); + } + + String name = kind.defaultName(); + int suffix = 2; + while (hasGroupName(layout, name)) { + name = kind.defaultName() + " " + suffix++; + } + + BeefToolLayout.Group group = new BeefToolLayout.Group(name); + page.groups().add(insertionIndex(page, kind), group); + return group; + } + + /** 新建分组在同页中的插入位置:原版分组紧随战斗分组,其余类别追加到末尾。 */ + private static int insertionIndex(BeefToolLayout.Page page, GroupKind kind) { + if (kind != GroupKind.VANILLA) { + return page.groups().size(); + } + for (int i = 0; i < page.groups().size(); i++) { + if (matchesDefaultName(page.groups().get(i).name(), GroupKind.COMBAT)) { + return i + 1; + } + } + return page.groups().size(); + } + + private static boolean hasGroupName(BeefToolLayout layout, String name) { + for (BeefToolLayout.Page page : layout.pages()) { + for (BeefToolLayout.Group group : page.groups()) { + if (name.equals(group.name())) return true; + } + } + return false; + } + + /** + * 分组类别。枚举声明顺序即 {@link #defaultLayout()} 生成的分组顺序, + * 因此同时决定模式配置界面中分组的默认排布。 + */ + public enum GroupKind { + TOOLS("Tools", null), + MINING("Mining", null), + COMBAT("Combat", null), + VANILLA("Vanilla", null), + AUXILIARY("Auxiliary", null), + /** + * Ex Deorum 专属分组,承载其三项工具模式。 + * + *

该分组的模块全部来自 exdeorum,因此绑定 {@code exdeorum} 作为前置模组: + * 未加载时默认布局不生成该分组,避免出现一个没有任何按钮的空分组。

+ */ + EXDEORUM("Ex Deorum", "exdeorum"); + + private final String defaultName; + private final String requiredMod; + + GroupKind(String defaultName, String requiredMod) { + this.defaultName = defaultName; + this.requiredMod = requiredMod; + } + + public String defaultName() { + return defaultName; + } + + /** 分组的前置模组是否已加载;不绑定前置模组的类别恒为 {@code true}。 */ + public boolean isLoaded() { + return requiredMod == null || ModList.get().isLoaded(requiredMod); + } + } + + public record Definition(String id, Component name, GroupKind group, + Availability availability, boolean exclusive) { + } + + public enum Availability { + ALWAYS { + @Override + boolean isAvailable(ItemStack target) { + return target != null && !target.isEmpty(); + } + }, + ENDLESS { + @Override + boolean isAvailable(ItemStack target) { + return target != null && target.getItem() instanceof EndlessBeafItem; + } + }, + BASE { + @Override + boolean isAvailable(ItemStack target) { + return target != null && BeefToolVariants.isBaseVariant(target); + } + }, + AE2 { + @Override + boolean isAvailable(ItemStack target) { + return ModList.get().isLoaded("ae2") && target != null && !target.isEmpty(); + } + }, + EXDEORUM { + @Override + boolean isAvailable(ItemStack target) { + // 三项 Ex Deorum 工具模式均依赖其配方体系,缺该模组时不在轮盘中出现 + return ModList.get().isLoaded("exdeorum") && target != null + && target.getItem() instanceof EndlessBeafItem; + } + }, + MALUM { + @Override + boolean isAvailable(ItemStack target) { + return ModList.get().isLoaded("malum") && target != null + && target.getItem() instanceof EndlessBeafItem; + } + }, + OCCULTISM { + @Override + boolean isAvailable(ItemStack target) { + // 匠心仪式挎包依赖 occultism 与 modonomicon 的预览 API,两者由 occultism 传递引入 + return ModList.get().isLoaded("occultism") && target != null + && target.getItem() instanceof EndlessBeafItem; + } + }, + MYSTICAL_AGRICULTURE { + @Override + boolean isAvailable(ItemStack target) { + return ModList.get().isLoaded("mysticalagriculture") && target != null + && target.getItem() instanceof EndlessBeafItem; + } + }, + REMOVED { + @Override + boolean isAvailable(ItemStack target) { + return false; + } + }, + OMNITOOLS { + @Override + boolean isAvailable(ItemStack target) { + return ModList.get().isLoaded("omnitools") && target != null && !target.isEmpty(); + } + }; + + abstract boolean isAvailable(ItemStack target); + } +} diff --git a/src/main/java/com/sorrowmist/useless/data/PlayerMiningData.java b/src/main/java/com/sorrowmist/useless/data/PlayerMiningData.java new file mode 100644 index 00000000..132f3347 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/data/PlayerMiningData.java @@ -0,0 +1,96 @@ +package com.sorrowmist.useless.data; + +import com.sorrowmist.useless.utils.mining.shape.ChainMiningShapes; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.state.BlockState; + +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +public class PlayerMiningData { + private final UUID playerId; + private boolean tabPressed = false; + /** 当前连锁形状。属于玩家选择而非扫描结果,因此不随缓存清理而重置。 */ + private ChainMiningShapes shape = ChainMiningShapes.SHAPELESS; + private BlockPos cachedPos = null; + private BlockState cachedState = null; + private List cachedBlocks = Collections.emptyList(); + /** + * 本次连锁的方块数量上限。 + * + *

该值来自服务端配置,客户端无法自行读取,因此随同步包下发;仅用于界面显示, + * 不参与任何判定,取值 0 表示尚未同步。 + */ + private int maxBlocks = 0; + + public PlayerMiningData(UUID playerId) { + this.playerId = playerId; + } + + public UUID getPlayerId() { + return this.playerId; + } + + public boolean isTabPressed() { + return this.tabPressed; + } + + public void setTabPressed(boolean tabPressed) { + this.tabPressed = tabPressed; + } + + public ChainMiningShapes getShape() { + return this.shape; + } + + public void setShape(ChainMiningShapes shape) { + this.shape = shape != null ? shape : ChainMiningShapes.SHAPELESS; + } + + public int getMaxBlocks() { + return this.maxBlocks; + } + + public void setMaxBlocks(int maxBlocks) { + this.maxBlocks = maxBlocks; + } + + public boolean isCacheValid(BlockPos currentPos) { + return this.cachedPos != null && this.cachedPos.equals(currentPos) && this.hasCachedBlocks(); + } + + public BlockPos getCachedPos() { + return this.cachedPos; + } + + public void setCachedPos(BlockPos cachedPos) { + this.cachedPos = cachedPos; + } + + public BlockState getCachedState() { + return this.cachedState; + } + + public void setCachedState(BlockState cachedState) { + this.cachedState = cachedState; + } + + public List getCachedBlocks() { + return this.cachedBlocks; + } + + public void setCachedBlocks(List cachedBlocks) { + this.cachedBlocks = cachedBlocks != null ? cachedBlocks : Collections.emptyList(); + } + + public boolean hasCachedBlocks() { + return !this.cachedBlocks.isEmpty(); + } + + public void clearCache() { + this.cachedPos = null; + this.cachedState = null; + this.cachedBlocks = Collections.emptyList(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/datagen/DataGenerators.java b/src/main/java/com/sorrowmist/useless/datagen/DataGenerators.java new file mode 100644 index 00000000..177839a7 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/datagen/DataGenerators.java @@ -0,0 +1,33 @@ +package com.sorrowmist.useless.datagen; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.datagen.providers.ULootTableProvider; +import com.sorrowmist.useless.datagen.providers.recipes.CraftingRecipes; +import com.sorrowmist.useless.datagen.providers.tags.UBlockTagsProvider; +import com.sorrowmist.useless.datagen.providers.tags.UItemTagsProvider; +import net.minecraft.data.DataGenerator; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod.EventBusSubscriber; +import net.minecraftforge.data.event.GatherDataEvent; + +@EventBusSubscriber(modid = UselessMod.MODID) +public class DataGenerators { + + @SubscribeEvent + public static void onGatherData(GatherDataEvent event) { + DataGenerator generator = event.getGenerator(); + var registries = event.getLookupProvider(); + var pack = generator.getVanillaPack(true); + var existingFileHelper = event.getExistingFileHelper(); + + var blockTagsProvider = pack.addProvider( + output -> new UBlockTagsProvider(output, registries, existingFileHelper)); + pack.addProvider(output -> new UItemTagsProvider(output, + registries, + blockTagsProvider.contentsGetter() + )); + pack.addProvider(output -> new ULootTableProvider(output, registries)); +// pack.addProvider(output -> new ModBiomeProvider(output, registries)); + pack.addProvider(output -> new CraftingRecipes(output, registries)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/datagen/providers/ULootTableProvider.java b/src/main/java/com/sorrowmist/useless/datagen/providers/ULootTableProvider.java new file mode 100644 index 00000000..46235d3f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/datagen/providers/ULootTableProvider.java @@ -0,0 +1,106 @@ +package com.sorrowmist.useless.datagen.providers; + +import com.sorrowmist.useless.content.blocks.GlowPlasticBlock; +import com.sorrowmist.useless.init.ModBlocks; +import net.minecraft.core.HolderLookup; +import net.minecraft.data.PackOutput; +import net.minecraft.data.loot.BlockLootSubProvider; +import net.minecraft.data.loot.LootTableProvider; +import net.minecraft.world.flag.FeatureFlags; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; + +public class ULootTableProvider extends LootTableProvider { + + private static final List SUB_PROVIDERS = List.of( + new SubProviderEntry(BlockLootProvider::new, LootContextParamSets.BLOCK) + ); + + public ULootTableProvider(PackOutput output, CompletableFuture registries) { + super(output, Set.of(), SUB_PROVIDERS, registries); + } + + private static class BlockLootProvider extends BlockLootSubProvider { + + BlockLootProvider(HolderLookup.Provider registries) { + super(Set.of(), FeatureFlags.REGISTRY.allFlags(), registries); + } + + @Override + protected void generate() { + this.dropSelf(ModBlocks.ORE_GENERATOR_BLOCK.get()); + this.dropSelf(ModBlocks.TELEPORT_BLOCK.get()); + this.dropSelf(ModBlocks.TELEPORT_BLOCK_2.get()); + this.dropSelf(ModBlocks.TELEPORT_BLOCK_3.get()); + this.dropSelf(ModBlocks.ADVANCED_ALLOY_FURNACE_BLOCK.get()); + this.dropSelf(ModBlocks.SUPERVISOR.get()); + this.dropSelf(ModBlocks.MULTIBLOCK_ALLOY_FURNACE_CORE.get()); + this.dropSelf(ModBlocks.ME_PATTERN_ASSEMBLY.get()); + this.dropSelf(ModBlocks.OMNIVERSAL_MOLD_HUB.get()); + this.dropSelf(ModBlocks.PASSIVE_CRAFTING_HATCH.get()); + this.dropSelf(ModBlocks.OMNIVERSAL_FURNACE_CASING.get()); + this.dropSelf(ModBlocks.TEMPERATURE_REGULATOR.get()); + + for (var coil : ModBlocks.USELESS_COILS.values()) { + this.dropSelf(coil.get()); + } + + for (var blockMap : GlowPlasticBlock.ALL_BLOCK_MAPS) { + for (var block : blockMap.values()) { + this.dropSelf(block.get()); + } + } + + for (var entry : ModBlocks.USELESS_GLASS_BLOCKS.entrySet()) { + this.dropSelf(entry.getValue().get()); + } + + // ECO 已安装时才有「无用型紧凑 C9/F9/L9」 + if (net.minecraftforge.fml.ModList.get().isLoaded("neoecoae")) { + this.dropSelf(com.sorrowmist.useless.compat.neoecoae.compact.NeoEcoCompactRegistry + .COMPACT_C9.get()); + this.dropSelf(com.sorrowmist.useless.compat.neoecoae.compact.NeoEcoCompactRegistry + .COMPACT_F9.get()); + this.dropSelf(com.sorrowmist.useless.compat.neoecoae.compact.NeoEcoCompactRegistry + .COMPACT_L9.get()); + } + } + + @Override + protected @NotNull Iterable getKnownBlocks() { + Stream.Builder blocks = Stream.builder(); + for (var blockMap : GlowPlasticBlock.ALL_BLOCK_MAPS) { + blockMap.values().forEach(block -> blocks.add(block.get())); + } + ModBlocks.USELESS_GLASS_BLOCKS.values().forEach(block -> blocks.add(block.get())); + ModBlocks.USELESS_COILS.values().forEach(block -> blocks.add(block.get())); + blocks.add(ModBlocks.ORE_GENERATOR_BLOCK.get()); + blocks.add(ModBlocks.TELEPORT_BLOCK.get()); + blocks.add(ModBlocks.TELEPORT_BLOCK_2.get()); + blocks.add(ModBlocks.TELEPORT_BLOCK_3.get()); + blocks.add(ModBlocks.ADVANCED_ALLOY_FURNACE_BLOCK.get()); + blocks.add(ModBlocks.SUPERVISOR.get()); + blocks.add(ModBlocks.MULTIBLOCK_ALLOY_FURNACE_CORE.get()); + blocks.add(ModBlocks.ME_PATTERN_ASSEMBLY.get()); + blocks.add(ModBlocks.OMNIVERSAL_MOLD_HUB.get()); + blocks.add(ModBlocks.PASSIVE_CRAFTING_HATCH.get()); + blocks.add(ModBlocks.OMNIVERSAL_FURNACE_CASING.get()); + blocks.add(ModBlocks.TEMPERATURE_REGULATOR.get()); + if (net.minecraftforge.fml.ModList.get().isLoaded("neoecoae")) { + blocks.add(com.sorrowmist.useless.compat.neoecoae.compact.NeoEcoCompactRegistry + .COMPACT_C9.get()); + blocks.add(com.sorrowmist.useless.compat.neoecoae.compact.NeoEcoCompactRegistry + .COMPACT_F9.get()); + blocks.add(com.sorrowmist.useless.compat.neoecoae.compact.NeoEcoCompactRegistry + .COMPACT_L9.get()); + } + return blocks.build().toList(); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/datagen/providers/recipes/CraftingRecipes.java b/src/main/java/com/sorrowmist/useless/datagen/providers/recipes/CraftingRecipes.java new file mode 100644 index 00000000..d3f50e8e --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/datagen/providers/recipes/CraftingRecipes.java @@ -0,0 +1,637 @@ +package com.sorrowmist.useless.datagen.providers.recipes; + +import appeng.core.definitions.AEBlocks; +import appeng.core.definitions.AEItems; +import appeng.core.definitions.AEParts; +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.api.enums.AlloyFurnaceMode; +import com.sorrowmist.useless.api.enums.EnumColor; +import com.sorrowmist.useless.content.blocks.GlowPlasticBlock; +import com.sorrowmist.useless.content.blocks.multiblock.UselessCoilBlock; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipeBuilder; +import com.sorrowmist.useless.init.ModBlocks; +import com.sorrowmist.useless.init.ModItems; +import com.sorrowmist.useless.init.ModTags; +import net.minecraft.core.HolderLookup; +import net.minecraft.data.PackOutput; +import net.minecraft.data.recipes.RecipeCategory; +import net.minecraft.data.recipes.RecipeOutput; +import net.minecraft.data.recipes.RecipeProvider; +import net.minecraft.data.recipes.ShapedRecipeBuilder; +import net.minecraft.data.recipes.ShapelessRecipeBuilder; +import net.minecraft.tags.ItemTags; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CompletableFuture; + +public class CraftingRecipes extends RecipeProvider { + public CraftingRecipes(PackOutput output, CompletableFuture registries) { + super(output, registries); + } + + @Override + protected void buildRecipes(@NotNull RecipeOutput consumer) { + this.addMoldRecipes(consumer); + this.addAdvancedAlloyFurnaceBlockRecipe(consumer); + this.addOmniversalFurnaceRecipes(consumer); + this.addEndlessBeafItemRecipe(consumer); + this.addOreGeneratorBlockRecipe(consumer); + this.addTeleportBlockRecipes(consumer); + this.addSupervisorRecipe(consumer); + this.addAE2GiftPackageRecipe(consumer); + this.addPatternConverterRecipe(consumer); + this.addPlasticRecipes(consumer); + this.addTemperatureRegulatorRecipe(consumer); + this.addAdvancedAlloyFurnaceRecipes(consumer); + } + + /** 任意塑料方块(1 个)→ 1 个温度调节器。 */ + private void addTemperatureRegulatorRecipe(RecipeOutput consumer) { + ShapelessRecipeBuilder.shapeless(RecipeCategory.MISC, + ModItems.TEMPERATURE_REGULATOR.get(), 1) + .requires(ModTags.PLASTIC_BLOCKS_ITEM) + .unlockedBy("has_plastic", has(ModTags.PLASTIC_BLOCKS_ITEM)) + .save(consumer, UselessMod.id("crafting/temperature_regulator")); + } + + private void addPlasticRecipes(RecipeOutput consumer) { + for (EnumColor color : EnumColor.valuesInOrder()) { + Block concrete = getConcreteBlock(color); + String prefix = color.getRegistryPrefix(); + + ShapedRecipeBuilder.shaped(RecipeCategory.BUILDING_BLOCKS, + GlowPlasticBlock.PLASTIC_BLOCK_ITEMS.get(color).get(), 8) + .pattern("CCC") + .pattern("C C") + .pattern("CCC") + .define('C', concrete) + .unlockedBy("has_" + prefix + "_concrete", has(concrete)) + .save(consumer, UselessMod.id("crafting/" + prefix + "_plastic")); + + ShapedRecipeBuilder.shaped(RecipeCategory.BUILDING_BLOCKS, + GlowPlasticBlock.GLOW_PLASTIC_BLOCK_ITEMS.get(color).get(), 8) + .pattern("CCC") + .pattern("CGC") + .pattern("CCC") + .define('C', concrete) + .define('G', Items.GLOWSTONE_DUST) + .unlockedBy("has_" + prefix + "_concrete", has(concrete)) + .save(consumer, UselessMod.id("crafting/" + prefix + "_glow_plastic")); + + addPlasticConversionRecipe(consumer, + GlowPlasticBlock.PLASTIC_CTM_BLOCK_ITEMS.get(color).get(), + GlowPlasticBlock.PLASTIC_BLOCK_ITEMS.get(color).get(), + prefix + "_plastic_ctm"); + addPlasticConversionRecipe(consumer, + GlowPlasticBlock.GLOW_PLASTIC_CTM_BLOCK_ITEMS.get(color).get(), + GlowPlasticBlock.GLOW_PLASTIC_BLOCK_ITEMS.get(color).get(), + prefix + "_glow_plastic_ctm"); + } + } + + private static void addPlasticConversionRecipe( + RecipeOutput consumer, Item output, Item ingredient, String recipeName) { + ShapedRecipeBuilder.shaped(RecipeCategory.BUILDING_BLOCKS, output, 8) + .pattern("PPP") + .pattern("P P") + .pattern("PPP") + .define('P', ingredient) + .unlockedBy("has_" + recipeName.replace("_ctm", ""), has(ingredient)) + .save(consumer, UselessMod.id("crafting/" + recipeName)); + } + + private static Block getConcreteBlock(EnumColor color) { + return switch (color) { + case BLACK -> Blocks.BLACK_CONCRETE; + case RED -> Blocks.RED_CONCRETE; + case DARK_RED -> Blocks.RED_CONCRETE_POWDER; + case GREEN -> Blocks.GREEN_CONCRETE; + case BROWN -> Blocks.BROWN_CONCRETE; + case BLUE -> Blocks.BLUE_CONCRETE; + case PURPLE -> Blocks.PURPLE_CONCRETE; + case CYAN -> Blocks.CYAN_CONCRETE; + case AQUA -> Blocks.CYAN_CONCRETE_POWDER; + case LIGHT_GRAY -> Blocks.LIGHT_GRAY_CONCRETE; + case GRAY -> Blocks.GRAY_CONCRETE; + case PINK -> Blocks.PINK_CONCRETE; + case LIME -> Blocks.LIME_CONCRETE; + case YELLOW -> Blocks.YELLOW_CONCRETE; + case LIGHT_BLUE -> Blocks.LIGHT_BLUE_CONCRETE; + case MAGENTA -> Blocks.MAGENTA_CONCRETE; + case ORANGE -> Blocks.ORANGE_CONCRETE; + case WHITE -> Blocks.WHITE_CONCRETE; + }; + } + + private void addMoldRecipes(RecipeOutput consumer) { + ShapedRecipeBuilder.shaped(RecipeCategory.MISC, ModItems.METAL_MOLD_PLATE.get(), 1) + .pattern("XXX") + .define('X', ModItems.USELESS_INGOT_TIER_1.get()) + .unlockedBy("has_ingot", has(ModItems.USELESS_INGOT_TIER_1.get())) + .save(consumer, UselessMod.id("mold/metal_mold_plate")); + + ShapedRecipeBuilder.shaped(RecipeCategory.MISC, ModItems.METAL_MOLD_ROD.get(), 1) + .pattern(" X") + .pattern(" X ") + .pattern("X ") + .define('X', ModItems.USELESS_INGOT_TIER_1.get()) + .unlockedBy("has_ingot", has(ModItems.USELESS_INGOT_TIER_1.get())) + .save(consumer, UselessMod.id("mold/metal_mold_rod")); + + ShapedRecipeBuilder.shaped(RecipeCategory.MISC, ModItems.METAL_MOLD_GEAR.get(), 1) + .pattern(" X ") + .pattern("X X") + .pattern(" X ") + .define('X', ModItems.USELESS_INGOT_TIER_1.get()) + .unlockedBy("has_ingot", has(ModItems.USELESS_INGOT_TIER_1.get())) + .save(consumer, UselessMod.id("mold/metal_mold_gear")); + + ShapedRecipeBuilder.shaped(RecipeCategory.MISC, ModItems.METAL_MOLD_WIRE.get(), 1) + .pattern(" X ") + .pattern("XXX") + .pattern(" X ") + .define('X', ModItems.USELESS_INGOT_TIER_1.get()) + .unlockedBy("has_ingot", has(ModItems.USELESS_INGOT_TIER_1.get())) + .save(consumer, UselessMod.id("mold/metal_mold_wire")); + + ShapedRecipeBuilder.shaped(RecipeCategory.MISC, ModItems.METAL_MOLD_BLOCK.get(), 1) + .pattern("XXX") + .pattern("X X") + .pattern("XXX") + .define('X', ModItems.USELESS_INGOT_TIER_1.get()) + .unlockedBy("has_ingot", has(ModItems.USELESS_INGOT_TIER_1.get())) + .save(consumer, UselessMod.id("mold/metal_mold_block")); + } + + private void addAdvancedAlloyFurnaceBlockRecipe(RecipeOutput consumer) { + ShapedRecipeBuilder.shaped(RecipeCategory.BUILDING_BLOCKS, + ModBlocks.ADVANCED_ALLOY_FURNACE_BLOCK.get(), + 1 + ) + .pattern("ABC") + .pattern("DEF") + .pattern("GHI") + .define('A', Blocks.EMERALD_BLOCK) + .define('B', Blocks.DIAMOND_BLOCK) + .define('C', Blocks.LAPIS_BLOCK) + .define('D', Blocks.IRON_BLOCK) + .define('E', Blocks.FURNACE) + .define('F', Blocks.GOLD_BLOCK) + .define('G', Items.WATER_BUCKET) + .define('H', Blocks.NETHERITE_BLOCK) + .define('I', Items.LAVA_BUCKET) + .unlockedBy("has_furnace", has(Blocks.FURNACE)) + .save(consumer); + } + + private void addOmniversalFurnaceRecipes(RecipeOutput consumer) { + ShapedRecipeBuilder.shaped(RecipeCategory.BUILDING_BLOCKS, + ModBlocks.MULTIBLOCK_ALLOY_FURNACE_CORE.get()) + .pattern("IRI") + .pattern("RFR") + .pattern("IRI") + .define('I', Items.IRON_INGOT) + .define('R', Items.REDSTONE) + .define('F', ModBlocks.ADVANCED_ALLOY_FURNACE_BLOCK.get()) + .unlockedBy("has_alloy_furnace", has(ModBlocks.ADVANCED_ALLOY_FURNACE_BLOCK.get())) + .save(consumer, UselessMod.id("crafting/multiblock_alloy_furnace_core")); + + ShapedRecipeBuilder.shaped(RecipeCategory.BUILDING_BLOCKS, ModBlocks.ME_PATTERN_ASSEMBLY.get()) + .pattern(" R ") + .pattern("CPC") + .pattern(" R ") + .define('R', Items.REDSTONE) + .define('C', Blocks.CHEST) + .define('P', AEBlocks.PATTERN_PROVIDER) + .unlockedBy("has_pattern_provider", has(AEBlocks.PATTERN_PROVIDER)) + .save(consumer, UselessMod.id("crafting/me_pattern_assembly")); + + ShapedRecipeBuilder.shaped(RecipeCategory.BUILDING_BLOCKS, ModBlocks.OMNIVERSAL_MOLD_HUB.get()) + .pattern(" R ") + .pattern("CHR") + .pattern(" R ") + .define('R', Items.REDSTONE) + .define('C', Blocks.CHEST) + .define('H', Blocks.HOPPER) + .unlockedBy("has_hopper", has(Blocks.HOPPER)) + .save(consumer, UselessMod.id("crafting/omniversal_mold_hub")); + + ShapedRecipeBuilder.shaped(RecipeCategory.BUILDING_BLOCKS, + ModBlocks.PASSIVE_CRAFTING_HATCH.get()) + .pattern(" C ") + .pattern("EPE") + .pattern(" R ") + .define('C', Items.CLOCK) + .define('E', AEParts.EXPORT_BUS) + .define('P', ModBlocks.ME_PATTERN_ASSEMBLY.get()) + .define('R', Items.REDSTONE) + .unlockedBy("has_pattern_assembly", has(ModBlocks.ME_PATTERN_ASSEMBLY.get())) + .save(consumer, UselessMod.id("crafting/passive_crafting_hatch")); + + ShapedRecipeBuilder.shaped(RecipeCategory.BUILDING_BLOCKS, + ModBlocks.OMNIVERSAL_FURNACE_CASING.get(), 8) + .pattern("NNN") + .pattern("NSN") + .pattern("NNN") + .define('N', Items.IRON_NUGGET) + .define('S', Blocks.SMOOTH_STONE) + .unlockedBy("has_smooth_stone", has(Blocks.SMOOTH_STONE)) + .save(consumer, UselessMod.id("crafting/omniversal_furnace_casing")); + + for (int tier = UselessCoilBlock.MIN_TIER; tier <= UselessCoilBlock.MAX_TIER; tier++) { + var ingot = switch (tier) { + case 1 -> ModItems.USELESS_INGOT_TIER_1.get(); + case 2 -> ModItems.USELESS_INGOT_TIER_2.get(); + case 3 -> ModItems.USELESS_INGOT_TIER_3.get(); + case 4 -> ModItems.USELESS_INGOT_TIER_4.get(); + case 5 -> ModItems.USELESS_INGOT_TIER_5.get(); + case 6 -> ModItems.USELESS_INGOT_TIER_6.get(); + case 7 -> ModItems.USELESS_INGOT_TIER_7.get(); + case 8 -> ModItems.USELESS_INGOT_TIER_8.get(); + case 9 -> ModItems.USELESS_INGOT_TIER_9.get(); + case UselessCoilBlock.USEFUL_TIER -> ModItems.USEFUL_INGOT.get(); + default -> throw new IllegalStateException("Unexpected coil tier: " + tier); + }; + ShapedRecipeBuilder.shaped(RecipeCategory.BUILDING_BLOCKS, + ModBlocks.USELESS_COILS.get(tier).get(), 16) + .pattern("CRC") + .pattern("RIR") + .pattern("CRC") + .define('C', Items.COPPER_INGOT) + .define('R', Items.REDSTONE) + .define('I', ingot) + .unlockedBy("has_tier_ingot", has(ingot)) + .save(consumer, UselessMod.id( + "crafting/" + UselessCoilBlock.registryName(tier))); + } + + } + + private void addAE2GiftPackageRecipe(RecipeOutput consumer) { + if (ModItems.AE2_GIFT_PACKAGE == null) { + return; + } + + ShapedRecipeBuilder.shaped(RecipeCategory.MISC, ModItems.AE2_GIFT_PACKAGE.get(), 1) + .pattern("SPS") + .pattern("PPP") + .pattern("SPS") + .define('P', ItemTags.PLANKS) + .define('S', Items.STICK) + .unlockedBy("has_planks", has(ItemTags.PLANKS)) + .save(consumer, UselessMod.id("crafting/ae2_gift_package")); + } + + private void addPatternConverterRecipe(RecipeOutput consumer) { + ShapedRecipeBuilder.shaped(RecipeCategory.MISC, ModItems.OMNIVERSAL_PATTERN_CONVERTER.get()) + .pattern("GPG") + .pattern(" L ") + .define('G', Items.YELLOW_DYE) + .define('P', AEItems.BLANK_PATTERN) + .define('L', AEItems.LOGIC_PROCESSOR) + .unlockedBy("has_blank_pattern", has(AEItems.BLANK_PATTERN)) + .save(consumer, UselessMod.id("crafting/omniversal_pattern_converter")); + } + + private void addSupervisorRecipe(RecipeOutput consumer) { + ShapelessRecipeBuilder.shapeless(RecipeCategory.BUILDING_BLOCKS, ModItems.SUPERVISOR.get()) + .requires(ModItems.USEFUL_INGOT.get()) + .requires(Items.PINK_WOOL) + .requires(Items.BLACK_WOOL) + .requires(Items.GRAY_WOOL) + .requires(Items.WHITE_WOOL) + .requires(Items.GREEN_WOOL) + .requires(Items.BLUE_WOOL) + .requires(Items.YELLOW_WOOL) + .requires(Items.ORANGE_WOOL) + .unlockedBy("has_useful_ingot", has(ModItems.USEFUL_INGOT.get())) + .save(consumer, UselessMod.id("crafting/supervisor")); + } + + private void addAdvancedAlloyFurnaceRecipes(RecipeOutput output) { + this.addUselessIngotRecipes(output); + this.addUselessGearRecipes(output); + this.addUselessGlassRecipes(output); + this.addPossibleUsefulIngotRecipe(output); + } + + private void addPossibleUsefulIngotRecipe(RecipeOutput output) { + // Possible Useful Ingot - 工作台合成配方(9个不同等级的无用锭) + ShapedRecipeBuilder.shaped(RecipeCategory.MISC, ModItems.POSSIBLE_USEFUL_INGOT.get(), 1) + .pattern("123") + .pattern("456") + .pattern("789") + .define('1', ModItems.USELESS_INGOT_TIER_1.get()) + .define('2', ModItems.USELESS_INGOT_TIER_2.get()) + .define('3', ModItems.USELESS_INGOT_TIER_3.get()) + .define('4', ModItems.USELESS_INGOT_TIER_4.get()) + .define('5', ModItems.USELESS_INGOT_TIER_5.get()) + .define('6', ModItems.USELESS_INGOT_TIER_6.get()) + .define('7', ModItems.USELESS_INGOT_TIER_7.get()) + .define('8', ModItems.USELESS_INGOT_TIER_8.get()) + .define('9', ModItems.USELESS_INGOT_TIER_9.get()) + .unlockedBy("has_ingot", has(ModItems.USELESS_INGOT_TIER_1.get())) + .save(output, UselessMod.id("crafting/possible_useful_ingot")); + } + + private void addUselessIngotRecipes(RecipeOutput output) { + // Tier 1 - 基础合成 + AdvancedAlloyFurnaceRecipeBuilder.create() + .input(Ingredient.of(Items.MUTTON), 2) + .input(Ingredient.of(Items.BEEF), 2) + .input(Ingredient.of(Items.CHICKEN), 2) + .input(Ingredient.of(Items.IRON_INGOT), 2) + .input(Ingredient.of(Items.GOLD_INGOT), 2) + .input(Ingredient.of(Items.DIAMOND), 2) + .output(ModItems.USELESS_INGOT_TIER_1.get(), 1) + .energy(10000) + .processTime(1) + .mode(AlloyFurnaceMode.NORMAL) + .save(output, UselessMod.id("advanced_alloy/ingot/useless_ingot_tier_1")); + + // Tier 2 - 使用1阶无用锭作为输入材料 + AdvancedAlloyFurnaceRecipeBuilder.create() + .input(Ingredient.of(Items.ROTTEN_FLESH), 2) + .input(Ingredient.of(Items.GUNPOWDER), 2) + .input(Ingredient.of(Items.BONE), 2) + .input(Ingredient.of(Items.GOLD_INGOT), 2) + .input(Ingredient.of(Items.DIAMOND), 2) + .input(Ingredient.of(ModItems.USELESS_INGOT_TIER_1.get()), 1) + .output(ModItems.USELESS_INGOT_TIER_2.get(), 1) + .energy(50000) + .processTime(1) + .mode(AlloyFurnaceMode.NORMAL) + .save(output, UselessMod.id("advanced_alloy/ingot/useless_ingot_tier_2")); + + // Tier 3 - 使用2阶无用锭作为输入材料 + AdvancedAlloyFurnaceRecipeBuilder.create() + .input(Ingredient.of(Items.NETHER_WART), 2) + .input(Ingredient.of(Items.BLAZE_ROD), 2) + .input(Ingredient.of(Items.NETHER_BRICK), 2) + .input(Ingredient.of(Items.GOLD_INGOT), 2) + .input(Ingredient.of(Items.DIAMOND), 2) + .input(Ingredient.of(ModItems.USELESS_INGOT_TIER_2.get()), 1) + .output(ModItems.USELESS_INGOT_TIER_3.get(), 1) + .energy(100000) + .processTime(1) + .mode(AlloyFurnaceMode.NORMAL) + .save(output, UselessMod.id("advanced_alloy/ingot/useless_ingot_tier_3")); + + // Tier 4 - 使用3阶无用锭作为输入材料 + AdvancedAlloyFurnaceRecipeBuilder.create() + .input(Ingredient.of(Items.ENDER_PEARL), 2) + .input(Ingredient.of(Items.CHORUS_FRUIT), 2) + .input(Ingredient.of(Items.SHULKER_SHELL), 2) + .input(Ingredient.of(Items.GOLD_BLOCK), 2) + .input(Ingredient.of(Items.DIAMOND_BLOCK), 2) + .input(Ingredient.of(ModItems.USELESS_INGOT_TIER_3.get()), 1) + .output(ModItems.USELESS_INGOT_TIER_4.get(), 1) + .energy(500000) + .processTime(1) + .mode(AlloyFurnaceMode.NORMAL) + .save(output, UselessMod.id("advanced_alloy/ingot/useless_ingot_tier_4")); + + // Tier 5 - 使用4阶无用锭作为输入材料 + AdvancedAlloyFurnaceRecipeBuilder.create() + .input(Ingredient.of(Items.WITHER_SKELETON_SKULL), 2) + .input(Ingredient.of(Items.PHANTOM_MEMBRANE), 2) + .input(Ingredient.of(Items.SEA_LANTERN), 2) + .input(Ingredient.of(Items.GOLD_BLOCK), 2) + .input(Ingredient.of(Items.DIAMOND_BLOCK), 2) + .input(Ingredient.of(ModItems.USELESS_INGOT_TIER_4.get()), 1) + .output(ModItems.USELESS_INGOT_TIER_5.get(), 1) + .energy(1000000) + .processTime(1) + .mode(AlloyFurnaceMode.NORMAL) + .save(output, UselessMod.id("advanced_alloy/ingot/useless_ingot_tier_5")); + + // Tier 6 - 使用5阶无用锭作为输入材料 + AdvancedAlloyFurnaceRecipeBuilder.create() + .input(Ingredient.of(Items.DRAGON_BREATH), 2) + .input(Ingredient.of(Items.TOTEM_OF_UNDYING), 2) + .input(Ingredient.of(Items.ENCHANTED_GOLDEN_APPLE), 2) + .input(Ingredient.of(Items.GOLD_BLOCK), 2) + .input(Ingredient.of(Items.DIAMOND_BLOCK), 2) + .input(Ingredient.of(ModItems.USELESS_INGOT_TIER_5.get()), 1) + .output(ModItems.USELESS_INGOT_TIER_6.get(), 1) + .energy(5000000) + .processTime(1) + .mode(AlloyFurnaceMode.NORMAL) + .save(output, UselessMod.id("advanced_alloy/ingot/useless_ingot_tier_6")); + + // Tier 7 - 使用6阶无用锭作为输入材料 + AdvancedAlloyFurnaceRecipeBuilder.create() + .input(Ingredient.of(Items.NETHER_STAR), 2) + .input(Ingredient.of(Items.WITHER_ROSE), 2) + .input(Ingredient.of(Items.GHAST_TEAR), 2) + .input(Ingredient.of(Items.DIAMOND_BLOCK), 2) + .input(Ingredient.of(Items.NETHERITE_INGOT), 2) + .input(Ingredient.of(ModItems.USELESS_INGOT_TIER_6.get()), 1) + .output(ModItems.USELESS_INGOT_TIER_7.get(), 1) + .energy(10000000) + .processTime(1) + .mode(AlloyFurnaceMode.NORMAL) + .save(output, UselessMod.id("advanced_alloy/ingot/useless_ingot_tier_7")); + + // Tier 8 - 使用7阶无用锭作为输入材料 + AdvancedAlloyFurnaceRecipeBuilder.create() + .input(Ingredient.of(Items.DRAGON_HEAD), 2) + .input(Ingredient.of(Items.ECHO_SHARD), 2) + .input(Ingredient.of(Items.CONDUIT), 2) + .input(Ingredient.of(Items.DIAMOND_BLOCK), 2) + .input(Ingredient.of(Items.NETHERITE_INGOT), 2) + .input(Ingredient.of(ModItems.USELESS_INGOT_TIER_7.get()), 1) + .output(ModItems.USELESS_INGOT_TIER_8.get(), 1) + .energy(50000000) + .processTime(1) + .mode(AlloyFurnaceMode.NORMAL) + .save(output, UselessMod.id("advanced_alloy/ingot/useless_ingot_tier_8")); + + // Tier 9 - 使用8阶无用锭作为输入材料 + AdvancedAlloyFurnaceRecipeBuilder.create() + .input(Ingredient.of(Items.TURTLE_HELMET), 2) + .input(Ingredient.of(Items.GOAT_HORN), 2) + .input(Ingredient.of(Items.RABBIT_FOOT), 2) + .input(Ingredient.of(Items.DIAMOND_BLOCK), 2) + .input(Ingredient.of(Items.NETHERITE_BLOCK), 2) + .input(Ingredient.of(ModItems.USELESS_INGOT_TIER_8.get()), 1) + .output(ModItems.USELESS_INGOT_TIER_9.get(), 1) + .energy(100000000) + .processTime(1) + .mode(AlloyFurnaceMode.NORMAL) + .save(output, UselessMod.id("advanced_alloy/ingot/useless_ingot_tier_9")); + + // Possible Useful Ingot - 合成配方(工作台) + // 使用9个不同等级的无用锭合成 + + // Useful Ingot - 高级熔炉配方 + AdvancedAlloyFurnaceRecipeBuilder.create() + .input(Ingredient.of(ModItems.POSSIBLE_USEFUL_INGOT.get()), 64) + .input(Ingredient.of(Blocks.IRON_BLOCK), 1024) + .input(Ingredient.of(Blocks.GOLD_BLOCK), 1024) + .input(Ingredient.of(Blocks.DIAMOND_BLOCK), 1024) + .input(Ingredient.of(Blocks.EMERALD_BLOCK), 1024) + .input(Ingredient.of(Blocks.NETHERITE_BLOCK), 1024) + .fluidInput(new FluidStack(Fluids.LAVA, 10000000)) + .output(ModItems.USEFUL_INGOT.get(), 1) + .energy(10000000) + .processTime(100) + .mode(AlloyFurnaceMode.NORMAL) + .save(output, UselessMod.id("advanced_alloy/ingot/useful_ingot")); + } + + private void addUselessGearRecipes(RecipeOutput output) { + // Gear Tier 1-9 + for (int i = 1; i <= 9; i++) { + var ingot = switch (i) { + case 1 -> ModItems.USELESS_INGOT_TIER_1.get(); + case 2 -> ModItems.USELESS_INGOT_TIER_2.get(); + case 3 -> ModItems.USELESS_INGOT_TIER_3.get(); + case 4 -> ModItems.USELESS_INGOT_TIER_4.get(); + case 5 -> ModItems.USELESS_INGOT_TIER_5.get(); + case 6 -> ModItems.USELESS_INGOT_TIER_6.get(); + case 7 -> ModItems.USELESS_INGOT_TIER_7.get(); + case 8 -> ModItems.USELESS_INGOT_TIER_8.get(); + case 9 -> ModItems.USELESS_INGOT_TIER_9.get(); + default -> ModItems.USELESS_INGOT_TIER_1.get(); + }; + var gear = switch (i) { + case 1 -> ModItems.USELESS_GEAR_TIER_1.get(); + case 2 -> ModItems.USELESS_GEAR_TIER_2.get(); + case 3 -> ModItems.USELESS_GEAR_TIER_3.get(); + case 4 -> ModItems.USELESS_GEAR_TIER_4.get(); + case 5 -> ModItems.USELESS_GEAR_TIER_5.get(); + case 6 -> ModItems.USELESS_GEAR_TIER_6.get(); + case 7 -> ModItems.USELESS_GEAR_TIER_7.get(); + case 8 -> ModItems.USELESS_GEAR_TIER_8.get(); + case 9 -> ModItems.USELESS_GEAR_TIER_9.get(); + default -> ModItems.USELESS_GEAR_TIER_1.get(); + }; + + AdvancedAlloyFurnaceRecipeBuilder.create() + .input(Ingredient.of(ingot), 4) + .mold(Ingredient.of(ModItems.METAL_MOLD_GEAR.get())) + .output(gear, 1) + .energy(1000 * i) + .processTime(40) + .mode(AlloyFurnaceMode.NORMAL) + .save(output, UselessMod.id("advanced_alloy/gear/useless_gear_tier_" + i)); + } + } + + private void addUselessGlassRecipes(RecipeOutput output) { + // Glass Tier 1-9 + for (int i = 1; i <= 9; i++) { + var ingot = switch (i) { + case 1 -> ModItems.USELESS_INGOT_TIER_1.get(); + case 2 -> ModItems.USELESS_INGOT_TIER_2.get(); + case 3 -> ModItems.USELESS_INGOT_TIER_3.get(); + case 4 -> ModItems.USELESS_INGOT_TIER_4.get(); + case 5 -> ModItems.USELESS_INGOT_TIER_5.get(); + case 6 -> ModItems.USELESS_INGOT_TIER_6.get(); + case 7 -> ModItems.USELESS_INGOT_TIER_7.get(); + case 8 -> ModItems.USELESS_INGOT_TIER_8.get(); + case 9 -> ModItems.USELESS_INGOT_TIER_9.get(); + default -> ModItems.USELESS_INGOT_TIER_1.get(); + }; + var glass = switch (i) { + case 1 -> ModItems.USELESS_GLASS_TIER_1.get(); + case 2 -> ModItems.USELESS_GLASS_TIER_2.get(); + case 3 -> ModItems.USELESS_GLASS_TIER_3.get(); + case 4 -> ModItems.USELESS_GLASS_TIER_4.get(); + case 5 -> ModItems.USELESS_GLASS_TIER_5.get(); + case 6 -> ModItems.USELESS_GLASS_TIER_6.get(); + case 7 -> ModItems.USELESS_GLASS_TIER_7.get(); + case 8 -> ModItems.USELESS_GLASS_TIER_8.get(); + case 9 -> ModItems.USELESS_GLASS_TIER_9.get(); + default -> ModItems.USELESS_GLASS_TIER_1.get(); + }; + + AdvancedAlloyFurnaceRecipeBuilder.create() + .input(Ingredient.of(Items.GLASS), 2) + .input(Ingredient.of(ingot), 1) + .mold(Ingredient.of(ModItems.METAL_MOLD_BLOCK.get())) + .output(glass, 1) + .energy(1000 * i) + .processTime(40) + .mode(AlloyFurnaceMode.NORMAL) + .save(output, UselessMod.id("advanced_alloy/glass/useless_glass_tier_" + i)); + } + } + + private void addEndlessBeafItemRecipe(RecipeOutput consumer) { + ShapedRecipeBuilder.shaped(RecipeCategory.MISC, ModItems.ENDLESS_BEAF_ITEM.get(), 1) + .pattern("ABC") + .pattern("DEF") + .pattern("IGH") + .define('A', ModItems.USELESS_INGOT_TIER_5.get()) + .define('B', ModItems.USELESS_INGOT_TIER_4.get()) + .define('C', ModItems.USELESS_INGOT_TIER_3.get()) + .define('D', Items.DIAMOND_PICKAXE) + .define('E', ModItems.USELESS_INGOT_TIER_2.get()) + .define('F', Items.NETHERITE_PICKAXE) + .define('G', ModItems.USELESS_INGOT_TIER_1.get()) + .define('H', Items.GOLDEN_CARROT) + .define('I', Items.GHAST_TEAR) + .unlockedBy("has_ingot", has(ModItems.USELESS_INGOT_TIER_1.get())) + .save(consumer); + } + + private void addOreGeneratorBlockRecipe(RecipeOutput consumer) { + ShapedRecipeBuilder.shaped(RecipeCategory.BUILDING_BLOCKS, + ModBlocks.ORE_GENERATOR_BLOCK.get(), + 1 + ) + .pattern("AAA") + .pattern("AAA") + .pattern("AAA") + .define('A', ModItems.USEFUL_INGOT.get()) + .unlockedBy("has_ingot", has(ModItems.USEFUL_INGOT.get())) + .save(consumer); + } + + private void addTeleportBlockRecipes(RecipeOutput consumer) { + ShapedRecipeBuilder.shaped(RecipeCategory.BUILDING_BLOCKS, + ModBlocks.TELEPORT_BLOCK.get(), + 1 + ) + .pattern("AAA") + .pattern("ABA") + .pattern("AAA") + .define('A', ItemTags.PLANKS) + .define('B', Blocks.DIRT) + .unlockedBy("has_planks", has(ItemTags.PLANKS)) + .save(consumer, UselessMod.id("teleport/teleport_block")); + + ShapedRecipeBuilder.shaped(RecipeCategory.BUILDING_BLOCKS, + ModBlocks.TELEPORT_BLOCK_2.get(), + 1 + ) + .pattern("BBB") + .pattern("BAB") + .pattern("BBB") + .define('A', ItemTags.PLANKS) + .define('B', Blocks.DIRT) + .unlockedBy("has_planks", has(ItemTags.PLANKS)) + .save(consumer, UselessMod.id("teleport/teleport_block_2")); + + ShapedRecipeBuilder.shaped(RecipeCategory.BUILDING_BLOCKS, + ModBlocks.TELEPORT_BLOCK_3.get(), + 1 + ) + .pattern("BBB") + .pattern("AAA") + .pattern("BBB") + .define('B', ItemTags.PLANKS) + .define('A', Blocks.DIRT) + .unlockedBy("has_planks", has(ItemTags.PLANKS)) + .save(consumer, UselessMod.id("teleport/teleport_block_3")); + } +} diff --git a/src/main/java/com/sorrowmist/useless/datagen/providers/tags/UBlockTagsProvider.java b/src/main/java/com/sorrowmist/useless/datagen/providers/tags/UBlockTagsProvider.java new file mode 100644 index 00000000..aab400da --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/datagen/providers/tags/UBlockTagsProvider.java @@ -0,0 +1,151 @@ +package com.sorrowmist.useless.datagen.providers.tags; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.blocks.GlowPlasticBlock; +import com.sorrowmist.useless.init.ModBlocks; +import com.sorrowmist.useless.init.ModTags; +import net.minecraft.core.HolderLookup; +import net.minecraft.data.PackOutput; +import net.minecraft.tags.BlockTags; +import net.minecraft.tags.TagKey; +import net.minecraft.world.item.DyeColor; +import net.minecraft.world.level.block.Block; +import net.minecraftforge.common.Tags; +import net.minecraftforge.common.data.BlockTagsProvider; +import net.minecraftforge.common.data.ExistingFileHelper; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CompletableFuture; + +public class UBlockTagsProvider extends BlockTagsProvider { + + public UBlockTagsProvider(PackOutput output, CompletableFuture registries, + ExistingFileHelper existingFileHelper) { + super(output, registries, UselessMod.MODID, existingFileHelper); + } + + @Override + public @NotNull String getName() { + return "Tags (Block)"; + } + + @Override + protected void addTags(HolderLookup.@NotNull Provider provider) { + this.addMinecraftTags(); + this.tag(ModTags.OMNIVERSAL_FURNACE_CASINGS) + .replace(false) + .add(ModBlocks.OMNIVERSAL_FURNACE_CASING.get()) + .add(ModBlocks.PASSIVE_CRAFTING_HATCH.get()); + + // 造化杖「无视等级」的例外清单:默认留空,整合包往生成的 json 里加方块即可。 + this.tag(ModTags.BEEF_TOOL_TIER_LOCKED).replace(false); + + this.addPlasticTags(); + this.addNeoForgeTags(); + } + + /** + * 四类塑料方块各自的分类标签,外加一个涵盖全部四类的总标签。 + * + *

分类标签与 {@link GlowPlasticBlock} 中的四个方块映射表一一对应; + * 总标签 {@link ModTags#PLASTIC_BLOCKS} 直接引用四个分类标签, + * 便于一次引用全部塑料方块(如维度地板白名单仅需填写 #useless_mod:plastic_blocks)。 + */ + private void addPlasticTags() { + this.tag(ModTags.PLASTIC).replace(false); + this.tag(ModTags.GLOW_PLASTIC).replace(false); + this.tag(ModTags.PLASTIC_CTM).replace(false); + this.tag(ModTags.GLOW_PLASTIC_CTM).replace(false); + + for (var block : GlowPlasticBlock.PLASTIC_BLOCKS.values()) { + this.tag(ModTags.PLASTIC).add(block.get()); + } + for (var block : GlowPlasticBlock.GLOW_PLASTIC_BLOCKS.values()) { + this.tag(ModTags.GLOW_PLASTIC).add(block.get()); + } + for (var block : GlowPlasticBlock.PLASTIC_CTM_BLOCKS.values()) { + this.tag(ModTags.PLASTIC_CTM).add(block.get()); + } + for (var block : GlowPlasticBlock.GLOW_PLASTIC_CTM_BLOCKS.values()) { + this.tag(ModTags.GLOW_PLASTIC_CTM).add(block.get()); + } + + // 总标签直接引用四个分类标签,避免逐个方块重复列出。 + this.tag(ModTags.PLASTIC_BLOCKS) + .replace(false) + .addTag(ModTags.PLASTIC) + .addTag(ModTags.GLOW_PLASTIC) + .addTag(ModTags.PLASTIC_CTM) + .addTag(ModTags.GLOW_PLASTIC_CTM); + } + + /** + * 将四类塑料方块纳入 MinecraftForge 的约定方块标签。 + * + *

塑料方块以颜色为唯一区分维度,因此全部归入 {@code c:dyed} 总标签, + * 并依颜色的染料映射归入对应的 {@code c:dyed/} 子标签。 + * {@code dark_red} 与 {@code aqua} 不存在对应的 {@link DyeColor}, + * 仅纳入总标签,不参与子标签归类。 + */ + private void addNeoForgeTags() { + for (var blockMap : GlowPlasticBlock.ALL_BLOCK_MAPS) { + for (var entry : blockMap.entrySet()) { + Block block = entry.getValue().get(); + this.tag(Tags.Blocks.DYED).add(block); + + DyeColor dyeColor = entry.getKey().getDyeColor(); + if (dyeColor != null) { + this.tag(dyedTag(dyeColor)).add(block); + } + } + } + } + + /** 将染料颜色映射到 MinecraftForge 的 {@code c:dyed/} 方块标签。 */ + private static TagKey dyedTag(DyeColor color) { + return switch (color) { + case WHITE -> Tags.Blocks.DYED_WHITE; + case ORANGE -> Tags.Blocks.DYED_ORANGE; + case MAGENTA -> Tags.Blocks.DYED_MAGENTA; + case LIGHT_BLUE -> Tags.Blocks.DYED_LIGHT_BLUE; + case YELLOW -> Tags.Blocks.DYED_YELLOW; + case LIME -> Tags.Blocks.DYED_LIME; + case PINK -> Tags.Blocks.DYED_PINK; + case GRAY -> Tags.Blocks.DYED_GRAY; + case LIGHT_GRAY -> Tags.Blocks.DYED_LIGHT_GRAY; + case CYAN -> Tags.Blocks.DYED_CYAN; + case PURPLE -> Tags.Blocks.DYED_PURPLE; + case BLUE -> Tags.Blocks.DYED_BLUE; + case BROWN -> Tags.Blocks.DYED_BROWN; + case GREEN -> Tags.Blocks.DYED_GREEN; + case RED -> Tags.Blocks.DYED_RED; + case BLACK -> Tags.Blocks.DYED_BLACK; + }; + } + + private void addMinecraftTags() { + this.tag(BlockTags.MINEABLE_WITH_PICKAXE) + .replace(false) + .add(ModBlocks.ORE_GENERATOR_BLOCK.get()) + .add(ModBlocks.TELEPORT_BLOCK.get()) + .add(ModBlocks.TELEPORT_BLOCK_2.get()) + .add(ModBlocks.TELEPORT_BLOCK_3.get()) + .add(ModBlocks.ADVANCED_ALLOY_FURNACE_BLOCK.get()) + .add(ModBlocks.MULTIBLOCK_ALLOY_FURNACE_CORE.get()) + .add(ModBlocks.ME_PATTERN_ASSEMBLY.get()) + .add(ModBlocks.OMNIVERSAL_MOLD_HUB.get()) + .add(ModBlocks.PASSIVE_CRAFTING_HATCH.get()) + .add(ModBlocks.OMNIVERSAL_FURNACE_CASING.get()) + .add(ModBlocks.TEMPERATURE_REGULATOR.get()); + + for (var coil : ModBlocks.USELESS_COILS.values()) { + this.tag(BlockTags.MINEABLE_WITH_PICKAXE).add(coil.get()); + } + + for (var blockMap : GlowPlasticBlock.ALL_BLOCK_MAPS) { + for (var block : blockMap.values()) { + this.tag(BlockTags.MINEABLE_WITH_PICKAXE).add(block.get()); + } + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/datagen/providers/tags/UItemTagsProvider.java b/src/main/java/com/sorrowmist/useless/datagen/providers/tags/UItemTagsProvider.java new file mode 100644 index 00000000..e3599ca3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/datagen/providers/tags/UItemTagsProvider.java @@ -0,0 +1,384 @@ +package com.sorrowmist.useless.datagen.providers.tags; + + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.blocks.GlowPlasticBlock; +import com.sorrowmist.useless.init.ModItems; +import com.sorrowmist.useless.init.ModTags; +import net.minecraft.core.HolderLookup; +import net.minecraft.data.PackOutput; +import net.minecraft.data.tags.ItemTagsProvider; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.ItemTags; +import net.minecraft.tags.TagKey; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.block.Block; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CompletableFuture; + +public class UItemTagsProvider extends ItemTagsProvider { + + public UItemTagsProvider(PackOutput output, CompletableFuture registries, CompletableFuture> blockTags) { + super(output, registries, blockTags); + } + + @Override + public @NotNull String getName() { + return "Tags (Item)"; + } + + @Override + protected void addTags(HolderLookup.@NotNull Provider provider) { + this.addUselessModTags(); + this.addCTags(); + this.addGtceuTags(); + this.addMinecraftTags(); + } + + private void addUselessModTags() { + // 塑料方块物品标签:供「任意塑料方块 → 温度调节器」配方引用。 + // 注意:不能直接 copy 方块总标签——它只引用四个分类标签,而分类标签没有对应的物品标签; + // 这里直接把全部塑料方块物品平铺进来。 + this.tag(ModTags.PLASTIC_BLOCKS_ITEM).replace(false); + for (var itemMap : GlowPlasticBlock.ALL_BLOCK_ITEM_MAPS) { + for (var item : itemMap.values()) { + this.tag(ModTags.PLASTIC_BLOCKS_ITEM).add(item.get()); + } + } + + this.tag(this.createTagKey("useless_ingots")) + .replace(false) + .add(ModItems.USELESS_INGOT_TIER_1.get()) + .add(ModItems.USELESS_INGOT_TIER_2.get()) + .add(ModItems.USELESS_INGOT_TIER_3.get()) + .add(ModItems.USELESS_INGOT_TIER_4.get()) + .add(ModItems.USELESS_INGOT_TIER_5.get()) + .add(ModItems.USELESS_INGOT_TIER_6.get()) + .add(ModItems.USELESS_INGOT_TIER_7.get()) + .add(ModItems.USELESS_INGOT_TIER_8.get()) + .add(ModItems.USELESS_INGOT_TIER_9.get()) + .add(ModItems.USEFUL_INGOT.get()); + + this.tag(ModTags.CATALYSTS) + .replace(false) + .add(ModItems.USELESS_INGOT_TIER_1.get()) + .add(ModItems.USELESS_INGOT_TIER_2.get()) + .add(ModItems.USELESS_INGOT_TIER_3.get()) + .add(ModItems.USELESS_INGOT_TIER_4.get()) + .add(ModItems.USELESS_INGOT_TIER_5.get()) + .add(ModItems.USELESS_INGOT_TIER_6.get()) + .add(ModItems.USELESS_INGOT_TIER_7.get()) + .add(ModItems.USELESS_INGOT_TIER_8.get()) + .add(ModItems.USELESS_INGOT_TIER_9.get()) + .add(ModItems.USEFUL_INGOT.get()); + + this.tag(ModTags.MOLDS) + .replace(false) + .add(ModItems.METAL_MOLD_PLATE.get()) + .add(ModItems.METAL_MOLD_ROD.get()) + .add(ModItems.METAL_MOLD_GEAR.get()) + .add(ModItems.METAL_MOLD_WIRE.get()) + .add(ModItems.METAL_MOLD_BLOCK.get()) + .addOptional(new ResourceLocation("powah", "energizing_orb")) + .addOptional(new ResourceLocation("extendedcrafting", "basic_table")) + .addOptional(new ResourceLocation("extendedcrafting", "advanced_table")) + .addOptional(new ResourceLocation("extendedcrafting", "elite_table")) + .addOptional(new ResourceLocation("extendedcrafting", "ultimate_table")) + .addOptional(new ResourceLocation("extendedcrafting", "compressor")) + .addOptional(new ResourceLocation("extendedcrafting", "crafting_core")) + .addOptional(new ResourceLocation("extendedcrafting", "ender_crafter")) + .addOptional(new ResourceLocation("extendedcrafting", "flux_crafter")) + .addOptional(new ResourceLocation("neoecoae", "integrated_working_station")) + .addOptional(new ResourceLocation("naturesaura", "wood_stand")) + .addOptional(new ResourceLocation("naturesaura", "nature_altar")) + .addOptional(new ResourceLocation("naturesaura", "animal_spawner")) + .addOptional(new ResourceLocation("naturesaura", "offering_table")) + .addOptional(new ResourceLocation("naturesaura", "conversion_catalyst")) + .addOptional(new ResourceLocation("naturesaura", "crushing_catalyst")) + .addOptional(new ResourceLocation("malum", "spirit_crucible")) + .addOptional(new ResourceLocation("malum", "spirit_altar")) + .addOptional(new ResourceLocation("create", "spout")) + .addOptional(new ResourceLocation("create", "mechanical_press")) + .addOptional(new ResourceLocation("create", "mechanical_mixer")) + .addOptional(new ResourceLocation("create", "basin")) + .addOptional(new ResourceLocation("create", "millstone")) + .addOptional(new ResourceLocation("create", "crushing_wheel")) + .addOptional(new ResourceLocation("create", "encased_fan")) + .addOptional(new ResourceLocation("create", "mechanical_crafter")) + .addOptional(new ResourceLocation("create", "deployer")) + .addOptional(new ResourceLocation("create", "mechanical_saw")) + .addOptional(new ResourceLocation("create", "blaze_burner")) + .addOptional(new ResourceLocation("useless_mod", "ritual_blueprint")); + } + + private void addCTags() { + TagKey crowbar = this.createExternalTagKey("c", "tools/crowbar"); + TagKey hammer = this.createExternalTagKey("c", "tools/hammer"); + TagKey mallet = this.createExternalTagKey("c", "tools/mallet"); + TagKey screwdriver = this.createExternalTagKey("c", "tools/screwdriver"); + TagKey wrench = this.createExternalTagKey("c", "tools/wrench"); + TagKey file = this.createExternalTagKey("c", "tools/file"); + TagKey knife = this.createExternalTagKey("c", "tools/knife"); + TagKey miningTool = this.createExternalTagKey("c", "tools/mining_tool"); + TagKey mortar = this.createExternalTagKey("c", "tools/mortar"); + TagKey saw = this.createExternalTagKey("c", "tools/saw"); + TagKey shear = this.createExternalTagKey("c", "tools/shear"); + TagKey tools = this.createExternalTagKey("c", "tools/tools"); + TagKey wireCutter = this.createExternalTagKey("c", "tools/wire_cutter"); + + this.tag(crowbar) + .replace(false) + .add(ModItems.ENDLESS_BEAF_ITEM.get()) + .add(ModItems.ENDLESS_BEAF_ITEM_NO_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_CROWBAR.get()); + + this.tag(hammer) + .replace(false) + .add(ModItems.ENDLESS_BEAF_ITEM.get()) + .add(ModItems.ENDLESS_BEAF_ITEM_NO_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_HAMMER.get()); + + this.tag(mallet) + .replace(false) + .add(ModItems.ENDLESS_BEAF_ITEM.get()) + .add(ModItems.ENDLESS_BEAF_ITEM_NO_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_MALLET.get()); + + this.tag(screwdriver) + .replace(false) + .add(ModItems.ENDLESS_BEAF_ITEM.get()) + .add(ModItems.ENDLESS_BEAF_ITEM_NO_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_SCREWDRIVER.get()); + + this.tag(wrench) + .replace(false) + .add(ModItems.ENDLESS_BEAF_ITEM.get()) + .add(ModItems.ENDLESS_BEAF_WRENCH.get()); + + this.tag(file) + .replace(false) + .add(ModItems.ENDLESS_BEAF_ITEM.get()) + .add(ModItems.ENDLESS_BEAF_ITEM_NO_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_SCREWDRIVER.get()) + .add(ModItems.ENDLESS_BEAF_MALLET.get()) + .add(ModItems.ENDLESS_BEAF_CROWBAR.get()) + .add(ModItems.ENDLESS_BEAF_HAMMER.get()); + + this.tag(knife) + .replace(false) + .add(ModItems.ENDLESS_BEAF_ITEM.get()) + .add(ModItems.ENDLESS_BEAF_ITEM_NO_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_SCREWDRIVER.get()) + .add(ModItems.ENDLESS_BEAF_MALLET.get()) + .add(ModItems.ENDLESS_BEAF_CROWBAR.get()) + .add(ModItems.ENDLESS_BEAF_HAMMER.get()); + + this.tag(miningTool) + .replace(false) + .add(ModItems.ENDLESS_BEAF_ITEM.get()) + .add(ModItems.ENDLESS_BEAF_ITEM_NO_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_SCREWDRIVER.get()) + .add(ModItems.ENDLESS_BEAF_MALLET.get()) + .add(ModItems.ENDLESS_BEAF_CROWBAR.get()) + .add(ModItems.ENDLESS_BEAF_HAMMER.get()); + + this.tag(mortar) + .replace(false) + .add(ModItems.ENDLESS_BEAF_ITEM.get()) + .add(ModItems.ENDLESS_BEAF_ITEM_NO_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_SCREWDRIVER.get()) + .add(ModItems.ENDLESS_BEAF_MALLET.get()) + .add(ModItems.ENDLESS_BEAF_CROWBAR.get()) + .add(ModItems.ENDLESS_BEAF_HAMMER.get()); + + this.tag(saw) + .replace(false) + .add(ModItems.ENDLESS_BEAF_ITEM.get()) + .add(ModItems.ENDLESS_BEAF_ITEM_NO_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_SCREWDRIVER.get()) + .add(ModItems.ENDLESS_BEAF_MALLET.get()) + .add(ModItems.ENDLESS_BEAF_CROWBAR.get()) + .add(ModItems.ENDLESS_BEAF_HAMMER.get()); + + this.tag(shear) + .replace(false) + .add(ModItems.ENDLESS_BEAF_ITEM.get()) + .add(ModItems.ENDLESS_BEAF_ITEM_NO_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_SCREWDRIVER.get()) + .add(ModItems.ENDLESS_BEAF_MALLET.get()) + .add(ModItems.ENDLESS_BEAF_CROWBAR.get()) + .add(ModItems.ENDLESS_BEAF_HAMMER.get()); + + this.tag(tools) + .replace(false) + .add(ModItems.ENDLESS_BEAF_ITEM.get()) + .add(ModItems.ENDLESS_BEAF_ITEM_NO_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_SCREWDRIVER.get()) + .add(ModItems.ENDLESS_BEAF_MALLET.get()) + .add(ModItems.ENDLESS_BEAF_CROWBAR.get()) + .add(ModItems.ENDLESS_BEAF_HAMMER.get()); + + this.tag(wireCutter) + .replace(false) + .add(ModItems.ENDLESS_BEAF_ITEM.get()) + .add(ModItems.ENDLESS_BEAF_ITEM_NO_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_CROWBAR.get()) + .add(ModItems.ENDLESS_BEAF_HAMMER.get()); + } + + private void addGtceuTags() { + TagKey crowbar = this.createExternalTagKey("gtceu", "crafting_tools/crowbar"); + TagKey hammer = this.createExternalTagKey("gtceu", "crafting_tools/hammer"); + TagKey mallet = this.createExternalTagKey("gtceu", "crafting_tools/mallet"); + TagKey screwdriver = this.createExternalTagKey("gtceu", "crafting_tools/screwdriver"); + TagKey wrench = this.createExternalTagKey("gtceu", "crafting_tools/wrench"); + TagKey file = this.createExternalTagKey("gtceu", "crafting_tools/file"); + TagKey knife = this.createExternalTagKey("gtceu", "crafting_tools/knife"); + TagKey mortar = this.createExternalTagKey("gtceu", "crafting_tools/mortar"); + TagKey saw = this.createExternalTagKey("gtceu", "crafting_tools/saw"); + TagKey wireCutter = this.createExternalTagKey("gtceu", "crafting_tools/wire_cutter"); + + this.tag(crowbar).replace(false).add(ModItems.ENDLESS_BEAF_CROWBAR.get()); + this.tag(hammer).replace(false).add(ModItems.ENDLESS_BEAF_HAMMER.get()); + this.tag(mallet).replace(false).add(ModItems.ENDLESS_BEAF_MALLET.get()); + this.tag(screwdriver).replace(false).add(ModItems.ENDLESS_BEAF_SCREWDRIVER.get()); + this.tag(wrench).replace(false).add(ModItems.ENDLESS_BEAF_WRENCH.get()); + + this.tag(file) + .replace(false) + .add(ModItems.ENDLESS_BEAF_ITEM.get()) + .add(ModItems.ENDLESS_BEAF_ITEM_NO_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_SCREWDRIVER.get()) + .add(ModItems.ENDLESS_BEAF_MALLET.get()) + .add(ModItems.ENDLESS_BEAF_CROWBAR.get()) + .add(ModItems.ENDLESS_BEAF_HAMMER.get()); + + this.tag(knife) + .replace(false) + .add(ModItems.ENDLESS_BEAF_ITEM.get()) + .add(ModItems.ENDLESS_BEAF_ITEM_NO_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_SCREWDRIVER.get()) + .add(ModItems.ENDLESS_BEAF_MALLET.get()) + .add(ModItems.ENDLESS_BEAF_CROWBAR.get()) + .add(ModItems.ENDLESS_BEAF_HAMMER.get()); + + this.tag(mortar) + .replace(false) + .add(ModItems.ENDLESS_BEAF_ITEM.get()) + .add(ModItems.ENDLESS_BEAF_ITEM_NO_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_SCREWDRIVER.get()) + .add(ModItems.ENDLESS_BEAF_MALLET.get()) + .add(ModItems.ENDLESS_BEAF_CROWBAR.get()) + .add(ModItems.ENDLESS_BEAF_HAMMER.get()); + + this.tag(saw) + .replace(false) + .add(ModItems.ENDLESS_BEAF_ITEM.get()) + .add(ModItems.ENDLESS_BEAF_ITEM_NO_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_SCREWDRIVER.get()) + .add(ModItems.ENDLESS_BEAF_MALLET.get()) + .add(ModItems.ENDLESS_BEAF_CROWBAR.get()) + .add(ModItems.ENDLESS_BEAF_HAMMER.get()); + + this.tag(wireCutter) + .replace(false) + .add(ModItems.ENDLESS_BEAF_ITEM.get()) + .add(ModItems.ENDLESS_BEAF_ITEM_NO_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_CROWBAR.get()) + .add(ModItems.ENDLESS_BEAF_HAMMER.get()); + } + + private void addMinecraftTags() { + this.tag(ItemTags.AXES) + .replace(false) + .add(ModItems.ENDLESS_BEAF_ITEM.get()) + .add(ModItems.ENDLESS_BEAF_ITEM_NO_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_SCREWDRIVER.get()) + .add(ModItems.ENDLESS_BEAF_MALLET.get()) + .add(ModItems.ENDLESS_BEAF_CROWBAR.get()) + .add(ModItems.ENDLESS_BEAF_HAMMER.get()); + + this.tag(ItemTags.BREAKS_DECORATED_POTS) + .replace(false) + .add(ModItems.ENDLESS_BEAF_ITEM.get()) + .add(ModItems.ENDLESS_BEAF_ITEM_NO_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_SCREWDRIVER.get()) + .add(ModItems.ENDLESS_BEAF_MALLET.get()) + .add(ModItems.ENDLESS_BEAF_CROWBAR.get()) + .add(ModItems.ENDLESS_BEAF_HAMMER.get()); + + this.tag(ItemTags.CLUSTER_MAX_HARVESTABLES) + .replace(false) + .add(ModItems.ENDLESS_BEAF_ITEM.get()) + .add(ModItems.ENDLESS_BEAF_ITEM_NO_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_SCREWDRIVER.get()) + .add(ModItems.ENDLESS_BEAF_MALLET.get()) + .add(ModItems.ENDLESS_BEAF_CROWBAR.get()) + .add(ModItems.ENDLESS_BEAF_HAMMER.get()); + + this.tag(ItemTags.HOES) + .replace(false) + .add(ModItems.ENDLESS_BEAF_ITEM.get()) + .add(ModItems.ENDLESS_BEAF_ITEM_NO_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_SCREWDRIVER.get()) + .add(ModItems.ENDLESS_BEAF_MALLET.get()) + .add(ModItems.ENDLESS_BEAF_CROWBAR.get()) + .add(ModItems.ENDLESS_BEAF_HAMMER.get()); + + this.tag(ItemTags.PICKAXES) + .replace(false) + .add(ModItems.ENDLESS_BEAF_ITEM.get()) + .add(ModItems.ENDLESS_BEAF_ITEM_NO_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_SCREWDRIVER.get()) + .add(ModItems.ENDLESS_BEAF_MALLET.get()) + .add(ModItems.ENDLESS_BEAF_CROWBAR.get()) + .add(ModItems.ENDLESS_BEAF_HAMMER.get()); + + this.tag(ItemTags.SHOVELS) + .replace(false) + .add(ModItems.ENDLESS_BEAF_ITEM.get()) + .add(ModItems.ENDLESS_BEAF_ITEM_NO_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_SCREWDRIVER.get()) + .add(ModItems.ENDLESS_BEAF_MALLET.get()) + .add(ModItems.ENDLESS_BEAF_CROWBAR.get()) + .add(ModItems.ENDLESS_BEAF_HAMMER.get()); + + this.tag(ItemTags.SWORDS) + .replace(false) + .add(ModItems.ENDLESS_BEAF_ITEM.get()) + .add(ModItems.ENDLESS_BEAF_ITEM_NO_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_WRENCH.get()) + .add(ModItems.ENDLESS_BEAF_SCREWDRIVER.get()) + .add(ModItems.ENDLESS_BEAF_MALLET.get()) + .add(ModItems.ENDLESS_BEAF_CROWBAR.get()) + .add(ModItems.ENDLESS_BEAF_HAMMER.get()); + } + + private TagKey createTagKey(String path) { + return ItemTags.create(new ResourceLocation(UselessMod.MODID, path)); + } + + private TagKey createExternalTagKey(String namespace, String path) { + return ItemTags.create(new ResourceLocation(namespace, path)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/energy/EnergyManager.java b/src/main/java/com/sorrowmist/useless/energy/EnergyManager.java new file mode 100644 index 00000000..ca10fe5b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/energy/EnergyManager.java @@ -0,0 +1,304 @@ +package com.sorrowmist.useless.energy; + +import net.minecraft.nbt.CompoundTag; + +import java.util.concurrent.atomic.AtomicLong; + +/** Thread-safe long-backed energy storage. */ +public class EnergyManager implements IEnergyManager { + + private final AtomicLong energy = new AtomicLong(0L); + private volatile long capacity; + private volatile long maxReceive; + private volatile long maxExtract; + private Runnable changeListener; + + public EnergyManager(long capacity) { + this(capacity, capacity, capacity); + } + + public EnergyManager(long capacity, long maxTransfer) { + this(capacity, maxTransfer, maxTransfer); + } + + private EnergyManager(long capacity, long maxReceive, long maxExtract) { + this(capacity, maxReceive, maxExtract, 0L); + } + + private EnergyManager(long capacity, long maxReceive, long maxExtract, long initialEnergy) { + this.capacity = Math.max(0L, capacity); + this.maxReceive = clamp(maxReceive, 0L, this.capacity); + this.maxExtract = clamp(maxExtract, 0L, this.capacity); + this.energy.set(clamp(initialEnergy, 0L, this.capacity)); + } + + public static Builder builder() { + return new Builder(); + } + + @Override + public long receiveEnergy(long requested, boolean simulate) { + if (!this.canReceive() || requested <= 0L) { + return 0L; + } + + while (true) { + long current = this.energy.get(); + long accepted = Math.min(this.capacity - current, Math.min(this.maxReceive, requested)); + if (accepted <= 0L || simulate) { + return Math.max(0L, accepted); + } + if (this.energy.compareAndSet(current, current + accepted)) { + this.notifyChange(); + return accepted; + } + } + } + + @Override + public int receiveEnergy(int maxReceive, boolean simulate) { + return (int) Math.min(Integer.MAX_VALUE, this.receiveEnergy((long) maxReceive, simulate)); + } + + @Override + public long extractEnergy(long requested, boolean simulate) { + if (!this.canExtract() || requested <= 0L) { + return 0L; + } + + while (true) { + long current = this.energy.get(); + long extracted = Math.min(current, Math.min(this.maxExtract, requested)); + if (extracted <= 0L || simulate) { + return Math.max(0L, extracted); + } + if (this.energy.compareAndSet(current, current - extracted)) { + this.notifyChange(); + return extracted; + } + } + } + + @Override + public int extractEnergy(int maxExtract, boolean simulate) { + return (int) Math.min(Integer.MAX_VALUE, this.extractEnergy((long) maxExtract, simulate)); + } + + @Override + public int getEnergyStored() { + return (int) Math.min(Integer.MAX_VALUE, this.getEnergyStoredLong()); + } + + @Override + public long getEnergyStoredLong() { + return this.energy.get(); + } + + @Override + public int getMaxEnergyStored() { + return (int) Math.min(Integer.MAX_VALUE, this.getMaxEnergyStoredLong()); + } + + @Override + public long getMaxEnergyStoredLong() { + return this.capacity; + } + + @Override + public long getMaxReceiveLong() { + return this.maxReceive; + } + + @Override + public long getMaxExtractLong() { + return this.maxExtract; + } + + @Override + public boolean canExtract() { + return this.maxExtract > 0L; + } + + @Override + public boolean canReceive() { + return this.maxReceive > 0L; + } + + @Override + public void setMaxEnergyStored(long capacity) { + long previousCapacity = this.capacity; + this.capacity = Math.max(0L, capacity); + this.maxReceive = Math.min(this.maxReceive, this.capacity); + this.maxExtract = Math.min(this.maxExtract, this.capacity); + this.setEnergyStored(this.energy.get()); + if (previousCapacity != this.capacity) { + this.notifyChange(); + } + } + + @Override + public void setMaxReceive(long maxReceive) { + this.maxReceive = clamp(maxReceive, 0L, this.capacity); + } + + @Override + public void setMaxExtract(long maxExtract) { + this.maxExtract = clamp(maxExtract, 0L, this.capacity); + } + + @Override + public void modifyEnergy(long delta) { + this.modifyEnergyStored(delta); + } + + @Override + public long modifyEnergyStored(long delta) { + if (delta == 0L) { + return 0L; + } + while (true) { + long current = this.energy.get(); + long next = clamp(saturatingAdd(current, delta), 0L, this.capacity); + if (next == current) { + return 0L; + } + if (this.energy.compareAndSet(current, next)) { + this.notifyChange(); + return next > current ? next - current : current - next; + } + } + } + + @Override + public boolean canWork(long energyRequired) { + return energyRequired >= 0L && this.energy.get() >= energyRequired; + } + + @Override + public boolean tryConsumeEnergy(long amount) { + if (amount <= 0L) { + return false; + } + + while (true) { + long current = this.energy.get(); + if (current < amount) { + return false; + } + if (this.energy.compareAndSet(current, current - amount)) { + this.notifyChange(); + return true; + } + } + } + + @Override + public CompoundTag serializeNBT() { + CompoundTag tag = new CompoundTag(); + tag.putLong("Energy", this.energy.get()); + tag.putLong("Capacity", this.capacity); + tag.putLong("MaxReceive", this.maxReceive); + tag.putLong("MaxExtract", this.maxExtract); + return tag; + } + + @Override + public void deserializeNBT(CompoundTag tag) { + long loadedCapacity = tag.contains("Capacity") ? tag.getLong("Capacity") : this.capacity; + this.capacity = Math.max(0L, loadedCapacity); + this.maxReceive = clamp(tag.contains("MaxReceive") ? tag.getLong("MaxReceive") : this.maxReceive, + 0L, this.capacity); + this.maxExtract = clamp(tag.contains("MaxExtract") ? tag.getLong("MaxExtract") : this.maxExtract, + 0L, this.capacity); + this.energy.set(clamp(tag.contains("Energy") ? tag.getLong("Energy") : this.energy.get(), + 0L, this.capacity)); + this.notifyChange(); + } + + @Override + public void setChangeListener(Runnable listener) { + this.changeListener = listener; + } + + @Override + public void removeChangeListener() { + this.changeListener = null; + } + + @Override + public void setEnergyStored(long energy) { + long next = clamp(energy, 0L, this.capacity); + long previous = this.energy.getAndSet(next); + if (previous != next) { + this.notifyChange(); + } + } + + private void notifyChange() { + if (this.changeListener != null) { + this.changeListener.run(); + } + } + + private static long clamp(long value, long minimum, long maximum) { + return Math.max(minimum, Math.min(value, maximum)); + } + + private static long saturatingAdd(long left, long right) { + if (right > 0L && left > Long.MAX_VALUE - right) { + return Long.MAX_VALUE; + } + if (right < 0L && left < Long.MIN_VALUE - right) { + return Long.MIN_VALUE; + } + return left + right; + } + + public static class Builder { + private long capacity = 10_000L; + private long maxReceive = 1_000L; + private long maxExtract = 1_000L; + private long initialEnergy = 0L; + private Runnable changeListener; + + public Builder capacity(long capacity) { + this.capacity = capacity; + return this; + } + + public Builder maxReceive(long maxReceive) { + this.maxReceive = maxReceive; + return this; + } + + public Builder maxExtract(long maxExtract) { + this.maxExtract = maxExtract; + return this; + } + + public Builder maxTransfer(long maxTransfer) { + this.maxReceive = maxTransfer; + this.maxExtract = maxTransfer; + return this; + } + + public Builder initialEnergy(long initialEnergy) { + this.initialEnergy = initialEnergy; + return this; + } + + public Builder onChange(Runnable listener) { + this.changeListener = listener; + return this; + } + + public EnergyManager build() { + EnergyManager manager = new EnergyManager( + this.capacity, this.maxReceive, this.maxExtract, this.initialEnergy); + if (this.changeListener != null) { + manager.setChangeListener(this.changeListener); + } + return manager; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/energy/IEnergyManager.java b/src/main/java/com/sorrowmist/useless/energy/IEnergyManager.java new file mode 100644 index 00000000..b550cfe3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/energy/IEnergyManager.java @@ -0,0 +1,54 @@ +package com.sorrowmist.useless.energy; + +import net.minecraft.nbt.CompoundTag; +import net.minecraftforge.energy.IEnergyStorage; + +/** Long-backed energy storage with an int Forge Energy compatibility surface. */ +public interface IEnergyManager extends IEnergyStorage { + + long receiveEnergy(long maxReceive, boolean simulate); + + long extractEnergy(long maxExtract, boolean simulate); + + long getEnergyStoredLong(); + + long getMaxEnergyStoredLong(); + + long getMaxReceiveLong(); + + long getMaxExtractLong(); + + void setEnergyStored(long energy); + + void setMaxEnergyStored(long capacity); + + void setMaxReceive(long maxReceive); + + void setMaxExtract(long maxExtract); + + void modifyEnergy(long delta); + + /** + * Changes stored energy without applying transfer-rate limits. + * + * @return the absolute amount that was actually added or removed + */ + long modifyEnergyStored(long delta); + + boolean canWork(long energyRequired); + + boolean tryConsumeEnergy(long amount); + + default double getEnergyPercentage() { + long capacity = this.getMaxEnergyStoredLong(); + return capacity <= 0L ? 0.0D : (double) this.getEnergyStoredLong() / capacity; + } + + CompoundTag serializeNBT(); + + void deserializeNBT(CompoundTag tag); + + void setChangeListener(Runnable listener); + + void removeChangeListener(); +} diff --git a/src/main/java/com/sorrowmist/useless/event/BeefInvulnerabilityOwnership.java b/src/main/java/com/sorrowmist/useless/event/BeefInvulnerabilityOwnership.java new file mode 100644 index 00000000..f44cf74c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/event/BeefInvulnerabilityOwnership.java @@ -0,0 +1,105 @@ +package com.sorrowmist.useless.event; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.world.entity.player.Player; +import org.jetbrains.annotations.Nullable; + +final class BeefInvulnerabilityOwnership { + private static final String ROOT_TAG = "useless_mod:beef_invulnerability"; + private static final String VERSION_TAG = "version"; + private static final String ACTIVE_TAG = "active"; + private static final String PREVIOUS_INVULNERABLE_TAG = "previous_invulnerable"; + private static final String PREVIOUS_MAX_HEALTH_BASE_TAG = "previous_max_health_base"; + // Version 3 also records the max-health base value for recovery from external changes. + private static final int CURRENT_VERSION = 3; + private static final double DEFAULT_MAX_HEALTH_BASE = 20.0D; + + private BeefInvulnerabilityOwnership() {} + + static @Nullable CompoundTag get(Player player) { + return get(player.getPersistentData()); + } + + static @Nullable CompoundTag get(CompoundTag persistentData) { + if (!persistentData.contains(Player.PERSISTED_NBT_TAG, Tag.TAG_COMPOUND)) { + return null; + } + + CompoundTag playerData = persistentData.getCompound(Player.PERSISTED_NBT_TAG); + return playerData.contains(ROOT_TAG, Tag.TAG_COMPOUND) ? playerData.getCompound(ROOT_TAG) : null; + } + + static CompoundTag getOrCreate(Player player) { + return getOrCreate(player.getPersistentData()); + } + + static CompoundTag getOrCreate(CompoundTag persistentData) { + if (!persistentData.contains(Player.PERSISTED_NBT_TAG, Tag.TAG_COMPOUND)) { + persistentData.put(Player.PERSISTED_NBT_TAG, new CompoundTag()); + } + + CompoundTag playerData = persistentData.getCompound(Player.PERSISTED_NBT_TAG); + if (!playerData.contains(ROOT_TAG, Tag.TAG_COMPOUND)) { + playerData.put(ROOT_TAG, new CompoundTag()); + } + return playerData.getCompound(ROOT_TAG); + } + + static boolean isMigrationComplete(@Nullable CompoundTag data) { + return data != null && data.getInt(VERSION_TAG) >= CURRENT_VERSION; + } + + static boolean isOwned(@Nullable CompoundTag data) { + return data != null && data.getBoolean(ACTIVE_TAG); + } + + static void markMigrationComplete(CompoundTag data) { + data.putInt(VERSION_TAG, CURRENT_VERSION); + } + + static void rememberMaxHealthBase(CompoundTag data, double baseValue) { + if (!data.contains(PREVIOUS_MAX_HEALTH_BASE_TAG, Tag.TAG_DOUBLE) + && Double.isFinite(baseValue) && baseValue > 0.0D) { + data.putDouble(PREVIOUS_MAX_HEALTH_BASE_TAG, baseValue); + } + } + + static double previousMaxHealthBase(@Nullable CompoundTag data) { + if (data != null && data.contains(PREVIOUS_MAX_HEALTH_BASE_TAG, Tag.TAG_DOUBLE)) { + double baseValue = data.getDouble(PREVIOUS_MAX_HEALTH_BASE_TAG); + if (Double.isFinite(baseValue) && baseValue > 0.0D) { + return baseValue; + } + } + return DEFAULT_MAX_HEALTH_BASE; + } + + static boolean claim(CompoundTag data, boolean previouslyInvulnerable) { + markMigrationComplete(data); + if (isOwned(data)) { + return false; + } + + data.putBoolean(ACTIVE_TAG, true); + data.putBoolean(PREVIOUS_INVULNERABLE_TAG, previouslyInvulnerable); + return true; + } + + static ReleaseResult release(@Nullable CompoundTag data) { + if (!isOwned(data)) { + return ReleaseResult.NOT_OWNED; + } + + boolean previousInvulnerable = data.getBoolean(PREVIOUS_INVULNERABLE_TAG); + data.remove(ACTIVE_TAG); + data.remove(PREVIOUS_INVULNERABLE_TAG); + data.remove(PREVIOUS_MAX_HEALTH_BASE_TAG); + markMigrationComplete(data); + return new ReleaseResult(true, previousInvulnerable); + } + + record ReleaseResult(boolean owned, boolean previousInvulnerable) { + private static final ReleaseResult NOT_OWNED = new ReleaseResult(false, false); + } +} diff --git a/src/main/java/com/sorrowmist/useless/event/EventHandler.java b/src/main/java/com/sorrowmist/useless/event/EventHandler.java new file mode 100644 index 00000000..9a3ed015 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/event/EventHandler.java @@ -0,0 +1,975 @@ +package com.sorrowmist.useless.event; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.client.StaffLinkClientHooks; +import com.sorrowmist.useless.content.items.BeefCraftingHandler; +import com.sorrowmist.useless.content.items.BeefMagnetHandler; +import com.sorrowmist.useless.content.items.BeefTimeAcceleration; +import com.sorrowmist.useless.content.items.EndlessBeafItem; +import com.sorrowmist.useless.content.stafflink.StaffLinkEngine; +import com.sorrowmist.useless.content.stafflink.StaffLinkTargets; +import com.sorrowmist.useless.compat.ae.AeDeviceLinker; +import com.sorrowmist.useless.compat.ae.AeLinkChannelBypass; +import com.sorrowmist.useless.compat.constructionwand.ConstructionWandLogic; +import com.sorrowmist.useless.compat.create.CreateStressCompatLoader; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeManager; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeCatalog; +import com.sorrowmist.useless.content.multiblock.OmniversalFurnaceAutoBuilder; +import com.sorrowmist.useless.content.blockentities.multiblock.MultiblockAlloyFurnaceCoreBlockEntity; +import com.sorrowmist.useless.core.common.FlyEffectedHolder; +import com.sorrowmist.useless.core.component.UComponents; +import com.sorrowmist.useless.core.config.BeefToolProtectionManager; +import com.sorrowmist.useless.core.config.ConfigManager; +import com.sorrowmist.useless.core.config.ChainGroupManager; +import com.sorrowmist.useless.data.BeefToolLayout; +import com.sorrowmist.useless.data.BeefToolLayoutManager; +import com.sorrowmist.useless.network.BeefInvulnerabilitySyncPacket; +import com.sorrowmist.useless.network.BeefInvulnerabilityStatePacket; +import com.sorrowmist.useless.network.BeefToolLayoutSyncPacket; +import com.sorrowmist.useless.network.StaffLinkBindPacket; +import com.sorrowmist.useless.utils.UselessItemUtils; +import com.sorrowmist.useless.utils.mining.MiningDispatcher; +import com.sorrowmist.useless.world.dimension.UselessDimensionConfigManager; +import com.sorrowmist.useless.world.dimension.UselessDimensions; +import net.minecraft.core.BlockPos; +import net.minecraft.core.GlobalPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.Pose; +import net.minecraft.world.entity.ai.attributes.AttributeInstance; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.monster.warden.Warden; +import net.minecraft.world.entity.player.Abilities; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraftforge.eventbus.api.EventPriority; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod.EventBusSubscriber; +import net.minecraftforge.event.AddReloadListenerEvent; +import net.minecraftforge.event.entity.living.LivingDamageEvent; +import net.minecraftforge.event.entity.living.LivingDeathEvent; +import net.minecraftforge.event.entity.living.LivingDropsEvent; +import net.minecraftforge.event.entity.living.LivingAttackEvent; +import net.minecraftforge.event.entity.living.LivingChangeTargetEvent; +import net.minecraftforge.event.entity.living.MobEffectEvent; +import net.minecraftforge.event.entity.player.AttackEntityEvent; +import net.minecraftforge.event.entity.player.PlayerEvent; +import net.minecraftforge.event.entity.player.PlayerInteractEvent; +import net.minecraftforge.event.level.BlockEvent; +import net.minecraftforge.event.level.LevelEvent; +import net.minecraftforge.event.server.ServerStartedEvent; +import net.minecraftforge.event.server.ServerStoppedEvent; +import net.minecraftforge.event.TickEvent.PlayerTickEvent; +import net.minecraftforge.event.TickEvent.ServerTickEvent; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; +import net.minecraftforge.server.ServerLifecycleHooks; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +@EventBusSubscriber(modid = UselessMod.MODID) +public class EventHandler { + private static final Set BEEF_PROTECTED_PLAYERS = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private static final Set BEEF_ADVANCED_STEALTH_PLAYERS = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private static final Set CLIENT_BEEF_ADVANCED_STEALTH_ENTITY_IDS = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private static final Set RESTORING_BEEF_PROTECTED_PLAYERS = ConcurrentHashMap.newKeySet(); + + @SubscribeEvent(priority = EventPriority.HIGHEST) + public static void onLivingIncomingDamage(LivingAttackEvent event) { + if (event.getEntity() instanceof Player player && shouldApplyBeefInvulnerability(player)) { + event.setCanceled(true); + player.setHealth(player.getMaxHealth()); + player.clearFire(); + player.fallDistance = 0.0F; + } + } + + @SubscribeEvent(priority = EventPriority.HIGHEST) + public static void onLivingChangeTarget(LivingChangeTargetEvent event) { + if (event.getNewTarget() instanceof Player player && hasBeefAdvancedStealthItem(player)) { + event.setCanceled(true); + } + } + + @SubscribeEvent(priority = EventPriority.HIGHEST) + public static void onMobEffectApplicable(MobEffectEvent.Applicable event) { + if (event.getEntity() instanceof Player player + && hasBeefInvulnerabilityItem(player) + && isNonBeneficialEffect(event.getEffectInstance())) { + event.setResult(net.minecraftforge.eventbus.api.Event.Result.DENY); + } + } + + @SubscribeEvent(priority = EventPriority.HIGHEST) + public static void onLivingDamagePre(LivingDamageEvent event) { + if (event.getEntity() instanceof Player player && shouldApplyBeefInvulnerability(player)) { + event.setAmount(0.0F); + player.setHealth(player.getMaxHealth()); + player.clearFire(); + player.fallDistance = 0.0F; + } + } + + @SubscribeEvent(priority = EventPriority.HIGHEST) + public static void onLivingDeath(LivingDeathEvent event) { + if (event.getEntity() instanceof Player player && shouldApplyBeefInvulnerability(player)) { + event.setCanceled(true); + restoreBeefProtectedPlayer(player); + } + } + + @SubscribeEvent(priority = EventPriority.HIGHEST, receiveCanceled = true) + public static void onForceKillDeathObserved(LivingDeathEvent event) { + EndlessBeafItem.observeForceKillDeath(event); + } + + @SubscribeEvent(priority = EventPriority.LOWEST, receiveCanceled = true) + public static void onBeefToolLivingDeath(LivingDeathEvent event) { + if (EndlessBeafItem.handleForceKillDeath(event)) { + return; + } + if (!(event.getSource().getEntity() instanceof Player player)) { + return; + } + + UselessItemUtils.tryCaptureSpawnEgg(event.getEntity(), player.getMainHandItem(), player); + } + + @SubscribeEvent(priority = EventPriority.LOWEST, receiveCanceled = true) + public static void onBeefToolMagnetDeath(LivingDeathEvent event) { + if (EndlessBeafItem.handleForceKillMagnetDeath(event)) { + return; + } + if (event.isCanceled() || !(event.getEntity().level() instanceof ServerLevel level)) { + return; + } + if (!(event.getSource().getEntity() instanceof Player player)) { + return; + } + + ItemStack mainHandItem = player.getMainHandItem(); + if (mainHandItem.getItem() instanceof EndlessBeafItem) { + BeefMagnetHandler.onEntityKilled(level, player, mainHandItem, event.getEntity().position()); + } + } + + @SubscribeEvent(priority = EventPriority.HIGHEST) + public static void onAttackEntity(AttackEntityEvent event) { + if (event.getTarget() instanceof Player player && hasBeefAdvancedStealthItem(player)) { + event.setCanceled(true); + } + } + + @SubscribeEvent(receiveCanceled = true) + public static void onLivingDrops(LivingDropsEvent event) { + EndlessBeafItem.observeForceKillDrops(event); + if (event.getSource().getEntity() instanceof Player player) { + ItemStack mainHandItem = player.getMainHandItem(); + if (mainHandItem.getItem() instanceof EndlessBeafItem) { + UselessItemUtils.tryAddCognizantDustDrop(event, mainHandItem); + UselessItemUtils.onLivingDrops(event, mainHandItem, player); + UselessItemUtils.tryAddBeheadingDrop(event, mainHandItem); + } + } + } + + /** + * 造化杖的方块破坏入口。 + * + *

该监听器的优先级必须高于 architectury 转发 {@code BlockEvent.BREAK} 所用的 {@code HIGH}: + * FTB Ultimine 经该桥接注册连锁回调,其回调签名不接收事件对象,无法感知本模组的取消, + * 一旦其先行执行便会按自身流程再破坏同一批方块并投出原版掉落, + * 与本模组改写后的掉落并存,表现为连锁挖掘同一方块产出两份物品。

+ * + *

同时需要接收已被取消的事件:该场景下取消由本模组自身或其它连锁模组发出, + * 前者必须被忽略以免重复处理,后者则要求本模组放弃破坏以避免与对方争夺同一批方块。

+ */ + @SubscribeEvent(priority = EventPriority.HIGHEST, receiveCanceled = true) + public static void onBlockBreak(BlockEvent.BreakEvent event) { + // 事件已被取消说明该次破坏不由本模组结算:自行发出的取消会在这里被拦下以避免递归, + // 其它模组发出的取消同样在此让路,否则同一方块会被两套连锁逻辑各破坏一次。 + if (event.isCanceled()) { + return; + } + Player player = event.getPlayer(); + ItemStack mainHandItem = player.getMainHandItem(); + if (mainHandItem.getItem() instanceof EndlessBeafItem) { + MiningDispatcher.dispatchBreak(event, mainHandItem, player); + } + } + + /** + * 造化杖携带者的挖掘速度补偿:把原版「离地」与「水下」两处惩罚按实际大小乘回去, + * 使飞行 / 泡水时的挖掘手感与陆上一致,而非再额外放大。 + * + *

原版 {@code Player#getDigSpeed}(1.21.1 反编译确认)会对速度施加两处惩罚:

+ *
    + *
  • {@code !onGround()} ⇒ 速度 {@code / 5}(飞行与游泳都会命中)
  • + *
  • {@code isEyeInFluid(WATER)} ⇒ 速度 {@code × SUBMERGED_MINING_SPEED}(默认 0.2,即 {@code / 5})
  • + *
+ * + *

本事件在公式末尾触发,{@code getOriginalSpeed()} 已是惩罚后的值, + * 且 {@code setNewSpeed} 会整体替换返回值、原版后续不再修正, + * 所以此处只需把被乘进去的因子乘回来即可精确还原陆上速度。 + * 游泳时两处叠加为 {@code / 25},旧代码固定 {@code ×5} 只补得回其中一项, + * 表现为「游泳/潜水时比陆上慢 5 倍」。

+ * + *

关键:离地判定必须用 {@code !player.onGround()} 对齐原版,不能用 {@code isInWater()}。 + * 后者是「身体触碰水」({@code wasTouchingWater}),站在浅水里即为 true, + * 而那种场景原版只有离地 {@code /5} 惩罚;若照旧无条件 {@code ×5}, + * 就会在浅水中凭空加速。

+ */ + @SubscribeEvent + public static void onBreakSpeed(PlayerEvent.BreakSpeed event) { + Player player = event.getEntity(); + ItemStack mainHandItem = player.getMainHandItem(); + if (!(mainHandItem.getItem() instanceof EndlessBeafItem)) return; + + // 累积「原版实际乘进速度的惩罚因子」,默认 1.0 = 无惩罚、不干预 + float penalty = 1.0F; + + // 1) 离地惩罚:原版 `!onGround()` 时速度 /5 + if (!player.onGround()) { + penalty *= 5.0F; + } + + // 2) 水下惩罚:原版 `isEyeInFluid(WATER)` 时速度 × SUBMERGED_MINING_SPEED。 + // 取其属性实际值而非写死 0.2,兼容改过该属性的整合包/附魔。 + if (player.isEyeInFluid(FluidTags.WATER)) { + float submerged = net.minecraft.world.item.enchantment.EnchantmentHelper.hasAquaAffinity(player) ? 1F : 0.2F; + if (submerged > 0.0F) { + penalty /= submerged; // 默认 0.2 ⇒ ×5 + } + } + + if (penalty != 1.0F) { + event.setNewSpeed(event.getOriginalSpeed() * penalty); + } + } + + private static final float DEFAULT_FLYING_SPEED = 0.05F; + private static final float FLYING_SPEED_EPSILON = 1.0E-6F; + /** + * 游戏模式切换后,接下来这么多个 tick 内每 tick 都强制重发一次能力包。 + *

+ * 只补发一次是不够的:原版/MinecraftForge 在切换时重置 abilities,客户端收到 CHANGE_GAME_MODE 后 + * 还会通过 {@code MultiPlayerGameMode#setLocalMode} 在本地再重置一遍,而这次本地重置 + * 服务端完全不知情。留一个窗口反复对齐,才能盖住客户端那次重置。 + */ + private static final int FLIGHT_RESYNC_WINDOW = 20; + private static final Map FLIGHT_RESYNC_QUEUE = new ConcurrentHashMap<>(); + /** + * 「粘滞飞行」玩家:客户端自己上报正在飞行的造化杖携带者。 + *

+ * 只要玩家还处在这个状态,服务端的 {@code flying} 就不允许被清掉。原因是实测 Re-Avaritia 的 + * {@code committee.nova.mods.avaritia.init.handler.AbilityHandler#updateClientServerFlight} + * 会每秒 1~2 次把 {@code mayfly} / {@code flying} 一起清成 false 并回传客户端, + * 把 MinecraftForge 保留的悬停(创造→生存切换)和玩家自己的起飞都冲掉。 + *

+ * 开关来自 {@link #onClientFlightIntent}(客户端发的 {@code ServerboundPlayerAbilitiesPacket}): + * 玩家落地或空中双击关飞行时客户端会上报 false,粘滞随之解除, + * 所以落地行走、普通起跳、主动关飞行都不受影响;而被其它 mod 在服务端偷偷清掉的情况 + * 不经过那条上报路径,于是能被我们拉回来。 + *

+ * 注意不要用 {@code player.onGround()} 做落地判断:{@code handleMovePlayer} 里 + * {@code setOnGround(flag4)} 的 flag4 含 {@code !player.mayFly()},只要 mayfly 开着, + * 服务端 onGround 就恒为 false,粘滞将永远解除不掉。 + */ + private static final Set STICKY_FLIGHT = ConcurrentHashMap.newKeySet(); + + /** 客户端上报飞行意图时更新粘滞状态(由 {@code ServerGamePacketListenerImplFlightMixin} 调用)。 */ + public static void onClientFlightIntent(Player player, boolean flying) { + if (flying) { + STICKY_FLIGHT.add(player.getUUID()); + } else { + STICKY_FLIGHT.remove(player.getUUID()); + } + } + + @SubscribeEvent + public static void onPlayerChangeGameMode(PlayerEvent.PlayerChangeGameModeEvent event) { + if (event.getEntity() instanceof ServerPlayer serverPlayer) { + FLIGHT_RESYNC_QUEUE.put(serverPlayer.getUUID(), FLIGHT_RESYNC_WINDOW); + // 本事件在 changeGameModeForPlayer 之前触发,此刻读到的就是「切换前」的状态 + if (serverPlayer.getAbilities().flying) { + STICKY_FLIGHT.add(serverPlayer.getUUID()); + } + } + } + + /** + * 造化杖作为材料参与 3x3 工作台合成时,让本次产出物品变为不可破坏(无限耐久)。 + *

+ * 走事件而不是 mixin {@code ResultSlot}:整合包里的 FastWorkbench 用 + * {@code CraftResultSlotExt extends ResultSlot} 覆写了 {@code onTake},注入父类会被绕过; + * 而本事件的触发点 {@code ResultSlot#checkTakeAchievements -> EventHooks.firePlayerCraftingEvent} + * 被 FastWorkbench 保留,因此事件路径可靠。 + */ + @SubscribeEvent + public static void onItemCrafted(PlayerEvent.ItemCraftedEvent event) { + if (event.getEntity().level().isClientSide()) { + return; + } + BeefCraftingHandler.applyUnbreakableToCraftResult(event.getCrafting(), event.getInventory()); + } + + @SubscribeEvent + public static void onPlayerTick(PlayerTickEvent event) { + if (event.phase != net.minecraftforge.event.TickEvent.Phase.END) return; + Player player = event.player; + if (player.level().isClientSide()) return; + + // 飞行 / 无敌保护 / 高级潜行 / 杀戮光环都要看「身上有没有造化杖」。 + // 合并成一趟扫描,避免每 tick 把背包走三遍(详见 UselessItemUtils#scanStaff)。 + UselessItemUtils.StaffScan scan = UselessItemUtils.scanStaff(player); + + updateBeefToolFlight(player, scan.anyStaff()); + + updateBeefInvulnerability(player, false, scan); + + MiningDispatcher.tickCacheUpdate(player); + + // 杀戮光环:间隔可配(服务端配置),并按玩家自身的 tickCount 错开相位, + // 避免全服玩家在同一 tick 集中结算 + int killAuraInterval = ConfigManager.getBeefKillAuraInterval(); + if (killAuraInterval > 0 && player.tickCount % killAuraInterval == 0) { + EndlessBeafItem.tickKillAura(player, scan.killAuraStaff()); + } + } + + /** + * 游戏模式切换的瞬间把飞行状态刷成目标值。 + *

+ * 供 {@code ServerPlayerGameModeMixin} 调用。服务端在切模式时会先按新模式重置 abilities, + * 而紧接着 {@code ServerPlayer#setGameMode} 就会把这个状态发给客户端(能力包 A/C)。 + * 如果不在这里立刻补回去,客户端会先收到 {@code mayfly=false},要等下一个 tick 模组的补发 + * 才恢复 —— 中间 1 帧双击空格起不了飞。在这里补掉,客户端从头到尾看不到 false。 + */ + public static void applyBeefToolFlightNow(Player player) { + updateBeefToolFlight(player); + } + + /** + * 造化杖飞行状态维护。 + *

+ * 关键点:能力包只在「服务端状态发生变化」时才发是不够的——客户端那份 abilities 会被 + * {@link net.minecraft.client.multiplayer.MultiPlayerGameMode#setLocalMode} 单独重置, + * 服务端不知情也就永远不会补发。所以这里每 tick 把 abilities 对齐到目标值, + * 只要有任何一项不一致、或处在游戏模式切换后的重发窗口内,就重发一次能力包。 + */ + private static void updateBeefToolFlight(Player player) { + updateBeefToolFlight(player, UselessItemUtils.hasTargetToolInInventory(player)); + } + + /** + * @param hasTargetTool 玩家身上(主手 / 副手 / 快捷栏 / 主背包)是否有造化杖; + * 由 {@code onPlayerTick} 的合并扫描或本类的自扫提供 + */ + private static void updateBeefToolFlight(Player player, boolean hasTargetTool) { + boolean hasItemInInventory = ConfigManager.shouldEnableFlightEffect() && hasTargetTool; + + // 创造/旁观模式自带飞行,这份能力归原版管,模组不接管也不撤销。 + // 重发窗口故意保留到离开该模式之后才开始倒数,那才是真正需要补发能力包的时机。 + if (player.isCreative() || player.isSpectator()) { + if (hasItemInInventory) { + FlyEffectedHolder.add(player); + } else { + FlyEffectedHolder.remove(player); + } + return; + } + + UUID uuid = player.getUUID(); + Integer remaining = FLIGHT_RESYNC_QUEUE.get(uuid); + boolean resyncing = remaining != null && remaining > 0; + if (resyncing) { + if (remaining <= 1) { + FLIGHT_RESYNC_QUEUE.remove(uuid); + } else { + FLIGHT_RESYNC_QUEUE.put(uuid, remaining - 1); + } + } + + if (hasItemInInventory) { + FlyEffectedHolder.add(player); + float flightSpeed = (float) ConfigManager.getBeefToolFlightSpeed(); + + // 粘滞飞行(见 STICKY_FLIGHT 注释):只要客户端自己还报着「我在飞」, + // 就不允许 flying 被外力(Re-Avaritia 每秒一次)清掉。 + // 玩家落地 / 主动关飞行时客户端会上报 false,粘滞随之解除。 + boolean flying = player.getAbilities().flying || STICKY_FLIGHT.contains(uuid); + + applyFlightAbilities(player, true, flying, flightSpeed, resyncing); + return; + } + + // 没有造化杖时只回收模组自己授予过的飞行,避免抢走其它来源的飞行能力。 + // 归属标记落在玩家持久化数据里,所以「带着模组授予的飞行离线后重登」也能正确回收。 + STICKY_FLIGHT.remove(uuid); + if (FlyEffectedHolder.remove(player)) { + applyFlightAbilities(player, false, false, DEFAULT_FLYING_SPEED, true); + } + } + + private static void applyFlightAbilities(Player player, + boolean mayfly, + boolean flying, + float flyingSpeed, + boolean forceSync) { + Abilities abilities = player.getAbilities(); + boolean abilitiesChanged = false; + if (abilities.mayfly != mayfly) { + abilities.mayfly = mayfly; + abilitiesChanged = true; + } + if (abilities.flying != flying) { + abilities.flying = flying; + abilitiesChanged = true; + } + if (Math.abs(abilities.getFlyingSpeed() - flyingSpeed) > FLYING_SPEED_EPSILON) { + abilities.setFlyingSpeed(flyingSpeed); + abilitiesChanged = true; + } + if (abilitiesChanged || forceSync) { + player.onUpdateAbilities(); + } + } + + public static void updateBeefInvulnerability(Player player) { + updateBeefInvulnerability(player, false); + } + + public static void updateBeefInvulnerability(Player player, boolean forceSync) { + if (player.level().isClientSide()) { + return; + } + + migrateLegacyBeefInvulnerability(player); + + applyBeefInvulnerability(player, forceSync, UselessItemUtils.scanStaff(player)); + } + + /** + * 供 {@code onPlayerTick} 使用:复用同一次 {@link UselessItemUtils#scanStaff} 的结果, + * 避免每 tick 重复走背包。 + */ + static void updateBeefInvulnerability(Player player, boolean forceSync, UselessItemUtils.StaffScan scan) { + if (player.level().isClientSide()) { + return; + } + + migrateLegacyBeefInvulnerability(player); + + applyBeefInvulnerability(player, forceSync, scan); + } + + private static void applyBeefInvulnerability(Player player, boolean forceSync, + UselessItemUtils.StaffScan scan) { + boolean hasItemInInventory = scan.invulnerabilityEnabled(); + boolean hasAdvancedStealth = scan.advancedStealthEnabled(); + if (hasAdvancedStealth && !hasItemInInventory) { + // 这里会改写物品组件(把无敌开关打开),所以改完必须重新确认一次 + UselessItemUtils.enableInvulnerabilityForAdvancedStealth(player); + hasItemInInventory = UselessItemUtils.hasInvulnerabilityEnabledTargetToolInInventory(player); + } + UUID uuid = player.getUUID(); + + if (hasItemInInventory) { + boolean newlyTracked = BEEF_PROTECTED_PLAYERS.add(uuid); + claimBeefInvulnerability(player); + if (newlyTracked) { + clearBeefNegativeEffects(player); + } + } else { + BEEF_PROTECTED_PLAYERS.remove(uuid); + releaseBeefInvulnerability(player); + // 关掉保护的那一刻身上若还带着火,把它扑掉:源头拦截只在保护期间生效, + // 保护关闭后原版会继续把余火烧完,这里顺手清一次(每玩家仅一次,无每-tick 开销)。 + player.clearFire(); + } + + boolean newlyStealthTracked = hasAdvancedStealth && BEEF_ADVANCED_STEALTH_PLAYERS.add(uuid); + boolean stealthReleased = !hasAdvancedStealth && BEEF_ADVANCED_STEALTH_PLAYERS.remove(uuid); + if (newlyStealthTracked) { + clearBeefAdvancedStealthState(player); + } + if (player instanceof ServerPlayer serverPlayer + && (forceSync || newlyStealthTracked || stealthReleased || player.tickCount % 20 == 0)) { + PacketDistributor.sendToPlayersTrackingEntityAndSelf( + serverPlayer, + new BeefInvulnerabilityStatePacket(serverPlayer.getId(), hasAdvancedStealth) + ); + } + } + + private static void clearBeefNegativeEffects(Player player) { + player.getActiveEffects().stream() + .filter(EventHandler::isNonBeneficialEffect) + .map(MobEffectInstance::getEffect) + .toList() + .forEach(player::removeEffect); + } + + private static void clearBeefAdvancedStealthState(Player player) { + if (player.level() instanceof ServerLevel serverLevel) { + for (Entity entity : serverLevel.getAllEntities()) { + if (entity instanceof Warden warden + && (warden.getTarget() == player || warden.getEntityAngryAt().orElse(null) == player)) { + warden.setAttackTarget(null); + warden.clearAnger(player); + } else if (entity instanceof Mob mob && mob.getTarget() == player) { + mob.setTarget(null); + } + } + } + } + + static boolean isNonBeneficialEffect(MobEffectInstance effect) { + return !effect.getEffect().isBeneficial(); + } + + private static void migrateLegacyBeefInvulnerability(Player player) { + CompoundTag ownershipData = BeefInvulnerabilityOwnership.get(player); + if (BeefInvulnerabilityOwnership.isMigrationComplete(ownershipData)) { + return; + } + + // Preserve an active ownership cycle while upgrading its metadata. + if (BeefInvulnerabilityOwnership.isOwned(ownershipData)) { + BeefInvulnerabilityOwnership.markMigrationComplete(ownershipData); + return; + } + + // Defer migration until an affected survival/adventure player carries the tool. + if (player.isCreative() + || player.isSpectator() + || !UselessItemUtils.hasTargetToolInInventory(player)) { + return; + } + + // Old builds, including v1 ownership tracking, could leave this flag behind + // after protection had already been released. + if (player.isInvulnerable()) { + player.setInvulnerable(false); + } + BeefInvulnerabilityOwnership.markMigrationComplete(BeefInvulnerabilityOwnership.getOrCreate(player)); + } + + private static void claimBeefInvulnerability(Player player) { + CompoundTag ownershipData = BeefInvulnerabilityOwnership.getOrCreate(player); + AttributeInstance maxHealth = player.getAttribute(Attributes.MAX_HEALTH); + if (maxHealth != null) { + BeefInvulnerabilityOwnership.rememberMaxHealthBase(ownershipData, maxHealth.getBaseValue()); + } + BeefInvulnerabilityOwnership.claim(ownershipData, player.isInvulnerable()); + if (!player.isInvulnerable()) { + player.setInvulnerable(true); + } + } + + private static boolean releaseBeefInvulnerability(Player player) { + BeefInvulnerabilityOwnership.ReleaseResult result = + BeefInvulnerabilityOwnership.release(BeefInvulnerabilityOwnership.get(player)); + if (!result.owned()) { + return false; + } + + player.setInvulnerable(result.previousInvulnerable()); + return true; + } + + public static boolean shouldApplyBeefInvulnerability(Player player) { + return !player.level().isClientSide() && hasBeefInvulnerabilityItem(player); + } + + public static boolean isRestoringBeefProtectedPlayer(Player player) { + return RESTORING_BEEF_PROTECTED_PLAYERS.contains(player.getUUID()); + } + + public static boolean hasBeefInvulnerabilityItem(Player player) { + if (player.level().isClientSide()) { + return UselessItemUtils.hasInvulnerabilityEnabledTargetToolInInventory(player); + } + return BEEF_PROTECTED_PLAYERS.contains(player.getUUID()) + || UselessItemUtils.hasInvulnerabilityEnabledTargetToolInInventory(player); + } + + public static boolean hasBeefAdvancedStealthItem(Player player) { + if (player.level().isClientSide()) { + return CLIENT_BEEF_ADVANCED_STEALTH_ENTITY_IDS.contains(player.getId()) + || UselessItemUtils.hasAdvancedStealthEnabledTargetToolInInventory(player); + } + return BEEF_ADVANCED_STEALTH_PLAYERS.contains(player.getUUID()) + || UselessItemUtils.hasAdvancedStealthEnabledTargetToolInInventory(player); + } + + public static boolean shouldApplyBeefAdvancedStealth(Player player) { + return !player.level().isClientSide() && hasBeefAdvancedStealthItem(player); + } + + public static boolean hasAnyBeefAdvancedStealthPlayers() { + return !BEEF_ADVANCED_STEALTH_PLAYERS.isEmpty() + || !CLIENT_BEEF_ADVANCED_STEALTH_ENTITY_IDS.isEmpty(); + } + + public static void setClientBeefAdvancedStealthState(int entityId, boolean stealthEnabled) { + if (stealthEnabled) { + CLIENT_BEEF_ADVANCED_STEALTH_ENTITY_IDS.add(entityId); + return; + } + CLIENT_BEEF_ADVANCED_STEALTH_ENTITY_IDS.remove(entityId); + } + + public static void clearClientBeefAdvancedStealthStates() { + CLIENT_BEEF_ADVANCED_STEALTH_ENTITY_IDS.clear(); + } + + public static void restoreBeefProtectedPlayer(Player player) { + if (!RESTORING_BEEF_PROTECTED_PLAYERS.add(player.getUUID())) { + return; + } + + try { + if (!player.level().isClientSide()) { + BEEF_PROTECTED_PLAYERS.add(player.getUUID()); + migrateLegacyBeefInvulnerability(player); + claimBeefInvulnerability(player); + restoreCorruptedMaxHealth(player); + } + + float maxHealth = player.getMaxHealth(); + if (!Float.isFinite(maxHealth) || maxHealth <= 0.0F) { + restoreCorruptedMaxHealth(player); + maxHealth = player.getMaxHealth(); + } + if (!Float.isFinite(maxHealth) || maxHealth <= 0.0F) { + return; + } + + player.dead = false; + player.deathTime = 0; + player.hurtTime = 0; + player.hurtDuration = 0; + player.setHealth(maxHealth); + player.setPose(Pose.STANDING); + player.clearFire(); + player.fallDistance = 0.0F; + if (player instanceof ServerPlayer serverPlayer) { + PacketDistributor.sendToPlayersTrackingEntityAndSelf( + serverPlayer, + new BeefInvulnerabilityStatePacket( + serverPlayer.getId(), BEEF_ADVANCED_STEALTH_PLAYERS.contains(serverPlayer.getUUID())) + ); + PacketDistributor.sendToPlayersTrackingEntityAndSelf(serverPlayer, new BeefInvulnerabilitySyncPacket(serverPlayer.getId(), maxHealth)); + } + } finally { + RESTORING_BEEF_PROTECTED_PLAYERS.remove(player.getUUID()); + } + } + + private static void restoreCorruptedMaxHealth(Player player) { + AttributeInstance maxHealth = player.getAttribute(Attributes.MAX_HEALTH); + if (maxHealth == null) { + return; + } + + if (Double.isFinite(maxHealth.getBaseValue()) && maxHealth.getBaseValue() > 0.0D + && Float.isFinite(player.getMaxHealth()) && player.getMaxHealth() > 0.0F) { + return; + } + + maxHealth.setBaseValue(BeefInvulnerabilityOwnership.previousMaxHealthBase( + BeefInvulnerabilityOwnership.get(player))); + } + + @SubscribeEvent + public static void onPlayerLoggedIn(PlayerEvent.PlayerLoggedInEvent event) { + updateBeefInvulnerability(event.getEntity(), true); + if (event.getEntity() instanceof ServerPlayer player) { + syncAdvancedStealthPlayersTo(player); + GrassWandDropHandler.onPlayerLoggedIn(player); + // 连点开关已改为客户端会话级状态(BeefAutoClicker),登录时由客户端自行重置。 + // 连锁等价组是玩家个人设置,客户端要靠它做右键连锁的本地预测, + // 因此登录时就把整份布局下发一次(没有存档的玩家不下发,等价于无等价组)。 + BeefToolLayout layout = BeefToolLayoutManager.load(player); + if (layout != null) { + PacketDistributor.sendToPlayer(player, new BeefToolLayoutSyncPacket(layout.toJson())); + } + } + } + + @SubscribeEvent + public static void onPlayerRespawn(PlayerEvent.PlayerRespawnEvent event) { + updateBeefInvulnerability(event.getEntity(), true); + } + + @SubscribeEvent + public static void onPlayerChangedDimension(PlayerEvent.PlayerChangedDimensionEvent event) { + updateBeefInvulnerability(event.getEntity(), true); + } + + @SubscribeEvent + public static void onPlayerStartTracking(PlayerEvent.StartTracking event) { + if (!(event.getEntity() instanceof ServerPlayer trackingPlayer) + || !(event.getTarget() instanceof ServerPlayer target) + || !shouldApplyBeefAdvancedStealth(target)) { + return; + } + + sendAdvancedStealthState(trackingPlayer, target); + } + + private static void syncAdvancedStealthPlayersTo(ServerPlayer viewer) { + for (ServerPlayer target : viewer.serverLevel().players()) { + if (target != viewer && shouldApplyBeefAdvancedStealth(target)) { + sendAdvancedStealthState(viewer, target); + } + } + } + + private static void sendAdvancedStealthState(ServerPlayer viewer, ServerPlayer target) { + PacketDistributor.sendToPlayer( + viewer, + new BeefInvulnerabilityStatePacket(target.getId(), true) + ); + } + + @SubscribeEvent + public static void onLevelLoad(LevelEvent.Load event) { + if (event.getLevel() instanceof ServerLevel level + && UselessDimensions.isUselessDimension(level.dimension())) { + UselessDimensionConfigManager.apply(level); + } + } + + @SubscribeEvent + public static void onPlayerLoggedOut(PlayerEvent.PlayerLoggedOutEvent event) { + BEEF_PROTECTED_PLAYERS.remove(event.getEntity().getUUID()); + BEEF_ADVANCED_STEALTH_PLAYERS.remove(event.getEntity().getUUID()); + // 玩家在创造模式下离线时,飞行重发窗口不会开始倒数,故在此一并清除 + FLIGHT_RESYNC_QUEUE.remove(event.getEntity().getUUID()); + STICKY_FLIGHT.remove(event.getEntity().getUUID()); + if (event.getEntity() instanceof ServerPlayer player) { + GrassWandDropHandler.onPlayerLoggedOut(player); + } + } + + @SubscribeEvent(priority = EventPriority.HIGHEST) + public static void onBlockInteract(PlayerInteractEvent.RightClickBlock event) { + ItemStack stack = event.getItemStack(); + if (!(stack.getItem() instanceof EndlessBeafItem)) return; + if (event.isCanceled() || ConstructionWandLogic.isEnabled(stack)) return; + + Player player = event.getEntity(); + if (BeefTimeAcceleration.shouldBlockOtherRightClick(stack, player)) return; + if (!player.isShiftKeyDown()) return; + + Level world = event.getLevel(); + BlockPos pos = event.getPos(); + BlockEntity be = world.getBlockEntity(pos); + if (be == null) return; + + if (be instanceof MultiblockAlloyFurnaceCoreBlockEntity) { + if (!world.isClientSide && player instanceof ServerPlayer serverPlayer) { + OmniversalFurnaceAutoBuilder.Result result = + OmniversalFurnaceAutoBuilder.build(serverPlayer, stack, pos); + player.displayClientMessage(result.message(), true); + } + event.setCanceled(true); + event.setCancellationResult(InteractionResult.sidedSuccess(world.isClientSide)); + return; + } + + String className = be.getClass().getName(); + if (!className.contains("WirelessAccessPoint")) return; + + // 「Shift + 右键无线访问点 = 给 AE 连接模式定一个绑定目标」只在 AE 连接模式开启时成立。 + // + // 这里必须判模式,否则它会无条件抢走这次交互:无线物流模式同样用 Shift + 右键绑定容器, + // 而无线访问点本身就是合法的 AE 端点(现在也允许绑进物流网络)。两者撞在同一个手势上, + // 谁先跑取决于事件注册顺序——早先这里不判模式,于是「想绑访问点进物流网络」时, + // 事件被本方法吃掉并设成 AE 连接目标,onStaffLinkBind 看到 isCanceled 直接返回, + // 表现就是「Shift 右键访问点会把杖子连到该网络,按键冲突」。 + // + // 两个模式本身已经互斥(开一个会关掉另一个),所以判模式就足以把语义分开: + // AE 连接模式 → 设绑定目标;无线物流模式 → 落到 onStaffLinkBind 绑进物流网络。 + if (!com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.AeNetworkConnectComponent.get(), false)) return; + + if (!world.isClientSide) { + GlobalPos globalPos = GlobalPos.of(world.dimension(), pos); + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.WIRELESS_LINK_TARGET.get(), globalPos); + player.displayClientMessage(Component.translatable("gui.useless_mod.wireless_access_point_bound", pos.toShortString()), true); + } + // 取消事件,阻止方块本身的逻辑(如 AE2 的拆卸或旋转) + event.setCanceled(true); + // 设置结果,告知系统处理已成功,停止后续传播 + event.setCancellationResult(InteractionResult.sidedSuccess(world.isClientSide)); + } + + /** + * AE 连接模式:右键「能连入 AE 网络」的机器,把它的 AE 节点并入工具绑定的那张网。 + * + *

必须拦在 {@link PlayerInteractEvent.RightClickBlock} 这一层,而不是 {@code Item#useOn}: + * 右键 ME 设备默认会开方块 GUI,只有取消本次交互才拦得住。 + * 潜行时直接让位给既有的「Shift + 右键绑定无线访问点 / 自动装配合金炉」。

+ */ + @SubscribeEvent(priority = EventPriority.HIGHEST) + public static void onAeConnectInteract(PlayerInteractEvent.RightClickBlock event) { + if (event.isCanceled() || event.getEntity().isShiftKeyDown()) return; + + ItemStack stack = event.getItemStack(); + if (!(stack.getItem() instanceof EndlessBeafItem)) return; + if (!com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.AeNetworkConnectComponent.get(), false)) return; + + Level level = event.getLevel(); + BlockPos pos = event.getPos(); + // 目标不是 AE 节点宿主就不接管,交给原有的右键链路(收菜 / 工具动作等)。 + if (!AeDeviceLinker.isLinkTarget(level, pos)) return; + + if (level instanceof ServerLevel serverLevel && event.getEntity() instanceof ServerPlayer serverPlayer) { + AeDeviceLinker.toggle(serverLevel, serverPlayer, stack, pos); + } + event.setCanceled(true); + event.setCancellationResult(InteractionResult.sidedSuccess(level.isClientSide())); + } + + /** + * AE 连接的续命闹钟:AE2 不保存非空间网格连接,区块 / 存档重载后要把登记过的连接补回来。 + * 每 20 tick 跑一次,链接表为空时几乎零开销。 + */ + @SubscribeEvent + public static void onServerTick(ServerTickEvent event) { + if (event.phase != net.minecraftforge.event.TickEvent.Phase.END) return; + MinecraftServer server = event.getServer(); + if (server.getTickCount() % 20 != 0) return; + AeDeviceLinker.ensureLinks(server); + } + + /** + * 无线物流搬运引擎。 + * + *

单独一个订阅而不是并进 {@link #onServerTick}:那条路径有 20 tick 的闸门, + * 而线路的搬运周期最短是 1 tick。

+ */ + @SubscribeEvent + public static void onStaffLinkTick(ServerTickEvent event) { + if (event.phase != net.minecraftforge.event.TickEvent.Phase.END) return; + MinecraftServer server = event.getServer(); + StaffLinkEngine.tick(server); + // 应力的驱动认领必须每 tick 续期/清理:桥一旦停手,只有这里能把方块身上的虚拟转速摘掉。 + CreateStressCompatLoader.sweep(server); + // 界面上的可用应力 / 需求是给人看的数字,20 tick 一次足够。 + if (server.getTickCount() % 20 == 0) { + CreateStressCompatLoader.syncStatus(server); + } + } + + /** + * 无线物流模式:潜行右键容器方块,把它绑进/解绑出这把杖的物流网络。 + * + *

与 {@link #onBlockInteract} 分开实现:那个方法分支多且早返回,这里目标类型完全不同 + * (探测的是物品/流体/能量/化学品/魔源能力,无线访问点不具备这些)。

+ * + *

绑定动作由客户端发起。「按住 Ctrl 批量」这个修饰键状态只存在于客户端, + * 服务端在事件里读不到,所以两边分工:客户端判断按键并发包,服务端只负责取消原版交互 + * 与校验后执行。否则服务端会用自己的判断再绑一次,批量就变成了重复绑定。

+ */ + @SubscribeEvent(priority = EventPriority.HIGHEST) + public static void onStaffLinkBind(PlayerInteractEvent.RightClickBlock event) { + if (event.isCanceled()) return; + + ItemStack stack = event.getItemStack(); + if (!(stack.getItem() instanceof EndlessBeafItem)) return; + if (!EndlessBeafItem.isStaffLinkEnabled(stack)) return; + + Player player = event.getEntity(); + if (!player.isShiftKeyDown()) return; + + Level level = event.getLevel(); + BlockPos pos = event.getPos(); + + if (level.isClientSide()) { + // 客户端这一侧没有可靠的能力信息(方块实体未必已同步),所以只发坐标 + 修饰键, + // 「这个位置能不能绑」交给服务端用真世界判断。 + boolean batch = StaffLinkClientHooks.isBatchModifierDown(); + PacketDistributor.sendToServer(new StaffLinkBindPacket(pos, batch)); + event.setCanceled(true); + event.setCancellationResult(InteractionResult.SUCCESS); + return; + } + + // 服务端:只在确实可绑时才吞掉交互。否则玩家会发现「右键没反应」——那其实是 + // 一次本该正常工作的方块交互被我们吃掉了。 + if (!StaffLinkTargets.isBindable(level, pos)) return; + event.setCanceled(true); + event.setCancellationResult(InteractionResult.SUCCESS); + } + + /** + * 服务器启动时构建配方索引 + */ + @SubscribeEvent + public static void onServerStarted(ServerStartedEvent event) { + GrassWandDropHandler.clearCache(); + UselessDimensionConfigManager.applyAll(event.getServer()); + AlloyFurnaceRecipeManager.getInstance().buildIndex(event.getServer().overworld()); + // 目录必须在放行进入世界之前构建完毕:查询路径在目录缺失时读到空结果,会使模具识别、 + // 样板解析与 JEI 展示在进入世界初期不可用。此处同步构建,代价是启动阶段耗时增加。 + AlloyFurnaceRecipeCatalog.prewarm(event.getServer().overworld()); + event.getServer().getPlayerList().getPlayers().forEach(EndlessBeafItem::refreshAttackDamage); + } + + @SubscribeEvent + public static void onServerStopped(ServerStoppedEvent event) { + BEEF_PROTECTED_PLAYERS.clear(); + BEEF_ADVANCED_STEALTH_PLAYERS.clear(); + GrassWandDropHandler.clearCache(); + // 通道豁免索引里存的是网格节点引用,别把它们留到下一局。 + AeLinkChannelBypass.clear(); + // 无线物流的调度表按 tick 计数,同样不能跨局沿用。 + StaffLinkEngine.clearRuntimeState(); + // 应力的驱动认领里记着「哪个方块被驱动成了什么转速」,跨局必须清空。 + CreateStressCompatLoader.clearRuntimeState(); + // 连锁等价组的解析缓存按玩家 UUID 索引,别留到下一局。 + ChainGroupManager.clearAll(); + // 生物保护名单的解析缓存与客户端镜像同理。 + BeefToolProtectionManager.clearAll(); + } + + /** + * 数据重载时重建配方索引 + */ + @SubscribeEvent + public static void onAddReloadListener(AddReloadListenerEvent event) { + // 在配方数据重载后重建索引 + event.addListener((stage, resourceManager, preparationsProfiler, reloadProfiler, backgroundExecutor, gameExecutor) -> { + return stage.wait(Collections.emptyList()).thenRunAsync(() -> { + // 配方数据已变更:清空查找缓存并标记索引需要重建 + // 索引会在下一次 findRecipe 时延迟重建(此处无法直接获取 Level) + AlloyFurnaceRecipeManager.getInstance().clearCache(); + var server = ServerLifecycleHooks.getCurrentServer(); + if (server != null) { + AlloyFurnaceRecipeManager.getInstance().invalidateIndex(server.overworld()); + // 同步重建:目录未就绪期间查询路径会读到空结果,须在重载流程内构建完毕。 + AlloyFurnaceRecipeCatalog.prewarm(server.overworld()); + server.getPlayerList().getPlayers().forEach(EndlessBeafItem::refreshAttackDamage); + } else { + AlloyFurnaceRecipeManager.getInstance().invalidateIndex(); + } + }, gameExecutor); + }); + } +} diff --git a/src/main/java/com/sorrowmist/useless/event/GrassWandDropHandler.java b/src/main/java/com/sorrowmist/useless/event/GrassWandDropHandler.java new file mode 100644 index 00000000..970609ca --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/event/GrassWandDropHandler.java @@ -0,0 +1,90 @@ +package com.sorrowmist.useless.event; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.api.enums.tool.EnchantMode; +import com.sorrowmist.useless.content.items.EndlessBeafItem; +import com.sorrowmist.useless.core.component.UComponents; +import com.sorrowmist.useless.core.config.ConfigManager; +import com.sorrowmist.useless.init.ModItems; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraftforge.fml.common.Mod.EventBusSubscriber; + +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +@EventBusSubscriber(modid = UselessMod.MODID) +public final class GrassWandDropHandler { + private static final ResourceLocation ADVANCEMENT_ID = UselessMod.id("grass_wand_drop"); + private static final String ADVANCEMENT_CRITERION = "main"; + private static final Set COMPLETED_PLAYERS = ConcurrentHashMap.newKeySet(); + + private GrassWandDropHandler() {} + + public static void onPlayerLoggedIn(ServerPlayer player) { + if (hasCompletedAdvancement(player)) { + COMPLETED_PLAYERS.add(player.getUUID()); + } else { + COMPLETED_PLAYERS.remove(player.getUUID()); + } + } + + public static void onPlayerLoggedOut(ServerPlayer player) { + COMPLETED_PLAYERS.remove(player.getUUID()); + } + + public static void clearCache() { + COMPLETED_PLAYERS.clear(); + } + + public static void onBlockDestroyed(net.minecraft.world.level.Level level, + net.minecraft.world.entity.player.Player breaker, net.minecraft.core.BlockPos pos, BlockState state) { + if (!(level instanceof net.minecraft.server.level.ServerLevel) + || !(breaker instanceof ServerPlayer player) + || COMPLETED_PLAYERS.contains(player.getUUID())) { + return; + } + if (player.isCreative() + || !isGrass(state) + || !ConfigManager.shouldEnableGrassWandDrop() + || level.getRandom().nextDouble() >= ConfigManager.getGrassWandDropProbability()) { + return; + } + + net.minecraft.advancements.Advancement advancement = getAdvancement(player); + if (advancement == null || !player.getAdvancements().award(advancement, ADVANCEMENT_CRITERION)) { + if (advancement != null && hasCompletedAdvancement(player)) { + COMPLETED_PLAYERS.add(player.getUUID()); + } + return; + } + + COMPLETED_PLAYERS.add(player.getUUID()); + + ItemStack wand = new ItemStack(ModItems.ENDLESS_BEAF_ITEM.get()); + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(wand, UComponents.EnchantModeComponent.get(), EnchantMode.FORTUNE); + wand.getOrCreateTag().putInt("CustomModelData", 0); + EndlessBeafItem.refreshEnchantments(wand, level); + level.addFreshEntity(new ItemEntity( + level, pos.getX() + 0.5D, pos.getY() + 0.5D, pos.getZ() + 0.5D, wand)); + } + + private static boolean isGrass(BlockState state) { + return state.is(Blocks.GRASS) || state.is(Blocks.TALL_GRASS); + } + + private static net.minecraft.advancements.Advancement getAdvancement(ServerPlayer player) { + return player.getServer().getAdvancements().getAdvancement(ADVANCEMENT_ID); + } + + private static boolean hasCompletedAdvancement(ServerPlayer player) { + net.minecraft.advancements.Advancement advancement = getAdvancement(player); + return advancement != null + && player.getAdvancements().getOrStartProgress(advancement).isDone(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/event/client/BeefToolFlightClient.java b/src/main/java/com/sorrowmist/useless/event/client/BeefToolFlightClient.java new file mode 100644 index 00000000..b72773f4 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/event/client/BeefToolFlightClient.java @@ -0,0 +1,68 @@ +package com.sorrowmist.useless.event.client; + +import com.sorrowmist.useless.utils.UselessItemUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.entity.player.Abilities; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod.EventBusSubscriber; +import net.minecraftforge.event.TickEvent.ClientTickEvent; + +/** + * 造化杖飞行的客户端侧兜底与状态记录。 + *

+ * 客户端那份 {@code abilities} 会被原版与其它 mod 改动(详见 + * {@code LocalPlayerMixin} 与 {@code MultiPlayerGameModeMixin} 的说明),这里做两件事: + *

    + *
  1. 记录「服务端确实授予过飞行能力」,作为外部清除拦截与本地兜底的闸门;
  2. + *
  3. 每 tick 兜一次 {@code mayfly}:带杖且服务端给过飞行时不允许它是 false。
  4. + *
+ * 放在 {@link ClientTickEvent.Pre}({@code Minecraft#tick()} 的头部、早于玩家 {@code aiStep}), + * 这样补回来的值当帧就能用于「双击起飞」的判定。 + */ +@EventBusSubscriber(Dist.CLIENT) +public final class BeefToolFlightClient { + /** + * 服务端是否真的授予过飞行能力(收到过 {@code canFly=true} 的能力包)。 + *

+ * 用来给外部清除拦截加闸:如果服务端把 {@code enable_flight_effect} 关掉了,它永远不会发 + * {@code canFly=true},客户端 {@code mayfly} 本来就该是 false —— 此时若还去拦截/兜底, + * 会把客户端强行设成「能飞」而服务端不认,反而可能被反作弊判定为 floating 踢出。 + * 所以只有服务端确认给过,才允许动作。 + */ + private static boolean serverGrantedFlight; + + private BeefToolFlightClient() {} + + /** 收到服务端能力包时更新「服务端给过飞行」标记(由 {@code ClientPacketListenerMixin} 调用)。 */ + public static void onServerAbilities(boolean mayfly) { + if (mayfly) { + serverGrantedFlight = true; + } + } + + public static boolean serverGrantedFlight() { + return serverGrantedFlight; + } + + @SubscribeEvent + public static void onClientTick(ClientTickEvent event) { + if (event.phase != net.minecraftforge.event.TickEvent.Phase.START) return; + LocalPlayer player = Minecraft.getInstance().player; + if (player == null) { + serverGrantedFlight = false; + return; + } + + Abilities abilities = player.getAbilities(); + // 带杖、服务端给过飞行,但本地 mayfly 是 false:被外部 mod(Re-Avaritia)清掉了。 + // 它紧接着的上报会被 LocalPlayerMixin 拦下,但万一存在不上报的清除路径,客户端就会一直 + // 停在「带杖却不能飞」——服务端状态没变、不会主动补发。这里兜住。 + if (!abilities.mayfly + && serverGrantedFlight + && UselessItemUtils.hasTargetToolInInventory(player)) { + abilities.mayfly = true; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/event/client/ClientEventBusSubscriber.java b/src/main/java/com/sorrowmist/useless/event/client/ClientEventBusSubscriber.java new file mode 100644 index 00000000..0af9373a --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/event/client/ClientEventBusSubscriber.java @@ -0,0 +1,506 @@ +package com.sorrowmist.useless.event.client; + +import com.mojang.blaze3d.platform.InputConstants; +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.api.enums.tool.EnchantMode; +import com.sorrowmist.useless.client.BeefAutoClicker; +import com.sorrowmist.useless.client.gui.MiningStatusGui; +import com.sorrowmist.useless.client.gui.ThermostatScreen; +import com.sorrowmist.useless.content.blockentities.multiblock.MultiblockAlloyFurnaceCoreBlockEntity; +import com.sorrowmist.useless.content.blocks.TemperatureRegulatorBlock; +import com.sorrowmist.useless.content.blocks.TeleportPadBlock; +import com.sorrowmist.useless.content.items.BeefTimeAcceleration; +import com.sorrowmist.useless.content.items.EndlessBeafItem; +import com.sorrowmist.useless.core.common.KeyBindings; +import com.sorrowmist.useless.core.component.UComponents; +import com.sorrowmist.useless.core.config.BeefToolProtectionManager; +import com.sorrowmist.useless.event.EventHandler; +import com.sorrowmist.useless.network.EnchantmentSwitchPacket; +import com.sorrowmist.useless.network.ForceBreakKeyPacket; +import com.sorrowmist.useless.network.ModeTogglePacket; +import com.sorrowmist.useless.network.ProtectEntityPacket; +import com.sorrowmist.useless.network.ShapeSwitchPacket; +import com.sorrowmist.useless.network.StaffLinkCyclePacket; +import com.sorrowmist.useless.network.StaffLinkOpenPacket; +import com.sorrowmist.useless.network.TabKeyPressedPacket; +import com.sorrowmist.useless.network.TeleportKeyPacket; +import com.sorrowmist.useless.utils.UselessItemUtils; +import net.minecraft.client.KeyMapping; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.EntityHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.eventbus.api.EventPriority; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod.EventBusSubscriber; +import net.minecraftforge.client.event.ClientPlayerNetworkEvent; +import net.minecraftforge.event.TickEvent.ClientTickEvent; +import net.minecraftforge.client.event.InputEvent; +import net.minecraftforge.client.event.RegisterGuiOverlaysEvent; +import net.minecraftforge.client.event.RegisterKeyMappingsEvent; +import net.minecraftforge.client.gui.overlay.VanillaGuiOverlay; +import net.minecraftforge.event.entity.EntityLeaveLevelEvent; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; +import org.lwjgl.glfw.GLFW; + +@EventBusSubscriber(modid = UselessMod.MODID, value = Dist.CLIENT) +public class ClientEventBusSubscriber { + // 跟踪Tab键的前一状态 + private static boolean lastTabPressed = false; + + + @SubscribeEvent + public static void onKeyInput(ClientTickEvent event) { + if (event.phase != net.minecraftforge.event.TickEvent.Phase.END) return; + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + if (player == null) return; + + if (mc.screen != null) return; + + // 无线物流:打开配置界面(仅手持造化杖时) + if (KeyBindings.OPEN_WIRELESS_LOGISTICS_KEY.get().consumeClick()) { + ItemStack mainHandItem = player.getMainHandItem(); + if (mainHandItem.getItem() instanceof EndlessBeafItem) { + PacketDistributor.sendToServer(new StaffLinkOpenPacket()); + } + } + + // 检测连锁键状态变化。Tab 键与 FTB 的连锁键并列,任一按下即视为连锁激活; + // 后者直接读取 FTB 自己的绑定,不在此处另行注册。 + boolean currentTabPressed = KeyBindings.isChainMiningKeyDown(); + if (currentTabPressed != lastTabPressed) { + PacketDistributor.sendToServer(new TabKeyPressedPacket(currentTabPressed)); + lastTabPressed = currentTabPressed; + } + + if (KeyBindings.SWITCH_FORTUNE_KEY.get().consumeClick()) { + ItemStack mainHandItem = player.getMainHandItem(); + if (mainHandItem.getItem() instanceof EndlessBeafItem) { + PacketDistributor.sendToServer( + new EnchantmentSwitchPacket(EnchantMode.FORTUNE)); + } + } + + if (KeyBindings.SWITCH_SILK_TOUCH_KEY.get().consumeClick()) { + ItemStack mainHandItem = player.getMainHandItem(); + if (mainHandItem.getItem() instanceof EndlessBeafItem) { + PacketDistributor.sendToServer( + new EnchantmentSwitchPacket(EnchantMode.SILK_TOUCH)); + } + } + + if (KeyBindings.TOGGLE_CHAIN_MODE_KEY.get().consumeClick()) { + ItemStack mainHandItem = player.getMainHandItem(); + if (mainHandItem.getItem() instanceof EndlessBeafItem) { + // 切换连锁挖矿模式 + boolean currentEnabled = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(mainHandItem, UComponents.EnhancedChainMiningComponent.get(), + false + ); + PacketDistributor.sendToServer( + new ModeTogglePacket(ModeTogglePacket.ModeType.CHAIN_MINING, !currentEnabled)); + } + } + + if (KeyBindings.SWITCH_FORCE_MINING_KEY.get().consumeClick()) { + ItemStack mainHandItem = player.getMainHandItem(); + if (mainHandItem.getItem() instanceof EndlessBeafItem) { + // 切换强制挖掘模式 + boolean currentEnabled = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(mainHandItem, UComponents.ForceMiningComponent.get(), false); + PacketDistributor.sendToServer( + new ModeTogglePacket(ModeTogglePacket.ModeType.FORCE_MINING, !currentEnabled)); + } + } + + if (KeyBindings.SWITCH_FARMLAND_MODE_KEY.get().consumeClick()) { + ItemStack mainHandItem = player.getMainHandItem(); + if (mainHandItem.getItem() instanceof EndlessBeafItem) { + // 切换右键泥土的结果:草径(铲子) <-> 耕地(锄头) + boolean currentFarmland = EndlessBeafItem.isFarmlandMode(mainHandItem); + PacketDistributor.sendToServer( + new ModeTogglePacket(ModeTogglePacket.ModeType.BEEF_FARMLAND_MODE, !currentFarmland)); + } + } + + if (KeyBindings.TOGGLE_CROP_HARVEST_KEY.get().consumeClick()) { + ItemStack mainHandItem = player.getMainHandItem(); + if (mainHandItem.getItem() instanceof EndlessBeafItem) { + // 切换「顺手收菜」(右键成熟作物:收获并保留种子于耕地) + boolean currentHarvest = EndlessBeafItem.isCropHarvestEnabled(mainHandItem); + PacketDistributor.sendToServer( + new ModeTogglePacket(ModeTogglePacket.ModeType.BEEF_CROP_HARVEST, !currentHarvest)); + } + } + + if (KeyBindings.TOGGLE_SHEARS_KEY.get().consumeClick()) { + ItemStack mainHandItem = player.getMainHandItem(); + if (mainHandItem.getItem() instanceof EndlessBeafItem) { + // 切换剪刀功能(剪羊毛 / 剪掉落,并对外声明剪刀能力) + boolean currentShears = EndlessBeafItem.isShearsEnabled(mainHandItem); + PacketDistributor.sendToServer( + new ModeTogglePacket(ModeTogglePacket.ModeType.BEEF_SHEARS, !currentShears)); + } + } + + if (KeyBindings.TOGGLE_FLINT_AND_STEEL_KEY.get().consumeClick()) { + ItemStack mainHandItem = player.getMainHandItem(); + if (mainHandItem.getItem() instanceof EndlessBeafItem) { + // 切换打火石功能(点亮营火/蜡烛,或在点击面点火) + boolean currentFlintAndSteel = EndlessBeafItem.isFlintAndSteelEnabled(mainHandItem); + PacketDistributor.sendToServer( + new ModeTogglePacket(ModeTogglePacket.ModeType.BEEF_FLINT_AND_STEEL, + !currentFlintAndSteel)); + } + } + + if (KeyBindings.TOGGLE_AUTO_CLICK_KEY.get().consumeClick()) { + // 连点开关是客户端会话级状态,不绑定物品:空手 / 任意物品下都能开关, + // 也不需要发包(真正的连点循环纯在客户端跑)。 + BeefAutoClicker.toggle(); + } + + if (KeyBindings.TOGGLE_KILL_AURA_KEY.get().consumeClick()) { + // 主手 / 副手 / 快捷栏 / 背包里任意一把杖都能开关,与服务端 findKillAuraToggleTarget 一致 + ItemStack staff = UselessItemUtils.findKillAuraToggleTarget(player); + if (!staff.isEmpty()) { + boolean currentAura = EndlessBeafItem.isKillAuraEnabled(staff); + PacketDistributor.sendToServer( + new ModeTogglePacket(ModeTogglePacket.ModeType.BEEF_KILL_AURA, !currentAura)); + } + } + + // 检测R键按下(触发强制破坏) + if (KeyBindings.TRIGGER_FORCE_MINING_KEY.get().consumeClick()) { + ItemStack mainHandItem = player.getMainHandItem(); + if (mainHandItem.getItem() instanceof EndlessBeafItem + && com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(mainHandItem, UComponents.ForceMiningComponent.get(), false)) { + // R键按下,发送强制破坏请求,同时传入当前Tab键状态 + boolean tabPressed = KeyBindings.isChainMiningKeyDown(); + PacketDistributor.sendToServer(new ForceBreakKeyPacket(tabPressed)); + } + } + + // 连点模式:开关为客户端会话级状态(BeefAutoClicker),开启后按配置速率重复触发右键 + BeefAutoClicker.tick(mc); + } + + /** + * Shift + 滚轮切换连锁形状 / 无线物流网络。 + * + *

只在「手持造化杖 + 没开任何界面」时才吃掉这次滚动,免得抢走其它模组的 Shift 滚轮用法 + * (物品栏滚动之类)。

+ * + *

优先级必须高于 FTB Ultimine:FTB 通过 Architectury 的 + * {@code ClientRawInputEvent.MOUSE_SCROLLED} 注册了同一个 MinecraftForge 鼠标滚动事件,并在 + * 「连锁键按下 + Shift + 滚轮」时以 {@code EventResult.interruptFalse()} 取消该事件。若本方法 + * 用默认优先级,事件已被 FTB 取消,形状切换包永远不会发出,表现为「按住连锁键滚轮毫无反应」。 + * 同理,形状分支不能依赖 {@code event.isCanceled()}:该标志在 FTB 先行处理时必然为真, + * 据此提前返回等于把功能再次关掉。

+ */ + @SubscribeEvent(priority = EventPriority.HIGHEST) + public static void onMouseScroll(InputEvent.MouseScrollingEvent event) { + if (event.getScrollDelta() == 0.0) return; + + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + if (player == null || mc.screen != null) return; + if (!player.isShiftKeyDown()) return; + + ItemStack mainHandItem = player.getMainHandItem(); + if (!(mainHandItem.getItem() instanceof EndlessBeafItem)) return; + + int delta = event.getScrollDelta() > 0.0 ? 1 : -1; + + // 按住连锁键时 Shift + 滚轮切换连锁形状。该分支先于无线物流判定: + // 两者虽然共用 Shift + 滚轮,但触发前提互斥(连锁键按下 / 未按下),不会同时命中。 + if (KeyBindings.isChainMiningKeyDown()) { + event.setCanceled(true); + PacketDistributor.sendToServer(new ShapeSwitchPacket(delta)); + return; + } + + // 无线物流分支仍避开已被其它模组处理的滚动,保持原有克制。 + if (event.isCanceled()) return; + + if (!EndlessBeafItem.isStaffLinkEnabled(mainHandItem)) return; + + event.setCanceled(true); + PacketDistributor.sendToServer(new StaffLinkCyclePacket(delta)); + } + + /** + * 短距传送的鼠标触发入口。 + * + *

该绑定挂在恒为非激活的冲突上下文上,不参与按键分发,因此按下时原版 keyUse + * 仍能取得点击,方块交互不受影响;此处只上报传送请求,不取消该事件。 + * 是否触发取决于绑定自身配置的主键与修饰键,玩家在按键设置中的改动即时生效。

+ */ + @SubscribeEvent + public static void onMouseButtonPre(InputEvent.MouseButton.Pre event) { + Minecraft mc = Minecraft.getInstance(); + // 保护手势必须最先判定:命中即取消并 return,短距传送分支不会执行。 + // 两者共用 Shift + 右键,命中生物时约定「保护优先」。 + if (tryProtectGesture(mc, event)) return; + // 权杖 Alt + 右键塑料方块:打开恒温配置界面。它同样要取消事件,否则原版右键照发。 + if (tryThermostatGesture(mc, event)) return; + + if (event.getAction() != GLFW.GLFW_PRESS) return; + + LocalPlayer player = mc.player; + if (player == null || mc.screen != null) return; + + KeyMapping mapping = KeyBindings.SHORT_TELEPORT_KEY.get(); + InputConstants.Key pressedKey = InputConstants.Type.MOUSE.getOrCreate(event.getButton()); + if (!pressedKey.equals(mapping.getKey())) return; + if (!mapping.getKeyModifier().isActive(mapping.getKeyConflictContext())) return; + + tryTriggerShortTeleport(mc, player, true); + } + + /** + * 短距传送的键盘触发入口。 + * + *

该绑定不参与 {@code KeyMapping} 分发,触发判定只能由原始输入事件完成。 + * 若仅保留鼠标分支,玩家把主键改绑为键盘按键后不会产生任何触发:鼠标事件 + * 携带的按键恒为鼠标键,与绑定主键不相等。此处按物理按下边沿补上键盘分支, + * 与鼠标分支共用同一套前置校验。键盘主键不与方块交互共用同一次点击, + * 故不让位于准星命中的方块。

+ */ + @SubscribeEvent + public static void onKeyInputEvent(InputEvent.Key event) { + if (event.getAction() != GLFW.GLFW_PRESS) return; + if (event.getKey() == GLFW.GLFW_KEY_UNKNOWN) return; + + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + if (player == null || mc.screen != null) return; + + KeyMapping mapping = KeyBindings.SHORT_TELEPORT_KEY.get(); + InputConstants.Key pressedKey = InputConstants.Type.KEYSYM.getOrCreate(event.getKey()); + if (!pressedKey.equals(mapping.getKey())) return; + if (!mapping.getKeyModifier().isActive(mapping.getKeyConflictContext())) return; + + tryTriggerShortTeleport(mc, player, false); + } + + /** + * 校验短距闪现的前置条件并上报按键包。 + * + * @param respectBlockPriority 是否让位于共用同一次点击的方块交互与时间加速; + * 鼠标右键需要让位,键盘主键不需要 + * @return true 表示本次按键已上报 + */ + private static boolean tryTriggerShortTeleport(Minecraft mc, LocalPlayer player, boolean respectBlockPriority) { + ItemStack mainHandItem = player.getMainHandItem(); + if (!(mainHandItem.getItem() instanceof EndlessBeafItem)) return false; + if (!EndlessBeafItem.isTeleportEnabled(mainHandItem)) return false; + + if (respectBlockPriority) { + // 时间加速同样绑定 Shift + 右键,两者不可同时触发:启用时交由方块交互处理。 + if (BeefTimeAcceleration.shouldBlockOtherRightClick(mainHandItem, player)) return false; + // 仅当准星命中的方块自身要独占该组合键时让位,普通方块不拦截闪现。 + if (isBlockInteractionPriority(mc)) return false; + } + + PacketDistributor.sendToServer(new TeleportKeyPacket()); + return true; + } + + /** + * Ctrl / Shift + 右键生物:切换生物保护名单。 + * + *

为什么用 {@code MouseButton.Pre} 而不是 {@code PlayerInteractEvent.EntityInteract}: + *

    + *
  • Ctrl 不会被 {@code ServerboundInteractPacket} 传输(它只带 {@code isShiftKeyDown()}), + * 服务端根本区分不出 Ctrl 与普通右键,只能用 {@code getModifiers()} 在客户端判断;
  • + *
  • 取消本事件发生在 {@code KeyMapping.click} 之前,{@code keyUse} 从不被按下, + * 于是 {@code startUseItem()} 不会被调用 —— 交互包不会发出,村民交易 GUI、 + * 剪羊毛等原版行为都不会发生;
  • + *
  • GLFW 鼠标按键没有键盘那样的 repeat,每次物理按下只触发一次,因此不需要去抖。
  • + *
+ * + * @return true 表示本次右键已被保护手势消费,调用方应立即 return + */ + private static boolean tryProtectGesture(Minecraft mc, InputEvent.MouseButton.Pre event) { + if (event.getAction() != GLFW.GLFW_PRESS) return false; + if (event.getButton() != GLFW.GLFW_MOUSE_BUTTON_RIGHT) return false; + + LocalPlayer player = mc.player; + if (player == null || mc.level == null || mc.screen != null || mc.getOverlay() != null) return false; + + // Ctrl 优先:两个修饰键同时按下时按「种类」处理 + boolean byType = (event.getModifiers() & InputConstants.MOD_CONTROL) != 0; + boolean byEntity = !byType && player.isShiftKeyDown(); + if (!byType && !byEntity) return false; + + // 必须手持造化杖(主手优先,其次副手) + ItemStack staff = player.getMainHandItem(); + if (!(staff.getItem() instanceof EndlessBeafItem)) { + staff = player.getOffhandItem(); + if (!(staff.getItem() instanceof EndlessBeafItem)) return false; + } + // 杀戮光环、范围伤害或保护名单模式至少开一个,否则完全让给原版交互 + if (!EndlessBeafItem.isProtectGestureAvailable(staff)) return false; + + // 「生物加速」模式开启时,Shift+右键生物让给生物加速(与闪现键让位同款)。 + // 用 byEntity 判定:Ctrl 按种类的手势不受影响。漏了这一步,生物加速永远不会触发—— + // 本事件被取消发生在 KeyMapping.click 之前,keyUse 不按下,交互包根本不会发出。 + if (byEntity && EndlessBeafItem.isEntityTimeAccelerationEnabled(staff)) return false; + + // 用当前帧重算准星命中,避免用到上一 tick 的旧 hitResult + mc.gameRenderer.pick(1.0F); + if (!(mc.hitResult instanceof EntityHitResult hit)) return false; + + Entity target = EndlessBeafItem.unwrapPartEntity(hit.getEntity()); + if (!(target instanceof LivingEntity) || target == player) return false; + + event.setCanceled(true); + PacketDistributor.sendToServer(new ProtectEntityPacket(target.getId(), byType)); + return true; + } + + /** + * 权杖 Alt + 右键 温度调节器方块:打开恒温源配置界面。 + * + *

Alt 判定用 GLFW 位域而不是 {@code InputConstants}。 本版本的 + * {@code InputConstants} 只有 {@code MOD_CONTROL},没有 {@code MOD_ALT},所以直接读 + * {@link InputEvent.MouseButton.Pre#getModifiers()} 里的 {@code GLFW_MOD_ALT}。

+ * + *

取消本事件发生在 {@code KeyMapping.click} 之前,{@code keyUse} 从不被按下, + * 于是原版右键交互包不会发出——权杖的 {@code useOn} 链、方块自身的 {@code useItemOn}、 + * 以及 {@code UselessMod#onRightClickBlock} 都不会跑。这与保护手势同一套机制。

+ * + * @return true 表示本次右键已被恒温手势消费,调用方应立即 return + */ + private static boolean tryThermostatGesture(Minecraft mc, InputEvent.MouseButton.Pre event) { + if (event.getAction() != GLFW.GLFW_PRESS) return false; + if (event.getButton() != GLFW.GLFW_MOUSE_BUTTON_RIGHT) return false; + if ((event.getModifiers() & GLFW.GLFW_MOD_ALT) == 0) return false; + + LocalPlayer player = mc.player; + if (player == null || mc.level == null || mc.screen != null || mc.getOverlay() != null) return false; + + // 必须手持造化杖(主手优先,其次副手),与保护手势同一把尺子。 + ItemStack staff = player.getMainHandItem(); + if (!(staff.getItem() instanceof EndlessBeafItem)) { + staff = player.getOffhandItem(); + if (!(staff.getItem() instanceof EndlessBeafItem)) return false; + } + + // 用当前帧重算准星命中,避免用到上一 tick 的旧 hitResult。 + mc.gameRenderer.pick(1.0F); + if (!(mc.hitResult instanceof BlockHitResult hit)) return false; + if (!(mc.level.getBlockState(hit.getBlockPos()).getBlock() instanceof TemperatureRegulatorBlock)) return false; + + event.setCanceled(true); + mc.setScreen(new ThermostatScreen(hit.getBlockPos())); + return true; + } + + /** + * 判断准星命中的方块是否要独占该组合键。 + * + *

仅三类既有交互需要让位:维度传送方块(潜行右键编辑配置)、合金炉核心 + * (潜行右键自动搭建)与无线接入点(潜行右键绑定目标)。其余方块不消费该组合键, + * 闪现照常执行。未命中方块时按未命中处理。

+ */ + private static boolean isBlockInteractionPriority(Minecraft mc) { + Level level = mc.level; + if (level == null) return false; + if (!(mc.hitResult instanceof BlockHitResult hit) + || hit.getType() != HitResult.Type.BLOCK) { + return false; + } + BlockPos pos = hit.getBlockPos(); + if (level.getBlockState(pos).getBlock() instanceof TeleportPadBlock) { + return true; + } + BlockEntity blockEntity = level.getBlockEntity(pos); + if (blockEntity instanceof MultiblockAlloyFurnaceCoreBlockEntity) { + return true; + } + return blockEntity != null + && blockEntity.getClass().getName().contains("WirelessAccessPoint"); + } + + + @SubscribeEvent + public static void onClientLoggingIn(ClientPlayerNetworkEvent.LoggingIn event) { + lastTabPressed = false; + // 连点开关是客户端会话级状态,登录时统一置关,避免跨会话残留 + BeefAutoClicker.reset(); + } + + @SubscribeEvent + public static void onClientLoggingOut(ClientPlayerNetworkEvent.LoggingOut event) { + clearClientBeefProtectionState(); + } + + @SubscribeEvent + public static void onClientPlayerClone(ClientPlayerNetworkEvent.Clone event) { + lastTabPressed = false; + } + + @SubscribeEvent + public static void onEntityLeaveLevel(EntityLeaveLevelEvent event) { + if (event.getLevel().isClientSide() && event.getEntity() instanceof net.minecraft.world.entity.player.Player player) { + EventHandler.setClientBeefAdvancedStealthState(player.getId(), false); + } + } + + private static void clearClientBeefProtectionState() { + EventHandler.clearClientBeefAdvancedStealthStates(); + BeefToolProtectionManager.clearAll(); + lastTabPressed = false; + } + @EventBusSubscriber(modid = UselessMod.MODID, value = Dist.CLIENT, bus = EventBusSubscriber.Bus.MOD) + public static final class ModEvents { + @SubscribeEvent + public static void registerKeyBindings(RegisterKeyMappingsEvent event) { + // 附魔切换 + event.register(KeyBindings.SWITCH_SILK_TOUCH_KEY.get()); + event.register(KeyBindings.SWITCH_FORTUNE_KEY.get()); + + // 模式开关 + event.register(KeyBindings.TOGGLE_CHAIN_MODE_KEY.get()); + event.register(KeyBindings.SWITCH_FORCE_MINING_KEY.get()); + event.register(KeyBindings.SWITCH_FARMLAND_MODE_KEY.get()); + event.register(KeyBindings.TOGGLE_CROP_HARVEST_KEY.get()); + // 这两个按键此前只被 onKeyInput / tooltip 使用,却漏了注册,导致按键实际不生效 + event.register(KeyBindings.TOGGLE_SHEARS_KEY.get()); + event.register(KeyBindings.TOGGLE_FLINT_AND_STEEL_KEY.get()); + // 连点模式(默认未绑定) + event.register(KeyBindings.TOGGLE_AUTO_CLICK_KEY.get()); + // 杀戮光环(默认未绑定) + event.register(KeyBindings.TOGGLE_KILL_AURA_KEY.get()); + + // 触发按键 + event.register(KeyBindings.TRIGGER_CHAIN_MINING_KEY.get()); + event.register(KeyBindings.TRIGGER_FORCE_MINING_KEY.get()); + + // 短距传送(造化杖) + event.register(KeyBindings.SHORT_TELEPORT_KEY.get()); + + // UI + event.register(KeyBindings.SWITCH_MODE_WHEEL_KEY.get()); + event.register(KeyBindings.OPEN_WIRELESS_LOGISTICS_KEY.get()); + } + + @SubscribeEvent + public static void registerGuiLayers(RegisterGuiOverlaysEvent event) { + event.registerAbove( + VanillaGuiOverlay.HOTBAR.id(), + "chain_mining_status", + (forgeGui, graphics, tick, width, height) -> MiningStatusGui.render(graphics, tick) + ); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/event/client/ModeWheelHandler.java b/src/main/java/com/sorrowmist/useless/event/client/ModeWheelHandler.java new file mode 100644 index 00000000..814cab00 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/event/client/ModeWheelHandler.java @@ -0,0 +1,76 @@ +package com.sorrowmist.useless.event.client; + +import com.sorrowmist.useless.client.gui.ModeWheelScreen; +import com.sorrowmist.useless.compat.enderio.EnderIOTravelCompat; +import com.sorrowmist.useless.compat.enderio.client.EnderIOTravelClientCompat; +import com.sorrowmist.useless.core.common.KeyBindings; +import com.sorrowmist.useless.utils.UselessItemUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.eventbus.api.EventPriority; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod.EventBusSubscriber; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.event.TickEvent.ClientTickEvent; +import net.minecraftforge.client.event.InputEvent; + +import java.util.AbstractMap; + +/** + * 模式轮盘处理器,负责处理模式轮盘的显示、隐藏和交互 + */ +@EventBusSubscriber(Dist.CLIENT) +public class ModeWheelHandler { + private static boolean suppressEnderIoTravelKey; + + /** + * 处理键盘按下事件 + */ + @SubscribeEvent + public static void onKeyPressed(InputEvent.Key event) { + Minecraft minecraft = Minecraft.getInstance(); + if (minecraft.player == null) return; + + // 检查是否是G键按下事件 (GLFW_PRESS = 1) + if (event.getKey() == KeyBindings.SWITCH_MODE_WHEEL_KEY.get().getKey().getValue() + && event.getAction() == 1) { + if (minecraft.screen instanceof ModeWheelScreen) { + minecraft.setScreen(null); + return; + } + if (minecraft.screen != null) return; + + // 使用工具方法查找目标工具 + ItemStack targetItem = UselessItemUtils.findTargetToolInHands(minecraft.player) + .map(AbstractMap.SimpleImmutableEntry::getKey) + .orElse(null); + + if (targetItem != null && !(minecraft.screen instanceof ModeWheelScreen)) { + // 显示模式轮盘屏幕 + suppressEnderIoTravelKey = true; + Minecraft.getInstance().setScreen(new ModeWheelScreen(targetItem)); + } + } + } + + /** + * The wheel and Ender IO both default to G. Keep Ender IO from interpreting the wheel-key + * release as a travel request while the wheel is being used. + */ + @SubscribeEvent(priority = EventPriority.HIGHEST) + public static void onClientTick(ClientTickEvent event) { + if (event.phase != net.minecraftforge.event.TickEvent.Phase.END) return; + if (!suppressEnderIoTravelKey) return; + + Minecraft minecraft = Minecraft.getInstance(); + if (ModList.get().isLoaded(EnderIOTravelCompat.MOD_ID)) { + EnderIOTravelClientCompat.suppressTravelKey(); + } + + boolean wheelKeyDown = KeyBindings.SWITCH_MODE_WHEEL_KEY.get().isDown(); + if (!wheelKeyDown && !(minecraft.screen instanceof ModeWheelScreen)) { + suppressEnderIoTravelKey = false; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/events/CommonEvents.java b/src/main/java/com/sorrowmist/useless/events/CommonEvents.java deleted file mode 100644 index 88b6c377..00000000 --- a/src/main/java/com/sorrowmist/useless/events/CommonEvents.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.sorrowmist.useless.events; - -import net.minecraft.ChatFormatting; -import net.minecraft.network.chat.Component; -import net.minecraft.resources.ResourceLocation; -import net.minecraftforge.api.distmarker.Dist; -import net.minecraftforge.event.entity.player.ItemTooltipEvent; -import net.minecraftforge.eventbus.api.SubscribeEvent; -import net.minecraftforge.fml.common.Mod; -import net.minecraftforge.registries.ForgeRegistries; - -@Mod.EventBusSubscriber(modid = "useless_mod", bus = Mod.EventBusSubscriber.Bus.FORGE, value = Dist.CLIENT) -public class CommonEvents { - - @SubscribeEvent - public static void itemTooltipEvent(ItemTooltipEvent event) { - ResourceLocation item = ForgeRegistries.ITEMS.getKey(event.getItemStack().getItem()); - if (item != null && item.getPath().equals("device_lava_gen")) { - event.getToolTip().add(1, Component.translatable("info.useless_mod.lava_gen_nether").withStyle(ChatFormatting.GOLD)); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/init/ClientSetup.java b/src/main/java/com/sorrowmist/useless/init/ClientSetup.java new file mode 100644 index 00000000..cdb2edbe --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/init/ClientSetup.java @@ -0,0 +1,334 @@ +package com.sorrowmist.useless.init; + +import appeng.api.stacks.AEItemKey; +import appeng.crafting.pattern.AEProcessingPattern; +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.api.enums.EnumColor; +import com.sorrowmist.useless.client.gui.AdvancedAlloyFurnaceScreen; +import com.sorrowmist.useless.client.gui.PatternAssemblyScreen; +import com.sorrowmist.useless.client.gui.MoldHubScreen; +import com.sorrowmist.useless.client.gui.MultiblockAlloyFurnaceScreen; +import com.sorrowmist.useless.client.gui.PassiveCraftingHatchScreen; +import com.sorrowmist.useless.client.gui.OreGeneratorScreen; +import com.sorrowmist.useless.client.gui.DimensionConfigScreen; +import com.sorrowmist.useless.client.gui.StaffLinkScreen; +import com.sorrowmist.useless.client.render.ctm.CtmModelRegistrar; +import com.sorrowmist.useless.client.render.supervisor.SupervisorModelLoader; +import com.sorrowmist.useless.compat.jei.JEIPlugin; +import com.sorrowmist.useless.content.blockentities.ThermostatBlockEntity; +import com.sorrowmist.useless.content.blocks.GlowPlasticBlock; +import com.sorrowmist.useless.content.blocks.TemperatureRegulatorBlock; +import com.sorrowmist.useless.content.items.EndlessBeafItem; +import com.sorrowmist.useless.client.gui.PatternConverterScreen; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.OmniversalPatternEncoding; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeCatalog; +import com.sorrowmist.useless.core.component.OmniversalPatternData; +import com.sorrowmist.useless.core.component.UComponents; +import com.sorrowmist.useless.utils.TemperatureColors; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod.EventBusSubscriber; +import net.minecraftforge.client.event.RegisterColorHandlersEvent; +import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; +import net.minecraft.client.gui.screens.MenuScreens; +import net.minecraftforge.client.event.ModelEvent; +import net.minecraftforge.event.TickEvent.ClientTickEvent; +import net.minecraftforge.client.event.RecipesUpdatedEvent; +import net.minecraftforge.event.TagsUpdatedEvent; +import net.minecraftforge.event.entity.player.ItemTooltipEvent; +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@EventBusSubscriber(modid = UselessMod.MODID, value = Dist.CLIENT) +public class ClientSetup { + private static Level observedClientLevel; + /** 客户端配方目录的待重建标记;登录、标签包与数据包重载都会置位,由 onClientTick 统一消费。 */ + private static volatile boolean recipeCatalogDirty; + /** + * 本次客户端会话是否已经收到过标签包。 + * + *

部分可选 adapter 依赖同步下来的物品标签。配方包先于标签包到达,若此时就构建, + * 依赖标签的 adapter 会全部产出为空,得到一份不完整的目录,并在标签到达后立刻被推翻重建。

+ */ + private static volatile boolean clientTagsReceived; + /** 脏标记置位后经过的 tick 数,用于标签包迟迟不到时兜底构建。 */ + private static int recipeCatalogDirtyTicks; + /** 等待标签包的上限(tick)。超过后不再等待,避免标签包缺失时目录永远构建不出来。 */ + private static final int TAG_WAIT_TICKS = 100; + /** + * 已在本线程发起目录构建、正等待就绪以刷新 JEI。 + * + *

目录构建由 {@link AlloyFurnaceRecipeCatalog#prewarm} 同步完成,JEI 的展示刷新须在 + * 客户端线程执行,因此这里记住「欠一次刷新」,在后续 tick 确认目录就绪后补上。

+ */ + private static boolean awaitingCatalogRefresh; + + public static void modifyBakedModels(ModelEvent.ModifyBakingResult event) { + CtmModelRegistrar.modifyBakingResult(event); + } + + public static void registerGeometryLoaders(ModelEvent.RegisterGeometryLoaders event) { + event.register("supervisor", SupervisorModelLoader.INSTANCE); + } + + public static void onItemColor(RegisterColorHandlersEvent.Item event) { + for (var itemMap : GlowPlasticBlock.ALL_BLOCK_ITEM_MAPS) { + for (EnumColor color : EnumColor.valuesInOrder()) { + var item = itemMap.get(color).get(); + event.register((stack, tintIndex) -> tintIndex == 0 ? color.getRgb() : 0xFFFFFFFF, + item); + } + } + // 温度调节器物品:无 BE,固定按默认温度(300K = 白色)着色。 + event.register((stack, tintIndex) -> tintIndex == 0 + ? TemperatureColors.forTemperature(ThermostatBlockEntity.DEFAULT_TEMPERATURE) + : 0xFFFFFFFF, + ModItems.TEMPERATURE_REGULATOR.get()); + } + + public static void onBlockColor(RegisterColorHandlersEvent.Block event) { + for (var blockMap : GlowPlasticBlock.ALL_BLOCK_MAPS) { + for (EnumColor color : EnumColor.valuesInOrder()) { + var block = blockMap.get(color).get(); + event.register((state, world, pos, tintIndex) -> tintIndex == 0 ? color.getRgb() : 0xFFFFFFFF, + block); + } + } + // 温度调节器:内板(tintindex 0)按温度染色,公式移植自气动工艺压缩铁块。 + // + // \u26d4 \u8fd9\u91cc\u53ea\u80fd\u8bfb BlockState\uff0c**\u4e0d\u80fd\u53bb\u67e5 BlockEntity**\uff1a + // \u533a\u5757\u7f51\u683c\u662f\u5728 RenderChunkRegion \u4e0a\u70d8\u7119\u7684\uff0c\u5b83\u7684 getBlockEntity \u6052\u4e3a null\uff0c + // \u5728\u8fd9\u91cc\u67e5 BE \u53ea\u4f1a\u6c38\u8fdc\u843d\u5230\u9ed8\u8ba4\u6e29\u5ea6\uff08\u75c7\u72b6\uff1a\u4e0d\u7ba1\u8bbe\u591a\u5c11\u5ea6\u989c\u8272\u90fd\u4e00\u6837\uff09\u3002 + // \u6e29\u5ea6\u5df2\u7531 ThermostatBlockEntity \u91cf\u5316\u6210\u6863\u4f4d\u5199\u8fdb\u72b6\u6001\u3002 + event.register((state, world, pos, tintIndex) -> { + if (tintIndex != 0 || !state.getValue(TemperatureRegulatorBlock.ENABLED)) { + // \u672a\u5f00\u542f\uff1a\u4e0d\u7740\u8272\uff0c\u4fdd\u7559\u8d34\u56fe\u539f\u8272\u3002 + return 0xFFFFFFFF; + } + long kelvin = TemperatureRegulatorBlock.temperatureForLevel( + state.getValue(TemperatureRegulatorBlock.TEMPERATURE_LEVEL)); + return TemperatureColors.forTemperature(kelvin); + }, ModBlocks.TEMPERATURE_REGULATOR.get()); + } + + public static void registerScreens(FMLClientSetupEvent event) { + event.enqueueWork(() -> { + MenuScreens.register(ModMenuType.ADVANCED_ALLOY_FURNACE_MENU.get(), AdvancedAlloyFurnaceScreen::new); + MenuScreens.register(ModMenuType.ME_PATTERN_ASSEMBLY_MENU.get(), PatternAssemblyScreen::new); + MenuScreens.register(ModMenuType.OMNIVERSAL_MOLD_HUB_MENU.get(), MoldHubScreen::new); + MenuScreens.register(ModMenuType.MULTIBLOCK_ALLOY_FURNACE_MENU.get(), MultiblockAlloyFurnaceScreen::new); + MenuScreens.register(ModMenuType.PASSIVE_CRAFTING_HATCH_MENU.get(), PassiveCraftingHatchScreen::new); + MenuScreens.register(ModMenuType.ORE_GENERATOR_MENU.get(), OreGeneratorScreen::new); + MenuScreens.register(ModMenuType.DIMENSION_CONFIG_MENU.get(), DimensionConfigScreen::new); + MenuScreens.register(ModMenuType.PATTERN_CONVERTER_MENU.get(), PatternConverterScreen::new); + MenuScreens.register(ModMenuType.STAFF_LINK_MENU.get(), StaffLinkScreen::new); + }); + } + + @SubscribeEvent + public static void onRecipesUpdated(RecipesUpdatedEvent event) { + // 只登记待刷新,不在这里直接重建:登录路径里本事件、标签包事件与 level 切换会在极短时间 + // 内接连触发,逐次重建等于把同一份目录完整算好几遍。 + markRecipeCatalogDirty(); + } + + @SubscribeEvent + public static void onClientTick(ClientTickEvent event) { + if (event.phase != net.minecraftforge.event.TickEvent.Phase.END) return; + Minecraft minecraft = Minecraft.getInstance(); + Level level = minecraft.level; + if (level != observedClientLevel) { + observedClientLevel = level; + if (level != null) { + // The initial JEI registration can happen on the title screen, before a client level + // exists. Rebuild once after joining a world so generated compat recipes are visible. + markRecipeCatalogDirty(); + } else { + AlloyFurnaceRecipeCatalog.invalidate(); + recipeCatalogDirty = false; + // 离开世界:标签会随下一次登录重新下发,必须把「已收到」重置掉, + // 否则下次进世界会跳过等待、又在标签落地前建出残缺目录。 + clientTagsReceived = false; + recipeCatalogDirtyTicks = 0; + awaitingCatalogRefresh = false; + } + } + + if (level == null) { + recipeCatalogDirtyTicks = 0; + return; + } + + // 合并点:无论脏标记来自登录、标签包还是数据包重载,都在这里统一重建一次。 + if (recipeCatalogDirty) { + // 登录路径里配方包会先于标签包到达。若在标签包落地前就构建,依赖标签的 adapter 会 + // 整批产出为空,得到一份随即被推翻的残缺目录(实测那次白跑花了 13.8 秒)。 + // 因此这里等到标签包到达再建;标签包始终不来时按 TAG_WAIT_TICKS 兜底。 + if (!clientTagsReceived && ++recipeCatalogDirtyTicks < TAG_WAIT_TICKS) { + return; + } + recipeCatalogDirty = false; + recipeCatalogDirtyTicks = 0; + beginRecipeCatalogRebuild(level); + } + + // 目录构建已在 beginRecipeCatalogRebuild 中同步完成,此处只负责在客户端线程 + // 刷新依赖目录的 JEI 展示与物品属性。 + if (awaitingCatalogRefresh && AlloyFurnaceRecipeCatalog.isReady(level)) { + awaitingCatalogRefresh = false; + onRecipeCatalogReady(); + } + } + + /** 标记客户端配方目录需要在下一个 tick 重建。 */ + private static void markRecipeCatalogDirty() { + recipeCatalogDirty = true; + } + + /** + * 失效目录并同步重建,同时记下「欠一次 JEI 刷新」。 + * + *

失效与构建之间不插入读取:构建按当时读到的 RecipeManager 内容产出快照,若中间有人触发 + * 同步构建,就会白算一份。

+ */ + private static void beginRecipeCatalogRebuild(Level level) { + AlloyFurnaceRecipeCatalog.invalidate(level); + awaitingCatalogRefresh = true; + AlloyFurnaceRecipeCatalog.prewarm(level); + } + + /** 目录已就绪:在客户端线程刷新依赖它的 JEI 展示与物品属性。 */ + private static void onRecipeCatalogReady() { + JEIPlugin.refreshAlloyFurnaceRecipes(); + if (Minecraft.getInstance().player != null) { + EndlessBeafItem.refreshAttackDamage(Minecraft.getInstance().player); + } + } + + @SubscribeEvent + public static void onTagsUpdated(TagsUpdatedEvent event) { + // Only react to the tag packet the client actually received. The integrated server also + // fires this event on its own thread for data-pack loads (including /reload); touching the + // client recipe catalog or JEI from there fails the reload. + if (event.getUpdateCause() != TagsUpdatedEvent.UpdateCause.CLIENT_PACKET_RECEIVED) return; + + // Optional recipe adapters may read synced item tags. Recipes are updated before the + // clientbound tag packet in some login paths, so refresh the catalog after those tags bind. + // + // 先置位「标签已到达」再登记重建:onClientTick 的合并点据此放行,避免在标签缺失时 + // 建出一份依赖标签的 adapter 全空的残缺目录。 + clientTagsReceived = true; + markRecipeCatalogDirty(); + } + + @SubscribeEvent + public static void onItemTooltip(ItemTooltipEvent event) { + if (!event.getItemStack().is(ModItems.OMNIVERSAL_PATTERN.get())) return; + var data = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(event.getItemStack(), UComponents.OMNIVERSAL_PATTERN_DATA.get()); + if (data == null) return; + List moldTags = + resolveMoldTagInputs(event.getItemStack(), data); + event.getToolTip().add(createRecipeTooltip(data.recipeId())); + if (!data.requiresMold()) { + event.getToolTip().add(Component.translatable( + "tooltip.useless_mod.omniversal_pattern.mold", + Component.translatable("tooltip.useless_mod.omniversal_pattern.no_mold")) + .withStyle(ChatFormatting.GOLD)); + } else if (!data.displayMolds().isEmpty()) { + Set tagMoldSlots = new HashSet<>(); + for (var moldTag : moldTags) { + tagMoldSlots.add(moldTag.moldSlot()); + } + for (int moldSlot = 0; moldSlot < data.displayMolds().size(); moldSlot++) { + // Older metadata stored one representative item for every mold slot. A tag-backed + // mold must be shown by its tag instead of that arbitrary representative. + if (tagMoldSlots.contains(moldSlot)) continue; + var mold = data.displayMolds().get(moldSlot); + event.getToolTip().add(Component.translatable( + "tooltip.useless_mod.omniversal_pattern.mold", + mold.getDisplayName()).withStyle(ChatFormatting.GOLD)); + } + appendMoldTagTooltips(event, moldTags); + } else if (!moldTags.isEmpty()) { + appendMoldTagTooltips(event, moldTags); + } else { + Component mold = data.displayMold().map(key -> key.getDisplayName()) + .orElseGet(() -> Component.translatable("tooltip.useless_mod.omniversal_pattern.unknown_mold")); + event.getToolTip().add(Component.translatable( + "tooltip.useless_mod.omniversal_pattern.mold", mold).withStyle(ChatFormatting.GOLD)); + } + } + + private static void appendMoldTagTooltips( + ItemTooltipEvent event, List moldTags) { + for (var moldTag : moldTags) { + event.getToolTip().add(Component.translatable( + "tooltip.useless_mod.omniversal_pattern.mold", + Component.literal("#" + moldTag.tag().location())) + .withStyle(ChatFormatting.GOLD)); + } + } + + /** + * Reconstructs mold tags for old metadata that predates the mold-tag field. This also makes + * an already encoded pattern update its tooltip without requiring the player to encode it + * again, while the bound recipe remains the source of truth for execution. + */ + private static List resolveMoldTagInputs( + ItemStack pattern, OmniversalPatternData data) { + if (!data.requiresMold()) return List.of(); + if (!data.moldTagInputSlots().isEmpty()) { + return data.moldTagInputSlots(); + } + var level = Minecraft.getInstance().level; + if (level == null) return List.of(); + try { + var entry = AlloyFurnaceRecipeCatalog.resolve(level, data.identity()); + if (entry.isEmpty() && pattern != null && !pattern.isEmpty()) { + AEItemKey definition = AEItemKey.of(pattern); + if (definition != null) { + entry = AlloyFurnaceRecipeCatalog.resolvePattern( + level, data.identity(), new AEProcessingPattern(definition)); + } + } + if (entry.isEmpty()) { + List byId = + AlloyFurnaceRecipeCatalog.entries(level).stream() + .filter(candidate -> candidate.identity().recipeId().equals(data.recipeId())) + .toList(); + if (byId.size() == 1) { + entry = java.util.Optional.of(byId.get(0)); + } + } + return entry.map(candidate -> + OmniversalPatternEncoding.resolveMoldTagInputSlots(candidate.recipe())) + .orElse(List.of()); + } catch (RuntimeException ignored) { + // Tooltips must remain safe while the client recipe catalog is still loading. + return List.of(); + } + } + + static Component createRecipeTooltip(ResourceLocation recipeId) { + return Component.translatable( + "tooltip.useless_mod.omniversal_pattern.recipe", recipeId.toString()) + .withStyle(ChatFormatting.DARK_GRAY); + } + @EventBusSubscriber(modid = UselessMod.MODID, value = Dist.CLIENT, bus = EventBusSubscriber.Bus.MOD) + public static final class ModEvents { + @SubscribeEvent public static void modifyBakedModels(ModelEvent.ModifyBakingResult event) { ClientSetup.modifyBakedModels(event); } + @SubscribeEvent public static void bakingCompleted(ModelEvent.BakingCompleted event) { CtmModelRegistrar.bakingCompleted(event); } + @SubscribeEvent public static void registerGeometryLoaders(ModelEvent.RegisterGeometryLoaders event) { ClientSetup.registerGeometryLoaders(event); } + @SubscribeEvent public static void onItemColor(RegisterColorHandlersEvent.Item event) { ClientSetup.onItemColor(event); } + @SubscribeEvent public static void onBlockColor(RegisterColorHandlersEvent.Block event) { ClientSetup.onBlockColor(event); } + @SubscribeEvent public static void registerScreens(FMLClientSetupEvent event) { ClientSetup.registerScreens(event); } + } +} diff --git a/src/main/java/com/sorrowmist/useless/init/ModBlockEntities.java b/src/main/java/com/sorrowmist/useless/init/ModBlockEntities.java new file mode 100644 index 00000000..e800ae43 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/init/ModBlockEntities.java @@ -0,0 +1,161 @@ +package com.sorrowmist.useless.init; + +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.blockentities.AdvancedAlloyFurnaceBlockEntity; +import com.sorrowmist.useless.content.blockentities.OreGeneratorBlockEntity; +import com.sorrowmist.useless.content.blockentities.ThermostatBlockEntity; +import com.sorrowmist.useless.content.blockentities.multiblock.MePatternAssemblyBlockEntity; +import com.sorrowmist.useless.content.blockentities.multiblock.MultiblockAlloyFurnaceCoreBlockEntity; +import com.sorrowmist.useless.content.blockentities.multiblock.OmniversalMoldHubBlockEntity; +import com.sorrowmist.useless.content.blockentities.multiblock.PassiveCraftingHatchBlockEntity; +import com.sorrowmist.useless.compat.draconicevolution.DraconicOpStorageCompat; +import com.sorrowmist.useless.compat.fluxnetworks.FluxNetworksEnergyCompat; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraftforge.eventbus.api.IEventBus; +import net.minecraftforge.common.capabilities.ForgeCapabilities; +import com.sorrowmist.useless.compat.forge1201.capability.RegisterCapabilitiesEvent; +import com.sorrowmist.useless.compat.forge1201.registry.DeferredHolder; +import com.sorrowmist.useless.compat.forge1201.registry.DeferredRegister; +import net.minecraftforge.fml.ModList; + +import appeng.capabilities.Capabilities; + +public final class ModBlockEntities { + private static final DeferredRegister> BLOCK_ENTITY_TYPES = + DeferredRegister.create(Registries.BLOCK_ENTITY_TYPE, UselessMod.MODID); + + private ModBlockEntities() {} + + public static void register(IEventBus modEventBus) { + BLOCK_ENTITY_TYPES.register(modEventBus); + modEventBus.addListener(ModBlockEntities::registerCapabilities); + } + + private static void registerCapabilities(RegisterCapabilitiesEvent event) { + // 注册高级合金炉的物品处理能力(支持方向感知) + event.registerBlockEntity( + ForgeCapabilities.ITEM_HANDLER, + ADVANCED_ALLOY_FURNACE.get(), + (blockEntity, side) -> blockEntity.getItemHandler(side) + ); + + event.registerBlockEntity( + ForgeCapabilities.ENERGY, + MULTIBLOCK_ALLOY_FURNACE_CORE.get(), + (blockEntity, side) -> blockEntity.getEnergyStorage()); + event.registerBlockEntity( + ForgeCapabilities.ITEM_HANDLER, + ME_PATTERN_ASSEMBLY.get(), + (blockEntity, side) -> blockEntity.getPatterns()); + event.registerBlockEntity( + appeng.capabilities.Capabilities.IN_WORLD_GRID_NODE_HOST, + ME_PATTERN_ASSEMBLY.get(), + (blockEntity, context) -> blockEntity); + event.registerBlockEntity( + ForgeCapabilities.ITEM_HANDLER, + OMNIVERSAL_MOLD_HUB.get(), + (blockEntity, side) -> blockEntity.getMolds()); + event.registerBlockEntity( + ForgeCapabilities.ITEM_HANDLER, + PASSIVE_CRAFTING_HATCH.get(), + (blockEntity, side) -> blockEntity.getPatterns()); + event.registerBlockEntity( + appeng.capabilities.Capabilities.IN_WORLD_GRID_NODE_HOST, + ORE_GENERATOR.get(), + (blockEntity, context) -> blockEntity); + + // 注册高级合金炉的能量处理能力 + event.registerBlockEntity( + ForgeCapabilities.ENERGY, + ADVANCED_ALLOY_FURNACE.get(), + (blockEntity, side) -> blockEntity.getEnergyStorage() + ); + + // 注册高级合金炉的流体处理能力(支持方向感知) + event.registerBlockEntity( + ForgeCapabilities.FLUID_HANDLER, + ADVANCED_ALLOY_FURNACE.get(), + (blockEntity, side) -> blockEntity.getCombinedFluidHandler(side) + ); + + // 注册高级合金炉的AE网络节点能力(用于连接AE网络) + event.registerBlockEntity( + appeng.capabilities.Capabilities.IN_WORLD_GRID_NODE_HOST, + ADVANCED_ALLOY_FURNACE.get(), + (blockEntity, context) -> blockEntity + ); + + if (ModList.get().isLoaded("draconicevolution")) { + DraconicOpStorageCompat.registerCapabilities(event); + } + if (ModList.get().isLoaded("mekanism")) { + invokeOptionalCapabilityLoader( + "com.sorrowmist.useless.compat.mekanism.MekanismCompatLoader", event); + } + if (ModList.get().isLoaded("pneumaticcraft")) { + invokeOptionalCapabilityLoader( + "com.sorrowmist.useless.compat.pneumaticcraft.ThermostatHeatCompat", event); + } + if (ModList.get().isLoaded(FluxNetworksEnergyCompat.MOD_ID)) { + FluxNetworksEnergyCompat.registerCapabilities(event); + } + } + + /** + * Keep optional Mekanism classes out of this always-loaded registry class. In particular, the + * JVM must not resolve AppMek's capability types on a server that does not install AppMek. + */ + private static void invokeOptionalCapabilityLoader(String className, RegisterCapabilitiesEvent event) { + try { + Class loader = Class.forName(className, true, ModBlockEntities.class.getClassLoader()); + loader.getMethod("registerCapabilities", RegisterCapabilitiesEvent.class).invoke(null, event); + } catch (ReflectiveOperationException | LinkageError exception) { + RateLimitedLogger.error(UselessMod.LOGGER, "Failed to register optional capabilities from {}", className, exception); + } + } + + public static final DeferredHolder, BlockEntityType> ORE_GENERATOR = + BLOCK_ENTITY_TYPES.register("ore_generator", + () -> BlockEntityType.Builder.of(OreGeneratorBlockEntity::new, + ModBlocks.ORE_GENERATOR_BLOCK.get() + ).build(null) + ); + + + public static final DeferredHolder, BlockEntityType> ADVANCED_ALLOY_FURNACE = + BLOCK_ENTITY_TYPES.register("advanced_alloy_furnace", + () -> BlockEntityType.Builder.of(AdvancedAlloyFurnaceBlockEntity::new, + ModBlocks.ADVANCED_ALLOY_FURNACE_BLOCK.get() + ).build(null) + ); + + public static final DeferredHolder, BlockEntityType> MULTIBLOCK_ALLOY_FURNACE_CORE = + BLOCK_ENTITY_TYPES.register("multiblock_alloy_furnace_core", + () -> BlockEntityType.Builder.of(MultiblockAlloyFurnaceCoreBlockEntity::new, + ModBlocks.MULTIBLOCK_ALLOY_FURNACE_CORE.get()).build(null)); + public static final DeferredHolder, BlockEntityType> ME_PATTERN_ASSEMBLY = + BLOCK_ENTITY_TYPES.register("me_pattern_assembly", + () -> BlockEntityType.Builder.of(MePatternAssemblyBlockEntity::new, + ModBlocks.ME_PATTERN_ASSEMBLY.get()).build(null)); + public static final DeferredHolder, BlockEntityType> OMNIVERSAL_MOLD_HUB = + BLOCK_ENTITY_TYPES.register("omniversal_mold_hub", + () -> BlockEntityType.Builder.of(OmniversalMoldHubBlockEntity::new, + ModBlocks.OMNIVERSAL_MOLD_HUB.get()).build(null)); + public static final DeferredHolder, BlockEntityType> PASSIVE_CRAFTING_HATCH = + BLOCK_ENTITY_TYPES.register("passive_crafting_hatch", + () -> BlockEntityType.Builder.of(PassiveCraftingHatchBlockEntity::new, + ModBlocks.PASSIVE_CRAFTING_HATCH.get()).build(null)); + + /** + * 「温度调节器」方块的恒温源状态。 + */ + public static final DeferredHolder, BlockEntityType> TEMPERATURE_REGULATOR = + BLOCK_ENTITY_TYPES.register("temperature_regulator", + () -> BlockEntityType.Builder.of(ThermostatBlockEntity::new, + ModBlocks.TEMPERATURE_REGULATOR.get()).build(null)); + + +} diff --git a/src/main/java/com/sorrowmist/useless/init/ModBlocks.java b/src/main/java/com/sorrowmist/useless/init/ModBlocks.java new file mode 100644 index 00000000..0d733f82 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/init/ModBlocks.java @@ -0,0 +1,150 @@ +package com.sorrowmist.useless.init; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.blocks.AdvancedAlloyFurnaceBlock; +import com.sorrowmist.useless.content.blocks.OreGeneratorBlock; +import com.sorrowmist.useless.content.blocks.SupervisorBlock; +import com.sorrowmist.useless.content.blocks.TeleportPadBlock; +import com.sorrowmist.useless.content.blocks.TemperatureRegulatorBlock; +import com.sorrowmist.useless.content.blocks.UselessGlassBlock; +import com.sorrowmist.useless.content.blocks.multiblock.MePatternAssemblyBlock; +import com.sorrowmist.useless.content.blocks.multiblock.MultiblockAlloyFurnaceCoreBlock; +import com.sorrowmist.useless.content.blocks.multiblock.MultiblockPartBlock; +import com.sorrowmist.useless.content.blocks.multiblock.OmniversalMoldHubBlock; +import com.sorrowmist.useless.content.blocks.multiblock.PassiveCraftingHatchBlock; +import com.sorrowmist.useless.content.blocks.multiblock.UselessCoilBlock; +import com.sorrowmist.useless.world.teleport.UselessDimTeleporter; +import com.sorrowmist.useless.world.teleport.UselessDimTeleporter2; +import com.sorrowmist.useless.world.teleport.UselessDimTeleporter3; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.material.MapColor; +import com.sorrowmist.useless.compat.forge1201.registry.DeferredBlock; +import com.sorrowmist.useless.compat.forge1201.registry.DeferredRegister; + +import java.util.LinkedHashMap; +import java.util.Map; + +public final class ModBlocks { + public static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks(UselessMod.MODID); + + public static final DeferredBlock TELEPORT_BLOCK = BLOCKS.register( + "teleport_block", + () -> new TeleportPadBlock( + UselessDimTeleporter::new, + BlockBehaviour.Properties.of() + .strength(2.0f, 65536.0f) + .requiresCorrectToolForDrops() + ) + ); + + public static final DeferredBlock TELEPORT_BLOCK_2 = BLOCKS.register( + "teleport_block_2", + () -> new TeleportPadBlock( + UselessDimTeleporter2::new, + BlockBehaviour.Properties.of() + .strength(2.0f, 65536.0f) + .requiresCorrectToolForDrops() + ) + ); + + public static final DeferredBlock TELEPORT_BLOCK_3 = BLOCKS.register( + "teleport_block_3", + () -> new TeleportPadBlock( + UselessDimTeleporter3::new, + BlockBehaviour.Properties.of() + .strength(2.0f, 65536.0f) + .requiresCorrectToolForDrops() + ) + ); + + public static final DeferredBlock ORE_GENERATOR_BLOCK = BLOCKS.register( + "ore_generator_block", OreGeneratorBlock::new + ); + + public static final DeferredBlock ADVANCED_ALLOY_FURNACE_BLOCK = BLOCKS.register( + "advanced_alloy_furnace_block", + AdvancedAlloyFurnaceBlock::new + ); + + public static final DeferredBlock SUPERVISOR = BLOCKS.register( + "supervisor", + () -> new SupervisorBlock(BlockBehaviour.Properties.of() + .noOcclusion() + .strength(2.0F)) + ); + + public static final DeferredBlock MULTIBLOCK_ALLOY_FURNACE_CORE = BLOCKS.register( + "multiblock_alloy_furnace_core", + () -> new MultiblockAlloyFurnaceCoreBlock(BlockBehaviour.Properties.of() + .strength(5.0F, 32768.0F).requiresCorrectToolForDrops())); + public static final DeferredBlock ME_PATTERN_ASSEMBLY = BLOCKS.register( + "me_pattern_assembly", + () -> new MePatternAssemblyBlock(BlockBehaviour.Properties.of() + .strength(4.0F, 1200.0F).requiresCorrectToolForDrops())); + public static final DeferredBlock OMNIVERSAL_MOLD_HUB = BLOCKS.register( + "omniversal_mold_hub", + () -> new OmniversalMoldHubBlock(BlockBehaviour.Properties.of() + .strength(4.0F, 1200.0F).requiresCorrectToolForDrops())); + public static final DeferredBlock PASSIVE_CRAFTING_HATCH = BLOCKS.register( + "passive_crafting_hatch", + () -> new PassiveCraftingHatchBlock(BlockBehaviour.Properties.of() + .strength(4.0F, 1200.0F).requiresCorrectToolForDrops())); + public static final DeferredBlock OMNIVERSAL_FURNACE_CASING = BLOCKS.register( + "omniversal_furnace_casing", + () -> new MultiblockPartBlock(BlockBehaviour.Properties.of() + .strength(4.0F, 1200.0F).requiresCorrectToolForDrops())); + + /** 温度调节器:可配置的无限恒温源,内板按温度染色。 */ + public static final DeferredBlock TEMPERATURE_REGULATOR = BLOCKS.register( + "temperature_regulator", + () -> new TemperatureRegulatorBlock(BlockBehaviour.Properties.of() + .mapColor(MapColor.METAL) + .strength(5.0F, 6.0F) + .lightLevel(TemperatureRegulatorBlock::lightForState) + .requiresCorrectToolForDrops())); + + public static final Map> USELESS_COILS = new LinkedHashMap<>(); + + static { + for (int tier = UselessCoilBlock.MIN_TIER; tier <= UselessCoilBlock.MAX_TIER; tier++) { + int capturedTier = tier; + USELESS_COILS.put(tier, BLOCKS.register(UselessCoilBlock.registryName(tier), + () -> new UselessCoilBlock(capturedTier, BlockBehaviour.Properties.of() + .strength(3.0F + capturedTier * 0.25F, 1200.0F + capturedTier * 600.0F) + .requiresCorrectToolForDrops()))); + } + } + + // 无用玻璃方块 - 防爆,Shift+右键快速破坏 + public static final Map> USELESS_GLASS_BLOCKS = new LinkedHashMap<>(); + + static final DeferredBlock USELESS_GLASS_TIER_1 = registerGlassBlock( + "useless_glass_tier_1", 1.0f, 1200.0f); + static final DeferredBlock USELESS_GLASS_TIER_2 = registerGlassBlock( + "useless_glass_tier_2", 1.5f, 1200.0f); + static final DeferredBlock USELESS_GLASS_TIER_3 = registerGlassBlock( + "useless_glass_tier_3", 2.0f, 2400.0f); + static final DeferredBlock USELESS_GLASS_TIER_4 = registerGlassBlock( + "useless_glass_tier_4", 2.5f, 2400.0f); + static final DeferredBlock USELESS_GLASS_TIER_5 = registerGlassBlock( + "useless_glass_tier_5", 3.0f, 3600.0f); + static final DeferredBlock USELESS_GLASS_TIER_6 = registerGlassBlock( + "useless_glass_tier_6", 3.5f, 3600.0f); + static final DeferredBlock USELESS_GLASS_TIER_7 = registerGlassBlock( + "useless_glass_tier_7", 4.0f, 4800.0f); + static final DeferredBlock USELESS_GLASS_TIER_8 = registerGlassBlock( + "useless_glass_tier_8", 4.5f, 6000.0f); + static final DeferredBlock USELESS_GLASS_TIER_9 = registerGlassBlock( + "useless_glass_tier_9", 5.0f, 7200.0f); + + private ModBlocks() {} + + private static DeferredBlock registerGlassBlock(String name, float hardness, float resistance) { + DeferredBlock block = BLOCKS.register(name, () -> new UselessGlassBlock( + BlockBehaviour.Properties.of().strength(hardness, resistance)) {} + ); + USELESS_GLASS_BLOCKS.put(name, block); + return block; + } +} diff --git a/src/main/java/com/sorrowmist/useless/init/ModCreativeTabs.java b/src/main/java/com/sorrowmist/useless/init/ModCreativeTabs.java new file mode 100644 index 00000000..aaf79beb --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/init/ModCreativeTabs.java @@ -0,0 +1,60 @@ +package com.sorrowmist.useless.init; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.blocks.GlowPlasticBlock; +import com.sorrowmist.useless.core.config.ConfigManager; +import com.sorrowmist.useless.utils.EnchantmentUtil; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.CreativeModeTab; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.enchantment.Enchantments; +import com.sorrowmist.useless.compat.forge1201.registry.DeferredHolder; +import com.sorrowmist.useless.compat.forge1201.registry.DeferredItem; +import com.sorrowmist.useless.compat.forge1201.registry.DeferredRegister; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +public class ModCreativeTabs { + public static final DeferredRegister CREATIVE_TAB = + DeferredRegister.create(Registries.CREATIVE_MODE_TAB, UselessMod.MODID); + + /** + * 由条件加载的兼容模块(例如 ECO 已安装时的紧凑方块)追加的创造栏条目。 + * 这些物品只在对应前置存在时才会被注册,因此这里必须允许运行时追加。 + */ + public static final List> EXTRA_TAB_ITEMS = + new ArrayList<>(); + + public static final DeferredHolder MAIN = CREATIVE_TAB.register( + "main", + () -> CreativeModeTab.builder() + .icon(() -> new ItemStack(ModItems.ADVANCED_ALLOY_FURNACE_BLOCK.get())) + .title(Component.translatable("itemGroup." + UselessMod.MODID + ".main")) + .displayItems((pParameters, pOutput) -> { + pOutput.accept(getItemStack(pParameters)); + + for (var itemMap : GlowPlasticBlock.ALL_BLOCK_ITEM_MAPS) { + for (DeferredItem item : itemMap.values()) { + pOutput.accept(item.get()); + } + } + + ModItems.CREATIVE_MAIN_TAB_ITEMS.forEach(pOutput::accept); + EXTRA_TAB_ITEMS.forEach(item -> pOutput.accept(item.get())); + }) + .build() + ); + + private static @NotNull ItemStack getItemStack(CreativeModeTab.ItemDisplayParameters pParameters) { + HolderLookup.Provider lookup = pParameters.holders(); + ItemStack endlessBeef = new ItemStack(ModItems.ENDLESS_BEAF_ITEM.get()); + // 与物品默认附魔模式保持一致:默认时运 + EnchantmentUtil.applyEnchantment(endlessBeef, lookup, Enchantments.BLOCK_FORTUNE, ConfigManager.getFortuneLevel()); + EnchantmentUtil.applyEnchantment(endlessBeef, lookup, Enchantments.MOB_LOOTING, ConfigManager.getLootingLevel()); + return endlessBeef; + } +} diff --git a/src/main/java/com/sorrowmist/useless/init/ModDamageTypes.java b/src/main/java/com/sorrowmist/useless/init/ModDamageTypes.java new file mode 100644 index 00000000..08d66a76 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/init/ModDamageTypes.java @@ -0,0 +1,27 @@ +package com.sorrowmist.useless.init; + +import com.sorrowmist.useless.UselessMod; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.damagesource.DamageType; +import net.minecraft.world.entity.player.Player; + +public final class ModDamageTypes { + public static final ResourceKey BEEF_TOOL = ResourceKey.create( + Registries.DAMAGE_TYPE, + UselessMod.id("beef_tool") + ); + + private ModDamageTypes() { + } + + public static DamageSource beefTool(ServerLevel level, Player player) { + Holder holder = level.registryAccess() + .registryOrThrow(Registries.DAMAGE_TYPE) + .getHolderOrThrow(BEEF_TOOL); + return new DamageSource(holder, player, player); + } +} diff --git a/src/main/java/com/sorrowmist/useless/init/ModEntities.java b/src/main/java/com/sorrowmist/useless/init/ModEntities.java new file mode 100644 index 00000000..0dec7828 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/init/ModEntities.java @@ -0,0 +1,29 @@ +package com.sorrowmist.useless.init; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.entities.BeefTimeAccelerationEntity; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MobCategory; +import com.sorrowmist.useless.compat.forge1201.registry.DeferredHolder; +import com.sorrowmist.useless.compat.forge1201.registry.DeferredRegister; + +public final class ModEntities { + public static final DeferredRegister> ENTITY_TYPES = + DeferredRegister.create(Registries.ENTITY_TYPE, UselessMod.MODID); + + public static final DeferredHolder, EntityType> BEEF_TIME_ACCELERATION = + ENTITY_TYPES.register( + "beef_time_acceleration", + () -> EntityType.Builder.of( + BeefTimeAccelerationEntity::new, + MobCategory.MISC) + .sized(0.1F, 0.1F) + .clientTrackingRange(64) + .updateInterval(1) + .build(UselessMod.id("beef_time_acceleration").toString()) + ); + + private ModEntities() { + } +} diff --git a/src/main/java/com/sorrowmist/useless/init/ModIngredientTypes.java b/src/main/java/com/sorrowmist/useless/init/ModIngredientTypes.java new file mode 100644 index 00000000..2f9ef381 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/init/ModIngredientTypes.java @@ -0,0 +1,24 @@ +package com.sorrowmist.useless.init; + +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.content.recipe.adapters.apotheosis.ApotheosisGemIngredient; +import com.sorrowmist.useless.content.recipe.adapters.enderio.SoulVialSetIngredient; +import com.sorrowmist.useless.content.recipe.adapters.hostilenetworks.DataModelRangeIngredient; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.common.crafting.CraftingHelper; +import net.minecraftforge.fml.ModList; + +/** Register native Forge ingredient serializers during common setup, before datapack loading. */ +public final class ModIngredientTypes { + private ModIngredientTypes() {} + public static void register() { + CraftingHelper.register(new ResourceLocation("useless_mod:top_level_nbt"),com.sorrowmist.useless.compat.forge1201.recipe.TopLevelNbtIngredient.TYPE); + CraftingHelper.register(new ResourceLocation("useless_mod:soul_vial_set"),SoulVialSetIngredient.TYPE); + if(ModList.get().isLoaded("enderio")) + CraftingHelper.register(new ResourceLocation("useless_mod:filled_soul"),com.sorrowmist.useless.compat.forge1201.enderio.FilledSoulIngredient.TYPE); + if(ModList.get().isLoaded(RecipeSourceIds.HOSTILE_NETWORKS)) + CraftingHelper.register(new ResourceLocation("useless_mod:data_model_range"),DataModelRangeIngredient.TYPE); + if(ModList.get().isLoaded(RecipeSourceIds.APOTHEOSIS)) + CraftingHelper.register(new ResourceLocation("useless_mod:apotheosis_gem"),ApotheosisGemIngredient.TYPE); + } +} diff --git a/src/main/java/com/sorrowmist/useless/init/ModItems.java b/src/main/java/com/sorrowmist/useless/init/ModItems.java new file mode 100644 index 00000000..39be03c2 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/init/ModItems.java @@ -0,0 +1,304 @@ +package com.sorrowmist.useless.init; + + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.api.enums.RarityExtension; +import com.sorrowmist.useless.api.enums.tool.ToolTypeMode; +import com.sorrowmist.useless.content.blockentities.AdvancedAlloyFurnaceBlockEntity; +import com.sorrowmist.useless.content.blocks.multiblock.UselessCoilBlock; +import com.sorrowmist.useless.content.items.AE2GiftPackageItem; +import com.sorrowmist.useless.content.items.AdvancedAlloyFurnaceBlockItem; +import com.sorrowmist.useless.content.items.EndlessBeafItem; +import com.sorrowmist.useless.content.items.IngotItem; +import com.sorrowmist.useless.content.items.MultiblockAlloyFurnaceCoreBlockItem; +import com.sorrowmist.useless.content.items.OmniversalPatternConverterItem; +import com.sorrowmist.useless.content.items.RitualBlueprintItem; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.Rarity; +import net.minecraftforge.fml.ModList; +import com.sorrowmist.useless.compat.forge1201.registry.DeferredItem; +import com.sorrowmist.useless.compat.forge1201.registry.DeferredRegister; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +public final class ModItems { + public static final DeferredRegister.Items ITEMS = DeferredRegister.createItems(UselessMod.MODID); + // 扳手子类物品注册 + public static final DeferredItem ENDLESS_BEAF_WRENCH = ITEMS.register( + "endless_beaf_wrench", + () -> new EndlessBeafItem(ToolTypeMode.WRENCH_MODE) + ); + // 螺丝刀子类物品注册 + public static final DeferredItem ENDLESS_BEAF_SCREWDRIVER = ITEMS.register( + "endless_beaf_screwdriver", + () -> new EndlessBeafItem(ToolTypeMode.SCREWDRIVER_MODE) + ); + // 软锤子类物品注册 + public static final DeferredItem ENDLESS_BEAF_MALLET = ITEMS.register( + "endless_beaf_mallet", + () -> new EndlessBeafItem(ToolTypeMode.MALLET_MODE) + ); + // 撬棍子类物品注册 + public static final DeferredItem ENDLESS_BEAF_CROWBAR = ITEMS.register( + "endless_beaf_crowbar", + () -> new EndlessBeafItem(ToolTypeMode.CROWBAR_MODE) + ); + // 硬锤子类物品注册 + public static final DeferredItem ENDLESS_BEAF_HAMMER = ITEMS.register( + "endless_beaf_hammer", + () -> new EndlessBeafItem(ToolTypeMode.HAMMER_MODE) + ); + public static final DeferredItem ENDLESS_BEAF_ITEM = ITEMS.register( + "endless_beaf_item", + () -> new EndlessBeafItem() + ); + public static final DeferredItem ENDLESS_BEAF_ITEM_NO_WRENCH = ITEMS.register( + "endless_beaf_item_no_wrench", + () -> new EndlessBeafItem(ToolTypeMode.NONE_MODE, false) + ); + + static final List> CREATIVE_MAIN_TAB_ITEMS = new ArrayList<>(); + public static final DeferredItem USELESS_INGOT_TIER_1 = registerAndAdd( + "useless_ingot_tier_1", + () -> new IngotItem(new Item.Properties().rarity(Rarity.COMMON), 1) + ); + public static final DeferredItem USELESS_INGOT_TIER_2 = registerAndAdd( + "useless_ingot_tier_2", + () -> new IngotItem(new Item.Properties().rarity(Rarity.COMMON), 2) + ); + public static final DeferredItem USELESS_INGOT_TIER_3 = registerAndAdd( + "useless_ingot_tier_3", + () -> new IngotItem(new Item.Properties().rarity(Rarity.UNCOMMON), 3) + ); + public static final DeferredItem USELESS_INGOT_TIER_4 = registerAndAdd( + "useless_ingot_tier_4", + () -> new IngotItem(new Item.Properties().rarity(Rarity.UNCOMMON), 4) + ); + public static final DeferredItem USELESS_INGOT_TIER_5 = registerAndAdd( + "useless_ingot_tier_5", + () -> new IngotItem(new Item.Properties().rarity(Rarity.RARE), 5) + ); + public static final DeferredItem USELESS_INGOT_TIER_6 = registerAndAdd( + "useless_ingot_tier_6", + () -> new IngotItem(new Item.Properties().rarity(Rarity.RARE), 6) + ); + public static final DeferredItem USELESS_INGOT_TIER_7 = registerAndAdd( + "useless_ingot_tier_7", + () -> new IngotItem(new Item.Properties().rarity(Rarity.EPIC), 7) + ); + public static final DeferredItem USELESS_INGOT_TIER_8 = registerAndAdd( + "useless_ingot_tier_8", + () -> new IngotItem(new Item.Properties().rarity(RarityExtension.MYTHIC.getValue()), 8) + ); + public static final DeferredItem USELESS_INGOT_TIER_9 = registerAndAdd( + "useless_ingot_tier_9", + () -> new IngotItem(new Item.Properties().rarity(RarityExtension.LEGENDARY.getValue()), 9) + ); + public static final DeferredItem POSSIBLE_USEFUL_INGOT = registerAndAdd( + "possible_useful_ingot", + () -> new Item(new Item.Properties().rarity(RarityExtension.LEGENDARY.getValue())) + ); + public static final DeferredItem USEFUL_INGOT = registerAndAdd( + "useful_ingot", + () -> new IngotItem( + new Item.Properties().rarity(RarityExtension.LEGENDARY.getValue()), + AdvancedAlloyFurnaceBlockEntity.USEFUL_INGOT_FURNACE_TIER) + ); + // 金属模具 + public static final DeferredItem METAL_MOLD_PLATE = registerAndAdd( + "metal_mold_plate", + () -> new Item(new Item.Properties()) + ); + public static final DeferredItem METAL_MOLD_ROD = registerAndAdd( + "metal_mold_rod", + () -> new Item(new Item.Properties()) + ); + public static final DeferredItem METAL_MOLD_GEAR = registerAndAdd( + "metal_mold_gear", + () -> new Item(new Item.Properties()) + ); + public static final DeferredItem METAL_MOLD_WIRE = registerAndAdd( + "metal_mold_wire", + () -> new Item(new Item.Properties()) + ); + public static final DeferredItem METAL_MOLD_BLOCK = registerAndAdd( + "metal_mold_block", + () -> new Item(new Item.Properties()) + ); + // 无用锭对应的齿轮物品 + public static final DeferredItem USELESS_GEAR_TIER_1 = registerAndAdd( + "useless_gear_tier_1", + () -> new Item(new Item.Properties().rarity(Rarity.COMMON)) + ); + public static final DeferredItem USELESS_GEAR_TIER_2 = registerAndAdd( + "useless_gear_tier_2", + () -> new Item(new Item.Properties().rarity(Rarity.COMMON)) + ); + public static final DeferredItem USELESS_GEAR_TIER_3 = registerAndAdd( + "useless_gear_tier_3", + () -> new Item(new Item.Properties().rarity(Rarity.UNCOMMON)) + ); + public static final DeferredItem USELESS_GEAR_TIER_4 = registerAndAdd( + "useless_gear_tier_4", + () -> new Item(new Item.Properties().rarity(Rarity.UNCOMMON)) + ); + public static final DeferredItem USELESS_GEAR_TIER_5 = registerAndAdd( + "useless_gear_tier_5", + () -> new Item(new Item.Properties().rarity(Rarity.RARE)) + ); + public static final DeferredItem USELESS_GEAR_TIER_6 = registerAndAdd( + "useless_gear_tier_6", + () -> new Item(new Item.Properties().rarity(Rarity.RARE)) + ); + public static final DeferredItem USELESS_GEAR_TIER_7 = registerAndAdd( + "useless_gear_tier_7", + () -> new Item(new Item.Properties().rarity(Rarity.EPIC)) + ); + public static final DeferredItem USELESS_GEAR_TIER_8 = registerAndAdd( + "useless_gear_tier_8", + () -> new Item(new Item.Properties().rarity(RarityExtension.MYTHIC.getValue())) + ); + public static final DeferredItem USELESS_GEAR_TIER_9 = registerAndAdd( + "useless_gear_tier_9", + () -> new Item(new Item.Properties().rarity(RarityExtension.MYTHIC.getValue())) + ); + // 无用玻璃方块物品 + public static final DeferredItem USELESS_GLASS_TIER_1 = registerAndAdd( + "useless_glass_tier_1", + () -> new BlockItem(ModBlocks.USELESS_GLASS_TIER_1.get(), new Item.Properties().rarity(Rarity.COMMON)) + ); + public static final DeferredItem USELESS_GLASS_TIER_2 = registerAndAdd( + "useless_glass_tier_2", + () -> new BlockItem(ModBlocks.USELESS_GLASS_TIER_2.get(), new Item.Properties().rarity(Rarity.COMMON)) + ); + public static final DeferredItem USELESS_GLASS_TIER_3 = registerAndAdd( + "useless_glass_tier_3", + () -> new BlockItem(ModBlocks.USELESS_GLASS_TIER_3.get(), new Item.Properties().rarity(Rarity.UNCOMMON)) + ); + public static final DeferredItem USELESS_GLASS_TIER_4 = registerAndAdd( + "useless_glass_tier_4", + () -> new BlockItem(ModBlocks.USELESS_GLASS_TIER_4.get(), new Item.Properties().rarity(Rarity.UNCOMMON)) + ); + public static final DeferredItem USELESS_GLASS_TIER_5 = registerAndAdd( + "useless_glass_tier_5", + () -> new BlockItem(ModBlocks.USELESS_GLASS_TIER_5.get(), new Item.Properties().rarity(Rarity.RARE)) + ); + public static final DeferredItem USELESS_GLASS_TIER_6 = registerAndAdd( + "useless_glass_tier_6", + () -> new BlockItem(ModBlocks.USELESS_GLASS_TIER_6.get(), new Item.Properties().rarity(Rarity.RARE)) + ); + public static final DeferredItem USELESS_GLASS_TIER_7 = registerAndAdd( + "useless_glass_tier_7", + () -> new BlockItem(ModBlocks.USELESS_GLASS_TIER_7.get(), new Item.Properties().rarity(Rarity.EPIC)) + ); + public static final DeferredItem USELESS_GLASS_TIER_8 = registerAndAdd( + "useless_glass_tier_8", + () -> new BlockItem(ModBlocks.USELESS_GLASS_TIER_8.get(), new Item.Properties().rarity(RarityExtension.MYTHIC.getValue())) + ); + public static final DeferredItem USELESS_GLASS_TIER_9 = registerAndAdd( + "useless_glass_tier_9", + () -> new BlockItem(ModBlocks.USELESS_GLASS_TIER_9.get(), new Item.Properties().rarity(RarityExtension.LEGENDARY.getValue())) + ); + static final DeferredItem TELEPORT_BLOCK_ITEM = registerAndAdd( + "teleport_block", + () -> new BlockItem(ModBlocks.TELEPORT_BLOCK.get(), new Item.Properties()) + ); + static final DeferredItem TELEPORT_BLOCK_ITEM_2 = registerAndAdd( + "teleport_block_2", + () -> new BlockItem(ModBlocks.TELEPORT_BLOCK_2.get(), new Item.Properties()) + ); + static final DeferredItem TELEPORT_BLOCK_ITEM_3 = registerAndAdd( + "teleport_block_3", + () -> new BlockItem(ModBlocks.TELEPORT_BLOCK_3.get(), new Item.Properties()) + ); + // 温度调节器方块物品 + public static final DeferredItem TEMPERATURE_REGULATOR = registerAndAdd( + "temperature_regulator", + () -> new BlockItem(ModBlocks.TEMPERATURE_REGULATOR.get(), new Item.Properties()) + ); + static final DeferredItem ORE_GENERATOR_BLOCK = registerAndAdd( + "ore_generator_block", + () -> new BlockItem(ModBlocks.ORE_GENERATOR_BLOCK.get(), new Item.Properties()) + ); + static final DeferredItem ADVANCED_ALLOY_FURNACE_BLOCK = registerAndAdd( + "advanced_alloy_furnace_block", + () -> new AdvancedAlloyFurnaceBlockItem(ModBlocks.ADVANCED_ALLOY_FURNACE_BLOCK.get(), new Item.Properties().stacksTo(1)) + ); + public static final DeferredItem SUPERVISOR = registerAndAdd( + "supervisor", + () -> new BlockItem(ModBlocks.SUPERVISOR.get(), new Item.Properties()) + ); + public static final DeferredItem MULTIBLOCK_ALLOY_FURNACE_CORE = registerAndAdd( + "multiblock_alloy_furnace_core", + () -> new MultiblockAlloyFurnaceCoreBlockItem( + ModBlocks.MULTIBLOCK_ALLOY_FURNACE_CORE.get(), new Item.Properties())); + public static final DeferredItem ME_PATTERN_ASSEMBLY = registerAndAdd( + "me_pattern_assembly", + () -> new BlockItem(ModBlocks.ME_PATTERN_ASSEMBLY.get(), new Item.Properties().stacksTo(1))); + public static final DeferredItem OMNIVERSAL_MOLD_HUB = registerAndAdd( + "omniversal_mold_hub", + () -> new BlockItem(ModBlocks.OMNIVERSAL_MOLD_HUB.get(), new Item.Properties().stacksTo(1))); + public static final DeferredItem PASSIVE_CRAFTING_HATCH = registerAndAdd( + "passive_crafting_hatch", + () -> new BlockItem(ModBlocks.PASSIVE_CRAFTING_HATCH.get(), new Item.Properties().stacksTo(1))); + public static final DeferredItem OMNIVERSAL_FURNACE_CASING = registerAndAdd( + "omniversal_furnace_casing", + () -> new BlockItem(ModBlocks.OMNIVERSAL_FURNACE_CASING.get(), new Item.Properties())); + public static final List> USELESS_COILS = new ArrayList<>(); + public static final DeferredItem OMNIVERSAL_PATTERN = registerAndAdd( + "omniversal_pattern", + () -> new com.sorrowmist.useless.content.items.OmniversalPatternItem()); + public static final DeferredItem OMNIVERSAL_PATTERN_CONVERTER = registerAndAdd( + "omniversal_pattern_converter", + () -> new OmniversalPatternConverterItem()); + static { + for (int tier = UselessCoilBlock.MIN_TIER; tier <= UselessCoilBlock.MAX_TIER; tier++) { + int capturedTier = tier; + USELESS_COILS.add(registerAndAdd(UselessCoilBlock.registryName(tier), + () -> new BlockItem(ModBlocks.USELESS_COILS.get(capturedTier).get(), new Item.Properties()))); + } + } + + public static final DeferredItem AE2_GIFT_PACKAGE; + public static final DeferredItem RITUAL_BLUEPRINT; + + static { + if (ModList.get().isLoaded("ae2")) { + AE2_GIFT_PACKAGE = ITEMS.register( + "ae2_gift_package", + () -> new AE2GiftPackageItem(new Item.Properties().rarity(Rarity.EPIC).stacksTo(64)) + ); + CREATIVE_MAIN_TAB_ITEMS.add(AE2_GIFT_PACKAGE); + } else { + AE2_GIFT_PACKAGE = null; + } + + if (ModList.get().isLoaded("occultism")) { + RITUAL_BLUEPRINT = ITEMS.register( + "ritual_blueprint", + OccultismItems::createRitualBlueprint + ); + CREATIVE_MAIN_TAB_ITEMS.add(RITUAL_BLUEPRINT); + } else { + RITUAL_BLUEPRINT = null; + } + } + + private ModItems() {} + + /** Keeps the optional Occultism-backed item class out of ModItems' eager linkage. */ + private static final class OccultismItems { + private static Item createRitualBlueprint() { + return new RitualBlueprintItem(new Item.Properties().stacksTo(1)); + } + } + + private static DeferredItem registerAndAdd(String name, Supplier supplier) { + DeferredItem deferred = ITEMS.register(name, supplier); + CREATIVE_MAIN_TAB_ITEMS.add(deferred); + return deferred; + } +} diff --git a/src/main/java/com/sorrowmist/useless/init/ModMenuType.java b/src/main/java/com/sorrowmist/useless/init/ModMenuType.java new file mode 100644 index 00000000..8c21a281 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/init/ModMenuType.java @@ -0,0 +1,66 @@ +package com.sorrowmist.useless.init; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.menus.AdvancedAlloyFurnaceMenu; +import com.sorrowmist.useless.content.menus.ChainGroupMenu; +import com.sorrowmist.useless.content.menus.ContainerPatternConverter; +import com.sorrowmist.useless.content.menus.MePatternAssemblyMenu; +import com.sorrowmist.useless.content.menus.OmniversalMoldHubMenu; +import com.sorrowmist.useless.content.menus.MultiblockAlloyFurnaceMenu; +import com.sorrowmist.useless.content.menus.OreGeneratorMenu; +import com.sorrowmist.useless.content.menus.PassiveCraftingHatchMenu; +import com.sorrowmist.useless.content.menus.StaffLinkMenu; +import com.sorrowmist.useless.content.menus.DimensionConfigMenu; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.inventory.MenuType; +import net.minecraftforge.eventbus.api.IEventBus; +import net.minecraftforge.common.extensions.IForgeMenuType; +import com.sorrowmist.useless.compat.forge1201.registry.DeferredRegister; + +import java.util.function.Supplier; + +public final class ModMenuType { + private static final DeferredRegister> MENU_TYPES = + DeferredRegister.create(Registries.MENU, UselessMod.MODID); + public static final Supplier> ADVANCED_ALLOY_FURNACE_MENU = + MENU_TYPES.register("advanced_alloy_furnace_menu", + () -> IForgeMenuType.create(AdvancedAlloyFurnaceMenu::new) + ); + public static final Supplier> ME_PATTERN_ASSEMBLY_MENU = + MENU_TYPES.register("me_pattern_assembly_menu", + () -> IForgeMenuType.create(MePatternAssemblyMenu::new)); + public static final Supplier> OMNIVERSAL_MOLD_HUB_MENU = + MENU_TYPES.register("omniversal_mold_hub_menu", + () -> IForgeMenuType.create(OmniversalMoldHubMenu::new)); + public static final Supplier> MULTIBLOCK_ALLOY_FURNACE_MENU = + MENU_TYPES.register("multiblock_alloy_furnace_menu", + () -> IForgeMenuType.create(MultiblockAlloyFurnaceMenu::new)); + public static final Supplier> PASSIVE_CRAFTING_HATCH_MENU = + MENU_TYPES.register("passive_crafting_hatch_menu", + () -> IForgeMenuType.create(PassiveCraftingHatchMenu::new)); + public static final Supplier> ORE_GENERATOR_MENU = + MENU_TYPES.register("ore_generator_menu", + () -> IForgeMenuType.create(OreGeneratorMenu::new)); + public static final Supplier> DIMENSION_CONFIG_MENU = + MENU_TYPES.register("dimension_config_menu", + () -> IForgeMenuType.create(DimensionConfigMenu::new)); + public static final Supplier> PATTERN_CONVERTER_MENU = + MENU_TYPES.register("omniversal_pattern_converter", + () -> ContainerPatternConverter.TYPE); + public static final Supplier> STAFF_LINK_MENU = + MENU_TYPES.register("staff_link_menu", + () -> IForgeMenuType.create(StaffLinkMenu::new)); + /** + * 连锁等价组界面用的空菜单:只为让界面继承 {@code AbstractContainerScreen} + * (JEI / EMI 的原料侧栏只画在容器界面旁边),不承载任何数据、也不下发。 + */ + public static final Supplier> CHAIN_GROUP_MENU = + MENU_TYPES.register("chain_group_menu", + () -> IForgeMenuType.create(ChainGroupMenu::new)); + + private ModMenuType() {} + + public static void register(IEventBus eventBus) { + MENU_TYPES.register(eventBus); + } +} diff --git a/src/main/java/com/sorrowmist/useless/init/ModNetwork.java b/src/main/java/com/sorrowmist/useless/init/ModNetwork.java new file mode 100644 index 00000000..cb476b2e --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/init/ModNetwork.java @@ -0,0 +1,223 @@ +package com.sorrowmist.useless.init; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.network.AECancelPacket; +import com.sorrowmist.useless.network.AEReturnOutputTogglePacket; +import com.sorrowmist.useless.network.AETaskProgressPacket; +import com.sorrowmist.useless.network.AETaskProgressRequestPacket; +import com.sorrowmist.useless.network.AeLinkPreviewPacket; +import com.sorrowmist.useless.network.AlloyFurnaceErrorHighlightPacket; +import com.sorrowmist.useless.network.AeLinkPreviewRequestPacket; +import com.sorrowmist.useless.network.AutoIOChangePacket; +import com.sorrowmist.useless.network.BeefInvulnerabilitySyncPacket; +import com.sorrowmist.useless.network.BeefInvulnerabilityStatePacket; +import com.sorrowmist.useless.network.BeefToolLayoutRequestPacket; +import com.sorrowmist.useless.network.BeefToolLayoutResultPacket; +import com.sorrowmist.useless.network.BeefToolLayoutSyncPacket; +import com.sorrowmist.useless.network.BeefToolLayoutUpdatePacket; +import com.sorrowmist.useless.network.EnchantmentSwitchPacket; +import com.sorrowmist.useless.network.FaceModeChangePacket; +import com.sorrowmist.useless.network.ForceBreakKeyPacket; +import com.sorrowmist.useless.network.MiningDataSyncPacket; +import com.sorrowmist.useless.network.ModeTogglePacket; +import com.sorrowmist.useless.network.MultiblockAlloyFurnaceEnergyLimitPacket; +import com.sorrowmist.useless.network.PatternPageChangePacket; +import com.sorrowmist.useless.network.ThermostatSetPacket; +import com.sorrowmist.useless.network.ProtectEntityPacket; +import com.sorrowmist.useless.network.PassiveCraftingSettingsPacket; +import com.sorrowmist.useless.network.PassiveCraftingSlotMultiplierPacket; +import com.sorrowmist.useless.network.PassiveCraftingStatusPacket; +import com.sorrowmist.useless.network.OreGeneratorOutputTogglePacket; +import com.sorrowmist.useless.network.OreGeneratorSettingsPacket; +import com.sorrowmist.useless.network.RitualSatchelPlacePacket; +import com.sorrowmist.useless.network.RedstoneControlPacket; +import com.sorrowmist.useless.network.SelectOmniversalPatternRecipePacket; +import com.sorrowmist.useless.network.ShapeSwitchPacket; +import com.sorrowmist.useless.network.TabKeyPressedPacket; +import com.sorrowmist.useless.network.TeleportKeyPacket; +import com.sorrowmist.useless.network.TankClearPacket; +import com.sorrowmist.useless.network.ToolTypeModeSwitchPacket; +import com.sorrowmist.useless.network.ConstructionWandCorePacket; +import com.sorrowmist.useless.network.ConstructionWandPreviewPacket; +import com.sorrowmist.useless.network.ConstructionWandPreviewRequestPacket; +import com.sorrowmist.useless.network.DimensionConfigGhostSlotPacket; +import com.sorrowmist.useless.network.DimensionConfigSubmitPacket; +import com.sorrowmist.useless.network.StaffLinkBindPacket; +import com.sorrowmist.useless.network.StaffLinkConfigurePacket; +import com.sorrowmist.useless.network.StaffLinkCyclePacket; +import com.sorrowmist.useless.network.StaffLinkDetachPacket; +import com.sorrowmist.useless.network.StaffLinkHighlightPacket; +import com.sorrowmist.useless.network.StaffLinkHighlightRequestPacket; +import com.sorrowmist.useless.network.StaffLinkNetworkPacket; +import com.sorrowmist.useless.network.StaffLinkOpenPacket; +import com.sorrowmist.useless.network.StaffLinkRenamePacket; +import com.sorrowmist.useless.network.StaffLinkReorderPacket; +import com.sorrowmist.useless.network.StaffLinkSyncPacket; +import com.sorrowmist.useless.network.StaffLinkStressStatusPacket; +import com.sorrowmist.useless.compat.forge1201.network.RegisterPayloadHandlersEvent; + +public class ModNetwork { + public static void registerPayloadHandlers(final RegisterPayloadHandlersEvent event) { + var registrar = event.registrar(UselessMod.MODID).versioned("17"); + registrar.playToServer(BeefToolLayoutRequestPacket.TYPE, + BeefToolLayoutRequestPacket.STREAM_CODEC, + BeefToolLayoutRequestPacket::handle); + registrar.playToClient(BeefToolLayoutSyncPacket.TYPE, + BeefToolLayoutSyncPacket.STREAM_CODEC, + BeefToolLayoutSyncPacket::handle); + registrar.playToServer(BeefToolLayoutUpdatePacket.TYPE, + BeefToolLayoutUpdatePacket.STREAM_CODEC, + BeefToolLayoutUpdatePacket::handle); + registrar.playToClient(BeefToolLayoutResultPacket.TYPE, + BeefToolLayoutResultPacket.STREAM_CODEC, + BeefToolLayoutResultPacket::handle); + registrar.playToServer(EnchantmentSwitchPacket.TYPE, EnchantmentSwitchPacket.STREAM_CODEC, + EnchantmentSwitchPacket::handle + ); + registrar.playToServer(ToolTypeModeSwitchPacket.TYPE, ToolTypeModeSwitchPacket.STREAM_CODEC, + ToolTypeModeSwitchPacket::handle + ); + registrar.playToServer(ConstructionWandCorePacket.TYPE, ConstructionWandCorePacket.STREAM_CODEC, + ConstructionWandCorePacket::handle + ); + registrar.playToServer(AeLinkPreviewRequestPacket.TYPE, AeLinkPreviewRequestPacket.STREAM_CODEC, + AeLinkPreviewRequestPacket::handle + ); + registrar.playToClient(AeLinkPreviewPacket.TYPE, AeLinkPreviewPacket.STREAM_CODEC, + AeLinkPreviewPacket::handle + ); + registrar.playToServer(ConstructionWandPreviewRequestPacket.TYPE, + ConstructionWandPreviewRequestPacket.STREAM_CODEC, + ConstructionWandPreviewRequestPacket::handle + ); + registrar.playToClient(ConstructionWandPreviewPacket.TYPE, + ConstructionWandPreviewPacket.STREAM_CODEC, + ConstructionWandPreviewPacket::handle + ); + registrar.playToServer(TabKeyPressedPacket.TYPE, TabKeyPressedPacket.STREAM_CODEC, + TabKeyPressedPacket::handle + ); + registrar.playToServer(TeleportKeyPacket.TYPE, TeleportKeyPacket.STREAM_CODEC, + TeleportKeyPacket::handle + ); + registrar.playToServer(ModeTogglePacket.TYPE, ModeTogglePacket.STREAM_CODEC, + ModeTogglePacket::handle + ); + registrar.playToServer(ProtectEntityPacket.TYPE, ProtectEntityPacket.STREAM_CODEC, + ProtectEntityPacket::handle + ); + registrar.playToServer(ShapeSwitchPacket.TYPE, ShapeSwitchPacket.STREAM_CODEC, + ShapeSwitchPacket::handle + ); + registrar.playToServer(ForceBreakKeyPacket.TYPE, ForceBreakKeyPacket.STREAM_CODEC, + ForceBreakKeyPacket::handle + ); + registrar.playToServer(TankClearPacket.TYPE, TankClearPacket.STREAM_CODEC, + TankClearPacket::handle + ); + registrar.playToClient(MiningDataSyncPacket.TYPE, MiningDataSyncPacket.STREAM_CODEC, + MiningDataSyncPacket::handle + ); + registrar.playToClient(AETaskProgressPacket.TYPE, AETaskProgressPacket.STREAM_CODEC, + AETaskProgressPacket::handle + ); + registrar.playToServer(AETaskProgressRequestPacket.TYPE, AETaskProgressRequestPacket.STREAM_CODEC, + AETaskProgressRequestPacket::handle + ); + registrar.playToClient(BeefInvulnerabilitySyncPacket.TYPE, BeefInvulnerabilitySyncPacket.STREAM_CODEC, + BeefInvulnerabilitySyncPacket::handle + ); + registrar.playToClient(BeefInvulnerabilityStatePacket.TYPE, BeefInvulnerabilityStatePacket.STREAM_CODEC, + BeefInvulnerabilityStatePacket::handle + ); + registrar.playToServer(PatternPageChangePacket.TYPE, PatternPageChangePacket.STREAM_CODEC, + PatternPageChangePacket::handle + ); + registrar.playToServer(FaceModeChangePacket.TYPE, FaceModeChangePacket.STREAM_CODEC, + FaceModeChangePacket::handle + ); + registrar.playToServer(AutoIOChangePacket.TYPE, AutoIOChangePacket.STREAM_CODEC, + AutoIOChangePacket::handle + ); + registrar.playToServer(RedstoneControlPacket.TYPE, RedstoneControlPacket.STREAM_CODEC, + RedstoneControlPacket::handle + ); + registrar.playToServer(AECancelPacket.TYPE, AECancelPacket.STREAM_CODEC, + AECancelPacket::handle + ); + registrar.playToServer(AEReturnOutputTogglePacket.TYPE, AEReturnOutputTogglePacket.STREAM_CODEC, + AEReturnOutputTogglePacket::handle); + registrar.playToServer(SelectOmniversalPatternRecipePacket.TYPE, + SelectOmniversalPatternRecipePacket.STREAM_CODEC, + SelectOmniversalPatternRecipePacket::handle); + registrar.playToServer(PassiveCraftingSettingsPacket.TYPE, + PassiveCraftingSettingsPacket.STREAM_CODEC, + PassiveCraftingSettingsPacket::handle); + registrar.playToServer(PassiveCraftingSlotMultiplierPacket.TYPE, + PassiveCraftingSlotMultiplierPacket.STREAM_CODEC, + PassiveCraftingSlotMultiplierPacket::handle); + registrar.playToServer(MultiblockAlloyFurnaceEnergyLimitPacket.TYPE, + MultiblockAlloyFurnaceEnergyLimitPacket.STREAM_CODEC, + MultiblockAlloyFurnaceEnergyLimitPacket::handle); + registrar.playToClient(PassiveCraftingStatusPacket.TYPE, + PassiveCraftingStatusPacket.STREAM_CODEC, + PassiveCraftingStatusPacket::handle); + registrar.playToServer(OreGeneratorSettingsPacket.TYPE, + OreGeneratorSettingsPacket.STREAM_CODEC, + OreGeneratorSettingsPacket::handle); + registrar.playToServer(OreGeneratorOutputTogglePacket.TYPE, + OreGeneratorOutputTogglePacket.STREAM_CODEC, + OreGeneratorOutputTogglePacket::handle); + registrar.playToServer(DimensionConfigGhostSlotPacket.TYPE, + DimensionConfigGhostSlotPacket.STREAM_CODEC, + DimensionConfigGhostSlotPacket::handle); + registrar.playToServer(DimensionConfigSubmitPacket.TYPE, + DimensionConfigSubmitPacket.STREAM_CODEC, + DimensionConfigSubmitPacket::handle); + registrar.playToServer(RitualSatchelPlacePacket.TYPE, + RitualSatchelPlacePacket.STREAM_CODEC, + RitualSatchelPlacePacket::handle); + registrar.playToServer(StaffLinkOpenPacket.TYPE, + StaffLinkOpenPacket.STREAM_CODEC, + StaffLinkOpenPacket::handle); + registrar.playToServer(StaffLinkBindPacket.TYPE, + StaffLinkBindPacket.STREAM_CODEC, + StaffLinkBindPacket::handle); + registrar.playToServer(StaffLinkDetachPacket.TYPE, + StaffLinkDetachPacket.STREAM_CODEC, + StaffLinkDetachPacket::handle); + registrar.playToServer(StaffLinkReorderPacket.TYPE, + StaffLinkReorderPacket.STREAM_CODEC, + StaffLinkReorderPacket::handle); + registrar.playToServer(StaffLinkConfigurePacket.TYPE, + StaffLinkConfigurePacket.STREAM_CODEC, + StaffLinkConfigurePacket::handle); + registrar.playToServer(StaffLinkRenamePacket.TYPE, + StaffLinkRenamePacket.STREAM_CODEC, + StaffLinkRenamePacket::handle); + registrar.playToServer(StaffLinkCyclePacket.TYPE, + StaffLinkCyclePacket.STREAM_CODEC, + StaffLinkCyclePacket::handle); + registrar.playToServer(StaffLinkNetworkPacket.TYPE, + StaffLinkNetworkPacket.STREAM_CODEC, + StaffLinkNetworkPacket::handle); + registrar.playToClient(StaffLinkSyncPacket.TYPE, + StaffLinkSyncPacket.STREAM_CODEC, + StaffLinkSyncPacket::handle); + registrar.playToServer(StaffLinkHighlightRequestPacket.TYPE, + StaffLinkHighlightRequestPacket.STREAM_CODEC, + StaffLinkHighlightRequestPacket::handle); + registrar.playToClient(StaffLinkHighlightPacket.TYPE, + StaffLinkHighlightPacket.STREAM_CODEC, + StaffLinkHighlightPacket::handle); + registrar.playToClient(StaffLinkStressStatusPacket.TYPE, + StaffLinkStressStatusPacket.STREAM_CODEC, + StaffLinkStressStatusPacket::handle); + registrar.playToClient(AlloyFurnaceErrorHighlightPacket.TYPE, + AlloyFurnaceErrorHighlightPacket.STREAM_CODEC, + AlloyFurnaceErrorHighlightPacket::handle); + registrar.playToServer(ThermostatSetPacket.TYPE, + ThermostatSetPacket.STREAM_CODEC, + ThermostatSetPacket::handle); + } +} diff --git a/src/main/java/com/sorrowmist/useless/init/ModPOIs.java b/src/main/java/com/sorrowmist/useless/init/ModPOIs.java new file mode 100644 index 00000000..4ede2273 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/init/ModPOIs.java @@ -0,0 +1,39 @@ +package com.sorrowmist.useless.init; + +import com.google.common.collect.ImmutableSet; +import com.sorrowmist.useless.UselessMod; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.entity.ai.village.poi.PoiType; +import com.sorrowmist.useless.compat.forge1201.registry.DeferredHolder; +import com.sorrowmist.useless.compat.forge1201.registry.DeferredRegister; + +public class ModPOIs { + public static final DeferredRegister POI_TYPES = DeferredRegister.create(Registries.POINT_OF_INTEREST_TYPE, UselessMod.MODID); + + public static final DeferredHolder TELEPORT_PAD_POI = POI_TYPES.register( + "teleport_pad", + () -> new PoiType( + ImmutableSet.copyOf(ModBlocks.TELEPORT_BLOCK.get().getStateDefinition().getPossibleStates()), + 0, + 1 + ) + ); + + public static final DeferredHolder TELEPORT_PAD_2_POI = POI_TYPES.register( + "teleport_pad_2", + () -> new PoiType( + ImmutableSet.copyOf(ModBlocks.TELEPORT_BLOCK_2.get().getStateDefinition().getPossibleStates()), + 0, + 1 + ) + ); + + public static final DeferredHolder TELEPORT_PAD_3_POI = POI_TYPES.register( + "teleport_pad_3", + () -> new PoiType( + ImmutableSet.copyOf(ModBlocks.TELEPORT_BLOCK_3.get().getStateDefinition().getPossibleStates()), + 0, + 1 + ) + ); +} diff --git a/src/main/java/com/sorrowmist/useless/init/ModRecipeSerializers.java b/src/main/java/com/sorrowmist/useless/init/ModRecipeSerializers.java new file mode 100644 index 00000000..af493ea4 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/init/ModRecipeSerializers.java @@ -0,0 +1,27 @@ +package com.sorrowmist.useless.init; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipeSerializer; +import com.sorrowmist.useless.content.recipe.BeefUnbreakableRecipe; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.item.crafting.SimpleCraftingRecipeSerializer; +import com.sorrowmist.useless.compat.forge1201.registry.DeferredHolder; +import com.sorrowmist.useless.compat.forge1201.registry.DeferredRegister; + + +public class ModRecipeSerializers { + public static final DeferredRegister> RECIPE_SERIALIZERS = + DeferredRegister.create(Registries.RECIPE_SERIALIZER, UselessMod.MODID); + + public static final DeferredHolder, AdvancedAlloyFurnaceRecipeSerializer> ADVANCED_ALLOY_FURNACE_SERIALIZER = + RECIPE_SERIALIZERS.register("advanced_alloy_furnace", + () -> AdvancedAlloyFurnaceRecipeSerializer.INSTANCE + ); + + /** 造化杖 + 任意物品 → 该物品的不可破坏版本(特殊配方,无状态)。 */ + public static final DeferredHolder, SimpleCraftingRecipeSerializer> BEEF_UNBREAKABLE_SERIALIZER = + RECIPE_SERIALIZERS.register("beef_unbreakable", + () -> new SimpleCraftingRecipeSerializer<>(BeefUnbreakableRecipe::new) + ); +} diff --git a/src/main/java/com/sorrowmist/useless/init/ModRecipeTypes.java b/src/main/java/com/sorrowmist/useless/init/ModRecipeTypes.java new file mode 100644 index 00000000..d8909802 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/init/ModRecipeTypes.java @@ -0,0 +1,17 @@ +package com.sorrowmist.useless.init; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.item.crafting.RecipeType; +import com.sorrowmist.useless.compat.forge1201.registry.DeferredHolder; +import com.sorrowmist.useless.compat.forge1201.registry.DeferredRegister; + + +public class ModRecipeTypes { + public static final DeferredRegister> RECIPE_TYPES = DeferredRegister + .create(Registries.RECIPE_TYPE, UselessMod.MODID); + + public static final DeferredHolder, RecipeType> ADVANCED_ALLOY_FURNACE_TYPE = + RECIPE_TYPES.register("advanced_alloy_furnace", () -> AdvancedAlloyFurnaceRecipe.TYPE); +} diff --git a/src/main/java/com/sorrowmist/useless/init/ModSounds.java b/src/main/java/com/sorrowmist/useless/init/ModSounds.java new file mode 100644 index 00000000..6c53913b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/init/ModSounds.java @@ -0,0 +1,32 @@ +package com.sorrowmist.useless.init; + +import com.sorrowmist.useless.UselessMod; +import net.minecraft.core.registries.Registries; +import net.minecraft.sounds.SoundEvent; +import com.sorrowmist.useless.compat.forge1201.registry.DeferredHolder; +import com.sorrowmist.useless.compat.forge1201.registry.DeferredRegister; + +public final class ModSounds { + public static final DeferredRegister SOUNDS = + DeferredRegister.create(Registries.SOUND_EVENT, UselessMod.MODID); + + public static final DeferredHolder HOWL = SOUNDS.register( + "howl", + () -> SoundEvent.createVariableRangeEvent(UselessMod.id("howl")) + ); + + /** 杀戮光环开启提示音。 */ + public static final DeferredHolder KILL_AURA_ON = SOUNDS.register( + "kill_aura_on", + () -> SoundEvent.createVariableRangeEvent(UselessMod.id("kill_aura_on")) + ); + + /** 杀戮光环关闭提示音。 */ + public static final DeferredHolder KILL_AURA_OFF = SOUNDS.register( + "kill_aura_off", + () -> SoundEvent.createVariableRangeEvent(UselessMod.id("kill_aura_off")) + ); + + private ModSounds() { + } +} diff --git a/src/main/java/com/sorrowmist/useless/init/ModTags.java b/src/main/java/com/sorrowmist/useless/init/ModTags.java new file mode 100644 index 00000000..80adbbed --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/init/ModTags.java @@ -0,0 +1,58 @@ +package com.sorrowmist.useless.init; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.ItemTags; +import net.minecraft.tags.BlockTags; +import net.minecraft.tags.TagKey; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.item.Item; + +public final class ModTags { + + public static final TagKey CATALYSTS = createItemTag("catalysts"); + public static final TagKey MOLDS = createItemTag("molds"); + public static final TagKey OMNIVERSAL_FURNACE_CASINGS = + BlockTags.create(new ResourceLocation("useless_mod", "omniversal_furnace_casings")); + + /** + * 例外清单:即便造化杖开启了「无视工具挖掘等级」,这些方块仍按等级判定。 + * + *

默认留空。整合包 / 数据包可把需要保留进度门槛的方块加进来, + * 无需改动代码或关闭整体开关。 + */ + public static final TagKey BEEF_TOOL_TIER_LOCKED = createBlockTag("beef_tool_tier_locked"); + + /** + * 塑料方块总标签:涵盖全部四类塑料方块(普通 / 发光 / 连接纹理 / 发光连接纹理)。 + * + *

用于需要整体引用塑料方块的场合,例如把 #useless_mod:plastic_blocks + * 填入维度地板白名单即可一次放行全部塑料方块。 + */ + public static final TagKey PLASTIC_BLOCKS = createBlockTag("plastic_blocks"); + + /** 塑料方块对应的物品标签(与方块标签同名),供「任意塑料方块 → 温度调节器」这类配方引用。 */ + public static final TagKey PLASTIC_BLOCKS_ITEM = createItemTag("plastic_blocks"); + + /** 普通塑料方块(不发光、无连接纹理)。 */ + public static final TagKey PLASTIC = createBlockTag("plastic"); + + /** 发光塑料方块。 */ + public static final TagKey GLOW_PLASTIC = createBlockTag("glow_plastic"); + + /** 连接纹理塑料方块(CTM)。 */ + public static final TagKey PLASTIC_CTM = createBlockTag("plastic_ctm"); + + /** 发光连接纹理塑料方块(CTM)。 */ + public static final TagKey GLOW_PLASTIC_CTM = createBlockTag("glow_plastic_ctm"); + + private static TagKey createItemTag(String path) { + return ItemTags.create(new ResourceLocation("useless_mod", path)); + } + + private static TagKey createBlockTag(String path) { + return BlockTags.create(new ResourceLocation("useless_mod", path)); + } + + private ModTags() { + } +} diff --git a/src/main/java/com/sorrowmist/useless/integration/dataenergistics/jei/UselessDataEnergisticsJeiPlugin.java b/src/main/java/com/sorrowmist/useless/integration/dataenergistics/jei/UselessDataEnergisticsJeiPlugin.java new file mode 100644 index 00000000..9cf63d67 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/integration/dataenergistics/jei/UselessDataEnergisticsJeiPlugin.java @@ -0,0 +1,59 @@ +package com.sorrowmist.useless.integration.dataenergistics.jei; + +import com.fish_dan_.data_energistics.api.entrypoint.jei.DataEnergisticsJeiEntrypoint; +import com.fish_dan_.data_energistics.api.entrypoint.jei.DataEnergisticsJeiPlugin; +import com.fish_dan_.data_energistics.api.entrypoint.jei.DataEnergisticsJeiRegistry; +import com.fish_dan_.data_energistics.menu.universal.UniversalPatternEncodingTermMenu; +import com.fish_dan_.data_energistics.registry.DEMenus; +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.compat.jei.AdvancedAlloyFurnaceRecipeCategory; +import com.sorrowmist.useless.compat.jei.OmniversalPatternJeiTransferHandler; + +import appeng.menu.me.items.PatternEncodingTermMenu; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.inventory.MenuType; + +import org.jetbrains.annotations.NotNull; + +/** + * Makes Data Energistics's universal pattern encoding terminal preserve the exact Omniversal Pattern recipe, + * including its internal mold, when a player transfers a recipe from JEI. + */ +@DataEnergisticsJeiEntrypoint +public final class UselessDataEnergisticsJeiPlugin implements DataEnergisticsJeiPlugin { + private static final ResourceLocation OMNIVERSAL_PATTERN_TRANSFER_ID = + UselessMod.id("omniversal_pattern_transfer"); + + /** Public constructor required by Data Energistics's JEI entrypoint scanner. */ + public UselessDataEnergisticsJeiPlugin() {} + + @Override + public void register(@NotNull DataEnergisticsJeiRegistry registry) { + registerTransfer(registry, UniversalPatternEncodingTermMenu.class, + DEMenus.UNIVERSAL_PATTERN_ENCODING_TERM.get()); + } + + /** + * Registers the alloy-furnace category transfer for one concrete AE2-compatible pattern-encoding menu. + * + * @param registry Data Energistics's generic JEI registration surface + * @param menuClass exact terminal menu class + * @param menuType exact terminal menu registration + * @param concrete pattern-encoding menu type + */ + static void registerTransfer( + @NotNull DataEnergisticsJeiRegistry registry, + @NotNull Class menuClass, + @NotNull MenuType menuType) { + registry.registerRecipeTransferHandler( + OMNIVERSAL_PATTERN_TRANSFER_ID, + menuClass, + menuType, + AdvancedAlloyFurnaceRecipeCategory.TYPE, + helper -> new OmniversalPatternJeiTransferHandler<>( + menuClass, + menuType, + helper)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/integration/dataenergistics/provider/AlloyFurnaceBigIntegerAdapterFactory.java b/src/main/java/com/sorrowmist/useless/integration/dataenergistics/provider/AlloyFurnaceBigIntegerAdapterFactory.java new file mode 100644 index 00000000..3794aca2 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/integration/dataenergistics/provider/AlloyFurnaceBigIntegerAdapterFactory.java @@ -0,0 +1,56 @@ +package com.sorrowmist.useless.integration.dataenergistics.provider; + +import appeng.api.networking.crafting.ICraftingProvider; +import com.fish_dan_.data_energistics.api.crafting.dispatch.CountedCraftingProviderAdapter; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.CraftingTaskContext; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.BooleanSupplier; +import java.util.function.IntSupplier; +import java.util.function.Supplier; +import java.util.function.ToIntFunction; + +/** + * bigint 适配器子类的唯一引用点,独立成一个类是有意的。 + * + *

为什么不能直接在基类的工厂里 new 子类

+ * + *

{@link AlloyFurnaceBigIntegerCraftingAdapter} 实现了数据能源 3.3.0 才有的 + * {@code BigIntegerCraftingProviderAdapter}。基类 + * {@link AlloyFurnaceCountedCraftingAdapter} 在旧版数据能源下也会被加载,而 + * JVM 校验一个方法体时,为了确认「子类可赋值给基类」可能会去加载子类 —— + * 那在旧版下就是 NoClassDefFoundError。

+ * + *

把 {@code new} 挪到本类之后,基类的常量池里只有本类(两版都存在), + * 它自己校验时永远不需要子类;而本类的 class 文件虽然引用了子类,却只在能力探针为真时 + * 才会被触达({@link DataEnergisticsBigIntSupport#AVAILABLE})。 + * 这就是常见的「两级间接」写法,比反射干净:没有反射、没有安全检查开销, + * 换版本重启即自动切换。

+ */ +final class AlloyFurnaceBigIntegerAdapterFactory { + + private AlloyFurnaceBigIntegerAdapterFactory() { + } + + /** 构造 bigint 适配器子类实例。调用方必须已确认 {@link DataEnergisticsBigIntSupport#AVAILABLE}。 */ + static @NotNull CountedCraftingProviderAdapter create( + @NotNull ICraftingProvider provider, + @NotNull BooleanSupplier online, + @NotNull String routeIdentity, + @NotNull String machineIdentity, + @NotNull Supplier<@Nullable CraftingTaskContext> taskContext, + @NotNull ToIntFunction remainingThreads, + @NotNull IntSupplier totalThreads, + @NotNull AlloyFurnaceCountedCraftingAdapter.BigIntegerPush bigIntegerPush) { + return new AlloyFurnaceBigIntegerCraftingAdapter( + provider, + online, + routeIdentity, + machineIdentity, + taskContext, + remainingThreads, + totalThreads, + bigIntegerPush); + } +} diff --git a/src/main/java/com/sorrowmist/useless/integration/dataenergistics/provider/AlloyFurnaceBigIntegerCraftingAdapter.java b/src/main/java/com/sorrowmist/useless/integration/dataenergistics/provider/AlloyFurnaceBigIntegerCraftingAdapter.java new file mode 100644 index 00000000..0403090e --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/integration/dataenergistics/provider/AlloyFurnaceBigIntegerCraftingAdapter.java @@ -0,0 +1,152 @@ +package com.sorrowmist.useless.integration.dataenergistics.provider; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.networking.crafting.ICraftingProvider; +import appeng.api.stacks.KeyCounter; +import com.fish_dan_.data_energistics.api.crafting.dispatch.BigIntegerCraftingAdmission; +import com.fish_dan_.data_energistics.api.crafting.dispatch.BigIntegerCraftingProviderAdapter; +import com.fish_dan_.data_energistics.api.crafting.dispatch.CountedCraftingCapacity; +import com.fish_dan_.data_energistics.api.crafting.dispatch.CountedCraftingTarget; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.CraftingTaskContext; +import it.unimi.dsi.fastutil.objects.ObjectList; +import it.unimi.dsi.fastutil.objects.ObjectLists; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.math.BigInteger; +import java.util.function.BooleanSupplier; +import java.util.function.IntSupplier; +import java.util.function.Supplier; +import java.util.function.ToIntFunction; + +/** + * 万象合金炉适配器的 bigint(exact)派发层,数据能源 3.3.0 专有。 + * + *

为什么不直接合进基类

+ * + *

本类 implements {@link BigIntegerCraftingProviderAdapter},而那个接口是数据能源 + * 3.3.0 才有的。把它单独放一个类,是为了让基类 + * {@link AlloyFurnaceCountedCraftingAdapter} 能在旧版数据能源下正常加载 —— + * 旧版下 {@link DataEnergisticsBigIntSupport#AVAILABLE} 为假,基类的工厂就不会 new 本类, + * 于是本类永远不会被加载(类加载是按需的,只是"被 import"不算)。

+ * + *

语义

+ * + *

本层只加两件事,两者都不改长版 counted 的行为:

+ *
    + *
  • {@link #captureCapacityFast}:3.3.0 的快速容量入口(返回不可变 {@link ObjectList}), + * 与基类的 {@code captureCapacity} 共用同一条算术,口径不会漂移;
  • + *
  • {@link #prepareBigIntegerBatch}:exact 一次性准入 —— 拿到的 prototype 是单次推送的 + * 原型,{@code count} 只通过 {@link BigIntegerCraftingAdmission#exactCount()} 传递, + * 剩余 {@code count-1} 份材料由数据能源自己的 BigInteger 账本扣除。
  • + *
+ * + *

只有真正具备 bigint 语义的样板才会被发布带 machine identity 的 target(判定仍在基类的 + * {@code supportsExactBatch}),因为数据能源的 exact 分支一旦接管就不会回落长版路径。

+ */ +final class AlloyFurnaceBigIntegerCraftingAdapter extends AlloyFurnaceCountedCraftingAdapter + implements BigIntegerCraftingProviderAdapter { + + AlloyFurnaceBigIntegerCraftingAdapter( + @NotNull ICraftingProvider provider, + @NotNull BooleanSupplier online, + @NotNull String routeIdentity, + @NotNull String machineIdentity, + @NotNull Supplier<@Nullable CraftingTaskContext> taskContext, + @NotNull ToIntFunction remainingThreads, + @NotNull IntSupplier totalThreads, + @NotNull BigIntegerPush bigIntegerPush) { + super(provider, online, routeIdentity, machineIdentity, taskContext, remainingThreads, totalThreads, + bigIntegerPush); + } + + // ==================== 3.3.0 的快速容量入口 ==================== + + @Override + public @NotNull ObjectList<@NotNull CountedCraftingCapacity> captureCapacityFast( + @NotNull IPatternDetails patternDetails, KeyCounter @NotNull [] prototype, long requestedCount) { + CountedCraftingCapacity single = capacityEntry(patternDetails, prototype, requestedCount); + return single == null ? ObjectLists.emptyList() : ObjectLists.singleton(single); + } + + // ==================== 原生 bigint(exact)路径 ==================== + + @Override + public @Nullable BigIntegerCraftingAdmission prepareBigIntegerBatch( + @NotNull IPatternDetails patternDetails, + KeyCounter @NotNull [] prototype, + @NotNull BigInteger requestedCount, + @NotNull CountedCraftingTarget requestedTarget) { + if (requestedCount.signum() <= 0 || !targetFor(patternDetails).equals(requestedTarget)) { + return null; + } + BigInteger accepted = availableBigIntegerCount(patternDetails, prototype, requestedCount); + return accepted.signum() <= 0 + ? null + : new AlloyFurnaceBigIntegerAdmission(this, patternDetails, prototype, accepted); + } + + /** + * bigint 批次的准入:{@code exactCount()} 可以超过 {@code long},因此 {@code count()} 保持接口默认 + * ({@code longValueExact()} 会在超限时抛异常而不是截断)—— exact 路径的记账一律读 {@code exactCount()}。 + */ + private static final class AlloyFurnaceBigIntegerAdmission implements BigIntegerCraftingAdmission { + private AdmissionState state; + private final BigInteger count; + private boolean transferredInputOwnership; + + private AlloyFurnaceBigIntegerAdmission( + @NotNull AlloyFurnaceCountedCraftingAdapter adapter, + @NotNull IPatternDetails patternDetails, + KeyCounter @NotNull [] preparedPrototype, + @NotNull BigInteger count) { + this.state = new PreparedState(adapter, patternDetails, preparedPrototype); + this.count = count; + } + + @Override + public @NotNull BigInteger exactCount() { + return count; + } + + @Override + public long count() { + // 3.4.0 起 BigIntegerCraftingAdmission 把 CountedCraftingAdmission 的 count() + // 重新声明为抽象方法,实现必须显式给出:超过 long 时抛异常而非截断(exact 记账一律读 exactCount())。 + return count.longValueExact(); + } + + @Override + public boolean hasTransferredInputOwnership() { + return transferredInputOwnership; + } + + @Override + public boolean commit(KeyCounter @NotNull [] prototype) { + if (!(state instanceof PreparedState prepared)) { + throw new IllegalStateException("Exact admission has already been committed"); + } + AlloyFurnaceCountedCraftingAdapter adapter = prepared.adapter(); + IPatternDetails patternDetails = prepared.patternDetails(); + KeyCounter[] preparedPrototype = prepared.prototype(); + if (prototype != preparedPrototype) { + throw new IllegalArgumentException("Exact admission must be committed with its prepared prototype"); + } + state = ReleasedState.RELEASED; + boolean accepted = adapter.dispatchBigInteger(patternDetails, prototype, count); + transferredInputOwnership = accepted; + return accepted; + } + + private sealed interface AdmissionState permits PreparedState, ReleasedState {} + + private record PreparedState( + @NotNull AlloyFurnaceCountedCraftingAdapter adapter, + @NotNull IPatternDetails patternDetails, + KeyCounter @NotNull [] prototype) implements AdmissionState {} + + private enum ReleasedState implements AdmissionState { + RELEASED + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/integration/dataenergistics/provider/AlloyFurnaceCountedCraftingAdapter.java b/src/main/java/com/sorrowmist/useless/integration/dataenergistics/provider/AlloyFurnaceCountedCraftingAdapter.java new file mode 100644 index 00000000..e2071632 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/integration/dataenergistics/provider/AlloyFurnaceCountedCraftingAdapter.java @@ -0,0 +1,710 @@ +package com.sorrowmist.useless.integration.dataenergistics.provider; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.networking.crafting.ICraftingProvider; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import appeng.api.stacks.KeyCounter; +import appeng.blockentity.crafting.IMolecularAssemblerSupportedPattern; +import com.fish_dan_.data_energistics.api.crafting.dispatch.CountedCraftingAdmission; +import com.fish_dan_.data_energistics.api.crafting.dispatch.CountedCraftingCapacity; +import com.fish_dan_.data_energistics.api.crafting.dispatch.CountedCraftingProviderAdapter; +import com.fish_dan_.data_energistics.api.crafting.dispatch.CountedCraftingRoutingMode; +import com.fish_dan_.data_energistics.api.crafting.dispatch.CountedCraftingTarget; +import com.fish_dan_.data_energistics.api.registry.provider.runtime.PatternProviderIdentity; +import com.sorrowmist.useless.content.blockentities.AdvancedAlloyFurnaceBlockEntity; +import com.sorrowmist.useless.content.blockentities.multiblock.MePatternAssemblyBlockEntity; +import com.sorrowmist.useless.content.blockentities.multiblock.MultiblockAlloyFurnaceCoreBlockEntity; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.AlloyFurnaceBigIntegerCrafting; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.CraftingTaskContext; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.DynamicComponentPattern; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.OmniversalPatternDetails; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.SmartDoublingPatterns; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; +import java.util.OptionalLong; +import java.util.function.BooleanSupplier; +import java.util.function.IntSupplier; +import java.util.function.Supplier; +import java.util.function.ToIntFunction; + +/** + * Bridges one alloy-furnace AE provider to Data Energistics counted Trinity dispatch. + * + *

两条路径并存:

+ *
    + *
  • 长版 counted({@link #prepareBatch} / {@link #prepareBatchForTarget}):一次物理提交最多 + * {@code Long.MAX / 每次合成消耗量} 个合成(= 一个 AE2 long 物理窗口),由本机在虚拟 3×3 上折叠。
  • + *
  • 原生 bigint(子类 {@link AlloyFurnaceBigIntegerCraftingAdapter} 的 + * {@code prepareBigIntegerBatch}):DE 3.3.0 的 exact 派发。 + * 它只在「本 target 已被异步提案排他预留」时触发,一次能交付超过 long 的批次; + * 关键是这里拿到的 prototype 是单次合成的原型,count 只通过 {@code exactCount()} 传递, + * 剩余 count-1 份材料由 DE 自己的 BigInteger 账本扣除,我们只消费手里那一份原型。
  • + *
+ * + *

哪些样板发布 machine identity:DE 的 exact 分支要求容量快照带 + * provider 无关的 machine identity(排他预留的前提),而它一旦生效就会完全接管该 provider + * 的派发(不会再回落长版路径)。所以只给真正实现了 bigint 语义的样板发 machine target:

+ *
    + *
  • AE2 合成样板:本机一次装配即可折叠任意份数,始终发布 machine target;
  • + *
  • 万象样板:只有宿主声明了 {@link CraftingTaskContext#supportsBigIntegerRecipeBatches()} + * (目前只有多方块核心)才发布 —— 它把配方折叠成 BigInteger 批次并按 count 收能量;
  • + *
  • 其余样板(含单方块的万象样板)继续用 {@code route(...)} 目标走长版路径。
  • + *
+ * + *

安全阀:因为接管后不回落,「本机此刻吃不下」必须体现为容量为零, + * 而不是先发布带 machine identity 的容量再在提交时拒绝 —— 后者会把该 provider 卡死在 exact 分支。 + * 见 {@link #maximumRecipeBatchCount} 里的可用性/能量闸。

+ * + *

为什么 bigint 部分在子类里

+ * + *

{@code BigIntegerCraftingProviderAdapter} 是数据能源 3.3.0 才引入的接口,3.2.2 及更早没有。 + * 所以本类刻意只依赖两个版本都存在的 API(注意它实现的 {@link CountedCraftingProviderAdapter} + * 两版都有)—— 这样旧版数据能源下这个类仍能正常加载并走长版 counted 路径。bigint 专属的 + * {@code prepareBigIntegerBatch} / {@code captureCapacityFast} 放在子类 + * {@link AlloyFurnaceBigIntegerCraftingAdapter},且子类只在能力探针通过时才会被加载: + * 引用它的唯一地方是 {@link AlloyFurnaceBigIntegerAdapterFactory},而那个类只在探针为真的分支里被触达。 + * 结果是:旧版能启动(功能降级),数据能源升级到 3.3.0 后重启即自动恢复 bigint, + * 无需改代码或配置。

+ */ +class AlloyFurnaceCountedCraftingAdapter implements CountedCraftingProviderAdapter { + private final ICraftingProvider provider; + private final BooleanSupplier online; + /** provider 局部稳定 route 身份。 */ + private final String routeIdentity; + /** provider 无关的物理机器身份(维度@坐标)。 */ + private final String machineIdentity; + private final Supplier<@Nullable CraftingTaskContext> taskContext; + private final ToIntFunction remainingThreads; + private final IntSupplier totalThreads; + /** 机器侧的大数推送入口:单位原型 + BigInteger 次数。 */ + private final BigIntegerPush bigIntegerPush; + + /** Pushes one homogeneous bigint batch into the machine. */ + @FunctionalInterface + interface BigIntegerPush { + boolean push(@NotNull IPatternDetails pattern, @NotNull BigInteger count, KeyCounter @NotNull [] unitPrototype); + } + + AlloyFurnaceCountedCraftingAdapter( + @NotNull ICraftingProvider provider, + @NotNull BooleanSupplier online, + @NotNull String routeIdentity, + @NotNull String machineIdentity, + @NotNull Supplier<@Nullable CraftingTaskContext> taskContext, + @NotNull ToIntFunction remainingThreads, + @NotNull IntSupplier totalThreads, + @NotNull BigIntegerPush bigIntegerPush) { + this.provider = provider; + this.online = online; + this.routeIdentity = routeIdentity; + this.machineIdentity = machineIdentity; + this.taskContext = taskContext; + this.remainingThreads = remainingThreads; + this.totalThreads = totalThreads; + this.bigIntegerPush = bigIntegerPush; + } + + /** + * Creates the live adapter used by one standalone advanced alloy furnace. + * + *

按能力探针二选一:3.3.0 的 bigint API 存在 ⇒ 子类(含 exact 派发); + * 不存在 ⇒ 本类(只走长版 counted)。子类引用只出现在探针通过的那条分支里, + * 旧版数据能源下那一行永远不会执行,于是子类也永远不会被加载。

+ */ + static AlloyFurnaceCountedCraftingAdapter forAdvancedAlloyFurnace( + @NotNull AdvancedAlloyFurnaceBlockEntity provider, + @NotNull PatternProviderIdentity identity) { + String digest = identity.digest(); + BooleanSupplier online = () -> isAdvancedAlloyFurnaceOnline(provider); + String machineIdentity = + AlloyFurnaceBigIntegerCrafting.machineIdentity(digest, provider.getLevel(), provider.getBlockPos()); + Supplier<@Nullable CraftingTaskContext> taskContext = () -> provider; + if (DataEnergisticsBigIntSupport.AVAILABLE) { + // 子类引用只出现在 holder 里:探针为假时这一行不执行 ⇒ holder 与子类都不会被加载。 + return (AlloyFurnaceCountedCraftingAdapter) AlloyFurnaceBigIntegerAdapterFactory.create( + provider, + online, + digest, + machineIdentity, + taskContext, + provider::getRemainingAETaskCount, + provider::getMaxAETaskCount, + provider::pushBigIntegerCraftingPattern); + } + return new AlloyFurnaceCountedCraftingAdapter( + provider, + online, + digest, + machineIdentity, + taskContext, + provider::getRemainingAETaskCount, + provider::getMaxAETaskCount, + provider::pushBigIntegerCraftingPattern); + } + + /** Creates the live adapter used by one ME pattern assembly and its linked multiblock controller. */ + static AlloyFurnaceCountedCraftingAdapter forMePatternAssembly( + @NotNull MePatternAssemblyBlockEntity provider, + @NotNull PatternProviderIdentity identity) { + String digest = identity.digest(); + BooleanSupplier online = () -> isMePatternAssemblyOnline(provider); + String machineIdentity = + AlloyFurnaceBigIntegerCrafting.machineIdentity(digest, provider.getLevel(), provider.getBlockPos()); + Supplier<@Nullable CraftingTaskContext> taskContext = provider::getController; + ToIntFunction remainingThreads = craftingPattern -> { + MultiblockAlloyFurnaceCoreBlockEntity controller = provider.getController(); + return controller == null ? 0 : controller.getRemainingAETaskCount(craftingPattern); + }; + IntSupplier totalThreads = () -> { + MultiblockAlloyFurnaceCoreBlockEntity controller = provider.getController(); + return controller == null ? 0 : controller.getMaxAETaskCount(); + }; + if (DataEnergisticsBigIntSupport.AVAILABLE) { + // 同上:子类引用只出现在 holder 里。 + return (AlloyFurnaceCountedCraftingAdapter) AlloyFurnaceBigIntegerAdapterFactory.create( + provider, + online, + digest, + machineIdentity, + taskContext, + remainingThreads, + totalThreads, + provider::pushBigIntegerCraftingPattern); + } + return new AlloyFurnaceCountedCraftingAdapter( + provider, + online, + digest, + machineIdentity, + taskContext, + remainingThreads, + totalThreads, + provider::pushBigIntegerCraftingPattern); + } + + // ==================== 长版 counted 路径 ==================== + + @Override + public @Nullable CountedCraftingAdmission prepareBatch( + @NotNull IPatternDetails patternDetails, KeyCounter @NotNull [] prototype, long requestedCount) { + return prepareAdmission(patternDetails, prototype, requestedCount); + } + + /** 单条容量上报;容量为 0 时返回 {@code null}(两个版本的容量入口共用)。 */ + final @Nullable CountedCraftingCapacity capacityEntry( + @NotNull IPatternDetails patternDetails, KeyCounter @NotNull [] prototype, long requestedCount) { + AvailableCapacity capacity = availableCapacity(patternDetails, prototype, requestedCount); + if (capacity.logicalCrafts() == 0L) { + return null; + } + return new CountedCraftingCapacity( + targetFor(patternDetails), + CountedCraftingRoutingMode.TARGETED, + OptionalLong.of(capacity.logicalCrafts()), + OptionalLong.of(capacity.maximumSingleBatch())); + } + + @Override + public @Nullable CountedCraftingAdmission prepareBatchForTarget( + @NotNull IPatternDetails patternDetails, + KeyCounter @NotNull [] prototype, + long requestedCount, + @NotNull CountedCraftingTarget requestedTarget) { + return targetFor(patternDetails).equals(requestedTarget) + ? prepareAdmission(patternDetails, prototype, requestedCount) + : null; + } + + /** + * bigint 批次的可接受量。 + * + *

合成样板:本机能一次装配折叠任意份数,上限由产物分段预算反推。

+ *

万象样板:只有宿主声明了 bigint 能力(多方块)才接,上限交给 + * {@link AlloyFurnaceBigIntegerCrafting#maximumCount} —— 它把配方可用性、材料窗口、 + * 产物分段与能量四道闸一起算完。其它样板返回 0,让 DE 继续走长版 counted 路径。

+ * + *

「产物分段预算」由宿主的 AIMD 控制器给出(见 {@code CraftingTaskContext#outputSegmentBudget}): + * 它把单批规模收敛到「一批大约一两个 tick 交付完」,所以容量永不因积压归零, + * 调度侧不会停一拍再重启。

+ */ + @NotNull BigInteger availableBigIntegerCount( + @NotNull IPatternDetails patternDetails, + KeyCounter @NotNull [] prototype, + @NotNull BigInteger requestedCount) { + if (!online.getAsBoolean()) { + return BigInteger.ZERO; + } + IPatternDetails original = SmartDoublingPatterns.unwrap(patternDetails); + if (!provider.getAvailablePatterns().contains(original)) { + return BigInteger.ZERO; + } + if (prototype == null || prototype.length == 0) { + return BigInteger.ZERO; + } + int threads = machineThreads(); + long segmentBudget = segmentBudget(); + if (original instanceof IMolecularAssemblerSupportedPattern) { + // 能量闸需要任务上下文才能解析催化剂效果;宿主缺席时只能放过容量, + // 实际扣电由提交侧兜底(见 AlloyFurnaceBigIntegerCrafting#maximumCraftingPatternCount)。 + return AlloyFurnaceBigIntegerCrafting.maximumCraftingPatternCount( + this.taskContext.get(), original, prototype, threads, segmentBudget, requestedCount); + } + if (original instanceof OmniversalPatternDetails omniversal) { + CraftingTaskContext context = this.taskContext.get(); + if (!AlloyFurnaceBigIntegerCrafting.supports(context)) { + return BigInteger.ZERO; + } + return AlloyFurnaceBigIntegerCrafting.maximumCount( + context, omniversal, prototype, threads, segmentBudget, requestedCount); + } + return BigInteger.ZERO; + } + + /** 宿主的单批分段预算;没有宿主时退回 1(最保守,只影响容量不会崩)。 */ + private long segmentBudget() { + CraftingTaskContext context = this.taskContext.get(); + return context == null ? 1L : Math.max(1L, context.outputSegmentBudget()); + } + + /** + * 本机当前线程数:多方块跟随线圈线程({@code coil_tier_N_threads}),单方块跟随炉子等级线程 + * ({@code furnace_tier_N_threads})。 + * + *

它就是「一台机器一 tick 能接几份窗口」的并行度,bigint 批次的窗口预算直接用它 —— + * 不设上限:线程数配到多少就吃多少,代价(产物分段规模、内存、NBT 体积)由配置者承担。

+ */ + private int machineThreads() { + return Math.max(1, this.totalThreads.getAsInt()); + } + + /** exact 批次的实际提交:把单位原型与 BigInteger 次数交给机器。 */ + boolean dispatchBigInteger( + @NotNull IPatternDetails patternDetails, KeyCounter @NotNull [] unitPrototype, @NotNull BigInteger count) { + if (availableBigIntegerCount(patternDetails, unitPrototype, count).compareTo(count) < 0) { + return false; + } + return this.bigIntegerPush.push(patternDetails, count, unitPrototype); + } + + // ==================== 共用:目标与容量 ==================== + + /** + * 合成样板、以及声明了 bigint 能力的万象样板,发布带 machine identity 的 TARGETED 目标 + * (exact 派发的前提);其余样板只发 route 目标 —— 这样 DE 不会对它们启用 exact 分支 + * (那会跳过长版路径,而它们没有 bigint 语义)。 + */ + @NotNull CountedCraftingTarget targetFor(@NotNull IPatternDetails patternDetails) { + return supportsExactBatch(patternDetails) + ? CountedCraftingTarget.machine(this.routeIdentity, this.machineIdentity) + : CountedCraftingTarget.route(this.routeIdentity); + } + + /** + * 该样板是否具备 bigint(exact)语义。 + * + *

万象样板只有在宿主声明了 {@link CraftingTaskContext#supportsBigIntegerRecipeBatches()} + * 时才算 —— 单方块高级合金炉没有实现折叠语义,给它发布 machine identity 会让 DE 切进 + * exact 分支且永不回落,而那边根本没有 bigint 接收入口。

+ */ + private boolean supportsExactBatch(@NotNull IPatternDetails patternDetails) { + IPatternDetails original = SmartDoublingPatterns.unwrap(patternDetails); + if (original instanceof IMolecularAssemblerSupportedPattern) { + return true; + } + return original instanceof OmniversalPatternDetails + && AlloyFurnaceBigIntegerCrafting.supports(this.taskContext.get()); + } + + /** Returns the largest safe count for one physical scaled-pattern submission. */ + static long maximumBatchCount( + @NotNull IPatternDetails patternDetails, + KeyCounter @NotNull [] prototype, + long requestedCount) { + validateRequestedCount(requestedCount); + SmartDoublingPatterns.Resolved execution = SmartDoublingPatterns.resolve(patternDetails); + long maximumCount = SmartDoublingPatterns.maximumSafeMultiplier(execution.pattern()) + / execution.operationsPerPush(); + for (KeyCounter counter : prototype) { + for (var entry : counter) { + long amount = entry.getLongValue(); + if (amount < 0L) { + throw new IllegalArgumentException("Crafting input amounts must not be negative"); + } + if (amount > 0L) { + maximumCount = Math.min(maximumCount, Long.MAX_VALUE / amount); + } + } + } + return Math.min(requestedCount, maximumCount); + } + + /** + * Creates a deep, exact scaled input snapshot without changing the caller-owned prototype. + * + *

一律按倍率放大:含返还物的样板已被 {@link #availableCapacity} 挡在 counted 批量之外 + * (见 {@link SmartDoublingPatterns#reusableInputSlots}),走不到这里。

+ */ + static KeyCounter[] scalePrototype(@NotNull IPatternDetails pattern, + KeyCounter @NotNull [] prototype, + long count, + @Nullable Level level) { + if (count <= 0L) { + throw new IllegalArgumentException("Counted crafting batch size must be positive"); + } + KeyCounter[] scaled = new KeyCounter[prototype.length]; + for (int index = 0; index < prototype.length; index++) { + KeyCounter source = prototype[index]; + KeyCounter targetCounter = new KeyCounter(); + for (var entry : source) { + long amount = entry.getLongValue(); + if (amount < 0L) { + throw new IllegalArgumentException("Crafting input amounts must not be negative"); + } + targetCounter.add(entry.getKey(), Math.multiplyExact(amount, count)); + } + scaled[index] = targetCounter; + } + return scaled; + } + + private @Nullable CountedCraftingAdmission prepareAdmission( + @NotNull IPatternDetails patternDetails, + KeyCounter @NotNull [] prototype, + long requestedCount) { + long acceptedCount = availableCapacity(patternDetails, prototype, requestedCount).logicalCrafts(); + return acceptedCount == 0L + ? null + : new AlloyFurnaceCountedCraftingAdmission(this, patternDetails, prototype, acceptedCount); + } + + AvailableCapacity availableCapacity( + @NotNull IPatternDetails patternDetails, + KeyCounter @NotNull [] prototype, + long requestedCount) { + validateRequestedCount(requestedCount); + if (!online.getAsBoolean()) { + return AvailableCapacity.EMPTY; + } + IPatternDetails original = SmartDoublingPatterns.unwrap(patternDetails); + if (!provider.getAvailablePatterns().contains(original)) { + return AvailableCapacity.EMPTY; + } + // 含返还物的样板一律不接 counted 批量:AE2 对这类输入只算 1 份,按倍率放大原型会向机器 + // 索要 count 份催化剂而实际只有 1 份。这里「退让」(容量 0)而不是拒收 —— + // 上层会回落 AE2 原生单份路径,永远正确,只是慢。 + // 关卡从本机取(providerLevel),不从 AE2 网格取(那要挂 CraftingService 的 mixin)。 + if (!SmartDoublingPatterns.reusableInputSlots(original, providerLevel()).isEmpty()) { + return AvailableCapacity.EMPTY; + } + long arithmeticMaximum = maximumBatchCount( + patternDetails, + prototype, + requestedCount); + CapacityLimits limits = maximumRecipeBatchCount(patternDetails, prototype, arithmeticMaximum); + if (limits.logicalMaximum() == 0L) { + return AvailableCapacity.EMPTY; + } + + IPatternDetails executionPattern = SmartDoublingPatterns.unwrap(patternDetails); + boolean craftingPattern = executionPattern instanceof IMolecularAssemblerSupportedPattern; + int remaining = Math.max(0, this.remainingThreads.applyAsInt(craftingPattern)); + int totalThreads = Math.max(0, this.totalThreads.getAsInt()); + int availableThreads = Math.min(remaining, totalThreads); + if (craftingPattern) { + // 合成样板/大数批次的槽位来自「回网队列」。积压时不再把槽位打到 0 + //(那等于二元拒绝,调度侧会停一拍再重启),而是保留 1 条 —— + // 单批规模由宿主的 AIMD 分段预算控制,它会把批次收敛到「一两个 tick 交付完」, + // 所以保留 1 条不会真的压垮队列。 + availableThreads = Math.max(1, availableThreads); + CraftingTaskContext context = this.taskContext.get(); + long perThreadCapacity = context == null + ? 1L : Math.max(1L, context.getCraftingPatternCapacity()); + long aggregateCapacity = saturatingMultiply(perThreadCapacity, availableThreads); + long logicalCrafts = Math.min(limits.logicalMaximum(), aggregateCapacity); + long maximumSingleBatch = logicalCrafts; + return logicalCrafts == 0L || maximumSingleBatch == 0L + ? AvailableCapacity.EMPTY + : new AvailableCapacity(logicalCrafts, maximumSingleBatch); + } + if (limits.singleThreadMaximum() == 0L) { + return AvailableCapacity.EMPTY; + } + long aggregateMaximum = saturatingMultiply(limits.singleThreadMaximum(), availableThreads); + long logicalCrafts = Math.min(limits.logicalMaximum(), aggregateMaximum); + long maximumSingleBatch = Math.min(limits.logicalMaximum(), limits.singleThreadMaximum()); + return logicalCrafts == 0L || maximumSingleBatch == 0L + ? AvailableCapacity.EMPTY + : new AvailableCapacity(logicalCrafts, maximumSingleBatch); + } + + private CapacityLimits maximumRecipeBatchCount( + IPatternDetails patternDetails, + KeyCounter[] prototype, + long arithmeticMaximum) { + CraftingTaskContext context = this.taskContext.get(); + if (arithmeticMaximum == 0L) { + return CapacityLimits.EMPTY; + } + SmartDoublingPatterns.Resolved execution = SmartDoublingPatterns.resolve(patternDetails); + if (execution.pattern() instanceof IMolecularAssemblerSupportedPattern) { + // 合成样板由本机在虚拟 3×3 上自执行,单次推送即可折叠整批,不存在配方侧并行概念。 + return new CapacityLimits(arithmeticMaximum, arithmeticMaximum); + } + if (context == null) { + return new CapacityLimits(arithmeticMaximum, arithmeticMaximum); + } + AdvancedAlloyFurnaceRecipe recipe = context.resolveTaskRecipe( + patternDetails, + List.of(), + List.of(), + compactInputs(prototype), + execution.operationsPerPush()); + if (recipe == null) { + return CapacityLimits.EMPTY; + } + + if (execution.pattern() instanceof OmniversalPatternDetails + && AlloyFurnaceBigIntegerCrafting.supports(context)) { + // 安全阀:本机一旦吃不下(档次/模具/能量任一不满足),必须发布「零容量」而不是带 + // machine identity 的容量 —— DE 的 exact 分支一旦接管就不会再回落长版路径, + // 先发布容量再在提交时拒绝会把该 provider 卡死。 + if (!context.isTaskRecipeAvailable(recipe)) { + return CapacityLimits.EMPTY; + } + BigInteger energyCap = AlloyFurnaceBigIntegerCrafting.maximumCountForEnergy(context, recipe); + if (energyCap != null && energyCap.signum() <= 0) { + return CapacityLimits.EMPTY; + } + } + + long manualOperations = SmartDoublingPatterns.manualOperationsPerPattern( + recipe, execution.pattern()); + if (manualOperations == 0L) { + return CapacityLimits.EMPTY; + } + BigInteger wrapperOperations = BigInteger.valueOf(execution.operationsPerPush()); + BigInteger operations = wrapperOperations + .multiply(BigInteger.valueOf(manualOperations)); + long maximum; + if (usesRecipeOutputs(execution.pattern())) { + Object2ObjectLinkedOpenHashMap outputs = new Object2ObjectLinkedOpenHashMap<>(); + recipe.outputs().forEach(output -> mergeOutput(outputs, GenericStack.fromItemStack(output))); + recipe.outputFluids().forEach(output -> mergeOutput(outputs, GenericStack.fromFluidStack(output))); + recipe.keyOutputs().forEach(output -> mergeOutput(outputs, output)); + maximum = limitByOutputs(arithmeticMaximum, outputs, operations); + } else { + Object2ObjectLinkedOpenHashMap declaredOutputs = new Object2ObjectLinkedOpenHashMap<>(); + execution.pattern().getOutputs().forEach(output -> mergeOutput(declaredOutputs, output)); + maximum = limitByOutputs(arithmeticMaximum, declaredOutputs, wrapperOperations); + if (maximum == 0L) { + return CapacityLimits.EMPTY; + } + + Object2ObjectLinkedOpenHashMap hiddenOutputs = new Object2ObjectLinkedOpenHashMap<>(); + for (GenericStack output : recipe.keyOutputs()) { + boolean declared = execution.pattern().getOutputs().stream() + .anyMatch(patternOutput -> output.what().equals(patternOutput.what())); + if (!declared) { + mergeOutput(hiddenOutputs, output); + } + } + maximum = limitByOutputs(maximum, hiddenOutputs, operations); + } + + if (maximum == 0L) { + return CapacityLimits.EMPTY; + } + long parallel = Math.max(1L, context.getTaskParallel(recipe, context.resolveTaskEffect(recipe))); + long singleThreadMaximum = divideByBigInteger(parallel, operations); + return singleThreadMaximum == 0L + ? CapacityLimits.EMPTY + : new CapacityLimits(maximum, singleThreadMaximum); + } + + private static long limitByOutputs( + long currentMaximum, + Object2ObjectLinkedOpenHashMap outputs, + BigInteger operationMultiplier) { + BigInteger maximumLong = BigInteger.valueOf(Long.MAX_VALUE); + long maximum = currentMaximum; + for (BigInteger amount : outputs.values()) { + BigInteger perLogicalCraft = amount.multiply(operationMultiplier); + if (perLogicalCraft.compareTo(maximumLong) > 0) { + return 0L; + } + maximum = Math.min(maximum, maximumLong.divide(perLogicalCraft).longValueExact()); + } + return maximum; + } + + private static boolean usesRecipeOutputs(IPatternDetails pattern) { + return pattern instanceof OmniversalPatternDetails + || pattern instanceof DynamicComponentPattern dynamic && dynamic.usesDynamicOutputs(); + } + + private static List compactInputs(KeyCounter[] prototype) { + Object2ObjectLinkedOpenHashMap inputs = new Object2ObjectLinkedOpenHashMap<>(); + for (KeyCounter counter : prototype) { + for (var entry : counter) { + inputs.merge(entry.getKey(), BigInteger.valueOf(entry.getLongValue()), BigInteger::add); + } + } + BigInteger maximumLong = BigInteger.valueOf(Long.MAX_VALUE); + ArrayList result = new ArrayList<>(inputs.size()); + for (var entry : inputs.object2ObjectEntrySet()) { + BigInteger remaining = entry.getValue(); + while (remaining.compareTo(maximumLong) > 0) { + result.add(new GenericStack(entry.getKey(), Long.MAX_VALUE)); + remaining = remaining.subtract(maximumLong); + } + if (remaining.signum() > 0) { + result.add(new GenericStack(entry.getKey(), remaining.longValueExact())); + } + } + return result; + } + + private static void mergeOutput( + Object2ObjectLinkedOpenHashMap outputs, + @Nullable GenericStack output) { + if (output == null || output.amount() <= 0L) { + return; + } + outputs.merge(output.what(), BigInteger.valueOf(output.amount()), BigInteger::add); + } + + private boolean dispatch( + @NotNull IPatternDetails patternDetails, + KeyCounter @NotNull [] prototype, + long count) { + if (availableCapacity(patternDetails, prototype, count).logicalCrafts() < count) { + return false; + } + // 判定「可复用输入」要关卡:注魔水晶这类带耐久返还的催化剂只有拿到 Level 才认得出来。 + Level level = providerLevel(); + // 统一走 SmartDoublingPatterns.scale:合成样板必须保留 IMolecularAssemblerSupportedPattern 身份, + // 否则接收方会把它当成处理样板去查合金炉配方。 + IPatternDetails scaledPattern = SmartDoublingPatterns.scale(patternDetails, count); + KeyCounter[] scaledPrototype = scalePrototype(patternDetails, prototype, count, level); + return provider.pushPattern(scaledPattern, scaledPrototype); + } + + /** 取 provider 所在关卡;非方块实体返回 {@code null}(退回保守判据,只少修一类)。 */ + private @Nullable Level providerLevel() { + return this.provider instanceof BlockEntity blockEntity ? blockEntity.getLevel() : null; + } + + private static boolean isAdvancedAlloyFurnaceOnline(@NotNull AdvancedAlloyFurnaceBlockEntity provider) { + Level level = provider.getLevel(); + return level != null + && !level.isClientSide + && !provider.isRemoved() + && provider.getMainNode().isActive(); + } + + private static boolean isMePatternAssemblyOnline(@NotNull MePatternAssemblyBlockEntity provider) { + Level level = provider.getLevel(); + return level != null + && !level.isClientSide + && !provider.isRemoved() + && provider.getMainNode().isActive() + && provider.getController() != null; + } + + private static void validateRequestedCount(long requestedCount) { + if (requestedCount <= 0L) { + throw new IllegalArgumentException("Requested counted crafting amount must be positive"); + } + } + + private static long saturatingMultiply(long left, int right) { + if (left <= 0L || right <= 0) { + return 0L; + } + return left > Long.MAX_VALUE / right ? Long.MAX_VALUE : left * right; + } + + private static long divideByBigInteger(long dividend, BigInteger divisor) { + if (dividend <= 0L || divisor.signum() <= 0) { + return 0L; + } + BigInteger result = BigInteger.valueOf(dividend).divide(divisor); + return result.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) >= 0 + ? Long.MAX_VALUE : result.longValueExact(); + } + + private record CapacityLimits(long logicalMaximum, long singleThreadMaximum) { + private static final CapacityLimits EMPTY = new CapacityLimits(0L, 0L); + } + + private record AvailableCapacity(long logicalCrafts, long maximumSingleBatch) { + private static final AvailableCapacity EMPTY = new AvailableCapacity(0L, 0L); + } + + /** One-shot admission that owns only temporary dispatch references until it is committed. */ + private static final class AlloyFurnaceCountedCraftingAdmission implements CountedCraftingAdmission { + private AdmissionState state; + private final long count; + private boolean transferredInputOwnership; + + private AlloyFurnaceCountedCraftingAdmission( + @NotNull AlloyFurnaceCountedCraftingAdapter adapter, + @NotNull IPatternDetails patternDetails, + KeyCounter @NotNull [] preparedPrototype, + long count) { + this.state = new PreparedAdmissionState(adapter, patternDetails, preparedPrototype); + this.count = count; + } + + @Override + public long count() { + return count; + } + + @Override + public boolean hasTransferredInputOwnership() { + return transferredInputOwnership; + } + + @Override + public boolean commit(KeyCounter @NotNull [] prototype) { + if (!(state instanceof PreparedAdmissionState prepared)) { + throw new IllegalStateException("Admission has already been committed"); + } + AlloyFurnaceCountedCraftingAdapter adapter = prepared.adapter(); + IPatternDetails patternDetails = prepared.patternDetails(); + KeyCounter[] preparedPrototype = prepared.prototype(); + if (prototype != preparedPrototype) { + throw new IllegalArgumentException("Admission must be committed with its prepared prototype"); + } + state = ReleasedAdmissionState.RELEASED; + boolean accepted = adapter.dispatch(patternDetails, prototype, count); + transferredInputOwnership = accepted; + return accepted; + } + + /** The admission's retained state is either dispatchable once or already released. */ + private sealed interface AdmissionState permits PreparedAdmissionState, ReleasedAdmissionState {} + + /** Retains the server-thread data required to execute exactly one provider dispatch. */ + private record PreparedAdmissionState( + @NotNull AlloyFurnaceCountedCraftingAdapter adapter, + @NotNull IPatternDetails patternDetails, + KeyCounter @NotNull [] prototype) implements AdmissionState {} + + /** Drops all temporary references as soon as the only commit attempt begins. */ + private enum ReleasedAdmissionState implements AdmissionState { + RELEASED + } + } + +} diff --git a/src/main/java/com/sorrowmist/useless/integration/dataenergistics/provider/DataEnergisticsBigIntSupport.java b/src/main/java/com/sorrowmist/useless/integration/dataenergistics/provider/DataEnergisticsBigIntSupport.java new file mode 100644 index 00000000..7fd59825 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/integration/dataenergistics/provider/DataEnergisticsBigIntSupport.java @@ -0,0 +1,75 @@ +package com.sorrowmist.useless.integration.dataenergistics.provider; + +import com.mojang.logging.LogUtils; +import org.slf4j.Logger; + +/** + * 「数据能源的 bigint API 到底在不在」的能力探针。 + * + *

为什么需要它

+ * + *

{@code BigIntegerCraftingProviderAdapter} / {@code BigIntegerCraftingAdmission} 是数据能源 + * 3.3.0 才引入的接口,3.2.2 及更早没有。我们的 bigint 适配器子类 + * {@link AlloyFurnaceBigIntegerCraftingAdapter} 实现了它,所以只要那个子类被加载, + * 旧版数据能源下就会 NoClassDefFoundError。

+ * + *

解决办法不是反射,而是把子类的加载推迟到一个运行期判定之后: + * {@link AlloyFurnaceCountedCraftingAdapter#forAdvancedAlloyFurnace} 与 + * {@link AlloyFurnaceCountedCraftingAdapter#forMePatternAssembly} 先读这里的 + * {@link #AVAILABLE},为真才去 new 子类。于是:

+ * + *
    + *
  • 旧版数据能源:探针为假 ⇒ 子类永不加载 ⇒ 只走长版 counted 路径,能正常启动;
  • + *
  • 升级到 3.3.0 之后:重启时探针为真 ⇒ 自动启用原生 bigint 派发。
  • + *
+ * + *

结论是「装旧版能玩、换新版自动恢复」,不需要改代码、不需要配置。本类的静态字段 + * 只在第一次触达时求值一次,因此换版本必须重启游戏(这本来就成立)。

+ * + *

为什么先查资源再 Class.forName

+ * + *

资源查找({@code getResource})只看 jar 里有没有这个 class 文件,完全不触发类加载与链接, + * 是最便宜的否定路径;只有它命中时才做一次 {@code Class.forName(initialize=false)} 确认类型 + * 真的可解析。任何一步失败都算「不可用」并降级 —— 这是刻意的:宁可少一个特性,也不要启动时崩。

+ */ +final class DataEnergisticsBigIntSupport { + + private static final Logger LOGGER = LogUtils.getLogger(); + + /** 3.3.0 才引入的接口;它的存在与否就是「原生 bigint 派发可用吗」的唯一判据。 */ + private static final String BIGINT_ADAPTER_TYPE = + "com.fish_dan_.data_energistics.api.crafting.dispatch.BigIntegerCraftingProviderAdapter"; + + /** 数据能源是否提供 3.3.0 的 bigint 派发 API(进程内只求值一次)。 */ + static final boolean AVAILABLE = probe(); + + private DataEnergisticsBigIntSupport() { + } + + private static boolean probe() { + boolean available = classFilePresent() && typeResolvable(); + if (available) { + LOGGER.info("DataEnergistics bigint API detected: native bigint dispatch enabled"); + } else { + LOGGER.info("DataEnergistics bigint API absent (pre-3.3.0): long-window counted dispatch only"); + } + return available; + } + + /** 只查 jar 里的 class 文件,不触发加载/链接。 */ + private static boolean classFilePresent() { + String resource = BIGINT_ADAPTER_TYPE.replace('.', '/') + ".class"; + ClassLoader loader = DataEnergisticsBigIntSupport.class.getClassLoader(); + return loader != null && loader.getResource(resource) != null; + } + + /** 确认类型真的可解析({@code initialize=false}:不跑静态初始化,不牵连依赖)。 */ + private static boolean typeResolvable() { + try { + Class.forName(BIGINT_ADAPTER_TYPE, false, DataEnergisticsBigIntSupport.class.getClassLoader()); + return true; + } catch (Throwable unavailable) { + return false; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/integration/dataenergistics/provider/OmniversalDynamicCraftingOutputAdapter.java b/src/main/java/com/sorrowmist/useless/integration/dataenergistics/provider/OmniversalDynamicCraftingOutputAdapter.java new file mode 100644 index 00000000..c0abbb47 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/integration/dataenergistics/provider/OmniversalDynamicCraftingOutputAdapter.java @@ -0,0 +1,70 @@ +package com.sorrowmist.useless.integration.dataenergistics.provider; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.GenericStack; +import com.fish_dan_.data_energistics.api.crafting.dynamic.DynamicCraftingOutput; +import com.fish_dan_.data_energistics.api.crafting.dynamic.DynamicCraftingOutputAdapter; +import com.fish_dan_.data_energistics.api.crafting.dynamic.DynamicCraftingOutputSemantics; +import com.fish_dan_.data_energistics.api.crafting.matching.ProcessingMatchMode; +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.OmniversalPatternDetails; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.SmartDoublingPatterns; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import it.unimi.dsi.fastutil.objects.ObjectList; +import it.unimi.dsi.fastutil.objects.ObjectLists; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Optional; + +/** + * 把万象样板的「id-only 产物槽」声明给数据能源,让它的 bigint(exact)分支也能对上账。 + * + *

为什么需要它:万象样板里被标记为「只按物品 id 匹配」的产物槽,其声明键只是模板 —— + * 实际落进网络的是配方产出的键(可能带着运行时组件)。数据能源的计划会把声明键写进 + * CPU 的 {@code waitingFor},如果实际键与模板键不等,那条等待就永远不会被满足。

+ * + *

注册本适配器后,数据能源会把这些槽按 {@link ProcessingMatchMode#ID} + * 处理(按注册物品匹配、忽略数据组件),并且保留机器返回的完整键 —— 它只放宽「哪条等待可以 + * 接受这个键」,从不把返回的键改写成模板。

+ * + *

本类必须无状态:不持有 CPU、供应器、网格、世界或合成任务(数据能源的接口契约要求)。

+ */ +public final class OmniversalDynamicCraftingOutputAdapter implements DynamicCraftingOutputAdapter { + /** 注册 id,同时也是去重与诊断用的稳定标识。 */ + public static final ResourceLocation ID = UselessMod.id("omniversal_pattern"); + + @Override + public @NotNull ResourceLocation id() { + return ID; + } + + @Override + public @NotNull Optional resolve(@NotNull IPatternDetails details) { + IPatternDetails original = SmartDoublingPatterns.unwrap(details); + if (!(original instanceof OmniversalPatternDetails omniversal)) { + return Optional.empty(); + } + List outputs = original.getOutputs(); + ObjectList declared = new ObjectArrayList<>(); + for (int slot = 0; slot < outputs.size(); slot++) { + GenericStack output = outputs.get(slot); + if (output == null || output.what() == null || output.amount() <= 0L) { + continue; + } + // 只有被标记为 id-only 的产物槽才是「模板键可能不等于实际键」; + // ID 模式只对物品键有意义(数据能源会在构造时拒绝非物品键)。 + if (!omniversal.isItemIdOutput(slot) || !(output.what() instanceof AEItemKey)) { + continue; + } + // ID 模式不携带配方标签,标签参数必须为空(数据能源在构造时会校验)。 + declared.add(new DynamicCraftingOutput( + output, ProcessingMatchMode.ID, ObjectLists.emptyList())); + } + return declared.isEmpty() + ? Optional.empty() + : Optional.of(new DynamicCraftingOutputSemantics(declared)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/integration/dataenergistics/provider/UselessDataEnergisticsPatternProviderPlugin.java b/src/main/java/com/sorrowmist/useless/integration/dataenergistics/provider/UselessDataEnergisticsPatternProviderPlugin.java new file mode 100644 index 00000000..cebd6167 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/integration/dataenergistics/provider/UselessDataEnergisticsPatternProviderPlugin.java @@ -0,0 +1,141 @@ +package com.sorrowmist.useless.integration.dataenergistics.provider; + +import com.fish_dan_.data_energistics.api.crafting.dispatch.CountedCraftingProviderAdapter; +import com.fish_dan_.data_energistics.api.entrypoint.DataEnergisticsEntrypoint; +import com.fish_dan_.data_energistics.api.entrypoint.DataEnergisticsPlugin; +import com.fish_dan_.data_energistics.api.entrypoint.DataEnergisticsRegistry; +import com.fish_dan_.data_energistics.api.registry.provider.PatternProviderRegistry; +import com.fish_dan_.data_energistics.api.registry.provider.callback.PatternProviderPostCommitContext; +import com.fish_dan_.data_energistics.api.registry.provider.definition.PatternProviderMetadata; +import com.fish_dan_.data_energistics.api.registry.provider.definition.PatternProviderRegistration; +import com.fish_dan_.data_energistics.api.registry.provider.definition.ProviderIdentityDescriptor; +import com.fish_dan_.data_energistics.api.registry.provider.runtime.PatternProviderFactoryContext; +import com.fish_dan_.data_energistics.api.registry.search.TrinityPatternSearchRegistry; +import com.fish_dan_.data_energistics.api.registry.search.TrinityPatternSearchTermRegistration; +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.blockentities.AdvancedAlloyFurnaceBlockEntity; +import com.sorrowmist.useless.content.blockentities.multiblock.MePatternAssemblyBlockEntity; +import com.sorrowmist.useless.init.ModBlockEntities; +import com.sorrowmist.useless.init.ModBlocks; +import com.sorrowmist.useless.integration.dataenergistics.search.OmniversalPatternMoldSearchTerms; +import it.unimi.dsi.fastutil.objects.ObjectLists; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** Registers UselessMod alloy-furnace providers with Data Energistics during its common setup scan. */ +@DataEnergisticsEntrypoint +public final class UselessDataEnergisticsPatternProviderPlugin implements DataEnergisticsPlugin { + private static final ResourceLocation RECIPE_CATEGORY_ID = UselessMod.id("advanced_alloy_furnace"); + private static final ResourceLocation ADVANCED_ALLOY_FURNACE_REGISTRATION_ID = + UselessMod.id("advanced_alloy_furnace"); + private static final ResourceLocation ME_PATTERN_ASSEMBLY_REGISTRATION_ID = + UselessMod.id("me_pattern_assembly"); + private static final ResourceLocation OMNIVERSAL_PATTERN_MOLD_SEARCH_REGISTRATION_ID = + UselessMod.id("omniversal_pattern_mold_search"); + + /** Public constructor required by the Data Energistics entrypoint scanner. */ + public UselessDataEnergisticsPatternProviderPlugin() {} + + @Override + public void register(@NotNull DataEnergisticsRegistry registry) { + ResourceLocation advancedAlloyFurnaceTypeId = ModBlockEntities.ADVANCED_ALLOY_FURNACE.getId(); + ResourceLocation advancedAlloyFurnaceItemId = ModBlocks.ADVANCED_ALLOY_FURNACE_BLOCK.getId(); + ResourceLocation mePatternAssemblyTypeId = ModBlockEntities.ME_PATTERN_ASSEMBLY.getId(); + ResourceLocation mePatternAssemblyItemId = ModBlocks.ME_PATTERN_ASSEMBLY.getId(); + registerAll(registry.patternProviders(), createRegistrations( + advancedAlloyFurnaceTypeId, + advancedAlloyFurnaceItemId, + mePatternAssemblyTypeId, + mePatternAssemblyItemId)); + registerSearchTerms(registry.trinityPatternSearch()); + // 万象样板的 id-only 产物槽:把「声明键只是模板、实际键来自配方」这件事告诉数据能源, + // 否则它的 bigint(exact)分支会把声明键写进 waitingFor,而实际键对不上 ⇒ 永远等待。 + registry.dynamicCraftingOutputs().register(new OmniversalDynamicCraftingOutputAdapter()); + } + + /** Creates the two immutable provider declarations used by this integration. */ + static @NotNull List<@NotNull PatternProviderRegistration> createRegistrations( + @NotNull ResourceLocation advancedAlloyFurnaceTypeId, + @NotNull ResourceLocation advancedAlloyFurnaceItemId, + @NotNull ResourceLocation mePatternAssemblyTypeId, + @NotNull ResourceLocation mePatternAssemblyItemId) { + return List.of( + new PatternProviderRegistration( + metadata( + ADVANCED_ALLOY_FURNACE_REGISTRATION_ID, + advancedAlloyFurnaceTypeId, + advancedAlloyFurnaceItemId), + UselessDataEnergisticsPatternProviderPlugin::createAdvancedAlloyFurnaceAdapter, + null, + UselessDataEnergisticsPatternProviderPlugin::markAdvancedAlloyFurnaceChanged), + new PatternProviderRegistration( + metadata( + ME_PATTERN_ASSEMBLY_REGISTRATION_ID, + mePatternAssemblyTypeId, + mePatternAssemblyItemId), + UselessDataEnergisticsPatternProviderPlugin::createMePatternAssemblyAdapter, + null, + UselessDataEnergisticsPatternProviderPlugin::markMePatternAssemblyChanged)); + } + + /** Registers every declaration as a separate atomic Data Energistics provider registration. */ + static void registerAll( + @NotNull PatternProviderRegistry registry, + @NotNull List<@NotNull PatternProviderRegistration> registrations) { + for (PatternProviderRegistration registration : registrations) { + registry.register(registration); + } + } + + /** Registers the hidden mold stored in an Omniversal Pattern as an independent Trinity search candidate. */ + static void registerSearchTerms(@NotNull TrinityPatternSearchRegistry registry) { + registry.register(new TrinityPatternSearchTermRegistration( + OMNIVERSAL_PATTERN_MOLD_SEARCH_REGISTRATION_ID, + new OmniversalPatternMoldSearchTerms.Contributor())); + } + + private static @NotNull PatternProviderMetadata metadata( + @NotNull ResourceLocation registrationId, + @NotNull ResourceLocation blockEntityTypeId, + @NotNull ResourceLocation workstationItemId) { + // 3.4.0 起 PatternProviderMetadata 的集合参数是 FastUtil ObjectList; + // 用 ObjectLists.singleton 提供单元素不可变视图(构造时会自行快照与排序)。 + return new PatternProviderMetadata( + registrationId, + new ProviderIdentityDescriptor.Block(blockEntityTypeId), + ObjectLists.singleton(RECIPE_CATEGORY_ID), + ObjectLists.singleton(workstationItemId)); + } + + private static @NotNull CountedCraftingProviderAdapter createAdvancedAlloyFurnaceAdapter( + @NotNull PatternProviderFactoryContext context) { + if (!(context.provider() instanceof AdvancedAlloyFurnaceBlockEntity provider)) { + throw new IllegalArgumentException("Advanced alloy furnace registration received an unexpected provider"); + } + return AlloyFurnaceCountedCraftingAdapter.forAdvancedAlloyFurnace(provider, context.identity()); + } + + private static @NotNull CountedCraftingProviderAdapter createMePatternAssemblyAdapter( + @NotNull PatternProviderFactoryContext context) { + if (!(context.provider() instanceof MePatternAssemblyBlockEntity provider)) { + throw new IllegalArgumentException("ME pattern assembly registration received an unexpected provider"); + } + return AlloyFurnaceCountedCraftingAdapter.forMePatternAssembly(provider, context.identity()); + } + + private static void markAdvancedAlloyFurnaceChanged(@NotNull PatternProviderPostCommitContext context) { + if (!(context.provider() instanceof AdvancedAlloyFurnaceBlockEntity provider)) { + throw new IllegalArgumentException("Advanced alloy furnace commit received an unexpected provider"); + } + provider.setChanged(); + } + + private static void markMePatternAssemblyChanged(@NotNull PatternProviderPostCommitContext context) { + if (!(context.provider() instanceof MePatternAssemblyBlockEntity provider)) { + throw new IllegalArgumentException("ME pattern assembly commit received an unexpected provider"); + } + provider.setChanged(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/integration/dataenergistics/search/OmniversalPatternMoldSearchTerms.java b/src/main/java/com/sorrowmist/useless/integration/dataenergistics/search/OmniversalPatternMoldSearchTerms.java new file mode 100644 index 00000000..31dd622b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/integration/dataenergistics/search/OmniversalPatternMoldSearchTerms.java @@ -0,0 +1,53 @@ +package com.sorrowmist.useless.integration.dataenergistics.search; + +import com.fish_dan_.data_energistics.api.registry.search.TrinityPatternSearchTermContributor; +import com.sorrowmist.useless.core.component.OmniversalPatternData; + +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import it.unimi.dsi.fastutil.objects.ObjectList; +import it.unimi.dsi.fastutil.objects.ObjectLists; +import net.minecraft.world.item.ItemStack; + +import java.util.List; + +/** + * Extracts encoded internal molds as separate Trinity pattern-search candidates. + */ +public final class OmniversalPatternMoldSearchTerms { + + private OmniversalPatternMoldSearchTerms() {} + + /** + * 数据能源 3.3.0 的 {@code TrinityPatternSearchTermContributor} 实现。 + * + *

3.3.0 起该接口的两个方法都是 default(新增了 FastUtil 版 {@code searchTermsFast}), + * 所以它不再是函数接口,不能再用方法引用注册,必须给出实现类型。

+ */ + public static final class Contributor implements TrinityPatternSearchTermContributor { + /** + * {@inheritDoc} + * + *

必须经由 {@link OmniversalPatternMoldSearchTerms#searchTerms(ItemStack)} 限定调用外层类的静态方法。 + * 接口自带的同名 default 方法返回空列表,而继承来的实例方法在本类作用域内优先于外层类的静态方法; + * 若以非限定名调用,编译为虚调用并静默落入该空实现,模具名候选将恒为空。

+ */ + @Override + public ObjectList searchTermsFast(ItemStack encodedPattern) { + return ObjectLists.unmodifiable(new ObjectArrayList<>( + OmniversalPatternMoldSearchTerms.searchTerms(encodedPattern))); + } + } + + /** + * Returns mold display names stored in one Omniversal Pattern, when that pattern needs molds. + * + * @param encodedPattern candidate encoded pattern stack + * @return independent mold-name candidates, or no candidates for every other pattern + */ + public static List searchTerms(ItemStack encodedPattern) { + return com.sorrowmist.useless.client.gui.OmniversalPatternMoldSearchTerms.searchTerms(encodedPattern); + } + static List searchTerms(OmniversalPatternData data) { + return com.sorrowmist.useless.client.gui.OmniversalPatternMoldSearchTerms.searchTerms(data); + } +} diff --git a/src/main/java/com/sorrowmist/useless/interfaces/IAugmentableBlockEntityAccessor.java b/src/main/java/com/sorrowmist/useless/interfaces/IAugmentableBlockEntityAccessor.java deleted file mode 100644 index 4954cf1d..00000000 --- a/src/main/java/com/sorrowmist/useless/interfaces/IAugmentableBlockEntityAccessor.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.sorrowmist.useless.interfaces; - -/* - * This file is part of Thermal Parallel. - * - * Copyright (c) 2025 EtSH-C2H6S - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -public interface IAugmentableBlockEntityAccessor { - int useless_mod$getParallel(); - void useless_mod$setParallel(int parallel); - - - -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/interfaces/IItemStorageCoFHAccessor.java b/src/main/java/com/sorrowmist/useless/interfaces/IItemStorageCoFHAccessor.java deleted file mode 100644 index 4a270fee..00000000 --- a/src/main/java/com/sorrowmist/useless/interfaces/IItemStorageCoFHAccessor.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.sorrowmist.useless.interfaces; - -/* - * This file is part of Thermal Parallel. - * - * Copyright (c) 2025 EtSH-C2H6S - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -public interface IItemStorageCoFHAccessor { - void useless_mod$setParallel(int count); - int useless_mod$getParallel(); -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/inventories/UselessTab.java b/src/main/java/com/sorrowmist/useless/inventories/UselessTab.java deleted file mode 100644 index 6c03db54..00000000 --- a/src/main/java/com/sorrowmist/useless/inventories/UselessTab.java +++ /dev/null @@ -1,131 +0,0 @@ -package com.sorrowmist.useless.inventories; - -import com.sorrowmist.useless.UselessMod; -import com.sorrowmist.useless.blocks.GlowPlasticBlock; -import com.sorrowmist.useless.blocks.advancedalloyfurnace.AdvancedAlloyFurnaceBlock; -import com.sorrowmist.useless.blocks.oregenerator.OreGeneratorBlock; -import com.sorrowmist.useless.blocks.CreatorDollBlock; -import com.sorrowmist.useless.blocks.teleport.TeleportBlock; -import com.sorrowmist.useless.blocks.teleport.TeleportBlock2; -import com.sorrowmist.useless.blocks.teleport.TeleportBlock3; -import com.sorrowmist.useless.items.EndlessBeafItem; -import com.sorrowmist.useless.registry.ModComponents; -import com.sorrowmist.useless.registry.ModIngots; -import com.sorrowmist.useless.registry.ModMolds; -import com.sorrowmist.useless.registry.ThermalMoreItems; -import com.sorrowmist.useless.registry.ThermalParallelItems; -import com.sorrowmist.useless.utils.ThermalDependencyHelper; -import net.minecraft.core.registries.Registries; -import net.minecraft.network.chat.Component; -import net.minecraft.world.item.CreativeModeTab; -import net.minecraft.world.item.Item; -import net.minecraft.world.item.ItemStack; -import net.minecraftforge.registries.DeferredRegister; -import net.minecraftforge.registries.RegistryObject; - -public class UselessTab { - public static final DeferredRegister CREATIVE_TAB = - DeferredRegister.create(Registries.CREATIVE_MODE_TAB, UselessMod.MOD_ID); - - public static final RegistryObject USELESS_TAB = - CREATIVE_TAB.register("useless_tab", () -> CreativeModeTab.builder() - .icon(() -> new ItemStack(EndlessBeafItem.ENDLESS_BEAF_ITEM.get())) - .title(Component.literal("无用之物")) - .displayItems(((pParameters, pOutput) -> { - // 基础物品 - 始终添加 - pOutput.accept(EndlessBeafItem.ENDLESS_BEAF_ITEM.get()); - pOutput.accept(TeleportBlock.TELEPORT_BLOCK_ITEM.get()); - pOutput.accept(OreGeneratorBlock.ORE_GENERATOR_BLOCK_ITEM.get()); - pOutput.accept(TeleportBlock2.TELEPORT_BLOCK_ITEM_2.get()); - pOutput.accept(TeleportBlock3.TELEPORT_BLOCK_ITEM_3.get()); - - pOutput.accept(AdvancedAlloyFurnaceBlock.ADVANCED_ALLOY_FURNACE_BLOCK_ITEM.get()); - - - // 发光塑料块 - 始终添加 - for (RegistryObject item : GlowPlasticBlock.GLOW_PLASTIC_BLOCK_ITEMS.values()) { - pOutput.accept(item.get()); - } - - // 添加制作者皮肤玩偶方块物品 - pOutput.accept(CreatorDollBlock.CREATOR_DOLL_BLOCK_ITEM.get()); - - pOutput.accept(ModIngots.USELESS_INGOT_TIER_1.get()); - pOutput.accept(ModIngots.USELESS_INGOT_TIER_2.get()); - pOutput.accept(ModIngots.USELESS_INGOT_TIER_3.get()); - pOutput.accept(ModIngots.USELESS_INGOT_TIER_4.get()); - pOutput.accept(ModIngots.USELESS_INGOT_TIER_5.get()); - pOutput.accept(ModIngots.USELESS_INGOT_TIER_6.get()); - pOutput.accept(ModIngots.USELESS_INGOT_TIER_7.get()); - pOutput.accept(ModIngots.USELESS_INGOT_TIER_8.get()); - pOutput.accept(ModIngots.USELESS_INGOT_TIER_9.get()); - - // 添加新的锭到创造标签 - pOutput.accept(ModIngots.POSSIBLE_USEFUL_INGOT.get()); - pOutput.accept(ModIngots.USEFUL_INGOT.get()); - - // 添加金属模具到创造标签 - pOutput.accept(ModMolds.METAL_MOLD_PLATE.get()); - pOutput.accept(ModMolds.METAL_MOLD_ROD.get()); - pOutput.accept(ModMolds.METAL_MOLD_GEAR.get()); - pOutput.accept(ModMolds.METAL_MOLD_WIRE.get()); - pOutput.accept(ModMolds.METAL_MOLD_BLOCK.get()); - - // 添加无用齿轮到创造标签 - pOutput.accept(ModComponents.USELESS_GEAR_TIER_1.get()); - pOutput.accept(ModComponents.USELESS_GEAR_TIER_2.get()); - pOutput.accept(ModComponents.USELESS_GEAR_TIER_3.get()); - pOutput.accept(ModComponents.USELESS_GEAR_TIER_4.get()); - pOutput.accept(ModComponents.USELESS_GEAR_TIER_5.get()); - pOutput.accept(ModComponents.USELESS_GEAR_TIER_6.get()); - pOutput.accept(ModComponents.USELESS_GEAR_TIER_7.get()); - pOutput.accept(ModComponents.USELESS_GEAR_TIER_8.get()); - pOutput.accept(ModComponents.USELESS_GEAR_TIER_9.get()); - - // 添加无用玻璃到创造标签 - pOutput.accept(ModComponents.USELESS_GLASS_TIER_1.get()); - pOutput.accept(ModComponents.USELESS_GLASS_TIER_2.get()); - pOutput.accept(ModComponents.USELESS_GLASS_TIER_3.get()); - pOutput.accept(ModComponents.USELESS_GLASS_TIER_4.get()); - pOutput.accept(ModComponents.USELESS_GLASS_TIER_5.get()); - pOutput.accept(ModComponents.USELESS_GLASS_TIER_6.get()); - pOutput.accept(ModComponents.USELESS_GLASS_TIER_7.get()); - pOutput.accept(ModComponents.USELESS_GLASS_TIER_8.get()); - pOutput.accept(ModComponents.USELESS_GLASS_TIER_9.get()); - - // 只在热力系列安装时添加 ThermalMore 整合的物品 - if (ThermalDependencyHelper.isAnyThermalModLoaded()) { - // 安全地添加 ThermalMore 物品 - safelyAddItem(pOutput, ThermalMoreItems.USELESS_INTEGRAL_COMPONENT_TIER_1); - safelyAddItem(pOutput, ThermalMoreItems.USELESS_INTEGRAL_COMPONENT_TIER_2); - safelyAddItem(pOutput, ThermalMoreItems.USELESS_INTEGRAL_COMPONENT_TIER_3); - safelyAddItem(pOutput, ThermalMoreItems.USELESS_INTEGRAL_COMPONENT_TIER_4); - safelyAddItem(pOutput, ThermalMoreItems.USELESS_INTEGRAL_COMPONENT_TIER_5); - safelyAddItem(pOutput, ThermalMoreItems.USELESS_INTEGRAL_COMPONENT_TIER_6); - safelyAddItem(pOutput, ThermalMoreItems.USELESS_INTEGRAL_COMPONENT_TIER_7); - safelyAddItem(pOutput, ThermalMoreItems.USELESS_INTEGRAL_COMPONENT_TIER_8); - safelyAddItem(pOutput, ThermalMoreItems.USELESS_INTEGRAL_COMPONENT_TIER_9); - safelyAddItem(pOutput, ThermalParallelItems.AUGMENT_PARALLEL_1); - safelyAddItem(pOutput, ThermalParallelItems.AUGMENT_PARALLEL_2); - safelyAddItem(pOutput, ThermalParallelItems.AUGMENT_PARALLEL_3); - } - })) - .build()); - - /** - * 安全地添加物品到创造标签 - * 防止在物品未注册时出现异常 - */ - private static void safelyAddItem(CreativeModeTab.Output output, RegistryObject itemRegistryObject) { - try { - if (itemRegistryObject != null && itemRegistryObject.isPresent()) { - Item item = itemRegistryObject.get(); - if (item != null) { - output.accept(item); - } - } - } catch (Exception e) { - // 忽略无法添加的物品 - } - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/inventory/slot/PatternSlotItemHandler.java b/src/main/java/com/sorrowmist/useless/inventory/slot/PatternSlotItemHandler.java new file mode 100644 index 00000000..5bc10a58 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/inventory/slot/PatternSlotItemHandler.java @@ -0,0 +1,78 @@ +package com.sorrowmist.useless.inventory.slot; + +import appeng.api.crafting.PatternDetailsHelper; +import appeng.client.gui.Icon; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.items.IItemHandler; +import net.minecraftforge.items.SlotItemHandler; +import org.jetbrains.annotations.NotNull; + +public class PatternSlotItemHandler extends SlotItemHandler { + + private Icon icon; + private boolean enabled = true; + private float opacity = 1.0f; + private boolean renderIconWithItem = false; + private boolean active = true; + + public PatternSlotItemHandler(IItemHandler itemHandler, int index, int xPosition, int yPosition) { + super(itemHandler, index, xPosition, yPosition); + } + + public void setIcon(Icon icon) { + this.icon = icon; + } + + public Icon getIcon() { + return this.icon; + } + + public boolean isSlotEnabled() { + return this.enabled; + } + + public void setSlotEnabled(boolean enabled) { + this.enabled = enabled; + } + + public float getOpacityOfIcon() { + return this.opacity; + } + + public void setOpacityOfIcon(float opacity) { + this.opacity = opacity; + } + + public boolean renderIconWithItem() { + return this.renderIconWithItem; + } + + public void setRenderIconWithItem(boolean renderIconWithItem) { + this.renderIconWithItem = renderIconWithItem; + } + + @Override + public boolean isActive() { + return this.active; + } + + public void setActive(boolean active) { + this.active = active; + } + + @Override + public int getMaxStackSize() { + return 1; + } + + @Override + public boolean mayPlace(@NotNull ItemStack stack) { + return PatternDetailsHelper.isEncodedPattern(stack); + } + + @Override + public boolean mayPickup(@NotNull Player player) { + return super.mayPickup(player); + } +} diff --git a/src/main/java/com/sorrowmist/useless/items/EndlessBeafItem.java b/src/main/java/com/sorrowmist/useless/items/EndlessBeafItem.java deleted file mode 100644 index 55a30409..00000000 --- a/src/main/java/com/sorrowmist/useless/items/EndlessBeafItem.java +++ /dev/null @@ -1,1732 +0,0 @@ -package com.sorrowmist.useless.items; - -/* - * This file is based on Apotheosis. - * - * Copyright (c) 2023 Brennan Ward - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import appeng.api.features.IGridLinkableHandler; -import appeng.api.implementations.blockentities.IWirelessAccessPoint; -import appeng.api.networking.IGrid; -import appeng.api.stacks.AEItemKey; -import appeng.api.storage.MEStorage; -import appeng.me.helpers.PlayerSource; -import com.mojang.datafixers.util.Pair; -import com.sorrowmist.useless.UselessMod; -import com.sorrowmist.useless.blocks.GlowPlasticBlock; -import com.sorrowmist.useless.client.KeyBindings; -import com.sorrowmist.useless.config.ConfigManager; -import com.sorrowmist.useless.modes.ModeManager; -import com.sorrowmist.useless.modes.ToolMode; -import com.sorrowmist.useless.networking.EnhancedChainMiningTogglePacket; -import com.sorrowmist.useless.networking.ForceMiningTogglePacket; -import com.sorrowmist.useless.networking.ModMessages; -import com.sorrowmist.useless.networking.ResetMasterPatternPacket; -import com.sorrowmist.useless.networking.SetMasterPatternPacket; -import com.sorrowmist.useless.networking.SetSlavePatternPacket; -import com.sorrowmist.useless.utils.BlockBreakUtils; -import com.sorrowmist.useless.utils.mining.MiningDispatcher; -import com.sorrowmist.useless.utils.pattern.PatternProviderEvent; -import com.sorrowmist.useless.utils.pattern.PatternProviderKey; -import com.sorrowmist.useless.utils.pattern.PatternProviderManager; -import com.sorrowmist.useless.utils.pattern.PatternProviderOperation; -import com.sorrowmist.useless.utils.pattern.PatternProviderSyncData; -import net.minecraft.ChatFormatting; -import net.minecraft.advancements.CriteriaTriggers; -import net.minecraft.client.KeyMapping; -import net.minecraft.client.Minecraft; -import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; -import net.minecraft.core.GlobalPos; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.nbt.NbtOps; -import net.minecraft.nbt.Tag; -import net.minecraft.network.chat.Component; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.sounds.SoundEvents; -import net.minecraft.sounds.SoundSource; -import net.minecraft.tags.BlockTags; -import net.minecraft.world.InteractionResult; -import net.minecraft.world.effect.MobEffectInstance; -import net.minecraft.world.effect.MobEffects; -import net.minecraft.world.entity.Entity; -import net.minecraft.world.entity.LivingEntity; -import net.minecraft.world.entity.item.ItemEntity; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.item.Item; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.PickaxeItem; -import net.minecraft.world.item.Rarity; -import net.minecraft.world.item.Tier; -import net.minecraft.world.item.Tiers; -import net.minecraft.world.item.TooltipFlag; -import net.minecraft.world.item.context.UseOnContext; -import net.minecraft.world.item.enchantment.Enchantment; -import net.minecraft.world.item.enchantment.EnchantmentHelper; -import net.minecraft.world.item.enchantment.Enchantments; -import net.minecraft.world.level.Level; -import net.minecraft.world.level.LevelAccessor; -import net.minecraft.world.level.LevelReader; -import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.block.Blocks; -import net.minecraft.world.level.block.entity.BlockEntity; -import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.phys.BlockHitResult; -import net.minecraft.world.phys.HitResult; -import net.minecraftforge.api.distmarker.Dist; -import net.minecraftforge.api.distmarker.OnlyIn; -import net.minecraftforge.common.ToolAction; -import net.minecraftforge.common.ToolActions; -import net.minecraftforge.event.TickEvent; -import net.minecraftforge.event.entity.living.LivingDropsEvent; -import net.minecraftforge.event.entity.player.PlayerEvent; -import net.minecraftforge.event.level.BlockEvent; -import net.minecraftforge.eventbus.api.SubscribeEvent; -import net.minecraftforge.fml.ModList; -import net.minecraftforge.fml.common.Mod; -import net.minecraftforge.registries.DeferredRegister; -import net.minecraftforge.registries.ForgeRegistries; -import net.minecraftforge.registries.RegistryObject; -import org.jetbrains.annotations.NotNull; - -import javax.annotation.Nullable; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; - -public class EndlessBeafItem extends PickaxeItem { - public static final DeferredRegister ITEMS = DeferredRegister.create(ForgeRegistries.ITEMS, UselessMod.MOD_ID); - - // AE2无线访问点链接相关 - private static final String TAG_ACCESS_POINT_POS = "accessPoint"; - public static final IGridLinkableHandler LINKABLE_HANDLER = new LinkableHandler(); - - // 飞行状态跟踪,避免重复设置导致卡顿 - private static final Map playerFlightStatus = new HashMap<>(); - - // 同步数据标签 - private static final String SYNC_DATA_TAG = "PatternProviderSyncData"; - - // 为了兼容其他类(如mixin),提供静态访问方法 - public static Map> masterToSlaves = new HashMap<>() { - @Override - public Set get(Object key) { - return PatternProviderManager.getMasterToSlaves().get(key); - } - - @Override - public Set put(PatternProviderKey key, Set value) { - return PatternProviderManager.getMasterToSlaves().put(key, value); - } - - @Override - public Set remove(Object key) { - return PatternProviderManager.getMasterToSlaves().remove(key); - } - - @Override - public void clear() { - PatternProviderManager.getMasterToSlaves().clear(); - } - - @Override - public boolean containsKey(Object key) { - return PatternProviderManager.getMasterToSlaves().containsKey(key); - } - - @Override - public boolean isEmpty() { - return PatternProviderManager.getMasterToSlaves().isEmpty(); - } - - @Override - public Set>> entrySet() { - return PatternProviderManager.getMasterToSlaves().entrySet(); - } - - @Override - public Set keySet() { - return PatternProviderManager.getMasterToSlaves().keySet(); - } - - @Override - public int size() { - return PatternProviderManager.getMasterToSlaves().size(); - } - }; - - public static Map slaveToMaster = new HashMap<>() { - @Override - public PatternProviderKey get(Object key) { - return PatternProviderManager.getSlaveToMaster().get(key); - } - - @Override - public PatternProviderKey put(PatternProviderKey key, PatternProviderKey value) { - return PatternProviderManager.getSlaveToMaster().put(key, value); - } - - @Override - public PatternProviderKey remove(Object key) { - return PatternProviderManager.getSlaveToMaster().remove(key); - } - - @Override - public void clear() { - PatternProviderManager.getSlaveToMaster().clear(); - } - - @Override - public boolean containsKey(Object key) { - return PatternProviderManager.getSlaveToMaster().containsKey(key); - } - - @Override - public boolean isEmpty() { - return PatternProviderManager.getSlaveToMaster().isEmpty(); - } - - @Override - public Set> entrySet() { - return PatternProviderManager.getSlaveToMaster().entrySet(); - } - - @Override - public Set keySet() { - return PatternProviderManager.getSlaveToMaster().keySet(); - } - - @Override - public int size() { - return PatternProviderManager.getSlaveToMaster().size(); - } - }; - - // 使用静态代理来处理currentSelectedMaster的访问 - public static PatternProviderKey getCurrentSelectedMaster() { - return PatternProviderManager.getCurrentSelectedMaster(); - } - - // 用于处理物品与无线访问点的绑定 - private static class LinkableHandler implements IGridLinkableHandler { - @Override - public boolean canLink(ItemStack stack) { - return stack.getItem() instanceof EndlessBeafItem; - } - - @Override - public void link(ItemStack itemStack, GlobalPos pos) { - GlobalPos.CODEC.encodeStart(NbtOps.INSTANCE, pos) - .result() - .ifPresent(tag -> itemStack.getOrCreateTag().put(TAG_ACCESS_POINT_POS, tag)); - } - - @Override - public void unlink(ItemStack itemStack) { - itemStack.removeTagKey(TAG_ACCESS_POINT_POS); - } - } - - public EndlessBeafItem(Tier pTier, int pAttackDamageModifier, float pAttackSpeedModifier, Properties pProperties) { - super(pTier, pAttackDamageModifier, pAttackSpeedModifier, pProperties); - } - - @Override - public boolean isDamageable(ItemStack stack) { - return false; // 物品不可损坏 - } - - @Override - public int getMaxDamage(ItemStack stack) { - return 0; // 最大耐久为0,表示无限 - } - - @Override - public boolean isBarVisible(ItemStack stack) { - return false; // 不显示耐久条 - } - - @Override - public void setDamage(ItemStack stack, int damage) { - // 阻止任何耐久度设置 - super.setDamage(stack, 0); - } - - @Override - public ItemStack getCraftingRemainingItem(ItemStack stack) { - // 返回物品本身,使其在合成后保留在工作台中 - return stack.copy(); - } - - @Override - public boolean hasCraftingRemainingItem(ItemStack stack) { - // 确保该物品有剩余物品(即本身) - return true; - } - - // 模式管理器实例 - private final ModeManager modeManager = new ModeManager(); - - // 检查是否处于精准采集模式 - public boolean isSilkTouchMode(ItemStack stack) { - modeManager.loadFromStack(stack); - return modeManager.isModeActive(ToolMode.SILK_TOUCH); - } - - // 获取强化连锁模式 - public boolean isEnhancedChainMiningMode(ItemStack stack) { - modeManager.loadFromStack(stack); - return modeManager.isModeActive(ToolMode.ENHANCED_CHAIN_MINING); - } - - // 切换强化连锁模式 - public boolean toggleEnhancedChainMiningMode(ItemStack stack) { - modeManager.loadFromStack(stack); - modeManager.toggleMode(ToolMode.ENHANCED_CHAIN_MINING); - modeManager.saveToStack(stack); - return modeManager.isModeActive(ToolMode.ENHANCED_CHAIN_MINING); - } - - // 检查是否处于强制挖掘模式 - public boolean isForceMiningMode(ItemStack stack) { - modeManager.loadFromStack(stack); - return modeManager.isModeActive(ToolMode.FORCE_MINING); - } - - // 切换强制挖掘模式 - public boolean toggleForceMiningMode(ItemStack stack) { - modeManager.loadFromStack(stack); - modeManager.toggleMode(ToolMode.FORCE_MINING); - modeManager.saveToStack(stack); - return modeManager.isModeActive(ToolMode.FORCE_MINING); - } - - // 检查是否处于AE存储优先模式 - public boolean isAEStoragePriorityMode(ItemStack stack) { - modeManager.loadFromStack(stack); - return modeManager.isModeActive(ToolMode.AE_STORAGE_PRIORITY); - } - - // 获取链接的无线访问点位置 - @Nullable - public static GlobalPos getLinkedPosition(ItemStack stack) { - CompoundTag tag = stack.getTag(); - if (tag != null && tag.contains(TAG_ACCESS_POINT_POS, Tag.TAG_COMPOUND)) { - return GlobalPos.CODEC.decode(NbtOps.INSTANCE, tag.get(TAG_ACCESS_POINT_POS)) - .result() - .map(Pair::getFirst) - .orElse(null); - } - return null; - } - - // 获取链接的AE网格 - @Nullable - public static IGrid getLinkedGrid(ItemStack stack, Level level, @Nullable Player player) { - if (!(level instanceof ServerLevel serverLevel)) { - return null; - } - - GlobalPos linkedPos = getLinkedPosition(stack); - if (linkedPos == null) { - return null; - } - - ServerLevel linkedLevel = serverLevel.getServer().getLevel(linkedPos.dimension()); - if (linkedLevel == null) { - return null; - } - - BlockEntity be = linkedLevel.getBlockEntity(linkedPos.pos()); - if (!(be instanceof IWirelessAccessPoint accessPoint)) { - return null; - } - - return accessPoint.getGrid(); - } - - // 将物品栈存入AE网络(带工具参数) - private static boolean storeItemInAENetwork(ItemStack stack, Player player, ItemStack toolStack) { - if (player == null || stack.isEmpty()) { - return false; - } - - // 获取工具 - ItemStack toolItem = toolStack; - if (toolItem == null || toolItem.isEmpty()) { - // 如果没有提供工具参数,从玩家手中获取 - ItemStack mainHandItem = player.getMainHandItem(); - ItemStack offHandItem = player.getOffhandItem(); - - if (mainHandItem.getItem() instanceof EndlessBeafItem) { - toolItem = mainHandItem; - } else if (offHandItem.getItem() instanceof EndlessBeafItem) { - toolItem = offHandItem; - } else { - return false; - } - } - - // 检查是否启用了AE存储优先模式 - EndlessBeafItem tool = (EndlessBeafItem) toolItem.getItem(); - boolean isAEStoragePriority = tool.isAEStoragePriorityMode(toolItem); - if (!isAEStoragePriority) { - return false; - } - - // 尝试从链接的无线访问点获取AE网络 - try { - // 获取链接的网格 - IGrid grid = getLinkedGrid(toolItem, player.level(), player); - if (grid == null) { - return false; - } - - // 获取物品存储处理程序 - MEStorage storage = grid.getStorageService().getInventory(); - if (storage == null) { - return false; - } - - // 转换为AE物品栈 - AEItemKey aeKey = AEItemKey.of(stack); - if (aeKey == null) { - return false; - } - - // 存入AE网络 - long inserted = storage.insert(aeKey, stack.getCount(), appeng.api.config.Actionable.MODULATE, new PlayerSource(player, null)); - // 如果插入的数量等于物品栈数量,说明全部存入 - if (inserted == stack.getCount()) { - return true; - } else if (inserted > 0) { - // 更新物品栈为剩余数量 - stack.setCount((int) (stack.getCount() - inserted)); - return stack.isEmpty(); - } else { - return false; - } - } catch (Exception e) { - // 忽略任何异常 - } - - return false; - } - - // 将物品列表存入AE网络或玩家背包(带工具参数) - public static void handleDrops(List drops, Player player, ItemStack toolStack) { - if (drops == null || drops.isEmpty()) { - return; - } - - // 遍历所有掉落物品 - Iterator iterator = drops.iterator(); - while (iterator.hasNext()) { - ItemStack dropStack = iterator.next(); - if (dropStack.isEmpty()) { - iterator.remove(); - continue; - } - - // 尝试存入AE网络 - if (storeItemInAENetwork(dropStack, player, toolStack)) { - iterator.remove(); - continue; - } - - // 尝试存入玩家背包 - if (!player.getInventory().add(dropStack)) { - // 背包已满,保留在掉落列表中,稍后生成物品实体 - continue; - } - - // 背包添加成功,从掉落列表中移除 - iterator.remove(); - } - } - - // 处理设置主扩展样板供应器按键(M键) - private void handleSetMasterPatternKey(ItemStack stack, Player player) { - // 获取玩家看向的方块 - double reachDistance = 4.5D; - HitResult hitResult = player.pick(reachDistance, 0.0F, false); - if (hitResult.getType() == HitResult.Type.BLOCK) { - BlockHitResult blockHitResult = (BlockHitResult) hitResult; - BlockPos pos = blockHitResult.getBlockPos(); - Direction direction = blockHitResult.getDirection(); - // 发送数据包到服务器,由服务器处理设置主方块 - ModMessages.sendToServer(new SetMasterPatternPacket(pos, direction)); - } else if (hitResult.getType() == HitResult.Type.MISS) { - // 如果点击到空气,发送数据包到服务器,由服务器处理重置主方块选择 - ModMessages.sendToServer(new ResetMasterPatternPacket()); - } - } - - // 处理设置从扩展样板供应器按键(S键) - private void handleSetSlavePatternKey(ItemStack stack, Player player) { - // 获取玩家看向的方块 - double reachDistance = 4.5D; - HitResult hitResult = player.pick(reachDistance, 0.0F, false); - if (hitResult.getType() == HitResult.Type.BLOCK) { - BlockHitResult blockHitResult = (BlockHitResult) hitResult; - BlockPos pos = blockHitResult.getBlockPos(); - Direction direction = blockHitResult.getDirection(); - // 发送数据包到服务器,由服务器处理设置从方块 - ModMessages.sendToServer(new SetSlavePatternPacket(pos, direction)); - } - } - - // 更新实际的附魔NBT - public void updateEnchantments(ItemStack stack) { - // 保存关键状态(使用局部变量存储,确保不会丢失) - ModeManager modeManager = new ModeManager(); - modeManager.loadFromStack(stack); - - boolean enhancedChainMining = isEnhancedChainMiningMode(stack); - boolean silkTouchMode = isSilkTouchMode(stack); - - // 获取现有的所有附魔 - Map enchantments = new HashMap<>(EnchantmentHelper.getEnchantments(stack)); - // 使用配置中的抢夺等级 - enchantments.put(Enchantments.MOB_LOOTING, ConfigManager.getLootingLevel()); - - if (silkTouchMode) { - // 精准采集模式 - enchantments.remove(Enchantments.BLOCK_FORTUNE); // 移除时运 - // 确保有精准采集,使用最高等级 - int silkTouchLevel = Math.max(1, enchantments.getOrDefault(Enchantments.SILK_TOUCH, 0)); - enchantments.put(Enchantments.SILK_TOUCH, silkTouchLevel); - } else { - // 时运模式 - enchantments.remove(Enchantments.SILK_TOUCH); // 移除精准采集 - // 确保有时运,使用配置中的等级 - int fortuneLevel = Math.max(ConfigManager.getFortuneLevel(), enchantments.getOrDefault(Enchantments.BLOCK_FORTUNE, 0)); - enchantments.put(Enchantments.BLOCK_FORTUNE, fortuneLevel); - } - - // 应用更新后的附魔 - EnchantmentHelper.setEnchantments(enchantments, stack); - - // 强制恢复关键状态标签(即使setEnchantments替换了整个NBT也能恢复) - // 使用getOrCreateTag确保标签存在 - CompoundTag finalTag = stack.getOrCreateTag(); - finalTag.putBoolean("EnhancedChainMining", enhancedChainMining); - finalTag.putBoolean("SilkTouchMode", silkTouchMode); - - // 设置模型切换谓词值 - if (silkTouchMode) { - finalTag.putFloat("useless_mod:silk_touch_mode", 1.0f); - } else { - finalTag.remove("useless_mod:silk_touch_mode"); - } - - // 确保标签被正确应用到物品上 - stack.setTag(finalTag); - - // 更新工具模式标签 - updateToolModeTags(stack, modeManager); - } - - // 检查是否安装了格雷科技mod - private boolean isGTCEUInstalled() { - return ModList.get().isLoaded("gtceu"); - } - - // 更新工具模式,通过NBT标签跟踪激活的工具模式 - private void updateToolModeTags(ItemStack stack, ModeManager modeManager) { - // 在NBT中存储激活的工具模式,以便在游戏逻辑中使用 - CompoundTag tag = stack.getOrCreateTag(); - CompoundTag toolModesTag = tag.getCompound("ToolModes"); - - // 清除旧的工具模式标签(如果存在) - tag.remove("ActiveToolTag"); - - // 根据激活的模式设置活动工具标签 - if (modeManager.isModeActive(ToolMode.WRENCH_MODE)) { - tag.putString("ActiveToolTag", "forge:tools/wrenches"); - } else if (isGTCEUInstalled() && modeManager.isModeActive(ToolMode.SCREWDRIVER_MODE)) { - tag.putString("ActiveToolTag", "forge:tools/screwdrivers"); - } else if (isGTCEUInstalled() && modeManager.isModeActive(ToolMode.MALLET_MODE)) { - tag.putString("ActiveToolTag", "forge:tools/mallets"); - } else if (modeManager.isModeActive(ToolMode.OMNITOOL_MODE)) { - // 添加兼容性检查,确保omnitools模组已安装 - ResourceLocation omnitoolId = new ResourceLocation("omnitools:omni_wrench"); - if (ForgeRegistries.ITEMS.containsKey(omnitoolId)) { - tag.putString("ActiveToolTag", "forge:tools/wrenches"); - } else { - // 如果omnitools模组未安装,移除OMNITOOL_MODE模式 - tag.remove("ActiveToolTag"); - } - } - - // 将更新后的标签放回stack - stack.setTag(tag); - } - - // 根据激活的模式切换物品实例 - public ItemStack switchToolModeItem(ItemStack oldStack, ModeManager modeManager) { - // 创建新的物品实例,根据激活的模式选择对应的子类 - ItemStack newStack = ItemStack.EMPTY; - - // 检查是否安装了格雷科技mod - boolean isGTCEUInstalled = isGTCEUInstalled(); - - // 检查激活的工具模式 - boolean hasWrenchMode = modeManager.isModeActive(ToolMode.WRENCH_MODE); - boolean hasScrewdriverMode = isGTCEUInstalled && modeManager.isModeActive(ToolMode.SCREWDRIVER_MODE); - boolean hasMalletMode = isGTCEUInstalled && modeManager.isModeActive(ToolMode.MALLET_MODE); - boolean hasCrowbarMode = isGTCEUInstalled && modeManager.isModeActive(ToolMode.CROWBAR_MODE); - boolean hasHammerMode = isGTCEUInstalled && modeManager.isModeActive(ToolMode.HAMMER_MODE); - boolean hasOmnitoolMode = modeManager.isModeActive(ToolMode.OMNITOOL_MODE); - - // 如果未安装格雷科技mod,禁用相关模式 - if (!isGTCEUInstalled) { - if (modeManager.isModeActive(ToolMode.SCREWDRIVER_MODE) || - modeManager.isModeActive(ToolMode.MALLET_MODE) || - modeManager.isModeActive(ToolMode.CROWBAR_MODE) || - modeManager.isModeActive(ToolMode.HAMMER_MODE)) { - // 禁用相关模式 - modeManager.setModeActive(ToolMode.SCREWDRIVER_MODE, false); - modeManager.setModeActive(ToolMode.MALLET_MODE, false); - modeManager.setModeActive(ToolMode.CROWBAR_MODE, false); - modeManager.setModeActive(ToolMode.HAMMER_MODE, false); - modeManager.saveToStack(oldStack); - } - } - - if (hasWrenchMode) { - // 创建扳手实例 - newStack = new ItemStack(ENDLESS_BEAF_WRENCH.get()); - } else if (hasScrewdriverMode) { - // 创建螺丝刀实例 - newStack = new ItemStack(ENDLESS_BEAF_SCREWDRIVER.get()); - } else if (hasMalletMode) { - // 创建锤子实例 - newStack = new ItemStack(ENDLESS_BEAF_MALLET.get()); - } else if (hasCrowbarMode) { - // 创建撬棍实例 - newStack = new ItemStack(ENDLESS_BEAF_CROWBAR.get()); - } else if (hasHammerMode) { - // 创建铁锤实例 - newStack = new ItemStack(ENDLESS_BEAF_HAMMER.get()); - } else if (hasOmnitoolMode) { - // 创建Omnitool扳手实例 - 通过物品ID获取 - // 添加兼容性检查,确保omnitools模组已安装 - ResourceLocation omnitoolId = new ResourceLocation("omnitools:omni_wrench"); - if (ForgeRegistries.ITEMS.containsKey(omnitoolId)) { - newStack = new ItemStack(ForgeRegistries.ITEMS.getValue(omnitoolId)); - } else { - // 如果omnitools模组未安装,使用基础实例 - newStack = new ItemStack(ENDLESS_BEAF_ITEM.get()); - // 禁用OMNITOOL_MODE模式 - modeManager.setModeActive(ToolMode.OMNITOOL_MODE, false); - modeManager.saveToStack(oldStack); - } - } else { - // 如果没有激活的工具模式,使用基础实例(具有所有标签) - newStack = new ItemStack(ENDLESS_BEAF_ITEM.get()); - } - - // 复制原有物品的所有NBT数据到新实例 - if (oldStack.hasTag() && !newStack.isEmpty()) { - newStack.setTag(oldStack.getTag().copy()); - } - - // 更新新实例的附魔NBT,确保模型切换谓词值被正确设置 - updateEnchantments(newStack); - - return newStack; - } - - // 添加PlayerTickEvent监听器,用于持续检查玩家物品栏并管理飞行权限 - // 修复:当物品被移出物品栏时,确保飞行权限被正确关闭 - @SubscribeEvent - public static void onPlayerTick(TickEvent.PlayerTickEvent event) { - Player player = event.player; - if (player == null) return; - - // 只在服务器端执行,避免客户端同步问题 - if (player.level().isClientSide()) return; - - // 检查玩家物品栏中是否有任何EndlessBeafItem变体 - boolean hasItemInInventory = player.getInventory().items.stream() - .anyMatch(item -> item.getItem() instanceof EndlessBeafItem || - (item.hasTag() && item.getTag().contains("ToolModes"))); - - UUID playerId = player.getUUID(); - Boolean currentFlightStatus = playerFlightStatus.getOrDefault(playerId, false); - - if (hasItemInInventory) { - // 物品在物品栏中,根据配置决定是否赋予飞行权限 - if (ConfigManager.enableFlightEffect() && !player.getAbilities().mayfly) { - player.getAbilities().mayfly = true; - player.onUpdateAbilities(); - } - playerFlightStatus.put(playerId, true); - - // 根据配置决定是否给予药水效果 - if (ConfigManager.enablePotionEffects()) { - // 给予饱和效果(不显示粒子,但显示图标) - MobEffectInstance baohe = player.getEffect(MobEffects.SATURATION); - if (baohe == null || baohe.getDuration() < 20) { - player.addEffect(new MobEffectInstance(MobEffects.SATURATION, 200, 0, true, false, true)); - } - - // 给予生命恢复效果(不显示粒子,但显示图标) - MobEffectInstance zaisheng = player.getEffect(MobEffects.REGENERATION); - if (zaisheng == null || (zaisheng.getDuration() < 20)) { - player.addEffect(new MobEffectInstance(MobEffects.REGENERATION, 200, 5, true, false, true)); - } - - // 给予夜视效果(不显示粒子,但显示图标) - MobEffectInstance yeshi = player.getEffect(MobEffects.NIGHT_VISION); - if (yeshi == null || (yeshi.getDuration() < 2000)) { - player.addEffect(new MobEffectInstance(MobEffects.NIGHT_VISION, 20000, 0, true, false, true)); - } - - // 新增:给予抗火效果(不显示粒子,但显示图标) - MobEffectInstance kanghuo = player.getEffect(MobEffects.FIRE_RESISTANCE); - if (kanghuo == null || kanghuo.getDuration() < 200) { - player.addEffect(new MobEffectInstance(MobEffects.FIRE_RESISTANCE, 2000, 0, true, false, true)); - } - - // 新增:给予水下呼吸效果(不显示粒子,但显示图标) - MobEffectInstance shuixiabreath = player.getEffect(MobEffects.WATER_BREATHING); - if (shuixiabreath == null || shuixiabreath.getDuration() < 200) { - player.addEffect(new MobEffectInstance(MobEffects.WATER_BREATHING, 2000, 0, true, false, true)); - } - - // 新增:给予抗性提升效果(不显示粒子,但显示图标) - MobEffectInstance kangxing = player.getEffect(MobEffects.DAMAGE_RESISTANCE); - if (kangxing == null || kangxing.getDuration() < 200) { - player.addEffect(new MobEffectInstance(MobEffects.DAMAGE_RESISTANCE, 2000, 5, true, false, true)); - } - } - } else { - // 物品不在物品栏中,对于非创造模式玩家,关闭飞行权限 - // 只有当玩家是因为我们的工具才获得飞行权限的情况下,才会关闭飞行权限 - if (!player.isCreative() && player.getAbilities().mayfly && playerFlightStatus.getOrDefault(playerId, false)) { - player.getAbilities().mayfly = false; - player.getAbilities().flying = false; - player.onUpdateAbilities(); - } - playerFlightStatus.put(playerId, false); - } - } - - // 切换模式的方法(供数据包调用) - public void switchEnchantmentMode(ItemStack stack, boolean silkTouchMode) { - // 使用模式管理器切换模式,保持其他模式状态 - modeManager.loadFromStack(stack); - - // 保存当前所有模式状态 - boolean aeStoragePriority = modeManager.isModeActive(ToolMode.AE_STORAGE_PRIORITY); - boolean forceMining = modeManager.isModeActive(ToolMode.FORCE_MINING); - boolean enhancedChainMining = modeManager.isModeActive(ToolMode.ENHANCED_CHAIN_MINING); - - if (silkTouchMode) { - modeManager.setModeActive(ToolMode.SILK_TOUCH, true); - modeManager.setModeActive(ToolMode.FORTUNE, false); - } else { - modeManager.setModeActive(ToolMode.SILK_TOUCH, false); - modeManager.setModeActive(ToolMode.FORTUNE, true); - } - - // 恢复其他重要模式状态 - modeManager.setModeActive(ToolMode.AE_STORAGE_PRIORITY, aeStoragePriority); - modeManager.setModeActive(ToolMode.FORCE_MINING, forceMining); - modeManager.setModeActive(ToolMode.ENHANCED_CHAIN_MINING, enhancedChainMining); - - modeManager.saveToStack(stack); - - // 更新实际的附魔NBT - updateEnchantments(stack); - - // 强制客户端更新物品渲染 - if (!stack.isEmpty()) { - // 通过修改NBT强制更新 - CompoundTag tag = stack.getOrCreateTag(); - tag.putLong("LastModeSwitch", System.currentTimeMillis()); - stack.setTag(tag); - } - } - - // 处理方块破坏事件,用于管理主从样板供应器关系 - @SubscribeEvent - public static void onBlockBreak(BlockEvent.BreakEvent event) { - LevelAccessor levelAccessor = event.getLevel(); - BlockPos pos = event.getPos(); - - // 检查所有主从关系,处理与该方块相关的所有主从关系 - List mastersToRemove = new ArrayList<>(); - List slavesToRemove = new ArrayList<>(); - - // 遍历所有方向,检查是否有任何主端或从端位于该方块的任何方向 - for (Direction direction : Direction.values()) { - // 检查所有主端,无论方向如何 - for (PatternProviderKey key : masterToSlaves.keySet()) { - if (key.getPos().equals(pos)) { - mastersToRemove.add(key); - } - } - - // 检查所有从端,无论方向如何 - for (PatternProviderKey key : slaveToMaster.keySet()) { - if (key.getPos().equals(pos)) { - slavesToRemove.add(key); - } - } - } - - // 如果没有相关的主从关系,直接返回 - if (mastersToRemove.isEmpty() && slavesToRemove.isEmpty()) { - return; - } - - // 处理所有相关的主端 - for (PatternProviderKey masterKey : mastersToRemove) { - handleMasterBreak(levelAccessor, masterKey); - } - - // 处理所有相关的从端 - for (PatternProviderKey slaveKey : slavesToRemove) { - handleSlaveBreak(levelAccessor, slaveKey); - clearSlavePatterns(levelAccessor, slaveKey); - } - } - - @SubscribeEvent - public static void onBreakSpeed(PlayerEvent.BreakSpeed event) { - Player player = event.getEntity(); - ItemStack mainHandItem = player.getMainHandItem(); - if (!(mainHandItem.getItem() instanceof EndlessBeafItem)) return; - - float newSpeed = event.getOriginalSpeed(); - - if (player.getAbilities().flying || player.isInWater()) { - newSpeed *= 5.0F; - } - event.setNewSpeed(newSpeed); - } - - // 监听方块掉落事件,防止从端样板供应器掉落样板 - @SubscribeEvent - public static void onBlockDrops(BlockEvent.BreakEvent event) { - PatternProviderEvent.onBlockDrops(event); - } - - // 处理主端样板供应器被破坏的逻辑 - public static void handleMasterBreak(LevelAccessor levelAccessor, PatternProviderKey masterKey) { - // 获取所有从端 - Set slaves = masterToSlaves.remove(masterKey); - if (slaves != null) { - for (PatternProviderKey slaveKey : slaves) { - // 清空从端的样板 - clearSlavePatterns(levelAccessor, slaveKey); - // 移除从端到主端的映射 - PatternProviderManager.getSlaveToMaster().remove(slaveKey); - // 移除同步时间记录 - PatternProviderManager.getLastSyncTime().remove(slaveKey); - } - } - - // 如果当前选择的主方块是被破坏的主方块,重置选择 - if (PatternProviderManager.getCurrentSelectedMaster() != null && PatternProviderManager.getCurrentSelectedMaster().equals(masterKey)) { - PatternProviderManager.setCurrentSelectedMaster(null); - } - - // 保存同步数据 - saveSyncDataStatic(levelAccessor); - } - - // 处理从端样板供应器被破坏的逻辑 - public static void handleSlaveBreak(LevelAccessor levelAccessor, PatternProviderKey slaveKey) { - // 获取对应的主端 - PatternProviderKey masterKey = PatternProviderManager.getSlaveToMaster().remove(slaveKey); - if (masterKey != null) { - // 从主端的从端列表中移除该从端 - Set slaves = PatternProviderManager.getMasterToSlaves().get(masterKey); - if (slaves != null) { - slaves.remove(slaveKey); - // 如果主端没有从端了,移除主端映射 - if (slaves.isEmpty()) { - PatternProviderManager.getMasterToSlaves().remove(masterKey); - } - } - // 移除同步时间记录 - PatternProviderManager.getLastSyncTime().remove(slaveKey); - } - - // 保存同步数据 - saveSyncDataStatic(levelAccessor); - } - - // 清空从端样板供应器的样板 - public static void clearSlavePatterns(LevelAccessor levelAccessor, PatternProviderKey slaveKey) { - PatternProviderEvent.clearSlavePatterns(levelAccessor, slaveKey); - } - - // 静态版本的保存同步数据方法 - private static void saveSyncDataStatic(LevelAccessor levelAccessor) { - if (!(levelAccessor instanceof ServerLevel serverLevel)) return; - - // 获取或创建同步数据 - PatternProviderSyncData syncData = serverLevel.getDataStorage().computeIfAbsent( - PatternProviderSyncData::load, - PatternProviderSyncData::new, - SYNC_DATA_TAG - ); - - // 清空现有数据 - syncData.clear(); - - // 保存主从关系 - syncData.getMasterToSlaves().putAll(PatternProviderManager.getMasterToSlaves()); - syncData.getSlaveToMaster().putAll(PatternProviderManager.getSlaveToMaster()); - - // 标记为已更改并保存 - syncData.setDirty(); - } - - // 设置为主方块 - public void setAsMaster(Level world, BlockPos masterPos, Direction direction, Player player) { - PatternProviderOperation.setAsMaster(world, masterPos, direction, player); - } - - // 添加为从方块 - public void addAsSlave(Level world, BlockPos slavePos, Direction direction, Player player) { - PatternProviderOperation.addAsSlave(world, slavePos, direction, player); - } - - // 重置主方块选择(Shift+右键空气) - public static void resetMasterPatternProvider(Level world) { - PatternProviderOperation.resetMasterPatternProvider(world); - } - - // 检查给定位置的从端是否属于当前选定的主端(用于渲染) - public static boolean isSlaveOfCurrentMaster(BlockPos slavePos, Level level) { - // 获取当前选定的主端 - PatternProviderKey selectedMasterKey = getCurrentSelectedMaster(); - if (selectedMasterKey == null) { - return false; - } - - // 遍历所有从端,检查是否有匹配的位置 - for (Map.Entry entry : slaveToMaster.entrySet()) { - PatternProviderKey slaveKey = entry.getKey(); - PatternProviderKey masterKey = entry.getValue(); - - // 检查从端位置是否匹配 - if (slaveKey.getPos().equals(slavePos)) { - // 获取主端的方块实体 - BlockEntity masterBlockEntity = level.getBlockEntity(selectedMasterKey.getPos()); - - // 检查主端是否是方块形式(直接放置的方块,不是面板) - String masterClassName = masterBlockEntity.getClass().getName(); - boolean isBlockForm = masterClassName.equals("com.glodblock.github.extendedae.common.tileentities.TileExPatternProvider") || - masterBlockEntity instanceof appeng.blockentity.crafting.PatternProviderBlockEntity || - masterClassName.equals("net.pedroksl.advanced_ae.common.entities.AdvPatternProviderEntity") || - masterClassName.equals("net.pedroksl.advanced_ae.common.entities.SmallAdvPatternProviderEntity"); - - if (isBlockForm) { - // 方块形式:只需要匹配位置 - if (masterKey.getPos().equals(selectedMasterKey.getPos())) { - return true; - } - } else { - // 面板形式:需要匹配位置和方向 - if (masterKey.equals(selectedMasterKey)) { - return true; - } - } - } - } - - return false; - } - - // 检查给定位置是否是主端样板供应器(用于渲染) - public static boolean isMasterPatternProvider(BlockPos pos) { - // 遍历所有主端,检查是否有匹配的位置 - for (PatternProviderKey masterKey : masterToSlaves.keySet()) { - if (masterKey.getPos().equals(pos)) { - return true; - } - } - - return false; - } - - // 检查给定位置是否是从端样板供应器(用于mixin和渲染) - public static boolean isSlavePatternProvider(BlockPos pos) { - // 遍历所有从端,检查是否有匹配的位置 - for (PatternProviderKey slaveKey : slaveToMaster.keySet()) { - if (slaveKey.getPos().equals(pos)) { - return true; - } - } - - return false; - } - - // 定期同步所有从方块 - private static void syncAllSlaves(Level world) { - // 调用工具类中的同步方法 - PatternProviderManager.syncAllSlaves(world); - } - - // 从游戏数据中加载同步数据 - private static void loadSyncData(Level world) { - if (!(world instanceof ServerLevel serverLevel)) return; - - PatternProviderSyncData syncData = serverLevel.getDataStorage().computeIfAbsent( - PatternProviderSyncData::load, - PatternProviderSyncData::new, - SYNC_DATA_TAG - ); - - // 加载主从关系 - 已移至PatternProviderManager - PatternProviderManager.loadSyncData(serverLevel); - } - - @OnlyIn(Dist.CLIENT) - @Override - public void appendHoverText(ItemStack stack, @Nullable Level level, List tooltip, TooltipFlag flag) { - super.appendHoverText(stack, level, tooltip, flag); - - // 显示当前模式 - if (isSilkTouchMode(stack)) { - tooltip.add(Component.translatable("tooltip.useless_mod.silk_touch_mode").withStyle(ChatFormatting.AQUA)); - int silkTouchLevel = EnchantmentHelper.getTagEnchantmentLevel(Enchantments.SILK_TOUCH, stack); - if (silkTouchLevel > 0) { - tooltip.add(Component.translatable("tooltip.useless_mod.silk_touch_level", silkTouchLevel).withStyle(ChatFormatting.GOLD)); - } - } else { - tooltip.add(Component.translatable("tooltip.useless_mod.fortune_mode").withStyle(ChatFormatting.GOLD)); - int fortuneLevel = EnchantmentHelper.getTagEnchantmentLevel(Enchantments.BLOCK_FORTUNE, stack); - if (fortuneLevel > 0) { - tooltip.add(Component.translatable("tooltip.useless_mod.fortune_level", fortuneLevel).withStyle(ChatFormatting.GREEN)); - if (fortuneLevel > ConfigManager.getFortuneLevel()) { - tooltip.add(Component.translatable("tooltip.useless_mod.external_enchantment").withStyle(ChatFormatting.RED)); - } - } - } - - // 增强连锁模式提示 - tooltip.add(Component.literal(isEnhancedChainMiningMode(stack) ? "增强连锁模式: 已开启" : "增强连锁模式: 已关闭").withStyle(isEnhancedChainMiningMode(stack) ? ChatFormatting.BLUE : ChatFormatting.GRAY)); - - // 强制挖掘模式提示 - tooltip.add(Component.literal(isForceMiningMode(stack) ? "强制挖掘模式: 已开启" : "强制挖掘模式: 已关闭").withStyle(isForceMiningMode(stack) ? ChatFormatting.RED : ChatFormatting.GRAY)); - - // 功能提示 - 动态显示按键绑定 - // 尝试获取实际按键绑定(仅在客户端) - String silkTouchKey = "Page Down"; - String fortuneKey = "Page Up"; - String chainMiningKey = "Tab"; - String enhancedChainMiningKey = "Numpad 8"; - String forceMiningKey = "Numpad 9"; - String triggerForceMiningKey = "R"; - String modeWheelKey = "G"; - - // 新增:主从选择按键 - String setMasterKey = "M"; - String setSlaveKey = "S"; - - try { - // 获取精准采集/时运切换按键 - KeyMapping silkTouchMapping = KeyBindings.SWITCH_SILK_TOUCH_KEY; - silkTouchKey = silkTouchMapping.getTranslatedKeyMessage().getString(); - - KeyMapping fortuneMapping = KeyBindings.SWITCH_FORTUNE_KEY; - fortuneKey = fortuneMapping.getTranslatedKeyMessage().getString(); - - // 获取连锁挖掘切换按键 - chainMiningKey = KeyBindings.SWITCH_CHAIN_MINING_KEY.getTranslatedKeyMessage().getString(); - - // 获取增强连锁模式切换按键 - enhancedChainMiningKey = KeyBindings.SWITCH_ENHANCED_CHAIN_MINING_KEY.getTranslatedKeyMessage().getString(); - - // 获取强制挖掘模式切换按键 - forceMiningKey = KeyBindings.SWITCH_FORCE_MINING_KEY.getTranslatedKeyMessage().getString(); - - // 获取强制挖掘触发按键 - triggerForceMiningKey = KeyBindings.TRIGGER_FORCE_MINING_KEY.getTranslatedKeyMessage().getString(); - - // 获取模式选择轮盘按键 - KeyMapping modeWheelMapping = KeyBindings.SWITCH_MODE_WHEEL_KEY; - modeWheelKey = modeWheelMapping.getTranslatedKeyMessage().getString(); - - // 获取主从选择按键 - KeyMapping setMasterMapping = KeyBindings.SET_MASTER_PATTERN_KEY; - setMasterKey = setMasterMapping.getTranslatedKeyMessage().getString(); - - KeyMapping setSlaveMapping = KeyBindings.SET_SLAVE_PATTERN_KEY; - setSlaveKey = setSlaveMapping.getTranslatedKeyMessage().getString(); - } catch (Exception e) { - // 如果获取失败,使用默认按键名称 - } - - // 添加动态按键提示 - tooltip.add(Component.translatable("tooltip.useless_mod.switch_enchantment").withStyle(ChatFormatting.LIGHT_PURPLE)); - tooltip.add(Component.literal("按住 " + chainMiningKey + "开启连锁挖掘").withStyle(ChatFormatting.LIGHT_PURPLE)); - tooltip.add(Component.literal("按下 " + enhancedChainMiningKey + "切换增强连锁挖掘").withStyle(ChatFormatting.LIGHT_PURPLE)); - tooltip.add(Component.literal("按下 " + forceMiningKey + "切换强制挖掘模式").withStyle(ChatFormatting.LIGHT_PURPLE)); - tooltip.add(Component.literal("按下 " + triggerForceMiningKey + "触发强制破坏").withStyle(ChatFormatting.RED)); - tooltip.add(Component.literal("按下 " + modeWheelKey + " 打开模式选择界面").withStyle(ChatFormatting.YELLOW)); - tooltip.add(Component.literal("按下 " + setMasterKey + "设置主扩展样板供应器").withStyle(ChatFormatting.BLUE)); - tooltip.add(Component.literal("按下 " + setSlaveKey + "设置从扩展样板供应器").withStyle(ChatFormatting.BLUE)); - tooltip.add(Component.translatable("tooltip.useless_mod.fast_break_plastic").withStyle(ChatFormatting.GREEN)); - tooltip.add(Component.translatable("tooltip.useless_mod.festive_affix").withStyle(ChatFormatting.BLUE)); - tooltip.add(Component.translatable("tooltip.useless_mod.auto_collect").withStyle(ChatFormatting.GREEN)); // 新增提示 - tooltip.add(Component.translatable("tooltip.useless_mod.enhanced_chain_description").withStyle(ChatFormatting.BLUE)); - } - - @Override - public boolean isFoil(ItemStack stack) { - return true; // 始终显示附魔光效 - } - - // 禁止效率附魔 - @Override - public boolean canApplyAtEnchantingTable(ItemStack stack, Enchantment enchantment) { - if (enchantment == Enchantments.BLOCK_EFFICIENCY) { - return false; // 禁止效率附魔 - } - return true; // 允许其他所有附魔 - } - - @Override - public int getEnchantmentValue(ItemStack stack) { - return 30; // 允许被附魔 - } - - @Override - public boolean isEnchantable(ItemStack stack) { - return true; // 允许被附魔 - } - - // 重写获取附魔等级的方法 - 现在直接使用NBT中的附魔数据 - @Override - public int getEnchantmentLevel(ItemStack stack, Enchantment enchantment) { - return EnchantmentHelper.getTagEnchantmentLevel(enchantment, stack); - } - - @Override - public void onCraftedBy(ItemStack stack, Level level, Player player) { - super.onCraftedBy(stack, level, player); - // 首次创建时设置为时运模式 - if (!stack.hasTag() || !stack.getTag().contains("SilkTouchMode")) { - switchEnchantmentMode(stack, false); // 默认时运模式 - } else { - // 确保已有抢夺附魔 - updateEnchantments(stack); - } - // 移除了连锁挖掘模式的默认设置,现在只根据按键状态控制 - } - - // 基础物品注册 - public static final RegistryObject ENDLESS_BEAF_ITEM = ITEMS.register("endless_beaf_item", - () -> new EndlessBeafItem( - Tiers.NETHERITE, // Tier - 可根据需要调整 - 50, // Attack damage modifier - 2.0f, // Attack speed modifier - new Item.Properties() - .stacksTo(1) - .rarity(Rarity.EPIC) - .durability(0) - ) { - @Override - public boolean isCorrectToolForDrops(ItemStack stack, BlockState state) { - return state.is(BlockTags.MINEABLE_WITH_PICKAXE) || - state.is(BlockTags.MINEABLE_WITH_AXE) || - state.is(BlockTags.MINEABLE_WITH_SHOVEL) || - state.is(BlockTags.MINEABLE_WITH_HOE); - } - }); - - // 扳手子类物品注册 - public static final RegistryObject ENDLESS_BEAF_WRENCH = ITEMS.register("endless_beaf_wrench", - () -> new EndlessBeafItem( - Tiers.NETHERITE, // Tier - 可根据需要调整 - 50, // Attack damage modifier - 2.0f, // Attack speed modifier - new Item.Properties() - .stacksTo(1) - .rarity(Rarity.EPIC) - .durability(0) - ) { - @Override - public boolean isCorrectToolForDrops(ItemStack stack, BlockState state) { - return state.is(BlockTags.MINEABLE_WITH_PICKAXE) || - state.is(BlockTags.MINEABLE_WITH_AXE) || - state.is(BlockTags.MINEABLE_WITH_SHOVEL) || - state.is(BlockTags.MINEABLE_WITH_HOE); - } - }); - - // 螺丝刀子类物品注册 - public static final RegistryObject ENDLESS_BEAF_SCREWDRIVER = ITEMS.register("endless_beaf_screwdriver", - () -> new EndlessBeafItem( - Tiers.NETHERITE, // Tier - 可根据需要调整 - 50, // Attack damage modifier - 2.0f, // Attack speed modifier - new Item.Properties() - .stacksTo(1) - .rarity(Rarity.EPIC) - .durability(0) - ) { - @Override - public boolean isCorrectToolForDrops(ItemStack stack, BlockState state) { - return state.is(BlockTags.MINEABLE_WITH_PICKAXE) || - state.is(BlockTags.MINEABLE_WITH_AXE) || - state.is(BlockTags.MINEABLE_WITH_SHOVEL) || - state.is(BlockTags.MINEABLE_WITH_HOE); - } - }); - - // 锤子子类物品注册 - public static final RegistryObject ENDLESS_BEAF_MALLET = ITEMS.register("endless_beaf_mallet", - () -> new EndlessBeafItem( - Tiers.NETHERITE, // Tier - 可根据需要调整 - 50, // Attack damage modifier - 2.0f, // Attack speed modifier - new Item.Properties() - .stacksTo(1) - .rarity(Rarity.EPIC) - .durability(0) - ) { - @Override - public boolean isCorrectToolForDrops(ItemStack stack, BlockState state) { - return state.is(BlockTags.MINEABLE_WITH_PICKAXE) || - state.is(BlockTags.MINEABLE_WITH_AXE) || - state.is(BlockTags.MINEABLE_WITH_SHOVEL) || - state.is(BlockTags.MINEABLE_WITH_HOE); - } - }); - - // 撬棍子类物品注册 - public static final RegistryObject ENDLESS_BEAF_CROWBAR = ITEMS.register("endless_beaf_crowbar", - () -> new EndlessBeafItem( - Tiers.NETHERITE, // Tier - 可根据需要调整 - 50, // Attack damage modifier - 2.0f, // Attack speed modifier - new Item.Properties() - .stacksTo(1) - .rarity(Rarity.EPIC) - .durability(0) - ) { - @Override - public boolean isCorrectToolForDrops(ItemStack stack, BlockState state) { - return state.is(BlockTags.MINEABLE_WITH_PICKAXE) || - state.is(BlockTags.MINEABLE_WITH_AXE) || - state.is(BlockTags.MINEABLE_WITH_SHOVEL) || - state.is(BlockTags.MINEABLE_WITH_HOE); - } - }); - - // 铁锤子类物品注册 - public static final RegistryObject ENDLESS_BEAF_HAMMER = ITEMS.register("endless_beaf_hammer", - () -> new EndlessBeafItem( - Tiers.NETHERITE, // Tier - 可根据需要调整 - 50, // Attack damage modifier - 2.0f, // Attack speed modifier - new Item.Properties() - .stacksTo(1) - .rarity(Rarity.EPIC) - .durability(0) - ) { - @Override - public boolean isCorrectToolForDrops(ItemStack stack, BlockState state) { - return state.is(BlockTags.MINEABLE_WITH_PICKAXE) || - state.is(BlockTags.MINEABLE_WITH_AXE) || - state.is(BlockTags.MINEABLE_WITH_SHOVEL) || - state.is(BlockTags.MINEABLE_WITH_HOE); - } - }); - - // 检查是否触发战利品大爆发 - private boolean shouldTriggerFestive(ItemStack stack) { - // 5% 概率 - return Math.random() < 0.05; - } - - // 显示触发提示 - private void sendFestiveMessage(Player player) { - if (player != null) { - player.displayClientMessage( - Component.translatable("message.useless_mod.festive_triggered"), - true - ); - } - } - - @Mod.EventBusSubscriber(modid = UselessMod.MOD_ID, bus = Mod.EventBusSubscriber.Bus.FORGE) - public static class EventHandler { - @SubscribeEvent - public static void onLivingDrops(LivingDropsEvent event) { - // 检查伤害来源是否是玩家 - if (event.getSource().getEntity() instanceof Player player) { - ItemStack mainHandItem = player.getMainHandItem(); - - // 检查主手物品是否是EndlessBeafItem - if (mainHandItem.getItem() instanceof EndlessBeafItem endlessBeaf) { - endlessBeaf.onLivingDrops(event, mainHandItem, player); - } - } - } - - @SubscribeEvent - public static void onBlockBreak(BlockEvent.BreakEvent event) { - Player player = event.getPlayer(); - if (player == null) return; - - ItemStack mainHandItem = player.getMainHandItem(); - - // 检查主手物品是否是EndlessBeafItem - if (mainHandItem.getItem() instanceof EndlessBeafItem) { - MiningDispatcher.dispatchBreak(event, mainHandItem, player); - } - } - - @OnlyIn(Dist.CLIENT) - @SubscribeEvent - public static void onClientTick(TickEvent.ClientTickEvent event) { - if (event.phase != TickEvent.Phase.END) return; - - Minecraft minecraft = Minecraft.getInstance(); - Player player = minecraft.player; - if (player == null) return; - - // 只检查增强连锁模式切换按键(数字键8) - while (KeyBindings.SWITCH_ENHANCED_CHAIN_MINING_KEY.consumeClick()) { - ModMessages.sendToServer(new EnhancedChainMiningTogglePacket()); - } - - // 检查强制挖掘模式切换按键(数字键9) - while (KeyBindings.SWITCH_FORCE_MINING_KEY.consumeClick()) { - ModMessages.sendToServer(new ForceMiningTogglePacket()); - } - - // 检查设置主扩展样板供应器按键(M键) - while (KeyBindings.SET_MASTER_PATTERN_KEY.consumeClick()) { - ItemStack mainHandItem = player.getMainHandItem(); - if (mainHandItem.getItem() instanceof EndlessBeafItem endlessBeaf) { - // 处理设置主方块按键逻辑 - endlessBeaf.handleSetMasterPatternKey(mainHandItem, player); - } - } - - // 检查设置从扩展样板供应器按键(S键) - while (KeyBindings.SET_SLAVE_PATTERN_KEY.consumeClick()) { - ItemStack mainHandItem = player.getMainHandItem(); - if (mainHandItem.getItem() instanceof EndlessBeafItem endlessBeaf) { - // 处理设置从方块按键逻辑 - endlessBeaf.handleSetSlavePatternKey(mainHandItem, player); - } - } - } - - @SubscribeEvent - public static void onPlayerTick(TickEvent.PlayerTickEvent event) { - Player player = event.player; - Level world = player.level(); - - // 只在服务器端执行 - if (world.isClientSide()) { - return; - } - - // 首次加载同步数据 - if (masterToSlaves.isEmpty() && slaveToMaster.isEmpty()) { - loadSyncData(world); - } - - // 定期同步扩展样板供应器主从方块 - if (event.phase == TickEvent.Phase.END) { - syncAllSlaves(world); - } - - // 处理飞行权限管理 - 每次tick都检查,不受phase限制 - // 检查玩家物品栏中是否有任何EndlessBeafItem变体 - boolean hasItemInInventory = player.getInventory().items.stream() - .anyMatch(item -> item.getItem() instanceof EndlessBeafItem || - (item.hasTag() && item.getTag().contains("ToolModes"))); - - UUID playerId = player.getUUID(); - - if (hasItemInInventory) { - // 物品在物品栏中,根据配置决定是否赋予飞行权限 - if (ConfigManager.enableFlightEffect() && !player.getAbilities().mayfly) { - player.getAbilities().mayfly = true; - player.onUpdateAbilities(); - } - playerFlightStatus.put(playerId, true); - - // 根据配置决定是否给予药水效果 - if (ConfigManager.enablePotionEffects()) { - // 给予饱和效果(不显示粒子,但显示图标) - MobEffectInstance baohe = player.getEffect(MobEffects.SATURATION); - if (baohe == null || baohe.getDuration() < 20) { - player.addEffect(new MobEffectInstance(MobEffects.SATURATION, 200, 0, true, false, true)); - } - - // 给予生命恢复效果(不显示粒子,但显示图标) - MobEffectInstance zaisheng = player.getEffect(MobEffects.REGENERATION); - if (zaisheng == null || (zaisheng.getDuration() < 20)) { - player.addEffect(new MobEffectInstance(MobEffects.REGENERATION, 200, 5, true, false, true)); - } - - // 给予夜视效果(不显示粒子,但显示图标) - MobEffectInstance yeshi = player.getEffect(MobEffects.NIGHT_VISION); - if (yeshi == null || (yeshi.getDuration() < 2000)) { - player.addEffect(new MobEffectInstance(MobEffects.NIGHT_VISION, 20000, 0, true, false, true)); - } - - // 新增:给予抗火效果(不显示粒子,但显示图标) - MobEffectInstance kanghuo = player.getEffect(MobEffects.FIRE_RESISTANCE); - if (kanghuo == null || kanghuo.getDuration() < 200) { - player.addEffect(new MobEffectInstance(MobEffects.FIRE_RESISTANCE, 2000, 0, true, false, true)); - } - - // 新增:给予水下呼吸效果(不显示粒子,但显示图标) - MobEffectInstance shuixiabreath = player.getEffect(MobEffects.WATER_BREATHING); - if (shuixiabreath == null || shuixiabreath.getDuration() < 200) { - player.addEffect(new MobEffectInstance(MobEffects.WATER_BREATHING, 2000, 0, true, false, true)); - } - - // 新增:给予抗性提升效果(不显示粒子,但显示图标) - MobEffectInstance kangxing = player.getEffect(MobEffects.DAMAGE_RESISTANCE); - if (kangxing == null || kangxing.getDuration() < 200) { - player.addEffect(new MobEffectInstance(MobEffects.DAMAGE_RESISTANCE, 2000, 5, true, false, true)); - } - } - } else { - // 物品不在物品栏中,对于非创造模式玩家,关闭飞行权限 - // 只有当玩家是因为我们的工具才获得飞行权限的情况下,才会关闭飞行权限 - if (!player.isCreative() && player.getAbilities().mayfly && playerFlightStatus.getOrDefault(playerId, false)) { - player.getAbilities().mayfly = false; - player.getAbilities().flying = false; - player.onUpdateAbilities(); - } - playerFlightStatus.put(playerId, false); - } - } - } - - // 处理掉落物事件的方法 - public void onLivingDrops(LivingDropsEvent event, ItemStack stack, Player player) { - if (!shouldTriggerFestive(stack)) { - return; - } - - LivingEntity killedEntity = event.getEntity(); - Level level = killedEntity.level(); - - if (!level.isClientSide) { - // 显示提示消息 - sendFestiveMessage(player); - // 直接修改掉落物堆叠数量 - 更简单有效的方法 - Collection drops = event.getDrops(); - List newDrops = new ArrayList<>(); - - for (ItemEntity itemEntity : drops) { - if (!isEquipment(itemEntity.getItem())) { - ItemStack itemStack = itemEntity.getItem(); - // 直接将堆叠数量乘以20 - int originalCount = itemStack.getCount(); - itemStack.setCount(originalCount * 20); - - // 重新创建ItemEntity以确保更新 - ItemEntity newItem = new ItemEntity( - level, - itemEntity.getX(), itemEntity.getY(), itemEntity.getZ(), - itemStack - ); - newItem.setDeltaMovement( - -0.3 + level.random.nextDouble() * 0.6, - 0.3 + level.random.nextDouble() * 0.3, - -0.3 + level.random.nextDouble() * 0.6 - ); - newDrops.add(newItem); - } else { - newDrops.add(itemEntity); - } - } - - // 清空原掉落物列表并添加新的 - drops.clear(); - drops.addAll(newDrops); - } - } - - // 检查物品是否是装备(基于Festive Affix的逻辑) - private boolean isEquipment(ItemStack stack) { - // 检查是否有装备标记(基于Festive Affix的逻辑) - if (stack.hasTag() && stack.getTag().getBoolean("apoth.equipment")) { - return true; - } - - // 可损坏的物品通常是装备(工具、武器、盔甲) - return stack.isDamageableItem(); - } - - @Override - public Component getName(ItemStack stack) { - // 获取基础名称 - Component baseName = super.getName(stack); - - // 根据模式添加后缀 - if (isSilkTouchMode(stack)) { - return Component.translatable("item.useless_mod.endless_beaf_item.silk_touch"); - } else { - return Component.translatable("item.useless_mod.endless_beaf_item.fortune"); - } - } - - @Override - public void inventoryTick(ItemStack pStack, Level pLevel, Entity pEntity, int pSlotId, boolean pIsSelected) { - super.inventoryTick(pStack, pLevel, pEntity, pSlotId, pIsSelected); - if(pEntity instanceof Player player){ - UUID playerId = player.getUUID(); - boolean hasItemInInventory = player.getInventory().items.stream() - .anyMatch(item -> item.getItem() instanceof EndlessBeafItem || - (item.hasTag() && item.getTag().contains("ToolModes"))); - - if (hasItemInInventory) { - // 根据配置决定是否给予药水效果 - if (ConfigManager.enablePotionEffects()) { - // 给予饱和效果(不显示粒子,但显示图标) - MobEffectInstance baohe = player.getEffect(MobEffects.SATURATION); - if (baohe == null || baohe.getDuration() < 20) { - player.addEffect(new MobEffectInstance(MobEffects.SATURATION, 200, 0, true, false, true)); - } - - // 给予生命恢复效果(不显示粒子,但显示图标) - MobEffectInstance zaisheng = player.getEffect(MobEffects.REGENERATION); - if (zaisheng == null || (zaisheng.getDuration() < 20)) { - player.addEffect(new MobEffectInstance(MobEffects.REGENERATION, 200, 5, true, false, true)); - } - - // 给予夜视效果(不显示粒子,但显示图标) - MobEffectInstance yeshi = player.getEffect(MobEffects.NIGHT_VISION); - if (yeshi == null || (yeshi.getDuration() < 2000)) { - player.addEffect(new MobEffectInstance(MobEffects.NIGHT_VISION, 20000, 0, true, false, true)); - } - - // 新增:给予抗火效果(不显示粒子,但显示图标) - MobEffectInstance kanghuo = player.getEffect(MobEffects.FIRE_RESISTANCE); - if (kanghuo == null || kanghuo.getDuration() < 200) { - player.addEffect(new MobEffectInstance(MobEffects.FIRE_RESISTANCE, 2000, 0, true, false, true)); - } - - // 新增:给予水下呼吸效果(不显示粒子,但显示图标) - MobEffectInstance shuixiabreath = player.getEffect(MobEffects.WATER_BREATHING); - if (shuixiabreath == null || shuixiabreath.getDuration() < 200) { - player.addEffect(new MobEffectInstance(MobEffects.WATER_BREATHING, 2000, 0, true, false, true)); - } - - // 新增:给予抗性提升效果(不显示粒子,但显示图标) - MobEffectInstance kangxing = player.getEffect(MobEffects.DAMAGE_RESISTANCE); - if (kangxing == null || kangxing.getDuration() < 200) { - player.addEffect(new MobEffectInstance(MobEffects.DAMAGE_RESISTANCE, 2000, 5, true, false, true)); - } - } - - // 新增:当物品在玩家物品栏内允许飞行(无论游戏模式) - // 修复:直接检查玩家当前飞行状态,兼容外部飞行管理系统 - if (ConfigManager.enableFlightEffect() && !player.getAbilities().mayfly) { - player.getAbilities().mayfly = true; - player.onUpdateAbilities(); - } - playerFlightStatus.put(playerId, true); - } else { - // 物品不在物品栏时,对于非创造模式的玩家,关闭飞行权限 - // 只有当玩家是因为我们的工具才获得飞行权限的情况下,才会关闭飞行权限 - if (!player.isCreative() && player.getAbilities().mayfly && playerFlightStatus.getOrDefault(playerId, false)) { - player.getAbilities().mayfly = false; - player.getAbilities().flying = false; - player.onUpdateAbilities(); - } - playerFlightStatus.put(playerId, false); - } - } - } - - @Override - public boolean isCorrectToolForDrops(@NotNull BlockState state) { - return true; - } - - @Override - public boolean canPerformAction(ItemStack stack, ToolAction toolAction) { - // 声明这个物品可以执行斧头和锄头的所有动作 - return toolAction.equals(ToolActions.AXE_STRIP) || - toolAction.equals(ToolActions.AXE_SCRAPE) || - toolAction.equals(ToolActions.AXE_WAX_OFF) || - ToolActions.DEFAULT_PICKAXE_ACTIONS.contains(toolAction) || - super.canPerformAction(stack, toolAction)|| - toolAction.equals(ToolActions.HOE_TILL); - } - - @Override - public boolean doesSneakBypassUse(ItemStack stack, LevelReader world, BlockPos pos, Player player) { - // 检查是否是塑料块,如果是则不跳过useOn方法,这样快速拆塑料块功能才能生效 - Block block = world.getBlockState(pos).getBlock(); - if (block instanceof GlowPlasticBlock) { - // 对于塑料块,不绕过useOn方法,以便执行快速破坏逻辑 - return false; - } - // 对于其他方块,允许Shift+右键事件传递到方块/机器,这对于格雷机器的边缘选择框功能至关重要 - return true; - } - - private boolean isPlasticBlock(Block block) { - // 直接检查是否是 GlowPlasticBlock 的实例 - if (block instanceof GlowPlasticBlock) { - return true; - } - return false; - } - - @NotNull - @Override - public InteractionResult useOn(UseOnContext context) { - Level world = context.getLevel(); - BlockPos blockpos = context.getClickedPos(); - Player player = context.getPlayer(); - BlockState blockstate = world.getBlockState(blockpos); - - // 修复:创造模式下不处理快速破坏塑料块 - if (player != null && player.isCreative()) { - return InteractionResult.PASS; - } - - // 按住 Shift 的右键仍然保留你原本的"快速破坏塑料块(不掉落粒子)"逻辑 - if (player != null && player.isShiftKeyDown()) { - if (isPlasticBlock(blockstate.getBlock())) { - if (!world.isClientSide) { - // 在服务器端:把方块的掉落物放进背包(或在背包满时丢出) - List drops = BlockBreakUtils.getBlockDrops(blockstate, (Level) world, blockpos, player, context.getItemInHand()); - for (ItemStack drop : drops) { - // 复制一个堆叠放入(以免修改原 list) - ItemStack toAdd = drop.copy(); - if (!BlockBreakUtils.addItemToPlayerInventory(player, toAdd)) { - // 背包满了:丢在玩家脚下 - player.drop(toAdd, false); - } - } - - // 移除方块并播放声音 - world.setBlock(blockpos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL_IMMEDIATE); - // 使用带冷却的音效播放 - BlockBreakUtils.playBreakSoundWithCooldown(world, blockpos, blockstate, player); - } else { - // 客户端只播放声音(不做掉落/方块移除) - world.playSound(player, blockpos, SoundEvents.STONE_BREAK, SoundSource.BLOCKS, 0.7F, 1.0F); - } - return InteractionResult.sidedSuccess(world.isClientSide); - } - } - - // 以下保持你原本的"万能工具作为斧头/锄头"等的行为 - BlockState resultToSet = null; - - // 1. 作为斧头(去皮) - BlockState axeResult = blockstate.getToolModifiedState(context, ToolActions.AXE_STRIP, false); - if (axeResult != null) { - world.playSound(player, blockpos, SoundEvents.AXE_STRIP, SoundSource.BLOCKS, 1.0F, 1.0F); - resultToSet = axeResult; - } - - // 2. 刮蜡 - if (resultToSet == null) { - BlockState scrapeResult = blockstate.getToolModifiedState(context, ToolActions.AXE_SCRAPE, false); - if (scrapeResult != null) { - world.playSound(player, blockpos, SoundEvents.AXE_SCRAPE, SoundSource.BLOCKS, 1.0F, 1.0F); - resultToSet = scrapeResult; - } - } - - // 3. 去蜡/解除氧化 - if (resultToSet == null) { - BlockState oxidizeResult = blockstate.getToolModifiedState(context, ToolActions.AXE_WAX_OFF, false); - if (oxidizeResult != null) { - world.playSound(player, blockpos, SoundEvents.AXE_WAX_OFF, SoundSource.BLOCKS, 1.0F, 1.0F); - resultToSet = oxidizeResult; - } - } - - // 4. 锄头耕地 - if (resultToSet == null) { - BlockState hoeResult = blockstate.getToolModifiedState(context, ToolActions.HOE_TILL, false); - if (hoeResult != null) { - world.playSound(player, blockpos, SoundEvents.HOE_TILL, SoundSource.BLOCKS, 1.0F, 1.0F); - resultToSet = hoeResult; - } - } - - if (resultToSet == null) { - return InteractionResult.PASS; - } - - if (!world.isClientSide) { - ItemStack stack = context.getItemInHand(); - if (player instanceof ServerPlayer serverPlayer) { - CriteriaTriggers.ITEM_USED_ON_BLOCK.trigger(serverPlayer, blockpos, stack); - } - world.setBlock(blockpos, resultToSet, Block.UPDATE_ALL_IMMEDIATE); - if (player != null) { - stack.hurtAndBreak(1, player, onBroken -> onBroken.broadcastBreakEvent(context.getHand())); - } - } - - return InteractionResult.sidedSuccess(world.isClientSide); - } - - @Override - public float getDestroySpeed(ItemStack stack, BlockState state) { - // 获取基础破坏速度 - float baseSpeed = 10.0f; - - // 只对有效方块应用速度加成 - if (state.getDestroySpeed(null, null) > 0) { - // 应用类似MinersFervorEnchant的机制 - // 基础速度7.5F + 每级4.5F加成,最大29.9999F - float maxSpeed = Math.min(29.9999F, baseSpeed); - float hardness = state.getDestroySpeed(null, null); - if (hardness > 0) { - return maxSpeed * hardness; - } - } - - return baseSpeed; - } - - -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/items/UpgradeAugmentItem.java b/src/main/java/com/sorrowmist/useless/items/UpgradeAugmentItem.java deleted file mode 100644 index da0ec4ed..00000000 --- a/src/main/java/com/sorrowmist/useless/items/UpgradeAugmentItem.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.sorrowmist.useless.items; - -/* - * This file is part of Thermal More Integral Components. - * - * Copyright (c) 2024 Elephant_dev - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import cofh.lib.util.helpers.StringHelper; -import cofh.thermal.lib.common.item.AugmentItem; -import net.minecraft.ChatFormatting; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.network.chat.Component; -import net.minecraft.world.item.Item; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.TooltipFlag; -import net.minecraft.world.level.Level; -import net.minecraftforge.api.distmarker.Dist; -import net.minecraftforge.api.distmarker.OnlyIn; -import org.jetbrains.annotations.Nullable; - -import java.util.List; - -public class UpgradeAugmentItem extends AugmentItem { - public UpgradeAugmentItem(Item.Properties builder, CompoundTag augmentData) { - super(builder, augmentData); - } - - @OnlyIn(Dist.CLIENT) - public void appendHoverText(ItemStack stack, @Nullable Level worldIn, List tooltip, TooltipFlag flagIn) { - tooltip.add(1, StringHelper.getTextComponent("useless_mod.augment.upgrade").withStyle(ChatFormatting.GOLD)); - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/menu/slot/HighStackSlot.java b/src/main/java/com/sorrowmist/useless/menu/slot/HighStackSlot.java deleted file mode 100644 index c410f608..00000000 --- a/src/main/java/com/sorrowmist/useless/menu/slot/HighStackSlot.java +++ /dev/null @@ -1,39 +0,0 @@ -// HighStackSlot.java -package com.sorrowmist.useless.menu.slot; - -import net.minecraft.world.item.ItemStack; -import net.minecraftforge.items.IItemHandler; -import net.minecraftforge.items.SlotItemHandler; - -import javax.annotation.Nonnull; - -public class HighStackSlot extends SlotItemHandler { - public HighStackSlot(IItemHandler itemHandler, int index, int xPosition, int yPosition) { - super(itemHandler, index, xPosition, yPosition); - } - - @Override - public int getMaxStackSize() { - return Integer.MAX_VALUE; - } - - @Override - public int getMaxStackSize(@Nonnull ItemStack stack) { - return Integer.MAX_VALUE; - } - - /** - * 获取用于显示的堆叠,可以修改显示的数量等 - */ - public ItemStack getDisplayStack() { - ItemStack stack = getItem(); - if (stack.isEmpty()) { - return ItemStack.EMPTY; - } - - // 总是创建一个显示用的堆叠(数量为1,实际数量通过工具提示显示) - ItemStack displayStack = stack.copy(); - displayStack.setCount(1); - return displayStack; - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/mixin/AEBaseMenuMixin.java b/src/main/java/com/sorrowmist/useless/mixin/AEBaseMenuMixin.java deleted file mode 100644 index 87e9a2c7..00000000 --- a/src/main/java/com/sorrowmist/useless/mixin/AEBaseMenuMixin.java +++ /dev/null @@ -1,132 +0,0 @@ -package com.sorrowmist.useless.mixin; - -import appeng.menu.AEBaseMenu; -import appeng.menu.implementations.PatternProviderMenu; -import com.sorrowmist.useless.items.EndlessBeafItem; -import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.inventory.ClickType; -import net.minecraft.world.item.ItemStack; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; - -@Mixin(AEBaseMenu.class) -public class AEBaseMenuMixin { - - @Inject(method = "quickMoveStack", at = @At("HEAD"), cancellable = true) - private void onQuickMoveStack(Player player, int idx, CallbackInfoReturnable cir) { - // 获取实际的AEBaseMenu实例 - AEBaseMenu menu = (AEBaseMenu)(Object)this; - - // 检查是否是PatternProviderMenu或扩展样板供应器菜单实例 - boolean isPatternMenu = menu instanceof PatternProviderMenu; - boolean isExPatternMenu = menu.getClass().getName().equals("com.glodblock.github.extendedae.container.ContainerExPatternProvider"); - boolean isAaeAdvPatternMenu = menu.getClass().getName().contains("AdvPatternProvider"); - - if (isPatternMenu || isExPatternMenu || isAaeAdvPatternMenu) { - // 检查当前菜单是否属于从端样板供应器 - boolean isSlave = isSlaveMenu(menu); - - if (isSlave) { - // 如果是从端,返回空栈,阻止快速转移 - cir.setReturnValue(ItemStack.EMPTY); - } - } - } - - @Inject(method = "clicked", at = @At("HEAD"), cancellable = true) - private void onClicked(int slotId, int button, ClickType clickType, Player player, CallbackInfo ci) { - // 获取实际的AEBaseMenu实例 - AEBaseMenu menu = (AEBaseMenu)(Object)this; - - // 检查是否是PatternProviderMenu或扩展样板供应器菜单实例 - boolean isPatternMenu = menu instanceof PatternProviderMenu; - boolean isExPatternMenu = menu.getClass().getName().equals("com.glodblock.github.extendedae.container.ContainerExPatternProvider"); - boolean isAaeAdvPatternMenu = menu.getClass().getName().contains("AdvPatternProvider"); - - if (isPatternMenu || isExPatternMenu || isAaeAdvPatternMenu) { - // 检查当前菜单是否属于从端样板供应器 - boolean isSlave = isSlaveMenu(menu); - - if (isSlave) { - // 阻止所有点击操作 - ci.cancel(); - } - } - } - - /** - * 检查当前菜单是否属于从端样板供应器 - * 对于方块形式:检查位置 - * 对于面板形式:检查位置和方向 - */ - private boolean isSlaveMenu(AEBaseMenu menu) { - try { - // 检查BlockEntity情况(方块形式) - if (menu.getBlockEntity() != null) { - BlockPos pos = menu.getBlockEntity().getBlockPos(); - // 方块形式:直接检查位置 - return com.sorrowmist.useless.items.EndlessBeafItem.isSlavePatternProvider(pos); - } - - // 检查Part情况(面板形式) - java.lang.reflect.Field partField = AEBaseMenu.class.getDeclaredField("part"); - partField.setAccessible(true); - Object part = partField.get(menu); - - if (part != null) { - try { - // 获取位置 - BlockPos pos = null; - Direction direction = null; - - // 尝试获取BlockEntity和位置 - java.lang.reflect.Method getBlockEntityMethod = part.getClass().getMethod("getBlockEntity"); - Object blockEntity = getBlockEntityMethod.invoke(part); - if (blockEntity != null) { - java.lang.reflect.Method getBlockPosMethod = blockEntity.getClass().getMethod("getBlockPos"); - pos = (BlockPos) getBlockPosMethod.invoke(blockEntity); - } - - // 尝试获取方向 - try { - java.lang.reflect.Method getSideMethod = part.getClass().getMethod("getSide"); - direction = (Direction) getSideMethod.invoke(part); - } catch (NoSuchMethodException e) { - // 有些面板可能没有getSide方法,尝试获取方向字段 - try { - java.lang.reflect.Field sideField = part.getClass().getDeclaredField("side"); - sideField.setAccessible(true); - direction = (Direction) sideField.get(part); - } catch (Exception ex) { - // 忽略方向获取异常,继续执行 - } - } - - if (pos != null) { - // 面板形式:检查该位置的所有从端,找到匹配的方向 - for (com.sorrowmist.useless.utils.pattern.PatternProviderKey slaveKey : - com.sorrowmist.useless.items.EndlessBeafItem.slaveToMaster.keySet()) { - if (slaveKey.getPos().equals(pos)) { - // 如果方向匹配或者方向为null(中心部件),则是从端 - if (direction == null || slaveKey.getDirection().equals(direction)) { - return true; - } - } - } - } - } catch (Exception e) { - // 忽略反射异常,继续执行 - } - } - } catch (Exception e) { - // 忽略反射异常 - } - - return false; - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/mixin/AbstractContainerScreenMixin.java b/src/main/java/com/sorrowmist/useless/mixin/AbstractContainerScreenMixin.java deleted file mode 100644 index 3996c2b6..00000000 --- a/src/main/java/com/sorrowmist/useless/mixin/AbstractContainerScreenMixin.java +++ /dev/null @@ -1,31 +0,0 @@ -// mixin/AbstractContainerScreenMixin.java -package com.sorrowmist.useless.mixin; - -import com.sorrowmist.useless.menu.slot.HighStackSlot; -import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; -import net.minecraft.world.inventory.Slot; -import net.minecraft.world.item.ItemStack; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.ModifyVariable; - -@Mixin(AbstractContainerScreen.class) -public class AbstractContainerScreenMixin { - @ModifyVariable( - method = {"renderSlot"}, - index = 5, - at = @At( - value = "STORE", - ordinal = 0 - ) - - ) - private ItemStack modifyRenderedStack(ItemStack original, GuiGraphics guiGraphics, Slot slot) { - if (slot instanceof HighStackSlot highStackSlot) { - return highStackSlot.getDisplayStack(); - } else { - return original; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/mixin/BlockGrassWandDropMixin.java b/src/main/java/com/sorrowmist/useless/mixin/BlockGrassWandDropMixin.java new file mode 100644 index 00000000..bdc4ec06 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/BlockGrassWandDropMixin.java @@ -0,0 +1,24 @@ +package com.sorrowmist.useless.mixin; + +import com.sorrowmist.useless.event.GrassWandDropHandler; +import net.minecraft.core.BlockPos; +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.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** 1.20.1 has no BlockDropsEvent. Run after real destruction, not simulated recipe/loot queries. */ +@Mixin(Block.class) +public abstract class BlockGrassWandDropMixin { + @Inject(method="playerDestroy", at=@At("TAIL")) + private void useless$grassReward(Level level,Player player,BlockPos pos,BlockState state, + BlockEntity entity,ItemStack tool,CallbackInfo ci) { + GrassWandDropHandler.onBlockDestroyed(level,player,pos,state); + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/BlockTravelAnchorMixin.java b/src/main/java/com/sorrowmist/useless/mixin/BlockTravelAnchorMixin.java deleted file mode 100644 index e24ab366..00000000 --- a/src/main/java/com/sorrowmist/useless/mixin/BlockTravelAnchorMixin.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.sorrowmist.useless.mixin; - -import de.castcrafter.travelanchors.block.BlockTravelAnchor; -import net.minecraft.core.BlockPos; -import net.minecraft.world.level.Level; -import net.minecraft.world.level.block.state.BlockState; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Mixin(BlockTravelAnchor.class) -public class BlockTravelAnchorMixin { - - @Inject( - method = "onRemove", - at = @At("HEAD") - ) - private void onRemoveInjection(BlockState state, Level level, BlockPos pos, - BlockState newState, boolean moved, CallbackInfo ci) { - if (!level.isClientSide && state.getBlock() != newState.getBlock()) { - handleTravelAnchorBreak(level, pos, state); - } - } - - private void handleTravelAnchorBreak(Level level, BlockPos pos, BlockState state) { - // TODO: 你的逻辑 - } -} diff --git a/src/main/java/com/sorrowmist/useless/mixin/ClientLevelMixin.java b/src/main/java/com/sorrowmist/useless/mixin/ClientLevelMixin.java new file mode 100644 index 00000000..6ba7f285 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/ClientLevelMixin.java @@ -0,0 +1,29 @@ +package com.sorrowmist.useless.mixin; + +import com.sorrowmist.useless.event.EventHandler; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.world.entity.Entity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.stream.StreamSupport; + +@Mixin(ClientLevel.class) +public class ClientLevelMixin { + @Inject(method = "entitiesForRendering", at = @At("RETURN"), cancellable = true) + private void useless_mod$filterProtectedPlayersFromRenderingEntities( + CallbackInfoReturnable> cir + ) { + if (!EventHandler.hasAnyBeefAdvancedStealthPlayers()) { + return; + } + + Iterable entities = cir.getReturnValue(); + cir.setReturnValue(() -> StreamSupport.stream(entities.spliterator(), false) + .filter(entity -> !(entity instanceof net.minecraft.world.entity.player.Player player + && EventHandler.hasBeefAdvancedStealthItem(player))) + .iterator()); + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/ClientPacketListenerMixin.java b/src/main/java/com/sorrowmist/useless/mixin/ClientPacketListenerMixin.java new file mode 100644 index 00000000..35eb1b42 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/ClientPacketListenerMixin.java @@ -0,0 +1,26 @@ +package com.sorrowmist.useless.mixin; + +import com.sorrowmist.useless.event.client.BeefToolFlightClient; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.network.protocol.game.ClientboundPlayerAbilitiesPacket; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * 记录「服务端确实授予过飞行能力」这一事实,供造化杖的客户端飞行兜底使用。 + *

+ * 只有服务端发过 {@code canFly=true},才说明飞行能力是模组该给的 —— + * {@code LocalPlayerMixin} 的外部清除拦截与 {@code BeefToolFlightClient} 的本地兜底都靠它加闸, + * 否则在服务端把 {@code enable_flight_effect} 关掉时会被误触发(客户端以为自己能飞、服务端不认)。 + */ +@Mixin(ClientPacketListener.class) +public class ClientPacketListenerMixin { + @Inject( + method = "handlePlayerAbilities(Lnet/minecraft/network/protocol/game/ClientboundPlayerAbilitiesPacket;)V", + at = @At("TAIL")) + private void useless_mod$trackServerGrantedFlight(ClientboundPlayerAbilitiesPacket packet, CallbackInfo ci) { + BeefToolFlightClient.onServerAbilities(packet.canFly()); + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/EntityGetterMixin.java b/src/main/java/com/sorrowmist/useless/mixin/EntityGetterMixin.java new file mode 100644 index 00000000..70950b0a --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/EntityGetterMixin.java @@ -0,0 +1,85 @@ +package com.sorrowmist.useless.mixin; + +import com.sorrowmist.useless.utils.ProtectedPlayerQueries; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySelector; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.ai.targeting.TargetingConditions; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.EntityGetter; +import net.minecraft.world.phys.AABB; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +/** Forge's Mixin 0.8.5 cannot inject into interfaces. Preserve the vanilla default + * queries here, changing only their input list to hide protected players. */ +@Mixin(EntityGetter.class) +public interface EntityGetterMixin { + /** @author UselessMod @reason Exclude advanced stealth players before selecting the nearest. */ + @Overwrite + default Player getNearestPlayer(double x, double y, double z, double range, + Predicate predicate) { + Player nearest = null; + double distance = -1; + for (Player player : ProtectedPlayerQueries.visiblePlayers((EntityGetter) this)) { + if (predicate != null && !predicate.test(player)) continue; + double candidate = player.distanceToSqr(x, y, z); + if ((range < 0 || candidate < range * range) && (distance < 0 || candidate < distance)) { + distance = candidate; + nearest = player; + } + } + return nearest; + } + + /** @author UselessMod @reason Preserve the alive query while excluding protected players. */ + @Overwrite + default boolean hasNearbyAlivePlayer(double x, double y, double z, double range) { + for (Player player : ProtectedPlayerQueries.visiblePlayers((EntityGetter) this)) { + if (EntitySelector.NO_SPECTATORS.test(player) + && EntitySelector.LIVING_ENTITY_STILL_ALIVE.test(player) + && (range < 0 || player.distanceToSqr(x, y, z) < range * range)) return true; + } + return false; + } + + /** @author UselessMod @reason Select from visible players with vanilla targeting conditions. */ + @Overwrite + default Player getNearestPlayer(TargetingConditions conditions, LivingEntity source) { + EntityGetter self = (EntityGetter) this; + return self.getNearestEntity(ProtectedPlayerQueries.visiblePlayers(self), conditions, + source, source.getX(), source.getY(), source.getZ()); + } + + /** @author UselessMod @reason Select from visible players with vanilla targeting conditions. */ + @Overwrite + default Player getNearestPlayer(TargetingConditions conditions, LivingEntity source, + double x, double y, double z) { + EntityGetter self = (EntityGetter) this; + return self.getNearestEntity(ProtectedPlayerQueries.visiblePlayers(self), conditions, source, x, y, z); + } + + /** @author UselessMod @reason Select from visible players with vanilla targeting conditions. */ + @Overwrite + default Player getNearestPlayer(TargetingConditions conditions, double x, double y, double z) { + EntityGetter self = (EntityGetter) this; + return self.getNearestEntity(ProtectedPlayerQueries.visiblePlayers(self), conditions, null, x, y, z); + } + + /** @author UselessMod @reason Preserve the vanilla AABB and targeting checks for visible players. */ + @Overwrite + default List getNearbyPlayers(TargetingConditions conditions, LivingEntity source, AABB area) { + List result = new ArrayList<>(); + for (Player player : ProtectedPlayerQueries.visiblePlayers((EntityGetter) this)) { + if (area.contains(player.getX(), player.getY(), player.getZ()) && conditions.test(source, player)) { + result.add(player); + } + } + return result; + } + +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/EntityMixin.java b/src/main/java/com/sorrowmist/useless/mixin/EntityMixin.java new file mode 100644 index 00000000..19b5ecac --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/EntityMixin.java @@ -0,0 +1,99 @@ +package com.sorrowmist.useless.mixin; + +import com.sorrowmist.useless.event.EventHandler; +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(Entity.class) +public class EntityMixin { + @Inject(method = "isPickable", at = @At("HEAD"), cancellable = true) + private void useless_mod$makeBeefPlayerUnpickable(CallbackInfoReturnable cir) { + Entity entity = (Entity) (Object) this; + if (entity instanceof Player player && EventHandler.hasBeefAdvancedStealthItem(player)) { + cir.setReturnValue(false); + } + } + + @Inject(method = "isAttackable", at = @At("HEAD"), cancellable = true) + private void useless_mod$makeBeefPlayerUnattackable(CallbackInfoReturnable cir) { + Entity entity = (Entity) (Object) this; + if (entity instanceof Player player && EventHandler.hasBeefAdvancedStealthItem(player)) { + cir.setReturnValue(false); + } + } + + @Inject(method = "canBeHitByProjectile", at = @At("HEAD"), cancellable = true) + private void useless_mod$makeBeefPlayerProjectileUntargetable(CallbackInfoReturnable cir) { + Entity entity = (Entity) (Object) this; + if (entity instanceof Player player && EventHandler.hasBeefAdvancedStealthItem(player)) { + cir.setReturnValue(false); + } + } + + /** + * 从源头拦截「被保护的玩家着火」。 + * + *

{@code setRemainingFireTicks} 是原版所有点火路径(火方块 {@code entityInside}、 + * 岩浆 {@code lavaHurt}、火附魔 / 火焰箭 / 火球走的 {@code igniteForSeconds})的 + * 唯一汇入点,且本身只是纯字段赋值。在 HEAD 直接取消写入,字段就恒为 0, + * 于是 {@code Entity#baseTick} 末尾的 {@code setSharedFlagOnFire(true)} 永远不会触发, + * 客户端的火焰叠加层与火焰粒子也就根本不会出现——无需每 tick 清理。

+ * + *

注意 {@link Player} 覆写了该方法(创造/无敌时把值钳到 1),虚分派下对玩家永远走 + * {@code Player} 的实现,因此 {@code PlayerMixin} 里必须再注一份,否则此处会被绕过。

+ */ + @Inject(method = "setRemainingFireTicks", at = @At("HEAD"), cancellable = true) + private void useless_mod$preventBeefPlayerIgnition(int ticks, CallbackInfo ci) { + if (ticks > 0 && (Object) this instanceof Player player && EventHandler.hasBeefInvulnerabilityItem(player)) { + ci.cancel(); + } + } + + @Inject(method = "setPos(DDD)V", at = @At("HEAD"), cancellable = true) + private void useless_mod$protectBeefPlayerFromUnsafeSetPos(double x, double y, double z, CallbackInfo ci) { + Entity entity = (Entity) (Object) this; + if (entity instanceof Player player && EventHandler.hasBeefInvulnerabilityItem(player) && isUnsafePosition(player, x, y, z)) { + EventHandler.restoreBeefProtectedPlayer(player); + ci.cancel(); + } + } + + @Inject(method = "setRemoved", at = @At("HEAD"), cancellable = true) + private void useless_mod$protectBeefPlayerFromSetRemoved(Entity.RemovalReason reason, CallbackInfo ci) { + Entity entity = (Entity) (Object) this; + if (entity instanceof Player player && shouldProtectFromRemoval(reason) && EventHandler.shouldApplyBeefInvulnerability(player)) { + EventHandler.restoreBeefProtectedPlayer(player); + ci.cancel(); + } + } + + @Inject(method = "remove", at = @At("HEAD"), cancellable = true) + private void useless_mod$protectBeefPlayerFromRemove(Entity.RemovalReason reason, CallbackInfo ci) { + Entity entity = (Entity) (Object) this; + if (entity instanceof Player player && shouldProtectFromRemoval(reason) && EventHandler.shouldApplyBeefInvulnerability(player)) { + EventHandler.restoreBeefProtectedPlayer(player); + ci.cancel(); + } + } + + private static boolean shouldProtectFromRemoval(Entity.RemovalReason reason) { + return reason == Entity.RemovalReason.DISCARDED; + } + + private static boolean isUnsafePosition(Player player, double x, double y, double z) { + if (!Double.isFinite(x) || !Double.isFinite(y) || !Double.isFinite(z)) { + return true; + } + + Level level = player.level(); + BlockPos pos = BlockPos.containing(x, y, z); + return y < level.getMinBuildHeight() || !level.getWorldBorder().isWithinBounds(pos); + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/GuiAssemblerMatrixPatternInfoMixin.java b/src/main/java/com/sorrowmist/useless/mixin/GuiAssemblerMatrixPatternInfoMixin.java deleted file mode 100644 index 8557d093..00000000 --- a/src/main/java/com/sorrowmist/useless/mixin/GuiAssemblerMatrixPatternInfoMixin.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.sorrowmist.useless.mixin; - - - -import com.sorrowmist.useless.config.ConfigManager; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.Constant; -import org.spongepowered.asm.mixin.injection.ModifyConstant; - -@Mixin(targets = "com.glodblock.github.extendedae.client.gui.GuiAssemblerMatrix$PatternInfo") -public class GuiAssemblerMatrixPatternInfoMixin { - - @ModifyConstant(method = "", constant = @Constant(intValue = 36)) - private int modifySlotCount(int original) { - int newSize = 36 * ConfigManager.getMatrixPatternCount(); - System.out.println("Modifying PatternInfo slot count from " + original + " to " + newSize); - return newSize; - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/mixin/LevelMixin.java b/src/main/java/com/sorrowmist/useless/mixin/LevelMixin.java index 0682c914..d01b4cbd 100644 --- a/src/main/java/com/sorrowmist/useless/mixin/LevelMixin.java +++ b/src/main/java/com/sorrowmist/useless/mixin/LevelMixin.java @@ -1,14 +1,65 @@ package com.sorrowmist.useless.mixin; import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.event.EventHandler; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; import net.minecraft.world.level.Level; +import net.minecraft.world.level.entity.EntityTypeTest; +import net.minecraft.world.phys.AABB; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + @Mixin(value = Level.class) public class LevelMixin { + @Inject(method = "getEntities(Lnet/minecraft/world/level/entity/EntityTypeTest;Lnet/minecraft/world/phys/AABB;Ljava/util/function/Predicate;)Ljava/util/List;", at = @At("RETURN"), cancellable = true) + private void useless_mod$filterBeefProtectedPlayers(EntityTypeTest entityTypeTest, AABB area, Predicate predicate, CallbackInfoReturnable> cir) { + List filtered = useless_mod$filterBeefProtectedPlayers(cir.getReturnValue()); + if (filtered != null) { + cir.setReturnValue(filtered); + } + } + + @Inject(method = "getEntities(Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/phys/AABB;Ljava/util/function/Predicate;)Ljava/util/List;", at = @At("RETURN"), cancellable = true) + private void useless_mod$filterBeefProtectedPlayersFromEntityQuery(Entity except, AABB area, Predicate predicate, CallbackInfoReturnable> cir) { + List filtered = useless_mod$filterBeefProtectedPlayers(cir.getReturnValue()); + if (filtered != null) { + cir.setReturnValue(filtered); + } + } + + private static List useless_mod$filterBeefProtectedPlayers(List entities) { + if (entities == null || entities.isEmpty()) { + return null; + } + + List filtered = null; + for (int i = 0; i < entities.size(); i++) { + T entity = entities.get(i); + if (entity instanceof Player player && EventHandler.hasBeefAdvancedStealthItem(player)) { + if (filtered == null) { + filtered = new ArrayList<>(entities.size()); + for (T previous : entities.subList(0, i)) { + filtered.add(previous); + } + } + continue; + } + + if (filtered != null) { + filtered.add(entity); + } + } + + return filtered; + } + @Inject( method = "isDay", at = @At("HEAD"), @@ -44,6 +95,6 @@ private void uselessDimAlwaysClear_thunder(CallbackInfoReturnable cir) private boolean isUselessDimension() { Level level = (Level) (Object) this; - return UselessMod.MOD_ID.equals(level.dimension().location().getNamespace()); + return UselessMod.MODID.equals(level.dimension().location().getNamespace()); } -} \ No newline at end of file +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/LivingEntityMixin.java b/src/main/java/com/sorrowmist/useless/mixin/LivingEntityMixin.java new file mode 100644 index 00000000..01510d72 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/LivingEntityMixin.java @@ -0,0 +1,75 @@ +package com.sorrowmist.useless.mixin; + +import com.sorrowmist.useless.event.EventHandler; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(LivingEntity.class) +public class LivingEntityMixin { + @Inject(method = "canBeSeenByAnyone", at = @At("HEAD"), cancellable = true) + private void useless_mod$hideProtectedPlayerFromVisibilityChecks(CallbackInfoReturnable cir) { + LivingEntity entity = (LivingEntity) (Object) this; + if (entity instanceof Player player && EventHandler.hasBeefAdvancedStealthItem(player)) { + cir.setReturnValue(false); + } + } + + @Inject(method = "handleEntityEvent", at = @At("HEAD"), cancellable = true) + private void useless_mod$protectBeefPlayerFromDeathEvent(byte id, CallbackInfo ci) { + LivingEntity entity = (LivingEntity) (Object) this; + if (id == 3 && entity instanceof Player player && EventHandler.hasBeefInvulnerabilityItem(player)) { + EventHandler.restoreBeefProtectedPlayer(player); + ci.cancel(); + } + } + + @Inject(method = "tickDeath", at = @At("HEAD"), cancellable = true) + private void useless_mod$protectBeefPlayerFromTickDeath(CallbackInfo ci) { + LivingEntity entity = (LivingEntity) (Object) this; + if (entity instanceof Player player && EventHandler.hasBeefInvulnerabilityItem(player)) { + EventHandler.restoreBeefProtectedPlayer(player); + ci.cancel(); + } + } + + @Inject(method = "setHealth", at = @At("HEAD"), cancellable = true) + private void useless_mod$protectBeefPlayerFromSetHealth(float health, CallbackInfo ci) { + LivingEntity entity = (LivingEntity) (Object) this; + if ((health <= 0.0F || Float.isNaN(health)) + && entity instanceof Player player + && EventHandler.hasBeefInvulnerabilityItem(player) + && !EventHandler.isRestoringBeefProtectedPlayer(player)) { + EventHandler.restoreBeefProtectedPlayer(player); + ci.cancel(); + } + } + + @Inject(method = "hurt", at = @At("HEAD"), cancellable = true) + private void useless_mod$protectBeefPlayerFromHurt(DamageSource source, float amount, CallbackInfoReturnable cir) { + LivingEntity entity = (LivingEntity) (Object) this; + if ((!Float.isFinite(amount) || amount >= entity.getMaxHealth() || entity.getHealth() - amount <= 0.0F) + && entity instanceof Player player + && EventHandler.hasBeefInvulnerabilityItem(player) + && !EventHandler.isRestoringBeefProtectedPlayer(player)) { + EventHandler.restoreBeefProtectedPlayer(player); + cir.setReturnValue(false); + } + } + + @Inject(method = "die", at = @At("HEAD"), cancellable = true) + private void useless_mod$protectBeefPlayerFromDie(DamageSource source, CallbackInfo ci) { + LivingEntity entity = (LivingEntity) (Object) this; + if (entity instanceof Player player + && EventHandler.hasBeefInvulnerabilityItem(player) + && !EventHandler.isRestoringBeefProtectedPlayer(player)) { + EventHandler.restoreBeefProtectedPlayer(player); + ci.cancel(); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/LocalPlayerMixin.java b/src/main/java/com/sorrowmist/useless/mixin/LocalPlayerMixin.java new file mode 100644 index 00000000..47ab78d1 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/LocalPlayerMixin.java @@ -0,0 +1,50 @@ +package com.sorrowmist.useless.mixin; + +import com.sorrowmist.useless.event.client.BeefToolFlightClient; +import com.sorrowmist.useless.utils.UselessItemUtils; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.entity.player.Abilities; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * 拦掉外部 mod 对造化杖飞行的「伪装成玩家操作」的清除上报。 + *

+ * 问题:Re-Avaritia 的 {@code AbilityHandler#updateClientServerFlight} + * 在客户端也有一份,它会直接把客户端的 {@code mayfly} / {@code flying} 清成 false, + * 然后调用 {@link LocalPlayer#onUpdateAbilities()} 发 {@code ServerboundPlayerAbilitiesPacket}。 + * 这个包和「玩家自己双击关飞行 / 落地」发出的包**完全一样**,服务端无法区分, + * 于是被当成玩家意图处理,把服务端的「粘滞飞行」一起解除 —— + * 表现为「切回生存后即使离地很高也会掉下来」。 + *

+ * 判据:服务端每 tick 都保证携带造化杖时 {@code mayfly=true},而 + *

    + *
  • 玩家自己的操作(双击关飞行、落地检查)只改 {@code flying},{@code mayfly} 仍是 true;
  • + *
  • 外部 mod 的清除会把 {@code mayfly} 一起清掉。
  • + *
+ * 所以「带着造化杖但客户端 {@code mayfly} 是 false」就是外部清除的精确特征。 + *

+ * 做法:把 {@code mayfly} 还原回去,并取消这次上报 —— 玩家自己的飞行意图就不会被误清除。 + * {@code flying} 不在这里动,交给服务端下一 tick 的补发,这样玩家真落地时仍然能正常停飞。 + */ +@Mixin(LocalPlayer.class) +public class LocalPlayerMixin { + @Inject(method = "onUpdateAbilities", at = @At("HEAD"), cancellable = true) + private void useless_mod$blockExternalFlightClear(CallbackInfo ci) { + LocalPlayer self = (LocalPlayer) (Object) this; + Abilities abilities = self.getAbilities(); + if (abilities.mayfly || !UselessItemUtils.hasTargetToolInInventory(self)) { + return; + } + // 加闸:服务端从没给过飞行能力(例如 enable_flight_effect 被关掉)时,mayfly=false 是正常的, + // 不能拦,否则会把客户端强行设成「能飞」而服务端不认,反而有被反作弊判 floating 的风险。 + if (!BeefToolFlightClient.serverGrantedFlight()) { + return; + } + + abilities.mayfly = true; + ci.cancel(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/MixinConfigPlugin.java b/src/main/java/com/sorrowmist/useless/mixin/MixinConfigPlugin.java deleted file mode 100644 index 4d751935..00000000 --- a/src/main/java/com/sorrowmist/useless/mixin/MixinConfigPlugin.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.sorrowmist.useless.mixin; - -import net.minecraftforge.fml.ModList; -import net.minecraftforge.fml.loading.LoadingModList; -import net.minecraftforge.fml.loading.moddiscovery.ModInfo; -import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; -import org.spongepowered.asm.mixin.extensibility.IMixinInfo; - -import java.util.List; -import java.util.Set; - -public class MixinConfigPlugin implements IMixinConfigPlugin { - - private static final String MODID_GTOCORE = "gtocore"; - - @Override - public void onLoad(String mixinPackage) { - } - - @Override - public String getRefMapperConfig() { - return null; - } - - @Override - public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { - // 检查这两个mixin是否需要gtocore模组 - if (mixinClassName.endsWith("jei.ForgePluginFinderMixin") || - mixinClassName.endsWith("emi.EmiAgnosMixin")) { - return isModLoaded(MODID_GTOCORE); - } - return true; - } - - @Override - public void acceptTargets(Set myTargets, Set otherTargets) { - } - - @Override - public List getMixins() { - return null; - } - - @Override - public void preApply(String targetClassName, org.objectweb.asm.tree.ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { - } - - @Override - public void postApply(String targetClassName, org.objectweb.asm.tree.ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { - } - - private static boolean isModLoaded(String modId) { - if (ModList.get() == null) { - return LoadingModList.get().getMods().stream() - .map(ModInfo::getModId) - .anyMatch(modId::equals); - } else { - return ModList.get().isLoaded(modId); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/mixin/MultiPlayerGameModeMixin.java b/src/main/java/com/sorrowmist/useless/mixin/MultiPlayerGameModeMixin.java new file mode 100644 index 00000000..fcdfd51f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/MultiPlayerGameModeMixin.java @@ -0,0 +1,68 @@ +package com.sorrowmist.useless.mixin; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.MultiPlayerGameMode; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.entity.player.Abilities; +import net.minecraft.world.level.GameType; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * 保护造化杖飞行不被原版的「客户端本地游戏模式重置」抹掉。 + *

+ * 背景:切换游戏模式时,客户端会收到 {@code ClientboundGameEventPacket(CHANGE_GAME_MODE)}, + * 走 {@code ClientPacketListener#handleGameEvent} → {@link MultiPlayerGameMode#setLocalMode(GameType)} + * → {@code GameType#updatePlayerAbilities},在本地把 mayfly / flying 按新模式整套重置。 + * 切到生存时这一步会把两者都清成 false,而服务端那边 MinecraftForge 是保留 flying 的 + * ({@code ServerPlayerGameMode#setGameModeForPlayer} 的补丁),造化杖也会继续授予 mayfly。 + * 更麻烦的是:客户端紧接着会把 {@code flying=false} 通过 + * {@code ServerboundPlayerAbilitiesPacket} 回传给服务端,于是服务端那份「保留的悬停」也被跟着清掉。 + *

+ * 做法:在重置前后把服务端上次同步过来的值原样还原回去。这不是「凭空造一个状态」—— + * 只是别让原版这次本地重置提前把飞行丢掉;如果服务端确实要收回(比如杖被拿走、配置关掉了), + * 它的能力包会紧随其后把客户端纠正过来,服务端始终是权威。 + *

+ * 只在切到非创造/非旁观时还原:切进创造/旁观时原版给的默认值本来就是对的,不能动。 + */ +@Mixin(MultiPlayerGameMode.class) +public class MultiPlayerGameModeMixin { + @Unique + private boolean useless_mod$mayflyBefore; + + @Unique + private boolean useless_mod$flyingBefore; + + @Inject(method = "setLocalMode(Lnet/minecraft/world/level/GameType;)V", at = @At("HEAD")) + private void useless_mod$captureAbilitiesBeforeReset(GameType type, CallbackInfo ci) { + Abilities abilities = useless_mod$localAbilities(); + if (abilities == null) { + return; + } + this.useless_mod$mayflyBefore = abilities.mayfly; + this.useless_mod$flyingBefore = abilities.flying; + } + + @Inject(method = "setLocalMode(Lnet/minecraft/world/level/GameType;)V", at = @At("RETURN")) + private void useless_mod$restoreAbilitiesAfterReset(GameType type, CallbackInfo ci) { + if (type == GameType.CREATIVE || type == GameType.SPECTATOR) { + return; + } + + Abilities abilities = useless_mod$localAbilities(); + if (abilities == null) { + return; + } + abilities.mayfly = this.useless_mod$mayflyBefore; + abilities.flying = this.useless_mod$flyingBefore; + } + + @Unique + private static Abilities useless_mod$localAbilities() { + LocalPlayer player = Minecraft.getInstance().player; + return player == null ? null : player.getAbilities(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/PatternAccessTermMenuMixin.java b/src/main/java/com/sorrowmist/useless/mixin/PatternAccessTermMenuMixin.java deleted file mode 100644 index 74435a31..00000000 --- a/src/main/java/com/sorrowmist/useless/mixin/PatternAccessTermMenuMixin.java +++ /dev/null @@ -1,170 +0,0 @@ -package com.sorrowmist.useless.mixin; - -import com.sorrowmist.useless.items.EndlessBeafItem; -import appeng.helpers.InventoryAction; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.world.level.block.entity.BlockEntity; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Mixin(appeng.menu.implementations.PatternAccessTermMenu.class) -public class PatternAccessTermMenuMixin { - - @Inject(method = "doAction", at = @At("HEAD"), cancellable = true, remap = false) - private void onDoAction(ServerPlayer player, InventoryAction action, int slot, long id, CallbackInfo ci) { - // 获取实际的PatternAccessTermMenu实例 - appeng.menu.implementations.PatternAccessTermMenu menu = (appeng.menu.implementations.PatternAccessTermMenu)(Object)this; - - // 使用反射获取byId字段 - try { - // 获取byId字段 - java.lang.reflect.Field byIdField = appeng.menu.implementations.PatternAccessTermMenu.class.getDeclaredField("byId"); - byIdField.setAccessible(true); - - // 获取byId映射 - var byId = (java.util.Map) byIdField.get(menu); - Object containerTracker = byId.get(id); - - if (containerTracker != null) { - // 获取container字段 - java.lang.reflect.Field containerField = containerTracker.getClass().getDeclaredField("container"); - containerField.setAccessible(true); - - // 获取PatternContainer实例 - Object container = containerField.get(containerTracker); - - // 检查是否是从端样板供应器 - if (isSlavePatternProvider(container)) { - // 阻止所有取出操作 - ci.cancel(); - } - } - } catch (Exception e) { - // 忽略反射异常 - } - } - - // 检查是否是从端样板供应器 - private boolean isSlavePatternProvider(Object container) { - try { - // 检查是否是普通AE2方块形式的样板供应器 - if (container instanceof appeng.blockentity.crafting.PatternProviderBlockEntity patternProvider) { - // 检查是否是从端 - return EndlessBeafItem.isSlavePatternProvider(patternProvider.getBlockPos()); - } - - // 检查是否是AE2面板形式的普通样板供应器 - if (container instanceof appeng.parts.crafting.PatternProviderPart patternProviderPart) { - // 获取位置,通过getHost()方法获取IPartHost,再获取BlockEntity,最后获取位置 - try { - appeng.api.parts.IPartHost host = patternProviderPart.getHost(); - if (host != null) { - BlockEntity blockEntity = host.getBlockEntity(); - if (blockEntity != null) { - // 检查是否是从端 - return EndlessBeafItem.isSlavePatternProvider(blockEntity.getBlockPos()); - } - } - } catch (Exception e) { - // 忽略反射异常 - } - } - - // 检查是否是方块形式的扩展样板供应器 - if (container.getClass().getName().equals("com.glodblock.github.extendedae.common.tileentities.TileExPatternProvider")) { - // 获取位置 - java.lang.reflect.Method getBlockPosMethod = container.getClass().getMethod("getBlockPos"); - var pos = (net.minecraft.core.BlockPos) getBlockPosMethod.invoke(container); - // 检查是否是从端 - return EndlessBeafItem.isSlavePatternProvider(pos); - } - - // 检查是否是面板形式的扩展样板供应器 - if (container.getClass().getName().equals("com.glodblock.github.extendedae.common.parts.PartExPatternProvider")) { - // 获取位置,通过getHost()方法获取IPartHost,再获取BlockEntity,最后获取位置 - try { - java.lang.reflect.Method getHostMethod = container.getClass().getMethod("getHost"); - Object host = getHostMethod.invoke(container); - if (host != null) { - java.lang.reflect.Method getBlockEntityMethod = host.getClass().getMethod("getBlockEntity"); - Object blockEntity = getBlockEntityMethod.invoke(host); - if (blockEntity != null) { - java.lang.reflect.Method getBlockPosMethod = blockEntity.getClass().getMethod("getBlockPos"); - var pos = (net.minecraft.core.BlockPos) getBlockPosMethod.invoke(blockEntity); - // 检查是否是从端 - return EndlessBeafItem.isSlavePatternProvider(pos); - } - } - } catch (Exception e) { - // 忽略反射异常 - } - } - - // 检查是否是方块形式的Advanced AE高级样板供应器 - if (container.getClass().getName().equals("net.pedroksl.advanced_ae.common.entities.AdvPatternProviderEntity")) { - // 获取位置 - java.lang.reflect.Method getBlockPosMethod = container.getClass().getMethod("getBlockPos"); - var pos = (net.minecraft.core.BlockPos) getBlockPosMethod.invoke(container); - // 检查是否是从端 - return EndlessBeafItem.isSlavePatternProvider(pos); - } - - // 检查是否是方块形式的Advanced AE小型高级样板供应器 - if (container.getClass().getName().equals("net.pedroksl.advanced_ae.common.entities.SmallAdvPatternProviderEntity")) { - // 获取位置 - java.lang.reflect.Method getBlockPosMethod = container.getClass().getMethod("getBlockPos"); - var pos = (net.minecraft.core.BlockPos) getBlockPosMethod.invoke(container); - // 检查是否是从端 - return EndlessBeafItem.isSlavePatternProvider(pos); - } - - // 检查是否是面板形式的Advanced AE高级样板供应器 - if (container.getClass().getName().equals("net.pedroksl.advanced_ae.common.parts.AdvPatternProviderPart")) { - // 获取位置,通过getHost()方法获取IPartHost,再获取BlockEntity,最后获取位置 - try { - java.lang.reflect.Method getHostMethod = container.getClass().getMethod("getHost"); - Object host = getHostMethod.invoke(container); - if (host != null) { - java.lang.reflect.Method getBlockEntityMethod = host.getClass().getMethod("getBlockEntity"); - Object blockEntity = getBlockEntityMethod.invoke(host); - if (blockEntity != null) { - java.lang.reflect.Method getBlockPosMethod = blockEntity.getClass().getMethod("getBlockPos"); - var pos = (net.minecraft.core.BlockPos) getBlockPosMethod.invoke(blockEntity); - // 检查是否是从端 - return EndlessBeafItem.isSlavePatternProvider(pos); - } - } - } catch (Exception e) { - // 忽略反射异常 - } - } - - // 检查是否是面板形式的Advanced AE小型高级样板供应器 - if (container.getClass().getName().equals("net.pedroksl.advanced_ae.common.parts.SmallAdvPatternProviderPart")) { - // 获取位置,通过getHost()方法获取IPartHost,再获取BlockEntity,最后获取位置 - try { - java.lang.reflect.Method getHostMethod = container.getClass().getMethod("getHost"); - Object host = getHostMethod.invoke(container); - if (host != null) { - java.lang.reflect.Method getBlockEntityMethod = host.getClass().getMethod("getBlockEntity"); - Object blockEntity = getBlockEntityMethod.invoke(host); - if (blockEntity != null) { - java.lang.reflect.Method getBlockPosMethod = blockEntity.getClass().getMethod("getBlockPos"); - var pos = (net.minecraft.core.BlockPos) getBlockPosMethod.invoke(blockEntity); - // 检查是否是从端 - return EndlessBeafItem.isSlavePatternProvider(pos); - } - } - } catch (Exception e) { - // 忽略反射异常 - } - } - } catch (Exception e) { - // 忽略反射异常 - } - - return false; - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/mixin/PatternSlotTrackerMixin.java b/src/main/java/com/sorrowmist/useless/mixin/PatternSlotTrackerMixin.java deleted file mode 100644 index 675330e2..00000000 --- a/src/main/java/com/sorrowmist/useless/mixin/PatternSlotTrackerMixin.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.sorrowmist.useless.mixin; - -import com.sorrowmist.useless.config.ConfigManager; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Redirect; - -@Mixin(targets = "com.glodblock.github.extendedae.container.ContainerAssemblerMatrix$PatternSlotTracker") -public class PatternSlotTrackerMixin { - - /** - * 修改PatternSlotTracker构造函数中client库存的创建 - * 将硬编码的36改为从配置获取 - */ - @Redirect( - method = "", - at = @At( - value = "NEW", - target = "appeng/util/inv/AppEngInternalInventory", - ordinal = 0 - ), - remap = false - ) - private appeng.util.inv.AppEngInternalInventory modifyClientInventory(int size) { - // 使用配置的倍数,而不是硬编码的36 - return new appeng.util.inv.AppEngInternalInventory(36 * ConfigManager.getMatrixPatternCount()); - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/mixin/PlayerMixin.java b/src/main/java/com/sorrowmist/useless/mixin/PlayerMixin.java new file mode 100644 index 00000000..65a4eec4 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/PlayerMixin.java @@ -0,0 +1,41 @@ +package com.sorrowmist.useless.mixin; + +import com.sorrowmist.useless.event.EventHandler; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(Player.class) +public class PlayerMixin { + @Inject(method = "canBeSeenAsEnemy", at = @At("HEAD"), cancellable = true) + private void useless_mod$hideProtectedPlayerFromEnemyChecks(CallbackInfoReturnable cir) { + Player player = (Player) (Object) this; + if (EventHandler.hasBeefAdvancedStealthItem(player)) { + cir.setReturnValue(false); + } + } + + @Inject(method = "attack", at = @At("HEAD"), cancellable = true) + private void useless_mod$preventAttackingBeefProtectedPlayer(Entity target, CallbackInfo ci) { + if (target instanceof Player player && EventHandler.hasBeefAdvancedStealthItem(player)) { + ci.cancel(); + } + } + + /** + * {@link Entity} 的 {@code setRemainingFireTicks} 被 {@link Player} 覆写 + * (带 {@code abilities.invulnerable} 钳制),虚分派下对玩家永远走这里。 + * 因此 {@code EntityMixin} 那份对玩家不会生效,必须在本类再拦一份, + * 否则「玩家保护开启后仍着火」的问题会原样保留。 + */ + @Inject(method = "setRemainingFireTicks", at = @At("HEAD"), cancellable = true) + private void useless_mod$preventBeefPlayerIgnition(int ticks, CallbackInfo ci) { + if (ticks > 0 && EventHandler.hasBeefInvulnerabilityItem((Player) (Object) this)) { + ci.cancel(); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/ServerGamePacketListenerImplFlightMixin.java b/src/main/java/com/sorrowmist/useless/mixin/ServerGamePacketListenerImplFlightMixin.java new file mode 100644 index 00000000..84529a7b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/ServerGamePacketListenerImplFlightMixin.java @@ -0,0 +1,44 @@ +package com.sorrowmist.useless.mixin; + +import com.sorrowmist.useless.event.EventHandler; +import net.minecraft.network.protocol.game.ServerboundPlayerAbilitiesPacket; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * 记录「客户端自己上报的飞行意图」,供造化杖的粘滞飞行使用。 + *

+ * 为什么需要这个:粘滞飞行要能区分两种情况—— + *

    + *
  • 玩家自己关掉飞行(落地、或空中双击)→ 应该允许,不能把人钉在空中;
  • + *
  • 别的 mod 在服务端偷偷把 {@code flying} 清掉(实测 Re-Avaritia 的 + * {@code AbilityHandler#updateClientServerFlight} 每秒清一次)→ 应该被我们拉回来。
  • + *
+ * 两者在服务端 {@code abilities} 上都是「flying 变成 false」,只看字段分不出来。 + * 但只有前者会经过这里:玩家关飞行时客户端会发 + * {@code ServerboundPlayerAbilitiesPacket},而 mod 的服务端代码是直接改字段的。 + *

+ * 不能用 {@code player.onGround()} 判断落地:{@code ServerGamePacketListenerImpl#handleMovePlayer} + * 里 {@code setOnGround(flag4)} 的 flag4 含 {@code !player.mayFly()},只要 mayfly 是开着的, + * 服务端的 onGround 就恒为 false。 + *

+ * 注意:注册在 json 的通用 {@code mixins} 段而不是 {@code server} 段 —— + * 单人存档的集成服务端属于客户端侧,放 {@code server} 段不会生效。 + */ +@Mixin(ServerGamePacketListenerImpl.class) +public class ServerGamePacketListenerImplFlightMixin { + @Shadow + public ServerPlayer player; + + @Inject( + method = "handlePlayerAbilities(Lnet/minecraft/network/protocol/game/ServerboundPlayerAbilitiesPacket;)V", + at = @At("HEAD")) + private void useless_mod$trackClientFlightIntent(ServerboundPlayerAbilitiesPacket packet, CallbackInfo ci) { + EventHandler.onClientFlightIntent(this.player, packet.isFlying()); + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/ServerGamePacketListenerImplMixin.java b/src/main/java/com/sorrowmist/useless/mixin/ServerGamePacketListenerImplMixin.java new file mode 100644 index 00000000..bae38ec4 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/ServerGamePacketListenerImplMixin.java @@ -0,0 +1,41 @@ +package com.sorrowmist.useless.mixin; + +import com.sorrowmist.useless.event.EventHandler; +import net.minecraft.network.protocol.game.ServerboundInteractPacket; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.world.entity.player.Player; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * 高级隐身状态下,拦下其它玩家对被隐身者的交互请求。 + *

+ * 有意注册在 json 的 {@code server} 段:拦截目标是 + * {@link ServerboundInteractPacket},即「别的玩家对我发起的交互」。单人存档里不存在 + * 其它玩家,这个包根本不会出现,因此没有在集成服务端生效的必要;只有独立服务端 + * (多人游戏)才需要它。放在 {@code server} 段可避免单人存档无谓地加载这个 mixin。 + *

+ * 对比 {@link ServerGamePacketListenerImplFlightMixin}:那个处理的是玩家自己的 + * 飞行上报,单人存档也需要,所以必须放通用段。两者注册段不同是刻意为之,别互相挪。 + */ +@Mixin(ServerGamePacketListenerImpl.class) +public class ServerGamePacketListenerImplMixin { + @Shadow + public ServerPlayer player; + + @Inject(method = "handleInteract", at = @At("HEAD"), cancellable = true) + private void useless_mod$preventProtectedPlayerInteraction( + ServerboundInteractPacket packet, + CallbackInfo ci + ) { + if (packet.getTarget(this.player.serverLevel()) instanceof Player target + && EventHandler.shouldApplyBeefAdvancedStealth(target)) { + ci.cancel(); + } + } + +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/ServerLevelMixin.java b/src/main/java/com/sorrowmist/useless/mixin/ServerLevelMixin.java new file mode 100644 index 00000000..e3951f7f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/ServerLevelMixin.java @@ -0,0 +1,40 @@ +package com.sorrowmist.useless.mixin; + +import com.sorrowmist.useless.event.EventHandler; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.stream.StreamSupport; + +@Mixin(ServerLevel.class) +public class ServerLevelMixin { + @Inject(method = "getAllEntities", at = @At("RETURN"), cancellable = true) + private void useless_mod$filterProtectedPlayersFromAllEntities( + CallbackInfoReturnable> cir + ) { + if (!EventHandler.hasAnyBeefAdvancedStealthPlayers()) { + return; + } + + Iterable entities = cir.getReturnValue(); + // Keep the original iterable live while filtering only its read path. + cir.setReturnValue(() -> StreamSupport.stream(entities.spliterator(), false) + .filter(entity -> !(entity instanceof Player player + && EventHandler.hasBeefAdvancedStealthItem(player))) + .iterator()); + } + + @Inject(method = "broadcastEntityEvent", at = @At("HEAD"), cancellable = true) + private void useless_mod$protectBeefPlayerFromBroadcastDeathEvent(Entity entity, byte eventId, CallbackInfo ci) { + if (eventId == 3 && entity instanceof Player player && EventHandler.shouldApplyBeefInvulnerability(player)) { + EventHandler.restoreBeefProtectedPlayer(player); + ci.cancel(); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/ServerPlayerGameModeMixin.java b/src/main/java/com/sorrowmist/useless/mixin/ServerPlayerGameModeMixin.java new file mode 100644 index 00000000..fb86e671 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/ServerPlayerGameModeMixin.java @@ -0,0 +1,45 @@ +package com.sorrowmist.useless.mixin; + +import com.sorrowmist.useless.event.EventHandler; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.ServerPlayerGameMode; +import net.minecraft.world.level.GameType; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * 让服务端在切换游戏模式的瞬间就把造化杖的飞行补回去,消掉客户端那 1 帧的空窗。 + *

+ * 问题:服务端切模式时 {@link ServerPlayerGameMode#setGameModeForPlayer} 会先按新模式 + * 重置 abilities(生存模式把 mayfly 清成 false,MinecraftForge 再把 flying 保留回来)。紧接着 + * {@code ServerPlayer#setGameMode} 就会把这个状态发给客户端({@code changeGameModeForPlayer} + * 里的能力包,以及 {@code setGameMode} 里的能力包 + CHANGE_GAME_MODE 事件)。 + *

+ * 而模组的授予逻辑跑在 {@code PlayerTickEvent.Post},比这几步晚 —— 所以客户端会先收到 + * {@code mayfly=false},要等下一个 tick 模组补发的包才恢复。实测就是 1 帧(50ms)的空窗, + * 玩家正好在这期间双击空格就起不了飞。 + *

+ * 做法:在这个方法的 RETURN 立刻调用模组的授予逻辑。此时 + * {@code gameModeForPlayer} 已经是新模式,所以 {@code player.isCreative()} 判断是对的, + * 补发出去的能力包直接带上 {@code mayfly=true},客户端从头到尾看不到 false。 + *

+ * 注意:本 mixin 注册在 json 的通用 {@code mixins} 段而不是 {@code server} 段 —— + * {@code server} 段只在独立服务端生效,单人存档的集成服务端属于客户端侧,放那里会失效。 + */ +@Mixin(ServerPlayerGameMode.class) +public class ServerPlayerGameModeMixin { + @Shadow + @Final + private ServerPlayer player; + + @Inject( + method = "setGameModeForPlayer(Lnet/minecraft/world/level/GameType;Lnet/minecraft/world/level/GameType;)V", + at = @At("RETURN")) + private void useless_mod$regrantStaffFlight(GameType newGameMode, GameType previousGameMode, CallbackInfo ci) { + EventHandler.applyBeefToolFlightNow(this.player); + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/TileAssemblerMatrixPatternMixin.java b/src/main/java/com/sorrowmist/useless/mixin/TileAssemblerMatrixPatternMixin.java deleted file mode 100644 index 52d3b3e5..00000000 --- a/src/main/java/com/sorrowmist/useless/mixin/TileAssemblerMatrixPatternMixin.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.sorrowmist.useless.mixin; - -import appeng.util.inv.AppEngInternalInventory; -import appeng.util.inv.FilteredInternalInventory; -import appeng.util.inv.filter.AEItemFilters; -import appeng.util.inv.filter.IAEItemFilter; -import com.glodblock.github.extendedae.common.tileentities.matrix.TileAssemblerMatrixPattern; -import com.sorrowmist.useless.config.ConfigManager; -import net.minecraft.core.BlockPos; -import net.minecraft.world.level.block.state.BlockState; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Mixin(TileAssemblerMatrixPattern.class) -public class TileAssemblerMatrixPatternMixin { - - private AppEngInternalInventory modifiedPatternInventory; - - /** - * 在构造函数完成后替换patternInventory - */ - @Inject( - method = "", - at = @At("RETURN") - ) - private void onConstructed(BlockPos pos, BlockState blockState, CallbackInfo ci) { - TileAssemblerMatrixPattern self = (TileAssemblerMatrixPattern) (Object) this; - - // 创建新的库存实例 - int newSize = 36 * ConfigManager.getMatrixPatternCount(); - this.modifiedPatternInventory = new AppEngInternalInventory(self, newSize, 1); - - // 使用反射替换原字段 - try { - java.lang.reflect.Field field = TileAssemblerMatrixPattern.class.getDeclaredField("patternInventory"); - field.setAccessible(true); - field.set(self, this.modifiedPatternInventory); - - // 也需要重新设置过滤器 - java.lang.reflect.Method setFilterMethod = AppEngInternalInventory.class.getDeclaredMethod("setFilter", IAEItemFilter.class); - setFilterMethod.setAccessible(true); - - // 重新创建过滤器 - java.lang.reflect.Field filterField = TileAssemblerMatrixPattern.class.getDeclaredField("exposedInventory"); - filterField.setAccessible(true); - - // 重新创建exposedInventory - FilteredInternalInventory newExposedInventory = new FilteredInternalInventory( - this.modifiedPatternInventory, - AEItemFilters.INSERT_ONLY - ); - filterField.set(self, newExposedInventory); - - } catch (Exception e) { - e.printStackTrace(); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/mixin/TileEntityDigitalMinerMixin.java b/src/main/java/com/sorrowmist/useless/mixin/TileEntityDigitalMinerMixin.java deleted file mode 100644 index 5ccf663b..00000000 --- a/src/main/java/com/sorrowmist/useless/mixin/TileEntityDigitalMinerMixin.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.sorrowmist.useless.mixin; - -import mekanism.common.tile.machine.TileEntityDigitalMiner; -import org.objectweb.asm.Opcodes; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -import java.lang.reflect.Field; - -@Mixin(value = mekanism.common.tile.machine.TileEntityDigitalMiner.class, remap = false) -public class TileEntityDigitalMinerMixin { - - @Inject( - method = "onUpdateServer", - at = @At( - value = "FIELD", - target = "Lmekanism/common/tile/machine/TileEntityDigitalMiner;delayTicks:I", - opcode = Opcodes.PUTFIELD, - ordinal = 0, - shift = At.Shift.AFTER - ) - ) - private void modifyDelayTicks(CallbackInfo ci) { - // 获取被混入的实例并修改 delayTicks 为 1 - TileEntityDigitalMiner miner = (TileEntityDigitalMiner) (Object) this; - - try { - Field delayTicksField = TileEntityDigitalMiner.class.getDeclaredField("delayTicks"); - delayTicksField.setAccessible(true); - delayTicksField.set(miner, 0); - } catch (Exception e) { - e.printStackTrace(); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/mixin/WrenchHookMixin.java b/src/main/java/com/sorrowmist/useless/mixin/WrenchHookMixin.java deleted file mode 100644 index b275e7cf..00000000 --- a/src/main/java/com/sorrowmist/useless/mixin/WrenchHookMixin.java +++ /dev/null @@ -1,154 +0,0 @@ -package com.sorrowmist.useless.mixin; - -import com.glodblock.github.extendedae.common.tileentities.TileExPatternProvider; -import com.sorrowmist.useless.items.EndlessBeafItem; -import net.minecraft.core.BlockPos; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.level.Level; -import net.minecraft.world.level.block.entity.BlockEntity; -import net.minecraft.world.phys.BlockHitResult; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; - -@Mixin(appeng.hooks.WrenchHook.class) -public class WrenchHookMixin { - - @Inject(method = "onPlayerUseBlock", at = @At(value = "INVOKE", target = "Lappeng/blockentity/AEBaseBlockEntity;disassembleWithWrench(Lnet/minecraft/world/entity/player/Player;Lnet/minecraft/world/level/Level;Lnet/minecraft/world/phys/BlockHitResult;Lnet/minecraft/world/item/ItemStack;)Lnet/minecraft/world/InteractionResult;", shift = At.Shift.BEFORE), cancellable = false, remap = false) - private static void onDisassembleWithWrench(Player player, Level level, net.minecraft.world.InteractionHand hand, BlockHitResult hitResult, CallbackInfoReturnable cir) { - if (level.isClientSide()) { - return; - } - - BlockPos pos = hitResult.getBlockPos(); - BlockEntity blockEntity = level.getBlockEntity(pos); - - // 处理直接放置的样板供应器(方块形式) - String blockEntityClassName = blockEntity.getClass().getName(); - if (blockEntityClassName.equals("com.glodblock.github.extendedae.common.tileentities.TileExPatternProvider") || - blockEntity instanceof appeng.blockentity.crafting.PatternProviderBlockEntity || - blockEntityClassName.equals("net.pedroksl.advanced_ae.common.entities.AdvPatternProviderEntity") || - blockEntityClassName.equals("net.pedroksl.advanced_ae.common.entities.SmallAdvPatternProviderEntity")) { - // 方块形式,不需要匹配方向,检查所有方向的主从关系 - handleBlockFormDisassembly(level, pos); - } else if (blockEntity instanceof appeng.api.parts.IPartHost partHost) { - // 处理面板形式的样板供应器 - // 计算玩家点击的精确位置(相对于方块的局部坐标) - net.minecraft.world.phys.Vec3 hitLocation = hitResult.getLocation().subtract(pos.getX(), pos.getY(), pos.getZ()); - - // 遍历所有方向,找到与点击位置最匹配的样板供应器 - appeng.api.parts.IPart targetPart = null; - net.minecraft.core.Direction targetDir = null; - double bestMatchScore = Double.MAX_VALUE; - - for (net.minecraft.core.Direction dir : net.minecraft.core.Direction.values()) { - appeng.api.parts.IPart part = partHost.getPart(dir); - if (part != null) { - String partName = part.getClass().getName(); - // 检查是否是任何类型的样板供应器 - if (partName.contains("PatternProvider")) { - // 计算该方向的样板供应器与点击位置的匹配程度 - double matchScore = calculateMatchScore(dir, hitLocation); - if (matchScore < bestMatchScore) { - targetPart = part; - targetDir = dir; - bestMatchScore = matchScore; - } - } - } - } - - // 如果找到匹配的样板供应器,处理它 - if (targetPart != null && targetDir != null) { - handlePanelFormDisassembly(level, pos, targetDir); - } - } - } - - // 计算方向与点击位置的匹配程度,分数越低表示匹配度越高 - private static double calculateMatchScore(net.minecraft.core.Direction dir, net.minecraft.world.phys.Vec3 hitLocation) { - // 计算点击位置在该方向上的投影 - double dx = hitLocation.x - 0.5; - double dy = hitLocation.y - 0.5; - double dz = hitLocation.z - 0.5; - - // 计算该方向的法向量 - int nx = dir.getStepX(); - int ny = dir.getStepY(); - int nz = dir.getStepZ(); - - // 对于每个方向,计算点击位置到该方向面板的距离 - // 面板的位置在方块的表面,距离方块中心0.5个单位 - double distanceToPanel = Math.abs(dx * nx + dy * ny + dz * nz) - 0.5; - - // 计算点击位置在面板平面内的偏移 - double inPlaneOffsetX = dx - nx * (0.5 + distanceToPanel * nx); - double inPlaneOffsetY = dy - ny * (0.5 + distanceToPanel * ny); - double inPlaneOffsetZ = dz - nz * (0.5 + distanceToPanel * nz); - - // 计算平面内的偏移距离 - double inPlaneDistance = Math.sqrt(inPlaneOffsetX * inPlaneOffsetX + inPlaneOffsetY * inPlaneOffsetY + inPlaneOffsetZ * inPlaneOffsetZ); - - // 总匹配分数 = 到面板的距离 + 平面内的偏移距离 - return Math.abs(distanceToPanel) + inPlaneDistance; - } - - // 处理方块形式的扩展样板供应器拆除逻辑(不需要匹配方向) - private static void handleBlockFormDisassembly(Level level, BlockPos pos) { - boolean handled = false; - - // 遍历所有可能的方向,检查是否是主端 - for (net.minecraft.core.Direction dir : net.minecraft.core.Direction.values()) { - com.sorrowmist.useless.utils.pattern.PatternProviderKey keyWithDir = new com.sorrowmist.useless.utils.pattern.PatternProviderKey(pos, dir); - if (EndlessBeafItem.masterToSlaves.containsKey(keyWithDir)) { - // 处理主端拆除,清空所有从端的样板并取消关系 - EndlessBeafItem.handleMasterBreak(level, keyWithDir); - handled = true; - break; - } - } - - // 如果不是主端,检查是否是从端(方块形式可能有多个方向的从端) - if (!handled) { - // 遍历所有从端,检查是否有匹配的位置 - for (com.sorrowmist.useless.utils.pattern.PatternProviderKey slaveKey : EndlessBeafItem.slaveToMaster.keySet()) { - if (slaveKey.getPos().equals(pos)) { - // 处理从端拆除,取消关系并清空样板 - EndlessBeafItem.handleSlaveBreak(level, slaveKey); - EndlessBeafItem.clearSlavePatterns(level, slaveKey); - handled = true; - break; - } - } - } - } - - // 处理面板形式的扩展样板供应器拆除逻辑(严格匹配方向) - private static void handlePanelFormDisassembly(Level level, BlockPos pos, net.minecraft.core.Direction direction) { - boolean handled = false; - - // 检查是否是主端(严格匹配方向) - com.sorrowmist.useless.utils.pattern.PatternProviderKey masterKey = new com.sorrowmist.useless.utils.pattern.PatternProviderKey(pos, direction); - if (EndlessBeafItem.masterToSlaves.containsKey(masterKey)) { - // 处理主端拆除,清空所有从端的样板并取消关系 - EndlessBeafItem.handleMasterBreak(level, masterKey); - handled = true; - } - - // 如果不是主端,检查是否是从端(严格匹配方向) - if (!handled) { - // 遍历所有从端,检查是否有匹配的位置和方向 - for (com.sorrowmist.useless.utils.pattern.PatternProviderKey slaveKey : EndlessBeafItem.slaveToMaster.keySet()) { - // 严格匹配位置和方向 - if (slaveKey.getPos().equals(pos) && slaveKey.getDirection().equals(direction)) { - // 处理从端拆除,取消关系并清空样板 - EndlessBeafItem.handleSlaveBreak(level, slaveKey); - EndlessBeafItem.clearSlavePatterns(level, slaveKey); - handled = true; - break; - } - } - } - } -} diff --git a/src/main/java/com/sorrowmist/useless/mixin/accessor/BeeAccessor.java b/src/main/java/com/sorrowmist/useless/mixin/accessor/BeeAccessor.java new file mode 100644 index 00000000..cfa59404 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/accessor/BeeAccessor.java @@ -0,0 +1,20 @@ +package com.sorrowmist.useless.mixin.accessor; + +import com.sorrowmist.useless.api.entity.BeeTimerAccess; +import net.minecraft.world.entity.animal.Bee; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +/** + * 为 {@link BeeTimerAccess} 生成访问器,暴露 {@code Bee.setHasNectar(boolean)}(包级私有)。 + * + *

蜜蜂采蜜归巢的「是否携带花粉」只有公开的 {@code hasNectar()} 读取,写入是包级私有的, + * 造化杖的「生物加速」需要它来加速采蜜。

+ */ +@Mixin(Bee.class) +public interface BeeAccessor extends BeeTimerAccess { + + @Override + @Invoker("setHasNectar") + void uselessMod$setBeeHasNectar(boolean value); +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/accessor/BlockEntityBotanyPotAccessor.java b/src/main/java/com/sorrowmist/useless/mixin/accessor/BlockEntityBotanyPotAccessor.java deleted file mode 100644 index c0d92f22..00000000 --- a/src/main/java/com/sorrowmist/useless/mixin/accessor/BlockEntityBotanyPotAccessor.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.sorrowmist.useless.mixin.accessor; - -import net.darkhax.botanypots.block.BlockEntityBotanyPot; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.gen.Accessor; - -import java.util.Random; - -@Mixin(value = BlockEntityBotanyPot.class ,remap = false) -public interface BlockEntityBotanyPotAccessor { - - // 访问私有字段 rng - @Accessor("rng") - Random getRng(); - - // 访问 protected 字段 doneGrowing - @Accessor("doneGrowing") - boolean isDoneGrowing(); - - @Accessor("doneGrowing") - void setDoneGrowing(boolean value); - -} diff --git a/src/main/java/com/sorrowmist/useless/mixin/accessor/DelayAccessor.java b/src/main/java/com/sorrowmist/useless/mixin/accessor/DelayAccessor.java deleted file mode 100644 index 38005137..00000000 --- a/src/main/java/com/sorrowmist/useless/mixin/accessor/DelayAccessor.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.sorrowmist.useless.mixin.accessor; - -import mekanism.common.tile.machine.TileEntityDigitalMiner; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.gen.Accessor; - -@Mixin(value = TileEntityDigitalMiner.class, remap = false) -public interface DelayAccessor { - @Accessor("delay") - void setDelay(int delay); -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/mixin/accessor/TurtleAccessor.java b/src/main/java/com/sorrowmist/useless/mixin/accessor/TurtleAccessor.java new file mode 100644 index 00000000..c40818e1 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/accessor/TurtleAccessor.java @@ -0,0 +1,24 @@ +package com.sorrowmist.useless.mixin.accessor; + +import com.sorrowmist.useless.api.entity.TurtleTimerAccess; +import net.minecraft.world.entity.animal.Turtle; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +/** + * 为 {@link TurtleTimerAccess} 生成访问器,暴露 {@code Turtle.layEggCounter}(包级私有)。 + * + *

海龟下蛋的倒计时在 {@code TurtleLayEggGoal#tick()} 里自增,超过 + * {@code adjustedTickDelay(200)} 才真正产卵。造化杖的「生物加速」需要直接推进它。

+ */ +@Mixin(Turtle.class) +public interface TurtleAccessor extends TurtleTimerAccess { + + @Override + @Accessor("layEggCounter") + int uselessMod$getLayEggCounter(); + + @Override + @Accessor("layEggCounter") + void uselessMod$setLayEggCounter(int value); +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/advancedae/AdvancedCraftingCpuAccessor.java b/src/main/java/com/sorrowmist/useless/mixin/advancedae/AdvancedCraftingCpuAccessor.java new file mode 100644 index 00000000..7e304dbc --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/advancedae/AdvancedCraftingCpuAccessor.java @@ -0,0 +1,16 @@ +package com.sorrowmist.useless.mixin.advancedae; + +import appeng.api.stacks.GenericStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Pseudo +@Mixin(targets = "net.pedroksl.advanced_ae.common.cluster.AdvCraftingCPU", remap = false) +public interface AdvancedCraftingCpuAccessor { + @Invoker("markDirty") + void uselessMod$markDirty(); + + @Invoker("updateOutput") + void uselessMod$updateOutput(GenericStack stack); +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/advancedae/CraftingCpuLogicMixin.java b/src/main/java/com/sorrowmist/useless/mixin/advancedae/CraftingCpuLogicMixin.java new file mode 100644 index 00000000..8a3c4591 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/advancedae/CraftingCpuLogicMixin.java @@ -0,0 +1,317 @@ +package com.sorrowmist.useless.mixin.advancedae; + +import appeng.api.config.Actionable; +import appeng.api.crafting.IPatternDetails; +import appeng.api.networking.crafting.ICraftingProvider; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.AEKeyType; +import appeng.api.stacks.GenericStack; +import appeng.api.stacks.KeyCounter; +import appeng.crafting.CraftingLink; +import appeng.crafting.execution.ExecutingCraftingJob; +import appeng.crafting.inv.ListCraftingInventory; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.sorrowmist.useless.compat.ae.DynamicReflectionSupport; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.DynamicPatternExecution; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.DynamicPatternCpuStateManager; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.DynamicPatternInsertContext; +import net.minecraft.nbt.CompoundTag; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +@Pseudo +@Mixin(targets = "net.pedroksl.advanced_ae.common.logic.AdvCraftingCPULogic", remap = false) +public abstract class CraftingCpuLogicMixin { + @Unique + private static final @Nullable Class LOGIC_CLASS = + DynamicReflectionSupport.findClassSafe("net.pedroksl.advanced_ae.common.logic.AdvCraftingCPULogic"); + @Unique + private static final @Nullable Class JOB_CLASS = + DynamicReflectionSupport.findClassSafe("net.pedroksl.advanced_ae.common.logic.ExecutingCraftingJob"); + @Unique + private static final @Nullable Class TRACKER_CLASS = + DynamicReflectionSupport.findClassSafe("net.pedroksl.advanced_ae.common.logic.ElapsedTimeTracker"); + @Unique + private static final @Nullable Field JOB_FIELD = DynamicReflectionSupport.findFieldSafe(LOGIC_CLASS, "job"); + @Unique + private static final @Nullable Field INVENTORY_FIELD = DynamicReflectionSupport.findFieldSafe(LOGIC_CLASS, "inventory"); + @Unique + private static final @Nullable Field CPU_FIELD = DynamicReflectionSupport.findFieldSafe(LOGIC_CLASS, "cpu"); + @Unique + private static final @Nullable Method FINISH_METHOD = + DynamicReflectionSupport.findMethodSafe(LOGIC_CLASS, "finishJob", boolean.class); + @Unique + private static final @Nullable Method POST_CHANGE_METHOD = + DynamicReflectionSupport.findMethodSafe(LOGIC_CLASS, "postChange", AEKey.class); + @Unique + private static final @Nullable Field JOB_WAITING_FIELD = DynamicReflectionSupport.findFieldSafe(JOB_CLASS, "waitingFor"); + @Unique + private static final @Nullable Field JOB_TRACKER_FIELD = DynamicReflectionSupport.findFieldSafe(JOB_CLASS, "timeTracker"); + @Unique + private static final @Nullable Field JOB_FINAL_OUTPUT_FIELD = DynamicReflectionSupport.findFieldSafe(JOB_CLASS, "finalOutput"); + @Unique + private static final @Nullable Field JOB_REMAINING_FIELD = DynamicReflectionSupport.findFieldSafe(JOB_CLASS, "remainingAmount"); + @Unique + private static final @Nullable Field JOB_LINK_FIELD = DynamicReflectionSupport.findFieldSafe(JOB_CLASS, "link"); + @Unique + private static final @Nullable Method DECREMENT_METHOD = + DynamicReflectionSupport.findMethodSafe(TRACKER_CLASS, "decrementItems", long.class, AEKeyType.class); + @Unique + private static final boolean AVAILABLE = LOGIC_CLASS != null && JOB_CLASS != null && TRACKER_CLASS != null + && JOB_FIELD != null && INVENTORY_FIELD != null && CPU_FIELD != null + && FINISH_METHOD != null && POST_CHANGE_METHOD != null && JOB_WAITING_FIELD != null + && JOB_TRACKER_FIELD != null && JOB_FINAL_OUTPUT_FIELD != null && JOB_REMAINING_FIELD != null + && JOB_LINK_FIELD != null && DECREMENT_METHOD != null; + + @Unique + @Nullable + private DynamicPatternInsertContext uselessMod$insertContext; + + @Inject(method = "insert", at = @At("HEAD")) + private void uselessMod$beginInsert(AEKey key, long amount, Actionable actionable, + CallbackInfoReturnable callback) { + if (AVAILABLE) { + uselessMod$insertContext = new DynamicPatternInsertContext(key, amount, actionable); + } + } + + @WrapOperation( + method = "insert", + at = @At(value = "INVOKE", + target = "Lappeng/crafting/inv/ListCraftingInventory;extract(Lappeng/api/stacks/AEKey;JLappeng/api/config/Actionable;)J", + ordinal = 0), + remap = false) + private long uselessMod$captureStrictMatch( + ListCraftingInventory waitingFor, AEKey key, long amount, Actionable mode, + Operation original) { + long strict = original.call(waitingFor, key, amount, mode); + if (AVAILABLE && mode == Actionable.SIMULATE && uselessMod$insertContext != null) { + uselessMod$insertContext.setStrictMatched(strict); + } + return strict; + } + + @Inject(method = "insert", at = @At("RETURN"), cancellable = true) + private void uselessMod$claimDynamicOutput(AEKey key, long amount, Actionable actionable, + CallbackInfoReturnable callback) { + if (!AVAILABLE) { + return; + } + DynamicPatternInsertContext context = uselessMod$insertContext; + uselessMod$insertContext = null; + if (context == null) { + return; + } + long remainder = Math.max(0L, context.requestedAmount() - context.strictMatched()); + if (remainder <= 0 || !DynamicPatternCpuStateManager.INSTANCE.hasAnyPending(this)) { + return; + } + DynamicPatternCpuStateManager.ClaimResult claims = + DynamicPatternCpuStateManager.INSTANCE.claim(this, key, remainder, actionable); + if (!claims.claimedAnything()) { + return; + } + if (actionable == Actionable.MODULATE) { + uselessMod$deductWaitingFor(claims); + long supplemental = uselessMod$applyInventoryClaims(key, claims) + + uselessMod$applyRequesterClaims(key, claims); + Object cpu = uselessMod$getCpu(); + if (cpu != null) { + ((AdvancedCraftingCpuAccessor) cpu).uselessMod$markDirty(); + } + callback.setReturnValue(callback.getReturnValue() + supplemental); + } else { + callback.setReturnValue(callback.getReturnValue() + claims.claimedAmount()); + } + } + + @WrapOperation( + method = "executeCrafting", + at = @At(value = "INVOKE", + target = "Lappeng/api/networking/crafting/ICraftingProvider;pushPattern(Lappeng/api/crafting/IPatternDetails;[Lappeng/api/stacks/KeyCounter;)Z"), + remap = false) + private boolean uselessMod$registerDynamicOutputs( + ICraftingProvider provider, IPatternDetails details, KeyCounter[] inputHolder, + Operation original) { + DynamicPatternExecution.Resolved resolved = AVAILABLE + ? DynamicPatternExecution.resolve(details) + : null; + if (resolved == null) { + return original.call(provider, details, inputHolder); + } + var dynamic = resolved.pattern(); + if (DynamicPatternCpuStateManager.INSTANCE.hasAmbiguousOutputRegistration(this, dynamic)) { + return false; + } + boolean pushed = original.call(provider, details, inputHolder); + Object job = uselessMod$getJob(); + if (pushed && job != null) { + CraftingLink link = uselessMod$getJobLink(job); + GenericStack finalOutput = uselessMod$getJobFinalOutput(job); + if (link != null) { + DynamicPatternCpuStateManager.INSTANCE.registerExpectedOutputs( + this, link.getCraftingID(), dynamic, + finalOutput == null ? null : finalOutput.what(), resolved.copies()); + } + } + return pushed; + } + + @Inject(method = "writeToNBT(Lnet/minecraft/nbt/CompoundTag;)V", at = @At("RETURN")) + private void uselessMod$writeDynamicState( + CompoundTag data, CallbackInfo callback) { + if (!AVAILABLE) { + return; + } + CompoundTag state = DynamicPatternCpuStateManager.INSTANCE.writeToTag(this); + if (state == null) { + data.remove(DynamicPatternCpuStateManager.NBT_KEY); + } else { + data.put(DynamicPatternCpuStateManager.NBT_KEY, state); + } + } + + @Inject(method = "readFromNBT(Lnet/minecraft/nbt/CompoundTag;)V", at = @At("RETURN")) + private void uselessMod$readDynamicState( + CompoundTag data, CallbackInfo callback) { + if (!AVAILABLE) { + return; + } + DynamicPatternCpuStateManager.INSTANCE.clear(this); + Object job = uselessMod$getJob(); + CraftingLink link = job == null ? null : uselessMod$getJobLink(job); + if (link != null && data.contains(DynamicPatternCpuStateManager.NBT_KEY, CompoundTag.TAG_COMPOUND)) { + DynamicPatternCpuStateManager.INSTANCE.readFromTag( + this, link.getCraftingID(), data.getCompound(DynamicPatternCpuStateManager.NBT_KEY)); + } + } + + @Inject(method = "finishJob", at = @At("HEAD")) + private void uselessMod$clearDynamicState(boolean success, CallbackInfo callback) { + if (AVAILABLE) { + DynamicPatternCpuStateManager.INSTANCE.clear(this); + } + } + + @Unique + @Nullable + private Object uselessMod$getJob() { + return DynamicReflectionSupport.get(JOB_FIELD, this); + } + + @Unique + @Nullable + private ListCraftingInventory uselessMod$getInventory() { + Object value = DynamicReflectionSupport.get(INVENTORY_FIELD, this); + return value instanceof ListCraftingInventory inventory ? inventory : null; + } + + @Unique + @Nullable + private Object uselessMod$getCpu() { + return DynamicReflectionSupport.get(CPU_FIELD, this); + } + + @Unique + @Nullable + private ListCraftingInventory uselessMod$getJobWaitingFor(Object job) { + Object value = DynamicReflectionSupport.get(JOB_WAITING_FIELD, job); + return value instanceof ListCraftingInventory inventory ? inventory : null; + } + + @Unique + @Nullable + private GenericStack uselessMod$getJobFinalOutput(Object job) { + Object value = DynamicReflectionSupport.get(JOB_FINAL_OUTPUT_FIELD, job); + return value instanceof GenericStack stack ? stack : null; + } + + @Unique + private long uselessMod$getJobRemaining(Object job) { + return DynamicReflectionSupport.getLong(JOB_REMAINING_FIELD, job, 0L); + } + + @Unique + private void uselessMod$setJobRemaining(Object job, long amount) { + DynamicReflectionSupport.setLong(JOB_REMAINING_FIELD, job, amount, "set Advanced AE remaining amount"); + } + + @Unique + @Nullable + private CraftingLink uselessMod$getJobLink(Object job) { + Object value = DynamicReflectionSupport.get(JOB_LINK_FIELD, job); + return value instanceof CraftingLink link ? link : null; + } + + @Unique + private void uselessMod$deductWaitingFor(DynamicPatternCpuStateManager.ClaimResult claims) { + Object job = uselessMod$getJob(); + ListCraftingInventory waitingFor = job == null ? null : uselessMod$getJobWaitingFor(job); + if (waitingFor == null) { + return; + } + for (DynamicPatternCpuStateManager.Claim claim : claims.claims()) { + waitingFor.extract(claim.exactExpectedKey(), claim.claimedAmount(), Actionable.MODULATE); + } + } + + @Unique + private void uselessMod$decrementItems(Object job, long amount, AEKeyType type) { + Object tracker = DynamicReflectionSupport.get(JOB_TRACKER_FIELD, job); + DynamicReflectionSupport.invoke(DECREMENT_METHOD, tracker, "decrement Advanced AE items", amount, type); + } + + @Unique + private long uselessMod$applyInventoryClaims( + AEKey incoming, DynamicPatternCpuStateManager.ClaimResult claims) { + long claimed = claims.claimedForInventory(); + Object job = uselessMod$getJob(); + ListCraftingInventory inventory = uselessMod$getInventory(); + if (claimed <= 0 || job == null || inventory == null) { + return 0; + } + uselessMod$decrementItems(job, claimed, incoming.getType()); + inventory.insert(incoming, claimed, Actionable.MODULATE); + return claimed; + } + + @Unique + private long uselessMod$applyRequesterClaims( + AEKey incoming, DynamicPatternCpuStateManager.ClaimResult claims) { + long claimed = claims.claimedForRequester(); + Object job = uselessMod$getJob(); + if (claimed <= 0 || job == null) { + return 0; + } + uselessMod$decrementItems(job, claimed, incoming.getType()); + CraftingLink link = uselessMod$getJobLink(job); + long inserted = link == null ? 0 : link.insert(incoming, claimed, Actionable.MODULATE); + DynamicReflectionSupport.invoke(POST_CHANGE_METHOD, this, "post Advanced AE dynamic output", incoming); + long remaining = Math.max(0L, uselessMod$getJobRemaining(job) - claimed); + uselessMod$setJobRemaining(job, remaining); + Object cpu = uselessMod$getCpu(); + if (remaining <= 0) { + DynamicReflectionSupport.invoke(FINISH_METHOD, this, "finish Advanced AE dynamic job", true); + if (cpu != null) { + ((AdvancedCraftingCpuAccessor) cpu).uselessMod$updateOutput(null); + } + } else if (cpu != null) { + GenericStack finalOutput = uselessMod$getJobFinalOutput(job); + if (finalOutput != null) { + ((AdvancedCraftingCpuAccessor) cpu).uselessMod$updateOutput( + new GenericStack(finalOutput.what(), remaining)); + } + } + return inserted; + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/advancedae/ExecutingCraftingJobMixin.java b/src/main/java/com/sorrowmist/useless/mixin/advancedae/ExecutingCraftingJobMixin.java new file mode 100644 index 00000000..f6bad553 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/advancedae/ExecutingCraftingJobMixin.java @@ -0,0 +1,27 @@ +package com.sorrowmist.useless.mixin.advancedae; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.crafting.PatternDetailsHelper; +import appeng.api.stacks.AEItemKey; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.AdvancedAlloyFurnacePatternResolver; +import net.minecraft.world.level.Level; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Pseudo +@Mixin(targets = "net.pedroksl.advanced_ae.common.logic.ExecutingCraftingJob", remap = false) +public abstract class ExecutingCraftingJobMixin { + @Redirect( + method = "(Lnet/minecraft/nbt/CompoundTag;Lnet/pedroksl/advanced_ae/common/logic/ExecutingCraftingJob$CraftingDifferenceListener;Lnet/pedroksl/advanced_ae/common/logic/AdvCraftingCPULogic;)V", + at = @At( + value = "INVOKE", + target = "Lappeng/api/crafting/PatternDetailsHelper;decodePattern(Lappeng/api/stacks/AEItemKey;Lnet/minecraft/world/level/Level;)Lappeng/api/crafting/IPatternDetails;", + remap = false)) + private IPatternDetails uselessMod$resolveRestoredPattern( + AEItemKey definition, Level level) { + IPatternDetails decoded = PatternDetailsHelper.decodePattern(definition, level); + return decoded == null ? null : AdvancedAlloyFurnacePatternResolver.resolve(decoded, level); + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/ae2/AEProcessingPatternMixin.java b/src/main/java/com/sorrowmist/useless/mixin/ae2/AEProcessingPatternMixin.java new file mode 100644 index 00000000..b0761bf8 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/ae2/AEProcessingPatternMixin.java @@ -0,0 +1,27 @@ +package com.sorrowmist.useless.mixin.ae2; + +import appeng.crafting.pattern.AEProcessingPattern; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.ProcessingPatternRecipeHolder; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeIdentity; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +/** Reserves a runtime recipe binding for a future processing-pattern converter. */ +@Mixin(value = AEProcessingPattern.class, remap = false) +public abstract class AEProcessingPatternMixin implements ProcessingPatternRecipeHolder { + @Unique + @Nullable + private AlloyFurnaceRecipeIdentity uselessMod$recipeIdentity; + + @Override + @Nullable + public AlloyFurnaceRecipeIdentity uselessMod$getRecipeIdentity() { + return uselessMod$recipeIdentity; + } + + @Override + public void uselessMod$setRecipeIdentity(@Nullable AlloyFurnaceRecipeIdentity identity) { + this.uselessMod$recipeIdentity = identity; + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/ae2/CraftingCalculationSmartDoublingMixin.java b/src/main/java/com/sorrowmist/useless/mixin/ae2/CraftingCalculationSmartDoublingMixin.java new file mode 100644 index 00000000..1e6874e1 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/ae2/CraftingCalculationSmartDoublingMixin.java @@ -0,0 +1,32 @@ +package com.sorrowmist.useless.mixin.ae2; + +import appeng.api.networking.IGrid; +import appeng.api.networking.crafting.CalculationStrategy; +import appeng.api.networking.crafting.ICraftingSimulationRequester; +import appeng.api.stacks.GenericStack; +import appeng.crafting.CraftingCalculation; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.SmartDoublingCalculationContext; +import net.minecraft.world.level.Level; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(value = CraftingCalculation.class, remap = false) +public abstract class CraftingCalculationSmartDoublingMixin implements SmartDoublingCalculationContext { + @Unique + private IGrid uselessMod$craftingGrid; + + @Inject(method = "", at = @At("RETURN")) + private void uselessMod$retainCraftingGrid( + Level level, IGrid grid, ICraftingSimulationRequester requester, + GenericStack output, CalculationStrategy strategy, CallbackInfo callback) { + uselessMod$craftingGrid = grid; + } + + @Override + public IGrid uselessMod$getCraftingGrid() { + return uselessMod$craftingGrid; + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/ae2/CraftingCpuLogicAccessor.java b/src/main/java/com/sorrowmist/useless/mixin/ae2/CraftingCpuLogicAccessor.java new file mode 100644 index 00000000..0c422daa --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/ae2/CraftingCpuLogicAccessor.java @@ -0,0 +1,21 @@ +package com.sorrowmist.useless.mixin.ae2; + +import appeng.api.stacks.AEKey; +import appeng.crafting.execution.ExecutingCraftingJob; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(targets = "appeng.crafting.execution.CraftingCpuLogic", remap = false) +public interface CraftingCpuLogicAccessor { + @Accessor("job") + @Nullable + ExecutingCraftingJob uselessMod$getJob(); + + @Invoker("finishJob") + void uselessMod$finishJob(boolean success); + + @Invoker("postChange") + void uselessMod$postChange(AEKey key); +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/ae2/CraftingCpuLogicMixin.java b/src/main/java/com/sorrowmist/useless/mixin/ae2/CraftingCpuLogicMixin.java new file mode 100644 index 00000000..77c16d57 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/ae2/CraftingCpuLogicMixin.java @@ -0,0 +1,218 @@ +package com.sorrowmist.useless.mixin.ae2; + +import appeng.api.config.Actionable; +import appeng.api.crafting.IPatternDetails; +import appeng.api.networking.crafting.ICraftingProvider; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import appeng.api.stacks.KeyCounter; +import appeng.crafting.CraftingLink; +import appeng.crafting.execution.CraftingCpuLogic; +import appeng.crafting.execution.ExecutingCraftingJob; +import appeng.crafting.inv.ListCraftingInventory; +import appeng.me.cluster.implementations.CraftingCPUCluster; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.DynamicPatternExecution; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.DynamicPatternCpuStateManager; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.DynamicPatternInsertContext; +import net.minecraft.nbt.CompoundTag; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + + +@Mixin(targets = "appeng.crafting.execution.CraftingCpuLogic", remap = false) +public abstract class CraftingCpuLogicMixin { + @Shadow(remap = false) + CraftingCPUCluster cluster; + + @Unique + @Nullable + private DynamicPatternInsertContext uselessMod$insertContext; + + @Inject(method = "insert", at = @At("HEAD")) + private void uselessMod$beginInsert(AEKey key, long amount, Actionable actionable, + CallbackInfoReturnable callback) { + uselessMod$insertContext = new DynamicPatternInsertContext(key, amount, actionable); + } + + @WrapOperation( + method = "insert", + at = @At( + value = "INVOKE", + target = "Lappeng/crafting/inv/ListCraftingInventory;extract(Lappeng/api/stacks/AEKey;JLappeng/api/config/Actionable;)J", + ordinal = 0), + remap = false) + private long uselessMod$captureStrictMatch( + ListCraftingInventory waitingFor, AEKey key, long amount, Actionable mode, + Operation original) { + long strict = original.call(waitingFor, key, amount, mode); + if (mode == Actionable.SIMULATE && uselessMod$insertContext != null) { + uselessMod$insertContext.setStrictMatched(strict); + } + return strict; + } + + @Inject(method = "insert", at = @At("RETURN"), cancellable = true) + private void uselessMod$claimDynamicOutput(AEKey key, long amount, Actionable actionable, + CallbackInfoReturnable callback) { + DynamicPatternInsertContext context = uselessMod$insertContext; + uselessMod$insertContext = null; + if (context == null) { + return; + } + long remainder = Math.max(0L, context.requestedAmount() - context.strictMatched()); + if (remainder <= 0 || !DynamicPatternCpuStateManager.INSTANCE.hasAnyPending(this)) { + return; + } + + DynamicPatternCpuStateManager.ClaimResult claims = + DynamicPatternCpuStateManager.INSTANCE.claim(this, key, remainder, actionable); + if (!claims.claimedAnything()) { + return; + } + + if (actionable == Actionable.MODULATE) { + uselessMod$deductWaitingFor(claims); + long supplemental = uselessMod$applyInventoryClaims(key, claims) + + uselessMod$applyRequesterClaims(key, claims); + cluster.markDirty(); + callback.setReturnValue(callback.getReturnValue() + supplemental); + } else { + callback.setReturnValue(callback.getReturnValue() + claims.claimedAmount()); + } + } + + @WrapOperation( + method = "executeCrafting", + at = @At( + value = "INVOKE", + target = "Lappeng/api/networking/crafting/ICraftingProvider;pushPattern(Lappeng/api/crafting/IPatternDetails;[Lappeng/api/stacks/KeyCounter;)Z"), + remap = false) + private boolean uselessMod$registerDynamicOutputs( + ICraftingProvider provider, IPatternDetails details, KeyCounter[] inputHolder, + Operation original) { + DynamicPatternExecution.Resolved resolved = DynamicPatternExecution.resolve(details); + if (resolved == null) { + return original.call(provider, details, inputHolder); + } + var dynamic = resolved.pattern(); + if (DynamicPatternCpuStateManager.INSTANCE.hasAmbiguousOutputRegistration(this, dynamic)) { + return false; + } + boolean pushed = original.call(provider, details, inputHolder); + if (pushed) { + ExecutingCraftingJob job = ((CraftingCpuLogicAccessor) (Object) this).uselessMod$getJob(); + if (job != null) { + CraftingLink link = ((ExecutingCraftingJobAccessor) job).uselessMod$getLink(); + GenericStack finalOutput = ((ExecutingCraftingJobAccessor) job).uselessMod$getFinalOutput(); + if (link != null) { + DynamicPatternCpuStateManager.INSTANCE.registerExpectedOutputs( + this, + link.getCraftingID(), + dynamic, + finalOutput == null ? null : finalOutput.what(), + resolved.copies()); + } + } + } + return pushed; + } + + @Inject(method = "writeToNBT(Lnet/minecraft/nbt/CompoundTag;)V", at = @At("RETURN")) + private void uselessMod$writeDynamicState( + CompoundTag data, CallbackInfo callback) { + CompoundTag state = DynamicPatternCpuStateManager.INSTANCE.writeToTag(this); + if (state == null) { + data.remove(DynamicPatternCpuStateManager.NBT_KEY); + } else { + data.put(DynamicPatternCpuStateManager.NBT_KEY, state); + } + } + + @Inject(method = "readFromNBT(Lnet/minecraft/nbt/CompoundTag;)V", at = @At("RETURN")) + private void uselessMod$readDynamicState( + CompoundTag data, CallbackInfo callback) { + DynamicPatternCpuStateManager.INSTANCE.clear(this); + ExecutingCraftingJob job = ((CraftingCpuLogicAccessor) (Object) this).uselessMod$getJob(); + if (job == null || !data.contains(DynamicPatternCpuStateManager.NBT_KEY, CompoundTag.TAG_COMPOUND)) { + return; + } + CraftingLink link = ((ExecutingCraftingJobAccessor) job).uselessMod$getLink(); + if (link != null) { + DynamicPatternCpuStateManager.INSTANCE.readFromTag( + this, link.getCraftingID(), data.getCompound(DynamicPatternCpuStateManager.NBT_KEY)); + } + } + + @Inject(method = "finishJob", at = @At("HEAD")) + private void uselessMod$clearDynamicState(boolean success, CallbackInfo callback) { + DynamicPatternCpuStateManager.INSTANCE.clear(this); + } + + @Unique + private void uselessMod$deductWaitingFor(DynamicPatternCpuStateManager.ClaimResult claims) { + ExecutingCraftingJob job = ((CraftingCpuLogicAccessor) (Object) this).uselessMod$getJob(); + if (job == null) { + return; + } + ListCraftingInventory waitingFor = ((ExecutingCraftingJobAccessor) job).uselessMod$getWaitingFor(); + for (DynamicPatternCpuStateManager.Claim claim : claims.claims()) { + waitingFor.extract(claim.exactExpectedKey(), claim.claimedAmount(), Actionable.MODULATE); + } + } + + @Unique + private long uselessMod$applyInventoryClaims( + AEKey incoming, DynamicPatternCpuStateManager.ClaimResult claims) { + long claimed = claims.claimedForInventory(); + if (claimed <= 0) { + return 0; + } + ExecutingCraftingJob job = ((CraftingCpuLogicAccessor) (Object) this).uselessMod$getJob(); + if (job == null) { + return 0; + } + ElapsedTimeTrackerAccessor tracker = (ElapsedTimeTrackerAccessor) (Object) + ((ExecutingCraftingJobAccessor) job).uselessMod$getTimeTracker(); + tracker.uselessMod$decrementItems(claimed, incoming.getType()); + ((CraftingCpuLogic) (Object) this).getInventory().insert(incoming, claimed, Actionable.MODULATE); + return claimed; + } + + @Unique + private long uselessMod$applyRequesterClaims( + AEKey incoming, DynamicPatternCpuStateManager.ClaimResult claims) { + long claimed = claims.claimedForRequester(); + if (claimed <= 0) { + return 0; + } + ExecutingCraftingJob job = ((CraftingCpuLogicAccessor) (Object) this).uselessMod$getJob(); + if (job == null) { + return 0; + } + ExecutingCraftingJobAccessor jobAccess = (ExecutingCraftingJobAccessor) job; + ((ElapsedTimeTrackerAccessor) (Object) jobAccess.uselessMod$getTimeTracker()) + .uselessMod$decrementItems(claimed, incoming.getType()); + CraftingLink link = jobAccess.uselessMod$getLink(); + long inserted = link == null ? 0 : link.insert(incoming, claimed, Actionable.MODULATE); + ((CraftingCpuLogicAccessor) (Object) this).uselessMod$postChange(incoming); + + long remaining = Math.max(0L, jobAccess.uselessMod$getRemainingAmount() - claimed); + jobAccess.uselessMod$setRemainingAmount(remaining); + if (remaining <= 0) { + ((CraftingCpuLogicAccessor) (Object) this).uselessMod$finishJob(true); + cluster.updateOutput(null); + } else if (jobAccess.uselessMod$getFinalOutput() != null) { + cluster.updateOutput(new GenericStack( + jobAccess.uselessMod$getFinalOutput().what(), remaining)); + } + return inserted; + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/ae2/CraftingServiceSmartDoublingMixin.java b/src/main/java/com/sorrowmist/useless/mixin/ae2/CraftingServiceSmartDoublingMixin.java new file mode 100644 index 00000000..3e03be8f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/ae2/CraftingServiceSmartDoublingMixin.java @@ -0,0 +1,40 @@ +package com.sorrowmist.useless.mixin.ae2; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.networking.crafting.ICraftingPlan; +import appeng.api.networking.crafting.ICraftingProvider; +import appeng.me.service.CraftingService; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.ScaledPattern; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.SmartDoublingPlanner; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.SmartDoublingPlans; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.SmartDoublingPatterns; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyVariable; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(value = CraftingService.class, priority = 1200, remap = false) +public abstract class CraftingServiceSmartDoublingMixin { + @ModifyVariable(method = "submitJob", at = @At("HEAD"), argsOnly = true, ordinal = 0) + private ICraftingPlan uselessMod$rewriteSubmittedPlan(ICraftingPlan plan) { + CraftingService service = (CraftingService) (Object) this; + // 判定「可复用输入」所需的关卡不在这里取:SmartDoublingPlanner 会从供应器方块实体 + // (本模组自己的机器)拿。⛔ 千万不要为了拿关卡往 CraftingService 上再挂 mixin —— + // 该类上已有 OmniSequence 的 OmniCraftingServiceMixin,多加一个会静默顶掉对方的注入, + // 导致 AppliedEnhancements 的 AELIS 精确规划器不参与、计划退化成 long 饱和值、任务卡在 0。 + return SmartDoublingPlans.rewriteForSubmission(plan, service::getProviders); + } + + @Inject(method = "getProviders", at = @At("HEAD"), cancellable = true) + private void uselessMod$getSmartDoublingProviders( + IPatternDetails pattern, CallbackInfoReturnable> callback) { + if (!(pattern instanceof ScaledPattern)) { + return; + } + + IPatternDetails original = SmartDoublingPatterns.unwrap(pattern); + Iterable providers = ((CraftingService) (Object) this).getProviders(original); + callback.setReturnValue(SmartDoublingPlanner.eligibleProviders(providers)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/ae2/CraftingSimulationStateAccessor.java b/src/main/java/com/sorrowmist/useless/mixin/ae2/CraftingSimulationStateAccessor.java new file mode 100644 index 00000000..60d152f1 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/ae2/CraftingSimulationStateAccessor.java @@ -0,0 +1,14 @@ +package com.sorrowmist.useless.mixin.ae2; + +import appeng.api.crafting.IPatternDetails; +import appeng.crafting.inv.CraftingSimulationState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.util.Map; + +@Mixin(value = CraftingSimulationState.class, remap = false) +public interface CraftingSimulationStateAccessor { + @Accessor("crafts") + Map uselessMod$getCrafts(); +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/ae2/CraftingSimulationStateSmartDoublingMixin.java b/src/main/java/com/sorrowmist/useless/mixin/ae2/CraftingSimulationStateSmartDoublingMixin.java new file mode 100644 index 00000000..7fd54e6f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/ae2/CraftingSimulationStateSmartDoublingMixin.java @@ -0,0 +1,30 @@ +package com.sorrowmist.useless.mixin.ae2; + +import appeng.crafting.CraftingCalculation; +import appeng.crafting.CraftingPlan; +import appeng.crafting.inv.CraftingSimulationState; +import appeng.me.service.CraftingService; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.SmartDoublingCalculationContext; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.SmartDoublingPlanner; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(value = CraftingSimulationState.class, priority = 1200, remap = false) +public abstract class CraftingSimulationStateSmartDoublingMixin { + @Inject(method = "buildCraftingPlan", at = @At("HEAD")) + private static void uselessMod$applySmartDoubling( + CraftingSimulationState state, CraftingCalculation calculation, + long calculatedAmount, CallbackInfoReturnable callback) { + var grid = ((SmartDoublingCalculationContext) (Object) calculation).uselessMod$getCraftingGrid(); + if (grid == null || !(grid.getCraftingService() instanceof CraftingService craftingService)) { + return; + } + + var crafts = ((CraftingSimulationStateAccessor) state).uselessMod$getCrafts(); + var rewritten = SmartDoublingPlanner.rewrite(crafts, craftingService::getProviders); + crafts.clear(); + crafts.putAll(rewritten); + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/ae2/CraftingTreeProcessMixin.java b/src/main/java/com/sorrowmist/useless/mixin/ae2/CraftingTreeProcessMixin.java new file mode 100644 index 00000000..fda73555 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/ae2/CraftingTreeProcessMixin.java @@ -0,0 +1,38 @@ +package com.sorrowmist.useless.mixin.ae2; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.networking.crafting.ICraftingService; +import appeng.api.stacks.AEKey; +import appeng.crafting.CraftingCalculation; +import appeng.crafting.CraftingTreeNode; +import appeng.crafting.CraftingTreeProcess; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.DynamicPatternPlanning; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(value = CraftingTreeProcess.class, remap = false) +public abstract class CraftingTreeProcessMixin { + @Shadow(remap = false) + @Final + IPatternDetails details; + + // Forge 47 / Mixin 0.8.5 cannot reliably load ModifyArgs synthetic classes + // from ModLauncher's module classloader on the AE Crafting Calculator thread. + // Wrap allocation instead: preserve every argument and change only the key. + @WrapOperation( + method = "", + at = @At(value = "NEW", target = "appeng/crafting/CraftingTreeNode"), + remap = false) + private CraftingTreeNode uselessMod$preferDeclaredDynamicInput( + ICraftingService service, CraftingCalculation calculation, AEKey key, + long amount, CraftingTreeProcess parent, int slot, + Operation original) { + AEKey preferred = DynamicPatternPlanning.preferDeclaredCraftableInput( + details, slot, key, service); + return original.call(service, calculation, preferred, amount, parent, slot); + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/ae2/ElapsedTimeTrackerAccessor.java b/src/main/java/com/sorrowmist/useless/mixin/ae2/ElapsedTimeTrackerAccessor.java new file mode 100644 index 00000000..628be89a --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/ae2/ElapsedTimeTrackerAccessor.java @@ -0,0 +1,11 @@ +package com.sorrowmist.useless.mixin.ae2; + +import appeng.api.stacks.AEKeyType; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(targets = "appeng.crafting.execution.ElapsedTimeTracker", remap = false) +public interface ElapsedTimeTrackerAccessor { + @Invoker("decrementItems") + void uselessMod$decrementItems(long amount, AEKeyType keyType); +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/ae2/ExecutingCraftingJobAccessor.java b/src/main/java/com/sorrowmist/useless/mixin/ae2/ExecutingCraftingJobAccessor.java new file mode 100644 index 00000000..7fc4cab5 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/ae2/ExecutingCraftingJobAccessor.java @@ -0,0 +1,29 @@ +package com.sorrowmist.useless.mixin.ae2; + +import appeng.api.stacks.GenericStack; +import appeng.crafting.CraftingLink; +import appeng.crafting.execution.ElapsedTimeTracker; +import appeng.crafting.inv.ListCraftingInventory; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(targets = "appeng.crafting.execution.ExecutingCraftingJob", remap = false) +public interface ExecutingCraftingJobAccessor { + @Accessor("waitingFor") + ListCraftingInventory uselessMod$getWaitingFor(); + + @Accessor("timeTracker") + ElapsedTimeTracker uselessMod$getTimeTracker(); + + @Accessor("finalOutput") + GenericStack uselessMod$getFinalOutput(); + + @Accessor("remainingAmount") + long uselessMod$getRemainingAmount(); + + @Accessor("remainingAmount") + void uselessMod$setRemainingAmount(long amount); + + @Accessor("link") + CraftingLink uselessMod$getLink(); +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/ae2/ExecutingCraftingJobMixin.java b/src/main/java/com/sorrowmist/useless/mixin/ae2/ExecutingCraftingJobMixin.java new file mode 100644 index 00000000..06f7f10d --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/ae2/ExecutingCraftingJobMixin.java @@ -0,0 +1,25 @@ +package com.sorrowmist.useless.mixin.ae2; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.crafting.PatternDetailsHelper; +import appeng.api.stacks.AEItemKey; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.AdvancedAlloyFurnacePatternResolver; +import net.minecraft.world.level.Level; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(targets = "appeng.crafting.execution.ExecutingCraftingJob", remap = false) +public abstract class ExecutingCraftingJobMixin { + @Redirect( + method = "(Lnet/minecraft/nbt/CompoundTag;Lappeng/crafting/execution/ExecutingCraftingJob$CraftingDifferenceListener;Lappeng/crafting/execution/CraftingCpuLogic;)V", + at = @At( + value = "INVOKE", + target = "Lappeng/api/crafting/PatternDetailsHelper;decodePattern(Lappeng/api/stacks/AEItemKey;Lnet/minecraft/world/level/Level;)Lappeng/api/crafting/IPatternDetails;", + remap = false)) + private IPatternDetails uselessMod$resolveRestoredPattern( + AEItemKey definition, Level level) { + IPatternDetails decoded = PatternDetailsHelper.decodePattern(definition, level); + return decoded == null ? null : AdvancedAlloyFurnacePatternResolver.resolve(decoded, level); + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/ae2/GridNodeMixin.java b/src/main/java/com/sorrowmist/useless/mixin/ae2/GridNodeMixin.java new file mode 100644 index 00000000..d7436216 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/ae2/GridNodeMixin.java @@ -0,0 +1,52 @@ +package com.sorrowmist.useless.mixin.ae2; + +import appeng.me.GridNode; +import com.sorrowmist.useless.compat.ae.AeLinkChannelBypass; +import org.slf4j.Logger; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +/** + * 精准静音 AE2 的「通道数超过节点容量」自检。 + * + *
+ * if (this.usedChannels > getMaxChannels()) {
+ *     LOG.error("Internal channel assignment error. Grid node {} has {} channels passing through it "
+ *             + "but it only supports up to {}. Please open an issue on the AE2 repository.", ...);
+ * }
+ * 
+ * + *

这条告警的原意是「这里本不该发生」。而造化杖的通道豁免正是故意让节点超容, + * 所以豁免一生效它就必然触发,玩家看着还像我们的 bug。这里直接把那次 {@code LOG.error} 调用掐掉, + * 仅限确实位于「链路」上的节点(机器侧子网 + 机器节点到控制器的祖先链)—— + * 其它节点、其它网格保持 AE2 原生行为,真正的通道分配异常照样报出来。

+ * + *

为什么是 {@code @Redirect} 而不是 {@code @Inject(cancellable = true)}

+ * + *

已知约束:{@code propagateChannelsUpwards(boolean)} 返回 {@code int}, + * 用 {@code @Inject} + {@code CallbackInfo} 会在应用 mixin 时直接抛 + * {@code InvalidInjectionException: CallbackInfoReturnable is required}, + * 整个 mixin 静默失效(配置里 {@code required = false},所以只有一条 WARN,不崩), + * 导致日志静音从未生效:接入造化杖后 AE2 的通道自检报错会持续刷屏。

+ * + *

改用 {@code @Redirect} 有两个好处:

+ *
    + *
  • 不碰控制流,方法的返回值原样保留。若用 {@code @Inject(cancellable = true)} 再 + * {@code cancel()},注入点在方法尾部,取消会连带跳过 {@code return this.usedChannels}, + * 让本节点对 {@code channelsByBlocks} 的贡献变成 0 —— 静音日志却改坏了通道统计。
  • + *
  • 只在确实要打这条 ERROR 时才走判定,没有额外开销。
  • + *
+ */ +@Mixin(targets = "appeng.me.GridNode", remap = false) +public abstract class GridNodeMixin { + @Redirect(method = "propagateChannelsUpwards", + at = @At(value = "INVOKE", + target = "Lorg/slf4j/Logger;error(Ljava/lang/String;[Ljava/lang/Object;)V"), + require = 1) + private void uselessMod$silenceLinkOverflowLog(Logger logger, String message, Object[] arguments) { + if (!AeLinkChannelBypass.carriesLinkTraffic((GridNode) (Object) this)) { + logger.error(message, arguments); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/ae2/PathingCalculationMixin.java b/src/main/java/com/sorrowmist/useless/mixin/ae2/PathingCalculationMixin.java new file mode 100644 index 00000000..3549e49c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/ae2/PathingCalculationMixin.java @@ -0,0 +1,61 @@ +package com.sorrowmist.useless.mixin.ae2; + +import appeng.api.networking.GridFlags; +import appeng.api.networking.IGrid; +import appeng.me.GridNode; +import com.sorrowmist.useless.compat.ae.AeLinkChannelBypass; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.Set; + +/** + * 让造化杖那条连接变成「一根不占通道、也不封顶的线缆」。 + * + *

AE2 的 {@code tryUseChannel} 是两段:先按路径上各节点的 + * {@code GridNode#getMaxChannels()}({@code 8 × factor},dense 才 {@code 32 × factor})校验能否放行, + * 再给路径上每个节点 {@code channelBottlenecks.addTo(pi, 1)} 记账。 + * 这里对「去路跨过造化杖连接」的请求直接放行且完全不记账:

+ *
    + *
  • 远端设备不受容量限制 ✓
  • + *
  • 远端流量不占用绑定网络线缆的通道额度 ⇒ 绑定网络自己的设备完全不受影响 ✓
  • + *
  • 但仍然 {@code channelNodes.add(start)}:{@code propagateAssignments} 靠 + * {@code channelNodes.contains(item)} 决定 {@code consumesChannel}, + * 而 {@code meetsChannelRequirements()} = {@code usedChannels > 0} 才是设备活着的唯一依据。
  • + *
+ * + *

不应改为「仅将容量抬升至 MAX 并按原逻辑记账」:那样远端流量会占满绑定网络线缆的计数, + * 导致绑定侧自身设备分配不到通道。

+ */ +@Mixin(targets = "appeng.me.pathfinding.PathingCalculation", remap = false) +public abstract class PathingCalculationMixin { + /** {@code private final Set channelNodes}:只读引用、往集合里加,不重新赋值。 */ + @Shadow(remap = false) + @Final + private Set channelNodes; + + /** 每次重算开始,把「链路节点」缓存作废(网格结构在这之前可能已经变过)。 */ + @Inject(method = "", at = @At("RETURN")) + private void uselessMod$beginCalculation(IGrid grid, CallbackInfo callback) { + AeLinkChannelBypass.invalidate(); + } + + @Inject(method = "tryUseChannel", at = @At("HEAD"), cancellable = true) + private void uselessMod$grantChannelAcrossLinks(GridNode start, CallbackInfoReturnable callback) { + if (!AeLinkChannelBypass.crossesLink(start)) { + return; + } + if (start.hasFlag(GridFlags.COMPRESSED_CHANNEL) && !start.getSubtreeAllowsCompressedChannels()) { + // 保持 AE2 原本对「压缩通道不能穿过这台设备」的约束(P2P 嵌套那条规则)。 + callback.setReturnValue(false); + return; + } + channelNodes.add(start); + callback.setReturnValue(true); + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/ae2/PathingServiceMixin.java b/src/main/java/com/sorrowmist/useless/mixin/ae2/PathingServiceMixin.java new file mode 100644 index 00000000..96ed78a3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/ae2/PathingServiceMixin.java @@ -0,0 +1,49 @@ +package com.sorrowmist.useless.mixin.ae2; + +import appeng.api.networking.IGridNode; +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.sorrowmist.useless.compat.ae.AeLinkChannelBypass; +import org.objectweb.asm.Opcodes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** + * 无控制器(ad-hoc)网格的通道配额里,把「造化杖连进来的那一侧」排除掉。 + * + *

AE2 对没有控制器的网格走的完全是另一套算法:{@code PathingService.calculateAdHocChannels()} + * 数一遍 {@code nodesNeedingChannels},一旦超过 + * {@code ChannelMode#getAdHocNetworkChannels()}(DEFAULT = 8,X2 = 16,X4 = 32), + * 就整张网全部归零。这道闸门跟「哪条连接」无关,所以必须单独处理: + * 把机器侧那片子网的节点从计数里剔除,其余节点照旧受 8 的约束。

+ * + *

注意 {@code nodesNeedingChannels} 是 {@code Set},foreach 的字节码会调 + * {@code Set.iterator()},所以这里必须返回 {@code Set} 而不是 {@code List}。

+ */ +@Mixin(targets = "appeng.me.service.PathingService", remap = false) +public abstract class PathingServiceMixin { + @ModifyExpressionValue(method = "calculateAdHocChannels", + at = @At(value = "FIELD", + target = "Lappeng/me/service/PathingService;nodesNeedingChannels:Ljava/util/Set;", + opcode = Opcodes.GETFIELD)) + private Set uselessMod$excludeLinkedNodes(Set original) { + if (original.isEmpty() || !AeLinkChannelBypass.hasLinks()) { + return original; + } + Set farSide = AeLinkChannelBypass.farSideNodes(); + if (farSide.isEmpty()) { + return original; + } + List kept = new ArrayList<>(original.size()); + for (IGridNode node : original) { + if (!farSide.contains(node)) { + kept.add(node); + } + } + return kept.size() == original.size() ? original : new LinkedHashSet<>(kept); + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/ae2/PatternAccessTerminalPacketSlotLimitMixin.java b/src/main/java/com/sorrowmist/useless/mixin/ae2/PatternAccessTerminalPacketSlotLimitMixin.java new file mode 100644 index 00000000..bca7a381 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/ae2/PatternAccessTerminalPacketSlotLimitMixin.java @@ -0,0 +1,47 @@ +package com.sorrowmist.useless.mixin.ae2; + +import appeng.core.sync.packets.PatternAccessTerminalPacket; +import com.sorrowmist.useless.content.blockentities.RecoverableItemStackHandler; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyArg; + +/** + * 放宽样板管理终端「单个样板容器」同步槽位的硬上限。 + * + *

AE2 在 {@link PatternAccessTerminalPacket} 的静态初始化里用 + * {@code ByteBufCodecs.map(..., com.sorrowmist.useless.compat.forge1201.network.PortCodecs.ITEM_STACK_OPTIONAL, 128)} 构造 + * 「槽位下标 → 样板」映射的编解码器,其中第四个参数是该映射允许的最大条目数, + * AE2 固定写作 128。该参数同时约束编码与解码两侧,因此它是整条同步链路的硬上限, + * 与容器真实容量无关。

+ * + *

万象合金炉样板总成的槽位容量由配置项 {@code pattern_slots} 决定(默认 108, + * 经 {@code normalizeInventorySlots} 归一化后上界为 {@link RecoverableItemStackHandler#MAX_SLOTS})。 + * 当容器内实际样板数超过 128 时,{@code PatternAccessTermMenu#sendFullUpdate} 会把全部非空槽位 + * 装进该映射,随后在 {@code write} 阶段抛 + * {@code EncoderException: N elements exceeded max size of: 128}。该异常发生在服务端编码 + * 自定义载荷时,网络层将其视为不可恢复的编码失败并中断连接,表现为打开样板管理终端或 + * 扩展样板管理终端立即掉线。

+ * + *

此处把上限抬到与样板总成最大槽位数一致的取值。采用 {@code Math.max} 而非直接替换, + * 是为了在 AE2 将来自行提高该上限时保留其更大的取值,避免本注入反向压低配额。

+ * + *

注入点选在 {@code } 中 {@code ByteBufCodecs.map} 的调用处,直接改写其第四个实参, + * 因此不需要访问 AE2 的私有字段,也不依赖该类的构造细节;编解码两侧共用同一个 + * {@code SLOTS_STREAM_CODEC},改一处即可同时放宽编码与解码。

+ */ +@Mixin(value = PatternAccessTerminalPacket.class, remap = false) +public abstract class PatternAccessTerminalPacketSlotLimitMixin { + + @ModifyArg( + method = "", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/network/codec/ByteBufCodecs;map(Ljava/util/function/IntFunction;Lnet/minecraft/network/codec/StreamCodec;Lnet/minecraft/network/codec/StreamCodec;I)Lnet/minecraft/network/codec/StreamCodec;" + ), + index = 3 + ) + private static int uselessMod$raiseSyncedSlotLimit(int originalLimit) { + return Math.max(originalLimit, RecoverableItemStackHandler.MAX_SLOTS); + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/ae2/PatternDetailsHelperOccultismMixin.java b/src/main/java/com/sorrowmist/useless/mixin/ae2/PatternDetailsHelperOccultismMixin.java new file mode 100644 index 00000000..72310f8b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/ae2/PatternDetailsHelperOccultismMixin.java @@ -0,0 +1,42 @@ +package com.sorrowmist.useless.mixin.ae2; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.crafting.PatternDetailsHelper; +import appeng.api.stacks.AEItemKey; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.OccultismBoundBookPatternDetails; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.OmniversalPatternWarnings; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(value = PatternDetailsHelper.class, priority = 1100, remap = false) +public abstract class PatternDetailsHelperOccultismMixin { + @Inject( + method = "decodePattern(Lappeng/api/stacks/AEItemKey;Lnet/minecraft/world/level/Level;)Lappeng/api/crafting/IPatternDetails;", + at = @At("RETURN"), cancellable = true) + private static void uselessMod$wrapBoundBookKey( + AEItemKey definition, Level level, CallbackInfoReturnable callback) { + try { + callback.setReturnValue(OccultismBoundBookPatternDetails.wrap( + callback.getReturnValue(), level)); + } catch (Exception exception) { + OmniversalPatternWarnings.wrapFailed("bound-book", definition, exception); + } + } + + @Inject( + method = "decodePattern(Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/level/Level;)Lappeng/api/crafting/IPatternDetails;", + at = @At("RETURN"), cancellable = true) + private static void uselessMod$wrapBoundBookStack( + ItemStack stack, Level level, CallbackInfoReturnable callback) { + try { + callback.setReturnValue(OccultismBoundBookPatternDetails.wrap( + callback.getReturnValue(), level)); + } catch (Exception exception) { + OmniversalPatternWarnings.wrapFailed("bound-book", AEItemKey.of(stack), exception); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/ae2/PatternDetailsHelperSmartDoublingMixin.java b/src/main/java/com/sorrowmist/useless/mixin/ae2/PatternDetailsHelperSmartDoublingMixin.java new file mode 100644 index 00000000..3d07bcba --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/ae2/PatternDetailsHelperSmartDoublingMixin.java @@ -0,0 +1,47 @@ +package com.sorrowmist.useless.mixin.ae2; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.crafting.PatternDetailsHelper; +import appeng.api.stacks.AEItemKey; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.OmniversalPatternWarnings; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.SmartDoublingPatterns; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(value = PatternDetailsHelper.class, priority = 1200, remap = false) +public abstract class PatternDetailsHelperSmartDoublingMixin { + @Inject( + method = "decodePattern(Lappeng/api/stacks/AEItemKey;Lnet/minecraft/world/level/Level;)Lappeng/api/crafting/IPatternDetails;", + at = @At("HEAD"), cancellable = true) + private static void uselessMod$decodeScaledKey( + AEItemKey definition, Level level, CallbackInfoReturnable callback) { + if (SmartDoublingPatterns.definitionOperations(definition) == null) { + return; + } + try { + callback.setReturnValue(SmartDoublingPatterns.restore(definition, level)); + } catch (Exception exception) { + OmniversalPatternWarnings.wrapFailed("smart-doubling", definition, exception); + } + } + + @Inject( + method = "decodePattern(Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/level/Level;)Lappeng/api/crafting/IPatternDetails;", + at = @At("HEAD"), cancellable = true) + private static void uselessMod$decodeScaledStack( + ItemStack stack, Level level, CallbackInfoReturnable callback) { + AEItemKey definition = AEItemKey.of(stack); + if (definition == null || SmartDoublingPatterns.definitionOperations(definition) == null) { + return; + } + try { + callback.setReturnValue(SmartDoublingPatterns.restore(definition, level)); + } catch (Exception exception) { + OmniversalPatternWarnings.wrapFailed("smart-doubling", definition, exception); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/ae2/PatternEncodingLogicMixin.java b/src/main/java/com/sorrowmist/useless/mixin/ae2/PatternEncodingLogicMixin.java new file mode 100644 index 00000000..5d47f790 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/ae2/PatternEncodingLogicMixin.java @@ -0,0 +1,224 @@ +package com.sorrowmist.useless.mixin.ae2; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.crafting.PatternDetailsHelper; +import appeng.core.definitions.AEItems; +import appeng.crafting.pattern.AEProcessingPattern; +import appeng.helpers.IPatternTerminalLogicHost; +import appeng.util.inv.AppEngInternalInventory; +import appeng.api.inventories.InternalInventory; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.OmniversalPatternDetails; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.OmniversalPatternEncoding; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.OmniversalPatternUploader; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.PatternUploadNotice; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.PendingOmniversalPatternHolder; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.ProcessingPatternRecipeHolder; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeCatalog; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeIdentity; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import com.sorrowmist.useless.core.config.ConfigManager; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Optional; + +/** + * Turns the pattern a pattern encoding terminal just produced into an Omniversal Pattern, but only + * when the player transferred an Omniversal Alloy Furnace recipe from JEI into that terminal. + * + *

The recipe identity — which carries the mold — is recorded by + * {@code OmniversalPatternJeiTransferHandler} the moment "+" is clicked and stored on this logic + * instance. Encoding is not intercepted at click time: the player still goes through AE2's normal + * flow, so a blank pattern is consumed from the terminal and the result appears in the bottom-right + * slot, and AE2's network-aware ingredient substitution still applies. + * + *

Hooking {@code onChangeInventory} rather than {@code PatternEncodingTermMenu#encode} covers + * every way a pattern can land in the encoded slot: AE2's own {@code encode()}, DataEnergistics' + * {@code encode()} mixin, and ExtendedAE's {@code UniversalPatternEncodingTermMenu}, which writes + * the slot directly. Calling {@code setItemDirect} from inside the callback does not recurse — + * {@code AppEngInternalInventory.notifyingChanges} is already true while the callback runs. A + * successful conversion consumes the pending JEI selection so it cannot affect a later encoding. + */ +@Mixin(targets = "appeng.parts.encoding.PatternEncodingLogic", remap = false) +public class PatternEncodingLogicMixin implements PendingOmniversalPatternHolder { + @Shadow(remap = false) + @Final + private IPatternTerminalLogicHost host; + + @Shadow(remap = false) + @Final + private AppEngInternalInventory encodedPatternInv; + + @Shadow(remap = false) + @Final + private AppEngInternalInventory blankPatternInv; + + /** + * Not persisted: a pending pick only makes sense within the session that made it, and a stale + * identity surviving a world reload would silently re-tag an unrelated pattern. + */ + @Unique + @Nullable + private AlloyFurnaceRecipeIdentity uselessMod$pendingOmniversalRecipe; + + @Unique + @Nullable + private String uselessMod$pendingOmniversalSourceId; + + @Override + @Nullable + public AlloyFurnaceRecipeIdentity uselessMod$getPendingOmniversalRecipe() { + return uselessMod$pendingOmniversalRecipe; + } + + @Override + public void uselessMod$setPendingOmniversalRecipe(@Nullable AlloyFurnaceRecipeIdentity identity) { + this.uselessMod$pendingOmniversalRecipe = identity; + } + + @Override + @Nullable + public String uselessMod$getPendingOmniversalSourceId() { + return uselessMod$pendingOmniversalSourceId; + } + + @Override + public void uselessMod$setPendingOmniversalSourceId(@Nullable String sourceId) { + this.uselessMod$pendingOmniversalSourceId = sourceId == null + ? RecipeSourceIds.UNKNOWN : RecipeSourceIds.normalize(sourceId); + } + + @Override + public void uselessMod$tryConvertPendingOmniversalPattern() { + uselessMod$tryConvertToOmniversal(); + } + + @Inject(method = "onChangeInventory(Lappeng/api/inventories/InternalInventory;I)V", at = @At("HEAD"), remap = false) + private void uselessMod$tryConvertToOmniversal(InternalInventory inv, int slot, CallbackInfo ci) { + if (inv != encodedPatternInv || slot != 0) return; + + uselessMod$tryConvertToOmniversal(); + } + + @Unique + private void uselessMod$tryConvertToOmniversal() { + AlloyFurnaceRecipeIdentity pending = uselessMod$pendingOmniversalRecipe; + if (pending == null) { + return; + } + + ItemStack pattern = encodedPatternInv.getStackInSlot(0); + if (pattern.isEmpty()) { + return; + } + + Level level = host.getLevel(); + if (level == null || level.isClientSide()) { + return; + } + + IPatternDetails details; + try { + details = PatternDetailsHelper.decodePattern(pattern, level); + } catch (RuntimeException exception) { + return; + } + if (details == null) { + return; + } + if (details instanceof OmniversalPatternDetails) { + return; + } + if (!(details instanceof AEProcessingPattern)) { + return; + } + + String sourceId = uselessMod$pendingOmniversalSourceId; + // The client and server can materialize a tag-backed Ingredient with different + // representative items. Resolve the selected recipe by id and the encoded pattern + // contents when the fingerprint differs, otherwise a shared chemical-conversion / + // oxidizing recipe is indistinguishable from the plain AE2 pattern at this point. + Optional entry; + try { + entry = AlloyFurnaceRecipeCatalog.resolvePattern(level, sourceId, pending, details); + } catch (RuntimeException exception) { + return; + } + if (entry.isEmpty()) { + return; + } + try { + if (!AlloyFurnaceRecipeCatalog.matchesRecipe( + level, sourceId, entry.get().recipe(), details)) { + return; + } + } catch (RuntimeException exception) { + return; + } + + if (details instanceof ProcessingPatternRecipeHolder holder) { + holder.uselessMod$setRecipeIdentity(pending); + } + + ItemStack omniversal; + try { + omniversal = OmniversalPatternEncoding.encode(pattern, details, entry.get(), level); + } catch (RuntimeException exception) { + return; + } + if (omniversal.isEmpty()) { + return; + } + + encodedPatternInv.setItemDirect(0, omniversal); + // A JEI selection applies to one encoding action. Do not let it upgrade a later + // ordinary AE2 pattern merely because that pattern happens to be uniquely matchable. + uselessMod$pendingOmniversalRecipe = null; + uselessMod$pendingOmniversalSourceId = null; + + // 样板生成后立即移交同网格的多方块合金炉样板总成,玩家无需再手动搬运。 + // 移交成功后清空终端槽位,使终端保持「已上传」的语义;不存在可接收的总成或 + // 全部总成已满时保留样板,玩家仍可按原有方式自行取出。此处仍处于 + // onChangeInventory 回调内,AppEngInternalInventory.notifyingChanges 已为 true, + // 因此回写槽位与写入总成均不会再次触发本回调。 + // 提示在槽位清空之后发送:玩家看到的顺序应为「样板已进入多方块合金炉」, + // 而不是先收到提示、再看到槽位被清空。 + // 上传总开关:关闭时样板留在编码终端内,由玩家自行取出, + // 与引入自动上传之前的行为完全一致。此处样板已写入槽位、pending 已清空, + // 因此直接返回即可,不需要额外的回滚。 + if (!ConfigManager.isOmniversalPatternAutoUploadEnabled()) { + return; + } + + ServerPlayer player = PatternUploadNotice.resolvePlayer(this, host); + OmniversalPatternUploader.Result result = OmniversalPatternUploader.upload(host, omniversal); + if (result.outcome() == OmniversalPatternUploader.Outcome.UPLOADED) { + encodedPatternInv.setItemDirect(0, ItemStack.EMPTY); + PatternUploadNotice.notifyUploaded(player, result.target()); + return; + } + // 网络中已有等价样板:本次编码不再写入,同时把这次消耗的空白样板还给玩家。 + // 找不到玩家时无法退还,此时保留样板,避免玩家在无提示的情况下净亏一个空白样板。 + if (result.outcome() == OmniversalPatternUploader.Outcome.DUPLICATE && player != null) { + encodedPatternInv.setItemDirect(0, ItemStack.EMPTY); + // 空白样板优先回到终端自带的空白样板槽:那里本就是玩家放置空白样板的位置, + // 刚被本次编码消耗掉一个,通常正好空出来。该槽被占用或放不下时再退背包。 + ItemStack refund = AEItems.BLANK_PATTERN.stack(1); + ItemStack remain = blankPatternInv.addItems(refund); + if (!remain.isEmpty()) { + player.getInventory().placeItemBackInInventory(remain, false); + } + PatternUploadNotice.notifyDuplicate(player); + } + } + +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/botanypots/AbstractBotanyPotBlockEntityAccessor.java b/src/main/java/com/sorrowmist/useless/mixin/botanypots/AbstractBotanyPotBlockEntityAccessor.java new file mode 100644 index 00000000..78b7cb5d --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/botanypots/AbstractBotanyPotBlockEntityAccessor.java @@ -0,0 +1,8 @@ +package com.sorrowmist.useless.mixin.botanypots; +import net.darkhax.botanypots.block.BlockEntityBotanyPot; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +@Mixin(value=BlockEntityBotanyPot.class,remap=false) +public interface AbstractBotanyPotBlockEntityAccessor { + @Accessor("growthTime") int uselessMod$getGrowthTime(); +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/botanypots/BlockEntityBotanyPotMixin.java b/src/main/java/com/sorrowmist/useless/mixin/botanypots/BlockEntityBotanyPotMixin.java deleted file mode 100644 index 886be7b9..00000000 --- a/src/main/java/com/sorrowmist/useless/mixin/botanypots/BlockEntityBotanyPotMixin.java +++ /dev/null @@ -1,382 +0,0 @@ -package com.sorrowmist.useless.mixin.botanypots; - -import com.sorrowmist.useless.config.ConfigManager; -import com.sorrowmist.useless.mixin.accessor.BlockEntityBotanyPotAccessor; -import net.darkhax.botanypots.BotanyPotHelper; -import net.darkhax.botanypots.block.BlockEntityBotanyPot; -import net.darkhax.botanypots.block.inv.BotanyPotContainer; -import net.darkhax.bookshelf.api.Services; -import net.darkhax.bookshelf.api.inventory.IInventoryAccess; -import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.level.Level; -import net.minecraft.world.level.block.state.BlockState; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -import java.util.List; -import java.util.Random; - -@SuppressWarnings({"AddedMixinMembersNamePattern"}) -@Mixin(BlockEntityBotanyPot.class) -public abstract class BlockEntityBotanyPotMixin { - - @Inject( - method = "tickPot(Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;Lnet/darkhax/botanypots/block/BlockEntityBotanyPot;)V", - at = @At("HEAD"), - cancellable = true, - remap = false - ) - private static void onTickPot(Level level, BlockPos pos, BlockState state, BlockEntityBotanyPot pot, CallbackInfo ci) { - // 获取配置的生长倍率 - int multiplier = ConfigManager.getBotanyPotGrowthMultiplier(); - - // 检查生长条件 - if (pot.getCrop() == null || pot.getSoil() == null || !pot.areGrowthConditionsMet()) return; - - BotanyPotContainer inv = pot.getInventory(); - int requiredTime = inv.getRequiredGrowthTime(); - if (requiredTime <= 0) requiredTime = 1; - - int actualTime = requiredTime / multiplier; - - BlockEntityBotanyPotAccessor accessor = (BlockEntityBotanyPotAccessor) pot; - Random rng = accessor.getRng(); - - // 正常加速生长模式 - if (actualTime >= 1) { - int growthThisTick = Math.max(1, multiplier); - int currentGrowth = pot.getGrowthTime(); - int newGrowth = currentGrowth + growthThisTick; - if (newGrowth >= requiredTime) { - // 只增长到requiredTime,然后收获重置 - pot.addGrowth(requiredTime - currentGrowth); - accessor.setDoneGrowing(true); - if (hasOutputSpace(level, pos, pot)) { - performHarvestToContainers(level, pos, pot, rng); - pot.resetGrowth(); - } - } else { - // 直接增长growthThisTick - pot.addGrowth(growthThisTick); - } - return; - } - - // 极高速生长模式 - 一次性生成多倍掉落物到容器 - if (!level.isClientSide) { - int dropsMultiplier = Math.max(1, (int) Math.ceil(multiplier / requiredTime)); - - // 检查容器是否有足够空间 - if (hasOutputSpace(level, pos, pot)) { - // 生成基础掉落物 - List baseDrops = BotanyPotHelper.generateDrop(rng, level, pos, pot, pot.getCrop()); - if (!baseDrops.isEmpty()) { - // 创建多倍掉落物列表 - performMultipliedHarvest(level, pos, pot, baseDrops, dropsMultiplier); - } - pot.resetGrowth(); - } - // 如果容器没有空间,则停止生长 - } - - ci.cancel(); - } - - /** - * 检查是否有输出空间(下方容器或自身槽位) - */ - private static boolean hasOutputSpace(Level level, BlockPos pos, BlockEntityBotanyPot pot) { - // 先检查自身槽位是否有空间 - if (hasSpaceInSelf(pot)) { - return true; - } - - // 如果自身槽位已满,检查下方容器是否有空间接收任意一种自身槽位中的物品 - IInventoryAccess exportTo = Services.INVENTORY_HELPER.getInventory(level, pos.below(), Direction.UP); - if (exportTo != null) { - BotanyPotContainer potInventory = (BotanyPotContainer) pot.getInventory(); - - // 检查自身槽位中的每种物品是否能在下方容器中找到空间 - for (int potSlot : BotanyPotContainer.STORAGE_SLOT) { - ItemStack stackInSlot = potInventory.getItem(potSlot); - if (!stackInSlot.isEmpty()) { - // 检查这个物品是否能放入下方容器 - if (canItemBeInserted(exportTo, stackInSlot)) { - return true; - } - } - } - } - - return false; - } - - /** - * 检查物品是否能被插入到容器中 - */ - private static boolean canItemBeInserted(IInventoryAccess container, ItemStack stack) { - for (int slot : container.getAvailableSlots()) { - ItemStack testStack = stack.copy(); - testStack.setCount(1); // 只测试1个物品 - ItemStack remaining = container.insert(slot, testStack, Direction.UP, false); - if (remaining.isEmpty()) { - return true; - } - } - return false; - } - - /** - * 检查自身物品栏是否有空间 - */ - private static boolean hasSpaceInSelf(BlockEntityBotanyPot pot) { - BotanyPotContainer potInventory = pot.getInventory(); - for (int slot : BotanyPotContainer.STORAGE_SLOT) { - ItemStack stackInSlot = potInventory.getItem(slot); - if (stackInSlot.isEmpty() || stackInSlot.getCount() < stackInSlot.getMaxStackSize()) { - return true; - } - } - return false; - } - - /** - * 将自身槽位的物品输出到下方容器 - */ - private static void exportSelfToBelowContainer(Level level, BlockPos pos, BlockEntityBotanyPot pot) { - IInventoryAccess exportTo = Services.INVENTORY_HELPER.getInventory(level, pos.below(), Direction.UP); - if (exportTo == null) return; - - BotanyPotContainer potInventory = (BotanyPotContainer) pot.getInventory(); - boolean exportedAny = false; - - // 遍历自身所有存储槽位 - for (int potSlot : BotanyPotContainer.STORAGE_SLOT) { - ItemStack stackInSlot = potInventory.getItem(potSlot); - if (stackInSlot.isEmpty()) continue; - - ItemStack remainingStack = stackInSlot.copy(); - int originalCount = remainingStack.getCount(); - - // 尝试输出到下方容器 - for (int exportSlot : exportTo.getAvailableSlots()) { - if (remainingStack.isEmpty()) break; - - // 使用模拟插入检查是否可以插入 - ItemStack simulatedRemaining = exportTo.insert(exportSlot, remainingStack.copy(), Direction.UP, false); - if (simulatedRemaining.getCount() < remainingStack.getCount()) { - // 可以插入,执行实际插入 - ItemStack beforeInsert = remainingStack.copy(); - remainingStack = exportTo.insert(exportSlot, remainingStack, Direction.UP, true); - - // 检查是否成功插入 - if (remainingStack.getCount() < beforeInsert.getCount()) { - exportedAny = true; - } - - // 如果物品完全输出,跳出循环 - if (remainingStack.isEmpty()) { - break; - } - } - } - - // 更新自身槽位的物品 - if (remainingStack.getCount() != originalCount) { - if (remainingStack.isEmpty()) { - potInventory.setItem(potSlot, ItemStack.EMPTY); - } else { - potInventory.setItem(potSlot, remainingStack); - } - } - } - - // 如果有物品被输出,标记为脏以保存更改 - if (exportedAny) { - pot.markDirty(); - } - } - - /** - * 执行多倍收获,一次性输出多倍物品 - */ - private static void performMultipliedHarvest(Level level, BlockPos pos, BlockEntityBotanyPot pot, List baseDrops, int multiplier) { - // 获取下方容器 - IInventoryAccess exportTo = Services.INVENTORY_HELPER.getInventory(level, pos.below(), Direction.UP); - BotanyPotContainer potInventory = (BotanyPotContainer) pot.getInventory(); - - // 对每个基础掉落物进行倍增 - for (ItemStack baseDrop : baseDrops) { - if (baseDrop.isEmpty()) continue; - - // 创建多倍堆叠 - ItemStack multipliedDrop = baseDrop.copy(); - multipliedDrop.setCount(baseDrop.getCount() * multiplier); - - ItemStack remainingDrop = multipliedDrop.copy(); - - // 优先尝试输出到下方容器 - if (exportTo != null) { - for (int exportSlot : exportTo.getAvailableSlots()) { - if (remainingDrop.isEmpty()) break; - - ItemStack beforeInsert = remainingDrop.copy(); - remainingDrop = exportTo.insert(exportSlot, remainingDrop, Direction.UP, false); - - // 如果成功插入了物品 - if (remainingDrop.getCount() < beforeInsert.getCount()) { - // 实际执行插入操作 - exportTo.insert(exportSlot, beforeInsert, Direction.UP, true); - } - } - } - - // 如果下方容器已满,尝试存入自身物品栏 - if (!remainingDrop.isEmpty()) { - for (int potSlot : BotanyPotContainer.STORAGE_SLOT) { - if (remainingDrop.isEmpty()) break; - - ItemStack stackInSlot = potInventory.getItem(potSlot); - - // 如果槽位为空或可以合并 - if (stackInSlot.isEmpty() || - (ItemStack.isSameItemSameTags(stackInSlot, remainingDrop) && - stackInSlot.getCount() < stackInSlot.getMaxStackSize())) { - - if (stackInSlot.isEmpty()) { - // 空槽位,直接设置物品(但不要超过最大堆叠) - int transferAmount = Math.min(remainingDrop.getCount(), remainingDrop.getMaxStackSize()); - ItemStack toSet = remainingDrop.copy(); - toSet.setCount(transferAmount); - potInventory.setItem(potSlot, toSet); - remainingDrop.shrink(transferAmount); - } else { - // 合并到现有堆栈 - int spaceLeft = stackInSlot.getMaxStackSize() - stackInSlot.getCount(); - int toAdd = Math.min(spaceLeft, remainingDrop.getCount()); - - if (toAdd > 0) { - stackInSlot.grow(toAdd); - potInventory.setItem(potSlot, stackInSlot); - remainingDrop.shrink(toAdd); - } - } - } - } - } - - // 如果自身物品栏也满了,销毁剩余物品 - if (!remainingDrop.isEmpty()) { - // 销毁多余物品 - } - } - } - - /** - * 执行单次收获(用于正常模式) - */ - private static void performHarvestToContainers(Level level, BlockPos pos, BlockEntityBotanyPot pot, Random rng) { - if (pot.getCrop() == null) return; - - List drops = BotanyPotHelper.generateDrop(rng, level, pos, pot, pot.getCrop()); - if (drops.isEmpty()) return; - - // 获取下方容器 - IInventoryAccess exportTo = Services.INVENTORY_HELPER.getInventory(level, pos.below(), Direction.UP); - BotanyPotContainer potInventory = (BotanyPotContainer) pot.getInventory(); - - for (ItemStack drop : drops) { - if (drop.isEmpty()) continue; - - ItemStack remainingDrop = drop.copy(); - - // 优先尝试输出到下方容器 - if (exportTo != null) { - for (int exportSlot : exportTo.getAvailableSlots()) { - if (remainingDrop.isEmpty()) break; - - ItemStack beforeInsert = remainingDrop.copy(); - remainingDrop = exportTo.insert(exportSlot, remainingDrop, Direction.UP, false); - - // 如果成功插入了物品 - if (remainingDrop.getCount() < beforeInsert.getCount()) { - // 实际执行插入操作 - exportTo.insert(exportSlot, beforeInsert, Direction.UP, true); - } - } - } - - // 如果下方容器已满,尝试存入自身物品栏 - if (!remainingDrop.isEmpty()) { - for (int potSlot : BotanyPotContainer.STORAGE_SLOT) { - if (remainingDrop.isEmpty()) break; - - ItemStack stackInSlot = potInventory.getItem(potSlot); - - // 如果槽位为空或可以合并 - if (stackInSlot.isEmpty() || - (ItemStack.isSameItemSameTags(stackInSlot, remainingDrop) && - stackInSlot.getCount() < stackInSlot.getMaxStackSize())) { - - if (stackInSlot.isEmpty()) { - // 空槽位,直接设置物品 - int transferAmount = Math.min(remainingDrop.getCount(), remainingDrop.getMaxStackSize()); - ItemStack toSet = remainingDrop.copy(); - toSet.setCount(transferAmount); - potInventory.setItem(potSlot, toSet); - remainingDrop.shrink(transferAmount); - } else { - // 合并到现有堆栈 - int spaceLeft = stackInSlot.getMaxStackSize() - stackInSlot.getCount(); - int toAdd = Math.min(spaceLeft, remainingDrop.getCount()); - - if (toAdd > 0) { - stackInSlot.grow(toAdd); - potInventory.setItem(potSlot, stackInSlot); - remainingDrop.shrink(toAdd); - } - } - } - } - } - - // 如果自身物品栏也满了,销毁剩余物品 - if (!remainingDrop.isEmpty()) { - // 销毁多余物品 - break; - } - } - } - - /** - * 在原有tick逻辑中注入容器检查,防止在容器满时继续生长 - */ - @Inject( - method = "tickPot", - at = @At( - value = "INVOKE", - target = "Lnet/darkhax/botanypots/block/BlockEntityBotanyPot;areGrowthConditionsMet()Z", - shift = At.Shift.AFTER - ), - cancellable = true, - remap = false - ) - private static void onGrowthCheck(Level level, BlockPos pos, BlockState state, BlockEntityBotanyPot pot, CallbackInfo ci) { - // 始终尝试将自身槽位的物品输出到下方容器(如果有的话) - if (!level.isClientSide) { - exportSelfToBelowContainer(level, pos, pot); - } - - // 如果作物已经成熟但没有输出空间,停止生长 - if (pot.isCropHarvestable() && !hasOutputSpace(level, pos, pot)) { - // 停止生长逻辑,防止继续生长和产生更多物品 - ci.cancel(); - } - } - - -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/mixin/botanypots/BotanyPotBlockEntityAccessor.java b/src/main/java/com/sorrowmist/useless/mixin/botanypots/BotanyPotBlockEntityAccessor.java new file mode 100644 index 00000000..eff8b99b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/botanypots/BotanyPotBlockEntityAccessor.java @@ -0,0 +1,9 @@ +package com.sorrowmist.useless.mixin.botanypots; +import net.darkhax.botanypots.block.BlockEntityBotanyPot; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +@Mixin(value=BlockEntityBotanyPot.class,remap=false) +public interface BotanyPotBlockEntityAccessor { + @Accessor("harvestDelay") int getHarvestDelay(); + @Accessor("harvestDelay") void setHarvestDelay(int ticks); +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/botanypots/BotanyPotBlockEntityMixin.java b/src/main/java/com/sorrowmist/useless/mixin/botanypots/BotanyPotBlockEntityMixin.java new file mode 100644 index 00000000..1a166c16 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/botanypots/BotanyPotBlockEntityMixin.java @@ -0,0 +1,24 @@ +package com.sorrowmist.useless.mixin.botanypots; + +import com.sorrowmist.useless.core.config.ConfigManager; +import net.darkhax.botanypots.block.BlockEntityBotanyPot; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.*; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** Advance only an actual native growth step, preserving all harvest events and inventory rules. */ +@Mixin(value=BlockEntityBotanyPot.class,remap=false) +public abstract class BotanyPotBlockEntityMixin { + @Inject(method="tickPot",at=@At(value="FIELD",target="Lnet/darkhax/botanypots/block/BlockEntityBotanyPot;growthTime:I",opcode=org.objectweb.asm.Opcodes.GETFIELD,ordinal=0)) + private static void useless$growthMultiplier(Level level,BlockPos pos,BlockState state,BlockEntityBotanyPot pot,CallbackInfo ci){ + int multiplier=ConfigManager.getBotanyPotGrowthMultiplier(); + if(multiplier<=1) return; + int current=pot.getGrowthTime(); + if(current<0) return; + long remaining=Integer.MAX_VALUE-(long)current-1L; + pot.addGrowth((int)Math.min(remaining,(long)multiplier-1L)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/botanypots/BotanyPotRendererMixin.java b/src/main/java/com/sorrowmist/useless/mixin/botanypots/BotanyPotRendererMixin.java new file mode 100644 index 00000000..3a8cd38d --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/botanypots/BotanyPotRendererMixin.java @@ -0,0 +1,26 @@ +package com.sorrowmist.useless.mixin.botanypots; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.sorrowmist.useless.core.config.ConfigManager; +import net.darkhax.botanypots.block.BotanyPotRenderer; +import net.darkhax.botanypots.block.BlockEntityBotanyPot; +import net.minecraft.client.renderer.MultiBufferSource; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(value=BotanyPotRenderer.class,remap=false) +public class BotanyPotRendererMixin { + + @Inject( + method = "render(Lnet/darkhax/botanypots/block/BlockEntityBotanyPot;FLcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;II)V", + at = @At("HEAD"), + cancellable = true + ) + private void checkRenderingEnabled(BlockEntityBotanyPot pot, float tickDelta, PoseStack pose, MultiBufferSource bufferSource, int light, int overlay, CallbackInfo ci) { + if (!ConfigManager.shouldEnableBotanyPotRendering()) { + ci.cancel(); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/botanypots/BotanyPotsForgeClientMixin.java b/src/main/java/com/sorrowmist/useless/mixin/botanypots/BotanyPotsForgeClientMixin.java deleted file mode 100644 index bdd2371f..00000000 --- a/src/main/java/com/sorrowmist/useless/mixin/botanypots/BotanyPotsForgeClientMixin.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.sorrowmist.useless.mixin.botanypots; - -import net.darkhax.botanypots.BotanyPotsForgeClient; -import net.minecraftforge.client.event.EntityRenderersEvent; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Mixin(BotanyPotsForgeClient.class) -public class BotanyPotsForgeClientMixin { - - private static final boolean DISABLE_POT_RENDERER = true; - - @Inject( - method = "registerEntityRenderers", - at = @At("HEAD"), - cancellable = true, - remap = false - ) - private static void disablePotRendererRegistration(EntityRenderersEvent.RegisterRenderers event, CallbackInfo ci) { - if (DISABLE_POT_RENDERER) { - // 取消注册花盆渲染器 - ci.cancel(); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/mixin/botanypots/PhasedDisplayStateRendererMixin.java b/src/main/java/com/sorrowmist/useless/mixin/botanypots/PhasedDisplayStateRendererMixin.java new file mode 100644 index 00000000..65148b63 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/botanypots/PhasedDisplayStateRendererMixin.java @@ -0,0 +1,22 @@ +package com.sorrowmist.useless.mixin.botanypots; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.darkhax.botanypots.data.displaystate.TransitionalDisplayState; +import net.darkhax.botanypots.data.displaystate.render.TransitionalDisplayStateRenderer; +import net.darkhax.botanypots.data.displaystate.render.DisplayStateRenderer; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.*; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(value=TransitionalDisplayStateRenderer.class,remap=false) +public class PhasedDisplayStateRendererMixin { + @Inject(method="render(Lnet/darkhax/botanypots/data/displaystate/TransitionalDisplayState;Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Lnet/minecraft/client/renderer/MultiBufferSource;IIF)V",at=@At("HEAD"),cancellable=true) + private void useless$maturePhase(TransitionalDisplayState display,PoseStack stack,Level level,BlockPos pos,MultiBufferSource buffers,int light,int overlay,float progress,CallbackInfo ci){ + if(display.phases.isEmpty()) return; + DisplayStateRenderer.renderState(display.phases.get(display.phases.size()-1),stack,level,pos,buffers,light,overlay,1F); + ci.cancel(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/compact/GridMachineClassMixin.java b/src/main/java/com/sorrowmist/useless/mixin/compact/GridMachineClassMixin.java new file mode 100644 index 00000000..3e61475f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/compact/GridMachineClassMixin.java @@ -0,0 +1,78 @@ +package com.sorrowmist.useless.mixin.compact; + +import appeng.api.networking.IGridNode; +import appeng.helpers.patternprovider.PatternContainer; +import com.google.common.collect.SetMultimap; +import com.sorrowmist.useless.compat.neoecoae.compact.CompactMachineClassMap; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +/** + * 让 AE2 的机器索引也能按「ECO 父类」查到紧凑方块主机。 + * + *

{@code appeng.me.Grid} 用宿主精确运行时类建索引: + *

+ *   void add(GridNode n, ...) { this.machines.put(n.getOwner().getClass(), n); }
+ *   public <T> Set<T> getMachines(Class<T> c) { ... this.machines.get(c) ... }   // 精确 key
+ * 
+ * ECO 通篇用 {@code getMachines(ECOComputationSystemBlockEntity.class)} / + * {@code getMachines(ECOCraftingSystemBlockEntity.class)} 找自己的主机,子类永远查不到。 + * 本 mixin 在 {@code add}/{@code remove} 里登记父类 key;非 {@link PatternContainer} 仍保留自己的 key, + * 让需要精确查询紧凑宿主类的逻辑继续可用。

+ * + *

为什么是「追加」而不是「替换」:样板管理终端是靠 + * {@code grid.getMachineClasses()} + {@code PatternContainer.class.isAssignableFrom(...)} + * 做发现的(见 ECO 的 {@code PatternCatalog}),那里需要看到紧凑主机自己的类; + * 聚合样板总线本身就是 {@link PatternContainer},因此这类对象只登记父类 key,避免样板管理终端 + * 按两个 machine class 重复遍历同一个容器。

+ * + *

⚠️ 参数类型必须与字节码里的调用点完全一致({@code (Object,Object)})—— + * 写成 {@code Class}/{@code IGridNode} 会被 mixin 判定为非法签名并整个混入静默失效, + * 所以这里的两个参数都声明为 {@code Object} 再在内部强转。

+ */ +@Mixin(targets = "appeng.me.Grid", remap = false) +public abstract class GridMachineClassMixin { + + @Redirect( + method = "add", + at = @At( + value = "INVOKE", + target = "Lcom/google/common/collect/SetMultimap;put(Ljava/lang/Object;Ljava/lang/Object;)Z", + remap = false)) + private boolean uselessMod$indexByPresentedClass(SetMultimap, IGridNode> machines, + Object ownerClass, + Object node) { + Class owner = (Class) ownerClass; + Class presented = CompactMachineClassMap.presentedClass(owner); + if (presented != owner && PatternContainer.class.isAssignableFrom(owner)) { + return machines.put(presented, (IGridNode) node); + } + boolean added = machines.put(owner, (IGridNode) node); + if (presented != owner) { + machines.put(presented, (IGridNode) node); + } + return added; + } + + @Redirect( + method = "remove", + at = @At( + value = "INVOKE", + target = "Lcom/google/common/collect/SetMultimap;remove(Ljava/lang/Object;Ljava/lang/Object;)Z", + remap = false)) + private boolean uselessMod$removeByPresentedClass(SetMultimap, IGridNode> machines, + Object ownerClass, + Object node) { + Class owner = (Class) ownerClass; + Class presented = CompactMachineClassMap.presentedClass(owner); + if (presented != owner && PatternContainer.class.isAssignableFrom(owner)) { + return machines.remove(presented, (IGridNode) node); + } + boolean removed = machines.remove(owner, (IGridNode) node); + if (presented != owner) { + machines.remove(presented, (IGridNode) node); + } + return removed; + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/compact/MenuDataCodecsCompactMixin.java b/src/main/java/com/sorrowmist/useless/mixin/compact/MenuDataCodecsCompactMixin.java new file mode 100644 index 00000000..b1605a00 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/compact/MenuDataCodecsCompactMixin.java @@ -0,0 +1,37 @@ +package com.sorrowmist.useless.mixin.compact; + +import com.lowdragmc.lowdraglib2.gui.factory.BlockUIMenuType; +import com.lowdragmc.lowdraglib2.gui.holder.ModularUIContainerMenu; +import com.sorrowmist.useless.compat.neoecoae.compact.entity.CompactF9BlockEntity; +import cn.dancingsnow.neoecoae.gui.crafting.PatternPreviewCodec; +import cn.dancingsnow.neoecoae.network.MenuDataTransport; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.entity.player.Player; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** Routes ECO PATTERNS chunks to the in-memory compact interface shadow. */ +@Mixin(targets = "cn.dancingsnow.neoecoae.network.MenuDataCodecs", remap = false) +public final class MenuDataCodecsCompactMixin { + @Inject(method = "receive", at = @At("HEAD"), cancellable = true, remap = false) + private static void uselessMod$receiveCompactPreview( + Player player, MenuDataTransport.Channel channel, FriendlyByteBuf buf, CallbackInfo ci) { + if (channel != MenuDataTransport.Channel.PATTERNS + || !(player.containerMenu instanceof ModularUIContainerMenu menu) + || !(menu.uiHolder instanceof BlockUIMenuType.BlockUIHolder holder) + || !(player.level().getBlockEntity(holder.pos) instanceof CompactF9BlockEntity compact)) { + return; + } + BlockPos target = buf.readBlockPos(); + if (!target.equals(holder.pos)) { + throw new IllegalArgumentException("Compact pattern preview target does not match menu host"); + } + CompoundTag payload = PatternPreviewCodec.read(buf); + compact.receivePatternPreview(payload); + ci.cancel(); + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/create/KineticBlockEntityMixin.java b/src/main/java/com/sorrowmist/useless/mixin/create/KineticBlockEntityMixin.java new file mode 100644 index 00000000..9d0013f5 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/create/KineticBlockEntityMixin.java @@ -0,0 +1,110 @@ +package com.sorrowmist.useless.mixin.create; + +import com.sorrowmist.useless.compat.create.StaffLinkStressState; +import com.simibubi.create.content.kinetics.base.KineticBlockEntity; +import net.minecraft.nbt.CompoundTag; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +/** + * 让被无线驱动的动力学方块对外表现为「自身动力源」。 + * + *

动力学方块的自身转速是写死在方块类里的(水车永远是那个转速、轴永远是 0), + * 没有任何接口能改写它。所以「把转速从远处注入」只能在询问的入口处拦截: + * 方块被无线认领之后,它的自身转速就按认领的转速回答,于是它会像一台真正的动力源那样 + * 带动整个网络。

+ * + *

只在服务端介入。客户端该看到的转速由方块实体同步过去,客户端的判定与渲染 + * 都以那份同步值为准;在这里也改会让两边算出来的网络状态不一致。

+ * + *

另外还负责在存档时把无线虚拟贡献从网络合计里扣掉,理由见下面那个注入点。

+ */ +@Mixin(value = KineticBlockEntity.class, remap = false) +public abstract class KineticBlockEntityMixin { + + @Inject(method = "getGeneratedSpeed", at = @At("HEAD"), cancellable = true, require = 1) + private void uselessMod$wirelessGeneratedSpeed(CallbackInfoReturnable cir) { + KineticBlockEntity self = (KineticBlockEntity) (Object) this; + if (self.getLevel() == null || self.getLevel().isClientSide) { + return; + } + StaffLinkStressState.INSTANCE.wirelessSpeed(self).ifPresent(cir::setReturnValue); + } + + /** + * 补回「上次退出前被无线驱动」的认领。 + * + *

为什么必须补。认领表是运行时状态,重启就没了。而方块实体自己的 {@code speed} + * 是写进存档的 —— 于是重进游戏后,那些被无线驱动的方块会出现一种矛盾状态: + * 身上还留着转速,但 {@code getGeneratedSpeed()} 因为查不到认领而返回 0。 + * 动力学既不当它是动力源、又看到它在转,接下来 {@code validateKinetics()} 会清掉它的转速、 + * 依赖它的邻居会去 {@code propagateMissingSource},而传播逻辑把「同一张网络里转速不一致」 + * 判成环路,直接 {@code destroyBlock} 打掉方块。实测症状:**重进游戏后成排的动力合成器 + * 两侧变成掉落物**。

+ * + *

{@code initialize()} 在方块实体的第一次 {@code tick()} 里、{@code validateKinetics()} + * 之前执行,所以在这里补上就来得及。补上之后它从第一 tick 起仍然表现为动力源, + * 引擎随后会把它换成正式认领;若这条线路已经不在了,临时认领会自然过期并被停掉。

+ */ + @Inject(method = "initialize", at = @At("TAIL"), require = 1) + private void uselessMod$reseedWirelessClaim(CallbackInfo ci) { + KineticBlockEntity self = (KineticBlockEntity) (Object) this; + if (self.getLevel() == null || self.getLevel().isClientSide) { + return; + } + // 判据:自己在转、却不是任何东西的下游、自身又产生不了转速 —— 只可能是它。 + // 真发电机(水车/风车轴承/创造马达等)自己覆写了 getGeneratedSpeed(),非 0,会被排除; + // 普通传动件有 source,也会被排除。 + if (self.getTheoreticalSpeed() == 0.0F || self.hasSource() + || self.getGeneratedSpeed() != 0.0F) { + return; + } + StaffLinkStressState.INSTANCE.reseedClaim(self); + } + + /** + * 存档时把「无线借来的容量 / 无线造成的负载」从网络合计里扣掉。 + * + *

为什么必须扣。动力学把网络合计按方块存盘:每个方块写自己的 + * {@code AddedCapacity} / {@code AddedStress},重载时用合计填 {@code unloadedCapacity} / + * {@code unloadedStress},再由 {@code addSilently} 逐块减掉各自的贡献。而我们的虚拟贡献 + * 没有归属方块 —— 减不掉,于是永久留在 {@code unloaded*} 里,表现为「读一次存档, + * 源网络凭空多出一份负载、目标网络凭空多出一份容量」,而且再也回不去。

+ * + *

扣掉之后,写进磁盘的合计与「各方块贡献之和」一致,重载时的加减就能对上账。 + * 只扣 {@code !clientPacket}(真正写盘的那一次):客户端同步要保留虚拟贡献, + * 护目镜与界面看到的应当是该网络真实的合计。

+ */ + @Inject( + method = "write(Lnet/minecraft/nbt/CompoundTag;Z)V", + at = @At("TAIL"), + require = 1) + private void uselessMod$stripWirelessContribution(CompoundTag compound, + boolean clientPacket, + CallbackInfo ci) { + if (clientPacket) { + return; + } + KineticBlockEntity self = (KineticBlockEntity) (Object) this; + if (self.network == null) { + return; + } + CompoundTag networkTag = compound.getCompound("Network"); + if (networkTag.isEmpty()) { + return; + } + float extraCapacity = StaffLinkStressState.INSTANCE.additionalCapacity(self.network); + if (extraCapacity != 0.0F) { + networkTag.putFloat("Capacity", + Math.max(0.0F, networkTag.getFloat("Capacity") - extraCapacity)); + } + float extraStress = StaffLinkStressState.INSTANCE.additionalStress(self.network); + if (extraStress != 0.0F) { + networkTag.putFloat("Stress", + Math.max(0.0F, networkTag.getFloat("Stress") - extraStress)); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/create/KineticNetworkMixin.java b/src/main/java/com/sorrowmist/useless/mixin/create/KineticNetworkMixin.java new file mode 100644 index 00000000..2a5b24e0 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/create/KineticNetworkMixin.java @@ -0,0 +1,95 @@ +package com.sorrowmist.useless.mixin.create; + +import com.sorrowmist.useless.compat.create.StaffLinkStressState; +import com.simibubi.create.content.kinetics.KineticNetwork; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +/** + * 把无线传输的应力容量与负载并进动力学网络的合计。 + * + *

网络的容量与应力都是「遍历成员再相加」,而无线那部分不属于任何一个方块: + * 容量是从远处借来的,负载是远处机器造成的。所以在两个求和的出口处把它们加上。

+ * + *

这样做的关键好处是不需要改动力学任何一处判定:过载、停转、转速上限、 + * 各种机器的转速要求,全都是拿这两个合计值比较出来的,补进去之后它们自动就成立了。

+ * + *

为什么还要动 {@code unloaded*} 这三个字段

+ * + *

动力学把网络合计**按方块**存进存档:每个方块写自己的 {@code AddedCapacity} / + * {@code AddedStress},重载时用 {@code initFromTE(合计)} 填进 {@code unloadedCapacity} / + * {@code unloadedStress},再由 {@code addSilently} 逐块减掉各自的贡献,剩下的就是「还没加载的 + * 成员」的那一份。

+ * + *

我们的虚拟贡献没有归属方块,于是减不掉、永远留在 {@code unloaded*} 里 —— + * 表现是「存档读一次,源网络就凭空多出一份负载 / 目标网络凭空多出一份容量」,而且再也回不去 + * (实测症状:源端零负载却报满消耗、可用恒为 0;目标端无动力源却报有容量)。

+ * + *

对策:{@code unloadedMembers <= 0} 说明没有任何成员处于未加载状态,此时 + * {@code unloaded*} 按定义就该是 0,直接清掉 —— 既自愈已经存坏的世界,也顺手抹平其它来源的漂移。 + * 写入侧另见 {@code KineticBlockEntityMixin} 里的「存档时扣掉虚拟贡献」。

+ */ +@Mixin(value = KineticNetwork.class, remap = false) +public abstract class KineticNetworkMixin { + + @Shadow + private float unloadedCapacity; + + @Shadow + private float unloadedStress; + + @Shadow + private int unloadedMembers; + + /** + * 没有任何未加载成员时清掉 {@code unloaded*} 的残留。 + * + * @return 是否真的清掉了东西(调用方据此决定要不要重新取值) + */ + private boolean uselessMod$dropOrphanedUnloaded() { + if (unloadedMembers > 0) { + return false; + } + boolean changed = unloadedCapacity != 0.0F || unloadedStress != 0.0F; + unloadedCapacity = 0.0F; + unloadedStress = 0.0F; + return changed; + } + + @Inject(method = "calculateCapacity", at = @At("HEAD"), require = 1) + private void uselessMod$clearOrphanedCapacity(CallbackInfoReturnable cir) { + uselessMod$dropOrphanedUnloaded(); + } + + @Inject(method = "calculateCapacity", at = @At("RETURN"), cancellable = true, require = 1) + private void uselessMod$addWirelessCapacity(CallbackInfoReturnable cir) { + KineticNetwork self = (KineticNetwork) (Object) this; + if (self.id == null) { + return; + } + float extra = StaffLinkStressState.INSTANCE.additionalCapacity(self.id); + if (extra != 0.0F) { + cir.setReturnValue(cir.getReturnValueF() + extra); + } + } + + @Inject(method = "calculateStress", at = @At("HEAD"), require = 1) + private void uselessMod$clearOrphanedStress(CallbackInfoReturnable cir) { + uselessMod$dropOrphanedUnloaded(); + } + + @Inject(method = "calculateStress", at = @At("RETURN"), cancellable = true, require = 1) + private void uselessMod$addWirelessStress(CallbackInfoReturnable cir) { + KineticNetwork self = (KineticNetwork) (Object) this; + if (self.id == null) { + return; + } + float extra = StaffLinkStressState.INSTANCE.additionalStress(self.id); + if (extra != 0.0F) { + cir.setReturnValue(cir.getReturnValueF() + extra); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/eae/GuiAssemblerMatrixPatternInfoMixin.java b/src/main/java/com/sorrowmist/useless/mixin/eae/GuiAssemblerMatrixPatternInfoMixin.java new file mode 100644 index 00000000..80c243b1 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/eae/GuiAssemblerMatrixPatternInfoMixin.java @@ -0,0 +1,17 @@ +package com.sorrowmist.useless.mixin.eae; + + +import com.sorrowmist.useless.core.config.ConfigManager; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.Constant; +import org.spongepowered.asm.mixin.injection.ModifyConstant; + +@Mixin(targets = "com.glodblock.github.extendedae.client.gui.GuiAssemblerMatrix$PatternInfo") +public class GuiAssemblerMatrixPatternInfoMixin { + + @ModifyConstant(method = "", constant = @Constant(intValue = 36)) + private int modifySlotCount(int original) { + int newSize = 36 * ConfigManager.getMatrixPatternCount(); + return newSize; + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/eae/PatternSlotTrackerMixin.java b/src/main/java/com/sorrowmist/useless/mixin/eae/PatternSlotTrackerMixin.java new file mode 100644 index 00000000..549f576d --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/eae/PatternSlotTrackerMixin.java @@ -0,0 +1,29 @@ +package com.sorrowmist.useless.mixin.eae; + +import appeng.util.inv.AppEngInternalInventory; +import com.sorrowmist.useless.core.config.ConfigManager; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(targets = "com.glodblock.github.extendedae.container.ContainerAssemblerMatrix$PatternSlotTracker") +public class PatternSlotTrackerMixin { + + /** + * 修改PatternSlotTracker构造函数中client库存的创建 + * 将硬编码的36改为从配置获取 + */ + @Redirect( + method = "", + at = @At( + value = "NEW", + target = "appeng/util/inv/AppEngInternalInventory", + ordinal = 0 + ), + remap = false + ) + private AppEngInternalInventory modifyClientInventory(int size) { + // 使用配置的倍数,而不是硬编码的36 + return new AppEngInternalInventory(36 * ConfigManager.getMatrixPatternCount()); + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/eae/TileAssemblerMatrixPatternMixin.java b/src/main/java/com/sorrowmist/useless/mixin/eae/TileAssemblerMatrixPatternMixin.java new file mode 100644 index 00000000..a9b09e50 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/eae/TileAssemblerMatrixPatternMixin.java @@ -0,0 +1,36 @@ +package com.sorrowmist.useless.mixin.eae; + +import appeng.util.inv.AppEngInternalInventory; +import com.glodblock.github.extendedae.common.tileentities.matrix.TileAssemblerMatrixPattern; +import com.sorrowmist.useless.core.config.ConfigManager; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(value = TileAssemblerMatrixPattern.class,remap = false) +public class TileAssemblerMatrixPatternMixin { + + @Mutable @Shadow @Final private AppEngInternalInventory patternInventory; + + /** + * 在构造函数完成后替换patternInventory + */ + @Inject( + method = "", + at = @At("RETURN") + ) + private void onConstructed(BlockPos pos, BlockState blockState, CallbackInfo ci) { + TileAssemblerMatrixPattern self = (TileAssemblerMatrixPattern) (Object) this; + + // 创建新的库存实例 + int newSize = 36 * ConfigManager.getMatrixPatternCount(); + this.patternInventory = new AppEngInternalInventory(self, newSize, 1); + this.patternInventory.setFilter(new TileAssemblerMatrixPattern.Filter(self::getLevel)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/emi/EmiAgnosMixin.java b/src/main/java/com/sorrowmist/useless/mixin/emi/EmiAgnosMixin.java deleted file mode 100644 index dbfe8e04..00000000 --- a/src/main/java/com/sorrowmist/useless/mixin/emi/EmiAgnosMixin.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.sorrowmist.useless.mixin.emi; - -import com.sorrowmist.useless.compat.emi.EMIPlugin; -import dev.emi.emi.platform.EmiAgnos; -import dev.emi.emi.registry.EmiPluginContainer; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; - -import java.util.List; - -@Mixin(EmiAgnos.class) -public class EmiAgnosMixin { - - @Inject(method = "getPlugins", at = @At("RETURN"), remap = false) - private static void injectGetPlugins(CallbackInfoReturnable> cir) { - // 获取EMI返回的插件列表 - List plugins = cir.getReturnValue(); - // 添加我们的EMI插件 - plugins.add(new EmiPluginContainer(new EMIPlugin(), "useless_mod")); - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/mixin/emi/JemiAEKeyMixin.java b/src/main/java/com/sorrowmist/useless/mixin/emi/JemiAEKeyMixin.java new file mode 100644 index 00000000..5fd52693 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/emi/JemiAEKeyMixin.java @@ -0,0 +1,20 @@ +package com.sorrowmist.useless.mixin.emi; + +import com.sorrowmist.useless.compat.jei.AEKeyJeiIngredients; +import com.sorrowmist.useless.compat.forge1201.jei.AEKeyEmiStack; +import dev.emi.emi.api.stack.EmiStack; +import mezz.jei.api.ingredients.IIngredientType; +import org.spongepowered.asm.mixin.*; +import org.spongepowered.asm.mixin.injection.*; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Pseudo +@Mixin(targets = "dev.emi.emi.jemi.JemiUtil", remap = false) +public abstract class JemiAEKeyMixin { + @Inject(method = "getStack(Lmezz/jei/api/ingredients/IIngredientType;Ljava/lang/Object;)Ldev/emi/emi/api/stack/EmiStack;", + at = @At("HEAD"), cancellable = true, require = 1) + private static void useless$aeKey(IIngredientType type, Object ingredient, CallbackInfoReturnable cir) { + if (type == AEKeyJeiIngredients.TYPE && ingredient instanceof AEKeyJeiIngredients.KeyIngredient key) + cir.setReturnValue(new AEKeyEmiStack(key)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/emi/JemiAEKeyRegistrationMixin.java b/src/main/java/com/sorrowmist/useless/mixin/emi/JemiAEKeyRegistrationMixin.java new file mode 100644 index 00000000..1fd8b6f0 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/emi/JemiAEKeyRegistrationMixin.java @@ -0,0 +1,16 @@ +package com.sorrowmist.useless.mixin.emi; + +import com.sorrowmist.useless.compat.forge1201.jei.AEKeyEmiStack; +import dev.emi.emi.api.EmiRegistry; +import org.spongepowered.asm.mixin.*; +import org.spongepowered.asm.mixin.injection.*; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Pseudo +@Mixin(targets = "dev.emi.emi.jemi.JemiPlugin", remap = false) +public abstract class JemiAEKeyRegistrationMixin { + @Inject(method = "register", at = @At("RETURN"), require = 1) + private void useless$aeKeySerializer(EmiRegistry registry, CallbackInfo ci) { + AEKeyEmiStack.register(registry); + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/enderio/EnderIOTravelHandlerMixin.java b/src/main/java/com/sorrowmist/useless/mixin/enderio/EnderIOTravelHandlerMixin.java new file mode 100644 index 00000000..dd98d93e --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/enderio/EnderIOTravelHandlerMixin.java @@ -0,0 +1,23 @@ +package com.sorrowmist.useless.mixin.enderio; + +import com.enderio.base.common.handler.TravelHandler; +import com.sorrowmist.useless.content.items.EndlessBeafItem; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.player.Player; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +/** Native Ender IO tests the exact travel-staff item; also recognize an enabled UselessMod staff. */ +@Mixin(value=TravelHandler.class,remap=false) +public abstract class EnderIOTravelHandlerMixin { + @Inject(method="canItemTeleport(Lnet/minecraft/world/entity/player/Player;Lnet/minecraft/world/InteractionHand;)Z", + at=@At("HEAD"),cancellable=true) + private static void useless$travelItem(Player player,InteractionHand hand,CallbackInfoReturnable cir) { + var stack=player.getItemInHand(hand); + if (stack.getItem() instanceof EndlessBeafItem && EndlessBeafItem.isTeleportEnabled(stack)) { + cir.setReturnValue(true); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/itemobliterator/ItemObliteratorContainerMixin.java b/src/main/java/com/sorrowmist/useless/mixin/itemobliterator/ItemObliteratorContainerMixin.java new file mode 100644 index 00000000..0bf33923 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/itemobliterator/ItemObliteratorContainerMixin.java @@ -0,0 +1,33 @@ +package com.sorrowmist.useless.mixin.itemobliterator; + +import com.sorrowmist.useless.compat.itemobliterator.ItemObliteratorProtection; +import java.util.List; + +import net.minecraftforge.event.entity.player.PlayerContainerEvent; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Pseudo +@Mixin( + targets = "elocindev.item_obliterator.neoforge.ItemObliterator", + remap = false, + priority = 11000) +public abstract class ItemObliteratorContainerMixin { + @Redirect( + method = "onPlayerContainer(Lnet/neoforged/neoforge/event/entity/player/PlayerContainerEvent;)V", + at = @At( + value = "INVOKE", + target = "Ljava/util/List;contains(Ljava/lang/Object;)Z", + remap = false), + remap = false) + private boolean uselessMod$keepProtectedItems( + List blacklist, Object itemId) { + if (itemId instanceof String id + && ItemObliteratorProtection.isProtectedItemId(id)) { + return false; + } + return blacklist.contains(itemId); + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/itemobliterator/ItemObliteratorUtilsMixin.java b/src/main/java/com/sorrowmist/useless/mixin/itemobliterator/ItemObliteratorUtilsMixin.java new file mode 100644 index 00000000..01a3ead4 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/itemobliterator/ItemObliteratorUtilsMixin.java @@ -0,0 +1,75 @@ +package com.sorrowmist.useless.mixin.itemobliterator; + +import com.sorrowmist.useless.compat.itemobliterator.ItemObliteratorProtection; +import net.minecraft.world.item.ItemStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Pseudo +@Mixin( + targets = "elocindev.item_obliterator.neoforge.utils.Utils", + remap = false, + priority = 11000) +public abstract class ItemObliteratorUtilsMixin { + @Inject( + method = "isDisabled(Ljava/lang/String;)Z", + at = @At("HEAD"), + cancellable = true, + remap = false) + private static void uselessMod$protectItemId( + String itemId, CallbackInfoReturnable cir) { + protectItemId(itemId, cir); + } + + @Inject( + method = "isDisabled(Lnet/minecraft/world/item/ItemStack;)Z", + at = @At("HEAD"), + cancellable = true, + remap = false) + private static void uselessMod$protectItemStack( + ItemStack stack, CallbackInfoReturnable cir) { + if (ItemObliteratorProtection.isProtected(stack)) { + cir.setReturnValue(false); + } + } + + @Inject( + method = "shouldRecipeBeDisabled(Ljava/lang/String;)Z", + at = @At("HEAD"), + cancellable = true, + remap = false) + private static void uselessMod$protectRecipeOutput( + String itemId, CallbackInfoReturnable cir) { + protectItemId(itemId, cir); + } + + @Inject( + method = "isDisabledInteract(Ljava/lang/String;)Z", + at = @At("HEAD"), + cancellable = true, + remap = false) + private static void uselessMod$protectInteraction( + String itemId, CallbackInfoReturnable cir) { + protectItemId(itemId, cir); + } + + @Inject( + method = "isDisabledAttack(Ljava/lang/String;)Z", + at = @At("HEAD"), + cancellable = true, + remap = false) + private static void uselessMod$protectAttack( + String itemId, CallbackInfoReturnable cir) { + protectItemId(itemId, cir); + } + + private static void protectItemId( + String itemId, CallbackInfoReturnable cir) { + if (ItemObliteratorProtection.isProtectedItemId(itemId)) { + cir.setReturnValue(false); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/jade/JadeItemStorageExtensionMixin.java b/src/main/java/com/sorrowmist/useless/mixin/jade/JadeItemStorageExtensionMixin.java new file mode 100644 index 00000000..824f15a4 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/jade/JadeItemStorageExtensionMixin.java @@ -0,0 +1,26 @@ +package com.sorrowmist.useless.mixin.jade; + +import com.sorrowmist.useless.compat.jade.JadePatternStorageSnapshot; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import snownee.jade.addon.universal.ItemStorageProvider; +import snownee.jade.api.view.ViewGroup; + +import java.util.List; + +@Mixin(value=ItemStorageProvider.class,remap=false) +public abstract class JadeItemStorageExtensionMixin { + @Inject(method = "getGroups", at = @At("HEAD"), cancellable = true) + private void useless$useCachedPatternGroups( + net.minecraft.server.level.ServerPlayer player, net.minecraft.server.level.ServerLevel level, + Object target, boolean showDetails, CallbackInfoReturnable>> cir) { + @Nullable List> groups = JadePatternStorageSnapshot.getGroups(target); + if (groups != null) { + cir.setReturnValue(groups); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/jade/JadeItemStorageProviderMixin.java b/src/main/java/com/sorrowmist/useless/mixin/jade/JadeItemStorageProviderMixin.java new file mode 100644 index 00000000..8045df1b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/jade/JadeItemStorageProviderMixin.java @@ -0,0 +1,19 @@ +package com.sorrowmist.useless.mixin.jade; + +import com.sorrowmist.useless.compat.jade.JadePatternStorageSnapshot; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import snownee.jade.addon.universal.ItemStorageProvider; +import snownee.jade.api.Accessor; + +@Mixin(value = ItemStorageProvider.class, remap = false) +public abstract class JadeItemStorageProviderMixin { + @Inject(method = "putData", at = @At("HEAD"), cancellable = true) + private static void useless$writeCachedPatternData(Accessor accessor, CallbackInfo ci) { + if (JadePatternStorageSnapshot.writeData(accessor)) { + ci.cancel(); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/jei/ForgePluginFinderMixin.java b/src/main/java/com/sorrowmist/useless/mixin/jei/ForgePluginFinderMixin.java deleted file mode 100644 index 290f036d..00000000 --- a/src/main/java/com/sorrowmist/useless/mixin/jei/ForgePluginFinderMixin.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.sorrowmist.useless.mixin.jei; - -import com.sorrowmist.useless.compat.jei.JEIPlugin; -import mezz.jei.api.IModPlugin; -import mezz.jei.forge.startup.ForgePluginFinder; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; - -import java.util.List; - -@Mixin(ForgePluginFinder.class) -public class ForgePluginFinderMixin { - - @Inject(method = "getModPlugins", at = @At("RETURN"), remap = false) - private static void injectGetModPlugins(CallbackInfoReturnable> cir) { - // 获取GTOCore返回的插件列表 - List plugins = cir.getReturnValue(); - // 添加我们的JEI插件 - plugins.add(new JEIPlugin()); - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/mixin/malum/SoulHarvestDropMixin.java b/src/main/java/com/sorrowmist/useless/mixin/malum/SoulHarvestDropMixin.java new file mode 100644 index 00000000..d70d8149 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/malum/SoulHarvestDropMixin.java @@ -0,0 +1,36 @@ +package com.sorrowmist.useless.mixin.malum; + +import com.sorrowmist.useless.content.items.EndlessBeafItem; +import com.sorrowmist.useless.core.component.UComponents; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraftforge.event.entity.living.LivingDeathEvent; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Pseudo +@Mixin(targets = "com.sammy.malum.core.handlers.SoulHarvestHandler", remap = false) +public abstract class SoulHarvestDropMixin { + @Inject(method = "onDeath", at = @At("HEAD"), cancellable = true, remap = false) + private static void uselessMod$disableBeefSoulDrops( + LivingDeathEvent event, CallbackInfo callback) { + if (isDisabledBeefDamage(event.getSource())) { + callback.cancel(); + } + } + + private static boolean isDisabledBeefDamage(DamageSource source) { + Entity attacker = source.getEntity(); + if (!(attacker instanceof Player player)) { + return false; + } + + var stack = player.getMainHandItem(); + return stack.getItem() instanceof EndlessBeafItem + && !com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.BeefMalumSpiritEnabledComponent.get(), false); + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/malum/SoulHarvestHandlerMixin.java b/src/main/java/com/sorrowmist/useless/mixin/malum/SoulHarvestHandlerMixin.java new file mode 100644 index 00000000..ff2636b0 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/malum/SoulHarvestHandlerMixin.java @@ -0,0 +1,51 @@ +package com.sorrowmist.useless.mixin.malum; + +import com.sorrowmist.useless.content.items.EndlessBeafItem; +import com.sorrowmist.useless.core.component.UComponents; +import com.sammy.malum.registry.common.MalumAttachmentTypes; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.event.entity.living.LivingDamageEvent; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Pseudo +@Mixin(targets = "com.sammy.malum.core.handlers.SoulDataHandler", remap = false) +public abstract class SoulHarvestHandlerMixin { + @Inject(method = "exposeSoul", at = @At("HEAD"), cancellable = true, remap = false) + private static void uselessMod$disableBeefSoulExposure( + LivingDamageEvent.Post event, CallbackInfo callback) { + if (isDisabledBeefDamage(event.getSource())) { + callback.cancel(); + } else if (isEnabledBeefDamage(event.getSource())) { + event.getEntity().getData(MalumAttachmentTypes.LIVING_SOUL_INFO).setExposed(); + } + } + + private static boolean isDisabledBeefDamage(DamageSource source) { + ItemStack stack = beefTool(source); + return !stack.isEmpty() + && !com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.BeefMalumSpiritEnabledComponent.get(), false); + } + + private static boolean isEnabledBeefDamage(DamageSource source) { + ItemStack stack = beefTool(source); + return !stack.isEmpty() + && com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.BeefMalumSpiritEnabledComponent.get(), false); + } + + private static ItemStack beefTool(DamageSource source) { + Entity attacker = source.getEntity(); + if (!(attacker instanceof Player player)) { + return ItemStack.EMPTY; + } + + ItemStack stack = player.getMainHandItem(); + return stack.getItem() instanceof EndlessBeafItem ? stack : ItemStack.EMPTY; + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/mek/CachedRecipeMixin.java b/src/main/java/com/sorrowmist/useless/mixin/mek/CachedRecipeMixin.java index b45d0ebd..db080511 100644 --- a/src/main/java/com/sorrowmist/useless/mixin/mek/CachedRecipeMixin.java +++ b/src/main/java/com/sorrowmist/useless/mixin/mek/CachedRecipeMixin.java @@ -1,37 +1,12 @@ package com.sorrowmist.useless.mixin.mek; -/* - * This file is based on Mekanism Upgrades: Reborn. - * - * Copyright (c) Y_Xiao233 - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import mekanism.api.recipes.cache.CachedRecipe; import com.sorrowmist.useless.utils.MekTemp; +import mekanism.api.recipes.cache.CachedRecipe; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import org.spongepowered.asm.mixin.injection.callback.LocalCapture; import java.util.function.IntSupplier; @@ -51,10 +26,9 @@ public abstract class CachedRecipeMixin { target = "Lmekanism/api/recipes/cache/CachedRecipe;finishProcessing(I)V", shift = At.Shift.AFTER ), - locals = LocalCapture.CAPTURE_FAILHARD, remap = false ) - private void injected(CallbackInfo ci, int operations) { + private void injected(CallbackInfo ci) { MekTemp.inject.accept(this.requiredTicks.getAsInt(), this::process); } -} \ No newline at end of file +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/mek/DigitalMinerMixin.java b/src/main/java/com/sorrowmist/useless/mixin/mek/DigitalMinerMixin.java index eb7b141e..2ece5d1e 100644 --- a/src/main/java/com/sorrowmist/useless/mixin/mek/DigitalMinerMixin.java +++ b/src/main/java/com/sorrowmist/useless/mixin/mek/DigitalMinerMixin.java @@ -1,77 +1,43 @@ package com.sorrowmist.useless.mixin.mek; -/* - * This file is based on Mekanism Upgrades: Reborn. - * - * Copyright (c) Y_Xiao233 - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import com.sorrowmist.useless.mixin.accessor.DelayAccessor; import mekanism.api.Upgrade; import mekanism.common.tile.machine.TileEntityDigitalMiner; +import com.sorrowmist.useless.utils.MekTemp; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.Slice; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(value = TileEntityDigitalMiner.class, remap = false) public abstract class DigitalMinerMixin { - @Shadow - private int delay; - - @Shadow - protected abstract void tryMineBlock(); - - @Shadow - public abstract void recalculateUpgrades(Upgrade upgrade); - - @Shadow - public abstract int getDelay(); + @Shadow private int delay; + @Shadow protected abstract void tryMineBlock(); + @Shadow public abstract void recalculateUpgrades(Upgrade upgrade); + @Shadow public abstract int getDelay(); @Inject( method = "onUpdateServer", at = @At( value = "INVOKE", target = "Lmekanism/common/capabilities/energy/MinerEnergyContainer;extract(Lmekanism/api/math/FloatingLong;Lmekanism/api/Action;Lmekanism/api/AutomationType;)Lmekanism/api/math/FloatingLong;", + ordinal = 1, // 第2次 extract(EXECUTE) shift = At.Shift.AFTER ), - slice = @Slice( - from = @At( - value = "INVOKE", - target = "Lmekanism/common/tile/machine/TileEntityDigitalMiner;setActive(Z)V" - ) - ), remap = false ) - public void injected(CallbackInfo ci) { + private void onEnergyExtracted(CallbackInfo cir) { if (this.delay < 0) { - for(int i = this.delay - 1; i < 0; ++i) { + // 补足所有负 delay 的挖掘 + int extraOperations = Math.min(-this.delay, MekTemp.MAX_EXTRA_OPERATIONS_PER_TICK); + for (int i = 0; i < extraOperations; i++) { this.tryMineBlock(); } - + // 重新计算 Speed 升级(delay 依赖它) this.recalculateUpgrades(Upgrade.SPEED); - ((DelayAccessor)this).setDelay(this.getDelay()); + // 重置 delay 为正确值 + this.delay = this.getDelay(); } } -} \ No newline at end of file +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/mek/ElectricPumpMixin.java b/src/main/java/com/sorrowmist/useless/mixin/mek/ElectricPumpMixin.java index 55270d90..4db2f0b3 100644 --- a/src/main/java/com/sorrowmist/useless/mixin/mek/ElectricPumpMixin.java +++ b/src/main/java/com/sorrowmist/useless/mixin/mek/ElectricPumpMixin.java @@ -1,31 +1,7 @@ package com.sorrowmist.useless.mixin.mek; -/* - * This file is based on Mekanism Upgrades: Reborn. - * - * Copyright (c) Y_Xiao233 - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import mekanism.common.tile.machine.TileEntityElectricPump; import com.sorrowmist.useless.utils.MekTemp; +import mekanism.common.tile.machine.TileEntityElectricPump; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; @@ -40,17 +16,14 @@ public abstract class ElectricPumpMixin { @Shadow protected abstract void onUpdateServer(); - @Inject( method = "onUpdateServer", at = @At( value = "INVOKE", target = "Lmekanism/common/tile/machine/TileEntityElectricPump;suck()Z", shift = At.Shift.AFTER - ), - remap = false - ) - public void injected(CallbackInfo ci) { + ) + ) private void injected(CallbackInfo cir) { MekTemp.inject.accept(this.ticksRequired, this::onUpdateServer); } -} \ No newline at end of file +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/mek/FluidicPlenisherMixin.java b/src/main/java/com/sorrowmist/useless/mixin/mek/FluidicPlenisherMixin.java index b0b39103..2fbb3bfb 100644 --- a/src/main/java/com/sorrowmist/useless/mixin/mek/FluidicPlenisherMixin.java +++ b/src/main/java/com/sorrowmist/useless/mixin/mek/FluidicPlenisherMixin.java @@ -1,29 +1,5 @@ package com.sorrowmist.useless.mixin.mek; -/* - * This file is based on Mekanism Upgrades: Reborn. - * - * Copyright (c) Y_Xiao233 - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - import com.sorrowmist.useless.utils.MekTemp; import mekanism.common.tile.machine.TileEntityFluidicPlenisher; import org.spongepowered.asm.mixin.Mixin; @@ -50,7 +26,7 @@ public abstract class FluidicPlenisherMixin { ), remap = false ) - public void injected(CallbackInfo ci) { + public void injected(CallbackInfo cir) { MekTemp.inject.accept(this.ticksRequired, this::onUpdateServer); } -} \ No newline at end of file +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/mek/FormulaicAssemblicatorMixin.java b/src/main/java/com/sorrowmist/useless/mixin/mek/FormulaicAssemblicatorMixin.java index 82708304..d0deeeed 100644 --- a/src/main/java/com/sorrowmist/useless/mixin/mek/FormulaicAssemblicatorMixin.java +++ b/src/main/java/com/sorrowmist/useless/mixin/mek/FormulaicAssemblicatorMixin.java @@ -1,29 +1,5 @@ package com.sorrowmist.useless.mixin.mek; -/* - * This file is based on Mekanism Upgrades: Reborn. - * - * Copyright (c) Y_Xiao233 - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - import com.sorrowmist.useless.utils.MekTemp; import mekanism.common.tile.machine.TileEntityFormulaicAssemblicator; import org.spongepowered.asm.mixin.Mixin; @@ -50,7 +26,7 @@ public abstract class FormulaicAssemblicatorMixin { ), remap = false ) - public void injected(CallbackInfo ci) { + public void injected(CallbackInfo cir) { MekTemp.inject.accept(this.ticksRequired, this::onUpdateServer); } -} \ No newline at end of file +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/mek/FuckingPrideHolidayMixin.java b/src/main/java/com/sorrowmist/useless/mixin/mek/FuckingPrideHolidayMixin.java new file mode 100644 index 00000000..fa9c1162 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/mek/FuckingPrideHolidayMixin.java @@ -0,0 +1,29 @@ +package com.sorrowmist.useless.mixin.mek; + +import mekanism.common.base.holiday.Holiday; +import mekanism.common.base.HolidayManager; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Set; +import java.util.stream.Collectors; + +@Mixin(value = HolidayManager.class, remap = false) +public class FuckingPrideHolidayMixin { + @Mutable + @Final + @Shadow + private static Set holidays; + + @Inject(method = "", at = @At("TAIL")) + private static void removePrideHoliday(CallbackInfo ci) { + holidays = holidays.stream() + .filter(holiday -> !"mekanism.common.base.holiday.Pride".equals(holiday.getClass().getName())) + .collect(Collectors.toUnmodifiableSet()); + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/mek/MekanismUtilsMixin.java b/src/main/java/com/sorrowmist/useless/mixin/mek/MekanismUtilsMixin.java index 453e9d2b..29d1965a 100644 --- a/src/main/java/com/sorrowmist/useless/mixin/mek/MekanismUtilsMixin.java +++ b/src/main/java/com/sorrowmist/useless/mixin/mek/MekanismUtilsMixin.java @@ -1,72 +1,23 @@ package com.sorrowmist.useless.mixin.mek; - -/* - * This file is based on Mekanism Upgrades: Reborn. - * - * Copyright (c) Y_Xiao233 - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - +import com.sorrowmist.useless.utils.MekUtils; import mekanism.api.math.FloatingLong; -import mekanism.api.math.MathUtils; import mekanism.common.tile.interfaces.IUpgradeTile; import mekanism.common.util.MekanismUtils; -import com.sorrowmist.useless.utils.MekUtils; import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Overwrite; - -/** - * Mixin for MekanismUtils to modify upgrade calculations - */ -@Mixin(value = MekanismUtils.class, remap = false) +import org.spongepowered.asm.mixin.injection.*; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +@Mixin(value=MekanismUtils.class,remap=false) public class MekanismUtilsMixin { - - /** - * @author sorrowmist - * @reason Overwrite to use custom time calculation with configurable multipliers - */ - @Overwrite - public static int getTicks(IUpgradeTile tile, int def) { - if (tile.supportsUpgrades()) { - double d = (double)def * MekUtils.time(tile); - return d >= 1.0 ? MathUtils.clampToInt(d) : MathUtils.clampToInt(1.0 / d) * -1; - } else { - return def; - } + @Inject(method="getTicks",at=@At("HEAD"),cancellable=true) + private static void ticks(IUpgradeTile tile,int base,CallbackInfoReturnable cir){ + if(tile.supportsUpgrades()) cir.setReturnValue(Math.max(1,(int)Math.ceil(base*MekUtils.time(tile)))); } - - /** - * @author sorrowmist - * @reason Overwrite to use custom energy consumption calculation with configurable multipliers - */ - @Overwrite - public static FloatingLong getEnergyPerTick(IUpgradeTile tile, FloatingLong def) { - return tile.supportsUpgrades() ? def.multiply(MekUtils.electricity(tile)) : def; + @Inject(method="getEnergyPerTick",at=@At("HEAD"),cancellable=true) + private static void energy(IUpgradeTile tile,FloatingLong base,CallbackInfoReturnable cir){ + if(tile.supportsUpgrades()) cir.setReturnValue(base.multiply(MekUtils.electricity(tile))); } - - /** - * @author sorrowmist - * @reason Overwrite to use custom energy capacity calculation with configurable multipliers - */ - @Overwrite - public static FloatingLong getMaxEnergy(IUpgradeTile tile, FloatingLong def) { - return tile.supportsUpgrades() ? def.multiply(MekUtils.capacity(tile)) : def; + @Inject(method="getMaxEnergy(Lmekanism/common/tile/interfaces/IUpgradeTile;Lmekanism/api/math/FloatingLong;)Lmekanism/api/math/FloatingLong;",at=@At("HEAD"),cancellable=true) + private static void capacity(IUpgradeTile tile,FloatingLong base,CallbackInfoReturnable cir){ + if(tile.supportsUpgrades()) cir.setReturnValue(base.multiply(MekUtils.capacity(tile))); } -} \ No newline at end of file +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/TileComponentEjectorMixin.java b/src/main/java/com/sorrowmist/useless/mixin/mek/TileComponentEjectorMixin.java similarity index 78% rename from src/main/java/com/sorrowmist/useless/mixin/TileComponentEjectorMixin.java rename to src/main/java/com/sorrowmist/useless/mixin/mek/TileComponentEjectorMixin.java index ab171bb4..2c1a0fc2 100644 --- a/src/main/java/com/sorrowmist/useless/mixin/TileComponentEjectorMixin.java +++ b/src/main/java/com/sorrowmist/useless/mixin/mek/TileComponentEjectorMixin.java @@ -1,5 +1,6 @@ -package com.sorrowmist.useless.mixin; +package com.sorrowmist.useless.mixin.mek; +import mekanism.common.tile.component.TileComponentEjector; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; @@ -7,15 +8,11 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -@Mixin(value = mekanism.common.tile.component.TileComponentEjector.class, remap = false) - - +@Mixin(value = TileComponentEjector.class, remap = false) public class TileComponentEjectorMixin { @Shadow(remap = false) private int tickDelay; - - @Inject( method = "outputItems", at = @At("RETURN") @@ -24,5 +21,4 @@ private void modifyTickDelay(CallbackInfo ci) { // 在方法末尾将 tickDelay 设置为 0 this.tickDelay = 0; } - -} \ No newline at end of file +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/mek/TileEntityDigitalMinerMixin.java b/src/main/java/com/sorrowmist/useless/mixin/mek/TileEntityDigitalMinerMixin.java new file mode 100644 index 00000000..c98577c0 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/mek/TileEntityDigitalMinerMixin.java @@ -0,0 +1,29 @@ +package com.sorrowmist.useless.mixin.mek; + +import mekanism.common.tile.machine.TileEntityDigitalMiner; +import org.objectweb.asm.Opcodes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(value = TileEntityDigitalMiner.class, remap = false) +public class TileEntityDigitalMinerMixin { + @Shadow private int delayTicks; + + @Inject( + method = "onUpdateServer", + at = @At( + value = "FIELD", + target = "Lmekanism/common/tile/machine/TileEntityDigitalMiner;delayTicks:I", + opcode = Opcodes.PUTFIELD, + ordinal = 0, + shift = At.Shift.AFTER + ) + ) + private void modifyDelayTicks(CallbackInfo cir) { + // 获取被混入的实例并修改 delayTicks 为 1 + this.delayTicks = 0; + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/mek/UpgradeMixin.java b/src/main/java/com/sorrowmist/useless/mixin/mek/UpgradeMixin.java index f2ff1657..dfa6a852 100644 --- a/src/main/java/com/sorrowmist/useless/mixin/mek/UpgradeMixin.java +++ b/src/main/java/com/sorrowmist/useless/mixin/mek/UpgradeMixin.java @@ -1,29 +1,5 @@ package com.sorrowmist.useless.mixin.mek; -/* - * This file is based on Mekanism Upgrades: Reborn. - * - * Copyright (c) Y_Xiao233 - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - import com.sorrowmist.useless.utils.MekTemp; import com.sorrowmist.useless.utils.MekUtils; import mekanism.api.Upgrade; @@ -56,4 +32,4 @@ private static String getName(String s) { private static int toFullStack(int i) { return !MekTemp.name.equals("speed") && !MekTemp.name.equals("energy") ? i : MekUtils.MAX_UPGRADE; } -} \ No newline at end of file +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/mek/UpgradeUtilsMixin.java b/src/main/java/com/sorrowmist/useless/mixin/mek/UpgradeUtilsMixin.java index c3462d23..9956226f 100644 --- a/src/main/java/com/sorrowmist/useless/mixin/mek/UpgradeUtilsMixin.java +++ b/src/main/java/com/sorrowmist/useless/mixin/mek/UpgradeUtilsMixin.java @@ -1,35 +1,11 @@ package com.sorrowmist.useless.mixin.mek; -/* - * This file is based on Mekanism Upgrades: Reborn. - * - * Copyright (c) Y_Xiao233 - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - +import com.sorrowmist.useless.utils.MekUtils; import mekanism.api.Upgrade; import mekanism.common.MekanismLang; import mekanism.common.config.MekanismConfig; import mekanism.common.tile.interfaces.IUpgradeTile; import mekanism.common.util.UpgradeUtils; -import com.sorrowmist.useless.utils.MekUtils; import net.minecraft.network.chat.Component; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Overwrite; @@ -51,7 +27,8 @@ public class UpgradeUtilsMixin { public static List getExpScaledInfo(IUpgradeTile tile, Upgrade upgrade) { ArrayList ret = new ArrayList(); if (tile.supportsUpgrades() && upgrade.getMax() > 1) { - ret.add(MekanismLang.UPGRADES_EFFECT.translate(MekUtils.time(tile))); + double effect = upgrade == Upgrade.SPEED ? 1.0 / MekUtils.time(tile) : MekUtils.time(tile); + ret.add(MekanismLang.UPGRADES_EFFECT.translate(effect)); } return ret; } @@ -65,11 +42,11 @@ public static List getMultScaledInfo(IUpgradeTile tile, Upgrade upgra ArrayList ret = new ArrayList(); if (tile.supportsUpgrades() && upgrade.getMax() > 1) { double effect = upgrade == Upgrade.ENERGY ? MekUtils.capacity(tile) : - (upgrade == Upgrade.SPEED ? MekUtils.time(tile) : + (upgrade == Upgrade.SPEED ? 1.0 / MekUtils.time(tile) : Math.pow(MekanismConfig.general.maxUpgradeMultiplier.get(), (double)tile.getComponent().getUpgrades(upgrade) / (double)upgrade.getMax())); ret.add(MekanismLang.UPGRADES_EFFECT.translate(MekUtils.exponential(effect))); } return ret; } -} \ No newline at end of file +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/pneumaticcraft/MachineAirHandlerMixin.java b/src/main/java/com/sorrowmist/useless/mixin/pneumaticcraft/MachineAirHandlerMixin.java new file mode 100644 index 00000000..4218a550 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/mixin/pneumaticcraft/MachineAirHandlerMixin.java @@ -0,0 +1,57 @@ +package com.sorrowmist.useless.mixin.pneumaticcraft; + +import com.sorrowmist.useless.content.stafflink.PressureSealRegistry; +import me.desht.pneumaticcraft.api.tileentity.IAirHandlerMachine; +import me.desht.pneumaticcraft.common.capabilities.MachineAirHandler; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * 让被无线物流绑定的气动方块「视同已连接」,不再向环境漏气。 + * + *

为什么要注入

+ * + *

气动工艺的方块在某个面没接空气处理器时会持续漏气:{@code MachineAirHandler#tick} 末尾会调 + * {@code handleAirLeak(...)},每 tick 销毁 {@code 压力 × 40 + 20} mL。典型受害者是压力室气阀 + * ({@code PressureChamberValveBlockEntity#checkForAirLeak} 只在朝外那面真的挂着 + * {@code AIR_HANDLER_MACHINE} 能力时才认为密封)。无线物流的注入只是 {@code addAir(...)}, + * 不会建立这样一个邻居 ⇒ 方块永远认为自己漏气,玩家灌进去的气被持续抽走。

+ * + *

为什么注入 {@code tick} 的开头

+ * + *

{@code MachineAirHandler#tick} 的第一行就是 {@code Direction actualLeakDir = leakDir;} + * —— 在方法开头读一次,之后用它决定要不要 {@code handleAirLeak}、要不要发同步包。所以只要在 + * HEAD 把 {@code leakDir} 清成 null,方块自己从头到尾都不会认为在漏气:既不销毁空气, + * 也不发 {@code PacketUpdatePressureBlock}(客户端因此也没有漏气粒子 / 音效)。

+ * + *

清 {@code leakDir} 走的是公开 API {@link IAirHandlerMachine#setSideLeaking},等价于把那个私有字段 + * 置 null,不需要 {@code @Shadow} 任何私有字段,比直接改字段稳。

+ * + *

不影响什么

+ * + *

{@code leakDir} 只管「未连接漏气」。超压泄压走的是独立的 {@code safetyLeaking / safetyLeakDir}, + * 爆炸判定 {@code doOverpressureChecks} 也不看它;{@code disperseAir}(向真正相邻的空气处理器扩散) + * 同样不受影响。所以超压仍会泄压 / 爆炸,与真正相邻的方块仍会正常交换空气。

+ */ +@Mixin(MachineAirHandler.class) +public abstract class MachineAirHandlerMixin { + + @Inject(method = "tick", at = @At("HEAD")) + private void uselessmod$sealWirelessLink(BlockEntity ownerTE, CallbackInfo ci) { + Level level = ownerTE.getLevel(); + // 顺序要紧:先按侧别早退,保证客户端线程永远不碰服务端集合(单人存档下两者不同线程)。 + if (level == null || level.isClientSide()) { + return; + } + if (PressureSealRegistry.isEmpty()) { + return; + } + if (PressureSealRegistry.isSealed(level, ownerTE.getBlockPos())) { + ((IAirHandlerMachine) (Object) this).setSideLeaking(null); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/mixin/thermal/AugmentableBlockEntityMixin.java b/src/main/java/com/sorrowmist/useless/mixin/thermal/AugmentableBlockEntityMixin.java deleted file mode 100644 index 9fae47c8..00000000 --- a/src/main/java/com/sorrowmist/useless/mixin/thermal/AugmentableBlockEntityMixin.java +++ /dev/null @@ -1,130 +0,0 @@ -package com.sorrowmist.useless.mixin.thermal; - -/* - * This file is part of Thermal Parallel. - * - * Copyright (c) 2025 EtSH-C2H6S - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import cofh.core.util.helpers.AugmentableHelper; -import cofh.lib.common.energy.EnergyStorageCoFH; -import cofh.lib.common.fluid.FluidStorageCoFH; -import cofh.lib.common.fluid.ManagedTankInv; -import cofh.lib.common.inventory.ManagedItemInv; -import cofh.thermal.lib.common.block.entity.AugmentableBlockEntity; -import com.sorrowmist.useless.interfaces.IAugmentableBlockEntityAccessor; -import com.sorrowmist.useless.interfaces.IItemStorageCoFHAccessor; -import com.sorrowmist.useless.utils.ThermalConfigUtils; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.world.item.enchantment.Enchantment; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.Unique; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import org.spongepowered.asm.mixin.injection.callback.LocalCapture; - -import java.util.Map; - -@Mixin(value = AugmentableBlockEntity.class, remap = false) -public abstract class AugmentableBlockEntityMixin implements IAugmentableBlockEntityAccessor { - @Shadow protected CompoundTag augmentNBT; - @Shadow protected ManagedItemInv inventory; - @Shadow protected ManagedTankInv tankInv; - @Shadow protected EnergyStorageCoFH energyStorage; - - @Unique private int useless_mod$parallel; - - public int useless_mod$getParallel() { - return this.useless_mod$parallel; - } - - public void useless_mod$setParallel(int parallel) { - this.useless_mod$parallel = parallel; - } - - @Inject(method = "setAttributesFromAugment", at = @At(value = "INVOKE", target = "Lcofh/core/util/helpers/AugmentableHelper;setAttributeFromAugmentMax(Lnet/minecraft/nbt/CompoundTag;Lnet/minecraft/nbt/CompoundTag;Ljava/lang/String;)V", ordinal = 0, shift = At.Shift.AFTER)) - public void setParallelAttribute(CompoundTag augmentData, CallbackInfo ci) { - AugmentableHelper.setAttributeFromAugmentAdd(this.augmentNBT, augmentData, "MachineParallel"); - } - - @Inject(method = "finalizeAttributes", at = @At("TAIL"), locals = LocalCapture.CAPTURE_FAILHARD) - public void finalizeAttributes(Map enchantmentMap, CallbackInfo ci, float holdingMod, float baseMod, float energyStorageMod, float fluidStorageMod, float itemStorageMod, float xpStorageMod, float energyXferMod, int storedXp, CompoundTag filterNBT) { - int parallel = (int) AugmentableHelper.getAttributeMod(this.augmentNBT, "MachineParallel"); - - if (ThermalConfigUtils.BASE_MOD_AFFECT_PARALLEL) { - parallel = (int) ((float) (parallel + 1) * baseMod - 1.0F); - } - - this.useless_mod$parallel = Math.max(1, parallel); - - // 创建 final 变量用于 lambda 表达式 - final int finalParallel = parallel; - - if (parallel > 0) { - if (ThermalConfigUtils.PARALLEL_INCREASE_ITEM_CAPACITY) { - // 使用 finalParallel 而不是 parallel - this.inventory.getInputSlots().forEach(itemStorageCoFH -> - ((IItemStorageCoFHAccessor) itemStorageCoFH).useless_mod$setParallel(finalParallel + 1)); - this.inventory.getOutputSlots().forEach(itemStorageCoFH -> - ((IItemStorageCoFHAccessor) itemStorageCoFH).useless_mod$setParallel(finalParallel + 1)); - } - - if (ThermalConfigUtils.PARALLEL_INCREASE_FLUID_CAPACITY) { - for (int i = 0; i < this.tankInv.getTanks(); ++i) { - FluidStorageCoFH tank = this.tankInv.getTank(i); - long newCapacity = (long) tank.getCapacity() * (finalParallel + 1); - // 防止整数溢出 - if (newCapacity > Integer.MAX_VALUE) { - newCapacity = Integer.MAX_VALUE; - } - this.tankInv.getTank(i).setCapacity((int) newCapacity); - } - } - - if (ThermalConfigUtils.PARALLEL_INCREASE_ENERGY_CAPACITY) { - long newCapacity = (long) this.energyStorage.getCapacity() * (finalParallel + 1); - // 防止整数溢出 - if (newCapacity > Integer.MAX_VALUE) { - newCapacity = Integer.MAX_VALUE; - } - this.energyStorage.setCapacity((int) newCapacity); - } - - if (ThermalConfigUtils.PARALLEL_INCREASE_ENERGY_TRANSFER) { - long newMaxReceive = (long) this.energyStorage.getMaxReceive() * (finalParallel + 1); - long newMaxExtract = (long) this.energyStorage.getMaxExtract() * (finalParallel + 1); - - // 防止整数溢出 - if (newMaxReceive > Integer.MAX_VALUE) { - newMaxReceive = Integer.MAX_VALUE; - } - if (newMaxExtract > Integer.MAX_VALUE) { - newMaxExtract = Integer.MAX_VALUE; - } - - this.energyStorage.setMaxReceive((int) newMaxReceive); - this.energyStorage.setMaxExtract((int) newMaxExtract); - } - } - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/mixin/thermal/ItemStorageCoFHMixin.java b/src/main/java/com/sorrowmist/useless/mixin/thermal/ItemStorageCoFHMixin.java deleted file mode 100644 index bf50e6ce..00000000 --- a/src/main/java/com/sorrowmist/useless/mixin/thermal/ItemStorageCoFHMixin.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.sorrowmist.useless.mixin.thermal; - -/* - * This file is part of Thermal Parallel. - * - * Copyright (c) 2025 EtSH-C2H6S - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import cofh.lib.common.inventory.ItemStorageCoFH; -import com.sorrowmist.useless.interfaces.IItemStorageCoFHAccessor; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Unique; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; - -@Mixin(value = ItemStorageCoFH.class, remap = false) -public class ItemStorageCoFHMixin implements IItemStorageCoFHAccessor { - @Unique private int useless_mod$parallel; - - public void useless_mod$setParallel(int count) { - this.useless_mod$parallel = count; - } - - public int useless_mod$getParallel() { - return this.useless_mod$parallel; - } - - @Inject(at = @At("RETURN"), method = "getSlotLimit", cancellable = true) - public void modifyItemStorage(int slot, CallbackInfoReturnable cir) { - if (this.useless_mod$parallel > 0) { - long newLimit = (long) cir.getReturnValueI() * (this.useless_mod$parallel + 1); - // 防止整数溢出,同时限制最大堆叠数为 2^31-1 - if (newLimit > Integer.MAX_VALUE) { - newLimit = Integer.MAX_VALUE; - } - cir.setReturnValue((int) newLimit); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/mixin/thermal/MachineBlockEntityMixin.java b/src/main/java/com/sorrowmist/useless/mixin/thermal/MachineBlockEntityMixin.java deleted file mode 100644 index 40bbadff..00000000 --- a/src/main/java/com/sorrowmist/useless/mixin/thermal/MachineBlockEntityMixin.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.sorrowmist.useless.mixin.thermal; - -/* - * This file is part of Thermal Parallel. - * - * Copyright (c) 2025 EtSH-C2H6S - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import cofh.thermal.lib.common.block.entity.MachineBlockEntity; -import cofh.thermal.lib.common.block.entity.Reconfigurable4WayBlockEntity; -import cofh.thermal.lib.util.recipes.MachineProperties; -import com.sorrowmist.useless.interfaces.IAugmentableBlockEntityAccessor; -import com.sorrowmist.useless.utils.ThermalConfigUtils; -import net.minecraft.core.BlockPos; -import net.minecraft.world.level.block.entity.BlockEntityType; -import net.minecraft.world.level.block.state.BlockState; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Mixin(value = MachineBlockEntity.class, remap = false) -public abstract class MachineBlockEntityMixin extends Reconfigurable4WayBlockEntity { - @Shadow protected int processMax; - @Shadow protected int process; - - public MachineBlockEntityMixin(BlockEntityType tileEntityTypeIn, BlockPos pos, BlockState state) { - super(tileEntityTypeIn, pos, state); - } - - @Shadow public abstract MachineProperties getMachineProperties(); - @Shadow protected abstract void resolveOutputs(); - @Shadow protected abstract void resolveInputs(); - @Shadow protected abstract boolean validateInputs(); - @Shadow protected abstract boolean validateOutputs(); - - @Inject(method = "tickServer", at = @At(value = "INVOKE", target = "Lcofh/thermal/lib/common/block/entity/MachineBlockEntity;transferOutput()V", ordinal = 0)) - public void addParallelLogic(CallbackInfo ci) { - int parallel = ((IAugmentableBlockEntityAccessor) this).useless_mod$getParallel(); - if (parallel > 0) { - for (int i = 0; i < parallel; ++i) { - if (!this.validateInputs() || !this.validateOutputs()) { - return; - } - - if (ThermalConfigUtils.PARALLEL_INCREASE_ENERGY_CONSUMPTION) { - // 检查能量是否足够,防止负数溢出 - int energyRequired = this.processMax; - if (energyRequired < 0) { - // 如果 processMax 已经溢出为负数,使用安全值 - energyRequired = Integer.MAX_VALUE / (parallel + 1); - } - - if (this.energyStorage.getEnergyStored() < energyRequired) { - return; - } - - // 安全地减少能量 - int currentEnergy = this.energyStorage.getEnergyStored(); - int newEnergy = currentEnergy - energyRequired; - if (newEnergy < 0) { - newEnergy = 0; - } - this.energyStorage.setEnergyStored(newEnergy); - } - - this.resolveOutputs(); - this.resolveInputs(); - this.markDirtyFast(); - } - } - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/mixin/thermal/Reconfigurable4WayBlockEntityMixin.java b/src/main/java/com/sorrowmist/useless/mixin/thermal/Reconfigurable4WayBlockEntityMixin.java deleted file mode 100644 index de835d17..00000000 --- a/src/main/java/com/sorrowmist/useless/mixin/thermal/Reconfigurable4WayBlockEntityMixin.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.sorrowmist.useless.mixin.thermal; - -/* - * This file is part of Thermal Parallel. - * - * Copyright (c) 2025 EtSH-C2H6S - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import cofh.thermal.lib.common.block.entity.AugmentableBlockEntity; -import cofh.thermal.lib.common.block.entity.Reconfigurable4WayBlockEntity; -import com.sorrowmist.useless.interfaces.IAugmentableBlockEntityAccessor; -import com.sorrowmist.useless.utils.ThermalConfigUtils; -import net.minecraft.core.BlockPos; -import net.minecraft.world.level.block.entity.BlockEntityType; -import net.minecraft.world.level.block.state.BlockState; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; - -@Mixin(value = Reconfigurable4WayBlockEntity.class, remap = false) -public abstract class Reconfigurable4WayBlockEntityMixin extends AugmentableBlockEntity { - public Reconfigurable4WayBlockEntityMixin(BlockEntityType tileEntityTypeIn, BlockPos pos, BlockState state) { - super(tileEntityTypeIn, pos, state); - } - - @Inject(at = @At("RETURN"), method = "getInputItemAmount", cancellable = true) - public void addParallelInput(CallbackInfoReturnable cir) { - if (ThermalConfigUtils.PARALLEL_INCREASE_ITEM_TRANSFER) { - int parallel = ((IAugmentableBlockEntityAccessor) this).useless_mod$getParallel(); - if (parallel > 0) { - cir.setReturnValue(cir.getReturnValueI() * (1 + parallel)); - } - } - } - - @Inject(at = @At("RETURN"), method = "getOutputItemAmount", cancellable = true) - public void addParallelOutput(CallbackInfoReturnable cir) { - if (ThermalConfigUtils.PARALLEL_INCREASE_ITEM_TRANSFER) { - int parallel = ((IAugmentableBlockEntityAccessor) this).useless_mod$getParallel(); - if (parallel > 0) { - cir.setReturnValue(cir.getReturnValueI() * (1 + parallel)); - } - } - } - - @Inject(at = @At("RETURN"), method = "getInputFluidAmount", cancellable = true) - public void addParallelInputFluid(CallbackInfoReturnable cir) { - if (ThermalConfigUtils.PARALLEL_INCREASE_FLUID_TRANSFER) { - int parallel = ((IAugmentableBlockEntityAccessor) this).useless_mod$getParallel(); - if (parallel > 0) { - cir.setReturnValue(cir.getReturnValueI() * (1 + parallel)); - } - } - } - - @Inject(at = @At("RETURN"), method = "getOutputFluidAmount", cancellable = true) - public void addParallelOutputFluid(CallbackInfoReturnable cir) { - if (ThermalConfigUtils.PARALLEL_INCREASE_FLUID_TRANSFER) { - int parallel = ((IAugmentableBlockEntityAccessor) this).useless_mod$getParallel(); - if (parallel > 0) { - cir.setReturnValue(cir.getReturnValueI() * (1 + parallel)); - } - } - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/mixin/thermal/ThermalAugmentRulesMixin.java b/src/main/java/com/sorrowmist/useless/mixin/thermal/ThermalAugmentRulesMixin.java deleted file mode 100644 index e9d8db4b..00000000 --- a/src/main/java/com/sorrowmist/useless/mixin/thermal/ThermalAugmentRulesMixin.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.sorrowmist.useless.mixin.thermal; - -/* - * This file is part of Thermal Parallel. - * - * Copyright (c) 2025 EtSH-C2H6S - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import cofh.thermal.lib.util.ThermalAugmentRules; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; - -@Mixin(value = ThermalAugmentRules.class, remap = false) -public class ThermalAugmentRulesMixin { - @Inject(at = @At("RETURN"), method = "isAdditive", cancellable = true) - private static void setCustomAdditive(String mod, CallbackInfoReturnable cir) { - if (!cir.getReturnValueZ()) { - cir.setReturnValue("MachineParallel".equals(mod)); - } - } - - @Inject(at = @At("RETURN"), method = "isInteger", cancellable = true) - private static void setCustomInt(String mod, CallbackInfoReturnable cir) { - if (!cir.getReturnValueZ()) { - cir.setReturnValue("MachineParallel".equals(mod)); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/modes/ModeManager.java b/src/main/java/com/sorrowmist/useless/modes/ModeManager.java deleted file mode 100644 index ce37c283..00000000 --- a/src/main/java/com/sorrowmist/useless/modes/ModeManager.java +++ /dev/null @@ -1,216 +0,0 @@ -package com.sorrowmist.useless.modes; - -import net.minecraft.nbt.CompoundTag; -import net.minecraft.world.item.ItemStack; - -import java.util.*; - -/** - * 模式管理器,处理模式的激活状态和互斥逻辑 - */ -public class ModeManager { - // 互斥模式组:同一组内的模式不能同时激活 - private static final Set> MUTUAL_EXCLUSION_GROUPS = new HashSet<>(); - - static { - // 初始化互斥模式组 - // 精准采集和时运是互斥的 - Set exclusiveGroup1 = new HashSet<>(); - exclusiveGroup1.add(ToolMode.SILK_TOUCH); - exclusiveGroup1.add(ToolMode.FORTUNE); - MUTUAL_EXCLUSION_GROUPS.add(exclusiveGroup1); - - // 六种工具模式是互斥的 - Set exclusiveGroup2 = new HashSet<>(); - exclusiveGroup2.add(ToolMode.WRENCH_MODE); - exclusiveGroup2.add(ToolMode.SCREWDRIVER_MODE); - exclusiveGroup2.add(ToolMode.MALLET_MODE); - exclusiveGroup2.add(ToolMode.CROWBAR_MODE); - exclusiveGroup2.add(ToolMode.HAMMER_MODE); - exclusiveGroup2.add(ToolMode.OMNITOOL_MODE); - MUTUAL_EXCLUSION_GROUPS.add(exclusiveGroup2); - } - - private final Map activeModes; - - /** - * 构造一个新的模式管理器 - */ - public ModeManager() { - this.activeModes = new EnumMap<>(ToolMode.class); - // 初始化所有模式为非激活状态 - for (ToolMode mode : ToolMode.values()) { - activeModes.put(mode, false); - } - } - - /** - * 从物品栈加载模式状态 - */ - public void loadFromStack(ItemStack stack) { - CompoundTag tag = stack.getOrCreateTag(); - CompoundTag modesTag = tag.getCompound("ToolModes"); - - for (ToolMode mode : ToolMode.values()) { - boolean isActive = modesTag.getBoolean(mode.getName()); - activeModes.put(mode, isActive); - } - - // 确保互斥模式的一致性 - ensureMutualExclusion(); - } - - /** - * 将模式状态保存到物品栈 - */ - public void saveToStack(ItemStack stack) { - CompoundTag tag = stack.getOrCreateTag(); - CompoundTag modesTag = new CompoundTag(); - - for (Map.Entry entry : activeModes.entrySet()) { - modesTag.putBoolean(entry.getKey().getName(), entry.getValue()); - } - - tag.put("ToolModes", modesTag); - } - - /** - * 切换指定模式的激活状态 - */ - public void toggleMode(ToolMode mode) { - // 特殊处理精准采集和时运模式的互斥切换 - if ((mode == ToolMode.SILK_TOUCH || mode == ToolMode.FORTUNE)) { - // 如果当前模式是激活的,切换到另一个模式 - if (activeModes.get(mode)) { - // 切换到相反的模式 - ToolMode oppositeMode = mode == ToolMode.SILK_TOUCH ? ToolMode.FORTUNE : ToolMode.SILK_TOUCH; - activeModes.put(mode, false); - activeModes.put(oppositeMode, true); - } else { - // 如果当前模式是未激活的,激活它并禁用另一个模式 - activeModes.put(mode, true); - ToolMode oppositeMode = mode == ToolMode.SILK_TOUCH ? ToolMode.FORTUNE : ToolMode.SILK_TOUCH; - activeModes.put(oppositeMode, false); - } - } else { - // 其他模式的正常切换逻辑 - boolean isActive = activeModes.get(mode); - activeModes.put(mode, !isActive); - - // 处理互斥模式 - handleMutualExclusion(mode); - } - } - - /** - * 设置指定模式的激活状态 - */ - public void setModeActive(ToolMode mode, boolean active) { - // 特殊处理精准采集和时运模式的互斥设置 - if ((mode == ToolMode.SILK_TOUCH || mode == ToolMode.FORTUNE)) { - // 如果要激活当前模式,直接激活并禁用另一个模式 - activeModes.put(mode, active); - ToolMode oppositeMode = mode == ToolMode.SILK_TOUCH ? ToolMode.FORTUNE : ToolMode.SILK_TOUCH; - activeModes.put(oppositeMode, !active); - } else { - // 其他模式的正常设置逻辑 - activeModes.put(mode, active); - - // 处理互斥模式 - if (active) { - handleMutualExclusion(mode); - } - } - } - - /** - * 检查指定模式是否激活 - */ - public boolean isModeActive(ToolMode mode) { - return activeModes.getOrDefault(mode, false); - } - - /** - * 获取所有激活的模式 - */ - public Set getActiveModes() { - Set result = new HashSet<>(); - for (Map.Entry entry : activeModes.entrySet()) { - if (entry.getValue()) { - result.add(entry.getKey()); - } - } - return result; - } - - /** - * 处理互斥模式逻辑 - */ - private void handleMutualExclusion(ToolMode activatedMode) { - if (!activeModes.get(activatedMode)) { - return; // 如果模式没有被激活,不需要处理 - } - - // 查找包含当前模式的互斥组 - for (Set group : MUTUAL_EXCLUSION_GROUPS) { - if (group.contains(activatedMode)) { - // 禁用同一组内的其他模式 - for (ToolMode mode : group) { - if (mode != activatedMode) { - activeModes.put(mode, false); - } - } - } - } - } - - /** - * 确保所有互斥模式组的一致性 - */ - private void ensureMutualExclusion() { - // 检查每个互斥组 - for (Set group : MUTUAL_EXCLUSION_GROUPS) { - // 计算组内激活的模式数量 - int activeCount = 0; - ToolMode lastActive = null; - - for (ToolMode mode : group) { - if (activeModes.get(mode)) { - activeCount++; - lastActive = mode; - } - } - - // 如果有多个模式被激活,只保留最后一个 - if (activeCount > 1) { - for (ToolMode mode : group) { - activeModes.put(mode, mode == lastActive); - } - } else if (activeCount == 0 && group.size() >= 2) { - // 如果没有模式被激活,并且是精准采集和时运的互斥组,默认激活时运模式 - boolean isSilkTouchFortuneGroup = group.contains(ToolMode.SILK_TOUCH) && group.contains(ToolMode.FORTUNE); - if (isSilkTouchFortuneGroup) { - // 默认激活时运模式 - activeModes.put(ToolMode.FORTUNE, true); - activeModes.put(ToolMode.SILK_TOUCH, false); - } - } - } - } - - /** - * 获取模式的总数 - */ - public int getTotalModes() { - return ToolMode.getTotalModes(); - } - - /** - * 复制模式管理器的状态 - */ - public ModeManager copy() { - ModeManager copy = new ModeManager(); - copy.activeModes.putAll(this.activeModes); - return copy; - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/modes/ModeWheelRenderer.java b/src/main/java/com/sorrowmist/useless/modes/ModeWheelRenderer.java deleted file mode 100644 index 6e3c13e6..00000000 --- a/src/main/java/com/sorrowmist/useless/modes/ModeWheelRenderer.java +++ /dev/null @@ -1,289 +0,0 @@ -package com.sorrowmist.useless.modes; - -import com.mojang.blaze3d.vertex.PoseStack; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.Font; -import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.network.chat.Component; -import net.minecraft.world.item.ItemStack; - -/** - * 模式圆盘渲染器,负责绘制模式选择圆盘和处理交互 - */ -public class ModeWheelRenderer { - // 圆盘的半径 - private static final int WHEEL_RADIUS = 120; - // 圆盘的内半径(用于绘制中心区域) - private static final int INNER_RADIUS = 30; - // 每个模式扇形区域的角度(弧度) - private static final float SEGMENT_ANGLE = (float) (2 * Math.PI / ToolMode.getTotalModes()); - // 高亮颜色(绿色) - private static final int HIGHLIGHT_COLOR = 0x4000FF00; - // 非高亮颜色(灰色) - private static final int NORMAL_COLOR = 0x80808080; - // 激活模式颜色(亮绿色) - private static final int ACTIVE_COLOR = 0x8000FF00; - // 鼠标悬停的模式索引 - private int hoveredModeIndex = -1; - // 模式管理器 - private final ModeManager modeManager; - - /** - * 构造一个新的模式圆盘渲染器 - * - * @param modeManager 模式管理器 - */ - public ModeWheelRenderer(ModeManager modeManager) { - this.modeManager = modeManager; - } - - /** - * 渲染模式选择圆盘 - * - * @param guiGraphics GUI 图形上下文 - * @param mouseX 鼠标 X 坐标 - * @param mouseY 鼠标 Y 坐标 - * @param screenWidth 屏幕宽度 - * @param screenHeight 屏幕高度 - */ - public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, int screenWidth, int screenHeight) { - // 计算圆盘中心坐标 - int centerX = screenWidth / 2; - int centerY = screenHeight / 2; - - // 更新鼠标悬停的模式 - updateHoveredMode(mouseX, mouseY, centerX, centerY); - - // 绘制一个非常简单的模式轮盘,避免任何可能导致黑屏的复杂绘制 - Font font = Minecraft.getInstance().font; - - // 绘制中心文字 - Component centerText = Component.literal("G"); - int textWidth = font.width(centerText); - int textHeight = font.lineHeight; - guiGraphics.drawString(font, centerText, centerX - textWidth / 2, centerY - textHeight / 2, 0x000000, false); - - // 绘制简单的模式列表,以文字形式显示 - int yOffset = 0; - for (ToolMode mode : ToolMode.values()) { - Component modeText = mode.getTooltip(); - int modeTextWidth = font.width(modeText); - - // 根据模式是否激活和是否悬停设置颜色 - int color = 0xFFFFFF; - if (modeManager.isModeActive(mode)) { - color = 0x00FF00; // 激活模式为绿色 - } - if (hoveredModeIndex == mode.getIndex()) { - color = 0xFFFF00; // 悬停模式为黄色 - } - - // 绘制模式文字 - guiGraphics.drawString(font, modeText, centerX - modeTextWidth / 2, centerY + 20 + yOffset, color, false); - yOffset += 12; - } - } - - /** - * 绘制模式扇形区域 - */ - private void renderModeSegments(GuiGraphics guiGraphics, int centerX, int centerY) { - PoseStack poseStack = guiGraphics.pose(); - - for (int i = 0; i < ToolMode.getTotalModes(); i++) { - ToolMode mode = ToolMode.byIndex(i); - float startAngle = i * SEGMENT_ANGLE - SEGMENT_ANGLE / 2; - float endAngle = (i + 1) * SEGMENT_ANGLE - SEGMENT_ANGLE / 2; - - // 确定扇形区域的颜色 - int color; - if (modeManager.isModeActive(mode)) { - color = ACTIVE_COLOR; - } else if (i == hoveredModeIndex) { - color = HIGHLIGHT_COLOR; - } else { - color = NORMAL_COLOR; - } - - // 绘制扇形区域 - drawSector(guiGraphics, centerX, centerY, startAngle, endAngle, WHEEL_RADIUS, INNER_RADIUS, color); - } - } - - /** - * 绘制中心区域 - */ - private void renderCenter(GuiGraphics guiGraphics, int centerX, int centerY) { - // 绘制中心白色圆形 - drawCircle(guiGraphics, centerX, centerY, INNER_RADIUS, 0xFFFFFFFF); - - // 绘制中心文字 - Font font = Minecraft.getInstance().font; - Component text = Component.literal("G"); - int textWidth = font.width(text); - int textHeight = font.lineHeight; - guiGraphics.drawString(font, text, centerX - textWidth / 2, centerY - textHeight / 2, 0x000000, false); - } - - /** - * 绘制圆形 - */ - private void drawCircle(GuiGraphics guiGraphics, int centerX, int centerY, int radius, int color) { - // 使用正方形填充近似圆形 - int diameter = radius * 2; - guiGraphics.fill(centerX - radius, centerY - radius, centerX + radius, centerY + radius, color); - } - - /** - * 绘制模式名称 - */ - private void renderModeNames(GuiGraphics guiGraphics, int centerX, int centerY) { - Font font = Minecraft.getInstance().font; - - for (int i = 0; i < ToolMode.getTotalModes(); i++) { - ToolMode mode = ToolMode.byIndex(i); - Component name = mode.getTooltip(); - float angle = (i + 0.5F) * SEGMENT_ANGLE - SEGMENT_ANGLE / 2; - - // 计算文字位置 - int textRadius = WHEEL_RADIUS + 10; - int textX = (int) (centerX + Math.cos(angle) * textRadius - font.width(name) / 2); - int textY = (int) (centerY + Math.sin(angle) * textRadius - font.lineHeight / 2); - - // 绘制文字 - guiGraphics.drawString(font, name, textX, textY, 0xFFFFFF, false); - } - } - - /** - * 绘制扇形区域 - */ - private void drawSector(GuiGraphics guiGraphics, int centerX, int centerY, float startAngle, float endAngle, int outerRadius, int innerRadius, int color) { - // 使用简单的线条绘制扇形轮廓,避免大面积填充导致黑屏 - int segments = 8; // 减少分段数,提高性能 - - // 绘制外圆弧 - for (int i = 0; i < segments; i++) { - float currentAngle = startAngle + (endAngle - startAngle) * i / segments; - float nextAngle = startAngle + (endAngle - startAngle) * (i + 1) / segments; - - int x1 = (int) (centerX + Math.cos(currentAngle) * outerRadius); - int y1 = (int) (centerY + Math.sin(currentAngle) * outerRadius); - int x2 = (int) (centerX + Math.cos(nextAngle) * outerRadius); - int y2 = (int) (centerY + Math.sin(nextAngle) * outerRadius); - - // 绘制外圆弧线段 - drawLine(guiGraphics, x1, y1, x2, y2, 0xFFFFFFFF); - } - - // 绘制内圆弧 - for (int i = 0; i < segments; i++) { - float currentAngle = startAngle + (endAngle - startAngle) * i / segments; - float nextAngle = startAngle + (endAngle - startAngle) * (i + 1) / segments; - - int x1 = (int) (centerX + Math.cos(currentAngle) * innerRadius); - int y1 = (int) (centerY + Math.sin(currentAngle) * innerRadius); - int x2 = (int) (centerX + Math.cos(nextAngle) * innerRadius); - int y2 = (int) (centerY + Math.sin(nextAngle) * innerRadius); - - // 绘制内圆弧线段 - drawLine(guiGraphics, x1, y1, x2, y2, 0xFFFFFFFF); - } - - // 绘制两条半径线 - int outerX1 = (int) (centerX + Math.cos(startAngle) * outerRadius); - int outerY1 = (int) (centerY + Math.sin(startAngle) * outerRadius); - int innerX1 = (int) (centerX + Math.cos(startAngle) * innerRadius); - int innerY1 = (int) (centerY + Math.sin(startAngle) * innerRadius); - drawLine(guiGraphics, outerX1, outerY1, innerX1, innerY1, 0xFFFFFFFF); - - int outerX2 = (int) (centerX + Math.cos(endAngle) * outerRadius); - int outerY2 = (int) (centerY + Math.sin(endAngle) * outerRadius); - int innerX2 = (int) (centerX + Math.cos(endAngle) * innerRadius); - int innerY2 = (int) (centerY + Math.sin(endAngle) * innerRadius); - drawLine(guiGraphics, outerX2, outerY2, innerX2, innerY2, 0xFFFFFFFF); - } - - /** - * 绘制线段的辅助方法 - */ - private void drawLine(GuiGraphics guiGraphics, int x1, int y1, int x2, int y2, int color) { - // 使用填充矩形模拟线段 - // 这种方式更简单,兼容性更好 - int dx = Math.abs(x2 - x1); - int dy = Math.abs(y2 - y1); - - if (dx > dy) { - // 水平线 - int minX = Math.min(x1, x2); - int maxX = Math.max(x1, x2); - int y = y1; - guiGraphics.fill(minX, y, maxX + 1, y + 1, color); - } else { - // 垂直线 - int minY = Math.min(y1, y2); - int maxY = Math.max(y1, y2); - int x = x1; - guiGraphics.fill(x, minY, x + 1, maxY + 1, color); - } - } - - /** - * 更新鼠标悬停的模式 - */ - private void updateHoveredMode(int mouseX, int mouseY, int centerX, int centerY) { - // 计算鼠标到中心的距离 - double dx = mouseX - centerX; - double dy = mouseY - centerY; - double distance = Math.sqrt(dx * dx + dy * dy); - - // 如果鼠标在圆盘范围内 - if (distance > INNER_RADIUS && distance < WHEEL_RADIUS) { - // 计算鼠标相对于中心的角度 - double angle = Math.atan2(dy, dx); - if (angle < 0) { - angle += 2 * Math.PI; - } - - // 计算当前角度对应的模式索引 - int modeIndex = (int) ((angle + SEGMENT_ANGLE / 2) / SEGMENT_ANGLE) % ToolMode.getTotalModes(); - hoveredModeIndex = modeIndex; - } else { - hoveredModeIndex = -1; - } - } - - /** - * 处理鼠标点击事件 - * - * @param mouseX 鼠标 X 坐标 - * @param mouseY 鼠标 Y 坐标 - * @param centerX 圆盘中心 X 坐标 - * @param centerY 圆盘中心 Y 坐标 - * @param itemStack 工具物品栈 - * @return 是否处理了点击事件 - */ - public boolean handleMouseClick(int mouseX, int mouseY, int centerX, int centerY, ItemStack itemStack) { - // 如果鼠标悬停在某个模式上 - if (hoveredModeIndex != -1) { - // 获取对应的模式 - ToolMode mode = ToolMode.byIndex(hoveredModeIndex); - - // 切换模式激活状态 - modeManager.toggleMode(mode); - - // 保存模式状态到物品栈 - modeManager.saveToStack(itemStack); - - return true; - } - return false; - } - - /** - * 获取当前鼠标悬停的模式索引 - */ - public int getHoveredModeIndex() { - return hoveredModeIndex; - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/modes/ToolMode.java b/src/main/java/com/sorrowmist/useless/modes/ToolMode.java deleted file mode 100644 index c4363d9c..00000000 --- a/src/main/java/com/sorrowmist/useless/modes/ToolMode.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.sorrowmist.useless.modes; - -import net.minecraft.network.chat.Component; - -/** - * 工具模式枚举,定义所有可用的工具模式 - */ -public enum ToolMode { - // 精准采集模式 - SILK_TOUCH("silk_touch", "tooltip.useless_mod.silk_touch_mode", 0), - // 时运模式 - FORTUNE("fortune", "tooltip.useless_mod.fortune_mode", 1), - // 连锁挖掘模式 - CHAIN_MINING("chain_mining", "tooltip.useless_mod.chain_mining_mode", 2), - // 增强连锁挖掘模式 - ENHANCED_CHAIN_MINING("enhanced_chain_mining", "tooltip.useless_mod.enhanced_chain_mining_mode", 3), - // 扳手模式 - WRENCH_MODE("wrench_mode", "tooltip.useless_mod.wrench_mode", 4), - // 螺丝刀模式 - SCREWDRIVER_MODE("screwdriver_mode", "tooltip.useless_mod.screwdriver_mode", 5), - // 锤子模式 - MALLET_MODE("mallet_mode", "tooltip.useless_mod.mallet_mode", 6), - // 撬棍模式 - CROWBAR_MODE("crowbar_mode", "tooltip.useless_mod.crowbar_mode", 7), - // 铁锤模式 - HAMMER_MODE("hammer_mode", "tooltip.useless_mod.hammer_mode", 8), - // Omnitool模式 - OMNITOOL_MODE("omnitool_mode", "tooltip.useless_mod.omnitool_mode", 9), - // 强制挖掘模式 - FORCE_MINING("force_mining", "tooltip.useless_mod.force_mining_mode", 10), - // AE存储优先模式 - AE_STORAGE_PRIORITY("ae_storage_priority", "tooltip.useless_mod.ae_storage_priority_mode", 11); - - private final String name; - private final String tooltipKey; - private final int index; - - ToolMode(String name, String tooltipKey, int index) { - this.name = name; - this.tooltipKey = tooltipKey; - this.index = index; - } - - /** - * 获取模式的名称 - */ - public String getName() { - return name; - } - - /** - * 获取模式的索引 - */ - public int getIndex() { - return index; - } - - /** - * 获取模式的 tooltip 文本 - */ - public Component getTooltip() { - return Component.translatable(tooltipKey); - } - - /** - * 通过索引获取模式 - */ - public static ToolMode byIndex(int index) { - for (ToolMode mode : values()) { - if (mode.index == index) { - return mode; - } - } - return SILK_TOUCH; // 默认返回精准采集模式 - } - - /** - * 获取模式的总数 - */ - public static int getTotalModes() { - return values().length; - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/network/AECancelPacket.java b/src/main/java/com/sorrowmist/useless/network/AECancelPacket.java new file mode 100644 index 00000000..c1d72300 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/AECancelPacket.java @@ -0,0 +1,51 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.blockentities.AdvancedAlloyFurnaceBlockEntity; +import com.sorrowmist.useless.content.blockentities.multiblock.MultiblockAlloyFurnaceCoreBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.block.entity.BlockEntity; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +/** + * 客户端→服务器:取消合金炉所有当前 AE 合成任务并返还材料。 + */ +public class AECancelPacket implements CustomPacketPayload { + + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "ae_cancel")); + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, pkt) -> buf.writeBlockPos(pkt.pos), + buf -> new AECancelPacket(buf.readBlockPos()) + ); + private final BlockPos pos; + + public AECancelPacket(BlockPos pos) { + this.pos = pos; + } + + public static void handle(AECancelPacket msg, IPayloadContext ctx) { + ctx.enqueueWork(() -> { + ServerPlayer player = (ServerPlayer) ctx.player(); + if (player == null) return; + + BlockEntity be = player.level().getBlockEntity(msg.pos); + if (be instanceof AdvancedAlloyFurnaceBlockEntity furnace) { + furnace.cancelAllAETasks(); + } else if (be instanceof MultiblockAlloyFurnaceCoreBlockEntity furnace) { + furnace.cancelAllAETasks(); + } + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/AEReturnOutputTogglePacket.java b/src/main/java/com/sorrowmist/useless/network/AEReturnOutputTogglePacket.java new file mode 100644 index 00000000..bde7e06f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/AEReturnOutputTogglePacket.java @@ -0,0 +1,48 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.blockentities.AdvancedAlloyFurnaceBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.block.entity.BlockEntity; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +/** + * 客户端→服务器:切换合金炉产物是否回 AE 网络。 + */ +public class AEReturnOutputTogglePacket implements CustomPacketPayload { + + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "ae_return_output_toggle")); + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, pkt) -> buf.writeBlockPos(pkt.pos), + buf -> new AEReturnOutputTogglePacket(buf.readBlockPos()) + ); + private final BlockPos pos; + + public AEReturnOutputTogglePacket(BlockPos pos) { + this.pos = pos; + } + + public static void handle(AEReturnOutputTogglePacket msg, IPayloadContext ctx) { + ctx.enqueueWork(() -> { + ServerPlayer player = (ServerPlayer) ctx.player(); + if (player == null) return; + + BlockEntity be = player.level().getBlockEntity(msg.pos); + if (be instanceof AdvancedAlloyFurnaceBlockEntity furnace) { + furnace.setReturnOutputToAe(!furnace.isReturnOutputToAe()); + } + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/AETaskProgressPacket.java b/src/main/java/com/sorrowmist/useless/network/AETaskProgressPacket.java new file mode 100644 index 00000000..4cb096a7 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/AETaskProgressPacket.java @@ -0,0 +1,100 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.client.network.ClientPacketHandlers; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.fml.loading.FMLEnvironment; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +public record AETaskProgressPacket(BlockPos pos, List tasks) implements CustomPacketPayload { + public AETaskProgressPacket { + pos = pos.immutable(); + tasks = List.copyOf(tasks); + } + + public static class TaskProgressData { + public final String productName; + public final int progress; + public final int maxProgress; + public final long craftCount; + public final long totalOutputCount; + public final String statusKey; + public final String statusDetail; + + public TaskProgressData(String productName, int progress, int maxProgress, long craftCount, long totalOutputCount) { + this(productName, progress, maxProgress, craftCount, totalOutputCount, + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.processing", ""); + } + + public TaskProgressData(String productName, int progress, int maxProgress, long craftCount, long totalOutputCount, String statusKey, String statusDetail) { + this.productName = productName; + this.progress = progress; + this.maxProgress = maxProgress; + this.craftCount = craftCount; + this.totalOutputCount = totalOutputCount; + this.statusKey = statusKey; + this.statusDetail = statusDetail; + } + } + + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, pkt) -> pkt.encode(buf), + buf -> decode(buf) + ); + + public static final Type TYPE = + new Type<>(new ResourceLocation(UselessMod.MODID, "ae_task_progress")); + + private void encode(FriendlyByteBuf buf) { + buf.writeBlockPos(pos); + buf.writeInt(tasks.size()); + for (TaskProgressData task : tasks) { + buf.writeUtf(task.productName); + buf.writeInt(task.progress); + buf.writeInt(task.maxProgress); + buf.writeLong(task.craftCount); + buf.writeLong(task.totalOutputCount); + buf.writeUtf(task.statusKey); + buf.writeUtf(task.statusDetail); + } + } + + public static AETaskProgressPacket decode(FriendlyByteBuf buf) { + BlockPos pos = buf.readBlockPos(); + int size = buf.readInt(); + List tasks = new ArrayList<>(); + for (int i = 0; i < size; i++) { + String productName = buf.readUtf(); + int progress = buf.readInt(); + int maxProgress = buf.readInt(); + long craftCount = buf.readLong(); + long totalOutputCount = buf.readLong(); + String statusKey = buf.readUtf(); + String statusDetail = buf.readUtf(); + tasks.add(new TaskProgressData(productName, progress, maxProgress, craftCount, totalOutputCount, statusKey, statusDetail)); + } + return new AETaskProgressPacket(pos, tasks); + } + + public static void handle(AETaskProgressPacket msg, IPayloadContext ctx) { + // 客户端类型统一收敛到 ClientPacketHandlers,避免专用服务器加载类时解析到客户端类。 + if (FMLEnvironment.dist != Dist.CLIENT) { + return; + } + ctx.enqueueWork(() -> ClientPacketHandlers.handleAETaskProgress(msg)); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/AETaskProgressRequestPacket.java b/src/main/java/com/sorrowmist/useless/network/AETaskProgressRequestPacket.java new file mode 100644 index 00000000..6d082d94 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/AETaskProgressRequestPacket.java @@ -0,0 +1,49 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.blockentities.multiblock.MultiblockAlloyFurnaceCoreBlockEntity; +import com.sorrowmist.useless.content.menus.MultiblockAlloyFurnaceMenu; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +public record AETaskProgressRequestPacket(int containerId, BlockPos pos) + implements CustomPacketPayload { + public static final Type TYPE = new Type<>( + UselessMod.id("ae_task_progress_request")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.of((buffer, packet) -> { + buffer.writeVarInt(packet.containerId); + buffer.writeBlockPos(packet.pos); + }, buffer -> new AETaskProgressRequestPacket( + buffer.readVarInt(), buffer.readBlockPos())); + + public AETaskProgressRequestPacket { + pos = pos.immutable(); + } + + public static void handle(AETaskProgressRequestPacket packet, IPayloadContext context) { + context.enqueueWork(() -> { + if (!(context.player() instanceof ServerPlayer player) + || !(player.containerMenu instanceof MultiblockAlloyFurnaceMenu menu) + || menu.containerId != packet.containerId + || !menu.getBlockPos().equals(packet.pos) + || !menu.stillValid(player)) { + return; + } + MultiblockAlloyFurnaceCoreBlockEntity core = menu.getCore(); + if (core != null && core.getBlockPos().equals(packet.pos)) { + core.sendAETaskProgressToPlayer(player); + } + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/AeLinkPreviewPacket.java b/src/main/java/com/sorrowmist/useless/network/AeLinkPreviewPacket.java new file mode 100644 index 00000000..438c2327 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/AeLinkPreviewPacket.java @@ -0,0 +1,97 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.client.render.AeLinkHighlightRenderer; +import com.sorrowmist.useless.compat.ae.AeDeviceLinker; +import com.sorrowmist.useless.core.component.UComponents; +import com.sorrowmist.useless.utils.UselessItemUtils; +import net.minecraft.core.BlockPos; +import net.minecraft.core.GlobalPos; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.fml.loading.FMLEnvironment; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +/** + * 服务端把「工具绑定的访问点 + 它连了哪些机器」发给客户端,用于手持杖子时的连接提示。 + * + *

不带 request id:提示是幂等的「最新值覆盖」,客户端只关心当前这个画面该画哪些方块。

+ */ +public record AeLinkPreviewPacket(ResourceLocation machineDimension, + BlockPos accessPoint, + List machines) implements CustomPacketPayload { + private static final int MAX_MACHINES = 1024; + + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "ae_link_preview")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.of( + (buf, packet) -> { + buf.writeResourceLocation(packet.machineDimension); + buf.writeBlockPos(packet.accessPoint); + buf.writeVarInt(packet.machines.size()); + for (BlockPos pos : packet.machines) buf.writeBlockPos(pos); + }, + buf -> { + ResourceLocation machineDimension = buf.readResourceLocation(); + BlockPos accessPoint = buf.readBlockPos(); + int size = buf.readVarInt(); + if (size < 0 || size > MAX_MACHINES) { + throw new IllegalArgumentException("Invalid AE link preview size: " + size); + } + List machines = new ArrayList<>(size); + for (int i = 0; i < size; i++) machines.add(buf.readBlockPos()); + return new AeLinkPreviewPacket(machineDimension, accessPoint, machines); + } + ); + + public AeLinkPreviewPacket { + if (machines.size() > MAX_MACHINES) { + throw new IllegalArgumentException("AE link preview is too large"); + } + machines = List.copyOf(machines); + } + + /** 服务端:读玩家手里的造化杖 → 取绑定坐标 → 把该访问点连着的机器回给客户端。 */ + public static void answer(ServerPlayer player) { + if (!(player.level() instanceof ServerLevel level)) { + return; + } + var toolEntry = UselessItemUtils.findTargetToolInHands(player); + if (toolEntry.isEmpty()) { + return; + } + ItemStack tool = toolEntry.get().getKey(); + GlobalPos bound = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(tool, UComponents.WIRELESS_LINK_TARGET.get()); + if (bound == null) { + return; + } + PacketDistributor.sendToPlayer(player, new AeLinkPreviewPacket( + level.dimension().location(), + bound.pos(), + AeDeviceLinker.linkedMachines(level, bound.dimension(), bound.pos()))); + } + + public static void handle(AeLinkPreviewPacket packet, IPayloadContext context) { + if (FMLEnvironment.dist == Dist.CLIENT) { + context.enqueueWork(() -> AeLinkHighlightRenderer.setLinks( + packet.machineDimension(), packet.accessPoint(), packet.machines())); + } + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/AeLinkPreviewRequestPacket.java b/src/main/java/com/sorrowmist/useless/network/AeLinkPreviewRequestPacket.java new file mode 100644 index 00000000..1e0fa7b3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/AeLinkPreviewRequestPacket.java @@ -0,0 +1,35 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +/** + * 客户端请求「我手上这把造化杖绑定的访问点,已经连了哪些机器」。 + * + *

不带参数:服务端自己从玩家手里找工具、读绑定坐标,客户端无从伪造目标。

+ */ +public record AeLinkPreviewRequestPacket() implements CustomPacketPayload { + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "ae_link_preview_request")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.unit(new AeLinkPreviewRequestPacket()); + + public static void handle(AeLinkPreviewRequestPacket packet, IPayloadContext context) { + context.enqueueWork(() -> { + if (context.player() instanceof ServerPlayer player) { + AeLinkPreviewPacket.answer(player); + } + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/AlloyFurnaceErrorHighlightPacket.java b/src/main/java/com/sorrowmist/useless/network/AlloyFurnaceErrorHighlightPacket.java new file mode 100644 index 00000000..ce258dea --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/AlloyFurnaceErrorHighlightPacket.java @@ -0,0 +1,61 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.client.network.ClientPacketHandlers; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.fml.loading.FMLEnvironment; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * 服务端把「未成型核心当前最低的那个错误方块」发给附近客户端,用于世界线框高亮。 + * + *

只发一条最低错误而不是全部:玩家按从下往上的顺序修,一次盯一个方块就够了。 + * {@code errorPos} 为 {@code null} 表示当前没有可高亮的错误,客户端据此清掉该核心的残留框。

+ * + *

不带 request id:提示是幂等的「最新值覆盖」。核心坐标 {@code corePos} 即客户端缓存的主键。

+ */ +public record AlloyFurnaceErrorHighlightPacket(BlockPos corePos, + @Nullable BlockPos errorPos) implements CustomPacketPayload { + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "alloy_furnace_error_highlight")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.of( + (buf, packet) -> { + buf.writeBlockPos(packet.corePos); + buf.writeBoolean(packet.errorPos != null); + if (packet.errorPos != null) { + buf.writeBlockPos(packet.errorPos); + } + }, + buf -> { + BlockPos corePos = buf.readBlockPos(); + BlockPos errorPos = buf.readBoolean() ? buf.readBlockPos() : null; + return new AlloyFurnaceErrorHighlightPacket(corePos, errorPos); + }); + + public AlloyFurnaceErrorHighlightPacket { + corePos = corePos.immutable(); + errorPos = errorPos == null ? null : errorPos.immutable(); + } + + public static void handle(AlloyFurnaceErrorHighlightPacket packet, IPayloadContext context) { + // 客户端类型收敛到 ClientPacketHandlers:本类会在专用服务端被加载, + // 直接引用客户端渲染器会让校验器在服务端解析到客户端类型。 + if (FMLEnvironment.dist != Dist.CLIENT) { + return; + } + context.enqueueWork(() -> ClientPacketHandlers.handleAlloyFurnaceErrorHighlight(packet)); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/AutoIOChangePacket.java b/src/main/java/com/sorrowmist/useless/network/AutoIOChangePacket.java new file mode 100644 index 00000000..5f68bfd7 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/AutoIOChangePacket.java @@ -0,0 +1,61 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.blockentities.AdvancedAlloyFurnaceBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.block.entity.BlockEntity; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +/** + * 客户端→服务器:切换自动输入或自动输出开关。 + */ +public class AutoIOChangePacket implements CustomPacketPayload { + + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "auto_io_change")); + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, pkt) -> { + buf.writeBlockPos(pkt.pos); + buf.writeBoolean(pkt.isOutput); + }, + buf -> new AutoIOChangePacket(buf.readBlockPos(), buf.readBoolean()) + ); + private final BlockPos pos; + private final boolean isOutput; + + /** + * @param pos 方块位置 + * @param isOutput true=自动输出, false=自动输入 + */ + public AutoIOChangePacket(BlockPos pos, boolean isOutput) { + this.pos = pos; + this.isOutput = isOutput; + } + + public static void handle(AutoIOChangePacket msg, IPayloadContext ctx) { + ctx.enqueueWork(() -> { + ServerPlayer player = (ServerPlayer) ctx.player(); + if (player == null) return; + + BlockEntity be = player.level().getBlockEntity(msg.pos); + if (be instanceof AdvancedAlloyFurnaceBlockEntity furnace) { + if (msg.isOutput) { + furnace.toggleAutoOutput(); + } else { + furnace.toggleAutoInput(); + } + } + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/BeefInvulnerabilityStatePacket.java b/src/main/java/com/sorrowmist/useless/network/BeefInvulnerabilityStatePacket.java new file mode 100644 index 00000000..24495825 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/BeefInvulnerabilityStatePacket.java @@ -0,0 +1,34 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.event.EventHandler; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +/** Synchronizes the advanced-stealth state of a tracked player. */ +public record BeefInvulnerabilityStatePacket(int entityId, boolean stealthEnabled) implements CustomPacketPayload { + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, pkt) -> { + buf.writeInt(pkt.entityId); + buf.writeBoolean(pkt.stealthEnabled); + }, + buf -> new BeefInvulnerabilityStatePacket(buf.readInt(), buf.readBoolean()) + ); + + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "beef_invulnerability_state") + ); + + public static void handle(BeefInvulnerabilityStatePacket msg, IPayloadContext ctx) { + ctx.enqueueWork(() -> EventHandler.setClientBeefAdvancedStealthState(msg.entityId, msg.stealthEnabled)); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/BeefInvulnerabilitySyncPacket.java b/src/main/java/com/sorrowmist/useless/network/BeefInvulnerabilitySyncPacket.java new file mode 100644 index 00000000..be6be524 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/BeefInvulnerabilitySyncPacket.java @@ -0,0 +1,40 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.client.network.ClientPacketHandlers; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.fml.loading.FMLEnvironment; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +public record BeefInvulnerabilitySyncPacket(int entityId, float health) implements CustomPacketPayload { + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, pkt) -> { + buf.writeInt(pkt.entityId); + buf.writeFloat(pkt.health); + }, + buf -> new BeefInvulnerabilitySyncPacket(buf.readInt(), buf.readFloat()) + ); + + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "beef_invulnerability_sync") + ); + + public static void handle(BeefInvulnerabilitySyncPacket msg, IPayloadContext ctx) { + // 本类会在 RegisterPayloadHandlersEvent 阶段被专用服务器加载, + // 因此这里只做分发检查 + 委托,客户端类型(Minecraft/LocalPlayer)全部在 ClientPacketHandlers 中引用。 + if (FMLEnvironment.dist != Dist.CLIENT) { + return; + } + ctx.enqueueWork(() -> ClientPacketHandlers.handleBeefInvulnerabilitySync(msg)); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/BeefToolLayoutRequestPacket.java b/src/main/java/com/sorrowmist/useless/network/BeefToolLayoutRequestPacket.java new file mode 100644 index 00000000..d8cf171d --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/BeefToolLayoutRequestPacket.java @@ -0,0 +1,37 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.data.BeefToolLayout; +import com.sorrowmist.useless.data.BeefToolLayoutManager; +import com.sorrowmist.useless.utils.UselessItemUtils; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +public record BeefToolLayoutRequestPacket() implements CustomPacketPayload { + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "beef_tool_layout_request")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.of((buffer, packet) -> {}, buffer -> new BeefToolLayoutRequestPacket()); + + public static void handle(BeefToolLayoutRequestPacket packet, IPayloadContext context) { + context.enqueueWork(() -> { + if (!(context.player() instanceof ServerPlayer player) + || UselessItemUtils.findTargetToolInHands(player).isEmpty()) { + return; + } + BeefToolLayout layout = BeefToolLayoutManager.loadOrCreate(player); + PacketDistributor.sendToPlayer(player, new BeefToolLayoutSyncPacket(layout.toJson())); + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/BeefToolLayoutResultPacket.java b/src/main/java/com/sorrowmist/useless/network/BeefToolLayoutResultPacket.java new file mode 100644 index 00000000..1ba6dd2a --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/BeefToolLayoutResultPacket.java @@ -0,0 +1,35 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.client.network.ClientPacketHandlers; +import com.sorrowmist.useless.data.BeefToolLayout; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.fml.loading.FMLEnvironment; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +public record BeefToolLayoutResultPacket(BeefToolLayout.Error error) implements CustomPacketPayload { + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "beef_tool_layout_result")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.of( + (buffer, packet) -> buffer.writeEnum(packet.error), + buffer -> new BeefToolLayoutResultPacket(buffer.readEnum(BeefToolLayout.Error.class))); + + public static void handle(BeefToolLayoutResultPacket packet, IPayloadContext context) { + // 客户端类型统一收敛到 ClientPacketHandlers,避免专用服务器加载类时解析到客户端类。 + if (FMLEnvironment.dist != Dist.CLIENT) { + return; + } + context.enqueueWork(() -> ClientPacketHandlers.handleBeefToolLayoutResult(packet)); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/BeefToolLayoutSyncPacket.java b/src/main/java/com/sorrowmist/useless/network/BeefToolLayoutSyncPacket.java new file mode 100644 index 00000000..36a72efd --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/BeefToolLayoutSyncPacket.java @@ -0,0 +1,35 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.client.network.ClientPacketHandlers; +import com.sorrowmist.useless.data.BeefToolLayout; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.fml.loading.FMLEnvironment; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +public record BeefToolLayoutSyncPacket(String json) implements CustomPacketPayload { + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "beef_tool_layout_sync")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.of( + (buffer, packet) -> buffer.writeUtf(packet.json, BeefToolLayout.MAX_TEXT_LENGTH), + buffer -> new BeefToolLayoutSyncPacket(buffer.readUtf(BeefToolLayout.MAX_TEXT_LENGTH))); + + public static void handle(BeefToolLayoutSyncPacket packet, IPayloadContext context) { + // 客户端类型统一收敛到 ClientPacketHandlers,避免专用服务器加载类时解析到客户端类。 + if (FMLEnvironment.dist != Dist.CLIENT) { + return; + } + context.enqueueWork(() -> ClientPacketHandlers.handleBeefToolLayoutSync(packet)); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/BeefToolLayoutUpdatePacket.java b/src/main/java/com/sorrowmist/useless/network/BeefToolLayoutUpdatePacket.java new file mode 100644 index 00000000..1bcd2d53 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/BeefToolLayoutUpdatePacket.java @@ -0,0 +1,63 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.core.config.BeefToolProtectionManager; +import com.sorrowmist.useless.core.config.ChainGroupManager; +import com.sorrowmist.useless.data.BeefToolLayout; +import com.sorrowmist.useless.data.BeefToolLayoutManager; +import com.sorrowmist.useless.utils.UselessItemUtils; +import com.sorrowmist.useless.utils.mining.MiningDispatcher; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +public record BeefToolLayoutUpdatePacket(String json) implements CustomPacketPayload { + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "beef_tool_layout_update")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.of( + (buffer, packet) -> buffer.writeUtf(packet.json, BeefToolLayout.MAX_TEXT_LENGTH), + buffer -> new BeefToolLayoutUpdatePacket(buffer.readUtf(BeefToolLayout.MAX_TEXT_LENGTH))); + + public static void handle(BeefToolLayoutUpdatePacket packet, IPayloadContext context) { + context.enqueueWork(() -> { + if (!(context.player() instanceof ServerPlayer player)) { + return; + } + if (UselessItemUtils.findTargetToolInHands(player).isEmpty()) { + sendError(player, BeefToolLayout.Error.INVALID_STRUCTURE); + return; + } + + try { + BeefToolLayout layout = BeefToolLayout.fromJson(packet.json); + BeefToolLayoutManager.validateForSave(layout); + BeefToolLayoutManager.normalizeForPlayer(player, layout); + BeefToolLayoutManager.validateForSave(layout); + BeefToolLayoutManager.save(player, layout); + // 布局与连锁等价组共用同一份存档:改动后必须丢弃等价组解析缓存, + // 并清掉 Tab 高亮缓存,否则玩家会看到按旧等价组算出来的连锁范围。 + ChainGroupManager.invalidate(player.getUUID()); + BeefToolProtectionManager.invalidate(player.getUUID()); + MiningDispatcher.clearPlayerCache(player); + PacketDistributor.sendToPlayer(player, new BeefToolLayoutSyncPacket(layout.toJson())); + } catch (BeefToolLayout.LayoutException exception) { + sendError(player, exception.error()); + } + }); + } + + private static void sendError(ServerPlayer player, BeefToolLayout.Error error) { + PacketDistributor.sendToPlayer(player, new BeefToolLayoutResultPacket(error)); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/ConstructionWandCorePacket.java b/src/main/java/com/sorrowmist/useless/network/ConstructionWandCorePacket.java new file mode 100644 index 00000000..8cd2c155 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/ConstructionWandCorePacket.java @@ -0,0 +1,42 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.api.enums.tool.ConstructionWandCoreMode; +import com.sorrowmist.useless.core.component.UComponents; +import com.sorrowmist.useless.content.items.EndlessBeafItem; +import com.sorrowmist.useless.utils.UselessItemUtils; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.ItemStack; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +public record ConstructionWandCorePacket(ConstructionWandCoreMode mode) implements CustomPacketPayload { + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "construction_wand_core")); + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, packet) -> buf.writeEnum(packet.mode), + buf -> new ConstructionWandCorePacket(buf.readEnum(ConstructionWandCoreMode.class)) + ); + + public static void handle(ConstructionWandCorePacket message, IPayloadContext context) { + context.enqueueWork(() -> { + ServerPlayer player = (ServerPlayer) context.player(); + var entry = UselessItemUtils.findTargetToolInHands(player); + if (entry.isEmpty()) return; + + ItemStack stack = entry.get().getKey(); + if (!(stack.getItem() instanceof EndlessBeafItem)) return; + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.ConstructionWandCoreComponent.get(), message.mode); + player.containerMenu.broadcastChanges(); + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/ConstructionWandPreviewPacket.java b/src/main/java/com/sorrowmist/useless/network/ConstructionWandPreviewPacket.java new file mode 100644 index 00000000..ecaacd94 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/ConstructionWandPreviewPacket.java @@ -0,0 +1,60 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.client.render.ConstructionWandPreviewRenderer; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.fml.loading.FMLEnvironment; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +public record ConstructionWandPreviewPacket(int requestId, List positions) + implements CustomPacketPayload { + private static final int MAX_POSITIONS = 4096; + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "construction_wand_preview")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.of( + (buf, packet) -> { + buf.writeVarInt(packet.requestId); + buf.writeVarInt(packet.positions.size()); + for (BlockPos pos : packet.positions) buf.writeBlockPos(pos); + }, + buf -> { + int requestId = buf.readVarInt(); + int size = buf.readVarInt(); + if (size < 0 || size > MAX_POSITIONS) { + throw new IllegalArgumentException("Invalid construction preview size: " + size); + } + List positions = new ArrayList<>(size); + for (int i = 0; i < size; i++) positions.add(buf.readBlockPos()); + return new ConstructionWandPreviewPacket(requestId, positions); + } + ); + + public ConstructionWandPreviewPacket { + if (positions.size() > MAX_POSITIONS) { + throw new IllegalArgumentException("Construction preview is too large"); + } + positions = List.copyOf(positions); + } + + public static void handle(ConstructionWandPreviewPacket packet, IPayloadContext context) { + if (FMLEnvironment.dist == Dist.CLIENT) { + context.enqueueWork(() -> ConstructionWandPreviewRenderer.setPreview( + packet.requestId, packet.positions)); + } + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/ConstructionWandPreviewRequestPacket.java b/src/main/java/com/sorrowmist/useless/network/ConstructionWandPreviewRequestPacket.java new file mode 100644 index 00000000..5df630cd --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/ConstructionWandPreviewRequestPacket.java @@ -0,0 +1,60 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.compat.constructionwand.ConstructionWandLogic; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.phys.BlockHitResult; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +public record ConstructionWandPreviewRequestPacket(int requestId, boolean air, + BlockHitResult hit, InteractionHand hand) + implements CustomPacketPayload { + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "construction_wand_preview_request")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.of( + (buf, packet) -> { + buf.writeVarInt(packet.requestId); + buf.writeBoolean(packet.air); + if (!packet.air) buf.writeBlockHitResult(packet.hit); + buf.writeEnum(packet.hand); + }, + buf -> { + int requestId = buf.readVarInt(); + boolean air = buf.readBoolean(); + BlockHitResult hit = air ? null : buf.readBlockHitResult(); + return new ConstructionWandPreviewRequestPacket( + requestId, air, hit, buf.readEnum(InteractionHand.class)); + } + ); + + public static ConstructionWandPreviewRequestPacket block(int requestId, BlockHitResult hit, + InteractionHand hand) { + return new ConstructionWandPreviewRequestPacket(requestId, false, hit, hand); + } + + public static ConstructionWandPreviewRequestPacket air(int requestId, InteractionHand hand) { + return new ConstructionWandPreviewRequestPacket(requestId, true, null, hand); + } + + public static void handle(ConstructionWandPreviewRequestPacket packet, IPayloadContext context) { + context.enqueueWork(() -> { + if (!(context.player() instanceof ServerPlayer player)) return; + PacketDistributor.sendToPlayer(player, new ConstructionWandPreviewPacket( + packet.requestId, + ConstructionWandLogic.preview(player, packet.hand, packet.hit, packet.air))); + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/DimensionConfigGhostSlotPacket.java b/src/main/java/com/sorrowmist/useless/network/DimensionConfigGhostSlotPacket.java new file mode 100644 index 00000000..26139749 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/DimensionConfigGhostSlotPacket.java @@ -0,0 +1,42 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.menus.DimensionConfigMenu; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +public record DimensionConfigGhostSlotPacket(int containerId, int slot, ResourceLocation blockId) + implements CustomPacketPayload { + public static final Type TYPE = new Type<>( + UselessMod.id("dimension_config_ghost_slot")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.of((buffer, packet) -> { + buffer.writeVarInt(packet.containerId); + buffer.writeVarInt(packet.slot); + buffer.writeResourceLocation(packet.blockId); + }, buffer -> new DimensionConfigGhostSlotPacket( + buffer.readVarInt(), buffer.readVarInt(), buffer.readResourceLocation())); + + public static void handle(DimensionConfigGhostSlotPacket packet, IPayloadContext context) { + context.enqueueWork(() -> { + if (!(context.player() instanceof ServerPlayer player) + || !(player.containerMenu instanceof DimensionConfigMenu menu) + || menu.containerId != packet.containerId + || !menu.stillValid(player)) { + return; + } + menu.setGhostBlockId(packet.slot, packet.blockId); + menu.broadcastChanges(); + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/DimensionConfigSubmitPacket.java b/src/main/java/com/sorrowmist/useless/network/DimensionConfigSubmitPacket.java new file mode 100644 index 00000000..37d10f85 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/DimensionConfigSubmitPacket.java @@ -0,0 +1,43 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.menus.DimensionConfigMenu; +import com.sorrowmist.useless.world.dimension.DimensionGenerationConfig; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +public record DimensionConfigSubmitPacket(int containerId, + DimensionGenerationConfig config, + boolean teleportAfterSave) + implements CustomPacketPayload { + public static final Type TYPE = new Type<>( + UselessMod.id("dimension_config_submit")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.of((buffer, packet) -> { + buffer.writeVarInt(packet.containerId); + packet.config.write(buffer); + buffer.writeBoolean(packet.teleportAfterSave); + }, buffer -> new DimensionConfigSubmitPacket( + buffer.readVarInt(), DimensionGenerationConfig.read(buffer), buffer.readBoolean())); + + public static void handle(DimensionConfigSubmitPacket packet, IPayloadContext context) { + context.enqueueWork(() -> { + if (!(context.player() instanceof ServerPlayer player) + || !(player.containerMenu instanceof DimensionConfigMenu menu) + || menu.containerId != packet.containerId + || !menu.stillValid(player)) { + return; + } + menu.submit(player, packet.config, packet.teleportAfterSave()); + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/EnchantmentSwitchPacket.java b/src/main/java/com/sorrowmist/useless/network/EnchantmentSwitchPacket.java new file mode 100644 index 00000000..da096189 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/EnchantmentSwitchPacket.java @@ -0,0 +1,63 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.api.enums.tool.EnchantMode; +import com.sorrowmist.useless.content.items.EndlessBeafItem; +import com.sorrowmist.useless.core.component.UComponents; +import com.sorrowmist.useless.utils.UselessItemUtils; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +public class EnchantmentSwitchPacket implements CustomPacketPayload { + + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "enchantment_switch")); + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, pkt) -> { + buf.writeEnum(pkt.mode); + }, + buf -> new EnchantmentSwitchPacket(buf.readEnum(EnchantMode.class)) + ); + private final EnchantMode mode; + + public EnchantmentSwitchPacket(EnchantMode mode) { + this.mode = mode; + } + + public static void handle(EnchantmentSwitchPacket msg, IPayloadContext ctx) { + ctx.enqueueWork(() -> { + ServerPlayer player = (ServerPlayer) ctx.player(); + var toolEntry = UselessItemUtils.findTargetToolInHands(player); + if (toolEntry.isEmpty()) return; // 没找到工具直接返回 + + var entry = toolEntry.get(); + ItemStack stack = entry.getKey(); + + Level level = player.level(); + + // 写入组件 + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.EnchantModeComponent.get(), msg.mode); + + // 切换模型数据 + stack.getOrCreateTag().putInt("CustomModelData", msg.mode == EnchantMode.FORTUNE ? 0 : 1); + + // 更新附魔 + EndlessBeafItem.refreshEnchantments(stack, level); + + // 显式同步物品到客户端 + player.containerMenu.broadcastChanges(); + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/FaceModeChangePacket.java b/src/main/java/com/sorrowmist/useless/network/FaceModeChangePacket.java new file mode 100644 index 00000000..6c42b2e7 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/FaceModeChangePacket.java @@ -0,0 +1,67 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.api.enums.FurnaceFace; +import com.sorrowmist.useless.api.enums.FurnaceFaceMode; +import com.sorrowmist.useless.content.blockentities.AdvancedAlloyFurnaceBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.block.entity.BlockEntity; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +/** + * 客户端→服务器:循环指定逻辑面的输入输出模式。 + */ +public class FaceModeChangePacket implements CustomPacketPayload { + + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "face_mode_change")); + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, pkt) -> { + buf.writeBlockPos(pkt.pos); + buf.writeByte(pkt.faceIndex); + buf.writeBoolean(pkt.reverse); + }, + buf -> new FaceModeChangePacket(buf.readBlockPos(), buf.readByte(), buf.readBoolean()) + ); + private final BlockPos pos; + private final byte faceIndex; + private final boolean reverse; + + /** + * @param pos 方块位置 + * @param faceIndex FurnaceFace的ordinal值 + */ + public FaceModeChangePacket(BlockPos pos, int faceIndex) { + this(pos, faceIndex, false); + } + + public FaceModeChangePacket(BlockPos pos, int faceIndex, boolean reverse) { + this.pos = pos; + this.faceIndex = (byte) faceIndex; + this.reverse = reverse; + } + + public static void handle(FaceModeChangePacket msg, IPayloadContext ctx) { + ctx.enqueueWork(() -> { + ServerPlayer player = (ServerPlayer) ctx.player(); + if (player == null) return; + + BlockEntity be = player.level().getBlockEntity(msg.pos); + if (be instanceof AdvancedAlloyFurnaceBlockEntity furnace) { + FurnaceFace face = FurnaceFace.values()[msg.faceIndex % FurnaceFace.COUNT]; + FurnaceFaceMode newMode = furnace.cycleFaceMode(face, msg.reverse); + } + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/ForceBreakKeyPacket.java b/src/main/java/com/sorrowmist/useless/network/ForceBreakKeyPacket.java new file mode 100644 index 00000000..70220e69 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/ForceBreakKeyPacket.java @@ -0,0 +1,48 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.utils.mining.MiningDispatcher; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +/** + * 强制破坏按键包 + * 用于客户端通知服务端执行强制破坏 + */ +public class ForceBreakKeyPacket implements CustomPacketPayload { + + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "force_break_key")); + + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, pkt) -> { + buf.writeBoolean(pkt.tabPressed); + }, + buf -> new ForceBreakKeyPacket(buf.readBoolean()) + ); + + // 是否同时按下了Tab键(连锁模式) + private final boolean tabPressed; + + public ForceBreakKeyPacket(boolean tabPressed) { + this.tabPressed = tabPressed; + } + + public static void handle(ForceBreakKeyPacket msg, IPayloadContext ctx) { + ctx.enqueueWork(() -> { + if (ctx.player() != null) { + // 直接执行强制破坏,传入当前是否按下Tab键 + MiningDispatcher.dispatchForceBreak(ctx.player(), msg.tabPressed); + } + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/MiningDataSyncPacket.java b/src/main/java/com/sorrowmist/useless/network/MiningDataSyncPacket.java new file mode 100644 index 00000000..d861abbc --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/MiningDataSyncPacket.java @@ -0,0 +1,79 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.data.PlayerMiningData; +import com.sorrowmist.useless.utils.mining.MiningDispatcher; +import com.sorrowmist.useless.utils.mining.shape.ChainMiningShapes; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +public record MiningDataSyncPacket(BlockPos cachedPos, List cachedBlocks, + ChainMiningShapes shape, int maxBlocks) implements CustomPacketPayload { + + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, pkt) -> { + buf.writeBoolean(pkt.cachedPos != null); + if (pkt.cachedPos != null) { + buf.writeBlockPos(pkt.cachedPos); + } + buf.writeBoolean(pkt.cachedBlocks != null); + if (pkt.cachedBlocks != null) { + buf.writeCollection(pkt.cachedBlocks, + (friendlyBuf, blockPos) -> friendlyBuf.writeBlockPos(blockPos) + ); + } + // 形状随缓存一并下发:客户端需要它来预测整片高亮与 HUD 显示, + // 按标识字符串传输而非枚举序号,便于后续增删形状而不破坏兼容。 + buf.writeUtf(pkt.shape.getId().toString()); + // 数量上限来自服务端配置,客户端读不到,随包下发仅供界面显示 + buf.writeVarInt(pkt.maxBlocks); + }, + buf -> { + BlockPos pos = null; + if (buf.readBoolean()) { + pos = buf.readBlockPos(); + } + List blocks = null; + if (buf.readBoolean()) { + blocks = buf.readCollection(ArrayList::new, friendlyByteBuf -> friendlyByteBuf.readBlockPos()); + } + ChainMiningShapes shape = ChainMiningShapes.byId(buf.readUtf()); + int maxBlocks = buf.readVarInt(); + return new MiningDataSyncPacket(pos, blocks, shape, maxBlocks); + } + ); + + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "mining_data_sync") + ); + + public MiningDataSyncPacket(PlayerMiningData data) { + this(data.getCachedPos(), data.getCachedBlocks(), data.getShape(), data.getMaxBlocks()); + } + + public static void handle(MiningDataSyncPacket msg, IPayloadContext ctx) { + ctx.enqueueWork(() -> { + if (ctx.player() != null) { + PlayerMiningData data = new PlayerMiningData(ctx.player().getUUID()); + data.setCachedPos(msg.cachedPos); + data.setCachedBlocks(msg.cachedBlocks); + data.setShape(msg.shape); + data.setMaxBlocks(msg.maxBlocks); + MiningDispatcher.setClientPlayerData(data); + } + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/ModeTogglePacket.java b/src/main/java/com/sorrowmist/useless/network/ModeTogglePacket.java new file mode 100644 index 00000000..f48079fc --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/ModeTogglePacket.java @@ -0,0 +1,366 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.items.EndlessBeafItem; +import com.sorrowmist.useless.content.items.BeefToolVariants; +import com.sorrowmist.useless.core.component.UComponents; +import com.sorrowmist.useless.event.EventHandler; +import com.sorrowmist.useless.init.ModSounds; +import com.sorrowmist.useless.utils.UselessItemUtils; +import com.sorrowmist.useless.utils.mining.MiningDispatcher; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.fml.ModList; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +public class ModeTogglePacket implements CustomPacketPayload { + + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "mode_toggle")); + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, pkt) -> { + buf.writeEnum(pkt.modeType); + buf.writeBoolean(pkt.enabled); + }, + buf -> new ModeTogglePacket(buf.readEnum(ModeType.class), buf.readBoolean()) + ); + private final ModeType modeType; + private final boolean enabled; + public ModeTogglePacket(ModeType modeType, boolean enabled) { + this.modeType = modeType; + this.enabled = enabled; + } + + public static void handle(ModeTogglePacket msg, IPayloadContext ctx) { + ctx.enqueueWork(() -> { + ServerPlayer player = (ServerPlayer) ctx.player(); + + // 杀戮光环:杖子放在副手 / 快捷栏 / 主背包里也要能开关,所以单独走「全背包」查找, + // 不能套用下面 findTargetToolInHands(只认手)的那道门。 + if (msg.modeType == ModeType.BEEF_KILL_AURA) { + ItemStack staff = UselessItemUtils.findKillAuraToggleTarget(player); + if (!staff.isEmpty()) { + EndlessBeafItem.setKillAuraEnabled(staff, msg.enabled); + // 光环不占右键,无需接入 disable* 互斥。 + // 音效只发给操作者本人:这是个人 UI 反馈,不该打扰周围玩家。 + player.playNotifySound( + (msg.enabled ? ModSounds.KILL_AURA_ON : ModSounds.KILL_AURA_OFF).get(), + SoundSource.MASTER, 1.0F, 1.0F); + player.containerMenu.broadcastChanges(); + } + return; + } + + var toolEntry = UselessItemUtils.findTargetToolInHands(player); + if (toolEntry.isEmpty()) return; // 没找到工具直接返回 + + var entry = toolEntry.get(); + ItemStack stack = entry.getKey(); + + // 根据模式类型处理不同的组件 + switch (msg.modeType) { + case CHAIN_MINING -> { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.EnhancedChainMiningComponent.get(), msg.enabled); + // 切换连锁模式时清空缓存,避免使用旧模式的缓存数据 + MiningDispatcher.clearPlayerCache(player); + } + case FORCE_MINING -> { + MiningDispatcher.clearPlayerCache(player); + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.ForceMiningComponent.get(), msg.enabled); + } + case AUTO_SMELT -> { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.AutoSmeltComponent.get(), msg.enabled); + } + case EXDEORUM_CROOK -> { + // 能力由 exdeorum 的配方体系提供,未加载时忽略,避免写入无人读取的组件 + if (ModList.get().isLoaded("exdeorum") && stack.getItem() instanceof EndlessBeafItem) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.ExDeorumCrookComponent.get(), msg.enabled); + } + } + case EXDEORUM_HAMMER -> { + if (ModList.get().isLoaded("exdeorum") && stack.getItem() instanceof EndlessBeafItem) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.ExDeorumHammerComponent.get(), msg.enabled); + } + } + case EXDEORUM_COMPRESSED_HAMMER -> { + if (ModList.get().isLoaded("exdeorum") && stack.getItem() instanceof EndlessBeafItem) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.ExDeorumCompressedHammerComponent.get(), msg.enabled); + } + } + case AE_STORAGE_PRIORITY -> { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.AEStoragePriorityComponent.get(), msg.enabled); + } + case AE_NETWORK_CONNECT -> { + if (stack.getItem() instanceof EndlessBeafItem) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.AeNetworkConnectComponent.get(), msg.enabled); + if (msg.enabled) { + disableRightClickConflicts(stack); + } + } + } + case WRENCH_TAG -> { + if (BeefToolVariants.isBaseVariant(stack) + && BeefToolVariants.isWrenchTagEnabled(stack) != msg.enabled) { + ItemStack replacement = BeefToolVariants.withWrenchTag(stack, msg.enabled); + player.setItemInHand(entry.getValue(), replacement); + stack = replacement; + } + } + case CONSTRUCTION_WAND -> { + if (stack.getItem() instanceof EndlessBeafItem) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.ConstructionWandEnabledComponent.get(), msg.enabled); + if (msg.enabled) { + disableAeNetworkConnect(stack); + // 建筑魔杖的「潜行右键 = 撤销」会抢走绑定容器的那次交互。 + disableStaffLink(stack); + } + } + } + case FORCE_KILL -> { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.ForceKillEnabledComponent.get(), msg.enabled); + } + case BEEF_MALUM_SPIRIT -> { + if (ModList.get().isLoaded("malum") && stack.getItem() instanceof EndlessBeafItem) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.BeefMalumSpiritEnabledComponent.get(), msg.enabled); + } + } + case BEEF_MYSTICAL_AGRICULTURE -> { + if (ModList.get().isLoaded("mysticalagriculture") + && stack.getItem() instanceof EndlessBeafItem) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.BeefMysticalAgricultureEnabledComponent.get(), msg.enabled); + } + } + case BEEF_BEHEADING -> { + if (stack.getItem() instanceof EndlessBeafItem) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.BeefBeheadingEnabledComponent.get(), msg.enabled); + } + } + case BEEF_TIME_ACCELERATION -> { + if (stack.getItem() instanceof EndlessBeafItem) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.BeefTimeAccelerationEnabledComponent.get(), msg.enabled); + if (msg.enabled) { + disableRitualSatchel(stack); + // 时间加速同样绑定「潜行右键」。 + disableStaffLink(stack); + } + } + } + case BEEF_INVULNERABILITY -> { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.BeefInvulnerabilityEnabledComponent.get(), msg.enabled); + if (!msg.enabled) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.BeefAdvancedStealthEnabledComponent.get(), false); + } + EventHandler.updateBeefInvulnerability(player, true); + } + case BEEF_ADVANCED_STEALTH -> { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.BeefAdvancedStealthEnabledComponent.get(), msg.enabled); + if (msg.enabled) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.BeefInvulnerabilityEnabledComponent.get(), true); + } + EventHandler.updateBeefInvulnerability(player, true); + } + case BEEF_CAPTURE -> { + if (stack.getItem() instanceof EndlessBeafItem) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.BeefCaptureEnabledComponent.get(), msg.enabled); + } + } + case BEEF_TELEPORT -> { + if (stack.getItem() instanceof EndlessBeafItem) { + EndlessBeafItem.setTeleportEnabled(stack, msg.enabled); + } + } + case BEEF_AOE_DAMAGE -> { + if (stack.getItem() instanceof EndlessBeafItem) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.BeefAoeDamageEnabledComponent.get(), msg.enabled); + } + } + case BEEF_MAGNET -> { + if (stack.getItem() instanceof EndlessBeafItem) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.BeefMagnetEnabledComponent.get(), msg.enabled); + } + } + case BEEF_FARMLAND_MODE -> { + if (stack.getItem() instanceof EndlessBeafItem) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.BeefFarmlandModeComponent.get(), msg.enabled); + } + } + case BEEF_CROP_HARVEST -> { + if (stack.getItem() instanceof EndlessBeafItem) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.BeefCropHarvestComponent.get(), msg.enabled); + if (msg.enabled) { + disableAeNetworkConnect(stack); + disableRitualSatchel(stack); + } + } + } + case BEEF_SHEARS -> { + if (stack.getItem() instanceof EndlessBeafItem) { + EndlessBeafItem.setShearsEnabled(stack, msg.enabled); + } + } + case BEEF_FLINT_AND_STEEL -> { + if (stack.getItem() instanceof EndlessBeafItem) { + EndlessBeafItem.setFlintAndSteelEnabled(stack, msg.enabled); + } + } + case BEEF_RITUAL_SATCHEL -> { + // 该能力依赖 occultism 的仪式挎包机制,未加载时直接忽略,避免写入无人读取的组件 + if (ModList.get().isLoaded("occultism") && stack.getItem() instanceof EndlessBeafItem) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.BeefRitualSatchelComponent.get(), msg.enabled); + if (msg.enabled) { + // 仪式摆放走的是「不潜行右键」手势,所以只关同样占该手势的模式。 + // 时间加速与无线物流都是潜行手势,与它不撞,不该被连坐关掉。 + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.BeefCropHarvestComponent.get(), false); + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.ConstructionWandEnabledComponent.get(), false); + disableAeNetworkConnect(stack); + } + } + } + case BEEF_RIPEN -> { + if (stack.getItem() instanceof EndlessBeafItem) { + EndlessBeafItem.setRipenEnabled(stack, msg.enabled); + } + } + case BEEF_FORCE_GROW -> { + if (stack.getItem() instanceof EndlessBeafItem) { + EndlessBeafItem.setForceGrowEnabled(stack, msg.enabled); + } + } + case BEEF_AUTO_CLICK -> { + // 连点开关已改为客户端会话级状态(BeefAutoClicker),不再写入物品组件。 + // 保留枚举值与分支只为不破坏 ModeType 的 ordinal 编码,这里不做任何事。 + } + case BEEF_WIRELESS_LOGISTICS -> { + if (stack.getItem() instanceof EndlessBeafItem) { + EndlessBeafItem.setStaffLinkEnabled(stack, msg.enabled); + if (msg.enabled) { + // 无线物流只用「潜行右键方块」这一个手势,所以只关掉同样占用它的模式: + // 时间加速、建筑魔杖(潜行右键 = 撤销)、AE 连接(潜行右键访问点 = 定绑定目标)。 + // + // 顺手收菜与匠心仪式挎包刻意不关:它们都要求「不潜行」 + // (见 EndlessBeafItem#useOn 里的 !player.isShiftKeyDown() 守卫), + // 与潜行手势天然错开,关掉只会让玩家白丢一个功能。 + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.BeefTimeAccelerationEnabledComponent.get(), false); + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.ConstructionWandEnabledComponent.get(), false); + disableAeNetworkConnect(stack); + } + } + } + case BEEF_PROTECT_MODE -> { + if (stack.getItem() instanceof EndlessBeafItem) { + EndlessBeafItem.setProtectModeEnabled(stack, msg.enabled); + } + } + case BEEF_ENTITY_TIME_ACCELERATION -> { + if (stack.getItem() instanceof EndlessBeafItem) { + EndlessBeafItem.setEntityTimeAccelerationEnabled(stack, msg.enabled); + // 它只作用于「潜行右键生物」,而建筑魔杖/时间加速/收菜等只管「潜行右键方块」, + // 作用对象不重叠,因此不接入 disable* 互斥。 + } + } + } + + // 显式同步物品到客户端 + player.containerMenu.broadcastChanges(); + }); + } + + /** + * AE 连接模式与其它右键模式互斥:打开它时把「顺手收菜 / 时间加速 / 建筑魔杖 / 无线物流」关掉。 + * + *

它两种手势都用:不潜行右键有 AE 节点的机器是「并入网络」,潜行右键无线访问点 + * 是「给连接模式定一个绑定目标」(见 {@code EventHandler} 里那段的说明)。因此两个手势上的 + * 模式都得让位 —— 这也是本方法比下面那几个「单手势」开关管得宽的原因。

+ */ + private static void disableRightClickConflicts(ItemStack stack) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.BeefCropHarvestComponent.get(), false); + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.BeefTimeAccelerationEnabledComponent.get(), false); + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.ConstructionWandEnabledComponent.get(), false); + disableStaffLink(stack); + } + + /** + * 关掉无线物流模式。 + * + *

它与 AE 连接撞的是同一次潜行右键:无线访问点既是 AE 端点(AE 连接模式下潜行右键 + * 会把它设成绑定目标并取消事件),现在也允许绑进物流网络。两者都开着时谁赢取决于事件顺序, + * 表现就是「想绑访问点进物流网络,结果杖子连到该网络去了」。

+ */ + private static void disableStaffLink(ItemStack stack) { + if (com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.StaffLinkEnabledComponent.get(), false)) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.StaffLinkEnabledComponent.get(), false); + } + } + + /** 反过来:打开其它占用右键的模式时,关掉 AE 连接模式。 */ + private static void disableAeNetworkConnect(ItemStack stack) { + if (com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.AeNetworkConnectComponent.get(), false)) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.AeNetworkConnectComponent.get(), false); + } + } + + /** + * 关掉匠心仪式挎包模式。 + * + *

它走「不潜行右键」手势,因此只该被同样占该手势的模式(顺手收菜、建筑魔杖、AE 连接)关掉; + * 潜行手势的时间加速与无线物流与它不撞。

+ */ + private static void disableRitualSatchel(ItemStack stack) { + if (com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.BeefRitualSatchelComponent.get(), false)) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(stack, UComponents.BeefRitualSatchelComponent.get(), false); + } + } + + @Override + public @NotNull Type type() { + return TYPE; + } + + public enum ModeType { + CHAIN_MINING, + FORCE_MINING, + AUTO_SMELT, + AE_STORAGE_PRIORITY, + AE_NETWORK_CONNECT, + WRENCH_TAG, + CONSTRUCTION_WAND, + FORCE_KILL, + BEEF_MALUM_SPIRIT, + BEEF_MYSTICAL_AGRICULTURE, + BEEF_BEHEADING, + BEEF_INVULNERABILITY, + BEEF_CAPTURE, + BEEF_TIME_ACCELERATION, + BEEF_TELEPORT, + BEEF_AOE_DAMAGE, + BEEF_MAGNET, + BEEF_ADVANCED_STEALTH, + BEEF_FARMLAND_MODE, + BEEF_CROP_HARVEST, + BEEF_SHEARS, + BEEF_FLINT_AND_STEEL, + BEEF_RITUAL_SATCHEL, + // 新增值必须追加在末尾:writeEnum 按 ordinal 编码 + BEEF_RIPEN, + BEEF_AUTO_CLICK, + BEEF_FORCE_GROW, + // 无线物流:潜行右键容器绑定/解绑,界面里配置搬运规则 + BEEF_WIRELESS_LOGISTICS, + // Ex Deorum 钩子 / 锤子 / 压缩锤:按对应配方体系改写挖掘掉落 + EXDEORUM_CROOK, + EXDEORUM_HAMMER, + EXDEORUM_COMPRESSED_HAMMER, + // 杀戮光环:手持造化杖每 20 tick 对「范围伤害」配置范围内生物结算一次攻击伤害 + BEEF_KILL_AURA, + // 保护名单模式:暂停光环与范围伤害,方便从容添加保护名单 + BEEF_PROTECT_MODE, + // 生物加速:潜行右键生物加速其「计时器」(生长/繁殖冷却/下蛋/补货/长毛…),不跑 AI 与移动 + BEEF_ENTITY_TIME_ACCELERATION + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/MultiblockAlloyFurnaceEnergyLimitPacket.java b/src/main/java/com/sorrowmist/useless/network/MultiblockAlloyFurnaceEnergyLimitPacket.java new file mode 100644 index 00000000..1066b60c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/MultiblockAlloyFurnaceEnergyLimitPacket.java @@ -0,0 +1,48 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.blockentities.multiblock.MultiblockAlloyFurnaceCoreBlockEntity; +import com.sorrowmist.useless.content.menus.MultiblockAlloyFurnaceMenu; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +public record MultiblockAlloyFurnaceEnergyLimitPacket( + int containerId, BlockPos pos, long energyLimit) implements CustomPacketPayload { + public static final Type TYPE = new Type<>( + UselessMod.id("multiblock_alloy_furnace_energy_limit")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.of((buffer, packet) -> { + buffer.writeVarInt(packet.containerId); + buffer.writeBlockPos(packet.pos); + buffer.writeLong(packet.energyLimit); + }, buffer -> new MultiblockAlloyFurnaceEnergyLimitPacket( + buffer.readVarInt(), buffer.readBlockPos(), buffer.readLong())); + + public static void handle( + MultiblockAlloyFurnaceEnergyLimitPacket packet, IPayloadContext context) { + context.enqueueWork(() -> { + if (!(context.player() instanceof ServerPlayer player) + || !(player.containerMenu instanceof MultiblockAlloyFurnaceMenu menu) + || menu.containerId != packet.containerId + || !menu.getBlockPos().equals(packet.pos) + || !menu.stillValid(player) + || packet.energyLimit < 0L) { + return; + } + MultiblockAlloyFurnaceCoreBlockEntity core = menu.getCore(); + if (core == null || !core.getBlockPos().equals(packet.pos)) return; + core.setAutomaticEnergyLimit(packet.energyLimit); + menu.broadcastChanges(); + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/OreGeneratorOutputTogglePacket.java b/src/main/java/com/sorrowmist/useless/network/OreGeneratorOutputTogglePacket.java new file mode 100644 index 00000000..128cc49c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/OreGeneratorOutputTogglePacket.java @@ -0,0 +1,45 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.blockentities.OreGeneratorBlockEntity; +import com.sorrowmist.useless.content.menus.OreGeneratorMenu; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +public record OreGeneratorOutputTogglePacket(int containerId, BlockPos pos) + implements CustomPacketPayload { + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "ore_generator_output_toggle")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.of((buf, packet) -> { + buf.writeVarInt(packet.containerId); + buf.writeBlockPos(packet.pos); + }, buf -> new OreGeneratorOutputTogglePacket(buf.readVarInt(), buf.readBlockPos())); + + public static void handle(OreGeneratorOutputTogglePacket packet, IPayloadContext context) { + context.enqueueWork(() -> { + if (!(context.player() instanceof ServerPlayer player) + || !(player.containerMenu instanceof OreGeneratorMenu menu) + || menu.containerId != packet.containerId + || !menu.getBlockPos().equals(packet.pos) + || !menu.stillValid(player)) { + return; + } + OreGeneratorBlockEntity generator = menu.getGenerator(); + if (generator == null) return; + generator.setOutputToAe(!generator.isOutputToAe()); + menu.broadcastChanges(); + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/OreGeneratorSettingsPacket.java b/src/main/java/com/sorrowmist/useless/network/OreGeneratorSettingsPacket.java new file mode 100644 index 00000000..378ac176 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/OreGeneratorSettingsPacket.java @@ -0,0 +1,48 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.blockentities.OreGeneratorBlockEntity; +import com.sorrowmist.useless.content.menus.OreGeneratorMenu; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +public record OreGeneratorSettingsPacket(int containerId, BlockPos pos, long outputRate) + implements CustomPacketPayload { + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "ore_generator_settings")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.of((buf, packet) -> { + buf.writeVarInt(packet.containerId); + buf.writeBlockPos(packet.pos); + buf.writeLong(packet.outputRate); + }, buf -> new OreGeneratorSettingsPacket( + buf.readVarInt(), buf.readBlockPos(), buf.readLong())); + + public static void handle(OreGeneratorSettingsPacket packet, IPayloadContext context) { + context.enqueueWork(() -> { + if (!(context.player() instanceof ServerPlayer player) + || !(player.containerMenu instanceof OreGeneratorMenu menu) + || menu.containerId != packet.containerId + || !menu.getBlockPos().equals(packet.pos) + || !menu.stillValid(player) + || packet.outputRate < 1L) { + return; + } + OreGeneratorBlockEntity generator = menu.getGenerator(); + if (generator == null) return; + generator.setOutputRate(packet.outputRate); + menu.broadcastChanges(); + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/PassiveCraftingSettingsPacket.java b/src/main/java/com/sorrowmist/useless/network/PassiveCraftingSettingsPacket.java new file mode 100644 index 00000000..827380d2 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/PassiveCraftingSettingsPacket.java @@ -0,0 +1,55 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.blockentities.multiblock.PassiveCraftingHatchBlockEntity; +import com.sorrowmist.useless.content.menus.PassiveCraftingHatchMenu; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +public record PassiveCraftingSettingsPacket( + int containerId, BlockPos pos, int intervalTicks, long multiplier) + implements CustomPacketPayload { + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "passive_crafting_settings")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.of((buf, packet) -> { + buf.writeVarInt(packet.containerId); + buf.writeBlockPos(packet.pos); + buf.writeVarInt(packet.intervalTicks); + buf.writeVarLong(packet.multiplier); + }, buf -> new PassiveCraftingSettingsPacket( + buf.readVarInt(), buf.readBlockPos(), buf.readVarInt(), buf.readVarLong())); + + public static void handle(PassiveCraftingSettingsPacket packet, IPayloadContext context) { + context.enqueueWork(() -> { + if (!(context.player() instanceof ServerPlayer player) + || !(player.containerMenu instanceof PassiveCraftingHatchMenu menu) + || menu.containerId != packet.containerId + || !menu.getBlockPos().equals(packet.pos) + || !menu.stillValid(player)) { + return; + } + PassiveCraftingHatchBlockEntity hatch = menu.getHatch(); + if (hatch == null || hatch.getBlockPos().distSqr(packet.pos) != 0.0D + || packet.intervalTicks < PassiveCraftingHatchBlockEntity.MIN_INTERVAL_TICKS + || packet.intervalTicks > PassiveCraftingHatchBlockEntity.MAX_INTERVAL_TICKS + || packet.multiplier < 1 + || packet.multiplier > hatch.getCurrentMaxParallel()) { + return; + } + hatch.applySettings(packet.intervalTicks, packet.multiplier); + menu.broadcastChanges(); + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/PassiveCraftingSlotMultiplierPacket.java b/src/main/java/com/sorrowmist/useless/network/PassiveCraftingSlotMultiplierPacket.java new file mode 100644 index 00000000..24ecef5a --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/PassiveCraftingSlotMultiplierPacket.java @@ -0,0 +1,72 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.blockentities.multiblock.PassiveCraftingHatchBlockEntity; +import com.sorrowmist.useless.content.menus.PassiveCraftingHatchMenu; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +/** + * Updates the batch size of a single passive pattern slot. + * + *

{@code slot < 0} clears every override so all slots follow the global multiplier again; + * a non-negative slot with {@code multiplier == 0} clears just that slot. The value is validated + * against the coil-tier parallel limit before it reaches the block entity, because a client that + * ignores the limit must not be able to persist an unreachable batch size. + */ +public record PassiveCraftingSlotMultiplierPacket( + int containerId, BlockPos pos, int slot, long multiplier) + implements CustomPacketPayload { + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "passive_crafting_slot_multiplier")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.of((buf, packet) -> { + buf.writeVarInt(packet.containerId); + buf.writeBlockPos(packet.pos); + buf.writeVarInt(packet.slot); + buf.writeVarLong(packet.multiplier); + }, buf -> new PassiveCraftingSlotMultiplierPacket( + buf.readVarInt(), buf.readBlockPos(), buf.readVarInt(), buf.readVarLong())); + + public static void handle(PassiveCraftingSlotMultiplierPacket packet, IPayloadContext context) { + context.enqueueWork(() -> { + if (!(context.player() instanceof ServerPlayer player) + || !(player.containerMenu instanceof PassiveCraftingHatchMenu menu) + || menu.containerId != packet.containerId + || !menu.getBlockPos().equals(packet.pos) + || !menu.stillValid(player)) { + return; + } + PassiveCraftingHatchBlockEntity hatch = menu.getHatch(); + if (hatch == null || hatch.getBlockPos().distSqr(packet.pos) != 0.0D) { + return; + } + if (packet.slot < 0) { + hatch.clearSlotMultipliers(); + menu.broadcastChanges(); + hatch.requestStatusSync(); + return; + } + if (packet.slot >= PassiveCraftingHatchBlockEntity.PATTERN_SLOTS || packet.multiplier < 0L) { + return; + } + if (packet.multiplier > 0L && packet.multiplier > hatch.getCurrentMaxParallel()) { + return; + } + hatch.setSlotMultiplier(packet.slot, packet.multiplier); + menu.broadcastChanges(); + hatch.requestStatusSync(); + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/PassiveCraftingStatusPacket.java b/src/main/java/com/sorrowmist/useless/network/PassiveCraftingStatusPacket.java new file mode 100644 index 00000000..26a8a015 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/PassiveCraftingStatusPacket.java @@ -0,0 +1,117 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.blockentities.multiblock.PassiveCraftingHatchBlockEntity; +import com.sorrowmist.useless.content.menus.PassiveCraftingHatchMenu; +import com.sorrowmist.useless.content.menus.PagedRecoverableMenu; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public record PassiveCraftingStatusPacket( + int containerId, BlockPos pos, + List statuses) + implements CustomPacketPayload { + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "passive_crafting_status")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.of(PassiveCraftingStatusPacket::encode, PassiveCraftingStatusPacket::decode); + + public PassiveCraftingStatusPacket { + pos = pos.immutable(); + statuses = List.copyOf(statuses); + if (statuses.size() > PagedRecoverableMenu.SLOTS_PER_PAGE) { + throw new IllegalArgumentException("Passive crafting status packets may contain one page only"); + } + validateStatuses(statuses); + } + + private static void encode(FriendlyByteBuf buf, PassiveCraftingStatusPacket packet) { + buf.writeVarInt(packet.containerId); + buf.writeBlockPos(packet.pos); + buf.writeVarInt(packet.statuses.size()); + for (PassiveCraftingHatchBlockEntity.SlotStatus status : packet.statuses) { + buf.writeVarInt(status.slot()); + buf.writeByte(status.state().ordinal()); + buf.writeVarInt(status.progress()); + buf.writeVarInt(status.maxProgress()); + buf.writeUtf(status.detail(), 256); + // 0 means "follow the global multiplier"; the client resolves the effective value. + buf.writeVarLong(status.multiplier()); + } + } + + private static PassiveCraftingStatusPacket decode(FriendlyByteBuf buf) { + int containerId = buf.readVarInt(); + BlockPos pos = buf.readBlockPos(); + int size = buf.readVarInt(); + if (size < 0 || size > PagedRecoverableMenu.SLOTS_PER_PAGE) { + throw new IllegalArgumentException("Invalid passive crafting status count: " + size); + } + var states = PassiveCraftingHatchBlockEntity.SlotState.values(); + List statuses = new ArrayList<>(size); + for (int index = 0; index < size; index++) { + int slot = buf.readVarInt(); + int stateIndex = buf.readUnsignedByte(); + if (slot < 0 || slot >= PassiveCraftingHatchBlockEntity.MAX_PATTERN_SLOTS) { + throw new IllegalArgumentException("Invalid passive crafting slot: " + slot); + } + if (stateIndex >= states.length) { + throw new IllegalArgumentException("Invalid passive crafting state: " + stateIndex); + } + int progress = buf.readVarInt(); + int maxProgress = buf.readVarInt(); + String detail = buf.readUtf(256); + long multiplier = buf.readVarLong(); + if (multiplier < 0L) { + throw new IllegalArgumentException("Invalid passive crafting slot multiplier: " + multiplier); + } + statuses.add(new PassiveCraftingHatchBlockEntity.SlotStatus( + slot, states[stateIndex], progress, maxProgress, detail, multiplier)); + } + return new PassiveCraftingStatusPacket(containerId, pos, statuses); + } + + private static void validateStatuses(List statuses) { + int page = -1; + Set seenSlots = new HashSet<>(); + for (PassiveCraftingHatchBlockEntity.SlotStatus status : statuses) { + int slot = status.slot(); + if (slot < 0 || slot >= PassiveCraftingHatchBlockEntity.MAX_PATTERN_SLOTS) { + throw new IllegalArgumentException("Invalid passive crafting slot: " + slot); + } + if (!seenSlots.add(slot)) { + throw new IllegalArgumentException("Duplicate passive crafting slot: " + slot); + } + int statusPage = slot / PagedRecoverableMenu.SLOTS_PER_PAGE; + if (page >= 0 && page != statusPage) { + throw new IllegalArgumentException("Passive crafting status packets may contain one page only"); + } + page = statusPage; + } + } + + public static void handle(PassiveCraftingStatusPacket packet, IPayloadContext context) { + context.enqueueWork(() -> { + if (context.player().containerMenu instanceof PassiveCraftingHatchMenu menu + && menu.containerId == packet.containerId + && menu.getBlockPos().equals(packet.pos)) { + menu.updateSlotStatuses(packet.statuses); + } + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/PatternPageChangePacket.java b/src/main/java/com/sorrowmist/useless/network/PatternPageChangePacket.java new file mode 100644 index 00000000..6509cf79 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/PatternPageChangePacket.java @@ -0,0 +1,40 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.menus.AdvancedAlloyFurnaceMenu; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +public class PatternPageChangePacket implements CustomPacketPayload { + + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "pattern_page_change")); + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, pkt) -> buf.writeInt(pkt.page), + buf -> new PatternPageChangePacket(buf.readInt()) + ); + private final int page; + + public PatternPageChangePacket(int page) { + this.page = page; + } + + public static void handle(PatternPageChangePacket msg, IPayloadContext ctx) { + ctx.enqueueWork(() -> { + ServerPlayer player = (ServerPlayer) ctx.player(); + if (player.containerMenu instanceof AdvancedAlloyFurnaceMenu menu) { + menu.setPatternPage(msg.page); + } + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/ProtectEntityPacket.java b/src/main/java/com/sorrowmist/useless/network/ProtectEntityPacket.java new file mode 100644 index 00000000..bc09a91b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/ProtectEntityPacket.java @@ -0,0 +1,132 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.items.EndlessBeafItem; +import com.sorrowmist.useless.core.config.BeefToolProtectionManager; +import com.sorrowmist.useless.data.BeefToolLayout; +import com.sorrowmist.useless.data.BeefToolLayoutManager; +import com.sorrowmist.useless.utils.UselessItemUtils; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * Ctrl / Shift + 右键生物:切换生物保护名单。 + * + *

客户端只上报「实体 id + 是否按种类」;目标解析、距离与前置条件校验全部在服务端完成, + * 避免被构造包写入任意实体。

+ * + *

为什么不在 {@code PlayerInteractEvent.EntityInteract} 里做:Ctrl 不会被 + * {@code ServerboundInteractPacket} 传输(它只带 {@code isShiftKeyDown()}), + * 服务端根本无法区分 Ctrl 与普通右键;而且原版交互(村民交易 GUI、剪羊毛)会在事件之后执行。 + * 客户端改在 {@code InputEvent.MouseButton.Pre} 拦截,取消后连交互包都不会发出。

+ */ +public record ProtectEntityPacket(int entityId, boolean byType) implements CustomPacketPayload { + + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "protect_entity")); + + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buffer, packet) -> { + buffer.writeVarInt(packet.entityId); + buffer.writeBoolean(packet.byType); + }, + buffer -> new ProtectEntityPacket(buffer.readVarInt(), buffer.readBoolean())); + + public static void handle(ProtectEntityPacket packet, IPayloadContext context) { + context.enqueueWork(() -> { + if (!(context.player() instanceof ServerPlayer player)) { + return; + } + if (!(player.level() instanceof ServerLevel level)) { + return; + } + + // 1) 必须手持造化杖 + var toolEntry = UselessItemUtils.findTargetToolInHands(player); + if (toolEntry.isEmpty()) { + return; + } + ItemStack stack = toolEntry.get().getKey(); + + // 2) 杀戮光环、范围伤害或保护名单模式至少开一个 + if (!EndlessBeafItem.isProtectGestureAvailable(stack)) { + return; + } + + // 3) 目标存在、是生物、不是玩家自己、在可交互距离内 + Entity raw = level.getEntity(packet.entityId()); + if (raw == null) { + return; + } + Entity target = EndlessBeafItem.unwrapPartEntity(raw); + if (!(target instanceof LivingEntity living) || living instanceof Player) { + return; + } + if (living == player || !player.canReach(living, 2.0)) { + return; + } + + // 4) 切换名单条目 + BeefToolLayout layout = BeefToolLayoutManager.loadOrCreate(player); + boolean byType = packet.byType(); + List list = byType ? layout.protectedTypes() : layout.protectedEntities(); + int max = byType ? BeefToolLayout.MAX_PROTECTED_TYPES + : BeefToolLayout.MAX_PROTECTED_ENTITIES; + String key = byType ? EndlessBeafItem.getEntityId(living) : living.getUUID().toString(); + + boolean added; + if (list.contains(key)) { + list.remove(key); + added = false; + } else { + if (list.size() >= max) { + return; + } + list.add(key); + added = true; + } + + try { + BeefToolLayoutManager.validateForSave(layout); + } catch (BeefToolLayout.LayoutException exception) { + return; + } + BeefToolLayoutManager.save(player, layout); + BeefToolProtectionManager.invalidate(player.getUUID()); + PacketDistributor.sendToPlayer(player, new BeefToolLayoutSyncPacket(layout.toJson())); + + // 5) 反馈:动作栏消息 + 提示音(沿用 StaffLinkBinding 的紫水晶 chime/break 范式) + String messageKey = byType + ? (added ? "gui.useless_mod.beef_protect.add_type" + : "gui.useless_mod.beef_protect.remove_type") + : (added ? "gui.useless_mod.beef_protect.add_entity" + : "gui.useless_mod.beef_protect.remove_entity"); + Component name = byType ? living.getType().getDescription() : living.getDisplayName(); + player.displayClientMessage(Component.translatable(messageKey, name), true); + level.playSound(null, player.blockPosition(), + added ? SoundEvents.AMETHYST_BLOCK_CHIME : SoundEvents.AMETHYST_BLOCK_BREAK, + SoundSource.PLAYERS, 0.7F, added ? 1.4F : 0.8F); + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/RedstoneControlPacket.java b/src/main/java/com/sorrowmist/useless/network/RedstoneControlPacket.java new file mode 100644 index 00000000..8bfc6db4 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/RedstoneControlPacket.java @@ -0,0 +1,51 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.blockentities.AdvancedAlloyFurnaceBlockEntity; +import com.sorrowmist.useless.content.blockentities.multiblock.MultiblockAlloyFurnaceCoreBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.block.entity.BlockEntity; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +/** + * 客户端→服务器:循环红石控制模式。 + */ +public class RedstoneControlPacket implements CustomPacketPayload { + + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "redstone_control")); + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, pkt) -> buf.writeBlockPos(pkt.pos), + buf -> new RedstoneControlPacket(buf.readBlockPos()) + ); + private final BlockPos pos; + + public RedstoneControlPacket(BlockPos pos) { + this.pos = pos; + } + + public static void handle(RedstoneControlPacket msg, IPayloadContext ctx) { + ctx.enqueueWork(() -> { + ServerPlayer player = (ServerPlayer) ctx.player(); + if (player == null) return; + + BlockEntity be = player.level().getBlockEntity(msg.pos); + if (be instanceof AdvancedAlloyFurnaceBlockEntity furnace) { + furnace.cycleRedstoneControlMode(); + } else if (be instanceof MultiblockAlloyFurnaceCoreBlockEntity furnace) { + furnace.cycleRedstoneControlMode(); + } + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/RitualSatchelPlacePacket.java b/src/main/java/com/sorrowmist/useless/network/RitualSatchelPlacePacket.java new file mode 100644 index 00000000..39a188f3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/RitualSatchelPlacePacket.java @@ -0,0 +1,77 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.items.EndlessBeafItem; +import com.sorrowmist.useless.core.component.UComponents; +import com.sorrowmist.useless.utils.UselessItemUtils; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.fml.ModList; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +/** + * 客户端在魔典预览的五芒星上右键后,将「要摆放哪座仪式」上报给服务端。 + * + *

方块的实际摆放与 AE 扣料全部在服务端完成,客户端仅负责上报预览信息, + * 以避免客户端伪造摆放结果。

+ */ +public class RitualSatchelPlacePacket implements CustomPacketPayload { + + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "ritual_satchel_place")); + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, pkt) -> { + buf.writeResourceLocation(pkt.multiblockId); + buf.writeBlockPos(pkt.anchor); + buf.writeEnum(pkt.facing); + }, + buf -> new RitualSatchelPlacePacket( + buf.readResourceLocation(), buf.readBlockPos(), buf.readEnum(Rotation.class)) + ); + + private final ResourceLocation multiblockId; + private final BlockPos anchor; + private final Rotation facing; + + public RitualSatchelPlacePacket(ResourceLocation multiblockId, BlockPos anchor, Rotation facing) { + this.multiblockId = multiblockId; + this.anchor = anchor; + this.facing = facing; + } + + public static void handle(RitualSatchelPlacePacket msg, IPayloadContext ctx) { + ctx.enqueueWork(() -> { + if (!(ctx.player() instanceof ServerPlayer player)) return; + // 没装 occultism 就没有这套能力,直接丢弃,避免类加载失败 + if (!ModList.get().isLoaded("occultism")) return; + + var toolEntry = UselessItemUtils.findTargetToolInHands(player); + if (toolEntry.isEmpty()) return; + + ItemStack stack = toolEntry.get().getKey(); + if (!(stack.getItem() instanceof EndlessBeafItem)) return; + if (!com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.BeefRitualSatchelComponent.get(), false)) return; + if (!(player.level() instanceof ServerLevel serverLevel)) return; + + com.sorrowmist.useless.compat.forge1201.OptionalIntegration.call("occultism", + "com.sorrowmist.useless.compat.occultism.RitualSatchelCompat","placeFromAe",Void.class,null, + new Class[]{ServerLevel.class,ServerPlayer.class,ItemStack.class,ResourceLocation.class,BlockPos.class,Rotation.class}, + serverLevel,player,stack,msg.multiblockId,msg.anchor,msg.facing); + + player.containerMenu.broadcastChanges(); + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/SelectOmniversalPatternRecipePacket.java b/src/main/java/com/sorrowmist/useless/network/SelectOmniversalPatternRecipePacket.java new file mode 100644 index 00000000..bf0f0bba --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/SelectOmniversalPatternRecipePacket.java @@ -0,0 +1,78 @@ +package com.sorrowmist.useless.network; + +import appeng.helpers.IPatternTerminalMenuHost; +import appeng.menu.me.items.PatternEncodingTermMenu; +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.PendingOmniversalPatternHolder; +import com.sorrowmist.useless.content.recipe.AlloyFurnaceRecipeIdentity; +import com.sorrowmist.useless.content.recipe.RecipeSourceIds; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +/** + * Tells the server which Omniversal Alloy Furnace recipe the player just transferred from JEI into a + * pattern encoding terminal, so the pattern that terminal encodes next becomes an Omniversal Pattern + * bound to that exact recipe — mold included. + * + *

Sent before the {@code SET_FILTER} packets that fill the terminal slots, so the pick is on + * record no matter how quickly encoding follows. + */ +public record SelectOmniversalPatternRecipePacket( + int containerId, ResourceLocation recipeId, String fingerprint, String sourceId) implements CustomPacketPayload { + public SelectOmniversalPatternRecipePacket { + sourceId = RecipeSourceIds.normalize(sourceId); + } + + public static final Type TYPE = + new Type<>(UselessMod.id("select_omniversal_pattern_recipe")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.of( + (buffer, packet) -> { + buffer.writeVarInt(packet.containerId); + com.sorrowmist.useless.compat.forge1201.network.PortCodecs.RESOURCE_LOCATION.encode(buffer, packet.recipeId); + buffer.writeUtf(packet.fingerprint); + buffer.writeUtf(packet.sourceId); + }, + buffer -> new SelectOmniversalPatternRecipePacket( + buffer.readVarInt(), com.sorrowmist.useless.compat.forge1201.network.PortCodecs.RESOURCE_LOCATION.decode(buffer), + buffer.readUtf(), buffer.readUtf())); + + public static void handle(SelectOmniversalPatternRecipePacket packet, IPayloadContext context) { + context.enqueueWork(() -> { + if (!(context.player() instanceof ServerPlayer player) + || !(player.containerMenu instanceof PatternEncodingTermMenu menu) + || menu.containerId != packet.containerId) return; + // getTarget() is the IPatternTerminalMenuHost the menu was opened against, which owns the + // PatternEncodingLogic the encoded pattern will pass through: the terminal part for the + // wired block, or ae2wtlib's WETMenuHost for the wireless one. + if (!(menu.getTarget() instanceof IPatternTerminalMenuHost host)) return; + if (!(host.getLogic() instanceof PendingOmniversalPatternHolder holder)) return; + + AlloyFurnaceRecipeIdentity identity; + try { + identity = new AlloyFurnaceRecipeIdentity(packet.recipeId, packet.fingerprint); + } catch (RuntimeException ignored) { + // A blank fingerprint can only come from a malformed packet; drop it rather than + // leaving a half-valid pick behind. + holder.uselessMod$setPendingOmniversalRecipe(null); + holder.uselessMod$setPendingOmniversalSourceId(null); + return; + } + holder.uselessMod$setPendingOmniversalRecipe(identity); + holder.uselessMod$setPendingOmniversalSourceId(packet.sourceId()); + // The encoded pattern can arrive before this custom selection packet. Re-run the same + // conversion check now so the terminal does not remain with a plain pattern forever. + holder.uselessMod$tryConvertPendingOmniversalPattern(); + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/ShapeSwitchPacket.java b/src/main/java/com/sorrowmist/useless/network/ShapeSwitchPacket.java new file mode 100644 index 00000000..24803791 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/ShapeSwitchPacket.java @@ -0,0 +1,47 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.utils.mining.MiningDispatcher; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +/** + * 连锁形状切换包。 + * + *

客户端在按住连锁键时以 Shift + 滚轮触发,只上报滚动方向;真正的取模循环与服务端 + * 状态更新由 {@link MiningDispatcher#cycleShape} 完成,避免两端各自维护下标而错位。 + */ +public class ShapeSwitchPacket implements CustomPacketPayload { + + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "shape_switch")); + + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, pkt) -> buf.writeVarInt(pkt.delta), + buf -> new ShapeSwitchPacket(buf.readVarInt()) + ); + + /** 滚动步数:正数为向后一个形状,负数为向前一个。 */ + private final int delta; + + public ShapeSwitchPacket(int delta) { + this.delta = delta; + } + + public static void handle(ShapeSwitchPacket msg, IPayloadContext ctx) { + ctx.enqueueWork(() -> { + if (ctx.player() != null) { + MiningDispatcher.cycleShape(ctx.player(), msg.delta); + } + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/StaffLinkBindPacket.java b/src/main/java/com/sorrowmist/useless/network/StaffLinkBindPacket.java new file mode 100644 index 00000000..b52bdd4e --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/StaffLinkBindPacket.java @@ -0,0 +1,79 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.items.EndlessBeafItem; +import com.sorrowmist.useless.content.stafflink.StaffLinkBinding; +import com.sorrowmist.useless.utils.UselessItemUtils; +import com.sorrowmist.useless.world.stafflink.StaffLinkManager; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.ItemStack; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * 客户端请求绑定 / 解绑一个(或一批)锚点。 + * + *

绑定必须由客户端发起:修饰键状态(Shift、Ctrl)只存在于客户端,服务端在 + * {@code PlayerInteractEvent} 里读不到 Ctrl。所以服务端那边只负责取消原版交互, + * 真正的绑定逻辑一律走这个包。

+ * + * @param pos 被右键的方块坐标 + * @param batch 是否批量(按住 Ctrl):把连在一起的同种机器一起绑定 + */ +public record StaffLinkBindPacket(BlockPos pos, boolean batch) implements CustomPacketPayload { + + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "staff_link_bind")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.of( + (buffer, packet) -> { + buffer.writeBlockPos(packet.pos()); + buffer.writeBoolean(packet.batch()); + }, + buffer -> new StaffLinkBindPacket(buffer.readBlockPos(), buffer.readBoolean())); + + public static void handle(StaffLinkBindPacket packet, IPayloadContext ctx) { + ctx.enqueueWork(() -> { + if (!(ctx.player() instanceof ServerPlayer player)) { + return; + } + if (!(player.level() instanceof ServerLevel level)) { + return; + } + // 服务端再验一次手持物:包是可以伪造的,不能只信客户端说「我拿着杖」。 + var toolEntry = UselessItemUtils.findTargetToolInHands(player); + if (toolEntry.isEmpty()) { + return; + } + ItemStack staff = toolEntry.get().getKey(); + if (!(staff.getItem() instanceof EndlessBeafItem)) { + return; + } + if (!EndlessBeafItem.isStaffLinkEnabled(staff)) { + return; + } + // 网络挂在归属者(玩家或队伍)名下,不再挂在杖上;杖只是「拿着它才能操作」的凭据。 + // 先并一次队,免得刚组队时这里先新建一张空网络。 + StaffLinkManager.mergePersonalIntoTeam(player.server, player); + UUID ownerId = StaffLinkManager.ownerIdOf(player); + if (packet.batch()) { + StaffLinkBinding.toggleBatch(level, player, ownerId, packet.pos()); + } else { + StaffLinkBinding.toggle(level, player, ownerId, packet.pos()); + } + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/StaffLinkConfigurePacket.java b/src/main/java/com/sorrowmist/useless/network/StaffLinkConfigurePacket.java new file mode 100644 index 00000000..982a0a87 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/StaffLinkConfigurePacket.java @@ -0,0 +1,200 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.menus.StaffLinkMenu; +import com.sorrowmist.useless.content.stafflink.LinkFilterCondition; +import com.sorrowmist.useless.content.stafflink.LinkFilterPattern; +import com.sorrowmist.useless.content.stafflink.LinkFilterSlot; +import com.sorrowmist.useless.content.stafflink.LinkMedium; +import com.sorrowmist.useless.content.stafflink.ResourceFamily; +import com.sorrowmist.useless.content.stafflink.StaffLinkEngine; +import com.sorrowmist.useless.content.stafflink.StaffLinkRoute; +import com.sorrowmist.useless.world.stafflink.StaffLinkManager; +import com.sorrowmist.useless.world.stafflink.StaffLinkNetwork; +import com.sorrowmist.useless.world.stafflink.StaffLinkSavedData; +import net.minecraft.core.GlobalPos; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.fluids.FluidStack; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +/** + * 客户端提交一条线路配置。 + * + *

服务端是唯一的校验点:只接受「当前打开的网络 + 已绑定的锚点 + 当前环境支持的资源类型」, + * 数值再由 {@link StaffLinkRoute} 的构造器夹取一次。通过后回发整网快照,客户端以服务端为准。

+ */ +public record StaffLinkConfigurePacket(GlobalPos anchor, int route, StaffLinkRoute config) + implements CustomPacketPayload { + + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "staff_link_configure")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.of( + (buffer, packet) -> { + StaffLinkRoute.writeAnchor(buffer, packet.anchor()); + buffer.writeVarInt(packet.route()); + StaffLinkRoute.STREAM_CODEC.encode(buffer, packet.config()); + }, + buffer -> { + GlobalPos anchor = StaffLinkRoute.readAnchor(buffer); + int route = buffer.readVarInt(); + StaffLinkRoute config = StaffLinkRoute.STREAM_CODEC.decode(buffer); + return new StaffLinkConfigurePacket(anchor, route, config); + }); + + public static void handle(StaffLinkConfigurePacket packet, IPayloadContext ctx) { + ctx.enqueueWork(() -> { + if (!(ctx.player() instanceof ServerPlayer player)) { + return; + } + if (!(player.containerMenu instanceof StaffLinkMenu menu)) { + return; + } + // 越权校验:界面所属的网络必须挂在他(或他队伍)名下。 + if (!StaffLinkManager.canAccess(player.server, player, menu.getNetworkId())) { + return; + } + StaffLinkNetwork network = StaffLinkManager.networkById(player.server, menu.getNetworkId()); + if (network == null || !network.isBound(packet.anchor())) { + return; + } + if (packet.route() < 0 || packet.route() >= StaffLinkNetwork.ROUTE_COUNT) { + return; + } + if (!packet.config().medium().isSupported()) { + return; + } + + // 以包里的 anchor / route 为准重写一次,防止客户端送来不匹配的字段。 + StaffLinkRoute sanitized = new StaffLinkRoute( + packet.anchor(), + packet.route(), + packet.config().enabled(), + packet.config().flow(), + packet.config().medium(), + sanitizeAmount(packet.config()), + sanitizeInterval(packet.config()), + packet.config().side(), + packet.config().weight(), + sanitizeFilter(packet.config().filter(), packet.config().medium())); + + network.putRoute(sanitized); + StaffLinkSavedData.get(player.server).markDirty(); + // 改完立刻重跑:新开的配对不该等上一次排定的退避。 + StaffLinkEngine.wake(network.id()); + PacketDistributor.sendToPlayer(player, StaffLinkSyncPacket.of( + player.server, StaffLinkManager.ownerIdOf(player), network)); + }); + } + + /** + * 应力线路上「数量」承载的是目标转速,要夹进合理区间。 + * + *

这里只做一个绝对上界的粗夹:真正的上限是动力学配置里的最高转速,那是可选集成 + * 侧才知道的事,由桥在运行时再夹一次。别的介质保持原值——它们的「数量」本来就没有上限。

+ * + *

气压是例外:它的「数量」承载的是目标气压(毫巴),真空是负值, + * 所以必须夹到 {@code [-1000, 20000]}(-1 ~ 20 bar)而不是「至少 1」。

+ */ + private static long sanitizeAmount(StaffLinkRoute config) { + if (config.medium().family() == ResourceFamily.PRESSURE) { + return Math.max(StaffLinkRoute.PRESSURE_MIN_MBAR, + Math.min(StaffLinkRoute.PRESSURE_MAX_MBAR, config.amount())); + } + if (config.medium().family() != ResourceFamily.STRESS) { + return config.amount(); + } + return Math.max(StaffLinkRoute.MIN_AMOUNT, + Math.min(StaffLinkRoute.STRESS_RPM_HARD_LIMIT, config.amount())); + } + + /** + * 应力线路上「周期」承载的是旋转方向,只允许「顺时针 / 逆时针」两个取值。 + * + *

别的取值一律收敛成顺时针,而不是拒绝整条配置:玩家可能只是刚从别的介质切过来, + * 那两个数字还没改,为此把配置打回去反而更让人困惑。

+ */ + private static int sanitizeInterval(StaffLinkRoute config) { + if (config.medium().family() != ResourceFamily.STRESS) { + return config.interval(); + } + return config.interval() == StaffLinkRoute.STRESS_COUNTER_CLOCKWISE + ? StaffLinkRoute.STRESS_COUNTER_CLOCKWISE + : StaffLinkRoute.STRESS_CLOCKWISE; + } + + /** + * 逐格清洗过滤器。 + * + *

一格现在有五样东西:标记、模式文本、白名单/黑名单方向、两条控制条件。{@link StaffLinkRoute} + * 的构造器只会把「格数」补齐到 {@code FILTER_LIMIT},不校验格内容; + * 一个被改过的客户端可以塞进来非法模式、越界的条件或跨族的控制材料。所以这里逐格重造一遍:

+ * + *
    + *
  • 模式文本必须能被 {@link LinkFilterPattern#parse} 接受,否则整格清空 + * (宁可「什么都不搬」,也不要留一个语义不明的字符串在存档里)。
  • + *
  • {@code exclude} 原样布尔。
  • + *
  • 两条条件:方向未知 → OFF;阈值夹非负;控制材料与线路族不符 / 该族不吃条件 → 清空。
  • + *
+ * + *

不能跳过这一步直接把 {@code packet.config().filter()} 传下去: + * 那样等于把服务端当成客户端的镜子,任何越界值都会直接落进存档。

+ */ + private static java.util.List sanitizeFilter( + java.util.List received, LinkMedium medium) { + java.util.List cleaned = + new java.util.ArrayList<>(StaffLinkRoute.FILTER_LIMIT); + for (int index = 0; index < StaffLinkRoute.FILTER_LIMIT; index++) { + LinkFilterSlot slot = received != null && index < received.size() + ? received.get(index) : null; + if (slot == null || slot.isEmpty()) { + cleaned.add(LinkFilterSlot.EMPTY); + continue; + } + LinkFilterSlot base; + if (slot.isPattern()) { + LinkFilterPattern pattern = LinkFilterPattern.parse(slot.pattern()); + if (pattern == null) { + cleaned.add(LinkFilterSlot.EMPTY); + continue; + } + base = LinkFilterSlot.ofPattern(pattern); + } else { + // 物品 / 流体标记:构造器已经归一化(count=1、类型互斥)。 + base = slot; + } + cleaned.add(base + .withExclude(slot.exclude()) + .withConditions(sanitizeCondition(slot.outCond(), medium), + sanitizeCondition(slot.inCond(), medium))); + } + return cleaned; + } + + /** 清洗一条控制条件:方向未知 → OFF;阈值非负;控制材料与线路族不符 → 清空。 */ + private static LinkFilterCondition sanitizeCondition(LinkFilterCondition cond, LinkMedium medium) { + if (cond == null || cond.isOff()) { + return LinkFilterCondition.OFF; + } + ResourceFamily family = medium.family(); + // 条件只对物品 / 流体有意义:化学品没有存量查询,能量 / 魔源 / 应力 / 气压压根不吃过滤器。 + if (family != ResourceFamily.ITEM && family != ResourceFamily.FLUID) { + return LinkFilterCondition.OFF; + } + ItemStack item = family == ResourceFamily.ITEM ? cond.item() : ItemStack.EMPTY; + FluidStack fluid = family == ResourceFamily.FLUID ? cond.fluid() : FluidStack.EMPTY; + return new LinkFilterCondition(cond.op(), item, fluid, Math.max(0L, cond.value())); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/StaffLinkCyclePacket.java b/src/main/java/com/sorrowmist/useless/network/StaffLinkCyclePacket.java new file mode 100644 index 00000000..890886d9 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/StaffLinkCyclePacket.java @@ -0,0 +1,95 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.items.EndlessBeafItem; +import com.sorrowmist.useless.content.menus.StaffLinkMenu; +import com.sorrowmist.useless.utils.UselessItemUtils; +import com.sorrowmist.useless.world.stafflink.StaffLinkManager; +import com.sorrowmist.useless.world.stafflink.StaffLinkNetwork; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.ItemStack; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.UUID; + +/** + * 切换当前网络:手持造化杖 Shift+滚轮,或者界面上的 {@code <} / {@code >} 按钮。 + * + *

包体只有一个方向值,服务端自己验手持、自己算下一张——客户端无从指定目标。

+ * + *

切的是归属者(玩家或队伍)名下的当前网络,与手上这把杖无关:换一把杖、 + * 把杖放进箱子,切到的那张仍然是你的。

+ */ +public record StaffLinkCyclePacket(int delta) implements CustomPacketPayload { + + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "staff_link_cycle")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.of( + (buffer, packet) -> buffer.writeVarInt(packet.delta()), + buffer -> new StaffLinkCyclePacket(buffer.readVarInt())); + + public static void handle(StaffLinkCyclePacket packet, IPayloadContext ctx) { + ctx.enqueueWork(() -> { + if (!(ctx.player() instanceof ServerPlayer player)) { + return; + } + // 服务端再验一次手持物与模式开关:包是可以伪造的。 + var toolEntry = UselessItemUtils.findTargetToolInHands(player); + if (toolEntry.isEmpty()) { + return; + } + ItemStack staff = toolEntry.get().getKey(); + if (!(staff.getItem() instanceof EndlessBeafItem)) { + return; + } + if (!EndlessBeafItem.isStaffLinkEnabled(staff)) { + return; + } + + // 先并一次队,免得刚组队时这里先给他新建一张空网络(看着像配置全丢了)。 + StaffLinkManager.mergePersonalIntoTeam(player.server, player); + UUID ownerId = StaffLinkManager.ownerIdOf(player); + if (!StaffLinkManager.cycleActive(player.server, ownerId, packet.delta())) { + return; + } + + List ids = StaffLinkManager.networkIds(player.server, ownerId); + int index = Math.floorMod(StaffLinkManager.activeIndex(player.server, ownerId), + Math.max(1, ids.size())); + StaffLinkNetwork network = StaffLinkManager.activeNetwork(player.server, ownerId); + if (network != null) { + // 界面开着时要跟着换一张网络,否则后续的编辑还是打到旧网络上。 + // 服务端菜单也必须换:所有编辑包都按菜单里的 networkId 寻址。 + if (player.containerMenu instanceof StaffLinkMenu menu) { + menu.setNetworkId(network.id()); + } + PacketDistributor.sendToPlayer(player, + StaffLinkSyncPacket.of(player.server, ownerId, network)); + } + player.displayClientMessage(Component.translatable( + "gui.useless_mod.wireless_logistics.network_switched", + index + 1, ids.size(), displayName(network)), true); + }); + } + + static Component displayName(StaffLinkNetwork network) { + if (network == null || network.name().isEmpty()) { + return Component.translatable("gui.useless_mod.wireless_logistics.network_unnamed"); + } + return Component.literal(network.name()); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/StaffLinkDetachPacket.java b/src/main/java/com/sorrowmist/useless/network/StaffLinkDetachPacket.java new file mode 100644 index 00000000..6ac4c1dc --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/StaffLinkDetachPacket.java @@ -0,0 +1,69 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.menus.StaffLinkMenu; +import com.sorrowmist.useless.content.stafflink.StaffLinkEngine; +import com.sorrowmist.useless.content.stafflink.StaffLinkRoute; +import com.sorrowmist.useless.world.stafflink.StaffLinkManager; +import com.sorrowmist.useless.world.stafflink.StaffLinkNetwork; +import com.sorrowmist.useless.world.stafflink.StaffLinkSavedData; +import net.minecraft.core.GlobalPos; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +/** + * 客户端请求解绑一个锚点。 + * + *

服务端只认「当前打开的那个物流界面所属的网络」,因此包体里不带网络 ID—— + * 玩家无法用它去动别人的网络。

+ */ +public record StaffLinkDetachPacket(GlobalPos anchor) implements CustomPacketPayload { + + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "staff_link_detach")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.of( + (buffer, packet) -> StaffLinkRoute.writeAnchor(buffer, packet.anchor()), + buffer -> new StaffLinkDetachPacket(StaffLinkRoute.readAnchor(buffer))); + + public static void handle(StaffLinkDetachPacket packet, IPayloadContext ctx) { + ctx.enqueueWork(() -> { + if (!(ctx.player() instanceof ServerPlayer player)) { + return; + } + if (!(player.containerMenu instanceof StaffLinkMenu menu)) { + return; + } + // 越权校验:界面所属的网络必须挂在他(或他队伍)名下。 + if (!StaffLinkManager.canAccess(player.server, player, menu.getNetworkId())) { + return; + } + StaffLinkNetwork network = StaffLinkManager.networkById(player.server, menu.getNetworkId()); + if (network == null || !network.isBound(packet.anchor())) { + return; + } + network.detach(packet.anchor()); + StaffLinkSavedData data = StaffLinkSavedData.get(player.server); + data.markDirty(); + if (network.isEmpty()) { + // 锚点清空 = 这张网络没有意义了:连登记一起删掉,界面随后自动关闭。 + data.remove(network.id()); + return; + } + StaffLinkEngine.wake(network.id()); + PacketDistributor.sendToPlayer(player, StaffLinkSyncPacket.of( + player.server, StaffLinkManager.ownerIdOf(player), network)); + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/StaffLinkHighlightPacket.java b/src/main/java/com/sorrowmist/useless/network/StaffLinkHighlightPacket.java new file mode 100644 index 00000000..524da145 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/StaffLinkHighlightPacket.java @@ -0,0 +1,158 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.client.network.ClientPacketHandlers; +import com.sorrowmist.useless.content.items.EndlessBeafItem; +import com.sorrowmist.useless.content.stafflink.LinkFlow; +import com.sorrowmist.useless.content.stafflink.StaffLinkRoute; +import com.sorrowmist.useless.utils.UselessItemUtils; +import com.sorrowmist.useless.world.stafflink.StaffLinkManager; +import com.sorrowmist.useless.world.stafflink.StaffLinkNetwork; +import net.minecraft.core.GlobalPos; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.fml.loading.FMLEnvironment; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +/** + * 服务端把「当前网络各锚点按流向分好的三组坐标」发给客户端,用于手持杖时的世界高亮。 + * + *

分三组而不是带一个流向字段:客户端只关心「用什么颜色画哪个方块」, + * 分组后渲染侧就是三次遍历,不用逐条判类型。

+ * + *
    + *
  • {@code release} —— 至少有一条启用的 {@link LinkFlow#RELEASE} 线路(发送端,暖色)
  • + *
  • {@code absorb} —— 没有启用的 RELEASE,但有启用的 ABSORB(接收端,冷色)
  • + *
  • {@code disabled} —— 所有线路都没启用(暗灰)
  • + *
+ * + *

同一锚点同时有启用的 RELEASE 和 ABSORB 时归 {@code release}(RELEASE 优先)。 + * 不带 request id:提示是幂等的「最新值覆盖」。

+ */ +public record StaffLinkHighlightPacket(List release, + List absorb, + List disabled) implements CustomPacketPayload { + private static final int MAX_ANCHORS = 4096; + + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "staff_link_highlight")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.of( + (buf, packet) -> { + writeAnchors(buf, packet.release); + writeAnchors(buf, packet.absorb); + writeAnchors(buf, packet.disabled); + }, + buf -> new StaffLinkHighlightPacket( + readAnchors(buf), readAnchors(buf), readAnchors(buf)) + ); + + public StaffLinkHighlightPacket { + release = List.copyOf(release); + absorb = List.copyOf(absorb); + disabled = List.copyOf(disabled); + if (release.size() + absorb.size() + disabled.size() > MAX_ANCHORS) { + throw new IllegalArgumentException("Staff link highlight is too large"); + } + } + + /** 锚点带维度,所以复用 {@link StaffLinkRoute} 现成的「维度 + 坐标」编解码。 */ + private static void writeAnchors(FriendlyByteBuf buf, List anchors) { + buf.writeVarInt(anchors.size()); + for (GlobalPos anchor : anchors) { + StaffLinkRoute.writeAnchor(buf, anchor); + } + } + + private static List readAnchors(FriendlyByteBuf buf) { + int size = buf.readVarInt(); + if (size < 0 || size > MAX_ANCHORS) { + throw new IllegalArgumentException("Invalid staff link highlight size: " + size); + } + List anchors = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + anchors.add(StaffLinkRoute.readAnchor(buf)); + } + return anchors; + } + + /** 服务端:读玩家手里的杖 → 取他(或他队伍)当前的网络 → 按流向三分类回给客户端。 */ + public static void answer(ServerPlayer player) { + // 高亮仍然要求「手持一把开着无线物流模式的杖」——这是玩家主动要看的意思表示。 + var toolEntry = UselessItemUtils.findTargetToolInHands(player); + if (toolEntry.isEmpty()) { + return; + } + ItemStack staff = toolEntry.get().getKey(); + if (!(staff.getItem() instanceof EndlessBeafItem)) { + // findTargetToolInHands 也会认 omnitools 的扳手模式,那类工具没有物流网络。 + return; + } + if (!EndlessBeafItem.isStaffLinkEnabled(staff)) { + return; + } + + MinecraftServer server = player.getServer(); + // 但高亮的是「他名下的当前网络」——换一把杖、把杖放进背包都不影响看到的是哪张网络。 + StaffLinkNetwork network = server == null + ? null : StaffLinkManager.activeNetwork(server, StaffLinkManager.ownerIdOf(player)); + if (network == null) { + // 一张有效网络都没有:回空包,让客户端把上一张网络的残留框清掉。 + PacketDistributor.sendToPlayer(player, + new StaffLinkHighlightPacket(List.of(), List.of(), List.of())); + return; + } + + List release = new ArrayList<>(); + List absorb = new ArrayList<>(); + List disabled = new ArrayList<>(); + for (GlobalPos anchor : network.anchors()) { + boolean hasRelease = false; + boolean hasAbsorb = false; + for (StaffLinkRoute route : network.routes()) { + if (!route.enabled() || !route.anchor().equals(anchor)) { + continue; + } + if (route.flow() == LinkFlow.RELEASE) { + hasRelease = true; + } else { + hasAbsorb = true; + } + } + if (hasRelease) { + release.add(anchor); + } else if (hasAbsorb) { + absorb.add(anchor); + } else { + disabled.add(anchor); + } + } + PacketDistributor.sendToPlayer(player, + new StaffLinkHighlightPacket(release, absorb, disabled)); + } + + public static void handle(StaffLinkHighlightPacket packet, IPayloadContext context) { + // 客户端类型收敛到 ClientPacketHandlers:本类会在专用服务端被加载, + // 直接引用客户端渲染器会让校验器在服务端解析到客户端类型。 + if (FMLEnvironment.dist != Dist.CLIENT) { + return; + } + context.enqueueWork(() -> ClientPacketHandlers.handleStaffLinkHighlight(packet)); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/StaffLinkHighlightRequestPacket.java b/src/main/java/com/sorrowmist/useless/network/StaffLinkHighlightRequestPacket.java new file mode 100644 index 00000000..6558b50c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/StaffLinkHighlightRequestPacket.java @@ -0,0 +1,36 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +/** + * 客户端请求「我手上这把造化杖的当前网络里,各锚点分别是什么流向」。 + * + *

不带参数:服务端自己验手持、自己取该玩家(或队伍)当前的网络,客户端无从伪造目标。 + * 手持杖时的世界高亮每 10 tick 问一次,见 {@code StaffLinkHighlightRenderer}。

+ */ +public record StaffLinkHighlightRequestPacket() implements CustomPacketPayload { + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "staff_link_highlight_request")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.unit(new StaffLinkHighlightRequestPacket()); + + public static void handle(StaffLinkHighlightRequestPacket packet, IPayloadContext context) { + context.enqueueWork(() -> { + if (context.player() instanceof ServerPlayer player) { + StaffLinkHighlightPacket.answer(player); + } + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/StaffLinkNetworkPacket.java b/src/main/java/com/sorrowmist/useless/network/StaffLinkNetworkPacket.java new file mode 100644 index 00000000..5591f104 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/StaffLinkNetworkPacket.java @@ -0,0 +1,119 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.menus.StaffLinkMenu; +import com.sorrowmist.useless.world.stafflink.StaffLinkManager; +import com.sorrowmist.useless.world.stafflink.StaffLinkNetwork; +import com.sorrowmist.useless.world.stafflink.StaffLinkSavedData; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * 网络级别的操作:新建一张、解散当前这张、给当前这张改名。 + * + *

三件事共用一条包:它们的作用对象都是「界面上当前那张网络」,参数形状也一样 + * (一个动作 + 一个可选名字),拆成三条包只会多两份样板。

+ * + *

网络挂在归属者(玩家或队伍)名下,所以新建 / 解散只需要归属者 ID, + * 不再需要「在玩家身上找一把登记了这张网络的杖」。每个动作前都过一遍 + * {@link StaffLinkManager#canAccess},防止伪造包去动别人的网络。

+ */ +public record StaffLinkNetworkPacket(Action action, String name) implements CustomPacketPayload { + + public enum Action { + /** 新建一张空网络并切过去。 */ + NEW, + /** 解散当前这张网络。 */ + DISSOLVE, + /** 给当前这张网络改名。 */ + RENAME + } + + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "staff_link_network")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.of( + (buffer, packet) -> { + buffer.writeEnum(packet.action()); + buffer.writeUtf(packet.name(), StaffLinkNetwork.MAX_NAME); + }, + buffer -> new StaffLinkNetworkPacket( + buffer.readEnum(Action.class), + buffer.readUtf(StaffLinkNetwork.MAX_NAME))); + + public static void handle(StaffLinkNetworkPacket packet, IPayloadContext ctx) { + ctx.enqueueWork(() -> { + if (!(ctx.player() instanceof ServerPlayer player)) { + return; + } + if (!(player.containerMenu instanceof StaffLinkMenu menu)) { + return; + } + UUID networkId = menu.getNetworkId(); + // 越权校验:只能动自己(或自己队伍)名下的网络。 + if (!StaffLinkManager.canAccess(player.server, player, networkId)) { + return; + } + switch (packet.action()) { + case RENAME -> rename(player, networkId, packet.name()); + case NEW -> create(player); + case DISSOLVE -> dissolve(player, networkId); + } + }); + } + + private static void rename(ServerPlayer player, UUID networkId, String name) { + StaffLinkNetwork network = StaffLinkManager.networkById(player.server, networkId); + if (network == null) { + return; + } + network.setName(name); + StaffLinkSavedData.get(player.server).markDirty(); + PacketDistributor.sendToPlayer(player, StaffLinkSyncPacket.of( + player.server, StaffLinkManager.ownerIdOf(player), network)); + } + + private static void create(ServerPlayer player) { + UUID ownerId = StaffLinkManager.ownerIdOf(player); + StaffLinkNetwork created = StaffLinkManager.createNetwork(player.server, ownerId); + if (created == null) { + return; + } + // 新网络成了当前网络,服务端菜单也要一起切过去。 + if (player.containerMenu instanceof StaffLinkMenu menu) { + menu.setNetworkId(created.id()); + } + PacketDistributor.sendToPlayer(player, + StaffLinkSyncPacket.of(player.server, ownerId, created)); + } + + private static void dissolve(ServerPlayer player, UUID networkId) { + UUID ownerId = StaffLinkManager.ownerIdOf(player); + StaffLinkManager.dissolveActive(player.server, ownerId); + + StaffLinkNetwork next = StaffLinkManager.activeNetwork(player.server, ownerId); + if (next == null) { + // 一张都不剩了:界面没有可编辑的对象,直接关掉。 + player.closeContainer(); + return; + } + // 解散后会自动切到相邻的一张,服务端菜单必须跟着走。 + if (player.containerMenu instanceof StaffLinkMenu menu) { + menu.setNetworkId(next.id()); + } + PacketDistributor.sendToPlayer(player, StaffLinkSyncPacket.of(player.server, ownerId, next)); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/StaffLinkOpenPacket.java b/src/main/java/com/sorrowmist/useless/network/StaffLinkOpenPacket.java new file mode 100644 index 00000000..f2e51606 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/StaffLinkOpenPacket.java @@ -0,0 +1,83 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.items.EndlessBeafItem; +import com.sorrowmist.useless.content.menus.StaffLinkMenu; +import com.sorrowmist.useless.utils.UselessItemUtils; +import com.sorrowmist.useless.world.stafflink.StaffLinkManager; +import com.sorrowmist.useless.world.stafflink.StaffLinkNetwork; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.item.ItemStack; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** 客户端请求打开无线物流界面(仅手持造化杖时发送)。 */ +public class StaffLinkOpenPacket implements CustomPacketPayload { + + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "staff_link_open")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.of((buffer, packet) -> { + }, buffer -> new StaffLinkOpenPacket()); + + public StaffLinkOpenPacket() { + } + + public static void handle(StaffLinkOpenPacket msg, IPayloadContext ctx) { + ctx.enqueueWork(() -> { + if (!(ctx.player() instanceof ServerPlayer player)) { + return; + } + // 杖是「打开配置界面」的钥匙,所以仍然要求手持。 + var toolEntry = UselessItemUtils.findTargetToolInHands(player); + if (toolEntry.isEmpty()) { + return; + } + ItemStack staff = toolEntry.get().getKey(); + if (!(staff.getItem() instanceof EndlessBeafItem)) { + return; + } + + // 但网络挂在归属者(玩家或队伍)名下,与手上这把杖无关。 + // 先并一次队:玩家可能刚被拉进队伍,不等 20 tick 的周期刷新也能立刻看到合并后的结果, + // 否则这里会先给他新建一张空网络,看着像「配置全没了」。 + StaffLinkManager.mergePersonalIntoTeam(player.server, player); + UUID ownerId = StaffLinkManager.ownerIdOf(player); + StaffLinkNetwork network = StaffLinkManager.activeNetwork(player.server, ownerId, true); + if (network == null) { + return; + } + + net.minecraftforge.network.NetworkHooks.openScreen(player, new MenuProvider() { + @Override + public @NotNull Component getDisplayName() { + return Component.translatable("menu.useless_mod.wireless_logistics"); + } + + @Override + public AbstractContainerMenu createMenu(int containerId, Inventory inventory, Player ignored) { + return new StaffLinkMenu(containerId, inventory, network.id()); + } + }, buffer -> buffer.writeUUID(network.id())); + + PacketDistributor.sendToPlayer(player, StaffLinkSyncPacket.of(player.server, ownerId, network)); + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/StaffLinkRenamePacket.java b/src/main/java/com/sorrowmist/useless/network/StaffLinkRenamePacket.java new file mode 100644 index 00000000..dd4776e1 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/StaffLinkRenamePacket.java @@ -0,0 +1,65 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.menus.StaffLinkMenu; +import com.sorrowmist.useless.content.stafflink.StaffLinkRoute; +import com.sorrowmist.useless.world.stafflink.StaffLinkManager; +import com.sorrowmist.useless.world.stafflink.StaffLinkNetwork; +import com.sorrowmist.useless.world.stafflink.StaffLinkSavedData; +import net.minecraft.core.GlobalPos; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +/** + * 客户端给某个已绑定锚点改名(空串表示恢复成方块本名)。 + * + *

与其它无线物流包一样,服务端只认「当前打开的那个界面所属的网络」,包体里不带网络 ID。

+ */ +public record StaffLinkRenamePacket(GlobalPos anchor, String name) implements CustomPacketPayload { + + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "staff_link_rename")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.of( + (buffer, packet) -> { + StaffLinkRoute.writeAnchor(buffer, packet.anchor()); + buffer.writeUtf(packet.name(), StaffLinkNetwork.MAX_ANCHOR_NAME); + }, + buffer -> new StaffLinkRenamePacket( + StaffLinkRoute.readAnchor(buffer), + buffer.readUtf(StaffLinkNetwork.MAX_ANCHOR_NAME))); + + public static void handle(StaffLinkRenamePacket packet, IPayloadContext ctx) { + ctx.enqueueWork(() -> { + if (!(ctx.player() instanceof ServerPlayer player)) { + return; + } + if (!(player.containerMenu instanceof StaffLinkMenu menu)) { + return; + } + // 越权校验:界面所属的网络必须挂在他(或他队伍)名下。 + if (!StaffLinkManager.canAccess(player.server, player, menu.getNetworkId())) { + return; + } + StaffLinkNetwork network = StaffLinkManager.networkById(player.server, menu.getNetworkId()); + if (network == null || !network.isBound(packet.anchor())) { + return; + } + network.setAnchorName(packet.anchor(), packet.name()); + StaffLinkSavedData.get(player.server).markDirty(); + PacketDistributor.sendToPlayer(player, StaffLinkSyncPacket.of( + player.server, StaffLinkManager.ownerIdOf(player), network)); + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/StaffLinkReorderPacket.java b/src/main/java/com/sorrowmist/useless/network/StaffLinkReorderPacket.java new file mode 100644 index 00000000..d9acab98 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/StaffLinkReorderPacket.java @@ -0,0 +1,81 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.menus.StaffLinkMenu; +import com.sorrowmist.useless.content.stafflink.StaffLinkEngine; +import com.sorrowmist.useless.content.stafflink.StaffLinkRoute; +import com.sorrowmist.useless.world.stafflink.StaffLinkManager; +import com.sorrowmist.useless.world.stafflink.StaffLinkNetwork; +import com.sorrowmist.useless.world.stafflink.StaffLinkSavedData; +import net.minecraft.core.GlobalPos; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +/** + * 客户端请求把某个锚点挪到列表里的指定位置。 + * + *

次序影响的是同权重下谁先拿:引擎在同一条线路上按权重降序排,权重相同时保持列表 + * 次序,而分配时除不尽的余数留给靠前的那个。

+ * + *

传的是目标下标而不是「上移/下移一格」:界面上可能开着搜索过滤,可见邻居未必是 + * 列表里的邻居。由客户端把「越过上一个可见行」翻译成目标下标,服务端夹到合法范围后照做, + * 这样过滤状态下也是「眼睛看到的那样动」。

+ * + *

与解绑一样,服务端只认「当前打开的那个物流界面所属的网络」,包体里不带网络 ID, + * 玩家无法用它去动别人的网络。

+ * + * @param anchor 要移动的锚点 + * @param targetIndex 目标位置(在完整锚点列表里的下标) + */ +public record StaffLinkReorderPacket(GlobalPos anchor, int targetIndex) implements CustomPacketPayload { + + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "staff_link_reorder")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.of( + (buffer, packet) -> { + StaffLinkRoute.writeAnchor(buffer, packet.anchor()); + buffer.writeVarInt(packet.targetIndex()); + }, + buffer -> new StaffLinkReorderPacket( + StaffLinkRoute.readAnchor(buffer), buffer.readVarInt())); + + public static void handle(StaffLinkReorderPacket packet, IPayloadContext ctx) { + ctx.enqueueWork(() -> { + if (!(ctx.player() instanceof ServerPlayer player)) { + return; + } + if (!(player.containerMenu instanceof StaffLinkMenu menu)) { + return; + } + // 越权校验:界面所属的网络必须挂在他(或他队伍)名下。 + if (!StaffLinkManager.canAccess(player.server, player, menu.getNetworkId())) { + return; + } + StaffLinkNetwork network = StaffLinkManager.networkById(player.server, menu.getNetworkId()); + if (network == null || !network.isBound(packet.anchor())) { + return; + } + if (!network.moveAnchorTo(packet.anchor(), packet.targetIndex())) { + // 位置没变,不必回包。 + return; + } + StaffLinkSavedData.get(player.server).markDirty(); + // 次序变了,立刻重跑一遍,别让玩家等上一次排定的退避。 + StaffLinkEngine.wake(network.id()); + PacketDistributor.sendToPlayer(player, StaffLinkSyncPacket.of( + player.server, StaffLinkManager.ownerIdOf(player), network)); + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/StaffLinkStressStatusPacket.java b/src/main/java/com/sorrowmist/useless/network/StaffLinkStressStatusPacket.java new file mode 100644 index 00000000..628040be --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/StaffLinkStressStatusPacket.java @@ -0,0 +1,113 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.client.network.ClientPacketHandlers; +import com.sorrowmist.useless.content.stafflink.StaffLinkRoute; +import net.minecraft.core.GlobalPos; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.fml.loading.FMLEnvironment; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * 服务端 → 客户端:应力线路上各端点的运行状态。 + * + *

为什么需要单独一个包。线路配置(转速、方向)本来就随整网快照下发,界面上能编辑。 + * 但「源网络现在有多少可用应力」「目标网络现在需要多少」「这个输出为什么没转」都是 + * 每 tick 变化的运行时状态,不属于配置,快照里没有它们的位置。

+ * + *

只发给正开着无线物流界面的玩家,且每 20 tick 一次:这些数字是给人看的, + * 不需要每 tick 刷新,也没必要广播给所有人。

+ * + * @param entries 每个「网络 × 锚点 × 线路」一条 + */ +public record StaffLinkStressStatusPacket(List entries) implements CustomPacketPayload { + + /** 状态码长度上限;状态是固定的几个短标识,不是自由文本。 */ + public static final int MAX_STATE = 32; + + /** 三个应力量:提供 / 消耗 / 第三个(语义随角色变)。 */ + public record Numbers(float supplied, float consumed, float extra) { + public static final Numbers ZERO = new Numbers(0.0F, 0.0F, 0.0F); + } + + /** + * 一个端点的运行状态。 + * + * @param input 这一端是释放端(源)还是吸收端(输出) + * @param local 本锚点的三个数,用于容器列表里那一行: + * 释放端 = 它所在网络自己提供了多少;吸收端 = 目标网络需要多少 + * @param line 整条线路的三个数,用于配置区的状态行: + * 提供(各源网络容量合计)/ 消耗(各源网络自身负载合计)/ 可用(两者之差, + * 也就是这条链路真正能借出去的额度)。吸收端不使用,恒为 0 + * @param state 状态码;空串表示正常。界面按它取翻译键 + */ + public record Entry(UUID networkId, GlobalPos anchor, int route, boolean input, + Numbers local, Numbers line, String state) { + } + + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "staff_link_stress_status")); + + public static final StreamCodec STREAM_CODEC = + StreamCodec.of( + (buffer, packet) -> { + buffer.writeVarInt(packet.entries().size()); + for (Entry entry : packet.entries()) { + buffer.writeUUID(entry.networkId()); + StaffLinkRoute.writeAnchor(buffer, entry.anchor()); + buffer.writeVarInt(entry.route()); + buffer.writeBoolean(entry.input()); + writeNumbers(buffer, entry.local()); + writeNumbers(buffer, entry.line()); + buffer.writeUtf(entry.state(), MAX_STATE); + } + }, + buffer -> { + int size = buffer.readVarInt(); + List entries = new ArrayList<>(size); + for (int index = 0; index < size; index++) { + UUID networkId = buffer.readUUID(); + GlobalPos anchor = StaffLinkRoute.readAnchor(buffer); + int route = buffer.readVarInt(); + boolean input = buffer.readBoolean(); + Numbers local = readNumbers(buffer); + Numbers routeNumbers = readNumbers(buffer); + String state = buffer.readUtf(MAX_STATE); + entries.add(new Entry(networkId, anchor, route, input, + local, routeNumbers, state)); + } + return new StaffLinkStressStatusPacket(List.copyOf(entries)); + }); + + private static void writeNumbers(FriendlyByteBuf buffer, Numbers numbers) { + buffer.writeFloat(numbers.supplied()); + buffer.writeFloat(numbers.consumed()); + buffer.writeFloat(numbers.extra()); + } + + private static Numbers readNumbers(FriendlyByteBuf buffer) { + return new Numbers(buffer.readFloat(), buffer.readFloat(), buffer.readFloat()); + } + + public static void handle(StaffLinkStressStatusPacket packet, IPayloadContext context) { + // 客户端类型统一收敛到 ClientPacketHandlers,避免专用服务器加载类时解析到客户端类。 + if (FMLEnvironment.dist != Dist.CLIENT) { + return; + } + context.enqueueWork(() -> ClientPacketHandlers.handleStaffLinkStressStatus(packet)); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/StaffLinkSyncPacket.java b/src/main/java/com/sorrowmist/useless/network/StaffLinkSyncPacket.java new file mode 100644 index 00000000..0a022ad3 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/StaffLinkSyncPacket.java @@ -0,0 +1,61 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.client.network.ClientPacketHandlers; +import com.sorrowmist.useless.world.stafflink.StaffLinkManager; +import com.sorrowmist.useless.world.stafflink.StaffLinkNetwork; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.fml.loading.FMLEnvironment; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.UUID; + +/** + * 服务端 → 客户端:下发整张无线物流网络的最新状态。 + * + *

顺带把「第几张 / 共几张」一起发下去:网络列表现在挂在归属者(玩家或队伍)名下、 + * 存在服务端存档里,客户端手里没有这份列表,界面上那句「网络 2/3」就没法自己算出来。

+ */ +public record StaffLinkSyncPacket(StaffLinkNetwork network, int index, int count) + implements CustomPacketPayload { + + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "staff_link_sync")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.of( + (buffer, packet) -> { + StaffLinkNetwork.STREAM_CODEC.encode(buffer, packet.network()); + buffer.writeVarInt(packet.index()); + buffer.writeVarInt(packet.count()); + }, + buffer -> new StaffLinkSyncPacket( + StaffLinkNetwork.STREAM_CODEC.decode(buffer), + buffer.readVarInt(), buffer.readVarInt())); + + /** 服务端:按归属者算出位置信息,一起打包。 */ + public static StaffLinkSyncPacket of(MinecraftServer server, UUID ownerId, StaffLinkNetwork network) { + List ids = StaffLinkManager.networkIds(server, ownerId); + int index = ids.indexOf(network.id()); + return new StaffLinkSyncPacket(network, Math.max(0, index), Math.max(1, ids.size())); + } + + public static void handle(StaffLinkSyncPacket packet, IPayloadContext context) { + // 客户端类型统一收敛到 ClientPacketHandlers,避免专用服务器加载类时解析到客户端类。 + if (FMLEnvironment.dist != Dist.CLIENT) { + return; + } + context.enqueueWork(() -> ClientPacketHandlers.handleStaffLinkSync(packet)); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/TabKeyPressedPacket.java b/src/main/java/com/sorrowmist/useless/network/TabKeyPressedPacket.java new file mode 100644 index 00000000..bcbfcf3a --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/TabKeyPressedPacket.java @@ -0,0 +1,40 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.utils.mining.MiningDispatcher; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +public class TabKeyPressedPacket implements CustomPacketPayload { + + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "tab_key_pressed")); + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, pkt) -> { + buf.writeBoolean(pkt.pressed); + }, + buf -> new TabKeyPressedPacket(buf.readBoolean()) + ); + private final boolean pressed; + + public TabKeyPressedPacket(boolean pressed) { + this.pressed = pressed; + } + + public static void handle(TabKeyPressedPacket msg, IPayloadContext ctx) { + ctx.enqueueWork(() -> { + if (ctx.player() != null) { + MiningDispatcher.setTabPressed(ctx.player(), msg.pressed); + } + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/TankClearPacket.java b/src/main/java/com/sorrowmist/useless/network/TankClearPacket.java new file mode 100644 index 00000000..5062c497 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/TankClearPacket.java @@ -0,0 +1,64 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.blockentities.AdvancedAlloyFurnaceBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.block.entity.BlockEntity; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +public class TankClearPacket implements CustomPacketPayload { + + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "tank_clear")); + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, pkt) -> { + buf.writeBlockPos(pkt.pos); + buf.writeInt(pkt.tankIndex); + buf.writeBoolean(pkt.isInput); + buf.writeBoolean(pkt.chemical); + }, + buf -> new TankClearPacket(buf.readBlockPos(), buf.readInt(), buf.readBoolean(), buf.readBoolean()) + ); + private final BlockPos pos; + private final int tankIndex; + private final boolean isInput; + private final boolean chemical; + + public TankClearPacket(BlockPos pos, int tankIndex, boolean isInput) { + this(pos, tankIndex, isInput, false); + } + + public TankClearPacket(BlockPos pos, int tankIndex, boolean isInput, boolean chemical) { + this.pos = pos; + this.tankIndex = tankIndex; + this.isInput = isInput; + this.chemical = chemical; + } + + public static void handle(TankClearPacket msg, IPayloadContext ctx) { + ctx.enqueueWork(() -> { + ServerPlayer player = (ServerPlayer) ctx.player(); + if (player == null) return; + + BlockEntity be = player.level().getBlockEntity(msg.pos); + if (be instanceof AdvancedAlloyFurnaceBlockEntity furnace) { + if (msg.chemical) { + furnace.clearChemicalTank(msg.tankIndex, msg.isInput); + } else { + furnace.clearFluidTank(msg.tankIndex, msg.isInput); + } + } + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/TeleportKeyPacket.java b/src/main/java/com/sorrowmist/useless/network/TeleportKeyPacket.java new file mode 100644 index 00000000..68560e4a --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/TeleportKeyPacket.java @@ -0,0 +1,47 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.items.EndlessBeafItem; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +/** + * 短距传送(闪现)按键包。 + * + *

造化杖的短距闪现由组合键触发:手持造化杖并按住 Shift 右键,且准星未命中需要独占 + * 该组合键的方块时,由客户端上报本包。该组合键虽注册为 KeyMapping 供玩家改键, + * 但其冲突上下文恒为非激活,不参与运行时分发,触发判定由 + * {@code InputEvent.MouseButton.Pre} 与 {@code InputEvent.Key} 按物理按下边沿完成; + * 若上下文处于激活态,同键位的原版 keyUse 会被修饰键桶遮蔽,方块交互将一并失效。 + * 客户端只负责上报按键,目的地计算、碰撞检查与传送结算全部在服务端完成, + * 避免客户端决定落点。

+ */ +public class TeleportKeyPacket implements CustomPacketPayload { + + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "teleport_key")); + + public static final StreamCodec STREAM_CODEC = + StreamCodec.of((buf, packet) -> { }, buf -> new TeleportKeyPacket()); + + public TeleportKeyPacket() { + } + + public static void handle(TeleportKeyPacket msg, IPayloadContext ctx) { + ctx.enqueueWork(() -> { + if (ctx.player() instanceof ServerPlayer serverPlayer) { + EndlessBeafItem.performShortTeleport(serverPlayer); + } + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/ThermostatSetPacket.java b/src/main/java/com/sorrowmist/useless/network/ThermostatSetPacket.java new file mode 100644 index 00000000..c6d115a8 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/ThermostatSetPacket.java @@ -0,0 +1,58 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.content.blockentities.ThermostatBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +/** + * 客户端提交「温度调节器」方块的温度 / 开关。 + * + *

服务端是唯一校验点:方块必须是温度调节器、玩家必须在触及距离内。温度再由 + * {@link ThermostatBlockEntity#setTemperature(long)} 夹一次。

+ */ +public record ThermostatSetPacket(BlockPos pos, long temperature, boolean enabled) + implements CustomPacketPayload { + + public static final Type TYPE = new Type<>( + new ResourceLocation(UselessMod.MODID, "thermostat_set")); + + public static final StreamCodec STREAM_CODEC = + StreamCodec.of( + (buffer, packet) -> { + buffer.writeBlockPos(packet.pos()); + buffer.writeVarLong(packet.temperature()); + buffer.writeBoolean(packet.enabled()); + }, + buffer -> new ThermostatSetPacket( + buffer.readBlockPos(), buffer.readVarLong(), buffer.readBoolean())); + + public static void handle(ThermostatSetPacket packet, IPayloadContext ctx) { + ctx.enqueueWork(() -> { + if (!(ctx.player() instanceof ServerPlayer player)) { + return; + } + BlockPos pos = packet.pos(); + if (!(player.level().getBlockEntity(pos) instanceof ThermostatBlockEntity thermostat)) { + return; + } + // 触及距离校验(与服务端菜单的 stillValid 同一把尺子)。 + if (player.distanceToSqr(pos.getX() + 0.5D, pos.getY() + 0.5D, pos.getZ() + 0.5D) > 64.0D) { + return; + } + thermostat.setTemperature(packet.temperature()); + thermostat.setEnabled(packet.enabled()); + }); + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/network/ToolTypeModeSwitchPacket.java b/src/main/java/com/sorrowmist/useless/network/ToolTypeModeSwitchPacket.java new file mode 100644 index 00000000..3e14783c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/network/ToolTypeModeSwitchPacket.java @@ -0,0 +1,74 @@ +package com.sorrowmist.useless.network; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.api.enums.tool.ToolTypeMode; +import com.sorrowmist.useless.content.items.BeefToolVariants; +import com.sorrowmist.useless.core.component.UComponents; +import com.sorrowmist.useless.init.ModItems; +import com.sorrowmist.useless.utils.UselessItemUtils; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import com.sorrowmist.useless.compat.forge1201.network.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import com.sorrowmist.useless.compat.forge1201.network.IPayloadContext; +import org.jetbrains.annotations.NotNull; + +public record ToolTypeModeSwitchPacket(ToolTypeMode mode) implements CustomPacketPayload { + public static final StreamCodec STREAM_CODEC = + StreamCodec.of( + (buf, packet) -> buf.writeEnum(packet.mode), + buf -> new ToolTypeModeSwitchPacket(buf.readEnum(ToolTypeMode.class)) + ); + + public static final Type TYPE = + new Type<>(new ResourceLocation(UselessMod.MODID, "tool_type_mode_switch")); + + public static void handle(ToolTypeModeSwitchPacket msg, IPayloadContext ctx) { + ctx.enqueueWork(() -> { + ServerPlayer player = (ServerPlayer) ctx.player(); + var toolEntry = UselessItemUtils.findTargetToolInHands(player); + if (toolEntry.isEmpty()) return; // 没找到工具直接返回 + + var entry = toolEntry.get(); + ItemStack targetItem = entry.getKey(); + InteractionHand targetHand = entry.getValue(); + + // 1. 创建新物品实例 + ItemStack newStack = switch (msg.mode) { + case NONE_MODE, WRENCH_MODE, SCREWDRIVER_MODE, MALLET_MODE, CROWBAR_MODE, HAMMER_MODE -> + BeefToolVariants.createForToolMode(targetItem, msg.mode); + case OMNITOOL_MODE -> { + ResourceLocation omnitoolId = new ResourceLocation("omnitools", "omni_wrench"); + Item toolItem = BuiltInRegistries.ITEM.get(omnitoolId); + if (toolItem != Items.AIR) { + yield new ItemStack(toolItem); + } else { + yield new ItemStack(ModItems.ENDLESS_BEAF_WRENCH.get()); + } + } + }; + + // 2. 复制原有物品的所有NBT数据到新实例 + UComponents.copyState(targetItem, newStack); + + // 3. 覆盖状态:最后设置当前的工具模式组件 + if (!newStack.isEmpty()) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(newStack, UComponents.CurrentToolTypeComponent.get(), msg.mode); + player.setItemInHand(targetHand, newStack); + + // 显式同步物品到客户端 + player.containerMenu.broadcastChanges(); + } + }); + } + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/com/sorrowmist/useless/networking/ChainMiningTogglePacket.java b/src/main/java/com/sorrowmist/useless/networking/ChainMiningTogglePacket.java deleted file mode 100644 index efe1e5f3..00000000 --- a/src/main/java/com/sorrowmist/useless/networking/ChainMiningTogglePacket.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.sorrowmist.useless.networking; - -import com.sorrowmist.useless.utils.mining.MiningDispatcher; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.server.level.ServerPlayer; -import net.minecraftforge.network.NetworkEvent; - -import java.util.function.Supplier; - -/** - * Tab键连锁挖掘切换数据包 - * 重构:使用 MiningDispatcher 管理玩家Tab键状态 - */ -public class ChainMiningTogglePacket { - private final boolean isPressed; - - public ChainMiningTogglePacket(boolean isPressed) { - this.isPressed = isPressed; - } - - public static void encode(ChainMiningTogglePacket packet, FriendlyByteBuf buffer) { - buffer.writeBoolean(packet.isPressed); - } - - public static ChainMiningTogglePacket decode(FriendlyByteBuf buffer) { - return new ChainMiningTogglePacket(buffer.readBoolean()); - } - - public static void handle(ChainMiningTogglePacket packet, Supplier context) { - context.get().enqueueWork(() -> { - ServerPlayer player = context.get().getSender(); - if (player != null) { - MiningDispatcher.setTabPressed(player, packet.isPressed); - } - }); - context.get().setPacketHandled(true); - } -} diff --git a/src/main/java/com/sorrowmist/useless/networking/ClearFluidPacket.java b/src/main/java/com/sorrowmist/useless/networking/ClearFluidPacket.java deleted file mode 100644 index 4d479937..00000000 --- a/src/main/java/com/sorrowmist/useless/networking/ClearFluidPacket.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.sorrowmist.useless.networking; - -import com.sorrowmist.useless.UselessMod; -import com.sorrowmist.useless.blocks.advancedalloyfurnace.AdvancedAlloyFurnaceBlockEntity; -import net.minecraft.core.BlockPos; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.world.level.Level; -import net.minecraft.world.level.block.entity.BlockEntity; -import net.minecraftforge.network.NetworkEvent; - -import java.util.function.Supplier; - -public class ClearFluidPacket { - private final BlockPos pos; - private final boolean clearInput; - private final int tankIndex; - - public ClearFluidPacket(BlockPos pos, boolean clearInput) { - this(pos, clearInput, -1); // 默认值-1表示清空所有槽位 - } - - public ClearFluidPacket(BlockPos pos, boolean clearInput, int tankIndex) { - this.pos = pos; - this.clearInput = clearInput; - this.tankIndex = tankIndex; - } - - public static void encode(ClearFluidPacket packet, FriendlyByteBuf buffer) { - buffer.writeBlockPos(packet.pos); - buffer.writeBoolean(packet.clearInput); - buffer.writeInt(packet.tankIndex); - } - - public static ClearFluidPacket decode(FriendlyByteBuf buffer) { - return new ClearFluidPacket( - buffer.readBlockPos(), - buffer.readBoolean(), - buffer.readInt() - ); - } - - public static void handle(ClearFluidPacket packet, Supplier context) { - context.get().enqueueWork(() -> { - ServerPlayer player = context.get().getSender(); - if (player != null) { - Level level = player.level(); - if (level.hasChunkAt(packet.pos)) { - BlockEntity blockEntity = level.getBlockEntity(packet.pos); - if (blockEntity instanceof AdvancedAlloyFurnaceBlockEntity furnace) { - if (packet.clearInput) { - if (packet.tankIndex >= 0) { - // 清空指定索引的输入流体槽 - furnace.clearInputFluid(packet.tankIndex); - } else { - // 清空所有输入流体槽 - furnace.clearInputFluid(); - } - } else { - if (packet.tankIndex >= 0) { - // 清空指定索引的输出流体槽 - furnace.clearOutputFluid(packet.tankIndex); - } else { - // 清空所有输出流体槽 - furnace.clearOutputFluid(); - } - } - } - } - } - }); - context.get().setPacketHandled(true); - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/networking/EnchantmentSwitchPacket.java b/src/main/java/com/sorrowmist/useless/networking/EnchantmentSwitchPacket.java deleted file mode 100644 index 65b03908..00000000 --- a/src/main/java/com/sorrowmist/useless/networking/EnchantmentSwitchPacket.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.sorrowmist.useless.networking; - -import com.sorrowmist.useless.items.EndlessBeafItem; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.network.chat.Component; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.world.item.ItemStack; -import net.minecraftforge.network.NetworkEvent; - -import java.util.function.Supplier; - -public class EnchantmentSwitchPacket { - private final boolean switchToSilkTouch; - - public EnchantmentSwitchPacket(boolean switchToSilkTouch) { - this.switchToSilkTouch = switchToSilkTouch; - } - - public EnchantmentSwitchPacket(FriendlyByteBuf buf) { - this.switchToSilkTouch = buf.readBoolean(); - } - - public void toBytes(FriendlyByteBuf buf) { - buf.writeBoolean(switchToSilkTouch); - } - - public boolean handle(Supplier supplier) { - NetworkEvent.Context context = supplier.get(); - context.enqueueWork(() -> { - ServerPlayer player = context.getSender(); - if (player != null) { - // 检查主手和副手物品 - ItemStack mainHandItem = player.getMainHandItem(); - ItemStack offHandItem = player.getOffhandItem(); - - ItemStack targetItem = null; - - // 检查主手 - if (mainHandItem.getItem() instanceof EndlessBeafItem) { - targetItem = mainHandItem; - } - // 检查副手 - else if (offHandItem.getItem() instanceof EndlessBeafItem) { - targetItem = offHandItem; - } - - if (targetItem != null && targetItem.getItem() instanceof EndlessBeafItem endlessBeaf) { - // 使用专门的方法切换附魔模式,确保模式管理器状态和NBT标签一致 - endlessBeaf.switchEnchantmentMode(targetItem, switchToSilkTouch); - - if (switchToSilkTouch) { - player.displayClientMessage(Component.translatable("message.useless_mod.switched_to_silk_touch"), true); - } else { - player.displayClientMessage(Component.translatable("message.useless_mod.switched_to_fortune"), true); - } - } - } - }); - return true; - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/networking/EnhancedChainMiningTogglePacket.java b/src/main/java/com/sorrowmist/useless/networking/EnhancedChainMiningTogglePacket.java deleted file mode 100644 index 79c1044b..00000000 --- a/src/main/java/com/sorrowmist/useless/networking/EnhancedChainMiningTogglePacket.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.sorrowmist.useless.networking; - -import com.sorrowmist.useless.items.EndlessBeafItem; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.world.item.ItemStack; -import net.minecraftforge.network.NetworkEvent; - -import java.util.function.Supplier; - -public class EnhancedChainMiningTogglePacket { - public EnhancedChainMiningTogglePacket() { - // 这个数据包不需要任何数据,因为它只是一个切换信号 - } - - public static void encode(EnhancedChainMiningTogglePacket packet, FriendlyByteBuf buffer) { - // 不需要编码任何数据 - } - - public static EnhancedChainMiningTogglePacket decode(FriendlyByteBuf buffer) { - return new EnhancedChainMiningTogglePacket(); - } - - public static void handle(EnhancedChainMiningTogglePacket packet, Supplier context) { - context.get().enqueueWork(() -> { - ServerPlayer player = context.get().getSender(); - if (player != null) { - // 检查主手和副手物品 - ItemStack mainHandItem = player.getMainHandItem(); - ItemStack offHandItem = player.getOffhandItem(); - - ItemStack targetItem = null; - - // 检查主手 - if (mainHandItem.getItem() instanceof EndlessBeafItem) { - targetItem = mainHandItem; - } - // 检查副手 - else if (offHandItem.getItem() instanceof EndlessBeafItem) { - targetItem = offHandItem; - } - - if (targetItem != null && targetItem.getItem() instanceof EndlessBeafItem endlessBeaf) { - // 在服务端切换增强连锁模式 - boolean newMode = endlessBeaf.toggleEnhancedChainMiningMode(targetItem); - - // 发送消息给玩家(只在服务端发送,确保一致性) - String messageKey = newMode ? "message.useless_mod.enhanced_chain_on" : "message.useless_mod.enhanced_chain_off"; - player.displayClientMessage(net.minecraft.network.chat.Component.translatable(messageKey), true); - } - } - }); - context.get().setPacketHandled(true); - } -} diff --git a/src/main/java/com/sorrowmist/useless/networking/FluidInteractionPacket.java b/src/main/java/com/sorrowmist/useless/networking/FluidInteractionPacket.java deleted file mode 100644 index 4aea8dcc..00000000 --- a/src/main/java/com/sorrowmist/useless/networking/FluidInteractionPacket.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.sorrowmist.useless.networking; - -import com.sorrowmist.useless.blocks.advancedalloyfurnace.AdvancedAlloyFurnaceBlockEntity; -import net.minecraft.core.BlockPos; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.level.Level; -import net.minecraft.world.level.block.entity.BlockEntity; -import net.minecraftforge.network.NetworkEvent; - -import java.util.function.Supplier; - -public class FluidInteractionPacket { - private final BlockPos pos; - private final boolean isInputTank; - private final boolean isFill; - private final int button; - private final int tankIndex; - - public FluidInteractionPacket(BlockPos pos, boolean isInputTank, boolean isFill, int button) { - this(pos, isInputTank, isFill, button, 0); - } - - public FluidInteractionPacket(BlockPos pos, boolean isInputTank, boolean isFill, int button, int tankIndex) { - this.pos = pos; - this.isInputTank = isInputTank; - this.isFill = isFill; - this.button = button; - this.tankIndex = tankIndex; - } - - public static void encode(FluidInteractionPacket packet, FriendlyByteBuf buffer) { - buffer.writeBlockPos(packet.pos); - buffer.writeBoolean(packet.isInputTank); - buffer.writeBoolean(packet.isFill); - buffer.writeInt(packet.button); - buffer.writeInt(packet.tankIndex); - } - - public static FluidInteractionPacket decode(FriendlyByteBuf buffer) { - return new FluidInteractionPacket( - buffer.readBlockPos(), - buffer.readBoolean(), - buffer.readBoolean(), - buffer.readInt(), - buffer.readInt() - ); - } - - public static void handle(FluidInteractionPacket packet, Supplier context) { - context.get().enqueueWork(() -> { - ServerPlayer player = context.get().getSender(); - if (player != null) { - Level level = player.level(); - if (level.hasChunkAt(packet.pos)) { - BlockEntity blockEntity = level.getBlockEntity(packet.pos); - if (blockEntity instanceof AdvancedAlloyFurnaceBlockEntity furnace) { - ItemStack carried = player.containerMenu.getCarried(); - if (!carried.isEmpty()) { - furnace.interactWithFluid(player, carried, packet.isInputTank, packet.isFill, packet.tankIndex); - // 更新客户端手持物品 - player.containerMenu.setCarried(carried); - } - } - } - } - }); - context.get().setPacketHandled(true); - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/networking/ForceMiningTogglePacket.java b/src/main/java/com/sorrowmist/useless/networking/ForceMiningTogglePacket.java deleted file mode 100644 index e099dcb7..00000000 --- a/src/main/java/com/sorrowmist/useless/networking/ForceMiningTogglePacket.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.sorrowmist.useless.networking; - -import com.sorrowmist.useless.items.EndlessBeafItem; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.world.item.ItemStack; -import net.minecraftforge.network.NetworkEvent; - -import java.util.function.Supplier; - -public class ForceMiningTogglePacket { - public ForceMiningTogglePacket() { - // 这个数据包不需要任何数据,因为它只是一个切换信号 - } - - public static void encode(ForceMiningTogglePacket packet, FriendlyByteBuf buffer) { - // 不需要编码任何数据 - } - - public static ForceMiningTogglePacket decode(FriendlyByteBuf buffer) { - return new ForceMiningTogglePacket(); - } - - public static void handle(ForceMiningTogglePacket packet, Supplier context) { - context.get().enqueueWork(() -> { - ServerPlayer player = context.get().getSender(); - if (player != null) { - // 检查主手和副手物品 - ItemStack mainHandItem = player.getMainHandItem(); - ItemStack offHandItem = player.getOffhandItem(); - - ItemStack targetItem = null; - - // 检查主手 - if (mainHandItem.getItem() instanceof EndlessBeafItem) { - targetItem = mainHandItem; - } - // 检查副手 - else if (offHandItem.getItem() instanceof EndlessBeafItem) { - targetItem = offHandItem; - } - - if (targetItem != null && targetItem.getItem() instanceof EndlessBeafItem endlessBeaf) { - // 在服务端切换强制挖掘模式 - boolean newMode = endlessBeaf.toggleForceMiningMode(targetItem); - - // 发送消息给玩家(只在服务端发送,确保一致性) - String messageKey = newMode ? "message.useless_mod.force_mining_on" : "message.useless_mod.force_mining_off"; - player.displayClientMessage(net.minecraft.network.chat.Component.translatable(messageKey), true); - } - } - }); - context.get().setPacketHandled(true); - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/networking/ModMessages.java b/src/main/java/com/sorrowmist/useless/networking/ModMessages.java deleted file mode 100644 index a2c8e6a3..00000000 --- a/src/main/java/com/sorrowmist/useless/networking/ModMessages.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.sorrowmist.useless.networking; - -import com.sorrowmist.useless.UselessMod; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.server.level.ServerPlayer; -import net.minecraftforge.network.NetworkDirection; -import net.minecraftforge.network.NetworkRegistry; -import net.minecraftforge.network.PacketDistributor; -import net.minecraftforge.network.simple.SimpleChannel; - -public class ModMessages { - private static SimpleChannel INSTANCE; - private static int packetId = 0; - private static int id() { return packetId++; } - - public static void register() { - SimpleChannel net = NetworkRegistry.ChannelBuilder - .named( ResourceLocation.fromNamespaceAndPath(UselessMod.MOD_ID, "messages")) - .networkProtocolVersion(() -> "1.0") - .clientAcceptedVersions(s -> true) - .serverAcceptedVersions(s -> true) - .simpleChannel(); - - INSTANCE = net; - - net.messageBuilder(EnchantmentSwitchPacket.class, id(), NetworkDirection.PLAY_TO_SERVER) - .decoder(EnchantmentSwitchPacket::new) - .encoder(EnchantmentSwitchPacket::toBytes) - .consumerMainThread(EnchantmentSwitchPacket::handle) - .add(); - - net.messageBuilder(EnhancedChainMiningTogglePacket.class, id(), NetworkDirection.PLAY_TO_SERVER) - .decoder(EnhancedChainMiningTogglePacket::decode) - .encoder(EnhancedChainMiningTogglePacket::encode) - .consumerMainThread(EnhancedChainMiningTogglePacket::handle) - .add(); - - net.messageBuilder(ToolModeSwitchPacket.class, id(), NetworkDirection.PLAY_TO_SERVER) - .decoder(ToolModeSwitchPacket::new) - .encoder(ToolModeSwitchPacket::encode) - .consumerMainThread(ToolModeSwitchPacket::handle) - .add(); - - net.messageBuilder(ForceMiningTogglePacket.class, id(), NetworkDirection.PLAY_TO_SERVER) - .decoder(ForceMiningTogglePacket::decode) - .encoder(ForceMiningTogglePacket::encode) - .consumerMainThread(ForceMiningTogglePacket::handle) - .add(); - - net.messageBuilder(TriggerForceMiningPacket.class, id(), NetworkDirection.PLAY_TO_SERVER) - .decoder(TriggerForceMiningPacket::decode) - .encoder(TriggerForceMiningPacket::encode) - .consumerMainThread(TriggerForceMiningPacket::handle) - .add(); - - net.messageBuilder(SetMasterPatternPacket.class, id(), NetworkDirection.PLAY_TO_SERVER) - .decoder(SetMasterPatternPacket::decode) - .encoder(SetMasterPatternPacket::encode) - .consumerMainThread(SetMasterPatternPacket::handle) - .add(); - - net.messageBuilder(SetSlavePatternPacket.class, id(), NetworkDirection.PLAY_TO_SERVER) - .decoder(SetSlavePatternPacket::decode) - .encoder(SetSlavePatternPacket::encode) - .consumerMainThread(SetSlavePatternPacket::handle) - .add(); - - net.messageBuilder(ResetMasterPatternPacket.class, id(), NetworkDirection.PLAY_TO_SERVER) - .decoder(ResetMasterPatternPacket::decode) - .encoder(ResetMasterPatternPacket::encode) - .consumerMainThread(ResetMasterPatternPacket::handle) - .add(); - - } - - public static void sendToServer(MSG message) { - INSTANCE.sendToServer(message); - } - - public static void sendToPlayer(MSG message, ServerPlayer player) { - INSTANCE.send(PacketDistributor.PLAYER.with(() -> player), message); - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/networking/ResetMasterPatternPacket.java b/src/main/java/com/sorrowmist/useless/networking/ResetMasterPatternPacket.java deleted file mode 100644 index 8c1c1c4d..00000000 --- a/src/main/java/com/sorrowmist/useless/networking/ResetMasterPatternPacket.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.sorrowmist.useless.networking; - -import com.sorrowmist.useless.items.EndlessBeafItem; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.server.level.ServerPlayer; -import net.minecraftforge.network.NetworkEvent; - -import java.util.function.Supplier; - -public class ResetMasterPatternPacket { - public ResetMasterPatternPacket() { - // 空构造函数 - } - - public static void encode(ResetMasterPatternPacket packet, FriendlyByteBuf buffer) { - // 不需要编码任何数据 - } - - public static ResetMasterPatternPacket decode(FriendlyByteBuf buffer) { - return new ResetMasterPatternPacket(); - } - - public static void handle(ResetMasterPatternPacket packet, Supplier contextSupplier) { - NetworkEvent.Context context = contextSupplier.get(); - context.enqueueWork(() -> { - ServerPlayer player = context.getSender(); - if (player == null) return; - - // 检查玩家主手是否是EndlessBeafItem - if (player.getMainHandItem().getItem() instanceof EndlessBeafItem) { - // 调用重置主方块的静态方法 - EndlessBeafItem.resetMasterPatternProvider(player.level()); - player.sendSystemMessage(net.minecraft.network.chat.Component.literal("已重置扩展样板供应器主方块选择").withStyle(net.minecraft.ChatFormatting.RED)); - } - }); - context.setPacketHandled(true); - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/networking/SetMasterPatternPacket.java b/src/main/java/com/sorrowmist/useless/networking/SetMasterPatternPacket.java deleted file mode 100644 index a2eacba8..00000000 --- a/src/main/java/com/sorrowmist/useless/networking/SetMasterPatternPacket.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.sorrowmist.useless.networking; - -import com.sorrowmist.useless.items.EndlessBeafItem; -import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.world.level.block.state.BlockState; -import net.minecraftforge.network.NetworkEvent; - -import java.util.function.Supplier; - -public class SetMasterPatternPacket { - private final BlockPos pos; - private final Direction direction; - - public SetMasterPatternPacket(BlockPos pos, Direction direction) { - this.pos = pos; - this.direction = direction; - } - - public static void encode(SetMasterPatternPacket packet, FriendlyByteBuf buffer) { - buffer.writeBlockPos(packet.pos); - buffer.writeEnum(packet.direction); - } - - public static SetMasterPatternPacket decode(FriendlyByteBuf buffer) { - return new SetMasterPatternPacket(buffer.readBlockPos(), buffer.readEnum(Direction.class)); - } - - public static void handle(SetMasterPatternPacket packet, Supplier contextSupplier) { - NetworkEvent.Context context = contextSupplier.get(); - context.enqueueWork(() -> { - ServerPlayer player = context.getSender(); - if (player == null) return; - - // 检查玩家主手是否是EndlessBeafItem - if (player.getMainHandItem().getItem() instanceof EndlessBeafItem endlessBeaf) { - // 调用设置主方块的方法 - // 不再检查方块类型,直接调用setAsMaster方法 - // setAsMaster方法内部会处理是否是扩展样板供应器 - endlessBeaf.setAsMaster(player.level(), packet.pos, packet.direction, player); - } - }); - context.setPacketHandled(true); - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/networking/SetSlavePatternPacket.java b/src/main/java/com/sorrowmist/useless/networking/SetSlavePatternPacket.java deleted file mode 100644 index f70da906..00000000 --- a/src/main/java/com/sorrowmist/useless/networking/SetSlavePatternPacket.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.sorrowmist.useless.networking; - -import com.sorrowmist.useless.items.EndlessBeafItem; -import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.world.level.block.state.BlockState; -import net.minecraftforge.network.NetworkEvent; - -import java.util.function.Supplier; - -public class SetSlavePatternPacket { - private final BlockPos pos; - private final Direction direction; - - public SetSlavePatternPacket(BlockPos pos, Direction direction) { - this.pos = pos; - this.direction = direction; - } - - public static void encode(SetSlavePatternPacket packet, FriendlyByteBuf buffer) { - buffer.writeBlockPos(packet.pos); - buffer.writeEnum(packet.direction); - } - - public static SetSlavePatternPacket decode(FriendlyByteBuf buffer) { - return new SetSlavePatternPacket(buffer.readBlockPos(), buffer.readEnum(Direction.class)); - } - - public static void handle(SetSlavePatternPacket packet, Supplier contextSupplier) { - NetworkEvent.Context context = contextSupplier.get(); - context.enqueueWork(() -> { - ServerPlayer player = context.getSender(); - if (player == null) return; - - // 检查玩家主手是否是EndlessBeafItem - if (player.getMainHandItem().getItem() instanceof EndlessBeafItem endlessBeaf) { - // 调用添加从方块的方法 - // 不再检查方块类型,直接调用addAsSlave方法 - // addAsSlave方法内部会处理是否是扩展样板供应器 - endlessBeaf.addAsSlave(player.level(), packet.pos, packet.direction, player); - } - }); - context.setPacketHandled(true); - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/networking/ToolModeSwitchPacket.java b/src/main/java/com/sorrowmist/useless/networking/ToolModeSwitchPacket.java deleted file mode 100644 index 473f15c6..00000000 --- a/src/main/java/com/sorrowmist/useless/networking/ToolModeSwitchPacket.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.sorrowmist.useless.networking; - -import com.sorrowmist.useless.items.EndlessBeafItem; -import com.sorrowmist.useless.modes.ModeManager; -import com.sorrowmist.useless.modes.ToolMode; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.world.item.ItemStack; -import net.minecraftforge.network.NetworkEvent; - -import java.util.function.Supplier; - -public class ToolModeSwitchPacket { - private final ToolMode mode; - - public ToolModeSwitchPacket(ToolMode mode) { - this.mode = mode; - } - - public ToolModeSwitchPacket(FriendlyByteBuf buf) { - this.mode = ToolMode.valueOf(buf.readUtf()); - } - - public void encode(FriendlyByteBuf buf) { - buf.writeUtf(mode.name()); - } - - public static void handle(ToolModeSwitchPacket packet, Supplier context) { - context.get().enqueueWork(() -> { - ServerPlayer player = context.get().getSender(); - if (player != null) { - // 检查主手和副手物品 - ItemStack mainHandItem = player.getMainHandItem(); - ItemStack offHandItem = player.getOffhandItem(); - - ItemStack targetItem = null; - net.minecraft.world.InteractionHand targetHand = null; - - // 检查主手 - net.minecraft.resources.ResourceLocation mainItemId = net.minecraftforge.registries.ForgeRegistries.ITEMS.getKey(mainHandItem.getItem()); - if (mainHandItem.getItem() instanceof EndlessBeafItem || - (mainItemId != null && mainItemId.toString().equals("omnitools:omni_wrench") && - mainHandItem.hasTag() && mainHandItem.getTag().contains("ToolModes"))) { - targetItem = mainHandItem; - targetHand = net.minecraft.world.InteractionHand.MAIN_HAND; - } - // 检查副手 - else { - net.minecraft.resources.ResourceLocation offItemId = net.minecraftforge.registries.ForgeRegistries.ITEMS.getKey(offHandItem.getItem()); - if (offHandItem.getItem() instanceof EndlessBeafItem || - (offItemId != null && offItemId.toString().equals("omnitools:omni_wrench") && - offHandItem.hasTag() && offHandItem.getTag().contains("ToolModes"))) { - targetItem = offHandItem; - targetHand = net.minecraft.world.InteractionHand.OFF_HAND; - } - } - - // 如果找到了目标物品 - if (targetItem != null && targetHand != null) { - // 创建模式管理器并加载当前状态 - ModeManager modeManager = new ModeManager(); - modeManager.loadFromStack(targetItem); - - // 切换模式 - modeManager.toggleMode(packet.mode); - - // 保存模式状态到物品栈 - modeManager.saveToStack(targetItem); - - // 无论当前物品是什么,都创建一个新的永恒牛排物品栈 - ItemStack newStack = new ItemStack(EndlessBeafItem.ENDLESS_BEAF_ITEM.get()); - - // 复制原有物品的所有NBT数据到新实例 - if (targetItem.hasTag()) { - newStack.setTag(targetItem.getTag().copy()); - } - - // 更新实际的附魔NBT - EndlessBeafItem baseItem = (EndlessBeafItem) EndlessBeafItem.ENDLESS_BEAF_ITEM.get(); - baseItem.updateEnchantments(newStack); - - // 切换物品实例 - ItemStack finalStack = baseItem.switchToolModeItem(newStack, modeManager); - if (!finalStack.isEmpty()) { - // 替换玩家手中的物品(在正确的手中) - player.setItemInHand(targetHand, finalStack); - } - } - } - }); - context.get().setPacketHandled(true); - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/networking/TriggerForceMiningPacket.java b/src/main/java/com/sorrowmist/useless/networking/TriggerForceMiningPacket.java deleted file mode 100644 index bd2af9e1..00000000 --- a/src/main/java/com/sorrowmist/useless/networking/TriggerForceMiningPacket.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.sorrowmist.useless.networking; - -import com.sorrowmist.useless.utils.mining.MiningDispatcher; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.server.level.ServerPlayer; -import net.minecraftforge.network.NetworkEvent; - -import java.util.function.Supplier; - -/** - * R键强制破坏触发数据包 - * 重构:使用 MiningDispatcher 执行强制破坏 - */ -public class TriggerForceMiningPacket { - private final boolean tabPressed; - - public TriggerForceMiningPacket() { - this(false); - } - - public TriggerForceMiningPacket(boolean tabPressed) { - this.tabPressed = tabPressed; - } - - public static void encode(TriggerForceMiningPacket packet, FriendlyByteBuf buffer) { - buffer.writeBoolean(packet.tabPressed); - } - - public static TriggerForceMiningPacket decode(FriendlyByteBuf buffer) { - return new TriggerForceMiningPacket(buffer.readBoolean()); - } - - public static void handle(TriggerForceMiningPacket packet, Supplier context) { - context.get().enqueueWork(() -> { - ServerPlayer player = context.get().getSender(); - if (player != null) { - MiningDispatcher.dispatchForceBreak(player, packet.tabPressed); - } - }); - context.get().setPacketHandled(true); - } -} diff --git a/src/main/java/com/sorrowmist/useless/recipes/ModRecipeSerializers.java b/src/main/java/com/sorrowmist/useless/recipes/ModRecipeSerializers.java deleted file mode 100644 index d89e4eb3..00000000 --- a/src/main/java/com/sorrowmist/useless/recipes/ModRecipeSerializers.java +++ /dev/null @@ -1,18 +0,0 @@ -// ModRecipeSerializers.java -package com.sorrowmist.useless.recipes; - -import com.sorrowmist.useless.UselessMod; -import com.sorrowmist.useless.recipes.advancedalloyfurnace.AdvancedAlloyFurnaceRecipe; -import net.minecraft.world.item.crafting.RecipeSerializer; -import net.minecraftforge.registries.DeferredRegister; -import net.minecraftforge.registries.ForgeRegistries; -import net.minecraftforge.registries.RegistryObject; - -public class ModRecipeSerializers { - public static final DeferredRegister> RECIPE_SERIALIZERS = - DeferredRegister.create(ForgeRegistries.RECIPE_SERIALIZERS, UselessMod.MOD_ID); - - public static final RegistryObject> ADVANCED_ALLOY_FURNACE_SERIALIZER = - RECIPE_SERIALIZERS.register("advanced_alloy_furnace", - () -> new AdvancedAlloyFurnaceRecipe.Serializer()); -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/recipes/ModRecipeTypes.java b/src/main/java/com/sorrowmist/useless/recipes/ModRecipeTypes.java deleted file mode 100644 index 64e6e047..00000000 --- a/src/main/java/com/sorrowmist/useless/recipes/ModRecipeTypes.java +++ /dev/null @@ -1,22 +0,0 @@ -// ModRecipeTypes.java -package com.sorrowmist.useless.recipes; - -import com.sorrowmist.useless.UselessMod; -import com.sorrowmist.useless.recipes.advancedalloyfurnace.AdvancedAlloyFurnaceRecipe; -import net.minecraft.world.item.crafting.RecipeType; -import net.minecraftforge.registries.DeferredRegister; -import net.minecraftforge.registries.ForgeRegistries; -import net.minecraftforge.registries.RegistryObject; - -public class ModRecipeTypes { - public static final DeferredRegister> RECIPE_TYPES = - DeferredRegister.create(ForgeRegistries.RECIPE_TYPES, UselessMod.MOD_ID); - - public static final RegistryObject> ADVANCED_ALLOY_FURNACE_TYPE = - RECIPE_TYPES.register("advanced_alloy_furnace", () -> new RecipeType() { - @Override - public String toString() { - return "useless_mod:advanced_alloy_furnace"; - } - }); -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/recipes/advancedalloyfurnace/AdvancedAlloyFurnaceRecipe.java b/src/main/java/com/sorrowmist/useless/recipes/advancedalloyfurnace/AdvancedAlloyFurnaceRecipe.java deleted file mode 100644 index a3f596c7..00000000 --- a/src/main/java/com/sorrowmist/useless/recipes/advancedalloyfurnace/AdvancedAlloyFurnaceRecipe.java +++ /dev/null @@ -1,619 +0,0 @@ -package com.sorrowmist.useless.recipes.advancedalloyfurnace; - -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.sorrowmist.useless.UselessMod; -import com.sorrowmist.useless.recipes.ModRecipeSerializers; -import com.sorrowmist.useless.recipes.ModRecipeTypes; -import net.minecraft.core.NonNullList; -import net.minecraft.core.RegistryAccess; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.util.GsonHelper; -import net.minecraft.world.Container; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.crafting.Ingredient; -import net.minecraft.world.item.crafting.Recipe; -import net.minecraft.world.item.crafting.RecipeSerializer; -import net.minecraft.world.item.crafting.RecipeType; -import net.minecraft.world.level.Level; -import net.minecraftforge.fluids.FluidStack; -import net.minecraftforge.registries.ForgeRegistries; - -import javax.annotation.Nullable; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -public class AdvancedAlloyFurnaceRecipe implements Recipe { - private final ResourceLocation id; - private final List inputItems; - private final List inputItemCounts; - private final List inputFluids; - private final List outputItems; - private final List outputFluids; - private final int energy; - private final int processTime; - - // 新增字段:催化剂和模具 - private final Ingredient catalyst; - private final int catalystCount; - private final Ingredient mold; - - // 新增字段:标记是否为有机灌注机配方 - private final boolean isInsolatorRecipe; - - // 新增字段:标记是否为冲压机配方 - private final boolean isPressRecipe; - - public AdvancedAlloyFurnaceRecipe(ResourceLocation id, List inputItems, - List inputItemCounts, List inputFluids, - List outputItems, List outputFluids, - int energy, int processTime) { - this(id, inputItems, inputItemCounts, inputFluids, outputItems, outputFluids, - energy, processTime, Ingredient.EMPTY, 0, Ingredient.EMPTY, false, false); - } - - // 新增构造函数 - public AdvancedAlloyFurnaceRecipe(ResourceLocation id, List inputItems, List inputItemCounts, - List inputFluids, List outputItems, List outputFluids, - int energy, int processTime, Ingredient catalyst, int catalystCount, Ingredient mold) { - this(id, inputItems, inputItemCounts, inputFluids, outputItems, outputFluids, - energy, processTime, catalyst, catalystCount, mold, false, false); - } - - // 新增构造函数,支持有机灌注机配方标记 - public AdvancedAlloyFurnaceRecipe(ResourceLocation id, List inputItems, List inputItemCounts, - List inputFluids, List outputItems, List outputFluids, - int energy, int processTime, Ingredient catalyst, int catalystCount, Ingredient mold, boolean isInsolatorRecipe) { - this(id, inputItems, inputItemCounts, inputFluids, outputItems, outputFluids, - energy, processTime, catalyst, catalystCount, mold, isInsolatorRecipe, false); - } - - // 新增构造函数,支持冲压机配方标记 - public AdvancedAlloyFurnaceRecipe(ResourceLocation id, List inputItems, List inputItemCounts, - List inputFluids, List outputItems, List outputFluids, - int energy, int processTime, Ingredient catalyst, int catalystCount, Ingredient mold, boolean isInsolatorRecipe, boolean isPressRecipe) { - this.id = id; - this.inputItems = inputItems; - this.inputItemCounts = inputItemCounts; - this.inputFluids = inputFluids; - this.outputItems = outputItems; - this.outputFluids = outputFluids; - this.energy = energy; - this.processTime = processTime; - this.catalyst = catalyst; - this.catalystCount = catalystCount; - this.mold = mold; - this.isInsolatorRecipe = isInsolatorRecipe; - this.isPressRecipe = isPressRecipe; - } - - // 新增:检查是否为有机灌注机配方 - public boolean isInsolatorRecipe() { - return isInsolatorRecipe; - } - - // 新增:检查是否为冲压机配方 - public boolean isPressRecipe() { - return isPressRecipe; - } - - @Override - public boolean matches(Container container, Level level) { - return false; - } - - - // 在 AdvancedAlloyFurnaceRecipe.java 中添加黑名单检查方法 - public boolean isCatalystAllowed() { - // 检查输出物品是否在黑名单中 - for (ItemStack output : outputItems) { - if (com.sorrowmist.useless.registry.BlacklistManager.isOutputBlacklisted(output)) { - return false; - } - } - return true; - } - - // 修复匹配逻辑 - 支持多个流体槽 - public boolean matches(List inputSlots, List inputFluidsList) { - - - // 检查流体 - if (!inputFluids.isEmpty()) { - if (inputFluidsList.isEmpty()) { - return false; - } - - // 对于多种流体输入的配方,需要检查所有配方流体是否都能在输入流体槽中找到匹配 - // 创建一个副本用于跟踪已匹配的配方流体 - List remainingRequiredFluids = new ArrayList<>(inputFluids); - - // 遍历所有输入流体槽 - for (FluidStack inputTank : inputFluidsList) { - if (inputTank.isEmpty()) { - continue; - } - - // 尝试匹配剩余的配方流体 - for (Iterator iterator = remainingRequiredFluids.iterator(); iterator.hasNext(); ) { - FluidStack requiredFluid = iterator.next(); - if (inputTank.getAmount() >= requiredFluid.getAmount() && inputTank.getFluid().isSame(requiredFluid.getFluid())) { - // 匹配成功,从剩余列表中移除 - iterator.remove(); - break; - } - } - - // 如果所有配方流体都已匹配,提前结束 - if (remainingRequiredFluids.isEmpty()) { - break; - } - } - - // 如果还有未匹配的配方流体,返回不匹配 - if (!remainingRequiredFluids.isEmpty()) { - return false; - } - } - - // 创建可用物品的副本用于匹配计算 - List availableItems = new ArrayList<>(); - for (int i = 0; i < inputSlots.size(); i++) { - ItemStack stack = inputSlots.get(i); - if (!stack.isEmpty()) { - availableItems.add(stack.copy()); - } - } - - // 直接遍历输入物品列表,不使用HashMap,避免重复Ingredient被覆盖 - for (int i = 0; i < inputItems.size(); i++) { - Ingredient ingredient = inputItems.get(i); - long requiredCount = inputItemCounts.get(i); - long foundCount = 0; - - // 在所有可用物品中查找匹配 - for (ItemStack available : availableItems) { - if (!available.isEmpty() && ingredient.test(available)) { - int takeAmount = (int) Math.min(available.getCount(), requiredCount - foundCount); - foundCount += takeAmount; - available.shrink(takeAmount); - - if (foundCount >= requiredCount) { - break; - } - } - } - - // 如果没有找到足够的物品,返回不匹配 - if (foundCount < requiredCount) { - return false; - } - } - return true; - } - - // 兼容旧版本的matches方法,只检查第一个流体槽 - public boolean matches(List inputSlots, FluidStack inputTank) { - List inputFluidsList = new ArrayList<>(); - if (inputTank != null && !inputTank.isEmpty()) { - inputFluidsList.add(inputTank); - } - return matches(inputSlots, inputFluidsList); - } - - // 修复匹配逻辑,使催化剂真正成为可选项 - public boolean matches(List inputSlots, FluidStack inputTank, - ItemStack catalystSlot, ItemStack moldSlot) { - // 检查模具匹配(如果配方需要模具)- 模具仍然是必须的 - if (requiresMold()) { - if (moldSlot.isEmpty()) { - return false; - } - if (!mold.test(moldSlot)) { - return false; - } - } - - // 修改:催化剂现在是可选的,不检查催化剂状态 - // 即使配方需要催化剂但没有放入,也允许匹配(并行数为1) - - // 然后检查输入物品和流体 - List inputFluidsList = new ArrayList<>(); - if (inputTank != null && !inputTank.isEmpty()) { - inputFluidsList.add(inputTank); - } - boolean itemsAndFluidMatch = matches(inputSlots, inputFluidsList); - return itemsAndFluidMatch; - } - - // 支持多个流体槽的matches方法 - 完整版本 - public boolean matches(List inputSlots, List inputFluidsList, - ItemStack catalystSlot, ItemStack moldSlot) { - // 检查模具匹配(如果配方需要模具)- 模具仍然是必须的 - if (requiresMold()) { - if (moldSlot.isEmpty()) { - return false; - } - if (!mold.test(moldSlot)) { - return false; - } - } - - // 修改:催化剂现在是可选的,不检查催化剂状态 - // 即使配方需要催化剂但没有放入,也允许匹配(并行数为1) - - // 然后检查输入物品和流体 - boolean itemsAndFluidMatch = matches(inputSlots, inputFluidsList); - return itemsAndFluidMatch; - } - - @Override - public ItemStack assemble(Container container, RegistryAccess registryAccess) { - return outputItems.isEmpty() ? ItemStack.EMPTY : outputItems.get(0).copy(); - } - - @Override - public boolean canCraftInDimensions(int width, int height) { - return true; - } - - @Override - public ItemStack getResultItem(RegistryAccess registryAccess) { - return outputItems.isEmpty() ? ItemStack.EMPTY : outputItems.get(0).copy(); - } - - @Override - public ResourceLocation getId() { - return id; - } - - @Override - public RecipeSerializer getSerializer() { - return ModRecipeSerializers.ADVANCED_ALLOY_FURNACE_SERIALIZER.get(); - } - - @Override - public RecipeType getType() { - return ModRecipeTypes.ADVANCED_ALLOY_FURNACE_TYPE.get(); - } - - @Override - public NonNullList getIngredients() { - NonNullList ingredients = NonNullList.create(); - ingredients.addAll(inputItems); - return ingredients; - } - - // 新增:检查配方是否需要催化剂 - public boolean requiresCatalyst() { - return !catalyst.isEmpty() && catalystCount > 0; - } - - // 新增:检查配方是否需要模具 - public boolean requiresMold() { - return !mold.isEmpty(); - } - - // 新增:获取催化剂 - public Ingredient getCatalyst() { - return catalyst; - } - - // 新增:获取催化剂数量 - public int getCatalystCount() { - return catalystCount; - } - - // 新增:获取模具 - public Ingredient getMold() { - return mold; - } - - // 修改消耗逻辑,考虑催化剂和并行数(支持单个流体槽) - public void consumeInputs(List inputSlots, FluidStack inputTank, ItemStack catalystSlot, int parallel) { - // 将单个流体槽转换为列表,调用支持多种流体的消耗方法 - List inputFluidsList = new ArrayList<>(); - if (inputTank != null && !inputTank.isEmpty()) { - inputFluidsList.add(inputTank); - } - consumeInputs(inputSlots, inputFluidsList, catalystSlot, parallel); - } - - // 添加支持多种流体输入的消耗方法 - public void consumeInputs(List inputSlots, List inputFluidsList, ItemStack catalystSlot, int parallel) { - // 消耗流体,乘以并行数 - if (!inputFluids.isEmpty()) { - // 遍历每个需要的流体 - for (FluidStack requiredFluid : inputFluids) { - int fluidToConsume = requiredFluid.getAmount() * parallel; - - // 遍历所有输入流体槽,找到匹配的流体并消耗 - for (FluidStack inputTank : inputFluidsList) { - if (!inputTank.isEmpty() && inputTank.getFluid().isSame(requiredFluid.getFluid())) { - int consumed = Math.min(inputTank.getAmount(), fluidToConsume); - inputTank.shrink(consumed); - fluidToConsume -= consumed; - - // 如果已经消耗了足够的流体,跳出循环 - if (fluidToConsume <= 0) { - break; - } - } - } - } - } - - // 修改:如果催化剂槽有物品且是有效的催化剂,就消耗催化剂,无论配方是否明确要求 - // 但有用的锭(USEFUL_INGOT)作为终极催化剂不会被消耗 - String catalystId = net.minecraftforge.registries.ForgeRegistries.ITEMS.getKey(catalystSlot.getItem()).toString(); - if (!catalystSlot.isEmpty() && com.sorrowmist.useless.registry.CatalystManager.getCatalystParallel(catalystSlot) > 1 && - !catalystId.equals("useless_mod:useful_ingot")) { - // 消耗一个催化剂 - catalystSlot.shrink(1); - } - - // 特殊处理:有机灌注机配方不消耗输入物品 - if (isInsolatorRecipe()) { - return; - } - - // 为每个配方输入创建消耗计数器,乘以并行数 - long[] remainingToConsume = new long[inputItems.size()]; - for (int i = 0; i < inputItems.size(); i++) { - // 对于冲压机配方,忽略底部输入栏对应的物品(索引为1),只消耗顶部输入栏(索引为0) - if (isPressRecipe() && i == 1) { - remainingToConsume[i] = 0; // 不消耗底部输入栏物品 - } else { - remainingToConsume[i] = inputItemCounts.get(i) * parallel; - } - } - - // 消耗物品输入 - 遍历所有输入槽位 - for (int slotIndex = 0; slotIndex < inputSlots.size(); slotIndex++) { - ItemStack slotStack = inputSlots.get(slotIndex); - if (slotStack.isEmpty()) continue; - - // 对于当前槽位,检查所有需要消耗的原料 - for (int ingredientIndex = 0; ingredientIndex < inputItems.size(); ingredientIndex++) { - if (remainingToConsume[ingredientIndex] <= 0) continue; - - Ingredient ingredient = inputItems.get(ingredientIndex); - if (ingredient.test(slotStack)) { - int consumeAmount = (int) Math.min(slotStack.getCount(), remainingToConsume[ingredientIndex]); - slotStack.shrink(consumeAmount); - remainingToConsume[ingredientIndex] -= consumeAmount; - - // 如果槽位物品被消耗完,设置为空并跳出内层循环 - if (slotStack.isEmpty()) { - inputSlots.set(slotIndex, ItemStack.EMPTY); - break; - } - } - } - } - } - - // 原有的consumeInputs方法,用于向后兼容 - public void consumeInputs(List inputSlots, FluidStack inputTank, ItemStack catalystSlot) { - consumeInputs(inputSlots, inputTank, catalystSlot, 1); - } - - public void consumeInputs(List inputSlots, FluidStack inputTank) { - consumeInputs(inputSlots, inputTank, ItemStack.EMPTY, 1); - } - - // Getters - public List getInputItems() { return inputItems; } - public List getInputItemCounts() { return inputItemCounts; } - public List getInputFluids() { return inputFluids; } - public List getOutputItems() { return outputItems; } - public List getOutputFluids() { return outputFluids; } - public int getEnergy() { return energy; } - public int getProcessTime() { return processTime; } - - public static class Serializer implements RecipeSerializer { - @Override - public AdvancedAlloyFurnaceRecipe fromJson(ResourceLocation recipeId, JsonObject json) { - // 解析输入物品 - List inputItems = new ArrayList<>(); - List inputCounts = new ArrayList<>(); - - // 在 Serializer.fromJson 方法中修改这部分 - if (json.has("ingredients")) { - JsonArray ingredients = GsonHelper.getAsJsonArray(json, "ingredients"); - for (JsonElement element : ingredients) { - JsonObject ingredientObj = element.getAsJsonObject(); - - Ingredient ingredient; - long count; - - // 检查是否存在嵌套的 ingredient 对象 - if (ingredientObj.has("ingredient")) { - JsonElement innerElement = ingredientObj.get("ingredient"); - - // 支持数组格式的 ingredient - if (innerElement.isJsonArray()) { - // 数组格式: {"ingredient": [{"tag": "a"}, {"tag": "b"}], "count": N} - ingredient = Ingredient.fromJson(innerElement); - } else { - // 对象格式: {"ingredient": {"tag": "a"}, "count": N} - ingredient = Ingredient.fromJson(innerElement.getAsJsonObject()); - } - count = GsonHelper.getAsLong(ingredientObj, "count", 1L); - } else { - // 格式2: 直接是原料对象,count作为属性 - ingredient = Ingredient.fromJson(ingredientObj); - count = GsonHelper.getAsLong(ingredientObj, "count", 1L); - } - - inputItems.add(ingredient); - inputCounts.add(count); - } - } - - // 解析输入流体 - List inputFluids = new ArrayList<>(); - if (json.has("input_fluids")) { - JsonArray inputFluidsArray = GsonHelper.getAsJsonArray(json, "input_fluids"); - for (JsonElement element : inputFluidsArray) { - JsonObject fluidObj = element.getAsJsonObject(); - ResourceLocation fluidId = ResourceLocation.parse(GsonHelper.getAsString(fluidObj, "fluid")); - int amount = GsonHelper.getAsInt(fluidObj, "amount", 1000); - inputFluids.add(new FluidStack(ForgeRegistries.FLUIDS.getValue(fluidId), amount)); - } - } else if (json.has("input_fluid")) { - // 向后兼容:支持单个输入流体 - JsonObject fluidObj = json.getAsJsonObject("input_fluid"); - ResourceLocation fluidId = ResourceLocation.parse(GsonHelper.getAsString(fluidObj, "fluid")); - int amount = GsonHelper.getAsInt(fluidObj, "amount", 1000); - inputFluids.add(new FluidStack(ForgeRegistries.FLUIDS.getValue(fluidId), amount)); - } - - // 解析输出物品 - List outputItems = new ArrayList<>(); - if (json.has("results")) { - JsonArray results = GsonHelper.getAsJsonArray(json, "results"); - for (JsonElement element : results) { - JsonObject resultObj = element.getAsJsonObject(); - ResourceLocation itemId = ResourceLocation.parse(GsonHelper.getAsString(resultObj, "item")); - int count = GsonHelper.getAsInt(resultObj, "count", 1); - ItemStack output = new ItemStack(ForgeRegistries.ITEMS.getValue(itemId), count); - outputItems.add(output); - } - } - - // 解析输出流体 - List outputFluids = new ArrayList<>(); - if (json.has("output_fluids")) { - JsonArray outputFluidsArray = GsonHelper.getAsJsonArray(json, "output_fluids"); - for (JsonElement element : outputFluidsArray) { - JsonObject fluidObj = element.getAsJsonObject(); - ResourceLocation fluidId = ResourceLocation.parse(GsonHelper.getAsString(fluidObj, "fluid")); - int amount = GsonHelper.getAsInt(fluidObj, "amount", 1000); - outputFluids.add(new FluidStack(ForgeRegistries.FLUIDS.getValue(fluidId), amount)); - } - } else if (json.has("output_fluid")) { - // 向后兼容:支持单个输出流体 - JsonObject fluidObj = json.getAsJsonObject("output_fluid"); - ResourceLocation fluidId = ResourceLocation.parse(GsonHelper.getAsString(fluidObj, "fluid")); - int amount = GsonHelper.getAsInt(fluidObj, "amount", 1000); - outputFluids.add(new FluidStack(ForgeRegistries.FLUIDS.getValue(fluidId), amount)); - } - - // 解析催化剂 - Ingredient catalyst = Ingredient.EMPTY; - int catalystCount = 0; - if (json.has("catalyst")) { - JsonElement catalystElement = json.get("catalyst"); - if (catalystElement.isJsonObject()) { - JsonObject catalystObj = catalystElement.getAsJsonObject(); - catalyst = Ingredient.fromJson(catalystObj); - catalystCount = GsonHelper.getAsInt(catalystObj, "count", 1); - } else if (catalystElement.isJsonPrimitive()) { - // 简写格式:直接是物品ID - String itemId = catalystElement.getAsString(); - catalyst = Ingredient.of(ForgeRegistries.ITEMS.getValue(ResourceLocation.parse(itemId))); - catalystCount = 1; - } - } - - // 解析模具 - Ingredient mold = Ingredient.EMPTY; - if (json.has("mold")) { - JsonElement moldElement = json.get("mold"); - if (moldElement.isJsonPrimitive()) { - String itemId = moldElement.getAsString(); - mold = Ingredient.of(ForgeRegistries.ITEMS.getValue(ResourceLocation.parse(itemId))); - } else if (moldElement.isJsonObject()) { - JsonObject moldObj = moldElement.getAsJsonObject(); - mold = Ingredient.fromJson(moldObj); - } - } - - int energy = GsonHelper.getAsInt(json, "energy", 2000); - int processTime = GsonHelper.getAsInt(json, "process_time", 200); - - return new AdvancedAlloyFurnaceRecipe(recipeId, inputItems, inputCounts, inputFluids, - outputItems, outputFluids, energy, processTime, catalyst, catalystCount, mold); - } - - @Nullable - @Override - public AdvancedAlloyFurnaceRecipe fromNetwork(ResourceLocation recipeId, FriendlyByteBuf buffer) { - int inputCount = buffer.readVarInt(); - List inputItems = new ArrayList<>(); - List inputCounts = new ArrayList<>(); - - for (int i = 0; i < inputCount; i++) { - inputItems.add(Ingredient.fromNetwork(buffer)); - inputCounts.add(buffer.readVarLong()); - } - - int inputFluidCount = buffer.readVarInt(); - List inputFluids = new ArrayList<>(); - for (int i = 0; i < inputFluidCount; i++) { - inputFluids.add(buffer.readFluidStack()); - } - - int outputCount = buffer.readVarInt(); - List outputItems = new ArrayList<>(); - for (int i = 0; i < outputCount; i++) { - outputItems.add(buffer.readItem()); - } - - int outputFluidCount = buffer.readVarInt(); - List outputFluids = new ArrayList<>(); - for (int i = 0; i < outputFluidCount; i++) { - outputFluids.add(buffer.readFluidStack()); - } - - // 读取催化剂和模具 - Ingredient catalyst = Ingredient.fromNetwork(buffer); - int catalystCount = buffer.readVarInt(); - Ingredient mold = Ingredient.fromNetwork(buffer); - - int energy = buffer.readVarInt(); - int processTime = buffer.readVarInt(); - - return new AdvancedAlloyFurnaceRecipe(recipeId, inputItems, inputCounts, inputFluids, - outputItems, outputFluids, energy, processTime, catalyst, catalystCount, mold); - } - - @Override - public void toNetwork(FriendlyByteBuf buffer, AdvancedAlloyFurnaceRecipe recipe) { - buffer.writeVarInt(recipe.inputItems.size()); - for (int i = 0; i < recipe.inputItems.size(); i++) { - recipe.inputItems.get(i).toNetwork(buffer); - buffer.writeVarLong(recipe.inputItemCounts.get(i)); - } - - buffer.writeVarInt(recipe.inputFluids.size()); - for (FluidStack fluid : recipe.inputFluids) { - buffer.writeFluidStack(fluid); - } - - buffer.writeVarInt(recipe.outputItems.size()); - for (ItemStack stack : recipe.outputItems) { - buffer.writeItem(stack); - } - - buffer.writeVarInt(recipe.outputFluids.size()); - for (FluidStack fluid : recipe.outputFluids) { - buffer.writeFluidStack(fluid); - } - - // 写入催化剂和模具 - recipe.catalyst.toNetwork(buffer); - buffer.writeVarInt(recipe.catalystCount); - recipe.mold.toNetwork(buffer); - - buffer.writeVarInt(recipe.energy); - buffer.writeVarInt(recipe.processTime); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/recipes/advancedalloyfurnace/AdvancedAlloyFurnaceRecipeManager.java b/src/main/java/com/sorrowmist/useless/recipes/advancedalloyfurnace/AdvancedAlloyFurnaceRecipeManager.java deleted file mode 100644 index 6d8f09b8..00000000 --- a/src/main/java/com/sorrowmist/useless/recipes/advancedalloyfurnace/AdvancedAlloyFurnaceRecipeManager.java +++ /dev/null @@ -1,703 +0,0 @@ -package com.sorrowmist.useless.recipes.advancedalloyfurnace; - -import com.sorrowmist.useless.UselessMod; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.crafting.Ingredient; -import net.minecraft.world.item.crafting.RecipeManager; -import net.minecraft.world.level.Level; -import net.minecraftforge.fluids.FluidStack; - -import javax.annotation.Nullable; -import java.util.*; - -public class AdvancedAlloyFurnaceRecipeManager { - private static final AdvancedAlloyFurnaceRecipeManager INSTANCE = new AdvancedAlloyFurnaceRecipeManager(); - - private final Map> recipeCache = new HashMap<>(); - - public static AdvancedAlloyFurnaceRecipeManager getInstance() { - return INSTANCE; - } - - private AdvancedAlloyFurnaceRecipeManager() {} - - // 转换后的配方缓存 - private final List thermalConvertedRecipes = new ArrayList<>(); - private boolean thermalRecipesConverted = false; - - private final List mekanismConvertedRecipes = new ArrayList<>(); - private boolean mekanismRecipesConverted = false; - - private final List industrialForegoingConvertedRecipes = new ArrayList<>(); - private boolean industrialForegoingRecipesConverted = false; - - private final List bloodMagicConvertedRecipes = new ArrayList<>(); - private boolean bloodMagicRecipesConverted = false; - - private final List embersConvertedRecipes = new ArrayList<>(); - private boolean embersRecipesConverted = false; - - // 优化后的配方查找方法(支持单个流体输入,保持向后兼容) - @Nullable - public AdvancedAlloyFurnaceRecipe getRecipeWithCatalystOrMold(Level level, List inputItems, - FluidStack inputFluid, ItemStack catalyst, - ItemStack mold) { - // 将单个流体转换为列表,调用支持多种流体的方法 - List fluidList = new ArrayList<>(); - if (!inputFluid.isEmpty()) { - fluidList.add(inputFluid); - } - return getRecipeWithCatalystOrMold(level, inputItems, fluidList, catalyst, mold); - } - - // 优化后的配方查找方法(支持多种流体输入) - @Nullable - public AdvancedAlloyFurnaceRecipe getRecipeWithCatalystOrMold(Level level, List inputItems, - List inputFluids, ItemStack catalyst, - ItemStack mold) { - if (level == null) { - return null; - } - - // 提前检查输入是否为空 - boolean hasItems = false; - for (ItemStack stack : inputItems) { - if (!stack.isEmpty()) { - hasItems = true; - break; - } - } - - boolean hasFluids = false; - for (FluidStack fluid : inputFluids) { - if (!fluid.isEmpty()) { - hasFluids = true; - break; - } - } - - if (!hasItems && !hasFluids) { - return null; - } - - // 检查缓存 - String cacheKey = generateCacheKey(inputItems, inputFluids, catalyst, mold); - if (recipeCache.containsKey(cacheKey)) { - return recipeCache.get(cacheKey).isEmpty() ? null : recipeCache.get(cacheKey).get(0); - } - - RecipeManager recipeManager = level.getRecipeManager(); - - // 转换热力配方并缓存(只在第一次调用时执行) - if (!thermalRecipesConverted) { - try { - // 导入热力配方转换器 - // 转换有机灌注机配方 - List insolatorRecipes = com.sorrowmist.useless.compat.thermal.ThermalRecipeConverter.convertInsolatorRecipes(recipeManager); - thermalConvertedRecipes.addAll(insolatorRecipes); - - - // 转换感应炉配方 - List smelterRecipes = com.sorrowmist.useless.compat.thermal.ThermalRecipeConverter.convertSmelterRecipes(recipeManager); - thermalConvertedRecipes.addAll(smelterRecipes); - - - // 转换冲压机配方 - List pressRecipes = com.sorrowmist.useless.compat.thermal.ThermalRecipeConverter.convertPressRecipes(recipeManager); - thermalConvertedRecipes.addAll(pressRecipes); - - - thermalRecipesConverted = true; - } catch (Exception e) { - // Failed to convert Thermal recipes, continue with other recipes - } - } - - // 转换Mekanism配方并缓存(只在第一次调用时执行) - if (!mekanismRecipesConverted) { - try { - // 导入Mekanism配方转换器 - List metallurgicInfuserRecipes = com.sorrowmist.useless.compat.mekanism.MekanismRecipeConverter.convertMetallurgicInfuserRecipes(recipeManager); - mekanismConvertedRecipes.addAll(metallurgicInfuserRecipes); - - // 添加Mekanism富集仓配方转换 - List enrichmentRecipes = com.sorrowmist.useless.compat.mekanism.MekanismRecipeConverter.convertEnrichmentChamberRecipes(recipeManager); - mekanismConvertedRecipes.addAll(enrichmentRecipes); - - mekanismRecipesConverted = true; - } catch (Exception e) { - // Failed to convert Mekanism recipes, continue with other recipes - } - } - - // 转换工业先锋配方并缓存(只在第一次调用时执行) - if (!industrialForegoingRecipesConverted) { - try { - // 导入工业先锋配方转换器 - List dissolutionRecipes = com.sorrowmist.useless.compat.industrialforegoing.IndustrialForegoingRecipeConverter.convertDissolutionChamberRecipes(recipeManager); - industrialForegoingConvertedRecipes.addAll(dissolutionRecipes); - - industrialForegoingRecipesConverted = true; - } catch (Exception e) { - // Failed to convert Industrial Foregoing recipes, continue with other recipes - } - } - - // 转换血魔法配方并缓存(只在第一次调用时执行) - if (!bloodMagicRecipesConverted) { - try { - // 导入血魔法配方转换器 - List bloodAltarRecipes = com.sorrowmist.useless.compat.bloodmagic.BloodMagicRecipeConverter.convertBloodAltarRecipes(recipeManager); - bloodMagicConvertedRecipes.addAll(bloodAltarRecipes); - - // 添加血魔法狱火熔炉配方转换 - List soulForgeRecipes = com.sorrowmist.useless.compat.bloodmagic.BloodMagicRecipeConverter.convertTartaricForgeRecipes(recipeManager); - bloodMagicConvertedRecipes.addAll(soulForgeRecipes); - - // 添加血魔法炼金术桌配方转换 - List alchemyTableRecipes = com.sorrowmist.useless.compat.bloodmagic.BloodMagicRecipeConverter.convertAlchemyTableRecipes(recipeManager); - bloodMagicConvertedRecipes.addAll(alchemyTableRecipes); - - bloodMagicRecipesConverted = true; - } catch (Exception e) { - // Failed to convert Blood Magic recipes, continue with other recipes - } - } - - // 转换余烬mod配方并缓存(只在第一次调用时执行) - if (!embersRecipesConverted) { - try { - // 导入余烬配方转换器 - List embersAlchemyRecipes = com.sorrowmist.useless.compat.embers.EmbersRecipeConverter.convertAlchemyTabletRecipes(recipeManager); - embersConvertedRecipes.addAll(embersAlchemyRecipes); - - // 添加余烬地质分离器配方转换 - List embersGeoSeparatorRecipes = com.sorrowmist.useless.compat.embers.EmbersRecipeConverter.convertGeoSeparatorRecipes(recipeManager); - embersConvertedRecipes.addAll(embersGeoSeparatorRecipes); - - // 添加余烬熔炼炉配方转换 - List embersMelterRecipes = com.sorrowmist.useless.compat.embers.EmbersRecipeConverter.convertMelterRecipes(recipeManager); - embersConvertedRecipes.addAll(embersMelterRecipes); - - // 添加余烬压印锤配方转换 - List embersStampingRecipes = com.sorrowmist.useless.compat.embers.EmbersRecipeConverter.convertStampingRecipes(recipeManager); - embersConvertedRecipes.addAll(embersStampingRecipes); - - // 添加余烬混合离心器配方转换 - List embersMixingRecipes = com.sorrowmist.useless.compat.embers.EmbersRecipeConverter.convertMixingRecipes(recipeManager); - embersConvertedRecipes.addAll(embersMixingRecipes); - - embersRecipesConverted = true; - } catch (Exception e) { - // Failed to convert Embers recipes, continue with other recipes - } - } - - // 获取所有高级合金炉配方 - Collection allRecipes = new ArrayList<>(recipeManager.getAllRecipesFor( - com.sorrowmist.useless.recipes.ModRecipeTypes.ADVANCED_ALLOY_FURNACE_TYPE.get() - )); - - // 添加转换后的热力配方 - allRecipes.addAll(thermalConvertedRecipes); - // 添加转换后的Mekanism配方 - allRecipes.addAll(mekanismConvertedRecipes); - // 添加转换后的工业先锋配方 - allRecipes.addAll(industrialForegoingConvertedRecipes); - // 添加转换后的血魔法配方 - allRecipes.addAll(bloodMagicConvertedRecipes); - // 添加转换后的余烬配方 - allRecipes.addAll(embersConvertedRecipes); - - boolean hasCatalyst = !catalyst.isEmpty(); - boolean hasMold = !mold.isEmpty(); - - AdvancedAlloyFurnaceRecipe catalystRecipe = null; - AdvancedAlloyFurnaceRecipe moldRecipe = null; - AdvancedAlloyFurnaceRecipe normalRecipe = null; - AdvancedAlloyFurnaceRecipe smeltingRecipe = null; - - // 先寻找完全匹配的配方 - for (AdvancedAlloyFurnaceRecipe recipe : allRecipes) { - if (recipe.matches(inputItems, inputFluids, catalyst, mold)) { - // 找到完全匹配的配方,直接返回 - List resultList = Collections.singletonList(recipe); - recipeCache.put(cacheKey, resultList); - return recipe; - } - } - - // 如果没有完全匹配的配方,再根据优先级寻找可能匹配的配方 - for (AdvancedAlloyFurnaceRecipe recipe : allRecipes) { - // 检查是否匹配输入物品和流体(不考虑催化剂和模具) - if (recipe.matches(inputItems, inputFluids, ItemStack.EMPTY, ItemStack.EMPTY)) { - // 根据优先级找到第一个匹配的配方 - if (recipe.requiresCatalyst() && !hasCatalyst && catalystRecipe == null) { - catalystRecipe = recipe; - } else if (recipe.requiresMold() && !hasMold && moldRecipe == null) { - moldRecipe = recipe; - } else if (!recipe.requiresCatalyst() && !recipe.requiresMold() && normalRecipe == null) { - normalRecipe = recipe; - } - } - - // 如果所有类型都找到了,提前退出循环 - if (catalystRecipe != null && moldRecipe != null && normalRecipe != null) { - break; - } - } - - // 检查原版熔炉配方 - boolean hasAnyFluids = false; - for (FluidStack fluid : inputFluids) { - if (!fluid.isEmpty()) { - hasAnyFluids = true; - break; - } - } - - if (normalRecipe == null && !hasAnyFluids && hasMold) { - // 检查模具是否为原版熔炉 - boolean isFurnaceMold = mold.getItem() == net.minecraft.world.item.Items.FURNACE; - if (isFurnaceMold) { - // 创建一个只包含非空输入物品的列表 - List nonEmptyInputs = new ArrayList<>(); - for (ItemStack stack : inputItems) { - if (!stack.isEmpty()) { - nonEmptyInputs.add(stack); - } - } - - // 只处理有非空输入物品的情况 - if (!nonEmptyInputs.isEmpty()) { - // 获取所有熔炉配方 - Collection smeltingRecipes = recipeManager.getAllRecipesFor( - net.minecraft.world.item.crafting.RecipeType.SMELTING - ); - - for (ItemStack stack : nonEmptyInputs) { - for (net.minecraft.world.item.crafting.SmeltingRecipe recipe : smeltingRecipes) { - if (recipe.getIngredients().get(0).test(stack)) { - // 将原版熔炉配方转换为高级合金炉配方 - List inputIngredients = new ArrayList<>(); - List inputCounts = new ArrayList<>(); - - // 只添加配方中定义的输入物品 - inputIngredients.add(recipe.getIngredients().get(0)); - inputCounts.add(1); - - List outputItems = new ArrayList<>(); - outputItems.add(recipe.getResultItem(null)); - - List emptyFluids = new ArrayList<>(); - - // 创建临时的高级合金炉配方,使用原版熔炉作为模具 - Ingredient furnaceIngredient = Ingredient.of(net.minecraft.world.item.Items.FURNACE); - - // 创建无用锭标签的催化剂,允许使用所有无用锭 - net.minecraft.tags.TagKey uselessIngotTag = - net.minecraft.tags.TagKey.create( - net.minecraft.core.registries.Registries.ITEM, - new net.minecraft.resources.ResourceLocation("useless_mod", "useless_ingots") - ); - Ingredient uselessIngotCatalyst = Ingredient.of(uselessIngotTag); - - // 将List转换为List - List longInputCounts = new ArrayList<>(); - for (Integer count : inputCounts) { - longInputCounts.add(count.longValue()); - } - - smeltingRecipe = new AdvancedAlloyFurnaceRecipe( - recipe.getId(), - inputIngredients, - longInputCounts, - emptyFluids, - outputItems, - emptyFluids, - 1000, // 能量消耗 - 40, // 处理时间:熔炉配方为40tick - uselessIngotCatalyst, // 催化剂:无用锭标签 - 1, // 催化剂数量 - furnaceIngredient // 模具:原版熔炉 - ); - break; - } - } - if (smeltingRecipe != null) { - break; - } - } - } - } - } - - // 选择要返回的配方 - AdvancedAlloyFurnaceRecipe selectedRecipe = null; - if (catalystRecipe != null) { - selectedRecipe = catalystRecipe; - } else if (moldRecipe != null) { - selectedRecipe = moldRecipe; - } else if (normalRecipe != null) { - selectedRecipe = normalRecipe; - } else if (smeltingRecipe != null) { - selectedRecipe = smeltingRecipe; - } - - // 更新缓存 - List resultList = new ArrayList<>(); - if (selectedRecipe != null) { - resultList.add(selectedRecipe); - } - recipeCache.put(cacheKey, resultList); - - return selectedRecipe; - } - - - // 修改原有的getRecipe方法,使用新的优先级逻辑 - @Nullable - public AdvancedAlloyFurnaceRecipe getRecipe(Level level, List inputItems, FluidStack inputFluid) { - // 使用空的催化剂和模具槽位来调用新方法 - return getRecipeWithCatalystOrMold(level, inputItems, inputFluid, ItemStack.EMPTY, ItemStack.EMPTY); - } - - // 添加支持多种流体输入的getRecipe方法 - @Nullable - public AdvancedAlloyFurnaceRecipe getRecipe(Level level, List inputItems, List inputFluids) { - // 使用空的催化剂和模具槽位来调用新方法 - return getRecipeWithCatalystOrMold(level, inputItems, inputFluids, ItemStack.EMPTY, ItemStack.EMPTY); - } - - // 支持多种流体输入的缓存键生成方法 - private String generateCacheKey(List inputItems, List inputFluids, ItemStack catalyst, ItemStack mold) { - StringBuilder key = new StringBuilder(); - - // 物品部分(排序以确保顺序不影响缓存键) - List sortedItems = new ArrayList<>(); - for (ItemStack stack : inputItems) { - if (!stack.isEmpty()) { - sortedItems.add(stack); - } - } - sortedItems.sort(Comparator.comparing(stack -> net.minecraftforge.registries.ForgeRegistries.ITEMS.getKey(stack.getItem()))); - - for (ItemStack stack : sortedItems) { - key.append(net.minecraftforge.registries.ForgeRegistries.ITEMS.getKey(stack.getItem())); - key.append(":").append(stack.getCount()).append(";"); - } - - // 流体部分(排序以确保顺序不影响缓存键) - List sortedFluids = new ArrayList<>(); - for (FluidStack fluid : inputFluids) { - if (!fluid.isEmpty()) { - sortedFluids.add(fluid); - } - } - sortedFluids.sort(Comparator.comparing(fluid -> net.minecraftforge.registries.ForgeRegistries.FLUIDS.getKey(fluid.getFluid()))); - - for (FluidStack fluid : sortedFluids) { - key.append(net.minecraftforge.registries.ForgeRegistries.FLUIDS.getKey(fluid.getFluid())); - key.append(":").append(fluid.getAmount()).append(";"); - } - - // 催化剂部分 - if (!catalyst.isEmpty()) { - key.append("CAT:").append(net.minecraftforge.registries.ForgeRegistries.ITEMS.getKey(catalyst.getItem())); - key.append(":").append(catalyst.getCount()).append(";"); - } - - // 模具部分 - if (!mold.isEmpty()) { - key.append("MOLD:").append(net.minecraftforge.registries.ForgeRegistries.ITEMS.getKey(mold.getItem())); - key.append(":").append(mold.getCount()).append(";"); - } - - return key.toString(); - } - - // 原来的缓存键生成方法,保持向后兼容 - private String generateCacheKey(List inputItems, FluidStack inputFluid, ItemStack catalyst, ItemStack mold) { - List fluidList = new ArrayList<>(); - if (!inputFluid.isEmpty()) { - fluidList.add(inputFluid); - } - return generateCacheKey(inputItems, fluidList, catalyst, mold); - } - - public void clearCache() { - recipeCache.clear(); - } - - public boolean isValidInputItem(Level level, ItemStack stack) { - if (level == null || stack.isEmpty()) return false; - - // 检查缓存 - String cacheKey = "ITEM_VALID:" + net.minecraftforge.registries.ForgeRegistries.ITEMS.getKey(stack.getItem()); - if (recipeCache.containsKey(cacheKey)) { - return !recipeCache.get(cacheKey).isEmpty(); - } - - RecipeManager recipeManager = level.getRecipeManager(); - - // 确保热力配方已转换 - if (!thermalRecipesConverted) { - try { - // 转换有机灌注机配方 - List insolatorRecipes = com.sorrowmist.useless.compat.thermal.ThermalRecipeConverter.convertInsolatorRecipes(recipeManager); - thermalConvertedRecipes.addAll(insolatorRecipes); - - // 转换感应炉配方 - List smelterRecipes = com.sorrowmist.useless.compat.thermal.ThermalRecipeConverter.convertSmelterRecipes(recipeManager); - thermalConvertedRecipes.addAll(smelterRecipes); - - // 转换冲压机配方 - List pressRecipes = com.sorrowmist.useless.compat.thermal.ThermalRecipeConverter.convertPressRecipes(recipeManager); - thermalConvertedRecipes.addAll(pressRecipes); - - thermalRecipesConverted = true; - } catch (Exception e) { - // Failed to convert Thermal recipes, continue with other recipes - } - } - - // 确保Mekanism配方已转换 - if (!mekanismRecipesConverted) { - try { - // 转换Mekanism冶金灌注机配方 - List metallurgicInfuserRecipes = com.sorrowmist.useless.compat.mekanism.MekanismRecipeConverter.convertMetallurgicInfuserRecipes(recipeManager); - mekanismConvertedRecipes.addAll(metallurgicInfuserRecipes); - - // 添加Mekanism富集仓配方转换 - List enrichmentRecipes = com.sorrowmist.useless.compat.mekanism.MekanismRecipeConverter.convertEnrichmentChamberRecipes(recipeManager); - mekanismConvertedRecipes.addAll(enrichmentRecipes); - - mekanismRecipesConverted = true; - } catch (Exception e) { - // Failed to convert Mekanism recipes, continue with other recipes - } - } - - // 确保工业先锋配方已转换 - if (!industrialForegoingRecipesConverted) { - try { - // 转换工业先锋化学溶解室配方 - List dissolutionRecipes = com.sorrowmist.useless.compat.industrialforegoing.IndustrialForegoingRecipeConverter.convertDissolutionChamberRecipes(recipeManager); - industrialForegoingConvertedRecipes.addAll(dissolutionRecipes); - - industrialForegoingRecipesConverted = true; - } catch (Exception e) { - // Failed to convert Industrial Foregoing recipes, continue with other recipes - } - } - - // 确保血魔法配方已转换 - if (!bloodMagicRecipesConverted) { - try { - // 转换血魔法血之祭坛配方 - List bloodAltarRecipes = com.sorrowmist.useless.compat.bloodmagic.BloodMagicRecipeConverter.convertBloodAltarRecipes(recipeManager); - bloodMagicConvertedRecipes.addAll(bloodAltarRecipes); - - // 添加血魔法狱火熔炉配方转换 - List soulForgeRecipes = com.sorrowmist.useless.compat.bloodmagic.BloodMagicRecipeConverter.convertTartaricForgeRecipes(recipeManager); - bloodMagicConvertedRecipes.addAll(soulForgeRecipes); - - // 添加血魔法炼金术桌配方转换 - List alchemyTableRecipes = com.sorrowmist.useless.compat.bloodmagic.BloodMagicRecipeConverter.convertAlchemyTableRecipes(recipeManager); - bloodMagicConvertedRecipes.addAll(alchemyTableRecipes); - - bloodMagicRecipesConverted = true; - } catch (Exception e) { - // Failed to convert Blood Magic recipes, continue with other recipes - } - } - - // 确保余烬mod配方已转换 - if (!embersRecipesConverted) { - try { - // 转换余烬炼金台配方 - List embersAlchemyRecipes = com.sorrowmist.useless.compat.embers.EmbersRecipeConverter.convertAlchemyTabletRecipes(recipeManager); - embersConvertedRecipes.addAll(embersAlchemyRecipes); - - // 添加余烬地质分离器配方转换 - List embersGeoSeparatorRecipes = com.sorrowmist.useless.compat.embers.EmbersRecipeConverter.convertGeoSeparatorRecipes(recipeManager); - embersConvertedRecipes.addAll(embersGeoSeparatorRecipes); - - // 添加余烬熔炼炉配方转换 - List embersMelterRecipes = com.sorrowmist.useless.compat.embers.EmbersRecipeConverter.convertMelterRecipes(recipeManager); - embersConvertedRecipes.addAll(embersMelterRecipes); - - // 添加余烬压印锤配方转换 - List embersStampingRecipes = com.sorrowmist.useless.compat.embers.EmbersRecipeConverter.convertStampingRecipes(recipeManager); - embersConvertedRecipes.addAll(embersStampingRecipes); - - // 添加余烬混合离心器配方转换 - List embersMixingRecipes = com.sorrowmist.useless.compat.embers.EmbersRecipeConverter.convertMixingRecipes(recipeManager); - embersConvertedRecipes.addAll(embersMixingRecipes); - - embersRecipesConverted = true; - } catch (Exception e) { - // Failed to convert Embers recipes, continue with other recipes - } - } - - // 获取所有高级合金炉配方 - Collection allRecipes = new ArrayList<>(recipeManager.getAllRecipesFor( - com.sorrowmist.useless.recipes.ModRecipeTypes.ADVANCED_ALLOY_FURNACE_TYPE.get() - )); - - // 添加转换后的配方 - allRecipes.addAll(thermalConvertedRecipes); - allRecipes.addAll(mekanismConvertedRecipes); - allRecipes.addAll(industrialForegoingConvertedRecipes); - allRecipes.addAll(bloodMagicConvertedRecipes); - allRecipes.addAll(embersConvertedRecipes); - - for (AdvancedAlloyFurnaceRecipe recipe : allRecipes) { - for (net.minecraft.world.item.crafting.Ingredient ingredient : recipe.getInputItems()) { - if (ingredient.test(stack)) { - // 缓存结果 - recipeCache.put(cacheKey, Collections.singletonList(recipe)); - return true; - } - } - } - - // 缓存结果 - recipeCache.put(cacheKey, Collections.emptyList()); - return false; - } - - // 优化后的流体验证方法 - public boolean isValidInputFluid(Level level, FluidStack fluid) { - if (fluid.isEmpty() || level == null) return false; - - // 检查缓存 - String cacheKey = "FLUID_VALID:" + net.minecraftforge.registries.ForgeRegistries.FLUIDS.getKey(fluid.getFluid()); - if (recipeCache.containsKey(cacheKey)) { - return !recipeCache.get(cacheKey).isEmpty(); - } - - RecipeManager recipeManager = level.getRecipeManager(); - - // 确保热力配方已转换 - if (!thermalRecipesConverted) { - try { - List convertedRecipes = com.sorrowmist.useless.compat.thermal.ThermalRecipeConverter.convertInsolatorRecipes(recipeManager); - thermalConvertedRecipes.addAll(convertedRecipes); - thermalRecipesConverted = true; - } catch (Exception e) { - // Failed to convert Thermal Insolator recipes, continue with other recipes - } - } - - // 确保Mekanism配方已转换 - if (!mekanismRecipesConverted) { - try { - // 转换Mekanism冶金灌注机配方 - List metallurgicInfuserRecipes = com.sorrowmist.useless.compat.mekanism.MekanismRecipeConverter.convertMetallurgicInfuserRecipes(recipeManager); - mekanismConvertedRecipes.addAll(metallurgicInfuserRecipes); - - // 添加Mekanism富集仓配方转换 - List enrichmentRecipes = com.sorrowmist.useless.compat.mekanism.MekanismRecipeConverter.convertEnrichmentChamberRecipes(recipeManager); - mekanismConvertedRecipes.addAll(enrichmentRecipes); - - mekanismRecipesConverted = true; - } catch (Exception e) { - // Failed to convert Mekanism recipes, continue with other recipes - } - } - - // 确保工业先锋配方已转换 - if (!industrialForegoingRecipesConverted) { - try { - // 转换工业先锋化学溶解室配方 - List dissolutionRecipes = com.sorrowmist.useless.compat.industrialforegoing.IndustrialForegoingRecipeConverter.convertDissolutionChamberRecipes(recipeManager); - industrialForegoingConvertedRecipes.addAll(dissolutionRecipes); - - industrialForegoingRecipesConverted = true; - } catch (Exception e) { - // Failed to convert Industrial Foregoing recipes, continue with other recipes - } - } - - // 确保血魔法配方已转换 - if (!bloodMagicRecipesConverted) { - try { - // 转换血魔法血之祭坛配方 - List bloodAltarRecipes = com.sorrowmist.useless.compat.bloodmagic.BloodMagicRecipeConverter.convertBloodAltarRecipes(recipeManager); - bloodMagicConvertedRecipes.addAll(bloodAltarRecipes); - - // 添加血魔法狱火熔炉配方转换 - List soulForgeRecipes = com.sorrowmist.useless.compat.bloodmagic.BloodMagicRecipeConverter.convertTartaricForgeRecipes(recipeManager); - bloodMagicConvertedRecipes.addAll(soulForgeRecipes); - - // 添加血魔法炼金术桌配方转换 - List alchemyTableRecipes = com.sorrowmist.useless.compat.bloodmagic.BloodMagicRecipeConverter.convertAlchemyTableRecipes(recipeManager); - bloodMagicConvertedRecipes.addAll(alchemyTableRecipes); - - bloodMagicRecipesConverted = true; - } catch (Exception e) { - // Failed to convert Blood Magic recipes, continue with other recipes - } - } - - // 确保余烬mod配方已转换 - if (!embersRecipesConverted) { - try { - // 转换余烬炼金台配方 - List embersAlchemyRecipes = com.sorrowmist.useless.compat.embers.EmbersRecipeConverter.convertAlchemyTabletRecipes(recipeManager); - embersConvertedRecipes.addAll(embersAlchemyRecipes); - - // 添加余烬地质分离器配方转换 - List embersGeoSeparatorRecipes = com.sorrowmist.useless.compat.embers.EmbersRecipeConverter.convertGeoSeparatorRecipes(recipeManager); - embersConvertedRecipes.addAll(embersGeoSeparatorRecipes); - - // 添加余烬熔炼炉配方转换 - List embersMelterRecipes = com.sorrowmist.useless.compat.embers.EmbersRecipeConverter.convertMelterRecipes(recipeManager); - embersConvertedRecipes.addAll(embersMelterRecipes); - - // 添加余烬压印锤配方转换 - List embersStampingRecipes = com.sorrowmist.useless.compat.embers.EmbersRecipeConverter.convertStampingRecipes(recipeManager); - embersConvertedRecipes.addAll(embersStampingRecipes); - - // 添加余烬混合离心器配方转换 - List embersMixingRecipes = com.sorrowmist.useless.compat.embers.EmbersRecipeConverter.convertMixingRecipes(recipeManager); - embersConvertedRecipes.addAll(embersMixingRecipes); - - embersRecipesConverted = true; - } catch (Exception e) { - // Failed to convert Embers recipes, continue with other recipes - } - } - - // 获取所有高级合金炉配方 - Collection allRecipes = new ArrayList<>(recipeManager.getAllRecipesFor( - com.sorrowmist.useless.recipes.ModRecipeTypes.ADVANCED_ALLOY_FURNACE_TYPE.get() - )); - - // 添加转换后的配方 - allRecipes.addAll(thermalConvertedRecipes); - allRecipes.addAll(mekanismConvertedRecipes); - allRecipes.addAll(industrialForegoingConvertedRecipes); - allRecipes.addAll(bloodMagicConvertedRecipes); - allRecipes.addAll(embersConvertedRecipes); - - for (AdvancedAlloyFurnaceRecipe recipe : allRecipes) { - List recipeFluids = recipe.getInputFluids(); - if (!recipeFluids.isEmpty()) { - for (FluidStack recipeFluid : recipeFluids) { - if (recipeFluid.getFluid().isSame(fluid.getFluid())) { - // 缓存结果 - recipeCache.put(cacheKey, Collections.singletonList(recipe)); - return true; - } - } - } - } - - // 缓存结果 - recipeCache.put(cacheKey, Collections.emptyList()); - return false; - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/registry/BlacklistManager.java b/src/main/java/com/sorrowmist/useless/registry/BlacklistManager.java deleted file mode 100644 index 37a51590..00000000 --- a/src/main/java/com/sorrowmist/useless/registry/BlacklistManager.java +++ /dev/null @@ -1,44 +0,0 @@ -// BlacklistManager.java -package com.sorrowmist.useless.registry; - -import net.minecraft.world.item.ItemStack; - -import java.util.HashSet; -import java.util.Set; - -public class BlacklistManager { - private static final Set BLACKLISTED_OUTPUTS = new HashSet<>(); - - static { - // 将所有无用锭添加到黑名单 - BLACKLISTED_OUTPUTS.add("useless_mod:useless_ingot_tier_1"); - BLACKLISTED_OUTPUTS.add("useless_mod:useless_ingot_tier_2"); - BLACKLISTED_OUTPUTS.add("useless_mod:useless_ingot_tier_3"); - BLACKLISTED_OUTPUTS.add("useless_mod:useless_ingot_tier_4"); - BLACKLISTED_OUTPUTS.add("useless_mod:useless_ingot_tier_5"); - BLACKLISTED_OUTPUTS.add("useless_mod:useless_ingot_tier_6"); - BLACKLISTED_OUTPUTS.add("useless_mod:useless_ingot_tier_7"); - BLACKLISTED_OUTPUTS.add("useless_mod:useless_ingot_tier_8"); - BLACKLISTED_OUTPUTS.add("useless_mod:useless_ingot_tier_9"); - // 添加新的锭到黑名单,防止它们被催化剂影响 - BLACKLISTED_OUTPUTS.add("useless_mod:useful_ingot"); - } - - public static boolean isOutputBlacklisted(ItemStack stack) { - if (stack.isEmpty()) return false; - String itemId = net.minecraftforge.registries.ForgeRegistries.ITEMS.getKey(stack.getItem()).toString(); - return BLACKLISTED_OUTPUTS.contains(itemId); - } - - public static void addToBlacklist(String itemId) { - BLACKLISTED_OUTPUTS.add(itemId); - } - - public static void removeFromBlacklist(String itemId) { - BLACKLISTED_OUTPUTS.remove(itemId); - } - - public static Set getBlacklist() { - return new HashSet<>(BLACKLISTED_OUTPUTS); - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/registry/CatalystManager.java b/src/main/java/com/sorrowmist/useless/registry/CatalystManager.java deleted file mode 100644 index d3febd72..00000000 --- a/src/main/java/com/sorrowmist/useless/registry/CatalystManager.java +++ /dev/null @@ -1,97 +0,0 @@ -// 文件:CatalystManager.java -package com.sorrowmist.useless.registry; - -import net.minecraft.ChatFormatting; -import net.minecraft.network.chat.Component; -import net.minecraft.world.item.ItemStack; - -import java.util.*; - -public class CatalystManager { - private static final Map CATALYST_PARALLEL_MAP = new LinkedHashMap<>(); - - static { - // 初始化催化剂并行数映射 - CATALYST_PARALLEL_MAP.put("useless_mod:useless_ingot_tier_1", 3); - CATALYST_PARALLEL_MAP.put("useless_mod:useless_ingot_tier_2", 9); - CATALYST_PARALLEL_MAP.put("useless_mod:useless_ingot_tier_3", 27); - CATALYST_PARALLEL_MAP.put("useless_mod:useless_ingot_tier_4", 81); - CATALYST_PARALLEL_MAP.put("useless_mod:useless_ingot_tier_5", 243); - CATALYST_PARALLEL_MAP.put("useless_mod:useless_ingot_tier_6", 729); - CATALYST_PARALLEL_MAP.put("useless_mod:useless_ingot_tier_7", 2187); - CATALYST_PARALLEL_MAP.put("useless_mod:useless_ingot_tier_8", 6561); - CATALYST_PARALLEL_MAP.put("useless_mod:useless_ingot_tier_9", 19683); - // 新增:有用的锭,无并行数上限 - CATALYST_PARALLEL_MAP.put("useless_mod:useful_ingot", Integer.MAX_VALUE); - } - - public static int getCatalystParallel(ItemStack stack) { - if (stack.isEmpty()) { - return 1; // 没有催化剂时并行数为1 - } - - String itemId = net.minecraftforge.registries.ForgeRegistries.ITEMS.getKey(stack.getItem()).toString(); - return CATALYST_PARALLEL_MAP.getOrDefault(itemId, 1); - } - - public static Map getAllCatalysts() { - return new HashMap<>(CATALYST_PARALLEL_MAP); - } - - // 获取催化剂名称(用于显示) - public static String getCatalystName(ItemStack stack) { - if (stack.isEmpty()) return ""; - - String itemId = net.minecraftforge.registries.ForgeRegistries.ITEMS.getKey(stack.getItem()).toString(); - switch (itemId) { - case "useless_mod:useless_ingot_tier_1": return "一阶无用锭"; - case "useless_mod:useless_ingot_tier_2": return "二阶无用锭"; - case "useless_mod:useless_ingot_tier_3": return "三阶无用锭"; - case "useless_mod:useless_ingot_tier_4": return "四阶无用锭"; - case "useless_mod:useless_ingot_tier_5": return "五阶无用锭"; - case "useless_mod:useless_ingot_tier_6": return "六阶无用锭"; - case "useless_mod:useless_ingot_tier_7": return "七阶无用锭"; - case "useless_mod:useless_ingot_tier_8": return "八阶无用锭"; - case "useless_mod:useless_ingot_tier_9": return "九阶无用锭"; - case "useless_mod:possible_useful_ingot": return "可能有用锭"; - case "useless_mod:useful_ingot": return "有用锭"; - default: return ""; - } - } - - - // 在 CatalystManager.java 中修改 getFormattedCatalystList 方法 - public static List getFormattedCatalystList() { - List list = new ArrayList<>(); - - // 按照阶数顺序创建催化剂列表 - ItemStack[] orderedCatalysts = new ItemStack[]{ - new ItemStack(ModIngots.USELESS_INGOT_TIER_1.get()), - new ItemStack(ModIngots.USELESS_INGOT_TIER_2.get()), - new ItemStack(ModIngots.USELESS_INGOT_TIER_3.get()), - new ItemStack(ModIngots.USELESS_INGOT_TIER_4.get()), - new ItemStack(ModIngots.USELESS_INGOT_TIER_5.get()), - new ItemStack(ModIngots.USELESS_INGOT_TIER_6.get()), - new ItemStack(ModIngots.USELESS_INGOT_TIER_7.get()), - new ItemStack(ModIngots.USELESS_INGOT_TIER_8.get()), - new ItemStack(ModIngots.USELESS_INGOT_TIER_9.get()), - new ItemStack(ModIngots.USEFUL_INGOT.get()) // 添加有用锭 - }; - - // 按照顺序添加催化剂信息 - for (ItemStack catalyst : orderedCatalysts) { - int parallel = getCatalystParallel(catalyst); - String catalystName = getCatalystName(catalyst); - String parallelText = parallel == Integer.MAX_VALUE ? "无限" : String.valueOf(parallel); - list.add(Component.literal("• " + catalystName + ": " + parallelText + "倍并行").withStyle(ChatFormatting.GREEN)); - } - - // 添加有用的锭的特殊属性说明 - list.add(Component.literal("• 有用锭特殊属性:").withStyle(ChatFormatting.GOLD)); - list.add(Component.literal(" - 作为催化剂时不会被消耗").withStyle(ChatFormatting.AQUA)); - list.add(Component.literal(" - 并行时能量消耗不会被倍增").withStyle(ChatFormatting.AQUA)); - list.add(Component.literal(" - 没有并行数上限限制").withStyle(ChatFormatting.AQUA)); - - return list; - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/registry/ModComponents.java b/src/main/java/com/sorrowmist/useless/registry/ModComponents.java deleted file mode 100644 index 93cdfa16..00000000 --- a/src/main/java/com/sorrowmist/useless/registry/ModComponents.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.sorrowmist.useless.registry; - -import com.sorrowmist.useless.UselessMod; -import net.minecraft.world.item.Item; -import net.minecraftforge.registries.DeferredRegister; -import net.minecraftforge.registries.ForgeRegistries; -import net.minecraftforge.registries.RegistryObject; - -public final class ModComponents { - public static final DeferredRegister ITEMS = DeferredRegister.create(ForgeRegistries.ITEMS, UselessMod.MOD_ID); - - // 无用锭对应的齿轮物品 - public static final RegistryObject USELESS_GEAR_TIER_1 = ITEMS.register("useless_gear_tier_1", - () -> new Item(new Item.Properties().rarity(ModIngots.COMMON))); - - public static final RegistryObject USELESS_GEAR_TIER_2 = ITEMS.register("useless_gear_tier_2", - () -> new Item(new Item.Properties().rarity(ModIngots.COMMON))); - - public static final RegistryObject USELESS_GEAR_TIER_3 = ITEMS.register("useless_gear_tier_3", - () -> new Item(new Item.Properties().rarity(ModIngots.UNCOMMON))); - - public static final RegistryObject USELESS_GEAR_TIER_4 = ITEMS.register("useless_gear_tier_4", - () -> new Item(new Item.Properties().rarity(ModIngots.UNCOMMON))); - - public static final RegistryObject USELESS_GEAR_TIER_5 = ITEMS.register("useless_gear_tier_5", - () -> new Item(new Item.Properties().rarity(ModIngots.RARE))); - - public static final RegistryObject USELESS_GEAR_TIER_6 = ITEMS.register("useless_gear_tier_6", - () -> new Item(new Item.Properties().rarity(ModIngots.RARE))); - - public static final RegistryObject USELESS_GEAR_TIER_7 = ITEMS.register("useless_gear_tier_7", - () -> new Item(new Item.Properties().rarity(ModIngots.EPIC))); - - public static final RegistryObject USELESS_GEAR_TIER_8 = ITEMS.register("useless_gear_tier_8", - () -> new Item(new Item.Properties().rarity(ModIngots.MYTHIC))); - - public static final RegistryObject USELESS_GEAR_TIER_9 = ITEMS.register("useless_gear_tier_9", - () -> new Item(new Item.Properties().rarity(ModIngots.LEGENDARY))); - - // 无用锭对应的玻璃物品 - public static final RegistryObject USELESS_GLASS_TIER_1 = ITEMS.register("useless_glass_tier_1", - () -> new Item(new Item.Properties().rarity(ModIngots.COMMON))); - - public static final RegistryObject USELESS_GLASS_TIER_2 = ITEMS.register("useless_glass_tier_2", - () -> new Item(new Item.Properties().rarity(ModIngots.COMMON))); - - public static final RegistryObject USELESS_GLASS_TIER_3 = ITEMS.register("useless_glass_tier_3", - () -> new Item(new Item.Properties().rarity(ModIngots.UNCOMMON))); - - public static final RegistryObject USELESS_GLASS_TIER_4 = ITEMS.register("useless_glass_tier_4", - () -> new Item(new Item.Properties().rarity(ModIngots.UNCOMMON))); - - public static final RegistryObject USELESS_GLASS_TIER_5 = ITEMS.register("useless_glass_tier_5", - () -> new Item(new Item.Properties().rarity(ModIngots.RARE))); - - public static final RegistryObject USELESS_GLASS_TIER_6 = ITEMS.register("useless_glass_tier_6", - () -> new Item(new Item.Properties().rarity(ModIngots.RARE))); - - public static final RegistryObject USELESS_GLASS_TIER_7 = ITEMS.register("useless_glass_tier_7", - () -> new Item(new Item.Properties().rarity(ModIngots.EPIC))); - - public static final RegistryObject USELESS_GLASS_TIER_8 = ITEMS.register("useless_glass_tier_8", - () -> new Item(new Item.Properties().rarity(ModIngots.MYTHIC))); - - public static final RegistryObject USELESS_GLASS_TIER_9 = ITEMS.register("useless_glass_tier_9", - () -> new Item(new Item.Properties().rarity(ModIngots.LEGENDARY))); -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/registry/ModIngots.java b/src/main/java/com/sorrowmist/useless/registry/ModIngots.java deleted file mode 100644 index a4289231..00000000 --- a/src/main/java/com/sorrowmist/useless/registry/ModIngots.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.sorrowmist.useless.registry; - -import com.sorrowmist.useless.UselessMod; -import net.minecraft.ChatFormatting; -import net.minecraft.world.item.Item; -import net.minecraft.world.item.Rarity; -import net.minecraftforge.registries.DeferredRegister; -import net.minecraftforge.registries.ForgeRegistries; -import net.minecraftforge.registries.RegistryObject; - -public final class ModIngots { - public static final DeferredRegister ITEMS = DeferredRegister.create(ForgeRegistries.ITEMS, UselessMod.MOD_ID); - - // 定义稀有度等级 - public static final Rarity COMMON = Rarity.COMMON; - public static final Rarity UNCOMMON = Rarity.UNCOMMON; - public static final Rarity RARE = Rarity.RARE; - public static final Rarity EPIC = Rarity.EPIC; - - // 自定义稀有度 - public static final Rarity MYTHIC = Rarity.create("mythic", style -> style.withColor(ChatFormatting.GOLD)); - public static final Rarity LEGENDARY = Rarity.create("legendary", style -> style.withColor(ChatFormatting.RED)); - - // 注册九种不同等级的锭 - public static final RegistryObject USELESS_INGOT_TIER_1 = ITEMS.register("useless_ingot_tier_1", - () -> new Item(new Item.Properties().rarity(COMMON))); - - public static final RegistryObject USELESS_INGOT_TIER_2 = ITEMS.register("useless_ingot_tier_2", - () -> new Item(new Item.Properties().rarity(COMMON))); - - public static final RegistryObject USELESS_INGOT_TIER_3 = ITEMS.register("useless_ingot_tier_3", - () -> new Item(new Item.Properties().rarity(UNCOMMON))); - - public static final RegistryObject USELESS_INGOT_TIER_4 = ITEMS.register("useless_ingot_tier_4", - () -> new Item(new Item.Properties().rarity(UNCOMMON))); - - public static final RegistryObject USELESS_INGOT_TIER_5 = ITEMS.register("useless_ingot_tier_5", - () -> new Item(new Item.Properties().rarity(RARE))); - - public static final RegistryObject USELESS_INGOT_TIER_6 = ITEMS.register("useless_ingot_tier_6", - () -> new Item(new Item.Properties().rarity(RARE))); - - public static final RegistryObject USELESS_INGOT_TIER_7 = ITEMS.register("useless_ingot_tier_7", - () -> new Item(new Item.Properties().rarity(EPIC))); - - public static final RegistryObject USELESS_INGOT_TIER_8 = ITEMS.register("useless_ingot_tier_8", - () -> new Item(new Item.Properties().rarity(MYTHIC))); - - public static final RegistryObject USELESS_INGOT_TIER_9 = ITEMS.register("useless_ingot_tier_9", - () -> new Item(new Item.Properties().rarity(LEGENDARY))); - - // 新增:可能有用的锭,仅作为合成中间材料 - public static final RegistryObject POSSIBLE_USEFUL_INGOT = ITEMS.register("possible_useful_ingot", - () -> new Item(new Item.Properties().rarity(LEGENDARY))); - - // 新增:有用的锭,作为终极催化剂 - public static final RegistryObject USEFUL_INGOT = ITEMS.register("useful_ingot", - () -> new Item(new Item.Properties().rarity(LEGENDARY))); -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/registry/ModMolds.java b/src/main/java/com/sorrowmist/useless/registry/ModMolds.java deleted file mode 100644 index c4dea99d..00000000 --- a/src/main/java/com/sorrowmist/useless/registry/ModMolds.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.sorrowmist.useless.registry; - -import com.sorrowmist.useless.UselessMod; -import net.minecraft.world.item.Item; -import net.minecraftforge.registries.DeferredRegister; -import net.minecraftforge.registries.ForgeRegistries; -import net.minecraftforge.registries.RegistryObject; - -public final class ModMolds { - public static final DeferredRegister ITEMS = DeferredRegister.create(ForgeRegistries.ITEMS, UselessMod.MOD_ID); - - // 注册四种金属模具 - public static final RegistryObject METAL_MOLD_PLATE = ITEMS.register("metal_mold_plate", - () -> new Item(new Item.Properties())); - - public static final RegistryObject METAL_MOLD_ROD = ITEMS.register("metal_mold_rod", - () -> new Item(new Item.Properties())); - - public static final RegistryObject METAL_MOLD_GEAR = ITEMS.register("metal_mold_gear", - () -> new Item(new Item.Properties())); - - public static final RegistryObject METAL_MOLD_WIRE = ITEMS.register("metal_mold_wire", - () -> new Item(new Item.Properties())); - - public static final RegistryObject METAL_MOLD_BLOCK = ITEMS.register("metal_mold_block", - () -> new Item(new Item.Properties())); -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/registry/RegistryHandler.java b/src/main/java/com/sorrowmist/useless/registry/RegistryHandler.java deleted file mode 100644 index e320371b..00000000 --- a/src/main/java/com/sorrowmist/useless/registry/RegistryHandler.java +++ /dev/null @@ -1,134 +0,0 @@ -package com.sorrowmist.useless.registry; - -import com.sorrowmist.useless.UselessMod; -import com.sorrowmist.useless.blocks.CreatorDollBlock; -import com.sorrowmist.useless.blocks.GlowPlasticBlock; -import com.sorrowmist.useless.blocks.ModBlockEntities; -import com.sorrowmist.useless.blocks.ModMenuTypes; -import com.sorrowmist.useless.blocks.advancedalloyfurnace.AdvancedAlloyFurnaceBlock; -import com.sorrowmist.useless.blocks.oregenerator.OreGeneratorBlock; -import com.sorrowmist.useless.blocks.teleport.TeleportBlock; -import com.sorrowmist.useless.blocks.teleport.TeleportBlock2; -import com.sorrowmist.useless.blocks.teleport.TeleportBlock3; -import com.sorrowmist.useless.inventories.UselessTab; -import com.sorrowmist.useless.items.EndlessBeafItem; -import com.sorrowmist.useless.recipes.ModRecipeSerializers; -import com.sorrowmist.useless.recipes.ModRecipeTypes; -import com.sorrowmist.useless.utils.ThermalDependencyHelper; -import com.sorrowmist.useless.worldgen.dimension.UselessDimension; -import com.sorrowmist.useless.worldgen.dimension.UselessDimension2; -import com.sorrowmist.useless.worldgen.dimension.UselessDimension3; -import net.minecraftforge.eventbus.api.IEventBus; -import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; -import net.minecraftforge.registries.DeferredRegister; - -import java.util.ArrayList; -import java.util.List; - -public class RegistryHandler { - // 存储所有需要注册的DeferredRegister - private static final List> REGISTRIES = new ArrayList<>(); - - // 存储其他类型的注册器 - private static final List OTHER_REGISTRIES = new ArrayList<>(); - - /** - * 初始化所有注册 - */ - public static void initAll(IEventBus modEventBus) { - // 收集所有需要注册的DeferredRegister - collectAllRegistries(); - - // 注册所有DeferredRegister - for (DeferredRegister registry : REGISTRIES) { - registry.register(modEventBus); - } - - // 注册其他类型的注册器 - for (Runnable registry : OTHER_REGISTRIES) { - registry.run(); - } - } - - /** - * 收集所有需要注册的DeferredRegister - */ - // 在 RegistryHandler.java 的 collectAllRegistries() 方法中添加 - private static void collectAllRegistries() { - // 物品注册 - addRegistry(EndlessBeafItem.ITEMS); - - // 创造模式标签注册 - addRegistry(UselessTab.CREATIVE_TAB); - - // 方块和物品注册 - addRegistry(TeleportBlock.BLOCKS); - addRegistry(TeleportBlock.ITEMS); - addRegistry(TeleportBlock2.BLOCKS); - addRegistry(TeleportBlock2.ITEMS); - addRegistry(TeleportBlock3.BLOCKS); - addRegistry(TeleportBlock3.ITEMS); - addRegistry(OreGeneratorBlock.BLOCKS); - addRegistry(OreGeneratorBlock.ITEMS); - addRegistry(GlowPlasticBlock.BLOCKS); - addRegistry(GlowPlasticBlock.ITEMS); - - - addRegistry(AdvancedAlloyFurnaceBlock.BLOCKS); - addRegistry(AdvancedAlloyFurnaceBlock.ITEMS); - addRegistry(CreatorDollBlock.BLOCKS); - addRegistry(CreatorDollBlock.ITEMS); - // 方块实体注册 - addRegistry(ModBlockEntities.BLOCK_ENTITIES); - - // 维度注册 - addRegistry(UselessDimension.CHUNK_GENERATORS); - addRegistry(UselessDimension2.CHUNK_GENERATORS); - addRegistry(UselessDimension3.CHUNK_GENERATORS); - - // 菜单注册 - addRegistry(ModMenuTypes.MENUS); - - // 添加锭的注册 - addRegistry(ModIngots.ITEMS); - // 添加金属模具的注册 - addRegistry(ModMolds.ITEMS); - // 添加齿轮和玻璃组件的注册 - addRegistry(ModComponents.ITEMS); - - - - addRegistry(ModRecipeTypes.RECIPE_TYPES); - addRegistry(ModRecipeSerializers.RECIPE_SERIALIZERS); - // 添加其他注册器 - if (ThermalDependencyHelper.isAnyThermalModLoaded()) { - addOtherRegistry(() -> ThermalMoreItems.ITEMS.register(FMLJavaModLoadingContext.get().getModEventBus())); - addOtherRegistry(() -> ThermalParallelItems.ITEMS.register(FMLJavaModLoadingContext.get().getModEventBus())); - } - } - - /** - * 安全地添加注册器到列表 - */ - private static void addRegistry(DeferredRegister registry) { - if (registry != null) { - REGISTRIES.add(registry); - } - } - - /** - * 添加其他类型的注册器 - */ - private static void addOtherRegistry(Runnable registryTask) { - if (registryTask != null) { - OTHER_REGISTRIES.add(registryTask); - } - } - - /** - * 获取注册器数量(用于调试) - */ - public static int getRegistryCount() { - return REGISTRIES.size() + OTHER_REGISTRIES.size(); - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/registry/ThermalMoreItems.java b/src/main/java/com/sorrowmist/useless/registry/ThermalMoreItems.java deleted file mode 100644 index cc62ae23..00000000 --- a/src/main/java/com/sorrowmist/useless/registry/ThermalMoreItems.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.sorrowmist.useless.registry; - -/* - * This file is part of Thermal More Integral Components. - * - * Copyright (c) 2024 Elephant_dev - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import cofh.core.common.item.CountedItem; -import cofh.core.common.item.ItemCoFH; -import cofh.core.util.helpers.AugmentDataHelper; -import cofh.lib.util.DeferredRegisterCoFH; -import com.sorrowmist.useless.UselessMod; -import com.sorrowmist.useless.items.UpgradeAugmentItem; -import net.minecraft.ChatFormatting; -import net.minecraft.world.item.Item; -import net.minecraft.world.item.Rarity; -import net.minecraftforge.registries.ForgeRegistries; -import net.minecraftforge.registries.RegistryObject; - -import java.util.function.Consumer; -import java.util.function.Supplier; - -public final class ThermalMoreItems { - public static final DeferredRegisterCoFH ITEMS = DeferredRegisterCoFH.create(ForgeRegistries.ITEMS, UselessMod.MOD_ID); - - public static final Rarity YELLOW = Rarity.UNCOMMON; - public static final Rarity EPIC = Rarity.EPIC; - public static final Rarity DARK_AQUA = Rarity.create("extra_dark_aqua", style -> style.withColor(ChatFormatting.DARK_AQUA)); - public static final Rarity DARK_PURPLE = Rarity.create("extra_dark_purple", style -> style.withColor(ChatFormatting.DARK_PURPLE)); - public static final Rarity RED = Rarity.create("extra_red", style -> style.withColor(ChatFormatting.RED)); - - // 重命名所有物品 - public static final RegistryObject USELESS_INTEGRAL_COMPONENT_TIER_1 = ITEMS.register("useless_integral_component_tier_1", - () -> new UpgradeAugmentItem((new Item.Properties()).rarity(RED), - AugmentDataHelper.builder().type("Upgrade").mod("BaseMod", 10.0F).build())); - - public static final RegistryObject USELESS_INTEGRAL_COMPONENT_TIER_2 = ITEMS.register("useless_integral_component_tier_2", - () -> new UpgradeAugmentItem((new Item.Properties()).rarity(RED), - AugmentDataHelper.builder().type("Upgrade").mod("BaseMod", 25.0F).build())); - - public static final RegistryObject USELESS_INTEGRAL_COMPONENT_TIER_3 = ITEMS.register("useless_integral_component_tier_3", - () -> new UpgradeAugmentItem((new Item.Properties()).rarity(YELLOW), - AugmentDataHelper.builder().type("Upgrade").mod("BaseMod", 50.0F).build())); - - public static final RegistryObject USELESS_INTEGRAL_COMPONENT_TIER_4 = ITEMS.register("useless_integral_component_tier_4", - () -> new UpgradeAugmentItem((new Item.Properties()).rarity(YELLOW), - AugmentDataHelper.builder().type("Upgrade").mod("BaseMod", 100.0F).build())); - - public static final RegistryObject USELESS_INTEGRAL_COMPONENT_TIER_5 = ITEMS.register("useless_integral_component_tier_5", - () -> new UpgradeAugmentItem((new Item.Properties()).rarity(DARK_AQUA), - AugmentDataHelper.builder().type("Upgrade").mod("BaseMod", 250.0F).build())); - - public static final RegistryObject USELESS_INTEGRAL_COMPONENT_TIER_6 = ITEMS.register("useless_integral_component_tier_6", - () -> new UpgradeAugmentItem((new Item.Properties()).rarity(DARK_AQUA), - AugmentDataHelper.builder().type("Upgrade").mod("BaseMod", 500.0F).build())); - - public static final RegistryObject USELESS_INTEGRAL_COMPONENT_TIER_7 = ITEMS.register("useless_integral_component_tier_7", - () -> new UpgradeAugmentItem((new Item.Properties()).rarity(DARK_PURPLE), - AugmentDataHelper.builder().type("Upgrade").mod("BaseMod", 1000.0F).build())); - - public static final RegistryObject USELESS_INTEGRAL_COMPONENT_TIER_8 = ITEMS.register("useless_integral_component_tier_8", - () -> new UpgradeAugmentItem((new Item.Properties()).rarity(DARK_PURPLE), - AugmentDataHelper.builder().type("Upgrade").mod("BaseMod", 2500.0F).build())); - - public static final RegistryObject USELESS_INTEGRAL_COMPONENT_TIER_9 = ITEMS.register("useless_integral_component_tier_9", - () -> new UpgradeAugmentItem((new Item.Properties()).rarity(EPIC), - AugmentDataHelper.builder().type("Upgrade").mod("BaseMod", 5000.0F).build())); - - - public static Supplier item(Consumer consumer) { - return item(consumer, false); - } - - public static Supplier item(Consumer consumer, boolean count) { - Item.Properties props = new Item.Properties(); - consumer.accept(props); - return count ? () -> new CountedItem(props) : () -> new ItemCoFH(props); - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/registry/ThermalParallelItems.java b/src/main/java/com/sorrowmist/useless/registry/ThermalParallelItems.java deleted file mode 100644 index 3552ef86..00000000 --- a/src/main/java/com/sorrowmist/useless/registry/ThermalParallelItems.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.sorrowmist.useless.registry; - -/* - * This file is part of Thermal Parallel. - * - * Copyright (c) 2025 EtSH-C2H6S - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import cofh.core.util.helpers.AugmentDataHelper; -import cofh.thermal.lib.common.item.AugmentItem; -import net.minecraft.world.item.Item; -import net.minecraftforge.registries.DeferredRegister; -import net.minecraftforge.registries.ForgeRegistries; -import net.minecraftforge.registries.RegistryObject; - -public class ThermalParallelItems { - public static final DeferredRegister ITEMS = DeferredRegister.create(ForgeRegistries.ITEMS, "useless_mod"); - - public static final RegistryObject AUGMENT_PARALLEL_1 = ITEMS.register("augment_parallel_1", - () -> new AugmentItem(new Item.Properties(), AugmentDataHelper.builder() - .type("Machine") - .mod("MachineParallel", 1.0F) - .build())); - - public static final RegistryObject AUGMENT_PARALLEL_2 = ITEMS.register("augment_parallel_2", - () -> new AugmentItem(new Item.Properties(), AugmentDataHelper.builder() - .type("Machine") - .mod("MachineParallel", 16.0F) - .build())); - - public static final RegistryObject AUGMENT_PARALLEL_3 = ITEMS.register("augment_parallel_3", - () -> new AugmentItem(new Item.Properties(), AugmentDataHelper.builder() - .type("Machine") - .mod("MachineParallel", 256.0F) - .build())); -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/utils/BlockBreakUtils.java b/src/main/java/com/sorrowmist/useless/utils/BlockBreakUtils.java deleted file mode 100644 index 8ff8c3b8..00000000 --- a/src/main/java/com/sorrowmist/useless/utils/BlockBreakUtils.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.sorrowmist.useless.utils; - -import net.minecraft.core.BlockPos; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.sounds.SoundSource; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.Items; -import net.minecraft.world.level.Level; -import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.level.storage.loot.LootParams; -import net.minecraft.world.level.storage.loot.parameters.LootContextParams; -import net.minecraft.world.phys.Vec3; - -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.stream.Collectors; - -/** - * 方块破坏工具类,包含普通破坏单个方块的逻辑 - */ -public class BlockBreakUtils { - - /** - * 获取方块掉落物 - */ - public static List getBlockDrops(BlockState state, Level level, BlockPos pos, Player player, ItemStack tool) { - if (!(level instanceof ServerLevel serverLevel)) { - return Collections.emptyList(); - } - - try { - // 创建LootParams来获取正确的掉落物 - LootParams.Builder lootParamsBuilder = new LootParams.Builder(serverLevel) - .withParameter(LootContextParams.ORIGIN, Vec3.atCenterOf(pos)) - .withParameter(LootContextParams.TOOL, tool) - .withParameter(LootContextParams.THIS_ENTITY, player) - .withParameter(LootContextParams.BLOCK_STATE, state) - .withOptionalParameter(LootContextParams.BLOCK_ENTITY, level.getBlockEntity(pos)); - - List drops = state.getDrops(lootParamsBuilder); - - // 过滤掉空气和空堆叠 - return drops.stream() - .filter(drop -> !drop.isEmpty() && drop.getItem() != Items.AIR) - .collect(Collectors.toList()); - - } catch (Exception e) { - // 忽略错误但不崩溃 - return Collections.emptyList(); - } - } - - /** - * 检查掉落物列表是否有效 - */ - public static boolean hasInvalidDrops(List drops) { - // 检查所有掉落物是否都是空气或无效物品 - for (ItemStack drop : drops) { - if (!drop.isEmpty() && drop.getItem() != Items.AIR) { - return false; // 至少有一个有效掉落物 - } - } - return true; // 所有掉落物都无效 - } - - /** - * 将物品添加到玩家背包 - */ - public static boolean addItemToPlayerInventory(Player player, ItemStack stack) { - if (player.getInventory().add(stack)) { - // 成功添加到背包 - return true; - } else { - // 背包已满 - return false; - } - } - - /** - * 带冷却的音效播放方法 - */ - public static void playBreakSoundWithCooldown(Level level, BlockPos pos, BlockState state, Player player) { - if (level.isClientSide()) return; - - UUID playerId = player.getUUID(); - long currentTime = System.currentTimeMillis(); - Long lastTime = lastSoundTime.get(playerId); - - // 检查冷却时间 - if (lastTime == null || currentTime - lastTime >= SOUND_COOLDOWN) { - level.playSound(null, pos, state.getSoundType().getBreakSound(), SoundSource.BLOCKS, 0.7F, 1.0F); - lastSoundTime.put(playerId, currentTime); - } - } - - // 音效冷却系统 - private static final Map lastSoundTime = new HashMap<>(); - private static final long SOUND_COOLDOWN = 50; // 50毫秒冷却时间 -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/utils/EnchantmentUtil.java b/src/main/java/com/sorrowmist/useless/utils/EnchantmentUtil.java new file mode 100644 index 00000000..77a382fb --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/utils/EnchantmentUtil.java @@ -0,0 +1,30 @@ +package com.sorrowmist.useless.utils; + +import net.minecraft.core.HolderLookup; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.enchantment.Enchantment; +import net.minecraft.world.item.enchantment.EnchantmentHelper; + +import java.util.HashMap; +import java.util.Map; + +public class EnchantmentUtil { + + /** + * 安全地向物品栈添加附魔 + * + * @param stack 目标物品栈 + * @param lookup 注册表查询句柄 (可以通过 level.registryAccess() 或物品栏参数获取) + * @param enchantment 附魔 + * @param level 附魔等级 + */ + public static void applyEnchantment(ItemStack stack, HolderLookup.Provider lookup, + Enchantment enchantment, int level) { + Map enchantments = new HashMap<>(EnchantmentHelper.getEnchantments(stack)); + int normalizedLevel = Math.max(0, level); + if (enchantments.getOrDefault(enchantment, 0) == normalizedLevel) return; + if (normalizedLevel == 0) enchantments.remove(enchantment); + else enchantments.put(enchantment, normalizedLevel); + EnchantmentHelper.setEnchantments(enchantments, stack); + } +} diff --git a/src/main/java/com/sorrowmist/useless/utils/EnergyStorageWrapper.java b/src/main/java/com/sorrowmist/useless/utils/EnergyStorageWrapper.java new file mode 100644 index 00000000..f51f6e74 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/utils/EnergyStorageWrapper.java @@ -0,0 +1,111 @@ +package com.sorrowmist.useless.utils; + +import net.minecraftforge.energy.IEnergyStorage; + +/** + * 能量存储包装类 - 用于解决 MinecraftForge 1.21 中 EnergyStorage 反序列化限制 + * 该类提供一个兼容的 IEnergyStorage 实现,可以安全地进行序列化/反序列化 + */ +public class EnergyStorageWrapper implements IEnergyStorage { + private int energy; + private final int capacity; + private final int maxReceive; + private final int maxExtract; + private final EnergyChangeListener energyChangeListener; + + public interface EnergyChangeListener { + void onEnergyChange(); + } + + public EnergyStorageWrapper(int capacity) { + this(capacity, capacity, capacity, 0, null); + } + + public EnergyStorageWrapper(int capacity, int maxTransfer) { + this(capacity, maxTransfer, maxTransfer, 0, null); + } + + public EnergyStorageWrapper(int capacity, int maxReceive, int maxExtract) { + this(capacity, maxReceive, maxExtract, 0, null); + } + + public EnergyStorageWrapper(int capacity, int maxReceive, int maxExtract, int initialEnergy) { + this(capacity, maxReceive, maxExtract, initialEnergy, null); + } + + public EnergyStorageWrapper(int capacity, int maxReceive, int maxExtract, int initialEnergy, EnergyChangeListener listener) { + this.capacity = Math.max(0, capacity); + this.maxReceive = Math.max(0, Math.min(capacity, maxReceive)); + this.maxExtract = Math.max(0, Math.min(capacity, maxExtract)); + this.energy = Math.max(0, Math.min(initialEnergy, capacity)); + this.energyChangeListener = listener; + } + + @Override + public int receiveEnergy(int maxReceive, boolean simulate) { + if (!canReceive()) { + return 0; + } + + int energyReceived = Math.min(capacity - energy, Math.min(this.maxReceive, maxReceive)); + if (!simulate && energyReceived > 0) { + energy += energyReceived; + notifyEnergyChange(); + } + + return energyReceived; + } + + @Override + public int extractEnergy(int maxExtract, boolean simulate) { + if (!canExtract()) { + return 0; + } + + int energyExtracted = Math.min(energy, Math.min(this.maxExtract, maxExtract)); + if (!simulate && energyExtracted > 0) { + energy -= energyExtracted; + notifyEnergyChange(); + } + + return energyExtracted; + } + + @Override + public int getEnergyStored() { + return energy; + } + + @Override + public int getMaxEnergyStored() { + return capacity; + } + + @Override + public boolean canExtract() { + return this.maxExtract > 0; + } + + @Override + public boolean canReceive() { + return this.maxReceive > 0; + } + + public void setEnergy(int energy) { + int oldEnergy = this.energy; + this.energy = Math.max(0, Math.min(energy, capacity)); + if (oldEnergy != this.energy) { + notifyEnergyChange(); + } + } + + public void modifyEnergy(int delta) { + setEnergy(this.energy + delta); + } + + private void notifyEnergyChange() { + if (energyChangeListener != null) { + energyChangeListener.onEnergyChange(); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/utils/ForceBreakUtils.java b/src/main/java/com/sorrowmist/useless/utils/ForceBreakUtils.java deleted file mode 100644 index 3b396145..00000000 --- a/src/main/java/com/sorrowmist/useless/utils/ForceBreakUtils.java +++ /dev/null @@ -1,365 +0,0 @@ -package com.sorrowmist.useless.utils; - -import com.sorrowmist.useless.UselessMod; -import com.sorrowmist.useless.items.EndlessBeafItem; -import net.minecraft.core.BlockPos; -import net.minecraft.core.registries.Registries; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.world.entity.item.ItemEntity; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.Items; -import net.minecraft.world.level.Level; -import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.block.Blocks; -import net.minecraft.world.level.block.entity.BlockEntity; -import net.minecraft.world.level.block.state.BlockState; -import net.minecraftforge.event.level.BlockEvent; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * 强制破坏工具类,包含强制破坏单个方块的逻辑 - */ -public class ForceBreakUtils { - - /** - * 强制破坏单个方块 - * @param event 方块破坏事件 - * @param level 世界 - * @param pos 方块位置 - * @param state 方块状态 - * @param player 玩家 - * @param stack 工具 - * @return 是否成功破坏 - */ - public static boolean forceBreakBlock(BlockEvent.BreakEvent event, Level level, BlockPos pos, BlockState state, Player player, ItemStack stack) { - // 检查方块是否含水(更通用的方法,适用于原版和模组方块) - // 获取方块的流体状态 - net.minecraft.world.level.material.FluidState fluidState = level.getFluidState(pos); - // 检查流体是否是水,并且是水源方块(level == 8表示水源) - if (fluidState.getType() == net.minecraft.world.level.material.Fluids.WATER && fluidState.isSource()) { - // 含水方块,直接返回false,让原版破坏逻辑处理 - return false; - } - - // 特殊处理混沌水晶:绕过击败混沌龙检查 - if (handleChaosCrystal(event, level, pos, state, player, stack)) { - return true; - } - - // 4. 强制破坏方块 - 最后破坏方块,这样确保获取了需要的物品 - event.setCanceled(true); - - // 在破坏方块之前,先处理扩展样板供应器的主从关系和样板清空 - // 这是因为createSilkTouchDrop会保存当前的NBT状态,所以需要先处理主从关系和清空样板 - com.sorrowmist.useless.items.EndlessBeafItem.onBlockBreak(event); - - // 然后再创建包含NBT的物品 - List drops = new ArrayList<>(); - - // 1. 检查是否为精准采集模式 - boolean isSilkTouch = isSilkTouchMode(stack); - - if (isSilkTouch) { - // 精准采集模式:只获取包含NBT的物品本身 - ItemStack silkTouchDrop = createSilkTouchDrop(level, pos, state); - drops.add(silkTouchDrop); - } else { - // 非精准采集模式:使用正常逻辑获取掉落物 - // 2. 尝试获取方块的掉落物(使用正常逻辑,包括战利品表和时运) - drops = BlockBreakUtils.getBlockDrops(state, level, pos, player, stack); - - // 3. 如果没有有效的掉落物,强制掉落一个方块 - if (drops == null || drops.isEmpty() || BlockBreakUtils.hasInvalidDrops(drops)) { - // 强制掉落一个目标方块 - ItemStack forcedDrop = new ItemStack(state.getBlock().asItem(), 1); - drops = Collections.singletonList(forcedDrop); - } - } - - // 精准采集模式下,先处理容器内容物,防止设置方块为空气时触发onRemove导致内容物掉落 - if (isSilkTouch) { - BlockEntity blockEntity = level.getBlockEntity(pos); - if (blockEntity != null) { - // 对于容器类BlockEntity,在破坏前确保内容物不会自动掉落 - // 这是因为当我们设置方块为空气时,会触发BlockState.onRemove(), - // 该方法会自动掉落容器内容物,导致重复掉落 - try { - // 对于箱子等容器,直接使用方块实体的方法来清空内容物,而不是通过IItemHandler - // 这样可以确保只影响当前方块实体,不影响相邻方块 - if (blockEntity instanceof net.minecraft.world.level.block.entity.ChestBlockEntity chestEntity) { - // 直接操作箱子的物品列表,只清空当前箱子的内容物 - // 这种方式不会影响相邻箱子,因为每个ChestBlockEntity只管理自己的27个物品槽 - for (int i = 0; i < chestEntity.getContainerSize(); i++) { - // 直接设置为空气,不产生掉落物 - chestEntity.setItem(i, ItemStack.EMPTY); - } - // 标记箱子已更改 - chestEntity.setChanged(); - } else if (blockEntity instanceof net.minecraft.world.level.block.entity.HopperBlockEntity hopperEntity) { - // 处理漏斗 - for (int i = 0; i < hopperEntity.getContainerSize(); i++) { - hopperEntity.setItem(i, ItemStack.EMPTY); - } - hopperEntity.setChanged(); - } else if (blockEntity instanceof net.minecraft.world.level.block.entity.DispenserBlockEntity dispenserEntity) { - // 处理发射器 - for (int i = 0; i < dispenserEntity.getContainerSize(); i++) { - dispenserEntity.setItem(i, ItemStack.EMPTY); - } - dispenserEntity.setChanged(); - } else if (blockEntity instanceof net.minecraft.world.level.block.entity.DropperBlockEntity dropperEntity) { - // 处理投掷器 - for (int i = 0; i < dropperEntity.getContainerSize(); i++) { - dropperEntity.setItem(i, ItemStack.EMPTY); - } - dropperEntity.setChanged(); - } else if (blockEntity instanceof net.minecraft.world.level.block.entity.ShulkerBoxBlockEntity shulkerEntity) { - // 处理潜影盒 - for (int i = 0; i < shulkerEntity.getContainerSize(); i++) { - shulkerEntity.setItem(i, ItemStack.EMPTY); - } - shulkerEntity.setChanged(); - } else { - // 对于其他容器,尝试使用IItemHandler - net.minecraftforge.items.IItemHandler itemHandler = - blockEntity.getCapability(net.minecraftforge.common.capabilities.ForgeCapabilities.ITEM_HANDLER).orElse(null); - if (itemHandler != null) { - // 尝试将处理程序转换为可修改的类型 - if (itemHandler instanceof net.minecraftforge.items.IItemHandlerModifiable modifiableHandler) { - // 清空容器内容物,但由于我们已经保存了NBT,所以不会丢失数据 - // 这样当设置方块为空气时,onRemove方法就不会再次掉落内容物 - for (int i = 0; i < modifiableHandler.getSlots(); i++) { - modifiableHandler.setStackInSlot(i, ItemStack.EMPTY); - } - } - } - } - - // 只处理SophisticatedStorage模组的扩展槽位 - if (blockEntity.getClass().getName().contains("sophisticatedstorage")) { - // 方法1:尝试通过反射清空升级槽位 - try { - // 尝试获取storageWrapper并清空升级槽位 - java.lang.reflect.Field storageWrapperField = blockEntity.getClass().getDeclaredField("storageWrapper"); - storageWrapperField.setAccessible(true); - Object storageWrapper = storageWrapperField.get(blockEntity); - - // 尝试获取upgradeHandler - java.lang.reflect.Method getUpgradeHandlerMethod = storageWrapper.getClass().getMethod("getUpgradeHandler"); - Object upgradeHandler = getUpgradeHandlerMethod.invoke(storageWrapper); - - // 清空升级槽位 - if (upgradeHandler instanceof net.minecraftforge.items.IItemHandlerModifiable modifiableUpgradeHandler) { - for (int i = 0; i < modifiableUpgradeHandler.getSlots(); i++) { - modifiableUpgradeHandler.setStackInSlot(i, ItemStack.EMPTY); - } - } - } catch (Exception e) { - // 如果反射方法失败,尝试方法2:直接操作NBT数据 - } - - // 方法2:直接操作NBT数据清理升级槽位 - CompoundTag blockEntityTag = blockEntity.saveWithoutMetadata(); - if (blockEntityTag.contains("storageWrapper")) { - CompoundTag storageWrapperTag = blockEntityTag.getCompound("storageWrapper"); - if (storageWrapperTag.contains("contents")) { - CompoundTag contentsTag = storageWrapperTag.getCompound("contents"); - if (contentsTag.contains("upgradeInventory")) { - // 清空升级槽位数据 - CompoundTag upgradeInventoryTag = new CompoundTag(); - upgradeInventoryTag.putInt("Size", 0); - upgradeInventoryTag.put("Items", new net.minecraft.nbt.ListTag()); - contentsTag.put("upgradeInventory", upgradeInventoryTag); - - // 更新storageWrapper标签 - storageWrapperTag.put("contents", contentsTag); - - // 更新方块实体标签 - blockEntityTag.put("storageWrapper", storageWrapperTag); - - // 将修改后的NBT数据加载回方块实体 - blockEntity.load(blockEntityTag); - } - } - } - - // 强制更新方块实体的NBT数据,确保清理后的状态被保存 - blockEntity.setChanged(); - } - } catch (Exception e) { - // 如果处理失败,忽略错误但继续执行 - } - } - } - - // 破坏方块,但不产生额外的掉落物 - // 1. 先移除BlockEntity,防止设置方块为空气时触发onRemove导致内容物掉落 - if (isSilkTouch) { - // 对于精准采集模式,我们已经保存了NBT数据,现在可以安全移除BlockEntity - level.removeBlockEntity(pos); - } - - // 2. 设置方块为空气 - level.setBlock(pos, Blocks.AIR.defaultBlockState(), 3); - level.sendBlockUpdated(pos, state, Blocks.AIR.defaultBlockState(), 3); - - // 3. 处理掉落物:优先存入AE网络,然后是玩家背包,最后是掉落 - // 只处理我们自己创建的掉落物,不允许原版掉落物生成 - EndlessBeafItem.handleDrops(drops, player, stack); - - // 4. 生成剩余的掉落物(如果有) - for (ItemStack drop : drops) { - if (!drop.isEmpty() && drop.getItem() != Items.AIR) { - // 掉落在玩家脚下 - ItemEntity itemEntity = new ItemEntity(level, - player.getX(), player.getY(), player.getZ(), - drop.copy()); - level.addFreshEntity(itemEntity); - } - } - - // 6. 播放破坏音效 - BlockBreakUtils.playBreakSoundWithCooldown(level, pos, state, player); - - return true; - } - - /** - * 检查工具是否处于精准采集模式 - */ - private static boolean isSilkTouchMode(ItemStack stack) { - if (stack.hasTag()) { - return stack.getTag().getBoolean("SilkTouchMode"); - } - return false; - } - - /** - * 创建包含NBT数据的方块物品 - */ - private static ItemStack createSilkTouchDrop(Level level, BlockPos pos, BlockState state) { - // 创建方块物品 - ItemStack stack = new ItemStack(state.getBlock().asItem(), 1); - - // 尝试获取并复制方块实体的NBT数据 - BlockEntity blockEntity = level.getBlockEntity(pos); - if (blockEntity != null) { - // 确保所有数据都已同步到NBT - blockEntity.setChanged(); // 标记为已更改,确保数据被保存 - - // 特殊处理:大箱子(DoubleChest) - if (blockEntity instanceof net.minecraft.world.level.block.entity.ChestBlockEntity chestEntity) { - // 获取当前箱子的完整数据,包括内容物 - // 注意:这里保存的是单个箱子的NBT数据,不包括相邻箱子的数据 - CompoundTag blockEntityTag = chestEntity.saveWithoutMetadata(); - if (!blockEntityTag.isEmpty()) { - // 创建方块物品的NBT标签 - CompoundTag itemTag = stack.getOrCreateTag(); - itemTag.put("BlockEntityTag", blockEntityTag); - } - } else { - // 普通方块实体处理 - CompoundTag blockEntityTag = blockEntity.saveWithoutMetadata(); - if (!blockEntityTag.isEmpty()) { - // 创建方块物品的NBT标签 - CompoundTag itemTag = stack.getOrCreateTag(); - itemTag.put("BlockEntityTag", blockEntityTag); - } - } - } - - return stack; - } - - /** - * 处理混沌水晶的特殊破坏逻辑 - */ - private static boolean handleChaosCrystal(BlockEvent.BreakEvent event, Level level, BlockPos pos, BlockState state, Player player, ItemStack stack) { - try { - // 使用反射检查并处理混沌水晶 - Class chaosCrystalClass = Class.forName("com.brandon3055.draconicevolution.blocks.ChaosCrystal"); - Class tileChaosCrystalClass = Class.forName("com.brandon3055.draconicevolution.blocks.tileentity.TileChaosCrystal"); - - // 检查当前方块是否是混沌水晶 - if (chaosCrystalClass.isInstance(state.getBlock())) { - // 获取方块实体 - BlockEntity tileEntity = level.getBlockEntity(pos); - if (tileChaosCrystalClass.isInstance(tileEntity)) { - // 直接调用setDefeated方法,这会正确设置guardianDefeated并触发状态更新 - try { - java.lang.reflect.Method setDefeatedMethod = tileChaosCrystalClass.getMethod("setDefeated"); - setDefeatedMethod.invoke(tileEntity); - } catch (Exception e) { - // 如果setDefeated方法不可用,尝试直接设置字段 - java.lang.reflect.Field guardianDefeatedField = tileChaosCrystalClass.getDeclaredField("guardianDefeated"); - guardianDefeatedField.setAccessible(true); - Object managedBool = guardianDefeatedField.get(tileEntity); - - Class managedBoolClass = managedBool.getClass(); - java.lang.reflect.Method setMethod = managedBoolClass.getMethod("set", boolean.class); - setMethod.invoke(managedBool, true); - } - - // 调用tick方法确保状态更新 - java.lang.reflect.Method tickMethod = tileChaosCrystalClass.getMethod("tick"); - tickMethod.invoke(tileEntity); - } - - // 如果是混沌水晶,直接生成掉落物并破坏方块,绕过detonate方法的重生逻辑 - try { - // 获取DEConfig.chaosDropCount的值 - Class deConfigClass = Class.forName("com.brandon3055.draconicevolution.DEConfig"); - java.lang.reflect.Field chaosDropCountField = deConfigClass.getDeclaredField("chaosDropCount"); - chaosDropCountField.setAccessible(true); - int chaosDropCount = chaosDropCountField.getInt(null); - - // 获取DEContent.CHAOS_SHARD - Class deContentClass = Class.forName("com.brandon3055.draconicevolution.init.DEContent"); - java.lang.reflect.Field chaosShardField = deConfigClass.getDeclaredField("CHAOS_SHARD"); - chaosShardField.setAccessible(true); - Object chaosShardObject = chaosShardField.get(null); - - // 确保CHAOS_SHARD是一个RegistryObject - Class registryObjectClass = Class.forName("net.minecraftforge.registries.RegistryObject"); - if (registryObjectClass.isInstance(chaosShardObject)) { - // 调用get方法获取实际物品 - java.lang.reflect.Method getMethod = registryObjectClass.getMethod("get"); - Object chaosShardItem = getMethod.invoke(chaosShardObject); - - // 创建掉落物 - ItemStack chaosShardStack = new ItemStack((net.minecraft.world.item.Item) chaosShardItem, chaosDropCount); - - // 直接掉落物品 - Block.popResource(level, pos, chaosShardStack); - - // 清除周围的水晶部分 - level.setBlock(pos.above(), Blocks.AIR.defaultBlockState(), 3); - level.setBlock(pos.above(2), Blocks.AIR.defaultBlockState(), 3); - level.setBlock(pos.below(), Blocks.AIR.defaultBlockState(), 3); - level.setBlock(pos.below(2), Blocks.AIR.defaultBlockState(), 3); - - // 直接设置方块为空气,绕过onRemove方法的detonate调用 - level.setBlock(pos, Blocks.AIR.defaultBlockState(), 3); - - // 播放破坏音效 - BlockBreakUtils.playBreakSoundWithCooldown(level, pos, state, player); - - return true; - } - } catch (Exception e) { - // 如果任何步骤失败,回退到正常的强制挖掘逻辑 - } - } - } catch (ClassNotFoundException e) { - // 如果Draconic Evolution模组没有安装,忽略此处理 - } catch (Exception e) { - // 忽略其他异常但不崩溃 - } - - return false; - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/utils/MekTemp.java b/src/main/java/com/sorrowmist/useless/utils/MekTemp.java index 254030c5..76a13642 100644 --- a/src/main/java/com/sorrowmist/useless/utils/MekTemp.java +++ b/src/main/java/com/sorrowmist/useless/utils/MekTemp.java @@ -4,16 +4,21 @@ public class MekTemp { public static String name; + public static final int MAX_EXTRA_OPERATIONS_PER_TICK = 128; public static final ThreadLocal isInjecting = ThreadLocal.withInitial(() -> false); - public static final BiConsumer inject = (reqTime, process) -> { + public static final BiConsumer + inject = (reqTime, process) -> { if (!isInjecting.get()) { isInjecting.set(true); - for(int i = reqTime; i < 0; ++i) { - process.run(); + try { + int extraOperations = Math.min(-reqTime, MAX_EXTRA_OPERATIONS_PER_TICK); + for(int i = 0; i < extraOperations; ++i) { + process.run(); + } + } finally { + isInjecting.set(false); } - - isInjecting.set(false); } }; -} \ No newline at end of file +} diff --git a/src/main/java/com/sorrowmist/useless/utils/MekUtils.java b/src/main/java/com/sorrowmist/useless/utils/MekUtils.java index de86bfcf..d683945a 100644 --- a/src/main/java/com/sorrowmist/useless/utils/MekUtils.java +++ b/src/main/java/com/sorrowmist/useless/utils/MekUtils.java @@ -2,7 +2,7 @@ import com.electronwill.nightconfig.core.Config; import com.electronwill.nightconfig.core.file.CommentedFileConfig; -import com.sorrowmist.useless.config.ConfigManager; +import com.sorrowmist.useless.core.config.ConfigManager; import mekanism.api.Upgrade; import mekanism.common.config.MekanismConfig; import mekanism.common.tile.interfaces.IUpgradeTile; @@ -12,9 +12,33 @@ public class MekUtils { - public static int MAX_UPGRADE = 32; + public static int MAX_UPGRADE = 16; + + static { + File file = FMLPaths.CONFIGDIR.get().resolve("useless_mod-server.toml").toFile(); + + CommentedFileConfig config = CommentedFileConfig.builder(file).autosave().sync().build(); + config.load(); + + Config mekanism = config.get("mekanism_upgrade"); + if (mekanism != null) { + int max = mekanism.getInt("max_upgrade"); + if (max < 1 || max > 64) { + max = 16; + } + MAX_UPGRADE = max; + } else { + config.set("mekanism_upgrade.max_upgrade", MAX_UPGRADE); + config.save(); + } + + config.close(); + } + public static double time(IUpgradeTile tile) { - return Math.pow((double)(MekanismConfig.general.maxUpgradeMultiplier.get() * ConfigManager.getTimeMultiplier()), (double)tile.getComponent().getUpgrades(Upgrade.SPEED) / (double)-8.0F); + double multiplier = Math.pow((double)(MekanismConfig.general.maxUpgradeMultiplier.get() * ConfigManager.getTimeMultiplier()), (double)tile.getComponent().getUpgrades(Upgrade.SPEED) / (double)-8.0F); + // 仅防御 NaN/Infinity 异常值,不再硬截断有效倍数,避免限制高倍率增强 + return Double.isFinite(multiplier) ? multiplier : 1.0; } public static double electricity(IUpgradeTile tile) { @@ -27,6 +51,10 @@ public static double capacity(IUpgradeTile tile) { } public static String exponential(double d) { + if (!Double.isFinite(d) || d <= 0) { + return "0"; + } + int significant = 4; int exp = (int)Math.floor(Math.log10(d)); d *= Math.pow((double)10.0F, (double)(-exp)); @@ -34,27 +62,4 @@ public static String exponential(double d) { double dt = (double)((int)Math.round(d * Math.pow((double)10.0F, (double)(significant - 1)))) / Math.pow((double)10.0F, (double)(significant - 1 - exp)); return Math.abs(exp) <= significant - 1 ? "" + dt : d + "E" + exp; } - - static { - File file = FMLPaths.CONFIGDIR.get().resolve("useless_mod-common.toml").toFile(); - - CommentedFileConfig config = CommentedFileConfig.builder(file).autosave().sync().build(); - config.load(); - - // 读取 section - Config mekanism = config.get("Mekanism升级设置"); - if (mekanism != null) { - int max = mekanism.getInt("最大升级数量"); - if (max < 1 || max > 64) { - max = 16; - } - MAX_UPGRADE = max; - } else { - // 没有 section,就写进去 - config.set("Mekanism升级设置.最大升级数量", MAX_UPGRADE); - config.save(); - } - - config.close(); - } -} \ No newline at end of file +} diff --git a/src/main/java/com/sorrowmist/useless/utils/MoldIdentifier.java b/src/main/java/com/sorrowmist/useless/utils/MoldIdentifier.java deleted file mode 100644 index a07e6405..00000000 --- a/src/main/java/com/sorrowmist/useless/utils/MoldIdentifier.java +++ /dev/null @@ -1,204 +0,0 @@ -package com.sorrowmist.useless.utils; - -import com.sorrowmist.useless.registry.ModIngots; -import com.sorrowmist.useless.registry.ModMolds; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.item.Item; -import net.minecraft.world.item.ItemStack; -import net.minecraftforge.registries.ForgeRegistries; - -/** - * 模具和机器标志物检测器 - * 用于检测物品是否为模具或其他模组的机器标志物,以防止配方冲突 - */ -public class MoldIdentifier { - - // 私有构造函数,防止实例化 - private MoldIdentifier() {} - - /** - * 检查物品是否为无用锭或有用锭(催化剂) - * @param stack 要检查的物品堆叠 - * @return 如果是无用锭或有用锭则返回true,否则返回false - */ - public static boolean isUselessIngot(ItemStack stack) { - if (stack.isEmpty()) return false; - - // 检查物品是否在无用锭或有用锭列表中 - return stack.is(ModIngots.USELESS_INGOT_TIER_1.get()) || - stack.is(ModIngots.USELESS_INGOT_TIER_2.get()) || - stack.is(ModIngots.USELESS_INGOT_TIER_3.get()) || - stack.is(ModIngots.USELESS_INGOT_TIER_4.get()) || - stack.is(ModIngots.USELESS_INGOT_TIER_5.get()) || - stack.is(ModIngots.USELESS_INGOT_TIER_6.get()) || - stack.is(ModIngots.USELESS_INGOT_TIER_7.get()) || - stack.is(ModIngots.USELESS_INGOT_TIER_8.get()) || - stack.is(ModIngots.USELESS_INGOT_TIER_9.get()) || - stack.is(ModIngots.USEFUL_INGOT.get()); - } - - /** - * 检查物品是否为金属模具 - * @param stack 要检查的物品堆叠 - * @return 如果是金属模具则返回true,否则返回false - */ - public static boolean isMetalMold(ItemStack stack) { - if (stack.isEmpty()) return false; - - // 检查物品是否在金属模具列表中 - return stack.is(ModMolds.METAL_MOLD_PLATE.get()) || - stack.is(ModMolds.METAL_MOLD_ROD.get()) || - stack.is(ModMolds.METAL_MOLD_GEAR.get()) || - stack.is(ModMolds.METAL_MOLD_WIRE.get()) || - stack.is(ModMolds.METAL_MOLD_BLOCK.get()); - } - - /** - * 检查物品是否为机器标志物(来自其他模组的机器) - * @param stack 要检查的物品堆叠 - * @return 如果是机器标志物则返回true,否则返回false - */ - public static boolean isMachineMarker(ItemStack stack) { - if (stack.isEmpty()) return false; - - // 这里可以添加其他模组机器的检测逻辑 - // 例如,检查物品的注册表名称是否包含特定模组的命名空间 - Item item = stack.getItem(); - ResourceLocation registryName = ForgeRegistries.ITEMS.getKey(item); - - // 示例:检测Mekanism模组的机器 - if (registryName != null && registryName.getNamespace().equals("mekanism")) { - return isMekanismMachine(registryName); - } - - // 示例:检测Applied Energistics 2模组的机器 - if (registryName != null && registryName.getNamespace().equals("ae2")) { - return isAE2Machine(registryName); - } - - // 示例:检测Thermal(热力系列)模组的机器 - if (registryName != null && isThermalNamespace(registryName.getNamespace())) { - return isThermalMachine(registryName); - } - - // 可以根据需要添加更多模组的检测逻辑 - - return false; - } - - /** - * 检查物品是否为原版熔炉 - * @param stack 要检查的物品堆叠 - * @return 如果是原版熔炉则返回true,否则返回false - */ - public static boolean isVanillaFurnace(ItemStack stack) { - if (stack.isEmpty()) return false; - return stack.is(net.minecraft.world.item.Items.FURNACE); - } - - /** - * 检查物品是否为可接受的标志物(模具、机器标志物或原版熔炉) - * @param stack 要检查的物品堆叠 - * @return 如果是可接受的标志物则返回true,否则返回false - */ - public static boolean isAcceptableMarker(ItemStack stack) { - return isMetalMold(stack) || isMachineMarker(stack) || isVanillaFurnace(stack) || isVanillaGlassPane(stack); - } - - /** - * 检查物品是否为玻璃板(包括所有颜色变体) - * @param stack 要检查的物品堆叠 - * @return 如果是玻璃板则返回true,否则返回false - */ - public static boolean isVanillaGlassPane(ItemStack stack) { - if (stack.isEmpty()) return false; - - // 检查物品是否为原版无色玻璃板 - if (stack.is(net.minecraft.world.item.Items.GLASS_PANE)) { - return true; - } - - // 检查物品是否为彩色玻璃板(通过注册表名称) - Item item = stack.getItem(); - ResourceLocation registryName = ForgeRegistries.ITEMS.getKey(item); - - return registryName != null && registryName.getPath().contains("glass_pane"); - } - - /** - * 检查物品是否为Mekanism模组的机器 - * @param registryName 物品的注册表名称 - * @return 如果是Mekanism机器则返回true,否则返回false - */ - private static boolean isMekanismMachine(ResourceLocation registryName) { - // 检查是否为Mekanism机器,这里检查注册表路径中的关键字 - return registryName.getPath().contains("machine") || registryName.getPath().contains("generator") || - registryName.getPath().contains("processor") || registryName.getPath().contains("factory") || - registryName.getPath().contains("metallurgic_infuser"); - } - - /** - * 检查物品是否为Applied Energistics 2模组的机器 - * @param registryName 物品的注册表名称 - * @return 如果是AE2机器则返回true,否则返回false - */ - private static boolean isAE2Machine(ResourceLocation registryName) { - // 检查是否为AE2机器,这里检查注册表路径中的关键字 - return registryName.getPath().contains("interface") || registryName.getPath().contains("terminal") || - registryName.getPath().contains("storage") || registryName.getPath().contains("controller") || - registryName.getPath().contains("drive") || registryName.getPath().contains("crafting") || - registryName.getPath().contains("processor") || registryName.getPath().contains("import") || - registryName.getPath().contains("export") || registryName.getPath().contains("p2p") || - registryName.getPath().contains("inscriber") || registryName.getPath().contains("knife") || - registryName.getPath().contains("charger"); - } - - /** - * 检查物品是否为TravelAnchors模组的机器 - * @param registryName 物品的注册表名称 - * @return 如果是TravelAnchors机器则返回true,否则返回false - */ - private static boolean isTravelAnchorsMachine(ResourceLocation registryName) { - // 检查是否为TravelAnchors机器,这里检查注册表路径中的关键字 - return registryName.getPath().contains("anchor") || registryName.getPath().contains("teleport"); - } - - /** - * 检查命名空间是否属于热力系列模组 - * @param namespace 物品的命名空间 - * @return 如果是热力系列命名空间则返回true,否则返回false - */ - private static boolean isThermalNamespace(String namespace) { - // 热力系列模组的命名空间列表 - return namespace.equals("thermal") || - namespace.equals("thermal_cultivation") || - namespace.equals("thermal_expansion") || - namespace.equals("thermal_foundation") || - namespace.equals("thermal_innovation") || - namespace.equals("thermal_locomotion"); - } - - /** - * 检查物品是否为热力系列模组的机器 - * @param registryName 物品的注册表名称 - * @return 如果是热力系列机器则返回true,否则返回false - */ - private static boolean isThermalMachine(ResourceLocation registryName) { - // 检查是否为热力系列机器,这里检查注册表路径中的关键字 - String path = registryName.getPath(); - return path.contains("machine") || - path.contains("generator") || - path.contains("furnace") || - path.contains("dynamo") || - path.contains("engine") || - path.contains("factory") || - path.contains("processor") || - path.contains("centrifuge") || - path.contains("smelter") || - path.contains("crucible") || - path.contains("compressor") || - path.contains("sawmill") || - path.contains("pulverizer") || - path.contains("refinery"); - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/utils/NumberFormatUtil.java b/src/main/java/com/sorrowmist/useless/utils/NumberFormatUtil.java deleted file mode 100644 index 09a292fc..00000000 --- a/src/main/java/com/sorrowmist/useless/utils/NumberFormatUtil.java +++ /dev/null @@ -1,106 +0,0 @@ -// NumberFormatUtil.java -package com.sorrowmist.useless.utils; - -public class NumberFormatUtil { - - /** - * 格式化物品数量显示(带小数简写) - * 例:1200 → 1.2K,2500000 → 2.5M,1e12 → 1T - * 优化:整数部分为三位数时不显示小数,小数部分为0时不显示小数 - */ - public static String formatItemCount(long number) { - if (number >= 1_000_000_000_000_000_000L) { - double value = number / 1_000_000_000_000_000_000.0; - return formatNumberWithCondition(value, "E"); - } else if (number >= 1_000_000_000_000_000L) { - double value = number / 1_000_000_000_000_000.0; - return formatNumberWithCondition(value, "P"); - } else if (number >= 1_000_000_000_000L) { - double value = number / 1_000_000_000_000.0; - return formatNumberWithCondition(value, "T"); - } else if (number >= 1_000_000_000L) { - double value = number / 1_000_000_000.0; - return formatNumberWithCondition(value, "G"); - } else if (number >= 1_000_000L) { - double value = number / 1_000_000.0; - return formatNumberWithCondition(value, "M"); - } else if (number >= 1_000L) { - double value = number / 1_000.0; - return formatNumberWithCondition(value, "K"); - } else { - return String.valueOf(number); - } - } - - /** - * 格式化流体数量显示 (1000mB = 1B, 1000B = 1KB) - * 优化:整数部分为三位数时不显示小数,小数部分为0时不显示小数 - */ - public static String formatFluidAmount(long amount) { - if (amount >= 1_000_000_000_000_000_000L) { - double value = amount / 1_000_000_000_000_000_000.0; - return formatNumberWithCondition(value, "PB"); - } else if (amount >= 1_000_000_000_000_000L) { - double value = amount / 1_000_000_000_000_000.0; - return formatNumberWithCondition(value, "TB"); - } else if (amount >= 1_000_000_000_000L) { - double value = amount / 1_000_000_000_000.0; - return formatNumberWithCondition(value, "GB"); - } else if (amount >= 1_000_000_000L) { - double value = amount / 1_000_000_000.0; - return formatNumberWithCondition(value, "MB"); - } else if (amount >= 1_000_000L) { - double value = amount / 1_000_000.0; - return formatNumberWithCondition(value, "KB"); - } else if (amount >= 1_000L) { - double value = amount / 1_000.0; - return formatNumberWithCondition(value, "B"); - } else { - return amount + "mB"; - } - } - - /** - * 格式化能量显示(FE) - * 优化:整数部分为三位数时不显示小数,小数部分为0时不显示小数 - */ - public static String formatEnergy(long energy) { - if (energy >= 1_000_000_000_000_000_000L) { - double value = energy / 1_000_000_000_000_000_000.0; - return formatNumberWithCondition(value, "EFE"); - } else if (energy >= 1_000_000_000_000_000L) { - double value = energy / 1_000_000_000_000_000.0; - return formatNumberWithCondition(value, "PFE"); - } else if (energy >= 1_000_000_000_000L) { - double value = energy / 1_000_000_000_000.0; - return formatNumberWithCondition(value, "TFE"); - } else if (energy >= 1_000_000_000L) { - double value = energy / 1_000_000_000.0; - return formatNumberWithCondition(value, "GFE"); - } else if (energy >= 1_000_000L) { - double value = energy / 1_000_000.0; - return formatNumberWithCondition(value, "MFE"); - } else if (energy >= 1_000L) { - double value = energy / 1_000.0; - return formatNumberWithCondition(value, "KFE"); - } else { - return energy + " FE"; - } - } - - /** - * 根据条件格式化数字:整数部分为三位数时不显示小数,小数部分为0时不显示小数 - */ - private static String formatNumberWithCondition(double value, String unit) { - int intPart = (int) value; - double decimalPart = value - intPart; - - // 如果整数部分 >= 100(三位数),或者小数部分为0,则只显示整数 - if (intPart >= 100 || Math.abs(decimalPart) < 0.0001) { - return intPart + unit; - } else { - // 显示一位小数 - return String.format("%.1f%s", value, unit); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/utils/ProtectedPlayerQueries.java b/src/main/java/com/sorrowmist/useless/utils/ProtectedPlayerQueries.java new file mode 100644 index 00000000..29d8e12f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/utils/ProtectedPlayerQueries.java @@ -0,0 +1,35 @@ +package com.sorrowmist.useless.utils; + +import com.sorrowmist.useless.event.EventHandler; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.EntityGetter; +import java.util.ArrayList; +import java.util.List; + +/** Static helpers must live outside interface mixins on Forge's Mixin 0.8.5. */ +public final class ProtectedPlayerQueries { + private ProtectedPlayerQueries() {} + public static List visiblePlayers(EntityGetter getter) { + List players = getter.players(); + if (!EventHandler.hasAnyBeefAdvancedStealthPlayers()) { + return players; + } + + List filtered = null; + for (int i = 0; i < players.size(); i++) { + Player player = players.get(i); + if (EventHandler.hasBeefAdvancedStealthItem(player)) { + if (filtered == null) { + filtered = new ArrayList<>(players.size() - 1); + filtered.addAll(players.subList(0, i)); + } + continue; + } + + if (filtered != null) { + filtered.add(player); + } + } + return filtered == null ? players : filtered; + } +} diff --git a/src/main/java/com/sorrowmist/useless/utils/RateLimitedLogger.java b/src/main/java/com/sorrowmist/useless/utils/RateLimitedLogger.java new file mode 100644 index 00000000..b8b13f8a --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/utils/RateLimitedLogger.java @@ -0,0 +1,63 @@ +package com.sorrowmist.useless.utils; + +import org.slf4j.Logger; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.LongSupplier; + +/** Keeps repeated failures in recipe scans and machine ticks from flooding the log. */ +public final class RateLimitedLogger { + private static final Gate GATE = new Gate(60_000_000_000L, 256, System::nanoTime); + + private RateLimitedLogger() {} + + public static void warn(Logger logger, String message, Object... arguments) { + if (logger.isWarnEnabled() && GATE.allow(logger.getName(), "WARN", message)) { + logger.warn(message, arguments); + } + } + + public static void error(Logger logger, String message, Object... arguments) { + if (logger.isErrorEnabled() && GATE.allow(logger.getName(), "ERROR", message)) { + logger.error(message, arguments); + } + } + + static final class Gate { + private record Key(String logger, String level, String message) {} + + private final long intervalNanos; + private final int maximumKeys; + private final LongSupplier clock; + private final Map lastMessages = new LinkedHashMap<>(); + + Gate(long intervalNanos, int maximumKeys, LongSupplier clock) { + if (intervalNanos <= 0 || maximumKeys <= 0) { + throw new IllegalArgumentException("Log limits must be positive"); + } + this.intervalNanos = intervalNanos; + this.maximumKeys = maximumKeys; + this.clock = clock; + } + + synchronized boolean allow(String logger, String level, String message) { + Key key = new Key(logger, level, message); + long now = clock.getAsLong(); + Long previous = lastMessages.get(key); + if (previous != null && now - previous < intervalNanos) { + return false; + } + if (previous == null && lastMessages.size() >= maximumKeys) { + // Keep existing windows intact: new categories cannot evict a hot failure + // and make it eligible to log again in the same interval. + lastMessages.entrySet().removeIf(entry -> now - entry.getValue() >= intervalNanos); + if (lastMessages.size() >= maximumKeys) { + return false; + } + } + lastMessages.put(key, now); + return true; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/utils/SlotRendererUtil.java b/src/main/java/com/sorrowmist/useless/utils/SlotRendererUtil.java deleted file mode 100644 index b7f71903..00000000 --- a/src/main/java/com/sorrowmist/useless/utils/SlotRendererUtil.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.sorrowmist.useless.utils; - -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.Font; -import net.minecraft.client.gui.GuiGraphics; - -public class SlotRendererUtil { - - - public static void renderFluidAmount(GuiGraphics guiGraphics, int amount, int x, int y, int width, int height) { - String text = NumberFormatUtil.formatFluidAmount(amount); // 使用完整单位 - Font font = Minecraft.getInstance().font; - - guiGraphics.pose().pushPose(); - float scale = 0.5f; // 缩小字体以适应完整单位 - guiGraphics.pose().translate(0, 0, 200.0F); - guiGraphics.pose().scale(scale, scale, 1.0F); - - int textX = Math.round((x + width - font.width(text) * scale) / scale); - int textY = Math.round((y + height - 8 * scale) / scale); - - guiGraphics.drawString(font, text, textX, textY, 0xFFFFFF, true); - guiGraphics.pose().popPose(); - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/utils/TemperatureColors.java b/src/main/java/com/sorrowmist/useless/utils/TemperatureColors.java new file mode 100644 index 00000000..3976f72e --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/utils/TemperatureColors.java @@ -0,0 +1,79 @@ +package com.sorrowmist.useless.utils; + +import net.minecraft.util.Mth; + +/** + * 温度 → 颜色 的映射,移植自气动工艺「压缩铁块」的着色公式 + * ({@code me.desht.pneumaticcraft.common.heat.HeatUtil#getColourForTemperature})。 + * + *

输入为开尔文,输出 {@code 0xAARRGGBB}。分段语义:

+ *
    + *
  • {@code < 273K}:青 → 蓝(越冷越蓝)
  • + *
  • {@code 273~323K}:白色(室温,无着色)
  • + *
  • {@code 323~873K}:红 → 橙
  • + *
  • {@code 873~2273K}:橙 → 黄
  • + *
  • {@code > 2273K}:黄 → 白热(色相停在黄,转而降低饱和度提亮)
  • + *
+ * + *

高温端之所以不停在纯黄:金属受热发光的真实序列是「暗红 → 橙 → 黄 → 白热」, + * 只到黄色会缺少「炽热」的观感。故 2273 K 之后色相钳在 60(黄)不再推进,改为 + * 让饱和度从 1 衰减到 {@link #WHITE_HOT_SATURATION},到 10 000 K 呈明亮的暖白(白热)。

+ * + *

纯 Java 实现,不引用任何第三方模组类型,也不依赖 {@code java.awt}。

+ */ +public final class TemperatureColors { + + /** 「橙 → 黄」段的终点温度(K),也是白热段的起点。取气动压缩铁块的温度上限。 */ + public static final int WHITE_HOT_START_KELVIN = 2273; + + /** 白热段的终点温度(K),再高也只到这个饱和度。 */ + public static final int WHITE_HOT_END_KELVIN = 10_000; + + /** + * 白热段终点的饱和度。不取 0(纯白):完全褪色会变成贴图的灰白,反而失去「热」的暖调。 + * 0.30 ⇒ 终态约为 {@code (255, 255, 179)} 的暖白。 + */ + public static final float WHITE_HOT_SATURATION = 0.30f; + + private TemperatureColors() { + } + + /** 按温度(开尔文)取内板颜色,返回 {@code 0xAARRGGBB}。 */ + public static int forTemperature(long kelvin) { + int t = (int) Math.min(Integer.MAX_VALUE, Math.max(0L, kelvin)); + + int hue; + float saturation; + if (t < 273) { + hue = 180 + (300 - t) / 5; // 180 -> 240:青 -> 蓝 + saturation = (273 - t) / 273f; + } else if (t < 323) { + hue = 360; + saturation = 0f; // 室温:白 + } else if (t < 873) { + hue = (int) ((t - 323) / 550f * 30f); // 0 -> 30:红 -> 橙 + saturation = (t - 323) / 550f; + } else { + // 873K 以上分两段:先「橙 → 黄」(色相 30 → 60),到 WHITE_HOT_START_KELVIN 后 + // 色相停在黄不再推进,改为让饱和度衰减到 WHITE_HOT_SATURATION,呈白热观感。 + // + // ⛔ 色相必须钳制:气动原版这个式子不钳制,是因为它方块的温度被硬夹在 0~2273K, + // hue 最多走到 45(黄)。我们的调节器允许任意高温,不钳制的话 11000K 会算出 + // hue=138 → 变成绿色(越热越绿,语义完全反了)。 + float ramp = Math.min(1f, (t - 873f) / (WHITE_HOT_START_KELVIN - 873f)); + hue = 30 + (int) (ramp * 30f); // 30 -> 60:橙 -> 黄 + + if (t <= WHITE_HOT_START_KELVIN) { + saturation = 1f; + } else { + float hot = Math.min(1f, (t - (float) WHITE_HOT_START_KELVIN) + / (WHITE_HOT_END_KELVIN - WHITE_HOT_START_KELVIN)); + saturation = 1f - hot * (1f - WHITE_HOT_SATURATION); + } + } + + // 气动原版对 hue 做了 floor 归一化;Mth.hsvToRgb 不做,这里把 360 度兜到 1 以下。 + float normalizedHue = Math.min(0.9999f, hue / 360f); + return 0xFF000000 | Mth.hsvToRgb(normalizedHue, saturation, 1f); + } +} diff --git a/src/main/java/com/sorrowmist/useless/utils/ThermalConfigUtils.java b/src/main/java/com/sorrowmist/useless/utils/ThermalConfigUtils.java deleted file mode 100644 index 80d1ac4b..00000000 --- a/src/main/java/com/sorrowmist/useless/utils/ThermalConfigUtils.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.sorrowmist.useless.utils; - -/* - * This file is part of Thermal Parallel. - * - * Copyright (c) 2025 EtSH-C2H6S - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import com.electronwill.nightconfig.core.Config; -import com.electronwill.nightconfig.core.file.CommentedFileConfig; -import net.minecraftforge.fml.loading.FMLPaths; - -import java.io.File; - -public class ThermalConfigUtils { - - // Thermal Parallel 配置默认值 - public static boolean PARALLEL_INCREASE_ITEM_CAPACITY = true; - public static boolean PARALLEL_INCREASE_FLUID_CAPACITY = true; - public static boolean PARALLEL_INCREASE_ENERGY_CAPACITY = false; - public static boolean PARALLEL_INCREASE_ITEM_TRANSFER = true; - public static boolean PARALLEL_INCREASE_FLUID_TRANSFER = true; - public static boolean PARALLEL_INCREASE_ENERGY_TRANSFER = true; - public static boolean PARALLEL_INCREASE_ENERGY_CONSUMPTION = false; - public static boolean BASE_MOD_AFFECT_PARALLEL = false; - public static boolean ADD_EXTRA_PARALLEL_AUGMENTS_TO_TAB = false; - - static { - loadConfig(); - } - - public static void loadConfig() { - File file = FMLPaths.CONFIGDIR.get().resolve("useless_mod-common.toml").toFile(); - CommentedFileConfig config = CommentedFileConfig.builder(file).autosave().sync().build(); - config.load(); - - // 读取 Thermal Parallel 配置 - Config thermalSection = config.get("Thermal Parallel 设置"); - if (thermalSection != null) { - PARALLEL_INCREASE_ITEM_CAPACITY = thermalSection.getOrElse("并行增加物品容量", true); - PARALLEL_INCREASE_FLUID_CAPACITY = thermalSection.getOrElse("并行增加流体容量", true); - PARALLEL_INCREASE_ENERGY_CAPACITY = thermalSection.getOrElse("并行增加能量容量", false); - PARALLEL_INCREASE_ITEM_TRANSFER = thermalSection.getOrElse("并行增加物品传输", true); - PARALLEL_INCREASE_FLUID_TRANSFER = thermalSection.getOrElse("并行增加流体传输", true); - PARALLEL_INCREASE_ENERGY_TRANSFER = thermalSection.getOrElse("并行增加能量传输", true); - PARALLEL_INCREASE_ENERGY_CONSUMPTION = thermalSection.getOrElse("并行增加能耗", false); - BASE_MOD_AFFECT_PARALLEL = thermalSection.getOrElse("基础倍率影响并行", false); - ADD_EXTRA_PARALLEL_AUGMENTS_TO_TAB = thermalSection.getOrElse("添加额外并行增强到标签", false); - } else { - // 如果没有找到配置节,写入默认值 - config.set("Thermal Parallel 设置.并行增加物品容量", PARALLEL_INCREASE_ITEM_CAPACITY); - config.set("Thermal Parallel 设置.并行增加流体容量", PARALLEL_INCREASE_FLUID_CAPACITY); - config.set("Thermal Parallel 设置.并行增加能量容量", PARALLEL_INCREASE_ENERGY_CAPACITY); - config.set("Thermal Parallel 设置.并行增加物品传输", PARALLEL_INCREASE_ITEM_TRANSFER); - config.set("Thermal Parallel 设置.并行增加流体传输", PARALLEL_INCREASE_FLUID_TRANSFER); - config.set("Thermal Parallel 设置.并行增加能量传输", PARALLEL_INCREASE_ENERGY_TRANSFER); - config.set("Thermal Parallel 设置.并行增加能耗", PARALLEL_INCREASE_ENERGY_CONSUMPTION); - config.set("Thermal Parallel 设置.基础倍率影响并行", BASE_MOD_AFFECT_PARALLEL); - config.set("Thermal Parallel 设置.添加额外并行增强到标签", ADD_EXTRA_PARALLEL_AUGMENTS_TO_TAB); - config.save(); - } - - config.close(); - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/utils/ThermalDependencyHelper.java b/src/main/java/com/sorrowmist/useless/utils/ThermalDependencyHelper.java deleted file mode 100644 index 7e32a1e5..00000000 --- a/src/main/java/com/sorrowmist/useless/utils/ThermalDependencyHelper.java +++ /dev/null @@ -1,27 +0,0 @@ -// ThermalDependencyHelper.java -package com.sorrowmist.useless.utils; - -import net.minecraftforge.fml.ModList; - -public class ThermalDependencyHelper { - private static final boolean THERMAL_LOADED = ModList.get().isLoaded("thermal"); - private static final boolean THERMAL_FOUNDATION_LOADED = ModList.get().isLoaded("thermal_foundation"); - private static final boolean THERMAL_EXPANSION_LOADED = ModList.get().isLoaded("thermal_expansion"); - private static final boolean THERMAL_DYNAMICS_LOADED = ModList.get().isLoaded("thermal_dynamics"); - - public static boolean isThermalLoaded() { - return THERMAL_LOADED; - } - - public static boolean isThermalFoundationLoaded() { - return THERMAL_FOUNDATION_LOADED; - } - - public static boolean isThermalExpansionLoaded() { - return THERMAL_EXPANSION_LOADED; - } - - public static boolean isAnyThermalModLoaded() { - return THERMAL_LOADED || THERMAL_FOUNDATION_LOADED || THERMAL_EXPANSION_LOADED || THERMAL_DYNAMICS_LOADED; - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/utils/UComponentUtils.java b/src/main/java/com/sorrowmist/useless/utils/UComponentUtils.java new file mode 100644 index 00000000..daa61fd9 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/utils/UComponentUtils.java @@ -0,0 +1,107 @@ +package com.sorrowmist.useless.utils; + +import com.sorrowmist.useless.core.component.UComponents; +import net.minecraft.world.item.ItemStack; + +public class UComponentUtils { + /** + * 获取物品的增强连锁挖矿模式 + * @param stack 物品栈 + * @return 增强连锁挖矿是否启用 + */ + public static boolean isEnhancedChainMiningEnabled(ItemStack stack) { + return com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.EnhancedChainMiningComponent.get(), false); + } + + /** + * 获取物品的强制挖掘状态 + * @param stack 物品栈 + * @return 强制挖掘是否启用 + */ + public static boolean isForceMiningEnabled(ItemStack stack) { + return com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.ForceMiningComponent.get(), false); + } + + /** + * 获取物品的自动熔炼状态 + * + *

开启后,造化杖挖掘产生的掉落物会先按熔炉配方熔炼一次再入库。

+ * + * @param stack 物品栈 + * @return 自动熔炼是否启用 + */ + public static boolean isAutoSmeltEnabled(ItemStack stack) { + return com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.AutoSmeltComponent.get(), false); + } + + /** + * 获取物品的 Ex Deorum 钩子模式状态。 + * + *

开启后,破坏方块时按 Ex Deorum 的钩子配方在其原有掉落之外追加产物。

+ * + * @param stack 物品栈 + * @return Ex Deorum 钩子模式是否启用 + */ + public static boolean isExDeorumCrookEnabled(ItemStack stack) { + return com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.ExDeorumCrookComponent.get(), false); + } + + /** + * 获取物品的 Ex Deorum 锤子模式状态。 + * + *

开启后,破坏方块时按 Ex Deorum 的锤子配方改写掉落。

+ * + * @param stack 物品栈 + * @return Ex Deorum 锤子模式是否启用 + */ + public static boolean isExDeorumHammerEnabled(ItemStack stack) { + return com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.ExDeorumHammerComponent.get(), false); + } + + /** + * 获取物品的 Ex Deorum 压缩锤模式状态。 + * + *

压缩锤配方与普通锤配方是两套独立数据,因此分列为独立开关。

+ * + * @param stack 物品栈 + * @return Ex Deorum 压缩锤模式是否启用 + */ + public static boolean isExDeorumCompressedHammerEnabled(ItemStack stack) { + return com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.ExDeorumCompressedHammerComponent.get(), false); + } + + /** + * 获取物品的AE存储优先状态 + * @param stack 物品栈 + * @return AE存储优先是否启用 + */ + public static boolean isAEStoragePriorityEnabled(ItemStack stack) { + return com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.AEStoragePriorityComponent.get(), false); + } + + /** + * 获取物品的「范围磁力」状态 + * + *

默认开启:没有显式写入组件时按启用处理,与物品属性初始化保持一致。

+ * + * @param stack 物品栈 + * @return 范围磁力是否启用 + */ + public static boolean isBeefMagnetEnabled(ItemStack stack) { + return com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.BeefMagnetEnabledComponent.get(), true); + } + + /** + * 判断本次产出是否需要「接管」——即是否要把掉落物收起来而不是留在原地走原版拾取。 + * + *

范围磁力是所有掉落物的总开关,AE 存储优先独立生效:只要两者任一开启, + * 造化杖产生的掉落物就需要经过 {@code MiningUtils.handleDrops} 处理 + * (AE 优先存入,塞不下的按磁力开关决定进背包还是落地)。

+ * + * @param stack 工具 + * @return 是否需要接管本次掉落 + */ + public static boolean shouldCollectDrops(ItemStack stack) { + return isBeefMagnetEnabled(stack) || isAEStoragePriorityEnabled(stack); + } +} diff --git a/src/main/java/com/sorrowmist/useless/utils/UselessItemUtils.java b/src/main/java/com/sorrowmist/useless/utils/UselessItemUtils.java new file mode 100644 index 00000000..eecf111c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/utils/UselessItemUtils.java @@ -0,0 +1,768 @@ +package com.sorrowmist.useless.utils; + +import com.sorrowmist.useless.api.enums.tool.EnchantMode; +import com.sorrowmist.useless.api.enums.tool.ToolTypeMode; +import com.sorrowmist.useless.content.items.EndlessBeafItem; +import com.sorrowmist.useless.core.component.UComponents; +import com.sorrowmist.useless.core.config.ConfigManager; +import com.sorrowmist.useless.utils.mining.MiningUtils; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.effect.MobEffect; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.ai.attributes.Attribute; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.SpawnEggItem; +import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.Level; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.event.entity.living.LivingDropsEvent; +import org.jetbrains.annotations.Nullable; + +import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +public class UselessItemUtils { + private static final String PRODUCTIVE_BEES_MOD_ID = "productivebees"; + private static final ResourceLocation COGNIZANT_DUST_ID = new ResourceLocation( + "mysticalagriculture", "cognizant_dust"); + + // 写进刷怪蛋前必须清除的字段,分四类: + // ① 位置/运动/朝向 —— 会覆盖放置点; + // ② 死亡瞬间状态(Health=0 会让生物一召唤出来就死); + // ③ 每只生物各不相同的运行时状态 —— 不清掉的话,同种生物的蛋永远无法堆叠; + // ④ MinecraftForge 自己的「生成来源」标记。 + // 注意:模组/脚本的数据容器(NeoForgeData / KubeJSPersistentData / neoforge:attachments)**不在这里整块丢** —— + // 里面可能装着整合包作者定义的「身份」。它们改由配置项按「具体键 / 命名空间」精确清理, + // 未知条目一律保留,避免误伤其它整合包(见 ConfigManager#getBeefCaptureStripKeys 等)。 + private static final List VOLATILE_ENTITY_KEYS = List.of( + "Pos", "Motion", "Rotation", "FallDistance", "Fire", "Air", "OnGround", "PortalCooldown", + "UUID", + "Health", "HurtTime", "HurtByTimestamp", "DeathTime", "AbsorptionAmount", + "Passengers", "Leash", + "Brain", "LeftHanded", "TicksFrozen", "HasVisualFire", "FallFlying", + "InLove", "LoveCause", "SleepingX", "SleepingY", "SleepingZ", "RaidId", + "forge:spawn_type", "neoforge:spawn_type"); + + // Mob.finalizeSpawn 每次生成都会给 FOLLOW_RANGE 随机加这个永久修饰符(每只都不一样),必须剔除 + private static final String RANDOM_SPAWN_BONUS_ID = "minecraft:random_spawn_bonus"; + + // MinecraftForge 数据附件的 NBT 键(Entity#saveWithoutId 写入的运行时数据) + private static final String ATTACHMENTS_KEY = "neoforge:attachments"; + + public static void applyEndlessBeafEffects(Player player) { + if (player == null) return; + + // 检查是否启用药水效果 + if (ConfigManager.shouldEnablePotionEffects()) { + List customEffects = ConfigManager.getCustomPotionEffects(); + + for (String effectConfig : customEffects) { + applyPotionEffectFromConfig(player, effectConfig); + } + } + } + + private static final int POTION_DURATION = 20000; + + /** + * 从配置条目解析并应用药水效果 + * 格式: "modid:effect_name,amplifier" + */ + private static void applyPotionEffectFromConfig(Player player, String effectConfig) { + try { + if (effectConfig == null) { + return; + } + + String[] parts = effectConfig.split(",", -1); + if (parts.length != 2) { + return; + } + + String effectId = parts[0].trim(); + int amplifier = Integer.parseInt(parts[1].trim()) - 1; + if (amplifier < 0) { + return; + } + + ResourceLocation location = ResourceLocation.tryParse(effectId); + if (location == null) { + return; + } + MobEffect effect = BuiltInRegistries.MOB_EFFECT.get(location); + + if (effect == null) { + return; + } + + MobEffectInstance currentEffect = player.getEffect(effect); + if (currentEffect == null || currentEffect.getDuration() < 200) { + player.addEffect(new MobEffectInstance(effect, POTION_DURATION, Math.max(0, amplifier), true, false, true)); + } + } catch (Exception e) { + // 静默处理配置解析错误,避免每tick输出日志 + } + } + + public static void onLivingDrops(LivingDropsEvent event, ItemStack stack, Player player) { + if (player == null) return; + + // 根据配置的概率判断是否触发 + int chance = ConfigManager.getFestiveDropChance(); + if (!(Math.random() * 100 < chance)) { + return; + } + + LivingEntity killedEntity = event.getEntity(); + Level level = killedEntity.level(); + + if (level.isClientSide()) return; + + sendFestiveMessage(player); + + Collection drops = event.getDrops(); + List remainingDrops = new ArrayList<>(); // 保留原样掉落的(装备与容器类) + List amplifiedDrops = new ArrayList<>(); // 放大后的战利品 + + for (ItemEntity itemEntity : drops) { + ItemStack dropStack = itemEntity.getItem(); + + if (isFestiveAmplifiable(dropStack)) { + amplifiedDrops.addAll(amplifyFestiveDrop(dropStack)); + } else { + // 装备、工具、容器及其它不可堆叠物品保持原版掉落行为 + remainingDrops.add(itemEntity); + } + } + + // 清空原掉落物,重新添加需保持原版行为的装备与容器(放大后的战利品走统一掉落通道) + drops.clear(); + drops.addAll(remainingDrops); + + if (!amplifiedDrops.isEmpty()) { + // 走 handleDrops 而不是直接 placeItemBackInInventory: + // 这样战利品大爆发才会像普通掉落一样受「AE 存储优先」与「范围磁力」影响 + // (AE 优先且已绑定 → 进 AE;否则磁力开 → 进背包;都没开 → 落在尸体处走原版拾取)。 + MiningUtils.handleDrops(player, amplifiedDrops, stack, killedEntity.position()); + } + } + + /** 战利品大爆发的数量放大倍数。 */ + private static final int FESTIVE_MULTIPLIER = 20; + + /** 单个掉落物放大后允许产生的堆叠数上限,防止极端数量生成过多实体。 */ + private static final int FESTIVE_MAX_STACKS = 16; + + /** + * 判定某个掉落物是否参与战利品大爆发的数量放大。 + * + *

旧实现以 {@code ItemStack#isDamageableItem()} 作为「是否属于装备」的判据, + * 该判据与物品实际类别并不对应。其实现为 + * {@code has(MAX_DAMAGE) && !has(UNBREAKABLE) && has(DAMAGE)},对以下三类掉落物均返回 + * {@code false},使其全部落入放大分支:

+ *
    + *
  • 以 {@code UNBREAKABLE} 组件表达不可损坏的装备与工具(该组件使判定直接为假);
  • + *
  • 本就不含耐久组件的模组饰品、武器与工具;
  • + *
  • 潜影盒、收纳袋等容器,其 {@code maxStackSize} 为 1,同样不含耐久组件。
  • + *
+ * + *

其中容器被放大时,{@code CONTAINER} / {@code BUNDLE_CONTENTS} 组件随物品副本一并复制, + * 内容物因此同步倍增。判定改为「能否自然堆叠」后,装备、工具、护甲与容器一律保持原版 + * 掉落行为;容器组件另作显式排除,避免堆叠上限大于 1 的模组容器绕过该判定。

+ * + * @param stack 待判定的掉落物 + * @return 该掉落物是否应被放大 + */ + private static boolean isFestiveAmplifiable(ItemStack stack) { + if (stack.isEmpty() || stack.getMaxStackSize() <= 1) { + return false; + } + // 容器即便可堆叠也不放大:放大将连同内容物一并复制 + return !stack.getCapability(net.minecraftforge.common.capabilities.ForgeCapabilities.ITEM_HANDLER).isPresent() + && !stack.getCapability(net.minecraftforge.common.capabilities.ForgeCapabilities.FLUID_HANDLER_ITEM).isPresent() + && stack.getTagElement("BlockEntityTag") == null + && (stack.getTag() == null || !stack.getTag().contains("Items", Tag.TAG_LIST)); + } + + /** + * 放大单个掉落物的数量,并按物品自身的堆叠上限拆分。 + * + *

此前的实现直接写入 {@code count * 20},未受堆叠上限约束,会构造出数量超过上限的 + * 非法堆叠。此处改为放大后按上限拆分,并限制堆叠总数,避免极端掉落产生大量实体。

+ * + * @param dropStack 原始掉落物 + * @return 拆分后的掉落物列表,至少含一项 + */ + private static List amplifyFestiveDrop(ItemStack dropStack) { + int maxStackSize = dropStack.getMaxStackSize(); + int total = dropStack.getCount() * FESTIVE_MULTIPLIER; + int maxTotal = maxStackSize * FESTIVE_MAX_STACKS; + if (total > maxTotal) { + total = maxTotal; + } + + List amplified = new ArrayList<>(); + int remaining = total; + while (remaining > 0) { + int size = Math.min(remaining, maxStackSize); + ItemStack copy = dropStack.copy(); + copy.setCount(size); + amplified.add(copy); + remaining -= size; + } + return amplified; + } + + public static void tryAddCognizantDustDrop(LivingDropsEvent event, ItemStack stack) { + if (!(stack.getItem() instanceof EndlessBeafItem) + || !com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.BeefMysticalAgricultureEnabledComponent.get(), false)) { + return; + } + + LivingEntity killedEntity = event.getEntity(); + Level level = killedEntity.level(); + if (level.isClientSide() || !level.getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) { + return; + } + + int count = killedEntity.getType() == EntityType.WITHER ? 4 + : killedEntity.getType() == EntityType.ENDER_DRAGON ? 6 : 0; + if (count == 0) { + return; + } + + Item cognizantDust = BuiltInRegistries.ITEM.get(COGNIZANT_DUST_ID); + if (cognizantDust != Items.AIR) { + event.getDrops().add(new ItemEntity( + level, + killedEntity.getX(), + killedEntity.getY(), + killedEntity.getZ(), + new ItemStack(cognizantDust, count) + )); + } + } + + public static void tryAddBeheadingDrop(LivingDropsEvent event, ItemStack stack) { + if (!(stack.getItem() instanceof EndlessBeafItem) + || !com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.BeefBeheadingEnabledComponent.get(), false)) { + return; + } + + LivingEntity killedEntity = event.getEntity(); + Level level = killedEntity.level(); + if (event.isCanceled() || level.isClientSide() + || !level.getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) { + return; + } + + ItemStack head = beheadingDrop(killedEntity); + if (!head.isEmpty()) { + event.getDrops().add(new ItemEntity( + level, + killedEntity.getX(), + killedEntity.getY(), + killedEntity.getZ(), + head + )); + } + } + + private static ItemStack beheadingDrop(LivingEntity entity) { + if (entity instanceof Player player) { + ItemStack head = new ItemStack(Items.PLAYER_HEAD); + head.getOrCreateTag().put("SkullOwner", net.minecraft.nbt.NbtUtils.writeGameProfile( + new CompoundTag(), player.getGameProfile())); + return head; + } + + EntityType type = entity.getType(); + if (type == EntityType.SKELETON || type == EntityType.STRAY) { + return new ItemStack(Items.SKELETON_SKULL); + } + if (type == EntityType.WITHER_SKELETON) { + return new ItemStack(Items.WITHER_SKELETON_SKULL); + } + if (type == EntityType.ZOMBIE || type == EntityType.ZOMBIE_VILLAGER + || type == EntityType.HUSK || type == EntityType.DROWNED) { + return new ItemStack(Items.ZOMBIE_HEAD); + } + if (type == EntityType.CREEPER) { + return new ItemStack(Items.CREEPER_HEAD); + } + if (type == EntityType.PIGLIN || type == EntityType.PIGLIN_BRUTE) { + return new ItemStack(Items.PIGLIN_HEAD); + } + if (type == EntityType.ENDER_DRAGON) { + return new ItemStack(Items.DRAGON_HEAD); + } + return ItemStack.EMPTY; + } + + public static void tryCaptureSpawnEgg(LivingEntity killedEntity, ItemStack stack, Player player) { + if (killedEntity.level().isClientSide() + || !(stack.getItem() instanceof EndlessBeafItem) + || !com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, UComponents.BeefCaptureEnabledComponent.get(), false)) { + return; + } + + ItemStack spawnEggStack = null; + if (ModList.get().isLoaded(PRODUCTIVE_BEES_MOD_ID)) { + spawnEggStack = com.sorrowmist.useless.compat.forge1201.OptionalIntegration.call( + PRODUCTIVE_BEES_MOD_ID, "com.sorrowmist.useless.compat.productivebees.ProductiveBeesCaptureCompat", + "tryCreateSpawnEgg", ItemStack.class, null, new Class[]{LivingEntity.class}, killedEntity); + } + + if (spawnEggStack == null) { + boolean carrier = false; + SpawnEggItem spawnEgg = SpawnEggItem.byId(killedEntity.getType()); + if (spawnEgg == null) { + // 无对应原版刷怪蛋的模组生物:猪刷怪蛋当外壳, + // 靠 ENTITY_DATA 的 id 让 SpawnEggItem#getType 还原成真正的实体 + spawnEgg = SpawnEggItem.byId(EntityType.PIG); + carrier = true; + } + if (spawnEgg == null) { + return; + } + spawnEggStack = new ItemStack(spawnEgg); + // 精准模式才写入完整 NBT;时运模式只写入生物类型(猪外壳依然靠它还原实体) + boolean precise = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(stack, + UComponents.EnchantModeComponent.get(), EnchantMode.FORTUNE) == EnchantMode.SILK_TOUCH; + applyCapturedEntityData(spawnEggStack, killedEntity, carrier, precise); + } + + if (spawnEggStack.isEmpty()) { + return; + } + + // 只按「范围磁力」决定去留,刻意绕过「AE 存储优先」: + // 刷怪蛋是玩家想立刻拿在手上的东西,不该被吸进 AE 网络。 + MiningUtils.handleDrops(player, java.util.List.of(spawnEggStack), stack, + killedEntity.position(), false); + } + + /** + * 把生物的 NBT 写进刷怪蛋。分两种模式(由造化杖当前的附魔模式决定): + * + *
    + *
  • 精准模式({@code SILK_TOUCH}):写入生物完整身份 NBT —— 保留自定义名、 + * 装备、Apotheosis 战利品表等,捕捉到的生物与野生个体完全一致。 + * 先清掉「死亡瞬间状态」与「每只生物各不相同的运行时状态」,这样同种生物产出的蛋仍可堆叠。
  • + *
  • 时运模式({@code FORTUNE}):只写「生物类型」,不带任何身份 NBT —— + * 有原版刷怪蛋的生物直接产出纯净原版蛋;没有原版蛋的模组生物仍用猪刷怪蛋当外壳, + * 只补一个 {@code id} 让 {@code SpawnEggItem#getType} 还原成真正的实体。 + * 此模式下同类生物的蛋 NBT 完全相同,必定可堆叠。
  • + *
+ */ + private static void applyCapturedEntityData(ItemStack eggStack, LivingEntity entity, boolean carrier, boolean precise) { + CompoundTag tag = new CompoundTag(); + if (precise) { + // 精准模式:完整身份 NBT + tag = entity.saveWithoutId(new CompoundTag()); + for (String key : VOLATILE_ENTITY_KEYS) { + tag.remove(key); + } + canonicalizeAttributes(tag, entity.getType()); + stripConfiguredKeys(tag); + stripAttachments(tag); + } + // 决定 SpawnEggItem 生成哪种实体:猪外壳必须靠它还原,精准模式也用它对齐 + if (precise || carrier) { + ResourceLocation id = BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()); + if (id != null) { + tag.putString("id", id.toString()); + } + } + if (!tag.isEmpty()) { + eggStack.getOrCreateTag().put("EntityTag", tag); + } + + // 显示名:猪外壳绝不允许露出「猪刷怪蛋」。 + Component name = null; + if (precise) { + // 精准模式: + // ① 有自定义名 → 用自定义名(普通蛋 / 猪外壳都适用) + // ② 否则若是猪外壳(无原版蛋的模组生物)→ 用该生物类型的本地化名 + // ③ 否则(有原版蛋且无自定义名)→ 保留原版蛋名(如「僵尸刷怪蛋」) + name = entity.getCustomName(); + if (name == null && carrier) { + name = Component.translatable(entity.getType().getDescriptionId()); + } + } else if (carrier) { + // 时运模式:只有猪外壳需要显名(否则会露出「猪刷怪蛋」); + // 有原版蛋的生物保持纯净原版蛋名,且同类蛋 NBT 完全一致、必定可堆叠。 + name = Component.translatable(entity.getType().getDescriptionId()); + } + if (name != null) { + eggStack.setHoverName(name.copy().withStyle(style -> style.withItalic(false))); + } + } + + /** + * 把 {@code attributes} 规范化成「只保留真正偏离默认值的属性」,并按 id 排序。 + *

关键:{@code AttributeMap} 的属性是**惰性**创建的({@code computeIfAbsent}), + * {@code save()} 只写出「被访问过」的属性 —— 一只活过的生物访问过的属性集合与顺序, + * 跟另一只同种生物完全不同。不规范化的话,两头原版牛的蛋 NBT 也永远不一样、无法堆叠。 + *

顺带剔除 {@code Mob.finalizeSpawn} 每次随机加在 FOLLOW_RANGE 上的 spawn bonus 修饰符。 + */ + @SuppressWarnings("unchecked") + private static void canonicalizeAttributes(CompoundTag tag, EntityType type) { + ListTag attributes = tag.getList("Attributes", Tag.TAG_COMPOUND); + if (attributes.isEmpty()) { + return; + } + AttributeSupplier defaults = DefaultAttributes.getSupplier((EntityType) type); + ListTag kept = new ListTag(); + for (int i = 0; i < attributes.size(); i++) { + CompoundTag attribute = attributes.getCompound(i); + ResourceLocation attributeId = ResourceLocation.tryParse(attribute.getString("Name")); + Attribute attributeType = attributeId == null + ? null + : BuiltInRegistries.ATTRIBUTE.get(attributeId); + if (attributeType == null) { + kept.add(attribute.copy()); + continue; + } + + ListTag modifiers = attribute.getList("Modifiers", Tag.TAG_COMPOUND); + ListTag keptModifiers = new ListTag(); + for (int j = 0; j < modifiers.size(); j++) { + CompoundTag modifier = modifiers.getCompound(j); + if (!"Random spawn bonus".equals(modifier.getString("Name"))) { + keptModifiers.add(modifier); + } + } + + // 默认数值 + 无有效修饰符 ⇒ 不是这只生物的身份,直接丢弃 + // (用 Double.compare 而非 ==:NaN 也是「默认值」的一种,== 对 NaN 恒为 false) + boolean isDefault = defaults != null && defaults.hasAttribute(attributeType) + && keptModifiers.isEmpty() + && Double.compare(attribute.getDouble("Base"), defaults.getBaseValue(attributeType)) == 0; + if (isDefault) { + continue; + } + + CompoundTag copy = attribute.copy(); + if (keptModifiers.isEmpty()) { + copy.remove("Modifiers"); + } else { + copy.put("Modifiers", keptModifiers); + } + kept.add(copy); + } + + if (kept.isEmpty()) { + tag.remove("Attributes"); + return; + } + kept.sort(Comparator.comparing((Tag entry) -> ((CompoundTag) entry).getString("Name"))); + tag.put("Attributes", kept); + } + + /** + * 按配置精确丢弃指定的 NBT 键:{@code 容器.子键}(不含点则视为顶层键)。 + *

模组/脚本的数据容器({@code NeoForgeData} / {@code KubeJSPersistentData})**不整块丢** —— + * 里面可能装着整合包作者定义的「身份」,整块丢会误伤其它整合包。 + * 这里只清配置里点名的、已知是「每只各不相同」的运行时数值(存活计时 / 落地坐标 / 衰老计时…)。 + */ + private static void stripConfiguredKeys(CompoundTag tag) { + for (String path : ConfigManager.getBeefCaptureStripKeys()) { + if (path == null || path.isEmpty()) { + continue; + } + int dot = path.indexOf('.'); + if (dot < 0) { + tag.remove(path); + continue; + } + String container = path.substring(0, dot); + String child = path.substring(dot + 1); + if (child.isEmpty() || !tag.contains(container, Tag.TAG_COMPOUND)) { + continue; + } + CompoundTag nested = tag.getCompound(container); + nested.remove(child); + if (nested.isEmpty()) { + tag.remove(container); + } + } + } + + /** + * 按配置丢弃 {@code neoforge:attachments} 里指定命名空间的附件(体温 {@code cold_sweat}、 + * 灵魂 {@code malum}、火焰 {@code blueflame} …)—— 这些每只生物都不一样,留着蛋就永远不堆叠。 + *

只清配置里点名的命名空间;其余(含 Apotheosis 的战利品表、以及所有未知模组)一律保留, + * 避免误伤其它整合包把「身份」存在附件里。 + */ + private static void stripAttachments(CompoundTag tag) { + CompoundTag attachments = tag.getCompound(ATTACHMENTS_KEY); + if (attachments.isEmpty()) { + return; + } + Set volatileNamespaces = + Set.copyOf(ConfigManager.getBeefCaptureStripAttachmentNamespaces()); + for (String key : new ArrayList<>(attachments.getAllKeys())) { + int colon = key.indexOf(':'); + String namespace = colon < 0 ? key : key.substring(0, colon); + if (volatileNamespaces.contains(namespace)) { + attachments.remove(key); + } + } + if (attachments.isEmpty()) { + tag.remove(ATTACHMENTS_KEY); + } + } + + // 显示触发提示 + private static void sendFestiveMessage(Player player) { + if (player != null) { + player.displayClientMessage( + Component.translatable("gui.useless_mod.festive_triggered"), + true + ); + } + } + + /** + * 检查物品是否是目标工具(牛排或特定模式的omnitools扳手) + */ + private static boolean isTargetTool(ItemStack itemStack) { + if (itemStack.isEmpty()) { + return false; + } + + // 检查是否是永恒牛排工具 + if (itemStack.getItem() instanceof EndlessBeafItem) { + return true; + } + + // 检查是否是omnitools扳手且处于正确模式 + ResourceLocation itemId = BuiltInRegistries.ITEM.getKey(itemStack.getItem()); + return itemId.getNamespace().equals("omnitools") + && com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(itemStack, UComponents.CurrentToolTypeComponent) == ToolTypeMode.OMNITOOL_MODE; + } + + /** + * 从玩家的主手和副手中查找目标工具 + * 返回包含目标物品和对应手的Optional + */ + public static Optional> findTargetToolInHands(Player player) { + if (player == null) { + return Optional.empty(); + } + + ItemStack mainHandItem = player.getMainHandItem(); + ItemStack offHandItem = player.getOffhandItem(); + + // 检查主手 + if (isTargetTool(mainHandItem)) { + return Optional.of(new SimpleImmutableEntry<>(mainHandItem, InteractionHand.MAIN_HAND)); + } + + // 检查副手 + if (isTargetTool(offHandItem)) { + return Optional.of(new SimpleImmutableEntry<>(offHandItem, InteractionHand.OFF_HAND)); + } + + return Optional.empty(); + } + + public static boolean hasTargetToolInInventory(Player player) { + if (player == null || player.getInventory() == null) { + return false; + } + + return isTargetTool(player.getMainHandItem()) + || isTargetTool(player.getOffhandItem()) + || player.getInventory().items.stream().anyMatch(UselessItemUtils::isTargetTool); + } + + /** + * 玩家身上是否带着指定物品(主手 / 副手 / 主背包)。 + *

+ * 注意 {@code player.getInventory().items} 只有主背包那 36 格,不含副手 + * (副手在 {@code getInventory().offhand} 里)。凡是「玩家是否携带」的判定都必须走这里, + * 否则造化杖放进副手就会失效。 + */ + public static boolean hasItemInInventory(Player player, Item item) { + if (player == null || player.getInventory() == null || item == null) { + return false; + } + + return player.getMainHandItem().is(item) + || player.getOffhandItem().is(item) + || player.getInventory().items.stream().anyMatch(stack -> stack.is(item)); + } + + public static boolean hasInvulnerabilityEnabledTargetToolInInventory(Player player) { + if (player == null || player.getInventory() == null) { + return false; + } + + return isInvulnerabilityEnabledTargetTool(player.getMainHandItem()) + || isInvulnerabilityEnabledTargetTool(player.getOffhandItem()) + || player.getInventory().items.stream().anyMatch(UselessItemUtils::isInvulnerabilityEnabledTargetTool); + } + + public static boolean hasAdvancedStealthEnabledTargetToolInInventory(Player player) { + if (player == null || player.getInventory() == null) { + return false; + } + + return isAdvancedStealthEnabledTargetTool(player.getMainHandItem()) + || isAdvancedStealthEnabledTargetTool(player.getOffhandItem()) + || player.getInventory().items.stream().anyMatch(UselessItemUtils::isAdvancedStealthEnabledTargetTool); + } + + /** + * 一趟「主手 → 副手 → 快捷栏 / 主背包」扫描的结果快照。 + * + *

造化杖相关的每-tick 判定(飞行 / 无敌保护 / 高级潜行 / 杀戮光环)原本各走一趟背包, + * 而 {@link #isTargetTool} 对每个非空物品都要做一次 {@code BuiltInRegistries.ITEM.getKey(...)} + * 注册表反查。合并成一趟后,四个结果一次算出,布尔语义与原先各自的 + * {@code hasXxxTargetToolInInventory} 逐个等价(都是存在性判定,与遍历顺序无关)。

+ * + * @param killAuraStaff 身上第一把「开着杀戮光环」的造化杖,没有则为 {@code null} + */ + public record StaffScan(boolean anyStaff, + boolean invulnerabilityEnabled, + boolean advancedStealthEnabled, + @Nullable ItemStack killAuraStaff) { + public boolean hasKillAura() { + return killAuraStaff != null; + } + } + + /** + * 一次性扫描玩家身上的造化杖相关状态,供 {@code EventHandler#onPlayerTick} 的三个消费者共用, + * 避免每 tick 把背包走三遍。 + * + *

遍历顺序与各个 {@code hasXxxTargetToolInInventory} 保持一致:主手 → 副手 → + * 快捷栏 / 主背包({@code items[0..8]} 是快捷栏、{@code [9..35]} 是主背包,当前手持格也在其中)。

+ */ + public static StaffScan scanStaff(Player player) { + if (player == null || player.getInventory() == null) { + return new StaffScan(false, false, false, null); + } + + boolean anyStaff = false; + boolean invulnerabilityEnabled = false; + boolean advancedStealthEnabled = false; + ItemStack killAuraStaff = null; + + var items = player.getInventory().items; + // i == -2 主手、i == -1 副手、其余为主背包/快捷栏下标 + for (int i = -2, size = items.size(); i < size; i++) { + ItemStack stack = i == -2 ? player.getMainHandItem() + : i == -1 ? player.getOffhandItem() + : items.get(i); + if (stack.isEmpty() || !isTargetTool(stack)) { + continue; + } + anyStaff = true; + invulnerabilityEnabled |= isInvulnerabilityEnabledTargetTool(stack); + advancedStealthEnabled |= isAdvancedStealthEnabledTargetTool(stack); + if (killAuraStaff == null + && stack.getItem() instanceof EndlessBeafItem + && EndlessBeafItem.isKillAuraEnabled(stack)) { + killAuraStaff = stack; + } + } + + return new StaffScan(anyStaff, invulnerabilityEnabled, advancedStealthEnabled, killAuraStaff); + } + + /** + * 找出「杀戮光环」开关要作用的那把造化杖:优先返回已经开着光环的那把(用于关闭), + * 否则返回身上第一把造化杖(用于开启)。 + * + *

只认造化杖本体(omnitools 扳手模式不给光环)。开关只在按键 / 点击那一刻调用, + * 不在每-tick 热路径上,因此这里直接扫一趟即可,不做缓存。

+ * + * @return 找到的杖;身上没有造化杖时返回 {@link ItemStack#EMPTY} + */ + public static ItemStack findKillAuraToggleTarget(Player player) { + if (player == null || player.getInventory() == null) { + return ItemStack.EMPTY; + } + + ItemStack firstStaff = ItemStack.EMPTY; + var items = player.getInventory().items; + for (int i = -2, size = items.size(); i < size; i++) { + ItemStack stack = i == -2 ? player.getMainHandItem() + : i == -1 ? player.getOffhandItem() + : items.get(i); + if (stack.isEmpty() || !(stack.getItem() instanceof EndlessBeafItem)) { + continue; + } + if (EndlessBeafItem.isKillAuraEnabled(stack)) { + // 已经开着光环:优先作用在它身上,按一次就是关闭 + return stack; + } + if (firstStaff.isEmpty()) { + firstStaff = stack; + } + } + return firstStaff; + } + + public static boolean enableInvulnerabilityForAdvancedStealth(Player player) { + if (player == null || player.getInventory() == null) { + return false; + } + + boolean changed = enableInvulnerabilityForAdvancedStealth(player.getMainHandItem()); + changed |= enableInvulnerabilityForAdvancedStealth(player.getOffhandItem()); + for (ItemStack itemStack : player.getInventory().items) { + changed |= enableInvulnerabilityForAdvancedStealth(itemStack); + } + if (changed) { + player.getInventory().setChanged(); + player.containerMenu.broadcastChanges(); + } + return changed; + } + + private static boolean isInvulnerabilityEnabledTargetTool(ItemStack itemStack) { + if (!isTargetTool(itemStack)) { + return false; + } + + Boolean enabled = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(itemStack, UComponents.BeefInvulnerabilityEnabledComponent.get()); + return enabled != null ? enabled : itemStack.getItem() instanceof EndlessBeafItem; + } + + private static boolean isAdvancedStealthEnabledTargetTool(ItemStack itemStack) { + return isTargetTool(itemStack) + && com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(itemStack, UComponents.BeefAdvancedStealthEnabledComponent.get(), false); + } + + private static boolean enableInvulnerabilityForAdvancedStealth(ItemStack itemStack) { + if (!isAdvancedStealthEnabledTargetTool(itemStack) + || com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(itemStack, UComponents.BeefInvulnerabilityEnabledComponent.get(), false)) { + return false; + } + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.set(itemStack, UComponents.BeefInvulnerabilityEnabledComponent.get(), true); + return true; + } +} diff --git a/src/main/java/com/sorrowmist/useless/utils/mining/AutoSmeltHelper.java b/src/main/java/com/sorrowmist/useless/utils/mining/AutoSmeltHelper.java new file mode 100644 index 00000000..f69f518b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/utils/mining/AutoSmeltHelper.java @@ -0,0 +1,110 @@ +package com.sorrowmist.useless.utils.mining; + +import com.sorrowmist.useless.utils.UComponentUtils; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.AbstractCookingRecipe; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.level.Level; + +import java.util.ArrayList; +import java.util.List; + +/** + * 挖掘时的自动熔炼:将掉落物按熔炉配方再次烧炼。 + * + *

仅识别原版三类烹饪配方(熔炉 / 高炉 / 烟熏炉),与配方转换开关无关: + * 此处查询的是世界当前的 {@code RecipeManager},因此数据包与其它模组新增的 + * 熔炼配方均可生效。

+ * + *

查询顺序固定为「熔炉 → 高炉 → 烟熏炉」:同一输入可能同时命中多条配方 + * (例如生铁既可熔炉烧炼也可高炉烧炼),固定顺序可保证产物稳定,不随配方表 + * 迭代顺序变化。

+ */ +public final class AutoSmeltHelper { + /** 查询顺序固定,避免同一输入命中多条配方时产物漂移。 */ + private static final List> RECIPE_TYPES = List.of( + RecipeType.SMELTING, + RecipeType.BLASTING, + RecipeType.SMOKING); + + private AutoSmeltHelper() { + } + + /** + * 对整批掉落物执行一次熔炼转换。 + * + *

未开启自动熔炼、世界为空或整批均无匹配配方时,原样返回入参, + * 保证调用方拿到的列表语义与未开启时完全一致。

+ * + * @param level 世界(必须为服务端世界,客户端没有完整配方表) + * @param drops 掉落物列表 + * @param tool 工具(用于读取自动熔炼开关) + * @return 熔炼后的掉落物列表 + */ + public static List smeltDrops(Level level, List drops, ItemStack tool) { + if (level == null || level.isClientSide() || drops == null || drops.isEmpty()) { + return drops; + } + if (!UComponentUtils.isAutoSmeltEnabled(tool)) { + return drops; + } + + List smelted = new ArrayList<>(drops.size()); + for (ItemStack drop : drops) { + smelted.addAll(smeltOne(level, drop)); + } + return MiningUtils.mergeItemStacks(smelted); + } + + /** + * 熔炼单个堆叠:按整堆数量换算,产物数量随输入数量等比放大。 + * + *

无任何匹配的烹饪配方时原样返回(例如圆石、矿物块等无需烧炼的掉落物)。

+ */ + private static List smeltOne(Level level, ItemStack input) { + if (input.isEmpty()) { + return List.of(input); + } + + AbstractCookingRecipe recipe = findRecipe(level, input); + if (recipe == null) { + return List.of(input); + } + + // 以单个样本获取产物,避免将整堆作为输入(部分模组配方不读取 count,会吞掉数量) + ItemStack sample = input.copyWithCount(1); + ItemStack result = recipe.assemble(new SimpleContainer(sample), level.registryAccess()); + if (result.isEmpty()) { + return List.of(input); + } + + // Split rather than clamp: a 64-item input with a 2-item yield must produce all 128 items. + return com.sorrowmist.useless.compat.forge1201.ItemStackAmounts.multiply(result, input.getCount()); + } + + /** 按固定顺序返回首个命中的烹饪配方。 */ + private static AbstractCookingRecipe findRecipe(Level level, ItemStack input) { + SimpleContainer recipeInput = new SimpleContainer(input.copyWithCount(1)); + for (RecipeType type : RECIPE_TYPES) { + AbstractCookingRecipe recipe = findRecipe(level, type, recipeInput); + if (recipe != null) { + return recipe; + } + } + return null; + } + + private static T findRecipe( + Level level, RecipeType type, SimpleContainer input) { + try { + return com.sorrowmist.useless.compat.forge1201.recipe.RecipeManagerAccess.getRecipeFor(level.getRecipeManager(), type, input, level) + .map(RecipeHolder::value) + .orElse(null); + } catch (Throwable ignored) { + // 单个模组的配方在匹配阶段抛出异常时,不应影响整批掉落物的处理 + return null; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/utils/mining/ChainMiningStrategy.java b/src/main/java/com/sorrowmist/useless/utils/mining/ChainMiningStrategy.java index 1ea593b9..88424f56 100644 --- a/src/main/java/com/sorrowmist/useless/utils/mining/ChainMiningStrategy.java +++ b/src/main/java/com/sorrowmist/useless/utils/mining/ChainMiningStrategy.java @@ -1,85 +1,136 @@ package com.sorrowmist.useless.utils.mining; +import com.sorrowmist.useless.core.config.ChainEquivalence; +import com.sorrowmist.useless.core.config.ChainGroupManager; +import com.sorrowmist.useless.data.PlayerMiningData; +import com.sorrowmist.useless.utils.UComponentUtils; import net.minecraft.core.BlockPos; import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; -import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; import net.minecraftforge.event.level.BlockEvent; import java.util.ArrayList; import java.util.List; -/** - * 连锁挖掘策略 - * - 普通连锁:BFS相邻方块 - * - 增强连锁:扫描范围内所有相同方块 +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 C-H716 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ public class ChainMiningStrategy implements MiningStrategy { private final boolean enhanced; - public ChainMiningStrategy(boolean enhanced) { + protected ChainMiningStrategy(boolean enhanced) { this.enhanced = enhanced; } + protected boolean isForceMining(ItemStack hand) { + return false; + } + + protected boolean canUseCache(ItemStack hand, boolean forceMining) { + return !forceMining || UComponentUtils.isForceMiningEnabled(hand); + } + + protected String getResultTranslationKey() { + return this.enhanced + ? "gui.useless_mod.enhanced_chain_mining_result" + : "gui.useless_mod.chain_mining_result"; + } + @Override public void handleBreak(BlockEvent.BreakEvent event, ItemStack hand, Player player) { if (!(event.getLevel() instanceof ServerLevel level)) return; BlockPos pos = event.getPos(); BlockState originState = event.getState(); - Block originBlock = originState.getBlock(); - - boolean forceMining = MiningUtils.isForceMiningMode(hand); - - // 查找需要破坏的方块列表 + PlayerMiningData playerData = MiningDispatcher.getOrCreatePlayerData(player); + boolean forceMining = this.isForceMining(hand); List blocksToMine; - if (this.enhanced) { - blocksToMine = MiningUtils.findBlocksToMineEnhanced(pos, originState, level, hand, forceMining); + + if (this.canUseCache(hand, forceMining) + && playerData.getCachedPos() != null + && playerData.getCachedPos().equals(pos) + && playerData.hasCachedBlocks()) { + blocksToMine = playerData.getCachedBlocks(); } else { - blocksToMine = MiningUtils.findBlocksToMine(pos, originState, level, hand, forceMining); + blocksToMine = MiningUtils.scanBlocksToMine( + pos, originState, level, hand, forceMining, this.enhanced, + playerData.getShape(), MiningUtils.getTargetFace(player), player); } if (blocksToMine.isEmpty()) { + playerData.clearCache(); return; } - // 执行挖掘 List allDrops = new ArrayList<>(); int actualMinedCount = 0; + int totalExperience = 0; + // 破坏前二次校验必须与扫描阶段使用同一套判定,否则等价组扩出的方块会在此被全部跳过 + ChainEquivalence equivalence = ChainGroupManager.equivalenceFor(player, originState.getBlock()); for (BlockPos targetPos : blocksToMine) { BlockState currentState = level.getBlockState(targetPos); - if (currentState.getBlock() != originBlock) continue; - - List fallbackDrops = forceMining - ? MiningUtils.getForcedFallbackDrops(currentState, level, targetPos, hand) - : List.of(); - List drops = MiningUtils.destroyBlockAndCollectDrops(level, targetPos, currentState, player, hand); - if (forceMining && MiningUtils.hasNoValidDrops(drops) && !MiningUtils.hasNoValidDrops(fallbackDrops)) { - drops = fallbackDrops; + + if (!equivalence.matches(currentState)) { + continue; + } + + // 单格失败不得中断整批:模组的破坏回调抛出异常(数据能源的三位一体样板核心 + // 在状态未就绪时会抛出)或方块拒绝被移除时,仅跳过该格并继续处理其余目标。 + try { + MiningUtils.MiningResult result = forceMining + ? MiningUtils.forceMineBlock(level, targetPos, currentState, player, hand) + : MiningUtils.mineBlock(level, targetPos, currentState, player, hand); + if (result.mined()) { + // 逐格改写:Ex Deorum 的锤子与钩子配方均以方块为索引键, + // 连锁覆盖的不同方块可能命中不同配方,故不能汇总后再统一处理。 + allDrops.addAll(MiningUtils.applyExDeorumDrops( + level, currentState, result.drops(), hand, targetPos, player)); + totalExperience += result.experience(); + actualMinedCount++; + } + } catch (Throwable failure) { + MiningUtils.reportBlockBreakFailure(currentState, targetPos, failure); } - allDrops.addAll(drops); - actualMinedCount++; } - // 统一处理掉落物 if (!MiningUtils.hasNoValidDrops(allDrops)) { - MiningUtils.handleDrops(player, MiningUtils.mergeItemStacks(allDrops), hand); + // 汇总后单次处理:AE 只收到一次请求;磁力关闭时统一落在连锁原点。 + MiningUtils.handleDrops(player, MiningUtils.mergeItemStacks(allDrops), hand, Vec3.atCenterOf(pos)); } - // 经验处理(时运模式) - if (!MiningUtils.isSilkTouchMode(hand)) { - originBlock.popExperience(level, pos, originBlock.getExpDrop(originState, level, level.random, pos, 0, 0) * actualMinedCount); + if (totalExperience > 0) { + player.giveExperiencePoints(totalExperience); } - // 显示结果 if (actualMinedCount > 0) { - String key = this.enhanced ? "增强连锁挖掘完成:" : "连锁挖掘完成:"; - player.displayClientMessage(Component.literal(key + "已挖掘 " + actualMinedCount + " 个方块"), true); + player.displayClientMessage(Component.translatable( + this.getResultTranslationKey(), actualMinedCount), true); } event.setCanceled(true); + playerData.clearCache(); } } diff --git a/src/main/java/com/sorrowmist/useless/utils/mining/DefaultMiningStrategy.java b/src/main/java/com/sorrowmist/useless/utils/mining/DefaultMiningStrategy.java index 9e7a192f..5c64b76a 100644 --- a/src/main/java/com/sorrowmist/useless/utils/mining/DefaultMiningStrategy.java +++ b/src/main/java/com/sorrowmist/useless/utils/mining/DefaultMiningStrategy.java @@ -7,29 +7,17 @@ import net.minecraft.world.level.block.state.BlockState; import net.minecraftforge.event.level.BlockEvent; -/** - * 默认挖掘策略:普通单方块挖掘 - * - 开启强制挖掘模式时,忽略工具检查直接挖掘 - * - 未开启时,仅当工具适当时才挖掘 - */ -public class DefaultMiningStrategy implements MiningStrategy { +public class DefaultMiningStrategy implements MiningStrategy { @Override public void handleBreak(BlockEvent.BreakEvent event, ItemStack hand, Player player) { BlockState state = event.getState(); - if (!(event.getLevel() instanceof ServerLevel level)) return; + ServerLevel level = (ServerLevel) event.getLevel(); BlockPos pos = event.getPos(); - boolean forceMining = MiningUtils.isForceMiningMode(hand); - boolean isCorrectTool = hand.isCorrectToolForDrops(state); - - if (forceMining) { - MiningUtils.processBlockBreak(level, pos, state, player, hand, true); - event.setCanceled(true); - } else if (isCorrectTool) { + if (MiningUtils.canMineBlock(state, hand, false)) { MiningUtils.processBlockBreak(level, pos, state, player, hand, false); event.setCanceled(true); } - // 工具不正确且未开启强制挖掘:不处理,使用原版行为 } } diff --git a/src/main/java/com/sorrowmist/useless/utils/mining/ForceBreakStrategy.java b/src/main/java/com/sorrowmist/useless/utils/mining/ForceBreakStrategy.java index 05da8573..1489010b 100644 --- a/src/main/java/com/sorrowmist/useless/utils/mining/ForceBreakStrategy.java +++ b/src/main/java/com/sorrowmist/useless/utils/mining/ForceBreakStrategy.java @@ -1,29 +1,18 @@ package com.sorrowmist.useless.utils.mining; - -import net.darkhax.bookshelf.api.registry.RegistryObject; import net.minecraft.core.BlockPos; import net.minecraft.server.level.ServerLevel; -import net.minecraft.world.Container; import net.minecraft.world.entity.player.Player; -import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; -import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.block.Blocks; -import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; -import net.minecraftforge.common.capabilities.ForgeCapabilities; import net.minecraftforge.event.level.BlockEvent; -import net.minecraftforge.items.IItemHandler; -import net.minecraftforge.items.IItemHandlerModifiable; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.List; /** - * R键强制单方块破坏策略 - * 绕过工具检查和原版掉落逻辑,强制破坏目标方块 + * R键单方块破坏策略 + * 特殊掉落逻辑: + * 1. 精准采集模式:强制获取带NBT的方块 + * 2. 非精准采集模式:正常获取掉落物,无掉落物时强制掉落方块本身 + * 3. 对万象合金炉特殊处理:使用方块的getDrops方法以保存数据 */ public class ForceBreakStrategy implements MiningStrategy { @@ -33,121 +22,7 @@ public void handleBreak(BlockEvent.BreakEvent event, ItemStack hand, Player play BlockPos pos = event.getPos(); BlockState state = event.getState(); - Block block = state.getBlock(); - - // 检查是否为混沌水晶(Draconic Evolution) - if (handleChaosCrystal(level, pos, state, player)) { - event.setCanceled(true); - return; - } - - boolean isSilkTouch = MiningUtils.isSilkTouchMode(hand); - - // 精准采集模式下,先处理容器内容物防止掉落 - if (isSilkTouch) { - clearContainerContents(level, pos); - } - - List fallbackDrops = MiningUtils.getForcedFallbackDrops(state, level, pos, hand); - List drops = MiningUtils.destroyBlockAndCollectDrops(level, pos, state, player, hand); - - if (MiningUtils.hasNoValidDrops(drops) && !MiningUtils.hasNoValidDrops(fallbackDrops)) { - drops = fallbackDrops; - } - MiningUtils.handleDrops(player, drops, hand); - - // 经验处理(时运模式) - if (!isSilkTouch) { - block.popExperience(level, pos, block.getExpDrop(state, level, level.random, pos, 0, 0)); - } - + MiningUtils.processBlockBreak(level, pos, state, player, hand, true); event.setCanceled(true); } - - /** - * 混沌水晶特殊处理 - */ - private boolean handleChaosCrystal(ServerLevel level, BlockPos pos, BlockState state, Player player) { - try { - Class chaosCrystalClass = Class.forName("com.brandon3055.draconicevolution.blocks.ChaosCrystal"); - Class tileChaosCrystalClass = Class.forName("com.brandon3055.draconicevolution.blocks.tileentity.TileChaosCrystal"); - - if (chaosCrystalClass.isInstance(state.getBlock())) { - BlockEntity tileEntity = level.getBlockEntity(pos); - if (tileChaosCrystalClass.isInstance(tileEntity)) { - try { - Method setDefeatedMethod = tileChaosCrystalClass.getMethod("setDefeated"); - setDefeatedMethod.invoke(tileEntity); - } catch (Exception e) { - Field guardianDefeatedField = tileChaosCrystalClass.getDeclaredField("guardianDefeated"); - guardianDefeatedField.setAccessible(true); - Object managedBool = guardianDefeatedField.get(tileEntity); - Method setMethod = managedBool.getClass().getMethod("set", boolean.class); - setMethod.invoke(managedBool, true); - } - - Method tickMethod = tileChaosCrystalClass.getMethod("tick"); - tickMethod.invoke(tileEntity); - } - - // 生成混沌碎片掉落 - try { - Class deConfigClass = Class.forName("com.brandon3055.draconicevolution.DEConfig"); - Field chaosDropCountField = deConfigClass.getDeclaredField("chaosDropCount"); - chaosDropCountField.setAccessible(true); - int chaosDropCount = chaosDropCountField.getInt(null); - - Class deContentClass = Class.forName("com.brandon3055.draconicevolution.init.DEContent"); - Field chaosShardField = deContentClass.getDeclaredField("CHAOS_SHARD"); - chaosShardField.setAccessible(true); - Object chaosShardObject = chaosShardField.get(null); - - if (chaosShardObject instanceof RegistryObject ro) { - Object chaosShardItem = ro.get(); - ItemStack chaosShardStack = new ItemStack((Item) chaosShardItem, chaosDropCount); - Block.popResource(level, pos, chaosShardStack); - } - - level.setBlock(pos.above(), Blocks.AIR.defaultBlockState(), 3); - level.setBlock(pos.above(2), Blocks.AIR.defaultBlockState(), 3); - level.setBlock(pos.below(), Blocks.AIR.defaultBlockState(), 3); - level.setBlock(pos.below(2), Blocks.AIR.defaultBlockState(), 3); - level.setBlock(pos, Blocks.AIR.defaultBlockState(), 3); - return true; - } catch (Exception ignored) { - } - } - } catch (ClassNotFoundException ignored) { - } catch (Exception ignored) { - } - return false; - } - - /** - * 精准采集模式下清理容器内容物,防止重复掉落 - */ - private void clearContainerContents(ServerLevel level, BlockPos pos) { - BlockEntity be = level.getBlockEntity(pos); - if (be == null) return; - - try { - // 对于常见容器类型,直接清空 - if (be instanceof Container container) { - for (int i = 0; i < container.getContainerSize(); i++) { - container.setItem(i, ItemStack.EMPTY); - } - container.setChanged(); - } else { - // 尝试通过能力系统清空 - IItemHandler handler = be.getCapability( - ForgeCapabilities.ITEM_HANDLER).orElse(null); - if (handler instanceof IItemHandlerModifiable modifiable) { - for (int i = 0; i < modifiable.getSlots(); i++) { - modifiable.setStackInSlot(i, ItemStack.EMPTY); - } - } - } - } catch (Exception ignored) { - } - } } diff --git a/src/main/java/com/sorrowmist/useless/utils/mining/ForceChainMiningStrategy.java b/src/main/java/com/sorrowmist/useless/utils/mining/ForceChainMiningStrategy.java index ce261820..ec7d00c8 100644 --- a/src/main/java/com/sorrowmist/useless/utils/mining/ForceChainMiningStrategy.java +++ b/src/main/java/com/sorrowmist/useless/utils/mining/ForceChainMiningStrategy.java @@ -1,110 +1,28 @@ package com.sorrowmist.useless.utils.mining; -import net.minecraft.core.BlockPos; -import net.minecraft.network.chat.Component; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; -import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.block.state.BlockState; -import net.minecraftforge.event.level.BlockEvent; - -import java.util.ArrayList; -import java.util.List; /** - * R键强制连锁破坏策略 + * R键连锁破坏策略 * 使用R键特殊的掉落逻辑进行连锁破坏 */ -public class ForceChainMiningStrategy implements MiningStrategy { +public class ForceChainMiningStrategy extends ChainMiningStrategy { private final boolean enhanced; - public ForceChainMiningStrategy(boolean enhanced) { + ForceChainMiningStrategy(boolean enhanced) { + super(enhanced); this.enhanced = enhanced; } @Override - public void handleBreak(BlockEvent.BreakEvent event, ItemStack hand, Player player) { - if (!(event.getLevel() instanceof ServerLevel level)) return; - - BlockPos pos = event.getPos(); - BlockState originState = event.getState(); - Block originBlock = originState.getBlock(); - - // 查找需要破坏的方块列表 - List blocksToMine; - if (this.enhanced) { - blocksToMine = MiningUtils.findBlocksToMineEnhanced(pos, originState, level, hand, false); - } else { - blocksToMine = MiningUtils.findBlocksToMine(pos, originState, level, hand, false); - } - - if (blocksToMine.isEmpty()) { - return; - } - - boolean isSilkTouch = MiningUtils.isSilkTouchMode(hand); - - // 执行连锁挖掘 - List allDrops = new ArrayList<>(); - int actualMinedCount = 0; - - for (BlockPos targetPos : blocksToMine) { - BlockState currentState = level.getBlockState(targetPos); - if (currentState.getBlock() != originBlock) continue; - - // 精准采集模式下先清理容器内容物 - if (isSilkTouch) { - clearContainerContents(level, targetPos); - } - - List fallbackDrops = MiningUtils.getForcedFallbackDrops(currentState, level, targetPos, hand); - List drops = MiningUtils.destroyBlockAndCollectDrops(level, targetPos, currentState, player, hand); - if (MiningUtils.hasNoValidDrops(drops) && !MiningUtils.hasNoValidDrops(fallbackDrops)) { - drops = fallbackDrops; - } - allDrops.addAll(drops); - actualMinedCount++; - } - - // 统一处理掉落物 - if (!MiningUtils.hasNoValidDrops(allDrops)) { - MiningUtils.handleDrops(player, MiningUtils.mergeItemStacks(allDrops), hand); - } - - // 经验处理(时运模式) - if (!isSilkTouch) { - originBlock.popExperience(level, pos, originBlock.getExpDrop(originState, level, level.random, pos, 0, 0) * actualMinedCount); - } - - if (actualMinedCount > 0) { - String key = this.enhanced ? "强制增强连锁挖掘完成:" : "强制连锁挖掘完成:"; - player.displayClientMessage(Component.literal(key + "已挖掘 " + actualMinedCount + " 个方块"), true); - } - - event.setCanceled(true); + protected boolean isForceMining(ItemStack hand) { + return true; } - private void clearContainerContents(ServerLevel level, BlockPos pos) { - net.minecraft.world.level.block.entity.BlockEntity be = level.getBlockEntity(pos); - if (be == null) return; - - try { - if (be instanceof net.minecraft.world.Container container) { - for (int i = 0; i < container.getContainerSize(); i++) { - container.setItem(i, ItemStack.EMPTY); - } - container.setChanged(); - } else { - net.minecraftforge.items.IItemHandler handler = be.getCapability( - net.minecraftforge.common.capabilities.ForgeCapabilities.ITEM_HANDLER).orElse(null); - if (handler instanceof net.minecraftforge.items.IItemHandlerModifiable modifiable) { - for (int i = 0; i < modifiable.getSlots(); i++) { - modifiable.setStackInSlot(i, ItemStack.EMPTY); - } - } - } - } catch (Exception ignored) { - } + @Override + protected String getResultTranslationKey() { + return this.enhanced + ? "gui.useless_mod.force_enhanced_chain_mining_result" + : "gui.useless_mod.force_chain_mining_result"; } } diff --git a/src/main/java/com/sorrowmist/useless/utils/mining/MiningDispatcher.java b/src/main/java/com/sorrowmist/useless/utils/mining/MiningDispatcher.java index 93678763..4033501e 100644 --- a/src/main/java/com/sorrowmist/useless/utils/mining/MiningDispatcher.java +++ b/src/main/java/com/sorrowmist/useless/utils/mining/MiningDispatcher.java @@ -1,55 +1,158 @@ package com.sorrowmist.useless.utils.mining; +import com.sorrowmist.useless.core.config.ConfigManager; +import com.sorrowmist.useless.data.PlayerMiningData; +import com.sorrowmist.useless.network.MiningDataSyncPacket; +import com.sorrowmist.useless.utils.UComponentUtils; +import com.sorrowmist.useless.utils.mining.shape.ChainMiningShapes; import net.minecraft.core.BlockPos; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; +import net.minecraft.network.chat.Component; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.state.BlockState; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod.EventBusSubscriber; import net.minecraftforge.event.entity.player.PlayerEvent; import net.minecraftforge.event.level.BlockEvent; -import net.minecraftforge.eventbus.api.SubscribeEvent; -import net.minecraftforge.fml.common.Mod; +import com.sorrowmist.useless.compat.forge1201.network.PacketDistributor; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; -/** - * 挖掘调度器 - * 根据模式分派不同的挖掘策略处理方块破坏事件 - * 基于1.21 neoforge版本的MiningDispatcher重构 - */ -@Mod.EventBusSubscriber(modid = "useless_mod") +@EventBusSubscriber public class MiningDispatcher { private static final MiningStrategy DEFAULT_STRATEGY = new DefaultMiningStrategy(); + // 普通连锁挖掘 private static final MiningStrategy CHAIN_STRATEGY = new ChainMiningStrategy(false); + // 增强连锁挖掘 private static final MiningStrategy ENHANCED_CHAIN_STRATEGY = new ChainMiningStrategy(true); + // R键单方块破坏策略 private static final MiningStrategy FORCE_STRATEGY = new ForceBreakStrategy(); + // R键普通连锁破坏策略 private static final MiningStrategy FORCE_CHAIN_STRATEGY = new ForceChainMiningStrategy(false); + // R键增强连锁破坏策略 private static final MiningStrategy FORCE_ENHANCED_CHAIN_STRATEGY = new ForceChainMiningStrategy(true); + // 存储每个玩家的挖矿数据(服务端) private static final Map playerDataMap = new ConcurrentHashMap<>(); + // 客户端数据存储 + private static PlayerMiningData clientPlayerData = null; + + public static PlayerMiningData getPlayerData(Player player) { + if (player.level().isClientSide()) { + return clientPlayerData; + } + return playerDataMap.get(player.getUUID()); + } + + public static void setClientPlayerData(PlayerMiningData data) { + clientPlayerData = data; + } + /** - * 获取或创建玩家的挖掘数据 + * 获取或创建玩家的挖矿数据 + * + * @param player 玩家 + * @return 玩家挖矿数据 */ - public static PlayerMiningData getOrCreatePlayerData(Player player) { + static PlayerMiningData getOrCreatePlayerData(Player player) { return playerDataMap.computeIfAbsent(player.getUUID(), PlayerMiningData::new); } + /** + * 把玩家当前的挖矿数据同步到其客户端。 + * + *

形状与 Tab 状态都参与客户端预测,任何一处变化都必须下发,否则两端判定不一致 + * 会出现整片高亮闪烁或实际破坏范围与预览不符。 + * + * @param player 玩家 + */ + public static void syncToClient(Player player) { + if (player instanceof ServerPlayer serverPlayer) { + PlayerMiningData data = getOrCreatePlayerData(serverPlayer); + // 数量上限在此刷新而非构造时固化:配置可在运行时被 OP 修改,随每次同步取当前值 + data.setMaxBlocks(ConfigManager.getChainMiningMaxBlocks()); + PacketDistributor.sendToPlayer(serverPlayer, new MiningDataSyncPacket(data)); + } + } + + /** + * 按滚动方向循环切换连锁形状。 + * + *

形状属于玩家状态而非物品状态:同一根造化杖在不同玩家手里、甚至同一玩家换手后, + * 都应当保持各自的选择,因此写入 {@link PlayerMiningData} 而不是物品组件。 + * 切换后立即清空方块缓存并同步,避免沿用旧形状的扫描结果。 + * + * @param player 玩家 + * @param delta 滚动步数,正数为向后一个形状 + */ + public static void cycleShape(Player player, int delta) { + if (delta == 0) { + return; + } + + PlayerMiningData data = getOrCreatePlayerData(player); + ChainMiningShapes shape = data.getShape().cycle(delta); + data.setShape(shape); + data.clearCache(); + syncToClient(player); + notifyShapeChanged(player, shape); + } + + /** + * 回一条形状切换的 actionbar。 + * + *

形状名与方向说明缺一不可:隧道类形状的实际走向取决于点击面与玩家朝向, + * 仅凭名称无法判断会向哪个方向挖掘。 + */ + private static void notifyShapeChanged(Player player, ChainMiningShapes shape) { + player.displayClientMessage(Component.translatable( + "gui.useless_mod.shape_switched", + Component.translatable(shape.getTranslationKey()), + Component.translatable(shape.getDescriptionKey())), true); + } + /** * 设置玩家的Tab键状态 + * + * @param player 玩家 + * @param pressed 是否按下 */ public static void setTabPressed(Player player, boolean pressed) { PlayerMiningData playerData = getOrCreatePlayerData(player); playerData.setTabPressed(pressed); + + syncToClient(player); } /** - * 分派挖掘逻辑 + * 清空玩家的方块缓存 + * 在切换连锁模式时调用,避免使用旧模式的缓存数据 + * + * @param player 玩家 + */ + public static void clearPlayerCache(ServerPlayer player) { + PlayerMiningData playerData = playerDataMap.get(player.getUUID()); + if (playerData != null) { + playerData.clearCache(); + // 同步到客户端 + MiningDataSyncPacket packet = new MiningDataSyncPacket(playerData); + PacketDistributor.sendToPlayer(player, packet); + } + } + + /** + * 入口方法:根据组件值分派挖掘逻辑。 + * + * @param event BreakEvent事件 + * @param item 主手物品 + * @param player 玩家 */ public static void dispatchBreak(BlockEvent.BreakEvent event, ItemStack item, Player player) { if (player.isCreative()) return; @@ -57,59 +160,129 @@ public static void dispatchBreak(BlockEvent.BreakEvent event, ItemStack item, Pl Level level = (Level) event.getLevel(); if (level.isClientSide()) return; - MiningStrategy strategy = DEFAULT_STRATEGY; + MiningStrategy strategy = DEFAULT_STRATEGY; // 默认普通(不连锁) - // 检查Tab键状态(连锁挖掘) - PlayerMiningData playerData = playerDataMap.get(player.getUUID()); - if (playerData != null && playerData.isTabPressed()) { - if (MiningUtils.isEnhancedChainMiningMode(item)) { + // 获取玩家的挖矿数据 + PlayerMiningData playerData = getOrCreatePlayerData(player); + + // 检查是否启用了连锁挖掘(仅Tab键,R键由dispatchForceBreak处理) + if (playerData.isTabPressed()) { + // false -> 普通连锁挖掘 + // true -> 增强连锁挖掘 + if (UComponentUtils.isEnhancedChainMiningEnabled(item)) { strategy = ENHANCED_CHAIN_STRATEGY; } else { strategy = CHAIN_STRATEGY; } } + // 调用策略处理 strategy.handleBreak(event, item, player); } + public static void tickCacheUpdate(Player player) { + PlayerMiningData data = getOrCreatePlayerData(player); + + ItemStack hand = player.getMainHandItem(); + if (hand.isEmpty()) { + data.clearCache(); + // 同步到客户端 + if (player instanceof ServerPlayer serverPlayer) { + MiningDataSyncPacket packet = new MiningDataSyncPacket(data); + PacketDistributor.sendToPlayer(serverPlayer, packet); + } + return; + } + + if (!(player.level() instanceof ServerLevel level)) return; + + // 只有按下Tab时才更新缓存,松开Tab时保留缓存 + if (!data.isTabPressed()) { + return; + } + + // 获取玩家指向的方块 + BlockPos currentPos = MiningUtils.getTargetBlockPos(player); + + if (currentPos != null) { + // 只有准星移动到了新方块,才重新计算 + if (data.getCachedPos() == null || !data.getCachedPos().equals(currentPos)) { + BlockState state = level.getBlockState(currentPos); + boolean enhancedChainMining = UComponentUtils.isEnhancedChainMiningEnabled(hand); + + List blocks = MiningUtils.scanBlocksToMine( + currentPos, state, level, hand, false, enhancedChainMining, + data.getShape(), MiningUtils.getTargetFace(player), player); + + data.setCachedPos(currentPos); + data.setCachedBlocks(blocks); + + // 同步到客户端 + if (player instanceof ServerPlayer serverPlayer) { + MiningDataSyncPacket packet = new MiningDataSyncPacket(data); + PacketDistributor.sendToPlayer(serverPlayer, packet); + } + } + } else { + // 准星指天或指向空气,清理当前缓存 + data.clearCache(); + // 同步到客户端 + if (player instanceof ServerPlayer serverPlayer) { + MiningDataSyncPacket packet = new MiningDataSyncPacket(data); + PacketDistributor.sendToPlayer(serverPlayer, packet); + } + } + } + + /** + * 处理玩家离开事件,清理玩家数据,避免内存泄漏 + * + * @param event 玩家离开事件 + */ + @SubscribeEvent + public static void onPlayerLogout(PlayerEvent.PlayerLoggedOutEvent event) { + if (event.getEntity() instanceof ServerPlayer player) { + playerDataMap.remove(player.getUUID()); + } + } + /** * 强制破坏分派方法(R键触发) + * 直接执行破坏,不通过BlockEvent + * + * @param player 玩家 + * @param tabPressed 是否同时按下了Tab键 */ public static void dispatchForceBreak(Player player, boolean tabPressed) { if (player.isCreative()) return; if (player.level().isClientSide()) return; + // 获取玩家指向的方块 BlockPos targetPos = MiningUtils.getTargetBlockPos(player); if (targetPos == null) return; - if (!(player.level() instanceof ServerLevel level)) return; + ServerLevel level = (ServerLevel) player.level(); BlockState state = level.getBlockState(targetPos); if (state.isAir()) return; ItemStack hand = player.getMainHandItem(); + if (!UComponentUtils.isForceMiningEnabled(hand)) return; MiningStrategy strategy; + // 根据是否按下Tab键选择策略 if (tabPressed) { - if (MiningUtils.isEnhancedChainMiningMode(hand)) { + // R + Tab:R键连锁破坏 + if (UComponentUtils.isEnhancedChainMiningEnabled(hand)) { strategy = FORCE_ENHANCED_CHAIN_STRATEGY; } else { strategy = FORCE_CHAIN_STRATEGY; } } else { + // 仅R:R键单方块破坏 strategy = FORCE_STRATEGY; } BlockEvent.BreakEvent dummyEvent = new BlockEvent.BreakEvent(level, targetPos, state, player); strategy.handleBreak(dummyEvent, hand, player); } - - /** - * 玩家离开时清理数据 - */ - @SubscribeEvent - public static void onPlayerLogout(PlayerEvent.PlayerLoggedOutEvent event) { - if (event.getEntity() instanceof ServerPlayer) { - playerDataMap.remove(event.getEntity().getUUID()); - } - } } diff --git a/src/main/java/com/sorrowmist/useless/utils/mining/MiningUtils.java b/src/main/java/com/sorrowmist/useless/utils/mining/MiningUtils.java index a249ddeb..9f8fd5e0 100644 --- a/src/main/java/com/sorrowmist/useless/utils/mining/MiningUtils.java +++ b/src/main/java/com/sorrowmist/useless/utils/mining/MiningUtils.java @@ -1,61 +1,529 @@ package com.sorrowmist.useless.utils.mining; -import com.sorrowmist.useless.UselessMod; -import com.sorrowmist.useless.config.ConfigManager; -import com.sorrowmist.useless.items.EndlessBeafItem; -import com.sorrowmist.useless.utils.pattern.PatternProviderEvent; +import com.sorrowmist.useless.utils.RateLimitedLogger; + +import com.mojang.logging.LogUtils; +import com.sorrowmist.useless.api.enums.tool.EnchantMode; +import com.sorrowmist.useless.compat.AE2Compat; +import com.sorrowmist.useless.compat.DraconicEvolutionCompat; +import com.sorrowmist.useless.compat.exdeorum.ExDeorumCompat; +import com.sorrowmist.useless.core.component.UComponents; +import com.sorrowmist.useless.core.config.ChainEquivalence; +import com.sorrowmist.useless.core.config.ChainGroupManager; +import com.sorrowmist.useless.core.config.ConfigManager; +import com.sorrowmist.useless.utils.UComponentUtils; import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import com.sorrowmist.useless.utils.mining.shape.ChainMiningShapeContext; +import com.sorrowmist.useless.utils.mining.shape.ChainMiningShapes; import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.server.level.ServerLevel; import net.minecraft.sounds.SoundSource; import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.ExperienceOrb; import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.HitResult; -import net.minecraftforge.event.level.BlockEvent; - -import java.util.*; +import net.minecraft.world.phys.Vec3; +import net.minecraftforge.fml.ModList; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; -/** - * 挖掘工具类,包含方块查找、破坏、掉落处理等核心逻辑。 - * 基于1.21 neoforge版本的挖掘逻辑重构。 - */ public class MiningUtils { + private static final Logger LOGGER = LogUtils.getLogger(); + + record MiningResult(List drops, int experience, boolean mined) { + private static final MiningResult NOT_MINED = new MiningResult(List.of(), 0, false); + } + + /** + * 一次破坏尝试的结果。 + * + *

和 {@link MiningResult} 的区别:这个只描述破坏回调这一步,不带经验等上层语义。

+ * + *

两个布尔量必须分开:{@code effective} 表示本次破坏产生了效果(应当消耗该位置并交付掉落), + * {@code blockRemoved} 表示方块确实从世界消失。二者在方块自行接管移除时并不等价—— + * 例如集成动力线缆按准星命中的部件拆除并保留整块方块,此时 {@code effective=true} 而 + * {@code blockRemoved=false}。依赖「方块已消失」的判定(如精准采集的回退掉落)必须读后者, + * 否则会在方块仍留在原地时凭空产出其本体物品。

+ */ + record BreakOutcome(boolean effective, boolean blockRemoved, List drops) { + private static final BreakOutcome REFUSED = new BreakOutcome(false, false, List.of()); + } + + /** + * 已经报过「破坏回调抛异常」的方块类型。 + * + *

连锁挖掘一次可能命中同一类型的几十个方块,每个都抛一次异常就等于往日志里灌几十条栈。 + * 这里按方块类型去重:第一次带异常对象报,之后同类只累计次数。

+ */ + private static final Set REPORTED_BREAK_FAILURES = ConcurrentHashMap.newKeySet(); + private static final Set REPORTED_REFUSED_REMOVALS = ConcurrentHashMap.newKeySet(); + + /** + * 获取强制挖掘兜底掉落物 + * 当方块正常破坏没有有效掉落时,返回一个与目标方块 NBT 完全一致的方块物品(含方块实体组件)。 + * + * @param state 方块状态 + * @param level 世界 + * @param pos 方块位置 + * @return 兜底掉落物列表 + */ + static List getForcedFallbackDrops(BlockState state, ServerLevel level, BlockPos pos) { + BlockEntity be = level.getBlockEntity(pos); + ItemStack stack = new ItemStack(state.getBlock().asItem()); + if (stack.isEmpty() || stack.is(Items.AIR)) { + return Collections.emptyList(); + } + + // 兜底掉落的语义是"复制目标方块本身",因此只要存在方块实体就写入其完整 NBT。 + // 使用 saveToItem 而非 collectComponents:后者只保存方块实体暴露的隐式物品组件, + // 会丢失存放在方块实体 NBT 中的数据(如 AE2 线缆总线的部件/连接信息)。 + // 丢失这些数据会导致放回时方块实体为空而自我失效(物品直接消失)。 + // saveToItem 会写入 BLOCK_ENTITY_DATA(完整自定义 NBT)并附加组件,放置时可完整还原, + // 与 Mekanism 纸箱保存整块方块实体数据的做法一致。 + if (be != null) { + be.saveToItem(stack); + } + return Collections.singletonList(stack); + } + + /** + * 判断工具是否处于精准采集(SILK_TOUCH)模式 + */ + static boolean isSilkTouch(ItemStack tool) { + return com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(tool, UComponents.EnchantModeComponent.get(), EnchantMode.FORTUNE) == EnchantMode.SILK_TOUCH; + } + + /** + * 检查是否完全没有有效掉落物 + */ + static boolean hasNoValidDrops(List drops) { + return drops.isEmpty() || drops.stream().allMatch(stack -> stack.isEmpty() || stack.is(Items.AIR)); + } + + public static boolean canMineBlock(BlockState state, ItemStack tool, boolean forceMining) { + return forceMining || !state.requiresCorrectToolForDrops() || tool.isCorrectToolForDrops(state); + } + + /** + * 选择强制挖掘最终应交付的掉落物。 + * 仅当掉落表和实际破坏均没有产生有效掉落时才使用方块本体兜底; + * 任何非空自然掉落都应被视为合法结果,即使掉落物本身是 BlockItem。 + */ + static List selectForcedDrops(List naturalDrops, List actualDrops, + List fallbackDrops) { + if (hasNoValidDrops(naturalDrops) + && hasNoValidDrops(actualDrops) + && !hasNoValidDrops(fallbackDrops)) { + return fallbackDrops; + } + return actualDrops; + } + + /** + * 处理方块破坏的核心逻辑:获取掉落物、处理掉落物、计算经验、破坏方块 + * + * @param level 世界 + * @param pos 方块位置 + * @param state 方块状态 + * @param player 玩家 + * @param tool 工具 + * @param forceMining 是否为强制挖掘模式 + */ + static void processBlockBreak(ServerLevel level, BlockPos pos, BlockState state, Player player, + ItemStack tool, boolean forceMining) { + if (level.isClientSide()) { + return; + } + + // 本方法由方块破坏事件直接调用:任一模组的破坏回调抛出异常,异常都会沿事件 + // 分发链回传并中断整个 tick。因此在此处捕获并上报异常。 + try { + MiningResult result = forceMining + ? forceMineBlock(level, pos, state, player, tool) + : mineBlock(level, pos, state, player, tool); + List drops = applyExDeorumDrops(level, state, result.drops(), tool, pos, player); + handleDrops(player, drops, tool, Vec3.atCenterOf(pos)); + if (result.experience() > 0) { + player.giveExperiencePoints(result.experience()); + } + } catch (Throwable failure) { + reportBlockBreakFailure(state, pos, failure); + } + } + + /** + * 按造化杖上启用的 Ex Deorum 模式改写破坏掉落。 + * + *

Ex Deorum 的三个工具效果由其 GlobalLootModifier 依工具标签触发, + * 而造化杖不在这些标签内,故此处显式调用其配方缓存完成等价改写。

+ * + *

未安装 Ex Deorum 或三个模式均未启用时原样返回入参, + * 保证未启用该兼容时的行为与改动前完全一致。

+ * + *

此处不以原始掉落为空作为提前返回条件:锤子与压缩锤以配方产物替换掉落, + * 而 Ex Deorum 的锤子配方可覆盖自身无自然掉落的方块(如圆石产出沙砾), + * 若在此跳过,此类方块将无法获得配方产物。

+ * + * @param level 服务端世界 + * @param state 被破坏的方块状态 + * @param drops 原始掉落 + * @param tool 造化杖 + * @return 改写后的掉落列表 + */ + static List applyExDeorumDrops(ServerLevel level, BlockState state, + List drops, ItemStack tool, + BlockPos pos, Player player) { + // 守卫必须在本方法内完成,且不能触碰 ExDeorumCompat 的任何成员:该类引用了 exdeorum + // 的类型,链接它即须解析 HammerRecipe 等外部类型。若把守卫放进该类(原 isLoaded() 即是), + // 那一行本身就会触发链接,未安装模组时直接抛 NoClassDefFoundError,守卫形同虚设。 + // 此处以纯 ModList 短路,使 ExDeorumCompat 仅在模组确已加载后才被链接, + // 与 FtbUltimineChainKeyCompat 采用同一策略。模组 id 用字面量,避免本行出现对该类的引用。 + if (!ModList.get().isLoaded("exdeorum")) { + return drops; + } + + boolean hammer = UComponentUtils.isExDeorumHammerEnabled(tool); + boolean compressedHammer = UComponentUtils.isExDeorumCompressedHammerEnabled(tool); + boolean crook = UComponentUtils.isExDeorumCrookEnabled(tool); + if (!hammer && !compressedHammer && !crook) { + return drops; + } + + return ExDeorumCompat.rewriteDrops(level, state, drops, tool, + hammer, compressedHammer, crook, pos, player); + } + + static MiningResult mineBlock(ServerLevel level, BlockPos pos, BlockState state, Player player, ItemStack tool) { + if (state.isAir() || !canMineBlock(state, tool, false)) { + return MiningResult.NOT_MINED; + } + + int experience = getExperience(level, pos, state, player, tool); + BreakOutcome outcome = destroyBlockAndCollectDrops(level, pos, state, player, tool, false); + return outcome.effective() + ? new MiningResult(outcome.drops(), experience, true) + : MiningResult.NOT_MINED; + } + + static MiningResult forceMineBlock(ServerLevel level, BlockPos pos, BlockState state, Player player, ItemStack tool) { + if (state.isAir() || isForceMiningBlacklisted(state)) { + return MiningResult.NOT_MINED; + } + if (DraconicEvolutionCompat.isChaosCrystal(state) + && DraconicEvolutionCompat.handleChaosCrystalBreak(level, pos, state, player)) { + return new MiningResult(List.of(), 0, true); + } + + int experience = getExperience(level, pos, state, player, tool); + if (isSilkTouch(tool)) { + List fallbackDrops = getForcedFallbackDrops(state, level, pos); + // 回退掉落复制的是方块本体,必须以「方块确实已从世界消失」为准: + // 方块自行接管移除时(如线缆按部件拆除)方块仍留在原地,此时发放本体会凭空产出物品。 + BreakOutcome outcome = destroyBlockAndCollectDrops(level, pos, state, player, tool, true); + if (!outcome.blockRemoved()) { + return outcome.effective() + ? new MiningResult(outcome.drops(), 0, true) + : MiningResult.NOT_MINED; + } + return new MiningResult(fallbackDrops, 0, true); + } + + BlockEntity blockEntity = level.getBlockEntity(pos); + List naturalDrops = Block.getDrops(state, level, pos, blockEntity, player, tool); + boolean useFallback = hasNoValidDrops(naturalDrops); + List fallbackDrops = useFallback + ? getForcedFallbackDrops(state, level, pos) + : List.of(); + BreakOutcome outcome = destroyBlockAndCollectDrops(level, pos, state, player, tool, true); + if (!outcome.effective()) { + return MiningResult.NOT_MINED; + } + // 回退掉落仅适用于方块确已消失的情形;方块被模组接管保留时只能交付实际产出的掉落。 + List drops = outcome.blockRemoved() + ? selectForcedDrops(naturalDrops, outcome.drops(), fallbackDrops) + : outcome.drops(); + return new MiningResult(drops, experience, true); + } + + private static int getExperience(ServerLevel level, BlockPos pos, BlockState state, Player player, ItemStack tool) { + if (com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.getOrDefault(tool, UComponents.EnchantModeComponent.get(), EnchantMode.FORTUNE) != EnchantMode.FORTUNE) { + return 0; + } + return state.getBlock().getExpDrop(state, level, level.getRandom(), pos, + net.minecraft.world.item.enchantment.EnchantmentHelper.getItemEnchantmentLevel( + net.minecraft.world.item.enchantment.Enchantments.BLOCK_FORTUNE, tool), + net.minecraft.world.item.enchantment.EnchantmentHelper.getItemEnchantmentLevel( + net.minecraft.world.item.enchantment.Enchantments.SILK_TOUCH, tool)); + } + + /** + * 快速破坏指定方块(Shift+右键物品使用时调用) + * 功能:掉落物直接进背包、背包满掉脚下、正确保留 waterlogged 水源、弹出经验、粒子音效 + * + * @param world 世界 + * @param pos 方块位置 + * @param state 方块状态 + * @param player 玩家(必须非空) + * @param tool 手中物品(用于计算掉落、附魔、耐久等) + */ + public static void quickBreakBlock(Level world, BlockPos pos, BlockState state, Player player, ItemStack tool) { + if (world.isClientSide()) { + world.playSound(player, pos, state.getSoundType().getBreakSound(), SoundSource.BLOCKS, 0.7F, 1.0F); + return; + } + + ServerLevel serverLevel = (ServerLevel) world; + + // 同样要把模组回调的异常挡在事件链之外:数据能源的三位一体样板核心在状态未就绪时 + // getDrops / playerWillDestroy 都会抛,异常冒回事件总线就会打断整 tick。 + try { + // 破坏必须走与准星挖掘同一套回调次序,否则重写 onDestroyedByPlayer 的模组 + // (如集成动力线缆按部件拆除)会在此被整块移除,其部件本体无从掉落。 + // 掉落直接取该流程的收集结果:回调与移除两阶段的产出均在其中。 + // 不可改用 Block.getDrops 重新计算——该流程已把收集到的掉落实体移除, + // 且掉落表结果不含移除阶段产出的内容物,重算会导致这部分掉落整体丢失。 + BreakOutcome outcome = destroyBlockAndCollectDrops(serverLevel, pos, state, player, tool, false); + if (!outcome.effective()) { + return; + } + + List drops = applyExDeorumDrops(serverLevel, state, outcome.drops(), tool, pos, player); + handleDrops(player, drops, tool, Vec3.atCenterOf(pos)); + } catch (Throwable failure) { + reportBlockBreakFailure(state, pos, failure); + } + } + + /** + * 合并相同物品的堆叠 + * + * @param items 要合并的物品列表 + * @return 合并后的物品列表 + */ + public static List mergeItemStacks(List items) { + List merged = new ArrayList<>(); + for (ItemStack item : items) { + if (item.isEmpty()) continue; + + boolean mergedFlag = false; + // 尝试合并到已有的堆叠中 + for (ItemStack mergedItem : merged) { + // 检查:物品相同、组件相同、且有堆叠空间 + if (ItemStack.isSameItemSameTags(item, mergedItem)) { + int remaining = mergedItem.getMaxStackSize() - mergedItem.getCount(); + if (remaining > 0) { + int addCount = Math.min(remaining, item.getCount()); + mergedItem.grow(addCount); + item.shrink(addCount); + if (item.isEmpty()) { + mergedFlag = true; + break; + } + } + } + } + + // 未完成合并(组件不同或空间不足)时,作为新堆加入 + if (!mergedFlag && !item.isEmpty()) { + merged.add(item.copy()); + } + } + return merged; + } + + /** + * 处理掉落物(优先入 AE,其次按磁力开关决定进背包还是留在原地)。 + * + *

兼容旧调用:落地点退回玩家脚下。

+ * + * @param player 玩家 + * @param drops 掉落物列表 + * @param tool 工具 + */ + public static void handleDrops(Player player, List drops, ItemStack tool) { + handleDrops(player, drops, tool, player.position()); + } + + /** + * 处理掉落物(优先入 AE,其次按磁力开关决定进背包还是留在原地)。 + * + *

行为矩阵:

+ *
    + *
  • AE 存储优先开启且已绑定无线访问点:先尝试存入 AE,未能存入的继续下一步。
  • + *
  • 范围磁力开启:剩余物品进背包,背包满则掉在玩家脚下。
  • + *
  • 范围磁力关闭:剩余物品在 {@code dropOrigin} 处落地,走原版拾取。
  • + *
+ * + *

注意 AE 存储优先是独立于范围磁力生效的:即使磁力关闭,只要 AE 优先开启, + * 产物仍会优先存入 AE,仅 AE 无法存入的部分落地。

+ * + * @param player 玩家 + * @param drops 掉落物列表 + * @param tool 工具 + * @param dropOrigin 磁力关闭时的落地点(AE 无法存入的部分亦落于此) + */ + public static void handleDrops(Player player, List drops, ItemStack tool, Vec3 dropOrigin) { + handleDrops(player, drops, tool, dropOrigin, true); + } + + /** + * 与 {@link #handleDrops(Player, List, ItemStack, Vec3)} 相同,但可以关掉 AE 分支。 + * + *

捕捉(刷怪蛋)走的是 {@code allowAe = false}:刷怪蛋是玩家想立刻拿在手上的东西, + * 不该被「AE 存储优先」吸进网络里,只按「范围磁力」决定进背包还是落地。

+ * + * @param allowAe 是否允许走「AE 存储优先」分支 + */ + public static void handleDrops(Player player, List drops, ItemStack tool, Vec3 dropOrigin, + boolean allowAe) { + boolean isAE2Loaded = ModList.get().isLoaded("ae2"); + boolean magnetEnabled = UComponentUtils.isBeefMagnetEnabled(tool); + + // 自动熔炼:在入库/落地之前先把掉落物炼一遍, + // 这样后续的 AE 优先与磁力拾取拿到的都是成品,不需要各自再处理。 + drops = AutoSmeltHelper.smeltDrops(player.level(), drops, tool); + + for (ItemStack drop : drops) { + if (drop.isEmpty()) continue; + + // 1. 尝试存入 AE2 (内部处理跨维度) + if (allowAe + && isAE2Loaded + && UComponentUtils.isAEStoragePriorityEnabled(tool) + && com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.has(tool, UComponents.WIRELESS_LINK_TARGET.get())) { + try { + int inserted = AE2Compat.tryInsertToLinkedGrid(tool, player, drop); + if (inserted > 0) { + drop.shrink(inserted); + } + } catch (Throwable ignored) { + } + } + + if (drop.isEmpty()) continue; + + // 2. 磁力开启:剩余进入背包 + if (magnetEnabled) { + if (!player.getInventory().add(drop)) { + player.drop(drop, false); + } + continue; + } - // ==================== 方块查找 ==================== + // 3. 磁力关闭:剩余留在原地走原版拾取 + dropAtOrigin(player.level(), dropOrigin, drop); + } + } /** - * 普通连锁模式下查找需要破坏的方块(BFS广度优先搜索,需要相邻) + * 在指定位置生成一个掉落实体,让它走原版拾取流程。 + * + *

仅在服务端生效;客户端调用会被忽略。

+ * + * @param level 世界 + * @param origin 落点 + * @param stack 掉落物 + */ + private static void dropAtOrigin(Level level, Vec3 origin, ItemStack stack) { + if (stack.isEmpty() || !(level instanceof ServerLevel serverLevel)) { + return; + } + ItemEntity entity = new ItemEntity(serverLevel, origin.x, origin.y, origin.z, stack.copy()); + serverLevel.addFreshEntity(entity); + } + + /** + * 普通连锁模式下查找需要破坏的方块 * * @param originPos 原点位置 * @param originState 原点方块状态 * @param level 世界 * @param stack 工具 * @param forceMining 是否为强制挖掘模式 - * @return 需要破坏的方块列表(按距离排序) + * @param shape 连锁形状 + * @param face 玩家点击的面 + * @param player 触发连锁的玩家,用于取该玩家自己的等价组 + * @return 需要破坏的方块列表 + */ + static List scanBlocksToMine(BlockPos originPos, BlockState originState, Level level, ItemStack stack, + boolean forceMining, boolean enhanced, ChainMiningShapes shape, + Direction face, Player player) { + return scanBlocks(originPos, originState, level, stack, forceMining, enhanced, true, shape, face, player); + } + + /** + * 右键连锁用的扫描:等价组 / 范围 / 数量上限与连锁挖掘完全共用, + * 区别只是不做「工具能否挖掘该方块」的门槛判定 + * (右键要作用的是耕地、原木、作物这些「工具动作能生效」的方块, + * 而它们未必是当前工具能正确采集的方块)。 + * + * @param originPos 原点位置 + * @param originState 原点方块状态 + * @param level 世界 + * @param enhanced 是否增强连锁(增强模式取消相邻限制,改为范围内扫描) + * @param shape 连锁形状 + * @param face 玩家点击的面 + * @param player 触发连锁的玩家 + * @return 连锁范围(含原点,按挖掘顺序排列) + */ + static List scanBlocksForUse(BlockPos originPos, BlockState originState, Level level, boolean enhanced, + ChainMiningShapes shape, Direction face, Player player) { + return scanBlocks(originPos, originState, level, ItemStack.EMPTY, false, enhanced, false, shape, face, player); + } + + /** + * 连锁扫描主流程 + * + * @param requireMineable true = 挖矿语义(额外做工具等级 / 强制挖掘黑名单判定), + * false = 右键语义(只看等价组与范围) */ - public static List findBlocksToMine(BlockPos originPos, BlockState originState, Level level, - ItemStack stack, boolean forceMining) { + private static List scanBlocks(BlockPos originPos, BlockState originState, Level level, ItemStack stack, + boolean forceMining, boolean enhanced, boolean requireMineable, +ChainMiningShapes shape, Direction face, Player player) { + if (forceMining && isForceMiningBlacklisted(originState)) { + return List.of(); + } + if (shape != null && !shape.usesBuiltinScan()) { + return scanShapeBlocks(originPos, originState, level, stack, forceMining, requireMineable, shape, face, player); + } + if (enhanced) { + return scanAreaBlocks(originPos, originState, level, stack, forceMining, requireMineable, player); + } + // 最大连锁数量 int maxBlocks = ConfigManager.getChainMiningMaxBlocks(); + // 获取连锁挖掘范围 int rangeX = ConfigManager.getChainMiningRangeX(); int rangeY = ConfigManager.getChainMiningRangeY(); int rangeZ = ConfigManager.getChainMiningRangeZ(); - Block originBlock = originState.getBlock(); + // 同类方块判定:玩家自己的等价组命中时按组匹配,否则退回严格同方块 + ChainEquivalence equivalence = ChainGroupManager.equivalenceFor(player, originState.getBlock()); List blocksToMine = new ArrayList<>(maxBlocks); - if (!forceMining && !stack.isCorrectToolForDrops(originState)) { - return blocksToMine; + // 检查原点方块是否可以被挖掘(工具等级检查) + if (requireMineable && !canMineBlock(originState, stack, forceMining)) { + return blocksToMine; // 返回空列表 } Queue queue = new LinkedList<>(); @@ -81,18 +549,22 @@ public static List findBlocksToMine(BlockPos originPos, BlockState ori int ny = cy + y; int nz = cz + z; + // 1. 距离快速过滤 if (Math.abs(nx - originPos.getX()) > rangeX || Math.abs(ny - originPos.getY()) > rangeY || Math.abs(nz - originPos.getZ()) > rangeZ) continue; + // 2. 访问过滤 long nLong = BlockPos.asLong(nx, ny, nz); if (visited.contains(nLong)) continue; + // 3. 状态检查 BlockPos neighborPos = new BlockPos(nx, ny, nz); BlockState nextState = level.getBlockState(neighborPos); - if (nextState.getBlock() == originBlock) { - if (forceMining || stack.isCorrectToolForDrops(nextState)) { + if (equivalence.matches(nextState)) { + if ((!requireMineable || canMineBlock(nextState, stack, forceMining)) + && !(forceMining && isForceMiningBlacklisted(nextState))) { visited.add(nLong); queue.add(neighborPos); } @@ -102,33 +574,103 @@ public static List findBlocksToMine(BlockPos originPos, BlockState ori } } + // 使用欧几里得距离平方进行排序 blocksToMine.sort(Comparator.comparingDouble(pos -> pos.distSqr(originPos))); + return blocksToMine; } + public static boolean isForceMiningBlacklisted(BlockState state) { + return ConfigManager.isBeefToolForceMiningBlockBlacklisted( + BuiltInRegistries.BLOCK.getKey(state.getBlock())); + } + /** - * 增强连锁模式下查找需要破坏的方块(直接扫描范围,不需要相邻) + * 按指定形状查找需要破坏的方块。 + * + *

形状负责布局,某个坐标是否可用仍由 {@link ChainMiningShapeContext#check} 依据等价组、 + * 范围、工具可采集与强制挖掘黑名单判定,因此形状实现无需重复这些规则。 + * + *

返回序列保持形状给出的挖掘顺序并截断到数量上限;布局重叠导致的重复坐标会被去重, + * 避免同一格被破坏两次。原点方块不可采集时返回空列表,与相邻扩散扫描的既有语义一致。 + * + * @param originPos 原点位置 + * @param originState 原点方块状态 + * @param level 世界 + * @param stack 工具 + * @param forceMining 是否为强制挖掘模式 + * @param requireMineable 是否需要「工具可采集」门槛 + * @param shape 连锁形状 + * @param face 玩家点击的面 + * @param player 触发连锁的玩家 + * @return 需要破坏的方块列表 + */ + private static List scanShapeBlocks(BlockPos originPos, BlockState originState, Level level, + ItemStack stack, boolean forceMining, boolean requireMineable, + ChainMiningShapes shape, Direction face, Player player) { + if (requireMineable && !canMineBlock(originState, stack, forceMining)) { + return List.of(); + } + + int maxBlocks = ConfigManager.getChainMiningMaxBlocks(); + // 同类方块判定:玩家自己的等价组命中时按组匹配,否则退回严格同方块 + ChainEquivalence equivalence = ChainGroupManager.equivalenceFor(player, originState.getBlock()); + ChainMiningShapeContext context = new ChainMiningShapeContext( + level, originPos, originState, face, player, stack, equivalence, + forceMining, requireMineable, maxBlocks, + ConfigManager.getChainMiningRangeX(), ConfigManager.getChainMiningRangeY(), + ConfigManager.getChainMiningRangeZ()); + + List shapeBlocks = shape.getBlocks(context); + List blocksToMine = new ArrayList<>(Math.min(shapeBlocks.size(), maxBlocks)); + LongOpenHashSet seen = new LongOpenHashSet(Math.max(16, shapeBlocks.size() * 2)); + for (BlockPos pos : shapeBlocks) { + if (blocksToMine.size() >= maxBlocks) { + break; + } + if (!seen.add(pos.asLong())) { + continue; + } + if (forceMining && isForceMiningBlacklisted(level.getBlockState(pos))) { + continue; + } + blocksToMine.add(pos); + } + + return blocksToMine; + } + + /** + * 增强连锁模式下查找需要破坏的方块 + * 增强连锁:取消相邻才能连锁的限制 * * @param originPos 原点位置 * @param originState 原点方块状态 * @param level 世界 * @param stack 工具 * @param forceMining 是否为强制挖掘模式 - * @return 需要破坏的方块列表(按距离排序) + * @param player 触发连锁的玩家,用于取该玩家自己的等价组 + * @return 需要破坏的方块列表 */ - public static List findBlocksToMineEnhanced(BlockPos originPos, BlockState originState, Level level, - ItemStack stack, boolean forceMining) { + private static List scanAreaBlocks(BlockPos originPos, BlockState originState, Level level, + ItemStack stack, boolean forceMining, boolean requireMineable, + Player player) { + // 最大连锁数量(包含原点方块) int maxBlocks = ConfigManager.getChainMiningMaxBlocks(); + // 获取连锁挖掘范围 int rangeX = ConfigManager.getChainMiningRangeX(); int rangeY = ConfigManager.getChainMiningRangeY(); int rangeZ = ConfigManager.getChainMiningRangeZ(); - Block originBlock = originState.getBlock(); + // 同类方块判定:玩家自己的等价组命中时按组匹配,否则退回严格同方块 + ChainEquivalence equivalence = ChainGroupManager.equivalenceFor(player, originState.getBlock()); List blocksToMine = new ArrayList<>(maxBlocks); - for (int x = -rangeX; x <= rangeX && blocksToMine.size() < maxBlocks; x++) { - for (int y = -rangeY; y <= rangeY && blocksToMine.size() < maxBlocks; y++) { - for (int z = -rangeZ; z <= rangeZ && blocksToMine.size() < maxBlocks; z++) { + // 增强连锁:直接在范围内扫描所有相同方块,不需要相邻限制 + // 先收集范围内全部匹配方块,再按距离排序、截断到上限,保证保留的是"最近"的方块而非扫描顺序靠前的。 + for (int x = -rangeX; x <= rangeX; x++) { + for (int y = -rangeY; y <= rangeY; y++) { + for (int z = -rangeZ; z <= rangeZ; z++) { int nx = originPos.getX() + x; int ny = originPos.getY() + y; int nz = originPos.getZ() + z; @@ -136,8 +678,9 @@ public static List findBlocksToMineEnhanced(BlockPos originPos, BlockS BlockPos targetPos = new BlockPos(nx, ny, nz); BlockState nextState = level.getBlockState(targetPos); - if (nextState.getBlock() == originBlock) { - if (forceMining || stack.isCorrectToolForDrops(nextState)) { + if (equivalence.matches(nextState)) { + if ((!requireMineable || canMineBlock(nextState, stack, forceMining)) + && !(forceMining && isForceMiningBlacklisted(nextState))) { blocksToMine.add(targetPos); } } @@ -145,164 +688,235 @@ public static List findBlocksToMineEnhanced(BlockPos originPos, BlockS } } + // 使用欧几里得距离平方进行排序(最近优先) blocksToMine.sort(Comparator.comparingDouble(pos -> pos.distSqr(originPos))); + + // 排序后截断到上限,确保保留距离最近的方块 + if (blocksToMine.size() > maxBlocks) { + return new ArrayList<>(blocksToMine.subList(0, maxBlocks)); + } + return blocksToMine; } - // ==================== 方块破坏 ==================== - /** - * 处理方块破坏的核心逻辑:获取掉落物、处理掉落物、计算经验、破坏方块 + * 通过方块自身的破坏回调破坏方块,并收集本次新生成的掉落实体。 + * 这样可以保留其他模组在破坏回调中实现的特殊掉落逻辑,同时仍然让掉落物进入背包。 * - * @param level 世界 - * @param pos 方块位置 - * @param state 方块状态 - * @param player 玩家 - * @param tool 工具 - * @param forceMining 是否为强制挖掘模式 + *

破坏按原版 {@code ServerPlayerGameMode#destroyBlock} 的次序执行,三步各自都可能产出掉落:

+ *
    + *
  1. {@code playerWillDestroy}:回调使用破坏前的方块状态(1.20.1 返回 void)。
  2. + *
  3. {@code onDestroyedByPlayer}:由方块自身决定是否移除。MinecraftForge 的默认实现即 + * {@code Level#removeBlock},因而会触发 {@code onRemove},容器类方块通常在此掉落内容物; + * 重写该方法的模组可以接管本次移除并返回 {@code false}(如集成动力线缆按准星命中的部件拆除, + * 整块方块予以保留)。返回 {@code true} 时随之调用 {@code Block#destroy} 收尾。
  4. + *
  5. {@code playerDestroy}:仅在方块确被移除时执行,经 {@code getDrops} 产出掉落表物品, + * 模组也可在此直接追加自定义掉落。
  6. + *
+ * + *

缺少第 2 步时,任何依赖 {@code onDestroyedByPlayer} 决定移除方式的模组都会失效: + * 其接管逻辑被完全跳过,方块按整块移除处理,且不再有机会掉落自身部件。

+ * + *

若方块既重写了 {@code getDrops} 并返回内容物,又在 {@code onRemove} 中掉落内容物, + * 同一批物品会被两条路径各生成一次。此处以 {@code playerDestroy} 阶段的掉落为基准, + * 移除阶段中与之完全相同的条目视为重复并丢弃,避免采集端把两份都收走; + * 配对要求物品、组件与数量三者同时一致,因此方块自身重复产出的同类掉落不受影响。

+ * + * @param level 世界 + * @param pos 方块位置 + * @param state 方块状态 + * @param player 玩家 + * @param tool 工具 + * @param force 是否为强制挖掘:为真时方块对本次移除的拒绝不成立,将补做一次整块移除 + * @return 本次破坏的结果(是否产生了有效破坏 + 掉落物列表) */ - public static void processBlockBreak(ServerLevel level, BlockPos pos, BlockState state, Player player, - ItemStack tool, boolean forceMining) { - List fallbackDrops = forceMining ? getForcedFallbackDrops(state, level, pos, tool) : Collections.emptyList(); - List drops = destroyBlockAndCollectDrops(level, pos, state, player, tool); - if (forceMining && hasNoValidDrops(drops) && !hasNoValidDrops(fallbackDrops)) { - drops = fallbackDrops; + static BreakOutcome destroyBlockAndCollectDrops(ServerLevel level, BlockPos pos, BlockState state, + Player player, ItemStack tool, boolean force) { + // 采用破坏前后 2 格膨胀范围内 ItemEntity 的差集来收集本次掉落, + // 2 格可覆盖部分模组把掉落物生成在方块中心 1 格外的情况;before/after 差集保证不会误收邻近方块的已有掉落。 + AABB area = new AABB(pos).inflate(2.0); + Set before = level.getEntitiesOfClass(ItemEntity.class, area) + .stream() + .map(Entity::getUUID) + .collect(Collectors.toSet()); + Set experienceBefore = level.getEntitiesOfClass(ExperienceOrb.class, area) + .stream() + .map(Entity::getUUID) + .collect(Collectors.toSet()); + + Block block = state.getBlock(); + BlockEntity blockEntity = level.getBlockEntity(pos); + + // 第一步:playerWillDestroy 回调使用破坏前的方块状态(1.20.1 返回 void)。 + // 该回调抛出即视为模组拒绝这次移除,尊重它并放弃本格的后续步骤。 + BlockState currentState; + try { + block.playerWillDestroy(level, pos, state, player); + currentState = state; + } catch (Throwable refusal) { + reportRefusedRemoval(block, pos, "playerWillDestroy", refusal); + return BreakOutcome.REFUSED; } - handleDrops(player, drops, tool); - // 经验处理(时运模式检查) - if (!isSilkTouchMode(tool)) { - state.getBlock().popExperience(level, pos, state.getBlock().getExpDrop(state, level, level.random, pos, 0, 0)); + // 第二步:移除阶段交给方块自身决定。重写 onDestroyedByPlayer 的模组可在此按部件拆除 + // 并返回 false;此时方块保留,但该模组通常已产出掉落,需一并收走。 + boolean removed; + try { + boolean canHarvest = currentState.canHarvestBlock(level, pos, player); + removed = currentState.onDestroyedByPlayer(level, pos, player, canHarvest, + level.getFluidState(pos)); + if (removed) { + currentState.getBlock().destroy(level, pos, currentState); + } + } catch (Throwable refusal) { + reportRefusedRemoval(block, pos, "onDestroyedByPlayer", refusal); + return BreakOutcome.REFUSED; } - } - /** - * 强制挖掘模式兜底掉落物:当方块正常破坏没有有效掉落时,返回方块本身 - */ - public static List getForcedFallbackDrops(BlockState state, ServerLevel level, BlockPos pos, - ItemStack tool) { - BlockEntity be = level.getBlockEntity(pos); - ItemStack stack = new ItemStack(state.getBlock().asItem()); - if (stack.isEmpty() || stack.getItem() == Items.AIR) { - return Collections.emptyList(); + Set consumed = ConcurrentHashMap.newKeySet(); + List removalDrops = collectNewDrops(level, area, before, consumed); + + if (!removed) { + // 方块未被移除时存在两种情形,必须区分对待: + // 一是模组接管了本次破坏并按自身语义完成拆除(如集成动力线缆按准星命中的部件拆除), + // 此时已有掉落产出,该结果即本次破坏的最终产物; + // 二是模组明确拒绝移除,未产出任何掉落。 + // 仅后者属于强制挖掘不接受的范围,补做一次整块移除;对前者补做强拆会连带移除 + // 该位置剩余部件,使组合体整块消失。 + // 常规挖掘一律尊重该结果——方块仍留在原地,按掉落表产出物品等同于凭空生成。 + if (hasNoValidDrops(removalDrops) && force && !level.getBlockState(pos).isAir()) { + try { + level.removeBlock(pos, false); + if (level.getBlockState(pos).isAir()) { + currentState.getBlock().destroy(level, pos, currentState); + } + removalDrops.addAll(collectNewDrops(level, area, before, consumed)); + } catch (Throwable refusal) { + reportRefusedRemoval(block, pos, "forced removal", refusal); + } + } + if (!level.getBlockState(pos).isAir()) { + discardNewExperience(level, area, experienceBefore); + return hasNoValidDrops(removalDrops) + ? BreakOutcome.REFUSED + : new BreakOutcome(true, false, removalDrops); + } + removed = true; } - boolean isSilk = isSilkTouchMode(tool); - if (isSilk && be != null) { - net.minecraft.nbt.CompoundTag tag = be.saveWithoutMetadata(); - if (!tag.isEmpty()) { - net.minecraft.nbt.CompoundTag itemTag = stack.getOrCreateTag(); - itemTag.put("BlockEntityTag", tag); - } + // 第三步:方块已移除,按掉落表产出物品。该回调抛出不影响方块已被移除的事实,仅上报。 + try { + currentState.getBlock().playerDestroy(level, player, pos, currentState, blockEntity, tool); + } catch (Throwable refusal) { + reportRefusedRemoval(block, pos, "playerDestroy", refusal); } - return Collections.singletonList(stack); + List callbackDrops = collectNewDrops(level, area, before, consumed); + + // 经验球统一在两个阶段结束后清理:本流程自行结算经验,两阶段新生成的经验球都不应留在世界上。 + discardNewExperience(level, area, experienceBefore); + + return new BreakOutcome(true, true, mergePhaseDrops(callbackDrops, removalDrops)); } /** - * 检查掉落物列表是否均为无效 + * 清理本次破坏新生成的经验球。 + * + *

经验由调用方按 {@code getExpDrop} 自行结算,这些经验球若留在世界上会与结算值重复, + * 且会被下一次差集收集误计为掉落。

*/ - public static boolean hasNoValidDrops(List drops) { - return drops.isEmpty() || drops.stream().allMatch(stack -> stack.isEmpty() || stack.getItem() == Items.AIR); + private static void discardNewExperience(ServerLevel level, AABB area, Set experienceBefore) { + for (ExperienceOrb experienceOrb : level.getEntitiesOfClass(ExperienceOrb.class, area)) { + if (!experienceBefore.contains(experienceOrb.getUUID())) { + experienceOrb.discard(); + } + } } /** - * 通过方块自身的破坏回调破坏方块,并收集本次新生成的掉落实体 + * 收集区域内自 {@code before} 之后新生成的 ItemEntity,取走其内容并移除实体。 + * + * @param consumed 跨阶段累积的已收走实体 id,避免同一实体被两个阶段重复计入 + * @return 本次新收走的掉落物列表 */ - public static List destroyBlockAndCollectDrops(ServerLevel level, BlockPos pos, BlockState state, - Player player, ItemStack tool) { - AABB area = new AABB(pos).inflate(1.0); - Set before = level.getEntitiesOfClass(ItemEntity.class, area) - .stream() - .map(Entity::getUUID) - .collect(Collectors.toSet()); - - destroyBlockWithoutDrops(level, pos, state, player, tool); - + private static List collectNewDrops(ServerLevel level, AABB area, Set before, + Set consumed) { List drops = new ArrayList<>(); - level.getEntitiesOfClass(ItemEntity.class, area).stream() - .filter(entity -> !before.contains(entity.getUUID())) - .forEach(entity -> { - ItemStack drop = entity.getItem().copy(); - if (!drop.isEmpty()) { - drops.add(drop); - } - entity.discard(); - }); + for (ItemEntity entity : level.getEntitiesOfClass(ItemEntity.class, area)) { + UUID id = entity.getUUID(); + if (before.contains(id) || !consumed.add(id)) { + continue; + } + ItemStack drop = entity.getItem().copy(); + if (!drop.isEmpty()) { + drops.add(drop); + } + entity.discard(); + } return drops; } /** - * 执行方块破坏回调并移除方块(不产生掉落物) - */ - public static void destroyBlockWithoutDrops(ServerLevel level, BlockPos pos, BlockState state, - Player player, ItemStack tool) { - BlockEntity be = level.getBlockEntity(pos); - state.getBlock().playerWillDestroy(level, pos, state, player); - state.getBlock().playerDestroy(level, player, pos, state, be, tool); - level.removeBlock(pos, false); - } - - // ==================== 掉落物处理 ==================== - - /** - * 合并相同物品的堆叠 + * 合并回调阶段与移除阶段的掉落,剔除移除阶段中与回调阶段完全重复的条目。 + * + *

重复来源:方块同时重写 {@code getDrops} 并返回内容物、又在 {@code onRemove} 中掉落内容物时, + * 同一批物品被两条路径各生成一次。仅当物品、组件与数量三者同时一致时才判定为重复, + * 且每个基准条目至多抵消一个候选条目。

*/ - public static List mergeItemStacks(List items) { - List merged = new ArrayList<>(); - for (ItemStack item : items) { - if (item.isEmpty()) continue; - - boolean mergedFlag = false; - for (ItemStack mergedItem : merged) { - if (ItemStack.isSameItemSameTags(item, mergedItem)) { - int remaining = mergedItem.getMaxStackSize() - mergedItem.getCount(); - if (remaining > 0) { - int addCount = Math.min(remaining, item.getCount()); - mergedItem.grow(addCount); - item.shrink(addCount); - if (item.isEmpty()) { - mergedFlag = true; - break; - } - } - } + private static List mergePhaseDrops(List callbackDrops, List removalDrops) { + List remaining = new ArrayList<>(callbackDrops); + List merged = new ArrayList<>(callbackDrops); + for (ItemStack candidate : removalDrops) { + int matched = indexOfSameStack(remaining, candidate); + if (matched >= 0) { + remaining.remove(matched); + continue; } + merged.add(candidate); + } + return merged; + } - if (!mergedFlag && !item.isEmpty()) { - merged.add(item.copy()); + /** 在池中查找与候选完全一致的条目(物品、组件、数量均相同);未命中返回 -1。 */ + private static int indexOfSameStack(List pool, ItemStack candidate) { + for (int i = 0; i < pool.size(); i++) { + ItemStack existing = pool.get(i); + if (existing.getCount() == candidate.getCount() + && ItemStack.isSameItemSameTags(existing, candidate)) { + return i; } } - return merged; + return -1; } - /** - * 处理掉落物(添加到背包或掉落) - */ - public static void handleDrops(Player player, List drops) { - handleDrops(player, drops, player.getMainHandItem()); + /** 方块自身拒绝被移除:按类型去重上报,避免一次连锁挖掘产生大量重复日志。 */ + private static void reportRefusedRemoval(Block block, BlockPos pos, String callback, Throwable refusal) { + if (REPORTED_REFUSED_REMOVALS.add(block)) { + RateLimitedLogger.warn(LOGGER, "Block {} refused removal at {} ({} threw {}: {}); skipping it, chain mining continues", + BuiltInRegistries.BLOCK.getKey(block), pos, callback, + refusal.getClass().getSimpleName(), refusal.getMessage(), refusal); + } } /** - * 处理掉落物(添加到背包或掉落),支持AE2优先存储 + * 单个方块在连锁挖掘中出错时的保护性上报:异常拦截于本格,后续方块继续挖掘。 + * 同样是按方块类型去重。 */ - public static void handleDrops(Player player, List drops, ItemStack tool) { - for (ItemStack drop : drops) { - if (drop.isEmpty()) continue; - - // 剩余进入背包 - if (!player.getInventory().add(drop)) { - player.drop(drop, false); - } + static void reportBlockBreakFailure(BlockState state, BlockPos pos, Throwable failure) { + Block block = state.getBlock(); + if (REPORTED_BREAK_FAILURES.add(block)) { + RateLimitedLogger.warn(LOGGER, "Failed to break {} at {}; skipping it, chain mining continues", + BuiltInRegistries.BLOCK.getKey(block), pos, failure); } } - // ==================== 工具方法 ==================== - /** * 获取玩家指向的方块位置 + * + * @param player 玩家 + * @return 方块位置,如果没有指向方块则返回null */ - public static BlockPos getTargetBlockPos(Player player) { - double reach = 4.5D; + static BlockPos getTargetBlockPos(Player player) { + double reach = player.getBlockReach(); HitResult hitResult = player.pick(reach, 0.0f, false); if (hitResult.getType() == HitResult.Type.BLOCK) { @@ -312,74 +926,25 @@ public static BlockPos getTargetBlockPos(Player player) { } /** - * 检查工具是否处于精准采集模式(通过NBT) - */ - public static boolean isSilkTouchMode(ItemStack stack) { - if (stack.hasTag()) { - return stack.getTag().getBoolean("SilkTouchMode"); - } - return false; - } - - /** - * 检查工具是否处于增强连锁模式(通过NBT) - */ - public static boolean isEnhancedChainMiningMode(ItemStack stack) { - if (stack.hasTag()) { - return stack.getTag().getBoolean("EnhancedChainMining"); - } - return false; - } - - /** - * 检查是否启用了连锁挖掘(Tab按下状态) - */ - public static boolean isChainMiningPressed(ItemStack stack) { - if (stack.hasTag()) { - net.minecraft.nbt.CompoundTag tag = stack.getTag(); - if (tag.contains("ChainMiningPressed")) { - return tag.getBoolean("ChainMiningPressed"); - } - // 兼容旧版:检查ToolModes - if (tag.contains("ToolModes")) { - net.minecraft.nbt.CompoundTag toolModes = tag.getCompound("ToolModes"); - if (toolModes.contains("CHAIN_MINING")) { - return toolModes.getBoolean("CHAIN_MINING"); - } - } - } - return false; - } - - /** - * 检查是否启用了强制挖掘模式(通过ModeManager NBT) + * 获取形状推进所依据的面。 + * + *

隧道类形状按「该面的反方向」推进,因此命中方块时取点击面,其反方向即远离玩家、 + * 指向方块内部的方向,与玩家对连锁走向的预期一致。 + * + *

未命中任何方块时没有真实点击面可用,此时退化为玩家朝向的反方向:该方向的反方向 + * 恰为玩家朝向,使推进仍沿视线向前。若直接返回玩家朝向,推进方向会反转成玩家背后, + * 与视线方向相反。 + * + * @param player 玩家 + * @return 命中面,未命中时为玩家朝向的反方向 */ - public static boolean isForceMiningMode(ItemStack stack) { - if (stack.hasTag()) { - net.minecraft.nbt.CompoundTag tag = stack.getTag(); - if (tag.contains("ToolModes")) { - net.minecraft.nbt.CompoundTag toolModes = tag.getCompound("ToolModes"); - return toolModes.getBoolean("FORCE_MINING"); - } - } - return false; - } + static Direction getTargetFace(Player player) { + double reach = player.getBlockReach(); + HitResult hitResult = player.pick(reach, 0.0f, false); - /** - * 快速破坏方块(Shift+右键快速破坏),掉落物直接进背包 - */ - public static void quickBreakBlock(Level world, BlockPos pos, BlockState state, Player player, ItemStack tool) { - if (world.isClientSide()) { - world.playSound(player, pos, state.getSoundType().getBreakSound(), SoundSource.BLOCKS, 0.7F, 1.0F); - return; + if (hitResult.getType() == HitResult.Type.BLOCK) { + return ((BlockHitResult) hitResult).getDirection(); } - - ServerLevel serverLevel = (ServerLevel) world; - BlockEntity blockEntity = world.getBlockEntity(pos); - - List drops = Block.getDrops(state, serverLevel, pos, blockEntity, player, tool); - handleDrops(player, drops, tool); - - world.destroyBlock(pos, false, player); + return player.getDirection().getOpposite(); } } diff --git a/src/main/java/com/sorrowmist/useless/utils/mining/PlayerMiningData.java b/src/main/java/com/sorrowmist/useless/utils/mining/PlayerMiningData.java deleted file mode 100644 index 05769b98..00000000 --- a/src/main/java/com/sorrowmist/useless/utils/mining/PlayerMiningData.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.sorrowmist.useless.utils.mining; - -import net.minecraft.core.BlockPos; - -import java.util.Collections; -import java.util.List; -import java.util.UUID; - -public class PlayerMiningData { - private final UUID playerId; - private boolean tabPressed = false; - private BlockPos cachedPos = null; - private List cachedBlocks = Collections.emptyList(); - - public PlayerMiningData(UUID playerId) { - this.playerId = playerId; - } - - public UUID getPlayerId() { - return this.playerId; - } - - public boolean isTabPressed() { - return this.tabPressed; - } - - public void setTabPressed(boolean tabPressed) { - this.tabPressed = tabPressed; - } - - public BlockPos getCachedPos() { - return this.cachedPos; - } - - public void setCachedPos(BlockPos cachedPos) { - this.cachedPos = cachedPos; - } - - public List getCachedBlocks() { - return this.cachedBlocks; - } - - public void setCachedBlocks(List cachedBlocks) { - this.cachedBlocks = cachedBlocks != null ? cachedBlocks : Collections.emptyList(); - } - - public boolean hasCachedBlocks() { - return !this.cachedBlocks.isEmpty(); - } - - public void clearCache() { - this.cachedPos = null; - this.cachedBlocks = Collections.emptyList(); - } -} diff --git a/src/main/java/com/sorrowmist/useless/utils/mining/RightClickChainer.java b/src/main/java/com/sorrowmist/useless/utils/mining/RightClickChainer.java new file mode 100644 index 00000000..a821c644 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/utils/mining/RightClickChainer.java @@ -0,0 +1,207 @@ +package com.sorrowmist.useless.utils.mining; + +import com.sorrowmist.useless.content.items.BeefCropHarvest; +import com.sorrowmist.useless.content.items.BeefRipen; +import com.sorrowmist.useless.content.items.EndlessBeafItem; +import com.sorrowmist.useless.data.PlayerMiningData; +import com.sorrowmist.useless.utils.UComponentUtils; +import com.sorrowmist.useless.utils.mining.shape.ChainMiningShapes; +import net.minecraft.advancements.CriteriaTriggers; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; +import net.minecraftforge.common.ToolAction; + +import java.util.ArrayList; +import java.util.List; + +/** + * 右键连锁:按住连锁键(Tab,与连锁挖掘同一个触发键)时,把造化杖自己的方块右键操作 + * ——土壤(锄头耕地 / 铲子铺路)、斧头(剥皮 / 刮铜 / 去蜡)、顺手收菜,以及催熟—— + * 按连锁挖掘那套「等价组 + X/Y/Z 范围 + 数量上限」整片执行。 + * + *

只覆盖工具自身的方块操作:扳手等其它方块交互、放置方块都不在此列。 + * 潜行时不连锁(只作用一块),方便把这次右键精确地用在单个方块上。 + * + *

本类放在 {@code utils.mining} 是因为要复用包内可见的 {@link MiningUtils#scanBlocksForUse}。 + */ +public final class RightClickChainer { + + private RightClickChainer() { + } + + /** + * 本次右键是否应该走连锁。 + * + *

判定条件:手持造化杖 + 按住连锁键 + 未潜行。Tab 状态由服务端维护并同步到客户端, + * 因此两端判定一致(客户端同样按连锁整片预测,不会闪烁)。 + * + *

连点模式触发的右键走的也是同一条 {@code useOn} 路径,所以按住连锁键时 + * 连点同样会连锁,不需要额外处理。 + */ + public static boolean shouldChain(Player player, ItemStack stack) { + if (player == null || player.isShiftKeyDown()) { + return false; + } + if (!(stack.getItem() instanceof EndlessBeafItem)) { + return false; + } + PlayerMiningData data = MiningDispatcher.getPlayerData(player); + return data != null && data.isTabPressed(); + } + + /** + * 把同一个工具动作作用到连锁范围内的所有同类方块。 + * + *

范围内不接受该动作的方块会被跳过(等价组可能把「同类但形态不同」的方块也算进来, + * 例如原木与去皮原木),因此不会改动不应处理的方块。 + * + * @param ctx 原始右键上下文(方向、手、工具沿用原上下文) + * @param ability 要作用的工具动作 + * @param sound 生效时播放一次的音效(整片只响一次,避免叠成噪音) + * @param levelEvent 额外 LevelEvent(刮铜 / 去蜡的粒子),小于 0 表示没有 + * @return 至少作用到一块时返回成功,否则返回 PASS 交回单块逻辑 + */ + public static InteractionResult applyToolAction(UseOnContext ctx, ToolAction ability, + SoundEvent sound, int levelEvent) { + Level level = ctx.getLevel(); + BlockPos origin = ctx.getClickedPos(); + BlockState originState = level.getBlockState(origin); + + Player player = ctx.getPlayer(); + List targets = MiningUtils.scanBlocksForUse( + origin, originState, level, UComponentUtils.isEnhancedChainMiningEnabled(ctx.getItemInHand()), + chainShape(player), ctx.getClickedFace(), player); + + boolean soundPlayed = false; + int applied = 0; + for (BlockPos targetPos : targets) { + BlockState state = level.getBlockState(targetPos); + UseOnContext targetCtx = contextAt(ctx, targetPos); + BlockState modified = state.getToolModifiedState(targetCtx, ability, false); + if (modified == null) { + continue; + } + + if (!soundPlayed) { + // 服务端带 player 播放会自动跳过本人(本地已预测),与单块逻辑保持一致 + level.playSound(ctx.getPlayer(), origin, sound, SoundSource.BLOCKS, 1.0F, 1.0F); + soundPlayed = true; + } + if (levelEvent >= 0) { + level.levelEvent(ctx.getPlayer(), levelEvent, targetPos, 0); + } + + if (!level.isClientSide) { + level.setBlock(targetPos, modified, 11); + if (ctx.getPlayer() instanceof ServerPlayer serverPlayer) { + CriteriaTriggers.ITEM_USED_ON_BLOCK.trigger(serverPlayer, targetPos, ctx.getItemInHand()); + } + } + applied++; + } + + if (applied == 0) { + return InteractionResult.PASS; + } + return InteractionResult.sidedSuccess(level.isClientSide); + } + + /** + * 连锁收菜:收获连锁范围内所有成熟作物(种子照旧留在地里)。 + * + * @return 实际收获的作物数量 + */ + public static int harvestCrops(ServerLevel level, BlockPos origin, Player player, ItemStack tool) { + BlockState originState = level.getBlockState(origin); + List targets = MiningUtils.scanBlocksForUse( + origin, originState, level, UComponentUtils.isEnhancedChainMiningEnabled(tool), + chainShape(player), player.getDirection(), player); + + // 需要接管掉落(范围磁力或 AE 存储优先任一开启)时先整片收集、合并同类项, + // 最后只调一次 handleDrops,避免逐株向 AE 发高频请求。 + boolean collect = UComponentUtils.shouldCollectDrops(tool); + List collected = collect ? new ArrayList<>() : null; + + int harvested = 0; + for (BlockPos targetPos : targets) { + if (collect) { + if (BeefCropHarvest.harvestAndCollect(level, targetPos, player, tool, collected)) { + harvested++; + } + } else if (BeefCropHarvest.harvest(level, targetPos, player, tool)) { + harvested++; + } + } + + if (collect && collected != null && !collected.isEmpty()) { + MiningUtils.handleDrops(player, MiningUtils.mergeItemStacks(collected), tool, Vec3.atCenterOf(origin)); + } + return harvested; + } + + /** + * 连锁催熟:把催熟作用到连锁范围内所有可催熟方块(含原点)。 + * + *

与连锁收菜共用同一套扫描(等价组 + X/Y/Z 范围 + 数量上限)。等价组按方块种类 + * 匹配,因此不同生长阶段的同一作物会被一起催熟;范围内本来就长满、或不受骨粉影响的 + * 方块会被逐块跳过({@link BeefRipen#ripenAt} 内部判定)。 + * + *

音效由调用方整片只播一次,这里只负责逐块催熟与粒子。 + * + * @return 实际催动的方块数量 + */ + public static int ripenBlocks(ServerLevel level, BlockPos origin, Player player, ItemStack tool) { + BlockState originState = level.getBlockState(origin); + List targets = MiningUtils.scanBlocksForUse( + origin, originState, level, UComponentUtils.isEnhancedChainMiningEnabled(tool), + chainShape(player), player.getDirection(), player); + + int ripened = 0; + // 花的掉落物整片收集、末尾只提交一次(与 harvestCrops 同款写法), + // 避免逐株向 AE 发上千次请求。 + List collected = new ArrayList<>(); + for (BlockPos targetPos : targets) { + if (BeefRipen.ripenAt(level, targetPos, player, tool, collected)) { + ripened++; + } + } + if (!collected.isEmpty()) { + MiningUtils.handleDrops(player, MiningUtils.mergeItemStacks(collected), tool, Vec3.atCenterOf(origin)); + } + return ripened; + } + + /** + * 取玩家当前选中的连锁形状,供右键连锁与挖掘共用同一套布局。 + * + *

形状保存在玩家挖矿数据中;数据缺失(例如右键路径尚未经过 Tab 同步)时回退到默认形状, + * 保证扫描入口不会因空引用而退化为单块处理。 + * + * @param player 玩家,允许为 null + * @return 当前形状,不可用时为默认形状 + */ + private static ChainMiningShapes chainShape(Player player) { + if (player == null) { + return ChainMiningShapes.SHAPELESS; + } + PlayerMiningData data = MiningDispatcher.getPlayerData(player); + return data != null ? data.getShape() : ChainMiningShapes.SHAPELESS; + } + + /** 以原上下文为准,为连锁中的另一个方块构造右键上下文(沿用同一朝向与手)。 */ + private static UseOnContext contextAt(UseOnContext origin, BlockPos pos) { + BlockHitResult hit = new BlockHitResult(Vec3.atCenterOf(pos), origin.getClickedFace(), pos, false); + return new UseOnContext(origin.getPlayer(), origin.getHand(), hit); + } +} diff --git a/src/main/java/com/sorrowmist/useless/utils/mining/shape/ChainMiningShape.java b/src/main/java/com/sorrowmist/useless/utils/mining/shape/ChainMiningShape.java new file mode 100644 index 00000000..bfdd3b41 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/utils/mining/shape/ChainMiningShape.java @@ -0,0 +1,33 @@ +package com.sorrowmist.useless.utils.mining.shape; + +import net.minecraft.core.BlockPos; +import net.minecraft.resources.ResourceLocation; + +import java.util.List; + +/** + * 连锁挖掘形状。 + * + *

形状负责在给定上下文下生成本次连锁的候选方块序列,序列顺序即挖掘顺序。 + * 形状只决定布局,某个候选方块是否真正被破坏仍由 + * {@link ChainMiningShapeContext#check(BlockPos)} 中的等价组判定与工具可采集判定共同决定。 + * + *

形状的标识用于物品组件持久化,因此必须保持稳定,不得随枚举顺序调整而改变。 + * + *

该形状抽象层的划分参考 FTB Ultimine(FTB 连锁)的挖掘形状设计。 + */ +public interface ChainMiningShape { + + /** + * {@return 该形状的稳定标识} + */ + ResourceLocation getId(); + + /** + * 计算本次连锁的候选方块序列。 + * + * @param context 形状上下文 + * @return 候选方块,按预期挖掘顺序排列 + */ + List getBlocks(ChainMiningShapeContext context); +} diff --git a/src/main/java/com/sorrowmist/useless/utils/mining/shape/ChainMiningShapeContext.java b/src/main/java/com/sorrowmist/useless/utils/mining/shape/ChainMiningShapeContext.java new file mode 100644 index 00000000..80970150 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/utils/mining/shape/ChainMiningShapeContext.java @@ -0,0 +1,74 @@ +package com.sorrowmist.useless.utils.mining.shape; + +import com.sorrowmist.useless.core.config.ChainEquivalence; +import com.sorrowmist.useless.utils.mining.MiningUtils; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +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.state.BlockState; + +/** + * 形状计算所需的上下文。 + * + *

由连锁扫描在构造形状之前一次性装配:等价组、范围半径、数量上限与工具语义均在此固化, + * 形状实现只需按布局枚举坐标并调用 {@link #check(BlockPos)} 过滤。 + * + * @param level 世界 + * @param originPos 连锁原点方块位置 + * @param originState 原点方块状态 + * @param face 玩家点击的面;无有效命中时退化为玩家朝向 + * @param player 触发连锁的玩家 + * @param tool 主手工具 + * @param equivalence 等价组判定,与后续破坏阶段的二次校验必须为同一实例 + * @param forceMining 是否为强制挖掘语义 + * @param requireMineable 是否需要「工具可采集」门槛(挖矿语义为真,右键语义为假) + * @param maxBlocks 本次连锁的方块数量上限 + * @param rangeX X 轴范围半径 + * @param rangeY Y 轴范围半径 + * @param rangeZ Z 轴范围半径 + */ +public record ChainMiningShapeContext(Level level, BlockPos originPos, BlockState originState, Direction face, + Player player, ItemStack tool, ChainEquivalence equivalence, + boolean forceMining, boolean requireMineable, int maxBlocks, + int rangeX, int rangeY, int rangeZ) { + + /** + * 判断坐标是否位于配置的连锁范围包围盒内。 + * + *

形状自身不重复实现边界检查,越界一律视为不可用,避免长隧道形状在 + * 极端配置下挖穿到数量上限。 + * + * @param pos 待判定坐标 + * @return 位于范围内返回 true + */ + public boolean withinRange(BlockPos pos) { + return Math.abs(pos.getX() - this.originPos.getX()) <= this.rangeX + && Math.abs(pos.getY() - this.originPos.getY()) <= this.rangeY + && Math.abs(pos.getZ() - this.originPos.getZ()) <= this.rangeZ; + } + + /** + * 判断坐标是否可作为本次连锁的目标。 + * + *

依次校验范围、等价组、工具可采集与强制挖掘黑名单。该判定必须与破坏阶段的二次校验 + * 使用同一等价组实例,否则等价组扩出的方块会在破坏阶段被整体跳过。 + * + * @param pos 待判定坐标 + * @return 可作为目标返回 true + */ + public boolean check(BlockPos pos) { + if (!withinRange(pos)) { + return false; + } + BlockState state = this.level.getBlockState(pos); + if (!this.equivalence.matches(state)) { + return false; + } + if (this.requireMineable && !MiningUtils.canMineBlock(state, this.tool, this.forceMining)) { + return false; + } + return !(this.forceMining && MiningUtils.isForceMiningBlacklisted(state)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/utils/mining/shape/ChainMiningShapes.java b/src/main/java/com/sorrowmist/useless/utils/mining/shape/ChainMiningShapes.java new file mode 100644 index 00000000..b83cfefa --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/utils/mining/shape/ChainMiningShapes.java @@ -0,0 +1,239 @@ +package com.sorrowmist.useless.utils.mining.shape; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.resources.ResourceLocation; + +import java.util.ArrayList; +import java.util.List; + +/** + * 内置连锁形状集合。 + * + *

枚举顺序即滚轮循环顺序,首项为默认值。 + * + *

{@link #SHAPELESS} 不提供固定布局:它在服务端沿用既有的相邻扩散扫描 + * (普通连锁按 26 邻域逐格扩展,增强连锁按范围全量收集后按距离截断), + * 因此该形状与历史行为完全一致。其余形状给出确定性布局,与增强连锁开关无关。 + * + *

形状的种类划分与各形状的布局语义参考 FTB Ultimine(FTB 连锁)的挖掘形状设计, + * 形状标识沿用其命名以便与上游语义对照;本文件为独立实现。 + */ +public enum ChainMiningShapes implements ChainMiningShape { + + /** + * 默认形状:布局由被破坏方块与相邻方块共同决定,使用既有扫描逻辑。 + */ + SHAPELESS("shapeless") { + @Override + public List getBlocks(ChainMiningShapeContext context) { + return List.of(); + } + + @Override + public boolean usesBuiltinScan() { + return true; + } + }, + + /** + * 沿视线方向推进的单格直线隧道。 + */ + SMALL_TUNNEL("small_tunnel") { + @Override + public List getBlocks(ChainMiningShapeContext context) { + List list = new ArrayList<>(context.maxBlocks()); + for (int i = 0; i < context.maxBlocks(); i++) { + BlockPos pos = context.originPos().relative(context.face(), -i); + if (!context.check(pos)) { + break; + } + list.add(pos); + } + return list; + } + }, + + /** + * 单层 3×3。 + */ + SMALL_SQUARE("small_square") { + @Override + public List getBlocks(ChainMiningShapeContext context) { + return tunnelLayers(context, 1); + } + }, + + /** + * 3×3 逐层向前推进的隧道。 + */ + LARGE_TUNNEL("large_tunnel") { + @Override + public List getBlocks(ChainMiningShapeContext context) { + return tunnelLayers(context, Integer.MAX_VALUE); + } + }, + + /** + * 对角隧道,逐层向下,用于向下开拓。 + */ + MINING_TUNNEL("mining_tunnel") { + @Override + public List getBlocks(ChainMiningShapeContext context) { + return diagonalTunnel(context, -1); + } + }, + + /** + * 对角隧道,逐层向上,用于向上逃生。 + */ + ESCAPE_TUNNEL("escape_tunnel") { + @Override + public List getBlocks(ChainMiningShapeContext context) { + return diagonalTunnel(context, 1); + } + }; + + private final ResourceLocation id; + + ChainMiningShapes(String path) { + this.id = new ResourceLocation("useless_mod", path); + } + + @Override + public ResourceLocation getId() { + return this.id; + } + + /** + * {@return true 表示该形状使用既有的相邻扩散扫描,{@link #getBlocks} 的返回值不被使用} + */ + public boolean usesBuiltinScan() { + return false; + } + + /** + * {@return 该形状的显示名翻译键} + */ + public String getTranslationKey() { + return "gui.useless_mod.shape." + this.id.getPath(); + } + + /** + * {@return 该形状的方向说明翻译键} + * + *

形状名只表达布局类型,不表达推进方向;隧道与对角类的实际走向取决于点击面与玩家朝向, + * 缺少该说明时玩家无法预判挖掘方向,因此显示名与方向说明成对出现。 + */ + public String getDescriptionKey() { + return "gui.useless_mod.shape." + this.id.getPath() + ".desc"; + } + + /** + * 按滚动方向循环取相邻形状。 + * + * @param delta 滚动步数,正数为向后一个形状 + * @return 切换后的形状 + */ + public ChainMiningShapes cycle(int delta) { + ChainMiningShapes[] values = values(); + int index = (ordinal() + delta) % values.length; + if (index < 0) { + index += values.length; + } + return values[index]; + } + + /** + * 按持久化标识解析形状。 + * + *

标识缺失或无法识别时回退到 {@link #SHAPELESS},保证旧存档中的物品仍可正常连锁。 + * + * @param id 形状标识,允许为 null + * @return 对应的形状,未命中时为默认形状 + */ + public static ChainMiningShapes byId(String id) { + if (id == null || id.isEmpty()) { + return SHAPELESS; + } + for (ChainMiningShapes shape : values()) { + if (shape.id.toString().equals(id) || shape.id.getPath().equals(id)) { + return shape; + } + } + return SHAPELESS; + } + + /** + * 生成 3×3 逐层推进的隧道。 + * + *

首层仅取 3×3 中除中心以外的八格,中心即原点;后续每一层整体前移一格。 + * 某一层没有任何格子可用时终止推进,避免在已挖空的区域继续向深处扩展。 + * + * @param context 形状上下文 + * @param maxDepth 推进层数上限 + * @return 候选方块序列 + */ + private static List tunnelLayers(ChainMiningShapeContext context, int maxDepth) { + List list = new ArrayList<>(); + BlockPos basePos = context.originPos(); + list.add(basePos); + int depth = 0; + + while (depth < maxDepth && list.size() < context.maxBlocks()) { + int size = list.size(); + layer: + for (int a = -1; a <= 1; a++) { + for (int b = -1; b <= 1; b++) { + if (depth > 0 || a != 0 || b != 0) { + BlockPos pos = switch (context.face().getAxis()) { + case X -> basePos.offset(0, a, b); + case Y -> basePos.offset(a, 0, b); + case Z -> basePos.offset(a, b, 0); + }; + if (context.check(pos)) { + list.add(pos); + if (list.size() >= context.maxBlocks()) { + break layer; + } + } + } + } + } + if (list.size() == size) { + break; + } + basePos = basePos.relative(context.face().getOpposite()); + depth++; + } + + return list; + } + + /** + * 生成沿水平朝向逐层升降的对角隧道。 + * + *

点击面为竖直面(顶面或底面)时没有可用的水平推进方向,此时改用玩家朝向的反方向作为 + * 推进基准,与既有连锁扫描中「准星朝上下时按视线方向展开」的处理保持一致。 + * + * @param context 形状上下文 + * @param yDirection 每推进一格的 Y 轴位移,负值向下、正值向上 + * @return 候选方块序列 + */ + private static List diagonalTunnel(ChainMiningShapeContext context, int yDirection) { + Direction face = context.face().getAxis().isVertical() + ? context.player().getDirection().getOpposite() + : context.face(); + + List list = new ArrayList<>(context.maxBlocks()); + for (int i = 0; i < context.maxBlocks(); i++) { + BlockPos pos = context.originPos().offset(-face.getStepX() * i, yDirection * i, -face.getStepZ() * i); + if (!context.check(pos)) { + break; + } + list.add(pos); + } + + return list; + } +} diff --git a/src/main/java/com/sorrowmist/useless/utils/pattern/AEPatternProviderHandler.java b/src/main/java/com/sorrowmist/useless/utils/pattern/AEPatternProviderHandler.java deleted file mode 100644 index f1a9726c..00000000 --- a/src/main/java/com/sorrowmist/useless/utils/pattern/AEPatternProviderHandler.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.sorrowmist.useless.utils.pattern; - -import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; -import net.minecraft.world.level.LevelAccessor; -import net.minecraft.world.level.block.entity.BlockEntity; -import net.minecraft.world.item.ItemStack; - -/** - * AE2普通样板供应器处理器 - */ -public class AEPatternProviderHandler implements PatternProviderHandler { - @Override - public String getProviderType() { - return "AECraftingPatternProvider"; - } - - @Override - public boolean isCompatible(BlockEntity blockEntity) { - return blockEntity instanceof appeng.blockentity.crafting.PatternProviderBlockEntity; - } - - @Override - public void clearPatterns(LevelAccessor levelAccessor, BlockPos pos) { - // 这个方法在PatternProviderManager中已经实现,这里不需要重复实现 - } - - @Override - public void syncPatterns(BlockPos masterPos, BlockPos slavePos, Direction direction, LevelAccessor levelAccessor) { - // 这个方法在PatternProviderManager中已经实现,这里不需要重复实现 - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/utils/pattern/AdvPatternProviderHandler.java b/src/main/java/com/sorrowmist/useless/utils/pattern/AdvPatternProviderHandler.java deleted file mode 100644 index 15f5c2e3..00000000 --- a/src/main/java/com/sorrowmist/useless/utils/pattern/AdvPatternProviderHandler.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.sorrowmist.useless.utils.pattern; - -import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; -import net.minecraft.world.level.LevelAccessor; -import net.minecraft.world.level.block.entity.BlockEntity; -import net.minecraft.world.item.ItemStack; - -/** - * 高级AE的高级样板供应器处理器 - */ -public class AdvPatternProviderHandler implements PatternProviderHandler { - @Override - public String getProviderType() { - return "AAEAdvPatternProvider"; - } - - @Override - public boolean isCompatible(BlockEntity blockEntity) { - return blockEntity != null && - (blockEntity.getClass().getName().equals("net.pedroksl.advanced_ae.common.entities.AdvPatternProviderEntity") || - blockEntity.getClass().getName().equals("net.pedroksl.advanced_ae.common.entities.SmallAdvPatternProviderEntity")); - } - - @Override - public void clearPatterns(LevelAccessor levelAccessor, BlockPos pos) { - // 这个方法在PatternProviderManager中已经实现,这里不需要重复实现 - } - - @Override - public void syncPatterns(BlockPos masterPos, BlockPos slavePos, Direction direction, LevelAccessor levelAccessor) { - // 这个方法在PatternProviderManager中已经实现,这里不需要重复实现 - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/utils/pattern/ExPatternProviderHandler.java b/src/main/java/com/sorrowmist/useless/utils/pattern/ExPatternProviderHandler.java deleted file mode 100644 index 99c493ca..00000000 --- a/src/main/java/com/sorrowmist/useless/utils/pattern/ExPatternProviderHandler.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.sorrowmist.useless.utils.pattern; - -import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; -import net.minecraft.world.level.LevelAccessor; -import net.minecraft.world.level.block.entity.BlockEntity; -import net.minecraft.world.item.ItemStack; - -/** - * ExtendedAE扩展样板供应器处理器 - */ -public class ExPatternProviderHandler implements PatternProviderHandler { - @Override - public String getProviderType() { - return "ExPatternProvider"; - } - - @Override - public boolean isCompatible(BlockEntity blockEntity) { - return blockEntity != null && blockEntity.getClass().getName().equals("com.glodblock.github.extendedae.common.tileentities.TileExPatternProvider"); - } - - @Override - public void clearPatterns(LevelAccessor levelAccessor, BlockPos pos) { - // 这个方法在PatternProviderManager中已经实现,这里不需要重复实现 - } - - @Override - public void syncPatterns(BlockPos masterPos, BlockPos slavePos, Direction direction, LevelAccessor levelAccessor) { - // 这个方法在PatternProviderManager中已经实现,这里不需要重复实现 - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/utils/pattern/PatternProviderEvent.java b/src/main/java/com/sorrowmist/useless/utils/pattern/PatternProviderEvent.java deleted file mode 100644 index 86856da3..00000000 --- a/src/main/java/com/sorrowmist/useless/utils/pattern/PatternProviderEvent.java +++ /dev/null @@ -1,194 +0,0 @@ -package com.sorrowmist.useless.utils.pattern; - -import com.sorrowmist.useless.UselessMod; -import net.minecraft.core.BlockPos; -import net.minecraft.world.level.LevelAccessor; -import net.minecraft.world.level.block.entity.BlockEntity; -import net.minecraftforge.event.level.BlockEvent; - -/** - * 样板供应器事件处理类,处理方块掉落、清理等事件 - */ -public class PatternProviderEvent { - /** - * 监听方块掉落事件,防止从端样板供应器掉落样板 - */ - public static void onBlockDrops(BlockEvent.BreakEvent event) { - LevelAccessor levelAccessor = event.getLevel(); - BlockPos pos = event.getPos(); - - // 检查是否有任何从端位于该方块位置 - for (com.sorrowmist.useless.utils.pattern.PatternProviderKey slaveKey : PatternProviderManager.getSlaveToMaster().keySet()) { - if (slaveKey.getPos().equals(pos)) { - // 从端被破坏,确保内部样板不会掉落 - BlockEntity blockEntity = levelAccessor.getBlockEntity(pos); - if (blockEntity != null) { - // 使用反射和条件类加载来处理不同类型的样板供应器 - try { - // 处理AE2普通样板供应器 - if (blockEntity instanceof appeng.helpers.patternprovider.PatternProviderLogicHost slaveHost) { - // 再次确认清空样板,防止任何可能的掉落 - var slaveLogic = slaveHost.getLogic(); - var slaveInv = slaveLogic.getPatternInv(); - slaveInv.clear(); - slaveLogic.updatePatterns(); - } - // 处理高级AE样板供应器 - else if (PatternProviderManager.checkIfClassImplementsInterface(blockEntity, "net.pedroksl.advanced_ae.common.logic.AdvPatternProviderLogicHost")) { - // 使用反射获取高级AE样板供应器的样板库存并清空 - Object patternContainer = blockEntity; - Class patternContainerClass = Class.forName("appeng.helpers.patternprovider.PatternContainer"); - java.lang.reflect.Method getTerminalPatternInventoryMethod = patternContainerClass.getMethod("getTerminalPatternInventory"); - Object slaveInv = getTerminalPatternInventoryMethod.invoke(patternContainer); - - if (slaveInv instanceof appeng.api.inventories.InternalInventory inv) { - inv.clear(); - } - } - // 处理面板形式 - else if (blockEntity instanceof appeng.api.parts.IPartHost slavePartHost) { - // 检查指定方向的部件,再次确认清空样板 - appeng.api.parts.IPart sidePart = slavePartHost.getPart(slaveKey.getDirection()); - if (sidePart != null) { - // 处理AE2普通样板供应器面板 - if (PatternProviderManager.checkIfClassImplementsInterface(sidePart, "appeng.helpers.patternprovider.PatternProviderLogicHost")) { - // 使用反射调用方法 - Object hostPart = sidePart; - Class hostClass = Class.forName("appeng.helpers.patternprovider.PatternProviderLogicHost"); - java.lang.reflect.Method getLogicMethod = hostClass.getMethod("getLogic"); - Object slaveLogic = getLogicMethod.invoke(hostPart); - - Class logicClass = Class.forName("appeng.helpers.patternprovider.PatternProviderLogic"); - java.lang.reflect.Method getPatternInvMethod = logicClass.getMethod("getPatternInv"); - Object slaveInv = getPatternInvMethod.invoke(slaveLogic); - - if (slaveInv instanceof appeng.api.inventories.InternalInventory inv) { - inv.clear(); - } - - java.lang.reflect.Method updatePatternsMethod = logicClass.getMethod("updatePatterns"); - updatePatternsMethod.invoke(slaveLogic); - } - } - // 处理高级AE样板供应器面板 - else if (PatternProviderManager.checkIfClassImplementsInterface(sidePart, "net.pedroksl.advanced_ae.common.logic.AdvPatternProviderLogicHost")) { - // 使用反射获取高级AE样板供应器的样板库存并清空 - Object patternContainer = sidePart; - Class patternContainerClass = Class.forName("appeng.helpers.patternprovider.PatternContainer"); - java.lang.reflect.Method getTerminalPatternInventoryMethod = patternContainerClass.getMethod("getTerminalPatternInventory"); - Object slaveInv = getTerminalPatternInventoryMethod.invoke(patternContainer); - - if (slaveInv instanceof appeng.api.inventories.InternalInventory inv) { - inv.clear(); - } - } - } - } catch (Exception e) { - // 忽略任何异常,确保不会因为可选模组缺失而崩溃 - } - } - break; - } - } - } - - /** - * 清除从方块的样板 - */ - public static void clearSlavePatterns(LevelAccessor levelAccessor, com.sorrowmist.useless.utils.pattern.PatternProviderKey slaveKey) { - BlockEntity slaveBlockEntity = levelAccessor.getBlockEntity(slaveKey.getPos()); - if (slaveBlockEntity == null) return; - - // 使用反射和条件类加载来处理不同类型的样板供应器 - try { - // 处理直接放置的样板供应器 - if (slaveBlockEntity instanceof appeng.helpers.patternprovider.PatternProviderLogicHost slaveHost) { - // 获取从端的pattern inventory并清空 - var slaveLogic = slaveHost.getLogic(); - var slaveInv = slaveLogic.getPatternInv(); - slaveInv.clear(); - } - // 处理高级AE样板供应器 - else if (PatternProviderManager.checkIfClassImplementsInterface(slaveBlockEntity, "net.pedroksl.advanced_ae.common.logic.AdvPatternProviderLogicHost")) { - // 使用反射获取高级AE样板供应器的样板库存并清空 - Object patternContainer = slaveBlockEntity; - Class patternContainerClass = Class.forName("appeng.helpers.patternprovider.PatternContainer"); - java.lang.reflect.Method getTerminalPatternInventoryMethod = patternContainerClass.getMethod("getTerminalPatternInventory"); - Object slaveInv = getTerminalPatternInventoryMethod.invoke(patternContainer); - - if (slaveInv instanceof appeng.api.inventories.InternalInventory inv) { - inv.clear(); - } - } - // 处理面板形式的样板供应器 - else if (slaveBlockEntity instanceof appeng.api.parts.IPartHost partHost) { - // 对于面板形式,需要获取对应的部件并清空样板 - appeng.api.parts.IPart targetPart = partHost.getPart(slaveKey.getDirection()); - if (targetPart != null) { - if (PatternProviderManager.checkIfClassImplementsInterface(targetPart, "appeng.helpers.patternprovider.PatternProviderLogicHost")) { - // 使用反射调用方法 - Object hostPart = targetPart; - Class hostClass = Class.forName("appeng.helpers.patternprovider.PatternProviderLogicHost"); - java.lang.reflect.Method getLogicMethod = hostClass.getMethod("getLogic"); - Object slaveLogic = getLogicMethod.invoke(hostPart); - - Class logicClass = Class.forName("appeng.helpers.patternprovider.PatternProviderLogic"); - java.lang.reflect.Method getPatternInvMethod = logicClass.getMethod("getPatternInv"); - Object slaveInv = getPatternInvMethod.invoke(slaveLogic); - - if (slaveInv instanceof appeng.api.inventories.InternalInventory inv) { - inv.clear(); - } - } - // 处理高级AE样板供应器面板 - else if (PatternProviderManager.checkIfClassImplementsInterface(targetPart, "net.pedroksl.advanced_ae.common.logic.AdvPatternProviderLogicHost")) { - // 使用反射获取高级AE样板供应器的样板库存并清空 - Object patternContainer = targetPart; - Class patternContainerClass = Class.forName("appeng.helpers.patternprovider.PatternContainer"); - java.lang.reflect.Method getTerminalPatternInventoryMethod = patternContainerClass.getMethod("getTerminalPatternInventory"); - Object slaveInv = getTerminalPatternInventoryMethod.invoke(patternContainer); - - if (slaveInv instanceof appeng.api.inventories.InternalInventory inv) { - inv.clear(); - } - } - } - // 尝试中心部件 - else { - appeng.api.parts.IPart centerPart = partHost.getPart(null); - if (centerPart != null) { - if (PatternProviderManager.checkIfClassImplementsInterface(centerPart, "appeng.helpers.patternprovider.PatternProviderLogicHost")) { - // 使用反射调用方法 - Object hostPart = centerPart; - Class hostClass = Class.forName("appeng.helpers.patternprovider.PatternProviderLogicHost"); - java.lang.reflect.Method getLogicMethod = hostClass.getMethod("getLogic"); - Object slaveLogic = getLogicMethod.invoke(hostPart); - - Class logicClass = Class.forName("appeng.helpers.patternprovider.PatternProviderLogic"); - java.lang.reflect.Method getPatternInvMethod = logicClass.getMethod("getPatternInv"); - Object slaveInv = getPatternInvMethod.invoke(slaveLogic); - - if (slaveInv instanceof appeng.api.inventories.InternalInventory inv) { - inv.clear(); - } - } - // 处理高级AE样板供应器中心部件 - else if (PatternProviderManager.checkIfClassImplementsInterface(centerPart, "net.pedroksl.advanced_ae.common.logic.AdvPatternProviderLogicHost")) { - // 使用反射获取高级AE样板供应器的样板库存并清空 - Object patternContainer = centerPart; - Class patternContainerClass = Class.forName("appeng.helpers.patternprovider.PatternContainer"); - java.lang.reflect.Method getTerminalPatternInventoryMethod = patternContainerClass.getMethod("getTerminalPatternInventory"); - Object slaveInv = getTerminalPatternInventoryMethod.invoke(patternContainer); - - if (slaveInv instanceof appeng.api.inventories.InternalInventory inv) { - inv.clear(); - } - } - } - } - } - } catch (Exception e) { - // 忽略清空样板失败的异常 - } - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/utils/pattern/PatternProviderHandler.java b/src/main/java/com/sorrowmist/useless/utils/pattern/PatternProviderHandler.java deleted file mode 100644 index a66c23ab..00000000 --- a/src/main/java/com/sorrowmist/useless/utils/pattern/PatternProviderHandler.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.sorrowmist.useless.utils.pattern; - -import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; -import net.minecraft.world.level.LevelAccessor; -import net.minecraft.world.level.block.entity.BlockEntity; - -/** - * 样板供应器处理器接口,定义不同类型样板供应器的处理方法 - */ -public interface PatternProviderHandler { - /** - * 获取供应器类型 - */ - String getProviderType(); - - /** - * 检查是否与给定的方块实体兼容 - */ - boolean isCompatible(BlockEntity blockEntity); - - /** - * 清除指定位置的样板 - */ - void clearPatterns(LevelAccessor levelAccessor, BlockPos pos); - - /** - * 同步主从样板供应器的样板 - */ - void syncPatterns(BlockPos masterPos, BlockPos slavePos, Direction direction, LevelAccessor levelAccessor); -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/utils/pattern/PatternProviderHandlerRegistry.java b/src/main/java/com/sorrowmist/useless/utils/pattern/PatternProviderHandlerRegistry.java deleted file mode 100644 index d6c68a71..00000000 --- a/src/main/java/com/sorrowmist/useless/utils/pattern/PatternProviderHandlerRegistry.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.sorrowmist.useless.utils.pattern; - -import java.util.HashMap; -import java.util.Map; - -/** - * 样板供应器处理器注册表,用于管理不同类型的样板供应器处理器 - */ -public class PatternProviderHandlerRegistry { - private static final Map handlers = new HashMap<>(); - - // 静态初始化,注册所有处理器 - static { - registerHandler(new AEPatternProviderHandler()); - registerHandler(new ExPatternProviderHandler()); - registerHandler(new AdvPatternProviderHandler()); - } - - /** - * 注册样板供应器处理器 - */ - public static void registerHandler(PatternProviderHandler handler) { - handlers.put(handler.getProviderType(), handler); - } - - /** - * 根据类型获取样板供应器处理器 - */ - public static PatternProviderHandler getHandler(String type) { - return handlers.get(type); - } - - /** - * 获取所有注册的处理器 - */ - public static Map getAllHandlers() { - return new HashMap<>(handlers); - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/utils/pattern/PatternProviderKey.java b/src/main/java/com/sorrowmist/useless/utils/pattern/PatternProviderKey.java deleted file mode 100644 index dde5e2a4..00000000 --- a/src/main/java/com/sorrowmist/useless/utils/pattern/PatternProviderKey.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.sorrowmist.useless.utils.pattern; - -import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; - -import java.util.Objects; - -/** - * 用于表示扩展样板供应器的键,包含BlockPos和Direction - */ -public class PatternProviderKey { - private final BlockPos pos; - private final Direction direction; - - public PatternProviderKey(BlockPos pos, Direction direction) { - this.pos = pos; - this.direction = direction; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PatternProviderKey that = (PatternProviderKey) o; - return pos.equals(that.pos) && direction == that.direction; - } - - @Override - public int hashCode() { - return Objects.hash(pos, direction); - } - - public BlockPos getPos() { - return pos; - } - - public Direction getDirection() { - return direction; - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/utils/pattern/PatternProviderManager.java b/src/main/java/com/sorrowmist/useless/utils/pattern/PatternProviderManager.java deleted file mode 100644 index b1a4506f..00000000 --- a/src/main/java/com/sorrowmist/useless/utils/pattern/PatternProviderManager.java +++ /dev/null @@ -1,643 +0,0 @@ -package com.sorrowmist.useless.utils.pattern; - -import com.sorrowmist.useless.UselessMod; -import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.world.level.LevelAccessor; -import net.minecraft.world.level.block.entity.BlockEntity; - -import java.util.*; - -/** - * 样板供应器管理器,用于处理主从关系和同步逻辑 - */ -public class PatternProviderManager { - // 扩展样板供应器主从同步相关 - private static final Map> masterToSlaves = new HashMap<>(); - private static final Map slaveToMaster = new HashMap<>(); - private static final Map lastSyncTime = new HashMap<>(); - private static final long SYNC_INTERVAL = 1000; // 同步间隔,防止频繁同步 - private static final String SYNC_DATA_TAG = "PatternProviderSyncData"; - public static PatternProviderKey currentSelectedMaster = null; // 当前选择的主方块 - - /** - * 清理无效的主从关系 - */ - public static void cleanupInvalidLinks(LevelAccessor levelAccessor) { - List mastersToRemove = new ArrayList<>(); - List slavesToRemove = new ArrayList<>(); - - // 检查主方块是否存在 - if (!masterToSlaves.isEmpty()) { - for (PatternProviderKey key : masterToSlaves.keySet()) { - if (!isValidProvider(levelAccessor, key)) { - mastersToRemove.add(key); - } - } - } - - // 检查从方块是否存在 - if (!slaveToMaster.isEmpty()) { - for (PatternProviderKey key : slaveToMaster.keySet()) { - if (!isValidProvider(levelAccessor, key)) { - slavesToRemove.add(key); - } - } - } - - // 移除无效的主方块 - for (PatternProviderKey masterKey : mastersToRemove) { - Set slaves = masterToSlaves.remove(masterKey); - if (slaves != null) { - for (PatternProviderKey slaveKey : slaves) { - slaveToMaster.remove(slaveKey); - } - } - } - - // 移除无效的从方块 - for (PatternProviderKey slaveKey : slavesToRemove) { - PatternProviderKey masterKey = slaveToMaster.remove(slaveKey); - if (masterKey != null) { - Set slaves = masterToSlaves.get(masterKey); - if (slaves != null) { - slaves.remove(slaveKey); - if (slaves.isEmpty()) { - masterToSlaves.remove(masterKey); - } - } - } - } - } - - /** - * 检查样板供应器是否有效 - */ - private static boolean isValidProvider(LevelAccessor levelAccessor, PatternProviderKey key) { - BlockEntity blockEntity = levelAccessor.getBlockEntity(key.getPos()); - // 检查方块实体是否存在 - if (blockEntity == null) { - return false; - } - - // 检查方块实体类型是否为有效的样板供应器 - String className = blockEntity.getClass().getName(); - return blockEntity instanceof appeng.blockentity.crafting.PatternProviderBlockEntity || - className.equals("com.glodblock.github.extendedae.common.tileentities.TileExPatternProvider") || - className.equals("net.pedroksl.advanced_ae.common.entities.AdvPatternProviderEntity") || - className.equals("net.pedroksl.advanced_ae.common.entities.SmallAdvPatternProviderEntity"); - } - - /** - * 处理主方块被破坏的情况 - */ - public static void handleMasterBreak(LevelAccessor levelAccessor, PatternProviderKey masterKey) { - Set slaves = masterToSlaves.remove(masterKey); - if (slaves != null) { - for (PatternProviderKey slaveKey : slaves) { - slaveToMaster.remove(slaveKey); - clearSlavePatterns(levelAccessor, slaveKey); - } - } - } - - /** - * 处理从方块被破坏的情况 - */ - public static void handleSlaveBreak(LevelAccessor levelAccessor, PatternProviderKey slaveKey) { - PatternProviderKey masterKey = slaveToMaster.remove(slaveKey); - if (masterKey != null) { - Set slaves = masterToSlaves.get(masterKey); - if (slaves != null) { - slaves.remove(slaveKey); - if (slaves.isEmpty()) { - masterToSlaves.remove(masterKey); - } - } - } - } - - /** - * 清除从方块的样板 - */ - public static void clearSlavePatterns(LevelAccessor levelAccessor, PatternProviderKey slaveKey) { - // 这个方法将在实际需要时实现,目前暂时为空 - // 清除样板的逻辑会在具体的同步方法中实现 - } - - /** - * 保存同步数据到世界保存 - */ - public static void saveSyncData(ServerLevel serverLevel) { - PatternProviderSyncData syncData = serverLevel.getDataStorage().computeIfAbsent( - PatternProviderSyncData::load, - PatternProviderSyncData::new, - SYNC_DATA_TAG - ); - - // 保存当前的主从关系到SavedData - syncData.clear(); - syncData.getMasterToSlaves().putAll(masterToSlaves); - syncData.getSlaveToMaster().putAll(slaveToMaster); - - // 标记数据为已更改 - syncData.setDirty(); - } - - /** - * 从世界保存加载同步数据 - */ - public static void loadSyncData(ServerLevel serverLevel) { - PatternProviderSyncData syncData = serverLevel.getDataStorage().computeIfAbsent( - PatternProviderSyncData::load, - PatternProviderSyncData::new, - SYNC_DATA_TAG - ); - - // 加载主从关系 - masterToSlaves.clear(); - masterToSlaves.putAll(syncData.getMasterToSlaves()); - - slaveToMaster.clear(); - slaveToMaster.putAll(syncData.getSlaveToMaster()); - } - - /** - * 获取主从关系映射 - */ - public static Map> getMasterToSlaves() { - return masterToSlaves; - } - - /** - * 获取从主关系映射 - */ - public static Map getSlaveToMaster() { - return slaveToMaster; - } - - /** - * 获取同步间隔 - */ - public static long getSyncInterval() { - return SYNC_INTERVAL; - } - - /** - * 获取最后同步时间映射 - */ - public static Map getLastSyncTime() { - return lastSyncTime; - } - - /** - * 获取当前选中的主方块 - */ - public static PatternProviderKey getCurrentSelectedMaster() { - return currentSelectedMaster; - } - - /** - * 设置当前选中的主方块 - */ - public static void setCurrentSelectedMaster(PatternProviderKey masterKey) { - currentSelectedMaster = masterKey; - } - - /** - * 定期同步所有从样板供应器 - */ - public static void syncAllSlaves(net.minecraft.world.level.Level world) { - long currentTime = System.currentTimeMillis(); - - // 遍历所有主从关系 - for (Map.Entry> entry : masterToSlaves.entrySet()) { - PatternProviderKey masterKey = entry.getKey(); - Set slaves = entry.getValue(); - - if (slaves.isEmpty()) continue; - - // 同步到所有从方块 - for (PatternProviderKey slaveKey : slaves) { - // 检查同步间隔 - Long lastSync = lastSyncTime.get(slaveKey); - if (lastSync == null || currentTime - lastSync > SYNC_INTERVAL) { - // 直接调用syncPatternsFromMaster方法进行同步 - syncPatternsFromMaster(world, slaveKey, masterKey); - } - } - } - } - - /** - * 从主样板供应器同步样板到从样板供应器 - */ - public static void syncPatternsFromMaster(net.minecraft.world.level.Level world, PatternProviderKey slaveKey, PatternProviderKey masterKey) { - if (masterKey == null) return; - - // 检查同步间隔 - long currentTime = System.currentTimeMillis(); - Long lastSync = lastSyncTime.get(slaveKey); - if (lastSync != null && currentTime - lastSync < SYNC_INTERVAL) { - return; - } - - // 获取主方块的BlockEntity - net.minecraft.world.level.block.entity.BlockEntity masterBlockEntity = world.getBlockEntity(masterKey.getPos()); - if (masterBlockEntity == null) return; - - // 获取从方块的BlockEntity - net.minecraft.world.level.block.entity.BlockEntity slaveBlockEntity = world.getBlockEntity(slaveKey.getPos()); - if (slaveBlockEntity == null) return; - - // 使用反射和条件类加载来处理不同类型的样板供应器,避免直接引用可选模组的类 - try { - List masterPatterns = new ArrayList<>(); - - // 检查是否是高级AE样板供应器(AdvPatternProviderLogicHost) - boolean isAdvPatternProviderHost = checkIfClassImplementsInterface(masterBlockEntity, "net.pedroksl.advanced_ae.common.logic.AdvPatternProviderLogicHost"); - if (isAdvPatternProviderHost) { - // 使用反射获取高级AE样板供应器的样板库存 - Object patternContainer = masterBlockEntity; - Class patternContainerClass = Class.forName("appeng.helpers.patternprovider.PatternContainer"); - java.lang.reflect.Method getTerminalPatternInventoryMethod = patternContainerClass.getMethod("getTerminalPatternInventory"); - Object masterInv = getTerminalPatternInventoryMethod.invoke(patternContainer); - - // 遍历样板库存 - if (masterInv instanceof appeng.api.inventories.InternalInventory inv) { - for (int i = 0; i < inv.size(); i++) { - net.minecraft.world.item.ItemStack stack = inv.getStackInSlot(i); - if (!stack.isEmpty()) { - masterPatterns.add(stack.copy()); - } - } - } - } - // 检查是否是AE2普通样板供应器或其扩展(使用接口而非具体类) - else if (checkIfClassImplementsInterface(masterBlockEntity, "appeng.helpers.patternprovider.PatternProviderLogicHost")) { - // 使用反射调用getLogic()方法 - Object masterHost = masterBlockEntity; - Class hostClass = Class.forName("appeng.helpers.patternprovider.PatternProviderLogicHost"); - java.lang.reflect.Method getLogicMethod = hostClass.getMethod("getLogic"); - Object masterLogic = getLogicMethod.invoke(masterHost); - - // 获取样板 - Class logicClass = Class.forName("appeng.helpers.patternprovider.PatternProviderLogic"); - java.lang.reflect.Method getPatternInvMethod = logicClass.getMethod("getPatternInv"); - Object masterInv = getPatternInvMethod.invoke(masterLogic); - - // 遍历样板库存 - if (masterInv instanceof appeng.api.inventories.InternalInventory inv) { - for (int i = 0; i < inv.size(); i++) { - net.minecraft.world.item.ItemStack stack = inv.getStackInSlot(i); - if (!stack.isEmpty()) { - masterPatterns.add(stack.copy()); - } - } - } - } - // 检查是否是AE2普通样板供应器或其扩展的面板形式 - else if (masterBlockEntity instanceof appeng.api.parts.IPartHost masterPartHost) { - // 尝试从面板获取样板 - masterPatterns = getPatternsFromPartHost(masterPartHost, masterKey.getDirection()); - } - - // 无论主端是否有样板,都需要同步到从端 - // 当主端没有样板时,清空从端的样板 - // 从slaveKey中获取从端的方向,确保同步到正确的面板 - syncToSlavePatternProvider(slaveBlockEntity, slaveKey.getDirection(), masterPatterns); - } catch (Exception e) { - // 忽略所有异常,确保不会因为可选模组缺失而崩溃 - } - - // 更新同步时间 - lastSyncTime.put(slaveKey, currentTime); - } - - /** - * 检查类是否实现了指定接口 - */ - public static boolean checkIfClassImplementsInterface(Object obj, String interfaceName) { - try { - Class interfaceClass = Class.forName(interfaceName); - return interfaceClass.isInstance(obj); - } catch (ClassNotFoundException e) { - // 接口不存在,说明对应模组未安装 - return false; - } - } - - /** - * 从IPartHost获取样板 - */ - private static List getPatternsFromPartHost(appeng.api.parts.IPartHost partHost, net.minecraft.core.Direction direction) { - List patterns = new ArrayList<>(); - - try { - // 检查指定方向的部件 - appeng.api.parts.IPart targetPart = partHost.getPart(direction); - if (targetPart != null) { - patterns = getPatternsFromPart(targetPart); - return patterns; // 即使patterns为空,也要返回,确保正确传递空状态 - } - - // 检查中心部件 - appeng.api.parts.IPart centerPart = partHost.getPart(null); - if (centerPart != null) { - patterns = getPatternsFromPart(centerPart); - return patterns; // 即使patterns为空,也要返回,确保正确传递空状态 - } - - // 检查所有方向的部件 - for (net.minecraft.core.Direction dir : net.minecraft.core.Direction.values()) { - appeng.api.parts.IPart sidePart = partHost.getPart(dir); - if (sidePart != null) { - patterns = getPatternsFromPart(sidePart); - return patterns; // 即使patterns为空,也要返回,确保正确传递空状态 - } - } - } catch (Exception e) { - // 忽略异常 - } - - return patterns; - } - - /** - * 从IPart获取样板 - */ - private static List getPatternsFromPart(appeng.api.parts.IPart part) { - List patterns = new ArrayList<>(); - - try { - // 检查部件是否实现了PatternProviderLogicHost接口(AE2普通样板供应器) - if (checkIfClassImplementsInterface(part, "appeng.helpers.patternprovider.PatternProviderLogicHost")) { - // 使用反射获取样板 - Class hostClass = Class.forName("appeng.helpers.patternprovider.PatternProviderLogicHost"); - java.lang.reflect.Method getLogicMethod = hostClass.getMethod("getLogic"); - Object logic = getLogicMethod.invoke(part); - - Class logicClass = Class.forName("appeng.helpers.patternprovider.PatternProviderLogic"); - java.lang.reflect.Method getPatternInvMethod = logicClass.getMethod("getPatternInv"); - Object inv = getPatternInvMethod.invoke(logic); - - if (inv instanceof appeng.api.inventories.InternalInventory internalInv) { - for (int i = 0; i < internalInv.size(); i++) { - net.minecraft.world.item.ItemStack stack = internalInv.getStackInSlot(i); - if (!stack.isEmpty()) { - patterns.add(stack.copy()); - } - } - } - } - // 检查部件是否实现了AdvPatternProviderLogicHost接口(高级AE样板供应器) - else if (checkIfClassImplementsInterface(part, "net.pedroksl.advanced_ae.common.logic.AdvPatternProviderLogicHost")) { - // 使用反射获取高级AE样板供应器的样板库存 - Object patternContainer = part; - Class patternContainerClass = Class.forName("appeng.helpers.patternprovider.PatternContainer"); - java.lang.reflect.Method getTerminalPatternInventoryMethod = patternContainerClass.getMethod("getTerminalPatternInventory"); - Object masterInv = getTerminalPatternInventoryMethod.invoke(patternContainer); - - if (masterInv instanceof appeng.api.inventories.InternalInventory inv) { - for (int i = 0; i < inv.size(); i++) { - net.minecraft.world.item.ItemStack stack = inv.getStackInSlot(i); - if (!stack.isEmpty()) { - patterns.add(stack.copy()); - } - } - } - } - } catch (Exception e) { - // 忽略异常 - } - - return patterns; - } - - /** - * 同步样板到从端样板供应器 - */ - private static void syncToSlavePatternProvider(net.minecraft.world.level.block.entity.BlockEntity slaveBlockEntity, net.minecraft.core.Direction slaveDirection, List masterPatterns) { - try { - // 检查是否是高级AE样板供应器(AdvPatternProviderLogicHost) - boolean isAdvPatternProviderHost = checkIfClassImplementsInterface(slaveBlockEntity, "net.pedroksl.advanced_ae.common.logic.AdvPatternProviderLogicHost"); - if (isAdvPatternProviderHost) { - // 使用反射获取高级AE样板供应器的样板库存 - Object patternContainer = slaveBlockEntity; - Class patternContainerClass = Class.forName("appeng.helpers.patternprovider.PatternContainer"); - java.lang.reflect.Method getTerminalPatternInventoryMethod = patternContainerClass.getMethod("getTerminalPatternInventory"); - Object slaveInv = getTerminalPatternInventoryMethod.invoke(patternContainer); - - if (slaveInv instanceof appeng.api.inventories.InternalInventory internalInv) { - // 清空从方块的inventory - for (int i = 0; i < internalInv.size(); i++) { - internalInv.setItemDirect(i, net.minecraft.world.item.ItemStack.EMPTY); - } - - // 复制主方块的所有pattern到从方块 - for (int i = 0; i < masterPatterns.size() && i < internalInv.size(); i++) { - internalInv.setItemDirect(i, masterPatterns.get(i)); - } - - // 高级AE样板供应器不需要updatePatterns方法,直接更新inventory即可 - } - } - // 检查是否是PatternProviderLogicHost类型 - else if (checkIfClassImplementsInterface(slaveBlockEntity, "appeng.helpers.patternprovider.PatternProviderLogicHost")) { - // 使用反射获取逻辑并同步 - Class hostClass = Class.forName("appeng.helpers.patternprovider.PatternProviderLogicHost"); - java.lang.reflect.Method getLogicMethod = hostClass.getMethod("getLogic"); - Object slaveLogic = getLogicMethod.invoke(slaveBlockEntity); - - Class logicClass = Class.forName("appeng.helpers.patternprovider.PatternProviderLogic"); - java.lang.reflect.Method getPatternInvMethod = logicClass.getMethod("getPatternInv"); - Object slaveInv = getPatternInvMethod.invoke(slaveLogic); - - if (slaveInv instanceof appeng.api.inventories.InternalInventory internalInv) { - // 清空从方块的inventory - for (int i = 0; i < internalInv.size(); i++) { - internalInv.setItemDirect(i, net.minecraft.world.item.ItemStack.EMPTY); - } - - // 复制主方块的所有pattern到从方块 - for (int i = 0; i < masterPatterns.size() && i < internalInv.size(); i++) { - internalInv.setItemDirect(i, masterPatterns.get(i)); - } - - // 更新从方块的patterns - java.lang.reflect.Method updatePatternsMethod = logicClass.getMethod("updatePatterns"); - updatePatternsMethod.invoke(slaveLogic); - - // 为从端设置过滤器,防止取出样板 - setPatternFilter(slaveLogic); - } - } - // 检查是否是面板形式 - else if (slaveBlockEntity instanceof appeng.api.parts.IPartHost slavePartHost) { - // 尝试同步到面板,传递从端的方向 - syncToSlavePartHost(slavePartHost, slaveDirection, masterPatterns); - } - } catch (Exception e) { - // 忽略所有异常 - } - } - - /** - * 同步样板到从端面板 - */ - private static void syncToSlavePartHost(appeng.api.parts.IPartHost slavePartHost, net.minecraft.core.Direction targetDirection, List masterPatterns) { - try { - // 首先检查指定方向的部件 - appeng.api.parts.IPart targetPart = slavePartHost.getPart(targetDirection); - if (targetPart != null) { - // 处理AE2普通样板供应器面板 - if (checkIfClassImplementsInterface(targetPart, "appeng.helpers.patternprovider.PatternProviderLogicHost")) { - // 使用反射同步 - Class hostClass = Class.forName("appeng.helpers.patternprovider.PatternProviderLogicHost"); - java.lang.reflect.Method getLogicMethod = hostClass.getMethod("getLogic"); - Object slaveLogic = getLogicMethod.invoke(targetPart); - - Class logicClass = Class.forName("appeng.helpers.patternprovider.PatternProviderLogic"); - java.lang.reflect.Method getPatternInvMethod = logicClass.getMethod("getPatternInv"); - Object slaveInv = getPatternInvMethod.invoke(slaveLogic); - - if (slaveInv instanceof appeng.api.inventories.InternalInventory internalInv) { - // 清空从方块的inventory - for (int i = 0; i < internalInv.size(); i++) { - internalInv.setItemDirect(i, net.minecraft.world.item.ItemStack.EMPTY); - } - - // 复制主方块的所有pattern到从方块 - for (int i = 0; i < masterPatterns.size() && i < internalInv.size(); i++) { - internalInv.setItemDirect(i, masterPatterns.get(i)); - } - - // 更新从方块的patterns - java.lang.reflect.Method updatePatternsMethod = logicClass.getMethod("updatePatterns"); - updatePatternsMethod.invoke(slaveLogic); - - // 为从端设置过滤器,防止取出样板 - setPatternFilter(slaveLogic); - } - return; // 已找到并同步指定方向的面板 - } - // 处理高级AE样板供应器面板 - else if (checkIfClassImplementsInterface(targetPart, "net.pedroksl.advanced_ae.common.logic.AdvPatternProviderLogicHost")) { - // 使用反射同步高级AE样板供应器 - Object patternContainer = targetPart; - Class patternContainerClass = Class.forName("appeng.helpers.patternprovider.PatternContainer"); - java.lang.reflect.Method getTerminalPatternInventoryMethod = patternContainerClass.getMethod("getTerminalPatternInventory"); - Object slaveInv = getTerminalPatternInventoryMethod.invoke(patternContainer); - - if (slaveInv instanceof appeng.api.inventories.InternalInventory internalInv) { - // 清空从方块的inventory - for (int i = 0; i < internalInv.size(); i++) { - internalInv.setItemDirect(i, net.minecraft.world.item.ItemStack.EMPTY); - } - - // 复制主方块的所有pattern到从方块 - for (int i = 0; i < masterPatterns.size() && i < internalInv.size(); i++) { - internalInv.setItemDirect(i, masterPatterns.get(i)); - } - - // 高级AE样板供应器不需要updatePatterns方法,直接更新inventory即可 - } - return; // 已找到并同步指定方向的面板 - } - } - - // 如果指定方向没有匹配的部件,遍历所有方向寻找第一个匹配的部件 - for (net.minecraft.core.Direction dir : net.minecraft.core.Direction.values()) { - // 跳过已经检查过的指定方向 - if (dir == targetDirection) continue; - - appeng.api.parts.IPart sidePart = slavePartHost.getPart(dir); - if (sidePart != null) { - // 处理AE2普通样板供应器面板 - if (checkIfClassImplementsInterface(sidePart, "appeng.helpers.patternprovider.PatternProviderLogicHost")) { - // 使用反射同步 - Class hostClass = Class.forName("appeng.helpers.patternprovider.PatternProviderLogicHost"); - java.lang.reflect.Method getLogicMethod = hostClass.getMethod("getLogic"); - Object slaveLogic = getLogicMethod.invoke(sidePart); - - Class logicClass = Class.forName("appeng.helpers.patternprovider.PatternProviderLogic"); - java.lang.reflect.Method getPatternInvMethod = logicClass.getMethod("getPatternInv"); - Object slaveInv = getPatternInvMethod.invoke(slaveLogic); - - if (slaveInv instanceof appeng.api.inventories.InternalInventory internalInv) { - // 清空从方块的inventory - for (int i = 0; i < internalInv.size(); i++) { - internalInv.setItemDirect(i, net.minecraft.world.item.ItemStack.EMPTY); - } - - // 复制主方块的所有pattern到从方块 - for (int i = 0; i < masterPatterns.size() && i < internalInv.size(); i++) { - internalInv.setItemDirect(i, masterPatterns.get(i)); - } - - // 更新从方块的patterns - java.lang.reflect.Method updatePatternsMethod = logicClass.getMethod("updatePatterns"); - updatePatternsMethod.invoke(slaveLogic); - - // 为从端设置过滤器,防止取出样板 - setPatternFilter(slaveLogic); - } - break; // 只同步第一个匹配的面板 - } - // 处理高级AE样板供应器面板 - else if (checkIfClassImplementsInterface(sidePart, "net.pedroksl.advanced_ae.common.logic.AdvPatternProviderLogicHost")) { - // 使用反射同步高级AE样板供应器 - Object patternContainer = sidePart; - Class patternContainerClass = Class.forName("appeng.helpers.patternprovider.PatternContainer"); - java.lang.reflect.Method getTerminalPatternInventoryMethod = patternContainerClass.getMethod("getTerminalPatternInventory"); - Object slaveInv = getTerminalPatternInventoryMethod.invoke(patternContainer); - - if (slaveInv instanceof appeng.api.inventories.InternalInventory internalInv) { - // 清空从方块的inventory - for (int i = 0; i < internalInv.size(); i++) { - internalInv.setItemDirect(i, net.minecraft.world.item.ItemStack.EMPTY); - } - - // 复制主方块的所有pattern到从方块 - for (int i = 0; i < masterPatterns.size() && i < internalInv.size(); i++) { - internalInv.setItemDirect(i, masterPatterns.get(i)); - } - - // 高级AE样板供应器不需要updatePatterns方法,直接更新inventory即可 - } - break; // 只同步第一个匹配的面板 - } - } - } - } catch (Exception e) { - // 忽略所有异常 - } - } - - /** - * 为样板供应器设置过滤器,防止取出样板 - */ - private static void setPatternFilter(Object logic) { - try { - // 获取PatternProviderLogic的patternInventory字段 - java.lang.reflect.Field logicField = logic.getClass().getDeclaredField("patternInventory"); - logicField.setAccessible(true); - Object inventoryObj = logicField.get(logic); - - // 检查是否为AppEngInternalInventory类型 - if (inventoryObj instanceof appeng.util.inv.AppEngInternalInventory appEngInv) { - // 设置过滤器,阻止提取样板 - appEngInv.setFilter(new appeng.util.inv.filter.IAEItemFilter() { - @Override - public boolean allowExtract(appeng.api.inventories.InternalInventory inv, int slot, int amount) { - return false; - } - }); - } - } catch (Exception e) { - // 忽略反射异常 - } - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/utils/pattern/PatternProviderOperation.java b/src/main/java/com/sorrowmist/useless/utils/pattern/PatternProviderOperation.java deleted file mode 100644 index 409839d5..00000000 --- a/src/main/java/com/sorrowmist/useless/utils/pattern/PatternProviderOperation.java +++ /dev/null @@ -1,429 +0,0 @@ -package com.sorrowmist.useless.utils.pattern; - -import com.sorrowmist.useless.UselessMod; -import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; -import net.minecraft.network.chat.Component; -import net.minecraft.ChatFormatting; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.level.Level; -import net.minecraft.world.level.block.entity.BlockEntity; -import appeng.api.parts.IPart; -import appeng.api.parts.IPartHost; - -import java.util.HashSet; -import java.util.Set; - -/** - * 样板供应器操作类,处理主从关系的设置和同步 - */ -public class PatternProviderOperation { - /** - * 设置主样板供应器 - */ - public static void setAsMaster(Level world, BlockPos masterPos, Direction direction, Player player) { - // 检查方块位置是否包含有效的样板供应器 - boolean hasValidProvider = false; - String providerType = "Unknown"; - - // 获取方块实体 - BlockEntity blockEntity = world.getBlockEntity(masterPos); - if (blockEntity != null) { - // 检查是否是直接放置的样板供应器 - String className = blockEntity.getClass().getName(); - if (className.equals("com.glodblock.github.extendedae.common.tileentities.TileExPatternProvider") || - blockEntity instanceof appeng.blockentity.crafting.PatternProviderBlockEntity || - className.equals("net.pedroksl.advanced_ae.common.entities.AdvPatternProviderEntity") || - className.equals("net.pedroksl.advanced_ae.common.entities.SmallAdvPatternProviderEntity")) { - providerType = PatternProviderUtils.getPatternProviderType(blockEntity, null); - hasValidProvider = !providerType.equals("Unknown"); - } - // 如果不是,检查是否包含样板供应器部件 - else { - try { - // 检查是否是IPartHost,包含部件 - if (blockEntity instanceof IPartHost partHost) { - // 找到与点击位置最匹配的样板供应器方向 - Direction actualDirection = PatternProviderUtils.findMatchingDirection(partHost, masterPos, direction, player); - - // 使用实际方向检查部件 - IPart targetPart = partHost.getPart(actualDirection); - if (targetPart != null) { - String partType = PatternProviderUtils.getPatternProviderType(null, targetPart); - if (!partType.equals("Unknown")) { - hasValidProvider = true; - providerType = partType; - direction = actualDirection; - } else { - // 检查中心部件(线缆) - IPart centerPart = partHost.getPart(null); - if (centerPart != null) { - String centerType = PatternProviderUtils.getPatternProviderType(null, centerPart); - if (!centerType.equals("Unknown")) { - hasValidProvider = true; - providerType = centerType; - direction = null; - } - } - } - } - } - } catch (Exception e) { - // 忽略检查失败的异常 - } - } - } - - if (hasValidProvider) { - // 创建主方块键 - com.sorrowmist.useless.utils.pattern.PatternProviderKey masterKey = new com.sorrowmist.useless.utils.pattern.PatternProviderKey(masterPos, direction != null ? direction : Direction.UP); - - // 如果当前主方块已经是从端,先移除它与原主端的关系 - if (PatternProviderManager.getSlaveToMaster().containsKey(masterKey)) { - com.sorrowmist.useless.utils.pattern.PatternProviderKey oldMaster = PatternProviderManager.getSlaveToMaster().remove(masterKey); - Set oldSlaves = PatternProviderManager.getMasterToSlaves().get(oldMaster); - if (oldSlaves != null) { - oldSlaves.remove(masterKey); - if (oldSlaves.isEmpty()) { - PatternProviderManager.getMasterToSlaves().remove(oldMaster); - } - } - } - - // 确保主端映射存在 - if (!PatternProviderManager.getMasterToSlaves().containsKey(masterKey)) { - PatternProviderManager.getMasterToSlaves().put(masterKey, new HashSet<>()); - } - - // 更新当前选择的主方块 - PatternProviderManager.setCurrentSelectedMaster(masterKey); - - if (player != null) { - player.sendSystemMessage(Component.literal("已将 " + providerType + " 设置为主样板供应器").withStyle(ChatFormatting.GREEN)); - } - - // 保存同步数据 - if (world instanceof ServerLevel) { - PatternProviderManager.saveSyncData((ServerLevel) world); - } - } else { - if (player != null) { - player.sendSystemMessage(Component.literal("该位置不包含有效的样板供应器").withStyle(ChatFormatting.RED)); - } - } - } - - /** - * 添加为从样板供应器 - */ - public static void addAsSlave(Level world, BlockPos slavePos, Direction direction, Player player) { - // 使用当前选择的主方块 - com.sorrowmist.useless.utils.pattern.PatternProviderKey masterKey = PatternProviderManager.getCurrentSelectedMaster(); - if (masterKey == null) { - if (player != null) { - player.sendSystemMessage(Component.literal("请先设置一个主样板供应器").withStyle(ChatFormatting.RED)); - } - return; - } - - // 检查从方块位置是否包含有效的样板供应器 - boolean hasValidProvider = false; - String slaveProviderType = "Unknown"; - - // 获取从方块实体 - BlockEntity blockEntity = world.getBlockEntity(slavePos); - if (blockEntity != null) { - // 检查是否是直接放置的样板供应器 - String className = blockEntity.getClass().getName(); - if (className.equals("com.glodblock.github.extendedae.common.tileentities.TileExPatternProvider") || - blockEntity instanceof appeng.blockentity.crafting.PatternProviderBlockEntity || - className.equals("net.pedroksl.advanced_ae.common.entities.AdvPatternProviderEntity") || - className.equals("net.pedroksl.advanced_ae.common.entities.SmallAdvPatternProviderEntity")) { - slaveProviderType = PatternProviderUtils.getPatternProviderType(blockEntity, null); - hasValidProvider = !slaveProviderType.equals("Unknown"); - } - // 如果不是,检查是否包含样板供应器部件 - else { - try { - // 检查是否是IPartHost,包含部件 - if (blockEntity instanceof IPartHost partHost) { - // 找到与点击位置最匹配的样板供应器方向 - Direction actualDirection = PatternProviderUtils.findMatchingDirection(partHost, slavePos, direction, player); - - // 使用实际方向检查部件 - IPart targetPart = partHost.getPart(actualDirection); - if (targetPart != null) { - String partType = PatternProviderUtils.getPatternProviderType(null, targetPart); - if (!partType.equals("Unknown")) { - hasValidProvider = true; - slaveProviderType = partType; - direction = actualDirection; - } else { - // 检查中心部件(线缆) - IPart centerPart = partHost.getPart(null); - if (centerPart != null) { - String centerType = PatternProviderUtils.getPatternProviderType(null, centerPart); - if (!centerType.equals("Unknown")) { - hasValidProvider = true; - slaveProviderType = centerType; - direction = null; - } - } - } - } - } - } catch (Exception e) { - // 忽略检查失败的异常 - } - } - } - - if (hasValidProvider) { - // 确保主端映射存在 - if (!PatternProviderManager.getMasterToSlaves().containsKey(masterKey)) { - PatternProviderManager.getMasterToSlaves().put(masterKey, new HashSet<>()); - } - - // 获取主方块的类型 - String masterProviderType = PatternProviderUtils.getMasterProviderType(world, masterKey); - - // 确保只有同类样板供应器可以绑定 - if (!slaveProviderType.equals(masterProviderType)) { - if (player != null) { - player.sendSystemMessage(Component.literal("只有同类样板供应器可以绑定").withStyle(ChatFormatting.RED)); - } - return; - } - - // 创建从方块的键 - com.sorrowmist.useless.utils.pattern.PatternProviderKey slaveKey = new com.sorrowmist.useless.utils.pattern.PatternProviderKey(slavePos, direction != null ? direction : Direction.UP); - - // 检查是否是方块形式 - String className = blockEntity.getClass().getName(); - boolean isBlockForm = className.equals("com.glodblock.github.extendedae.common.tileentities.TileExPatternProvider") || - blockEntity instanceof appeng.blockentity.crafting.PatternProviderBlockEntity || - className.equals("net.pedroksl.advanced_ae.common.entities.AdvPatternProviderEntity") || - className.equals("net.pedroksl.advanced_ae.common.entities.SmallAdvPatternProviderEntity"); - - // 如果该方块已经是从方块,先移除其从方块关系 - if (isBlockForm) { - // 方块形式:检查该位置是否有任何从端关系(不考虑方向) - for (com.sorrowmist.useless.utils.pattern.PatternProviderKey existingSlaveKey : new HashSet<>(PatternProviderManager.getSlaveToMaster().keySet())) { - if (existingSlaveKey.getPos().equals(slavePos)) { - com.sorrowmist.useless.utils.pattern.PatternProviderKey oldMaster = PatternProviderManager.getSlaveToMaster().remove(existingSlaveKey); - if (oldMaster != null) { - Set oldSlaves = PatternProviderManager.getMasterToSlaves().get(oldMaster); - if (oldSlaves != null) { - oldSlaves.remove(existingSlaveKey); - if (oldSlaves.isEmpty()) { - PatternProviderManager.getMasterToSlaves().remove(oldMaster); - } - } - } - break; // 只需要移除一个,因为一个位置只能有一个从端 - } - } - } else { - // 面板形式:需要考虑方向 - if (PatternProviderManager.getSlaveToMaster().containsKey(slaveKey)) { - com.sorrowmist.useless.utils.pattern.PatternProviderKey oldMaster = PatternProviderManager.getSlaveToMaster().remove(slaveKey); - if (oldMaster != null) { - Set oldSlaves = PatternProviderManager.getMasterToSlaves().get(oldMaster); - if (oldSlaves != null) { - oldSlaves.remove(slaveKey); - if (oldSlaves.isEmpty()) { - PatternProviderManager.getMasterToSlaves().remove(oldMaster); - } - } - } - } - } - - // 如果该方块是主方块,先移除其主方块关系 - if (isBlockForm) { - // 方块形式:检查该位置是否有任何主端关系(不考虑方向) - for (com.sorrowmist.useless.utils.pattern.PatternProviderKey existingMasterKey : new HashSet<>(PatternProviderManager.getMasterToSlaves().keySet())) { - if (existingMasterKey.getPos().equals(slavePos)) { - Set oldSlaves = PatternProviderManager.getMasterToSlaves().remove(existingMasterKey); - if (oldSlaves != null) { - for (com.sorrowmist.useless.utils.pattern.PatternProviderKey oldSlave : oldSlaves) { - PatternProviderManager.getSlaveToMaster().remove(oldSlave); - } - } - break; // 只需要移除一个,因为一个位置只能有一个主端 - } - } - } else { - // 面板形式:需要考虑方向 - if (PatternProviderManager.getMasterToSlaves().containsKey(slaveKey)) { - Set oldSlaves = PatternProviderManager.getMasterToSlaves().remove(slaveKey); - if (oldSlaves != null) { - for (com.sorrowmist.useless.utils.pattern.PatternProviderKey oldSlave : oldSlaves) { - PatternProviderManager.getSlaveToMaster().remove(oldSlave); - } - } - } - } - - // 检查从方块是否已经是当前主方块的从端 - if (PatternProviderManager.getMasterToSlaves().get(masterKey).contains(slaveKey)) { - if (player != null) { - player.sendSystemMessage(Component.literal("该样板供应器已经是当前主端的从端").withStyle(ChatFormatting.YELLOW)); - } - return; - } - - // 检查从方块是否是主方块本身 - if (slaveKey.equals(masterKey)) { - if (player != null) { - player.sendSystemMessage(Component.literal("不能将主方块添加为自己的从端").withStyle(ChatFormatting.RED)); - } - return; - } - - // 添加为从方块 - PatternProviderManager.getMasterToSlaves().get(masterKey).add(slaveKey); - PatternProviderManager.getSlaveToMaster().put(slaveKey, masterKey); - - if (player != null) { - String providerTypeName = "样板供应器"; - if (slaveProviderType.equals("ExPatternProvider")) { - providerTypeName = "扩展样板供应器"; - } else if (slaveProviderType.equals("AECraftingPatternProvider")) { - providerTypeName = "AE2普通样板供应器"; - } else if (slaveProviderType.equals("AAEAdvPatternProvider")) { - providerTypeName = "高级AE高级样板供应器"; - } - player.sendSystemMessage(Component.literal("已将此" + providerTypeName + "设为从方块,跟随主方块").withStyle(ChatFormatting.BLUE)); - } - - // 立即同步一次 - PatternProviderManager.syncPatternsFromMaster(world, slaveKey, masterKey); - - // 获取从端样板供应器并设置为不在终端显示 - if (world instanceof ServerLevel serverLevel) { - BlockEntity slaveBlockEntity = serverLevel.getBlockEntity(slavePos); - if (slaveBlockEntity != null) { - // 使用反射和条件类加载来设置从端样板供应器不在终端显示 - try { - // 检查是否是IPartHost,包含部件 - if (slaveBlockEntity instanceof appeng.api.parts.IPartHost partHost) { - // 检查指定方向的部件,设置从端样板供应器不在终端显示 - appeng.api.parts.IPart targetPart = partHost.getPart(direction); - if (targetPart != null) { - // 处理AE2普通样板供应器 - if (targetPart instanceof appeng.helpers.patternprovider.PatternProviderLogicHost slaveHostPart) { - // 设置AE2普通从端样板供应器不在终端显示 - appeng.api.util.IConfigManager configManager = slaveHostPart.getConfigManager(); - configManager.putSetting(appeng.api.config.Settings.PATTERN_ACCESS_TERMINAL, appeng.api.config.YesNo.NO); - } - // 处理其他类型的样板供应器,使用反射 - else if (PatternProviderManager.checkIfClassImplementsInterface(targetPart, "appeng.api.util.IConfigurableObject")) { - // 使用反射调用getConfigManager方法 - Object configurableObject = targetPart; - Class configurableClass = Class.forName("appeng.api.util.IConfigurableObject"); - java.lang.reflect.Method getConfigManagerMethod = configurableClass.getMethod("getConfigManager"); - Object configManagerObj = getConfigManagerMethod.invoke(configurableObject); - - if (configManagerObj instanceof appeng.api.util.IConfigManager configManager) { - // 设置从端样板供应器不在终端显示 - configManager.putSetting(appeng.api.config.Settings.PATTERN_ACCESS_TERMINAL, appeng.api.config.YesNo.NO); - } - } - } else { - // 检查中心部件(线缆) - appeng.api.parts.IPart centerPart = partHost.getPart(null); - if (centerPart != null) { - // 处理AE2普通样板供应器 - if (centerPart instanceof appeng.helpers.patternprovider.PatternProviderLogicHost slaveHostPart) { - // 设置AE2普通从端样板供应器不在终端显示 - appeng.api.util.IConfigManager configManager = slaveHostPart.getConfigManager(); - configManager.putSetting(appeng.api.config.Settings.PATTERN_ACCESS_TERMINAL, appeng.api.config.YesNo.NO); - } - // 处理其他类型的样板供应器,使用反射 - else if (PatternProviderManager.checkIfClassImplementsInterface(centerPart, "appeng.api.util.IConfigurableObject")) { - // 使用反射调用getConfigManager方法 - Object configurableObject = centerPart; - Class configurableClass = Class.forName("appeng.api.util.IConfigurableObject"); - java.lang.reflect.Method getConfigManagerMethod = configurableClass.getMethod("getConfigManager"); - Object configManagerObj = getConfigManagerMethod.invoke(configurableObject); - - if (configManagerObj instanceof appeng.api.util.IConfigManager configManager) { - // 设置从端样板供应器不在终端显示 - configManager.putSetting(appeng.api.config.Settings.PATTERN_ACCESS_TERMINAL, appeng.api.config.YesNo.NO); - } - } - } - } - } - // 直接放置的样板供应器 - else { - // 处理AE2普通样板供应器 - if (slaveBlockEntity instanceof appeng.helpers.patternprovider.PatternProviderLogicHost slaveHost) { - // 直接放置的AE2普通样板供应器 - appeng.api.util.IConfigManager configManager = slaveHost.getConfigManager(); - configManager.putSetting(appeng.api.config.Settings.PATTERN_ACCESS_TERMINAL, appeng.api.config.YesNo.NO); - } - // 处理其他类型的样板供应器,使用反射 - else if (PatternProviderManager.checkIfClassImplementsInterface(slaveBlockEntity, "appeng.api.util.IConfigurableObject")) { - // 使用反射调用getConfigManager方法 - Object configurableObject = slaveBlockEntity; - Class configurableClass = Class.forName("appeng.api.util.IConfigurableObject"); - java.lang.reflect.Method getConfigManagerMethod = configurableClass.getMethod("getConfigManager"); - Object configManagerObj = getConfigManagerMethod.invoke(configurableObject); - - if (configManagerObj instanceof appeng.api.util.IConfigManager configManager) { - // 设置从端样板供应器不在终端显示 - configManager.putSetting(appeng.api.config.Settings.PATTERN_ACCESS_TERMINAL, appeng.api.config.YesNo.NO); - } - } - } - } catch (Exception e) { - // 忽略任何异常,确保不会因为可选模组缺失而崩溃 - } - } - } - - // 保存同步数据 - if (world instanceof ServerLevel) { - PatternProviderManager.saveSyncData((ServerLevel) world); - } - } else { - if (player != null) { - player.sendSystemMessage(Component.literal("该位置不包含有效的样板供应器").withStyle(ChatFormatting.RED)); - } - } - } - - /** - * 从主样板供应器同步样板到从样板供应器 - */ - public static void syncPatternsFromMaster(Level world, com.sorrowmist.useless.utils.pattern.PatternProviderKey slaveKey, com.sorrowmist.useless.utils.pattern.PatternProviderKey masterKey) { - // 直接调用PatternProviderManager的同步方法 - PatternProviderManager.syncPatternsFromMaster(world, slaveKey, masterKey); - } - - /** - * 重置主样板供应器选择 - */ - public static void resetMasterPatternProvider(Level world) { - // 取消当前的主方块选择状态 - PatternProviderManager.setCurrentSelectedMaster(null); - - // 清空临时的同步时间记录 - PatternProviderManager.getLastSyncTime().clear(); - - // 保存同步数据(这里实际上不需要保存,因为我们没有修改主从关系) - // 但为了保持代码一致性,仍然调用保存方法 - if (world instanceof ServerLevel serverLevel) { - com.sorrowmist.useless.utils.pattern.PatternProviderSyncData syncData = serverLevel.getDataStorage().computeIfAbsent( - com.sorrowmist.useless.utils.pattern.PatternProviderSyncData::load, - com.sorrowmist.useless.utils.pattern.PatternProviderSyncData::new, - "PatternProviderSyncData" - ); - syncData.setDirty(); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/utils/pattern/PatternProviderSyncData.java b/src/main/java/com/sorrowmist/useless/utils/pattern/PatternProviderSyncData.java deleted file mode 100644 index 6790ef88..00000000 --- a/src/main/java/com/sorrowmist/useless/utils/pattern/PatternProviderSyncData.java +++ /dev/null @@ -1,153 +0,0 @@ -package com.sorrowmist.useless.utils.pattern; - -import net.minecraft.nbt.CompoundTag; -import net.minecraft.nbt.ListTag; -import net.minecraft.world.level.saveddata.SavedData; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -/** - * 用于存储同步数据的SavedData类 - */ -public class PatternProviderSyncData extends SavedData { - private final Map> masterToSlaves = new HashMap<>(); - private final Map slaveToMaster = new HashMap<>(); - - @Override - public CompoundTag save(CompoundTag tag) { - // 保存主从关系 - CompoundTag masterToSlavesTag = new CompoundTag(); - for (Map.Entry> entry : this.masterToSlaves.entrySet()) { - PatternProviderKey masterKey = entry.getKey(); - Set slaves = entry.getValue(); - - ListTag slaveList = new ListTag(); - for (PatternProviderKey slave : slaves) { - CompoundTag posTag = new CompoundTag(); - posTag.putInt("x", slave.getPos().getX()); - posTag.putInt("y", slave.getPos().getY()); - posTag.putInt("z", slave.getPos().getZ()); - posTag.putString("direction", slave.getDirection().getName()); - slaveList.add(posTag); - } - - String masterKeyStr = masterKey.getPos().getX() + "," + masterKey.getPos().getY() + "," + masterKey.getPos().getZ() + "," + masterKey.getDirection().getName(); - masterToSlavesTag.put(masterKeyStr, slaveList); - } - tag.put("MasterToSlaves", masterToSlavesTag); - - // 保存从主关系 - CompoundTag slaveToMasterTag = new CompoundTag(); - for (Map.Entry entry : this.slaveToMaster.entrySet()) { - PatternProviderKey slave = entry.getKey(); - PatternProviderKey master = entry.getValue(); - - String slaveKeyStr = slave.getPos().getX() + "," + slave.getPos().getY() + "," + slave.getPos().getZ() + "," + slave.getDirection().getName(); - CompoundTag masterTag = new CompoundTag(); - masterTag.putInt("x", master.getPos().getX()); - masterTag.putInt("y", master.getPos().getY()); - masterTag.putInt("z", master.getPos().getZ()); - masterTag.putString("direction", master.getDirection().getName()); - slaveToMasterTag.put(slaveKeyStr, masterTag); - } - tag.put("SlaveToMaster", slaveToMasterTag); - - return tag; - } - - public static PatternProviderSyncData load(CompoundTag tag) { - PatternProviderSyncData data = new PatternProviderSyncData(); - - // 加载主从关系 - if (tag.contains("MasterToSlaves")) { - CompoundTag masterToSlavesTag = tag.getCompound("MasterToSlaves"); - for (String masterKeyStr : masterToSlavesTag.getAllKeys()) { - ListTag slaveList = masterToSlavesTag.getList(masterKeyStr, CompoundTag.TAG_COMPOUND); - - // 解析主方块位置和方向 - String[] masterCoords = masterKeyStr.split(","); - if (masterCoords.length == 4) { - try { - net.minecraft.core.BlockPos masterPos = new net.minecraft.core.BlockPos( - Integer.parseInt(masterCoords[0]), - Integer.parseInt(masterCoords[1]), - Integer.parseInt(masterCoords[2]) - ); - net.minecraft.core.Direction masterDirection = net.minecraft.core.Direction.byName(masterCoords[3]); - PatternProviderKey masterKey = new PatternProviderKey(masterPos, masterDirection); - - // 解析从方块位置和方向 - Set slaves = new HashSet<>(); - for (int i = 0; i < slaveList.size(); i++) { - CompoundTag posTag = slaveList.getCompound(i); - net.minecraft.core.BlockPos slavePos = new net.minecraft.core.BlockPos( - posTag.getInt("x"), - posTag.getInt("y"), - posTag.getInt("z") - ); - net.minecraft.core.Direction slaveDirection = net.minecraft.core.Direction.byName(posTag.getString("direction")); - slaves.add(new PatternProviderKey(slavePos, slaveDirection)); - } - - data.masterToSlaves.put(masterKey, slaves); - } catch (NumberFormatException e) { - // 忽略格式错误的坐标 - } - } - } - } - - // 加载从主关系 - if (tag.contains("SlaveToMaster")) { - CompoundTag slaveToMasterTag = tag.getCompound("SlaveToMaster"); - for (String slaveKeyStr : slaveToMasterTag.getAllKeys()) { - CompoundTag masterTag = slaveToMasterTag.getCompound(slaveKeyStr); - - // 解析从方块位置和方向 - String[] slaveCoords = slaveKeyStr.split(","); - if (slaveCoords.length == 4) { - try { - net.minecraft.core.BlockPos slavePos = new net.minecraft.core.BlockPos( - Integer.parseInt(slaveCoords[0]), - Integer.parseInt(slaveCoords[1]), - Integer.parseInt(slaveCoords[2]) - ); - net.minecraft.core.Direction slaveDirection = net.minecraft.core.Direction.byName(slaveCoords[3]); - PatternProviderKey slaveKey = new PatternProviderKey(slavePos, slaveDirection); - - // 解析主方块位置和方向 - net.minecraft.core.BlockPos masterPos = new net.minecraft.core.BlockPos( - masterTag.getInt("x"), - masterTag.getInt("y"), - masterTag.getInt("z") - ); - net.minecraft.core.Direction masterDirection = net.minecraft.core.Direction.byName(masterTag.getString("direction")); - PatternProviderKey masterKey = new PatternProviderKey(masterPos, masterDirection); - - data.slaveToMaster.put(slaveKey, masterKey); - } catch (NumberFormatException e) { - // 忽略格式错误的坐标 - } - } - } - } - - return data; - } - - public Map> getMasterToSlaves() { - return masterToSlaves; - } - - public Map getSlaveToMaster() { - return slaveToMaster; - } - - public void clear() { - masterToSlaves.clear(); - slaveToMaster.clear(); - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/utils/pattern/PatternProviderUtils.java b/src/main/java/com/sorrowmist/useless/utils/pattern/PatternProviderUtils.java deleted file mode 100644 index 3290294d..00000000 --- a/src/main/java/com/sorrowmist/useless/utils/pattern/PatternProviderUtils.java +++ /dev/null @@ -1,158 +0,0 @@ -package com.sorrowmist.useless.utils.pattern; - -import com.sorrowmist.useless.UselessMod; -import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.level.block.entity.BlockEntity; -import net.minecraft.world.level.Level; -import net.minecraft.world.phys.Vec3; -import appeng.api.parts.IPart; -import appeng.api.parts.IPartHost; - -/** - * 样板供应器工具类,提供类型检测、方向匹配等通用功能 - */ -public class PatternProviderUtils { - /** - * 获取样板供应器类型 - */ - public static String getPatternProviderType(BlockEntity blockEntity, IPart part) { - // 使用字符串类名检测,避免直接引用可选模组的类 - if (blockEntity != null) { - String className = blockEntity.getClass().getName(); - if (className.equals("com.glodblock.github.extendedae.common.tileentities.TileExPatternProvider")) { - return "ExPatternProvider"; // ExtendedAE扩展样板供应器 - } else if (blockEntity instanceof appeng.blockentity.crafting.PatternProviderBlockEntity) { - return "AECraftingPatternProvider"; // AE2普通样板供应器 - } else if (className.equals("net.pedroksl.advanced_ae.common.entities.AdvPatternProviderEntity") || - className.equals("net.pedroksl.advanced_ae.common.entities.SmallAdvPatternProviderEntity")) { - return "AAEAdvPatternProvider"; // 高级AE的高级样板供应器(普通和小型) - } - } - - if (part != null) { - String partName = part.getClass().getName(); - if (partName.contains("ExPatternProvider")) { - return "ExPatternProvider"; // 面板形式的ExtendedAE扩展样板供应器 - } else if (partName.contains("PatternProvider") && !partName.contains("AdvPatternProvider")) { - return "AECraftingPatternProvider"; // 面板形式的AE2普通样板供应器 - } else if (partName.contains("AdvPatternProvider")) { - return "AAEAdvPatternProvider"; // 面板形式的高级AE高级样板供应器 - } - } - return "Unknown"; - } - - /** - * 找到与点击位置最匹配的样板供应器方向 - */ - public static Direction findMatchingDirection(IPartHost partHost, BlockPos blockPos, Direction initialDirection, Player player) { - // 如果是直接放置的方块,直接返回初始方向 - BlockEntity blockEntity = player.level().getBlockEntity(blockPos); - if (blockEntity != null) { - String className = blockEntity.getClass().getName(); - if (className.equals("com.glodblock.github.extendedae.common.tileentities.TileExPatternProvider") || - blockEntity instanceof appeng.blockentity.crafting.PatternProviderBlockEntity || - className.equals("net.pedroksl.advanced_ae.common.entities.AdvPatternProviderEntity")) { - return initialDirection; - } - } - - // 计算玩家点击的精确位置 - net.minecraft.world.phys.HitResult hitResult = player.pick(4.5D, 0.0F, false); - if (hitResult.getType() != net.minecraft.world.phys.HitResult.Type.BLOCK) { - return initialDirection; - } - - net.minecraft.world.phys.BlockHitResult blockHitResult = (net.minecraft.world.phys.BlockHitResult) hitResult; - net.minecraft.world.phys.Vec3 hitLocation = blockHitResult.getLocation().subtract(blockPos.getX(), blockPos.getY(), blockPos.getZ()); - - // 遍历所有方向,找到与点击位置最匹配的样板供应器 - Direction bestDirection = initialDirection; - double bestMatchScore = Double.MAX_VALUE; - - for (Direction dir : Direction.values()) { - appeng.api.parts.IPart part = partHost.getPart(dir); - if (part != null) { - String partType = getPatternProviderType(null, part); - if (!partType.equals("Unknown")) { - // 计算该方向与点击位置的匹配程度 - double matchScore = calculateMatchScore(dir, hitLocation); - if (matchScore < bestMatchScore) { - bestMatchScore = matchScore; - bestDirection = dir; - } - } - } - } - - return bestDirection; - } - - /** - * 计算部件与点击位置的匹配分数 - */ - public static double calculateMatchScore(Direction dir, net.minecraft.world.phys.Vec3 hitLocation) { - // 计算点击位置在该方向上的投影 - double dx = hitLocation.x - 0.5; - double dy = hitLocation.y - 0.5; - double dz = hitLocation.z - 0.5; - - // 计算该方向的法向量 - int nx = dir.getStepX(); - int ny = dir.getStepY(); - int nz = dir.getStepZ(); - - // 对于每个方向,计算点击位置到该方向面板的距离 - // 面板的位置在方块的表面,距离方块中心0.5个单位 - double distanceToPanel = Math.abs(dx * nx + dy * ny + dz * nz) - 0.5; - - // 计算点击位置在面板平面内的偏移 - double inPlaneOffsetX = dx - nx * (0.5 + distanceToPanel * nx); - double inPlaneOffsetY = dy - ny * (0.5 + distanceToPanel * ny); - double inPlaneOffsetZ = dz - nz * (0.5 + distanceToPanel * nz); - - // 计算平面内的偏移距离 - double inPlaneDistance = Math.sqrt(inPlaneOffsetX * inPlaneOffsetX + inPlaneOffsetY * inPlaneOffsetY + inPlaneOffsetZ * inPlaneOffsetZ); - - // 总匹配分数 = 到面板的距离 + 平面内的偏移距离 - return Math.abs(distanceToPanel) + inPlaneDistance; - } - - /** - * 获取主样板供应器的类型 - */ - public static String getMasterProviderType(Level world, PatternProviderKey masterKey) { - BlockEntity blockEntity = world.getBlockEntity(masterKey.getPos()); - if (blockEntity != null) { - // 检查是否是直接放置的样板供应器 - String className = blockEntity.getClass().getName(); - if (className.equals("com.glodblock.github.extendedae.common.tileentities.TileExPatternProvider") || - blockEntity instanceof appeng.blockentity.crafting.PatternProviderBlockEntity || - className.equals("net.pedroksl.advanced_ae.common.entities.AdvPatternProviderEntity")) { - return getPatternProviderType(blockEntity, null); - } - // 如果是IPartHost,检查部件 - else if (blockEntity instanceof appeng.api.parts.IPartHost partHost) { - // 检查指定方向的部件 - appeng.api.parts.IPart targetPart = partHost.getPart(masterKey.getDirection()); - if (targetPart != null) { - String partType = getPatternProviderType(null, targetPart); - if (!partType.equals("Unknown")) { - return partType; - } - } - // 检查中心部件 - appeng.api.parts.IPart centerPart = partHost.getPart(null); - if (centerPart != null) { - String centerType = getPatternProviderType(null, centerPart); - if (!centerType.equals("Unknown")) { - return centerType; - } - } - } - } - return "Unknown"; - } -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/world/ae/AeConnectLinkSavedData.java b/src/main/java/com/sorrowmist/useless/world/ae/AeConnectLinkSavedData.java new file mode 100644 index 00000000..6a8307a6 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/world/ae/AeConnectLinkSavedData.java @@ -0,0 +1,164 @@ +package com.sorrowmist.useless.world.ae; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.saveddata.SavedData; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** + * 造化杖「AE 连接模式」建立的跨空间连接登记表。 + * + *

AE2 不会把这种连接写进存档:网格的持久化数据里只有节点与机器,没有连接, + * 连接完全是运行时对象(原版量子网络桥同样只在两侧均存活时重建)。 + * 因此连接的持久化由建立方负责——这里按「访问点维度+坐标 / 机器维度+坐标」四条信息登记, + * 配合 {@code AeDeviceLinker} 的低频自愈在区块/存档重载后重建连接。

+ * + *

两个维度都必须记录:连接允许跨维度(访问点与机器分处不同维度), + * 若只记录机器维度,自愈会在机器所在维度查找访问点:既可能查找失败,也可能命中同坐标的 + * 另一台访问点(不同维度坐标对齐在整合包中很常见)。

+ */ +public final class AeConnectLinkSavedData extends SavedData { + private static final String DATA_NAME = "useless_mod_ae_connect_links"; + private static final int CURRENT_VERSION = 1; + private static final String TAG_VERSION = "Version"; + private static final String TAG_LINKS = "Links"; + private static final String TAG_MACHINE_DIMENSION = "MachineDimension"; + private static final String TAG_ACCESS_POINT_DIMENSION = "AccessPointDimension"; + private static final String TAG_ACCESS_POINT = "AccessPoint"; + private static final String TAG_MACHINE = "Machine"; + + private final Set links = new LinkedHashSet<>(); + + public static AeConnectLinkSavedData get(MinecraftServer server) { + return server.overworld().getDataStorage().computeIfAbsent( + AeConnectLinkSavedData::load, + AeConnectLinkSavedData::new, + DATA_NAME); + } + + private static AeConnectLinkSavedData load(CompoundTag tag) { + AeConnectLinkSavedData data = new AeConnectLinkSavedData(); + ListTag list = tag.getList(TAG_LINKS, Tag.TAG_COMPOUND); + for (int i = 0; i < list.size(); i++) { + Link link = readLink(list.getCompound(i)); + if (link != null) { + data.links.add(link); + } + } + return data; + } + + @Nullable + private static Link readLink(CompoundTag entry) { + ResourceKey machineDimension = readDimension(entry.getString(TAG_MACHINE_DIMENSION)); + ResourceKey accessPointDimension = readDimension(entry.getString(TAG_ACCESS_POINT_DIMENSION)); + if (machineDimension == null || accessPointDimension == null) { + return null; + } + return new Link(machineDimension, accessPointDimension, + BlockPos.of(entry.getLong(TAG_ACCESS_POINT)), + BlockPos.of(entry.getLong(TAG_MACHINE))); + } + + @Nullable + private static ResourceKey readDimension(String value) { + ResourceLocation id = ResourceLocation.tryParse(value); + return id == null ? null : ResourceKey.create(Registries.DIMENSION, id); + } + + public boolean contains(Link link) { + return links.contains(link); + } + + public void add(Link link) { + if (links.add(link)) { + setDirty(); + } + } + + public void remove(Link link) { + if (links.remove(link)) { + setDirty(); + } + } + + public boolean isEmpty() { + return links.isEmpty(); + } + + /** 自愈遍历用的快照:避免遍历过程中改集合。 */ + public List snapshot() { + return new ArrayList<>(links); + } + + /** 某台访问点在某个维度下连了哪些机器——客户端渲染连接提示用。 */ + public List machinesOf(ResourceKey machineDimension, + ResourceKey accessPointDimension, + BlockPos accessPoint) { + List result = new ArrayList<>(); + for (Link link : links) { + if (link.machineDimension().equals(machineDimension) + && link.accessPointDimension().equals(accessPointDimension) + && link.accessPoint().equals(accessPoint)) { + result.add(link.machine()); + } + } + return result; + } + + /** 机器方块没了:把指向它的登记一并解除,避免存档里越攒越多死链接。 */ + public void removeByMachine(ResourceKey machineDimension, BlockPos machine) { + if (links.removeIf(link -> link.machineDimension().equals(machineDimension) + && link.machine().equals(machine))) { + setDirty(); + } + } + + /** 访问点没了:绑定的那张网已经不存在,指向它的登记全部作废。 */ + public void removeByAccessPoint(ResourceKey accessPointDimension, BlockPos accessPoint) { + if (links.removeIf(link -> link.accessPointDimension().equals(accessPointDimension) + && link.accessPoint().equals(accessPoint))) { + setDirty(); + } + } + + @Override + public CompoundTag save(CompoundTag tag) { + return save(tag, net.minecraft.core.RegistryAccess.EMPTY); + } + + public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) { + tag.putInt(TAG_VERSION, CURRENT_VERSION); + ListTag list = new ListTag(); + for (Link link : links) { + CompoundTag entry = new CompoundTag(); + entry.putString(TAG_MACHINE_DIMENSION, link.machineDimension().location().toString()); + entry.putString(TAG_ACCESS_POINT_DIMENSION, link.accessPointDimension().location().toString()); + entry.putLong(TAG_ACCESS_POINT, link.accessPoint().asLong()); + entry.putLong(TAG_MACHINE, link.machine().asLong()); + list.add(entry); + } + tag.put(TAG_LINKS, list); + return tag; + } + + /** 一条「访问点 ↔ 机器」的连接登记(两个维度都要记,见类注释)。 */ + public record Link(ResourceKey machineDimension, + ResourceKey accessPointDimension, + BlockPos accessPoint, + BlockPos machine) { + } +} diff --git a/src/main/java/com/sorrowmist/useless/world/dimension/AbstractPlasticPlatformGenerator.java b/src/main/java/com/sorrowmist/useless/world/dimension/AbstractPlasticPlatformGenerator.java new file mode 100644 index 00000000..e96fc4c5 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/world/dimension/AbstractPlasticPlatformGenerator.java @@ -0,0 +1,207 @@ +package com.sorrowmist.useless.world.dimension; + +/* + * Portions of the boundary, road, and center-marker generation algorithm are + * adapted from GT New Horizons/PersonalSpace: + * https://github.com/GTNewHorizons/PersonalSpace + * PersonalSpace is licensed under LGPL-3.0; see LICENSES/PersonalSpace-LGPL-3.0.txt. + * The remaining generator code and platform-specific behavior are original. + */ + +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.WorldGenRegion; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.NoiseColumn; +import net.minecraft.world.level.StructureManager; +import net.minecraft.world.level.biome.BiomeManager; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.levelgen.GenerationStep; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.RandomState; +import net.minecraft.world.level.levelgen.blending.Blender; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +/** + * Common terrain and surface-feature implementation for the three generators. + * + *

所有布局数学都在 {@link PlatformLayout} 里,本类只负责把它接到区块生成流程上。 + * 这样客户端预览可以复用同一份算法,不会与实际生成结果脱节。 + */ +public abstract class AbstractPlasticPlatformGenerator extends ChunkGenerator { + private static final int MIN_BUILD_Y = -64; + private volatile DimensionGenerationConfig configuration = DimensionGenerationConfig.defaults(); + + AbstractPlasticPlatformGenerator(BiomeSource biomeSource) { + super(biomeSource); + } + + /** 该维度的平台样式;布局数学统一由 {@link PlatformLayout} 提供。 */ + protected abstract PlatformStyle style(); + + public final void setConfiguration(DimensionGenerationConfig configuration) { + this.configuration = configuration.normalized(); + } + + @Override + public void applyCarvers(@NotNull WorldGenRegion region, long seed, + @NotNull RandomState randomState, @NotNull BiomeManager biomeManager, + @NotNull StructureManager structureManager, @NotNull ChunkAccess chunk, + @NotNull GenerationStep.Carving step) { + // This dimension deliberately has no caves or other terrain carving. + } + + @Override + public void buildSurface(@NotNull WorldGenRegion region, @NotNull StructureManager structureManager, + @NotNull RandomState randomState, @NotNull ChunkAccess chunk) { + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + Heightmap oceanFloor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG); + Heightmap worldSurface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG); + Heightmap motionBlocking = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING); + Heightmap motionBlockingNoLeaves = chunk.getOrCreateHeightmapUnprimed( + Heightmap.Types.MOTION_BLOCKING_NO_LEAVES); + + int minY = region.getMinBuildHeight(); + int maxY = region.getMaxBuildHeight(); + DimensionGenerationConfig config = configuration; + PlatformStyle style = style(); + int bottomY = PlatformLayout.clampY(PlatformLayout.bottomY(config), minY, maxY); + int topY = PlatformLayout.clampY(PlatformLayout.topY(config), minY, maxY); + int bedrockY = PlatformLayout.bedrockY(config, minY, maxY); + ChunkPos chunkPos = chunk.getPos(); + + for (int localX = 0; localX < 16; localX++) { + for (int localZ = 0; localZ < 16; localZ++) { + if (config.generateBedrock()) { + chunk.setBlockState(pos.set(localX, bedrockY, localZ), + Blocks.BEDROCK.defaultBlockState(), false); + } + + for (int y = bottomY + 1; y <= topY; y++) { + chunk.setBlockState(pos.set(localX, y, localZ), + PlatformLayout.platformState(style, config, chunkPos.x, chunkPos.z, + localX, localZ), false); + } + for (int y = topY + 1; y < maxY; y++) { + chunk.setBlockState(pos.set(localX, y, localZ), Blocks.AIR.defaultBlockState(), false); + } + + List surfaces = PlatformLayout.surfaceLevels(style, config, + chunkPos.x, chunkPos.z, localX, localZ, minY, maxY); + for (int surfaceY : surfaces) { + BlockState decoration = PlatformLayout.surfaceDecoration(style, config, + chunkPos.x, chunkPos.z, localX, localZ); + if (decoration != null) { + chunk.setBlockState(pos.set(localX, surfaceY, localZ), decoration, false); + } + } + + int highest = PlatformLayout.highestSurface(style, config, + chunkPos.x, chunkPos.z, localX, localZ, minY, maxY); + if (highest >= minY && highest < maxY) { + BlockState state = chunk.getBlockState(pos.set(localX, highest, localZ)); + worldSurface.update(localX, highest, localZ, state); + oceanFloor.update(localX, highest, localZ, state); + motionBlocking.update(localX, highest, localZ, state); + motionBlockingNoLeaves.update(localX, highest, localZ, state); + } + } + } + } + + @Override + public void spawnOriginalMobs(@NotNull WorldGenRegion region) { + // No mobs are spawned by this generator. + } + + @Override + public int getGenDepth() { + return 384; + } + + @Override + public @NotNull CompletableFuture fillFromNoise(@NotNull Executor executor, + @NotNull Blender blender, + @NotNull RandomState randomState, + @NotNull StructureManager structureManager, + @NotNull ChunkAccess chunk) { + return CompletableFuture.completedFuture(chunk); + } + + @Override + public int getSeaLevel() { + return 0; + } + + @Override + public int getMinY() { + return MIN_BUILD_Y; + } + + @Override + public int getBaseHeight(int x, int z, @NotNull Heightmap.Types heightmap, + @NotNull LevelHeightAccessor level, @NotNull RandomState randomState) { + DimensionGenerationConfig config = configuration; + int surface = PlatformLayout.highestSurface(style(), config, x >> 4, z >> 4, x & 15, z & 15, + level.getMinBuildHeight(), level.getMaxBuildHeight()); + if (surface < level.getMinBuildHeight()) return level.getMinBuildHeight(); + return Math.min(level.getMaxBuildHeight(), surface + 1); + } + + @Override + public @NotNull NoiseColumn getBaseColumn(int x, int z, @NotNull LevelHeightAccessor level, + @NotNull RandomState randomState) { + int minBuild = level.getMinBuildHeight(); + int maxBuild = level.getMaxBuildHeight(); + BlockState[] column = new BlockState[level.getHeight()]; + Arrays.fill(column, Blocks.AIR.defaultBlockState()); + DimensionGenerationConfig config = configuration; + PlatformStyle style = style(); + int localX = x & 15; + int localZ = z & 15; + int bottomY = PlatformLayout.clampY(PlatformLayout.bottomY(config), minBuild, maxBuild); + int topY = PlatformLayout.clampY(PlatformLayout.topY(config), minBuild, maxBuild); + int bedrockY = PlatformLayout.bedrockY(config, minBuild, maxBuild); + + if (config.generateBedrock() && bedrockY >= minBuild && bedrockY < maxBuild) { + column[bedrockY - minBuild] = Blocks.BEDROCK.defaultBlockState(); + } + int chunkX = x >> 4; + int chunkZ = z >> 4; + for (int y = bottomY + 1; y <= topY && y < maxBuild; y++) { + if (y >= minBuild) column[y - minBuild] = + PlatformLayout.platformState(style, config, chunkX, chunkZ, localX, localZ); + } + + List surfaces = PlatformLayout.surfaceLevels(style, config, chunkX, chunkZ, + localX, localZ, minBuild, maxBuild); + for (int surfaceY : surfaces) { + BlockState decoration = PlatformLayout.surfaceDecoration(style, config, + chunkX, chunkZ, localX, localZ); + if (decoration != null && surfaceY >= minBuild && surfaceY < maxBuild) { + column[surfaceY - minBuild] = decoration; + } + } + return new NoiseColumn(minBuild, column); + } + + @Override + public void addDebugScreenInfo(List list, @NotNull RandomState randomState, @NotNull BlockPos pos) { + DimensionGenerationConfig config = configuration; + list.add("Useless Dimension - " + style().debugName()); + list.add("Height: Y=" + PlatformLayout.bottomY(config) + " ~ " + PlatformLayout.topY(config)); + list.add("Layers: " + config.platformLayers()); + list.add("Boundary: " + config.boundaryIntervalX() + " x " + config.boundaryIntervalZ()); + list.add("Road: width=" + config.roadWidth() + ", mode=" + config.mode()); + list.add("Center marker: " + config.centerMarkerEnabled()); + } +} diff --git a/src/main/java/com/sorrowmist/useless/world/dimension/DimensionGenerationConfig.java b/src/main/java/com/sorrowmist/useless/world/dimension/DimensionGenerationConfig.java new file mode 100644 index 00000000..9a85c15e --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/world/dimension/DimensionGenerationConfig.java @@ -0,0 +1,666 @@ +package com.sorrowmist.useless.world.dimension; + +/* + * The boundary, road, and center-marker option model follows concepts from + * GT New Horizons/PersonalSpace: + * https://github.com/GTNewHorizons/PersonalSpace + * PersonalSpace is licensed under LGPL-3.0; see LICENSES/PersonalSpace-LGPL-3.0.txt. + * The record, codec, persistence, validation, and Useless Mod integration are + * original code. + */ + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import com.sorrowmist.useless.core.config.ConfigManager; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; + +import java.util.Objects; + +/** Settings used by all three Useless Dimension chunk generators. */ +public record DimensionGenerationConfig( + ResourceLocation borderBlockId, + ResourceLocation fillBlockId, + ResourceLocation centerBlockId, + int platformLayers, + int platformStartY, + boolean generateBedrock, + boolean bedrockAtBottom, + Features features) { + + public static final ResourceLocation DEFAULT_BORDER_BLOCK = + new ResourceLocation("useless_mod", "aqua_glow_plastic"); + public static final ResourceLocation DEFAULT_FILL_BLOCK = + new ResourceLocation("useless_mod", "white_glow_plastic"); + public static final ResourceLocation DEFAULT_CENTER_BLOCK = + new ResourceLocation("useless_mod", "light_gray_glow_plastic"); + public static final ResourceLocation DEFAULT_BOUNDARY_BLOCK_A = + new ResourceLocation("useless_mod", "black_glow_plastic"); + public static final ResourceLocation DEFAULT_BOUNDARY_BLOCK_B = + new ResourceLocation("useless_mod", "yellow_glow_plastic"); + public static final ResourceLocation DEFAULT_ROAD_SURFACE_BLOCK = + new ResourceLocation("useless_mod", "black_plastic_ctm"); + public static final ResourceLocation DEFAULT_ROAD_MARKING_BLOCK = + new ResourceLocation("useless_mod", "yellow_plastic_ctm"); + + /** 平台模式:马路、多联。 */ + public enum Mode { + /** 马路模式:区块之间按边界间隔生成马路。 */ + ROAD, + /** 多联模式:按边界间隔把相邻区块合并成一个平台。 */ + MULTI; + + public static Mode fromOrdinal(int ordinal) { + return ordinal == MULTI.ordinal() ? MULTI : ROAD; + } + + /** 切换到下一个模式:马路与多联之间循环。 */ + public Mode next() { + return this == ROAD ? MULTI : ROAD; + } + + /** 按名称解析模式;无法识别(含旧版的纯方块模式)时回落到马路模式。 */ + public static Mode byName(String name) { + return "MULTI".equalsIgnoreCase(name) ? MULTI : ROAD; + } + } + + /** Additional surface feature settings. */ + public record Features( + ResourceLocation boundaryBlockAId, + ResourceLocation boundaryBlockBId, + int boundaryIntervalX, + int boundaryIntervalZ, + int roadWidth, + Mode mode, + ResourceLocation roadBlockAId, + ResourceLocation roadBlockBId, + ResourceLocation roadBlockCId, + boolean centerMarkerEnabled, + ResourceLocation centerMarkerBlockId) { + + private static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + ResourceLocation.CODEC.optionalFieldOf("boundary_block_a", DEFAULT_BOUNDARY_BLOCK_A) + .forGetter(Features::boundaryBlockAId), + ResourceLocation.CODEC.optionalFieldOf("boundary_block_b", DEFAULT_BOUNDARY_BLOCK_B) + .forGetter(Features::boundaryBlockBId), + Codec.INT.optionalFieldOf("boundary_interval_x", 1) + .forGetter(Features::boundaryIntervalX), + Codec.INT.optionalFieldOf("boundary_interval_z", 1) + .forGetter(Features::boundaryIntervalZ), + Codec.INT.optionalFieldOf("road_width", 1) + .forGetter(Features::roadWidth), + Codec.STRING.optionalFieldOf("mode", Mode.ROAD.name()) + .forGetter(features -> features.mode().name()), + ResourceLocation.CODEC.optionalFieldOf("road_block_a", DEFAULT_ROAD_SURFACE_BLOCK) + .forGetter(Features::roadBlockAId), + ResourceLocation.CODEC.optionalFieldOf("road_block_b", DEFAULT_ROAD_MARKING_BLOCK) + .forGetter(Features::roadBlockBId), + ResourceLocation.CODEC.optionalFieldOf("road_block_c", DEFAULT_ROAD_MARKING_BLOCK) + .forGetter(Features::roadBlockCId), + Codec.BOOL.optionalFieldOf("center_marker_enabled", false) + .forGetter(Features::centerMarkerEnabled), + ResourceLocation.CODEC.optionalFieldOf("center_marker_block", DEFAULT_CENTER_BLOCK) + .forGetter(Features::centerMarkerBlockId) + ).apply(instance, (boundaryA, boundaryB, intervalX, intervalZ, roadWidth, mode, + roadA, roadB, roadC, centerEnabled, marker) -> + new Features(boundaryA, boundaryB, intervalX, intervalZ, roadWidth, + Mode.byName(mode), roadA, roadB, roadC, + centerEnabled, marker))); + + public Features { + boundaryBlockAId = Objects.requireNonNull(boundaryBlockAId); + boundaryBlockBId = Objects.requireNonNull(boundaryBlockBId); + mode = Objects.requireNonNull(mode); + roadBlockAId = Objects.requireNonNull(roadBlockAId); + roadBlockBId = Objects.requireNonNull(roadBlockBId); + roadBlockCId = Objects.requireNonNull(roadBlockCId); + centerMarkerBlockId = Objects.requireNonNull(centerMarkerBlockId); + } + + private static Features defaults() { + return new Features( + DEFAULT_BOUNDARY_BLOCK_A, + DEFAULT_BOUNDARY_BLOCK_B, + 1, + 1, + 1, + Mode.ROAD, + DEFAULT_ROAD_SURFACE_BLOCK, + DEFAULT_ROAD_MARKING_BLOCK, + DEFAULT_ROAD_MARKING_BLOCK, + false, + DEFAULT_CENTER_BLOCK); + } + + private Features normalized() { + return new Features( + boundaryBlockAId, + boundaryBlockBId, + Mth.clamp(boundaryIntervalX, 0, 256), + Mth.clamp(boundaryIntervalZ, 0, 256), + Mth.clamp(roadWidth, 0, 16), + mode, + roadBlockAId, + roadBlockBId, + roadBlockCId, + centerMarkerEnabled, + centerMarkerBlockId); + } + } + + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + ResourceLocation.CODEC.optionalFieldOf("border_block", DEFAULT_BORDER_BLOCK) + .forGetter(DimensionGenerationConfig::borderBlockId), + ResourceLocation.CODEC.optionalFieldOf("fill_block", DEFAULT_FILL_BLOCK) + .forGetter(DimensionGenerationConfig::fillBlockId), + ResourceLocation.CODEC.optionalFieldOf("center_block", DEFAULT_CENTER_BLOCK) + .forGetter(DimensionGenerationConfig::centerBlockId), + Codec.INT.optionalFieldOf("platform_layers", 69) + .forGetter(DimensionGenerationConfig::platformLayers), + Codec.INT.optionalFieldOf("platform_start_y", -64) + .forGetter(DimensionGenerationConfig::platformStartY), + Codec.BOOL.optionalFieldOf("generate_bedrock", true) + .forGetter(DimensionGenerationConfig::generateBedrock), + Codec.BOOL.optionalFieldOf("bedrock_at_bottom", false) + .forGetter(DimensionGenerationConfig::bedrockAtBottom), + Features.CODEC.optionalFieldOf("features", Features.defaults()) + .forGetter(DimensionGenerationConfig::features) + ).apply(instance, DimensionGenerationConfig::new)); + + /** Keeps callers that used the original seven-field record. */ + public DimensionGenerationConfig(ResourceLocation borderBlockId, ResourceLocation fillBlockId, + ResourceLocation centerBlockId, int platformLayers, + int platformStartY, boolean generateBedrock, + boolean bedrockAtBottom) { + this(borderBlockId, fillBlockId, centerBlockId, platformLayers, platformStartY, + generateBedrock, bedrockAtBottom, Features.defaults()); + } + + public DimensionGenerationConfig { + borderBlockId = Objects.requireNonNull(borderBlockId); + fillBlockId = Objects.requireNonNull(fillBlockId); + centerBlockId = Objects.requireNonNull(centerBlockId); + features = Objects.requireNonNull(features); + } + + public static DimensionGenerationConfig defaults() { + return new DimensionGenerationConfig( + DEFAULT_BORDER_BLOCK, + DEFAULT_FILL_BLOCK, + DEFAULT_CENTER_BLOCK, + 69, + -64, + true, + false, + Features.defaults()); + } + public ResourceLocation boundaryBlockAId() { + return features.boundaryBlockAId(); + } + + public ResourceLocation boundaryBlockBId() { + return features.boundaryBlockBId(); + } + + public int boundaryIntervalX() { + return features.boundaryIntervalX(); + } + + public int boundaryIntervalZ() { + return features.boundaryIntervalZ(); + } + + public int roadWidth() { + return features.roadWidth(); + } + + public Mode mode() { + return features.mode(); + } + + public ResourceLocation roadBlockAId() { + return features.roadBlockAId(); + } + + public ResourceLocation roadBlockBId() { + return features.roadBlockBId(); + } + + public ResourceLocation roadBlockCId() { + return features.roadBlockCId(); + } + + public boolean centerMarkerEnabled() { + return features.centerMarkerEnabled(); + } + + public ResourceLocation centerMarkerBlockId() { + return features.centerMarkerBlockId(); + } + + public boolean isValid() { + if (platformLayers < 1 || platformLayers > 256 + || platformStartY < -64 || platformStartY > 256) return false; + if (boundaryIntervalX() < 0 || boundaryIntervalX() > 256 + || boundaryIntervalZ() < 0 || boundaryIntervalZ() > 256 + || roadWidth() < 0 || roadWidth() > 16 + || mode() == null) return false; + // 多联模式把边界间隔当作合并尺寸,必须大于零。 + if (mode() == Mode.MULTI + && (boundaryIntervalX() < 1 || boundaryIntervalZ() < 1)) return false; + return true; + } + + public boolean hasValidBlockIds() { + if (!isValidBlockId(borderBlockId) || !isValidBlockId(fillBlockId) + || !isValidBlockId(centerBlockId)) { + return false; + } + if (hasBoundaryFeatures() + && (!isValidBlockId(boundaryBlockAId()) || !isValidBlockId(boundaryBlockBId()))) return false; + if (hasRoadFeatures() + && (!isValidBlockId(roadBlockAId()) + || (mode() == Mode.ROAD && !isValidBlockId(roadBlockBId())) + || (mode() == Mode.ROAD && roadWidth() * 16 >= 4 + && !isValidBlockId(roadBlockCId())))) return false; + return !hasCenterMarkerFeature() || isValidBlockId(centerMarkerBlockId()); + } + + public boolean hasAllowedBlockIds() { + if (!isAllowedBlockId(borderBlockId) || !isAllowedBlockId(fillBlockId) + || !isAllowedBlockId(centerBlockId)) { + return false; + } + if (hasBoundaryFeatures() + && (!isAllowedBlockId(boundaryBlockAId()) || !isAllowedBlockId(boundaryBlockBId()))) return false; + if (hasRoadFeatures() + && (!isAllowedBlockId(roadBlockAId()) + || (mode() == Mode.ROAD && !isAllowedBlockId(roadBlockBId())) + || (mode() == Mode.ROAD && roadWidth() * 16 >= 4 + && !isAllowedBlockId(roadBlockCId())))) return false; + return !hasCenterMarkerFeature() || isAllowedBlockId(centerMarkerBlockId()); + } + + /** + * Boundary and road slots are editable only when the generator can actually + * place the corresponding layout. + */ + public static boolean areBoundaryAndRoadFeaturesEnabled( + int boundaryIntervalX, int boundaryIntervalZ, int roadWidth) { + return roadWidth > 0 && (boundaryIntervalX > 0 || boundaryIntervalZ > 0); + } + + private boolean hasBoundaryFeatures() { + // 多联模式不使用边界与道路布局,其方块配置不参与校验。 + if (mode() == Mode.MULTI) return false; + return areBoundaryAndRoadFeaturesEnabled(boundaryIntervalX(), boundaryIntervalZ(), roadWidth()); + } + + private boolean hasRoadFeatures() { + if (mode() == Mode.MULTI) return false; + return areBoundaryAndRoadFeaturesEnabled(boundaryIntervalX(), boundaryIntervalZ(), roadWidth()); + } + + private boolean hasCenterMarkerFeature() { + // 中心标记只在马路模式生效。 + if (mode() == Mode.MULTI) return false; + return centerMarkerEnabled() && boundaryIntervalX() > 0 && boundaryIntervalZ() > 0; + } + + public static boolean isValidBlockId(ResourceLocation id) { + if (id == null) return false; + Block block = BuiltInRegistries.BLOCK.get(id); + return block != Blocks.AIR && block.asItem() != Items.AIR; + } + + public static boolean isAllowedBlockId(ResourceLocation id) { + return isValidBlockId(id) && ConfigManager.isUselessDimensionFloorBlockAllowed(id); + } + + public DimensionGenerationConfig normalized() { + return new DimensionGenerationConfig( + borderBlockId, + fillBlockId, + centerBlockId, + Mth.clamp(platformLayers, 1, 256), + Mth.clamp(platformStartY, -64, 256), + generateBedrock, + bedrockAtBottom, + features.normalized()); + } + + public Block borderBlock() { + return resolveBlock(borderBlockId, Blocks.BLUE_WOOL); + } + + public Block fillBlock() { + return resolveBlock(fillBlockId, Blocks.WHITE_WOOL); + } + + public Block centerBlock() { + return resolveBlock(centerBlockId, Blocks.GRAY_WOOL); + } + + public Block boundaryBlockA() { + return resolveBlock(boundaryBlockAId(), Blocks.BLUE_WOOL); + } + + public Block boundaryBlockB() { + return resolveBlock(boundaryBlockBId(), Blocks.WHITE_WOOL); + } + + public Block roadBlockA() { + return resolveBlock(roadBlockAId(), Blocks.BLACK_WOOL); + } + + public Block roadBlockB() { + return resolveBlock(roadBlockBId(), Blocks.YELLOW_WOOL); + } + + public Block roadBlockC() { + return resolveBlock(roadBlockCId(), Blocks.YELLOW_WOOL); + } + + public Block centerMarkerBlock() { + return resolveBlock(centerMarkerBlockId(), Blocks.GRAY_WOOL); + } + + public ItemStack borderBlockItem() { + return blockItem(borderBlockId); + } + + public ItemStack fillBlockItem() { + return blockItem(fillBlockId); + } + + public ItemStack centerBlockItem() { + return blockItem(centerBlockId); + } + + public void write(FriendlyByteBuf buffer) { + buffer.writeResourceLocation(borderBlockId); + buffer.writeResourceLocation(fillBlockId); + buffer.writeResourceLocation(centerBlockId); + buffer.writeVarInt(platformLayers); + buffer.writeInt(platformStartY); + buffer.writeBoolean(generateBedrock); + buffer.writeBoolean(bedrockAtBottom); + + buffer.writeResourceLocation(boundaryBlockAId()); + buffer.writeResourceLocation(boundaryBlockBId()); + buffer.writeVarInt(boundaryIntervalX()); + buffer.writeVarInt(boundaryIntervalZ()); + buffer.writeVarInt(roadWidth()); + buffer.writeVarInt(mode().ordinal()); + buffer.writeResourceLocation(roadBlockAId()); + buffer.writeResourceLocation(roadBlockBId()); + buffer.writeResourceLocation(roadBlockCId()); + buffer.writeBoolean(centerMarkerEnabled()); + buffer.writeResourceLocation(centerMarkerBlockId()); + } + + public static DimensionGenerationConfig read(FriendlyByteBuf buffer) { + ResourceLocation border = buffer.readResourceLocation(); + ResourceLocation fill = buffer.readResourceLocation(); + ResourceLocation center = buffer.readResourceLocation(); + int platformLayers = buffer.readVarInt(); + int platformStartY = buffer.readInt(); + boolean generateBedrock = buffer.readBoolean(); + boolean bedrockAtBottom = buffer.readBoolean(); + + Features features = new Features( + buffer.readResourceLocation(), + buffer.readResourceLocation(), + buffer.readVarInt(), + buffer.readVarInt(), + buffer.readVarInt(), + Mode.fromOrdinal(buffer.readVarInt()), + buffer.readResourceLocation(), + buffer.readResourceLocation(), + buffer.readResourceLocation(), + buffer.readBoolean(), + buffer.readResourceLocation()); + return new DimensionGenerationConfig(border, fill, center, platformLayers, platformStartY, + generateBedrock, bedrockAtBottom, features); + } + + public CompoundTag save(CompoundTag tag) { + tag.putString("border_block", borderBlockId.toString()); + tag.putString("fill_block", fillBlockId.toString()); + tag.putString("center_block", centerBlockId.toString()); + tag.putInt("platform_layers", platformLayers); + tag.putInt("platform_start_y", platformStartY); + tag.putBoolean("generate_bedrock", generateBedrock); + tag.putBoolean("bedrock_at_bottom", bedrockAtBottom); + + CompoundTag featureTag = new CompoundTag(); + featureTag.putString("boundary_block_a", boundaryBlockAId().toString()); + featureTag.putString("boundary_block_b", boundaryBlockBId().toString()); + featureTag.putInt("boundary_interval_x", boundaryIntervalX()); + featureTag.putInt("boundary_interval_z", boundaryIntervalZ()); + featureTag.putInt("road_width", roadWidth()); + featureTag.putString("mode", mode().name()); + featureTag.putString("road_block_a", roadBlockAId().toString()); + featureTag.putString("road_block_b", roadBlockBId().toString()); + featureTag.putString("road_block_c", roadBlockCId().toString()); + featureTag.putBoolean("center_marker_enabled", centerMarkerEnabled()); + featureTag.putString("center_marker_block", centerMarkerBlockId().toString()); + tag.put("features", featureTag); + return tag; + } + + public static DimensionGenerationConfig load(CompoundTag tag) { + DimensionGenerationConfig defaults = defaults(); + Features features = Features.defaults(); + if (tag.contains("features")) { + CompoundTag featureTag = tag.getCompound("features"); + features = new Features( + readId(featureTag, "boundary_block_a", features.boundaryBlockAId()), + readId(featureTag, "boundary_block_b", features.boundaryBlockBId()), + featureTag.contains("boundary_interval_x") + ? featureTag.getInt("boundary_interval_x") : features.boundaryIntervalX(), + featureTag.contains("boundary_interval_z") + ? featureTag.getInt("boundary_interval_z") : features.boundaryIntervalZ(), + featureTag.contains("road_width") ? featureTag.getInt("road_width") : features.roadWidth(), + Mode.byName(featureTag.contains("mode") + ? featureTag.getString("mode") : features.mode().name()), + readId(featureTag, "road_block_a", features.roadBlockAId()), + readId(featureTag, "road_block_b", features.roadBlockBId()), + readId(featureTag, "road_block_c", features.roadBlockCId()), + featureTag.contains("center_marker_enabled") + && featureTag.getBoolean("center_marker_enabled"), + readId(featureTag, "center_marker_block", features.centerMarkerBlockId())); + } + return new DimensionGenerationConfig( + readId(tag, "border_block", defaults.borderBlockId), + readId(tag, "fill_block", defaults.fillBlockId), + readId(tag, "center_block", defaults.centerBlockId), + tag.contains("platform_layers") ? tag.getInt("platform_layers") : defaults.platformLayers, + tag.contains("platform_start_y") ? tag.getInt("platform_start_y") : defaults.platformStartY, + tag.contains("generate_bedrock") ? tag.getBoolean("generate_bedrock") : defaults.generateBedrock, + tag.contains("bedrock_at_bottom") ? tag.getBoolean("bedrock_at_bottom") : defaults.bedrockAtBottom, + features).normalized(); + } + + /* ================= 预设导入导出 ================= */ + + /** 预设 JSON 的格式版本;导入时版本不匹配即拒绝。 */ + private static final int PRESET_VERSION = 1; + /** 预设文本的最大字节数,避免超长内容进入剪贴板与网络包。 */ + public static final int MAX_PRESET_BYTES = 8192; + + /** 把当前配置序列化成可粘贴分享的 JSON 文本。 */ + public String toPresetJson() { + JsonObject root = new JsonObject(); + root.addProperty("version", PRESET_VERSION); + root.addProperty("borderBlock", borderBlockId.toString()); + root.addProperty("fillBlock", fillBlockId.toString()); + root.addProperty("centerBlock", centerBlockId.toString()); + root.addProperty("platformLayers", platformLayers); + root.addProperty("platformStartY", platformStartY); + root.addProperty("generateBedrock", generateBedrock); + root.addProperty("bedrockAtBottom", bedrockAtBottom); + root.addProperty("boundaryBlockA", boundaryBlockAId().toString()); + root.addProperty("boundaryBlockB", boundaryBlockBId().toString()); + root.addProperty("boundaryIntervalX", boundaryIntervalX()); + root.addProperty("boundaryIntervalZ", boundaryIntervalZ()); + root.addProperty("roadWidth", roadWidth()); + root.addProperty("mode", mode().name()); + root.addProperty("roadBlockA", roadBlockAId().toString()); + root.addProperty("roadBlockB", roadBlockBId().toString()); + root.addProperty("roadBlockC", roadBlockCId().toString()); + root.addProperty("centerMarkerEnabled", centerMarkerEnabled()); + root.addProperty("centerMarkerBlock", centerMarkerBlockId().toString()); + return root.toString(); + } + + /** + * 解析导入的预设文本。任何结构缺失、类型不符或方块被禁用都会抛出异常, + * 由调用方转成界面提示,而不是静默套用一份残缺配置。 + */ + public static DimensionGenerationConfig fromPresetJson(String text) throws PresetException { + if (text == null || text.isBlank()) { + throw new PresetException(PresetError.INVALID_TEXT); + } + if (text.getBytes(java.nio.charset.StandardCharsets.UTF_8).length > MAX_PRESET_BYTES) { + throw new PresetException(PresetError.LIMIT); + } + try { + JsonElement parsed = JsonParser.parseString(text); + if (!parsed.isJsonObject()) { + throw new PresetException(PresetError.INVALID_TEXT); + } + JsonObject root = parsed.getAsJsonObject(); + if (!root.has("version")) { + throw new PresetException(PresetError.INVALID_STRUCTURE); + } + if (root.get("version").getAsInt() != PRESET_VERSION) { + throw new PresetException(PresetError.UNSUPPORTED_VERSION); + } + + Features features = new Features( + readPresetId(root, "boundaryBlockA"), + readPresetId(root, "boundaryBlockB"), + readPresetInt(root, "boundaryIntervalX"), + readPresetInt(root, "boundaryIntervalZ"), + readPresetInt(root, "roadWidth"), + Mode.byName(readPresetString(root, "mode")), + readPresetId(root, "roadBlockA"), + readPresetId(root, "roadBlockB"), + readPresetId(root, "roadBlockC"), + readPresetBool(root, "centerMarkerEnabled"), + readPresetId(root, "centerMarkerBlock")); + + DimensionGenerationConfig config = new DimensionGenerationConfig( + readPresetId(root, "borderBlock"), + readPresetId(root, "fillBlock"), + readPresetId(root, "centerBlock"), + readPresetInt(root, "platformLayers"), + readPresetInt(root, "platformStartY"), + readPresetBool(root, "generateBedrock"), + readPresetBool(root, "bedrockAtBottom"), + features).normalized(); + + if (!config.isValid()) { + throw new PresetException(PresetError.INVALID_STRUCTURE); + } + if (!config.hasAllowedBlockIds()) { + throw new PresetException(PresetError.BLOCKED_BLOCK); + } + return config; + } catch (PresetException exception) { + throw exception; + } catch (JsonParseException | IllegalStateException | UnsupportedOperationException + | ClassCastException | NumberFormatException exception) { + throw new PresetException(PresetError.INVALID_TEXT); + } + } + + private static String readPresetString(JsonObject root, String key) throws PresetException { + JsonElement value = root.get(key); + if (value == null || !value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString()) { + throw new PresetException(PresetError.INVALID_STRUCTURE); + } + return value.getAsString(); + } + + private static int readPresetInt(JsonObject root, String key) throws PresetException { + JsonElement value = root.get(key); + if (value == null || !value.isJsonPrimitive() || !value.getAsJsonPrimitive().isNumber()) { + throw new PresetException(PresetError.INVALID_STRUCTURE); + } + return value.getAsInt(); + } + + private static boolean readPresetBool(JsonObject root, String key) throws PresetException { + JsonElement value = root.get(key); + if (value == null || !value.isJsonPrimitive() || !value.getAsJsonPrimitive().isBoolean()) { + throw new PresetException(PresetError.INVALID_STRUCTURE); + } + return value.getAsBoolean(); + } + + private static ResourceLocation readPresetId(JsonObject root, String key) throws PresetException { + ResourceLocation id = ResourceLocation.tryParse(readPresetString(root, key)); + if (id == null) { + throw new PresetException(PresetError.INVALID_STRUCTURE); + } + return id; + } + + /** 预设导入的失败原因,用于映射到界面提示。 */ + public enum PresetError { + INVALID_TEXT, + UNSUPPORTED_VERSION, + INVALID_STRUCTURE, + BLOCKED_BLOCK, + LIMIT + } + + public static final class PresetException extends Exception { + private final PresetError error; + + public PresetException(PresetError error) { + super(error.name()); + this.error = error; + } + + public PresetError error() { + return error; + } + } + + public static ResourceLocation blockId(ItemStack stack) { + if (!(stack.getItem() instanceof BlockItem blockItem)) return null; + return BuiltInRegistries.BLOCK.getKey(blockItem.getBlock()); + } + + private static ResourceLocation readId(CompoundTag tag, String key, ResourceLocation fallback) { + ResourceLocation value = ResourceLocation.tryParse(tag.getString(key)); + return value == null ? fallback : value; + } + + private static Block resolveBlock(ResourceLocation id, Block fallback) { + Block block = BuiltInRegistries.BLOCK.get(id); + return block == Blocks.AIR ? fallback : block; + } + + private static ItemStack blockItem(ResourceLocation id) { + Block block = BuiltInRegistries.BLOCK.get(id); + if (block == Blocks.AIR || block.asItem() == Items.AIR) return ItemStack.EMPTY; + return new ItemStack(block.asItem()); + } +} diff --git a/src/main/java/com/sorrowmist/useless/world/dimension/PlatformLayout.java b/src/main/java/com/sorrowmist/useless/world/dimension/PlatformLayout.java new file mode 100644 index 00000000..e1ce7cc8 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/world/dimension/PlatformLayout.java @@ -0,0 +1,285 @@ +package com.sorrowmist.useless.world.dimension; + +/* + * Portions of the boundary, road, and center-marker generation algorithm are + * adapted from GT New Horizons/PersonalSpace: + * https://github.com/GTNewHorizons/PersonalSpace + * PersonalSpace is licensed under LGPL-3.0; see LICENSES/PersonalSpace-LGPL-3.0.txt. + * The remaining generator code and platform-specific behavior are original. + */ + +import net.minecraft.world.level.block.state.BlockState; + +import java.util.ArrayList; +import java.util.List; + +/** + * 平台布局的纯函数实现:给定样式与配置,算出某个坐标该放什么方块。 + * + *

这是布局的唯一真相源——世界生成器({@code AbstractPlasticPlatformGenerator})与 + * 客户端预览({@code PlatformPreview})都调用这里,因此预览不会与实际生成结果脱节。 + * + *

双侧安全:本类只依赖 {@link DimensionGenerationConfig}、{@link BlockState} + * 与 {@link PlatformStyle},不引用 {@code WorldGenRegion} / {@code ChunkAccess} / + * {@code ChunkGenerator} 等服务端专属类型,客户端可直接调用。 + */ +public final class PlatformLayout { + + /** 区块内的平台角色,决定该列用边框 / 填充 / 中心方块。 */ + public enum Role { + BORDER, + FILL, + CENTER + } + + private PlatformLayout() { + } + + /* ================= 高度 ================= */ + + public static int topY(DimensionGenerationConfig config) { + return config.platformStartY() + config.platformLayers(); + } + + public static int bottomY(DimensionGenerationConfig config) { + return config.platformStartY(); + } + + public static int bedrockY(DimensionGenerationConfig config, int minY, int maxY) { + if (config.bedrockAtBottom()) return clampY(minY, minY, maxY); + return clampY(bottomY(config), minY, maxY); + } + + /* ================= 平台方块 ================= */ + + public static BlockState platformState(PlatformStyle style, DimensionGenerationConfig config, + int chunkX, int chunkZ, int localX, int localZ) { + if (config.mode() == DimensionGenerationConfig.Mode.MULTI) { + return multiModePlatformState(style, config, chunkX, chunkZ, localX, localZ); + } + return platformBlockState(style, config, localX, localZ); + } + + public static BlockState platformBlockState(PlatformStyle style, DimensionGenerationConfig config, + int localX, int localZ) { + return switch (style.platformRole(localX, localZ)) { + case BORDER -> borderState(config); + case CENTER -> centerState(config); + case FILL -> fillState(config); + }; + } + + public static BlockState borderState(DimensionGenerationConfig config) { + return config.borderBlock().defaultBlockState(); + } + + public static BlockState fillState(DimensionGenerationConfig config) { + return config.fillBlock().defaultBlockState(); + } + + public static BlockState centerState(DimensionGenerationConfig config) { + return config.centerBlock().defaultBlockState(); + } + + /** + * 多联模式:把边界间隔当作合并尺寸,把 sizeX×sizeZ 个区块当成一个整体平台。 + * 边框走向与厚度由样式声明:默认只在合并组的起始列与起始行铺一条边框带, + * 相邻合并组共用它,因此交界处不会叠加出两条边框;二维度改为四边环绕, + * 每条边各铺一格,交界处由相邻两组各出一条边、合成一条两格宽的边框带。 + * 组内部(包括区块之间的接缝)全部是填充方块,中心方块以填充区居中, + * 占 1 格或 2×2 格。 + */ + private static BlockState multiModePlatformState(PlatformStyle style, + DimensionGenerationConfig config, + int chunkX, int chunkZ, + int localX, int localZ) { + int sizeX = Math.max(1, config.boundaryIntervalX()); + int sizeZ = Math.max(1, config.boundaryIntervalZ()); + // 合并区域内的相对坐标,范围分别是 [0, sizeX * 16) 与 [0, sizeZ * 16)。 + int groupX = mod(chunkX, sizeX) * 16 + localX; + int groupZ = mod(chunkZ, sizeZ) * 16 + localZ; + // 厚度由具体生成器声明的多联边框宽度决定,并且不会超过半个合并组。 + int thickness = Math.min(style.multiBorderThickness(), + (Math.min(sizeX, sizeZ) * 16 - 1) / 2); + int widthX = sizeX * 16; + int widthZ = sizeZ * 16; + + if (style.isMultiCenterMarker(groupX, groupZ, widthX, widthZ, thickness)) { + return centerState(config); + } + // 默认只在起始列与起始行铺边框;声明四边环绕的样式则在四个方向各铺一条, + // 相邻两组交界处由两侧各出一条,合成一条两格宽的边框带。 + boolean onBorder = style.multiBorderSurrounds() + ? groupX < thickness || groupX >= widthX - thickness + || groupZ < thickness || groupZ >= widthZ - thickness + : groupX < thickness || groupZ < thickness; + return onBorder ? borderState(config) : fillState(config); + } + + /* ================= 表面装饰 ================= */ + + public static List surfaceLevels(PlatformStyle style, DimensionGenerationConfig config, + int chunkX, int chunkZ, + int localX, int localZ, int minY, int maxY) { + List surfaces = new ArrayList<>(); + int top = topY(config); + if (top >= minY && top < maxY + && !platformState(style, config, chunkX, chunkZ, localX, localZ).isAir()) { + surfaces.add(top); + } + return surfaces; + } + + public static int highestSurface(PlatformStyle style, DimensionGenerationConfig config, + int chunkX, int chunkZ, + int localX, int localZ, int minY, int maxY) { + List surfaces = surfaceLevels(style, config, chunkX, chunkZ, + localX, localZ, minY, maxY); + return surfaces.isEmpty() ? Integer.MIN_VALUE : surfaces.get(surfaces.size() - 1); + } + + /** 顶层装饰方块;无装饰时返回 {@code null}。 */ + public static BlockState surfaceDecoration(PlatformStyle style, DimensionGenerationConfig config, + int chunkX, int chunkZ, int localX, int localZ) { + // 多联模式不生成马路与边界装饰,平台由填充方块无缝连成一片。 + if (config.mode() == DimensionGenerationConfig.Mode.MULTI) return null; + int intervalX = config.boundaryIntervalX(); + int intervalZ = config.boundaryIntervalZ(); + int roadWidth = config.roadWidth(); + if (intervalX <= 0 && intervalZ <= 0) return null; + + int periodX = intervalX > 0 ? intervalX + roadWidth : 0; + int periodZ = intervalZ > 0 ? intervalZ + roadWidth : 0; + int chunkModX = periodX > 0 ? mod(chunkX, periodX) : 0; + int chunkModZ = periodZ > 0 ? mod(chunkZ, periodZ) : 0; + boolean roadX = roadWidth > 0 && intervalX > 0 && chunkModX >= intervalX; + boolean roadZ = roadWidth > 0 && intervalZ > 0 && chunkModZ >= intervalZ; + int roadStartBoundaryWidth = style.roadStartBoundaryWidth(); + int roadCenterLineWidth = style.roadCenterLineWidth(); + int worldX = chunkX * 16 + localX; + int worldZ = chunkZ * 16 + localZ; + + if (roadX || roadZ) { + int roadWidthBlocks = roadWidth * 16 - roadStartBoundaryWidth; + if (roadX && roadZ) { + int rawOffsetX = (chunkModX - intervalX) * 16 + localX; + int rawOffsetZ = (chunkModZ - intervalZ) * 16 + localZ; + int offsetX = rawOffsetX - roadStartBoundaryWidth; + int offsetZ = rawOffsetZ - roadStartBoundaryWidth; + if (offsetX < 0 || offsetZ < 0) { + if (!style.unshiftedRoadIntersectionLayout()) { + return boundaryState(config, worldX, worldZ); + } + // The shared negative corner is still the boundary tile; + // only the adjacent tiles belong to the road edge. + if (rawOffsetX < roadStartBoundaryWidth + && rawOffsetZ < roadStartBoundaryWidth) { + return boundaryState(config, worldX, worldZ); + } + } + boolean unshifted = style.unshiftedRoadIntersectionLayout(); + int intersectionOffsetX = unshifted ? rawOffsetX : offsetX; + int intersectionOffsetZ = unshifted ? rawOffsetZ : offsetZ; + int intersectionWidth = unshifted ? roadWidth * 16 : roadWidthBlocks; + boolean onIntersectionEdgeX = unshifted + ? rawOffsetX <= roadStartBoundaryWidth || rawOffsetX == intersectionWidth - 1 + : intersectionOffsetX == 0 || intersectionOffsetX == intersectionWidth - 1; + boolean onIntersectionEdgeZ = unshifted + ? rawOffsetZ <= roadStartBoundaryWidth || rawOffsetZ == intersectionWidth - 1 + : intersectionOffsetZ == 0 || intersectionOffsetZ == intersectionWidth - 1; + if (config.mode() == DimensionGenerationConfig.Mode.ROAD + && onIntersectionEdgeX && onIntersectionEdgeZ) { + return config.roadBlockB().defaultBlockState(); + } + return config.roadBlockA().defaultBlockState(); + } + if (roadX) { + int offset = (chunkModX - intervalX) * 16 + localX - roadStartBoundaryWidth; + if (offset < 0) return boundaryState(config, worldX, worldZ); + return roadBlockState(config, offset, worldZ, roadWidthBlocks, roadCenterLineWidth); + } + int offset = (chunkModZ - intervalZ) * 16 + localZ - roadStartBoundaryWidth; + if (offset < 0) return boundaryState(config, worldX, worldZ); + return roadBlockState(config, offset, worldX, roadWidthBlocks, roadCenterLineWidth); + } + + // Boundary strips belong to the road layout. A zero-width road means + // that neither the road nor its boundary should be generated. + if (roadWidth > 0) { + boolean boundaryX = isBoundaryChunk(chunkX, intervalX, roadWidth); + boolean previousBoundaryX = roadStartBoundaryWidth == 0 + && isPreviousBoundaryChunk(chunkX, intervalX, roadWidth); + boolean boundaryZ = isBoundaryChunk(chunkZ, intervalZ, roadWidth); + boolean previousBoundaryZ = roadStartBoundaryWidth == 0 + && isPreviousBoundaryChunk(chunkZ, intervalZ, roadWidth); + if ((boundaryX && localX == 0) || (previousBoundaryX && localX == 15) + || (boundaryZ && localZ == 0) || (previousBoundaryZ && localZ == 15)) { + return boundaryState(config, worldX, worldZ); + } + } + + if (config.centerMarkerEnabled() && intervalX > 0 && intervalZ > 0) { + int periodBlocksX = periodX * 16; + int periodBlocksZ = periodZ * 16; + int areaWidthX = intervalX * 16; + int areaWidthZ = intervalZ * 16; + int centerX = intervalX * 8; + int centerZ = intervalZ * 8; + int areaX = mod(chunkX * 16 + localX, periodBlocksX); + int areaZ = mod(chunkZ * 16 + localZ, periodBlocksZ); + if (areaX < areaWidthX && areaZ < areaWidthZ + && style.isCenterMarkerPosition(areaX, areaZ, centerX, centerZ)) { + return config.centerMarkerBlock().defaultBlockState(); + } + } + return null; + } + + private static BlockState roadBlockState(DimensionGenerationConfig config, int offsetInRoad, + int alongRoad, int roadWidthBlocks, int centerLineWidth) { + if (offsetInRoad == 0 || offsetInRoad == roadWidthBlocks - 1) { + return config.roadBlockB().defaultBlockState(); + } + if (roadWidthBlocks >= 4) { + int center = roadWidthBlocks / 2; + boolean onCenterLine = centerLineWidth == 1 + ? offsetInRoad == center + : offsetInRoad == center || offsetInRoad == center - 1; + if (onCenterLine + && mod(alongRoad + 2, 8) < 4) { + return config.roadBlockC().defaultBlockState(); + } + } + return config.roadBlockA().defaultBlockState(); + } + + private static BlockState boundaryState(DimensionGenerationConfig config, int worldX, int worldZ) { + return (((worldX + worldZ) & 1) == 0 + ? config.boundaryBlockA() : config.boundaryBlockB()).defaultBlockState(); + } + + /* ================= 周期判定 ================= */ + + public static boolean isBoundaryChunk(int chunk, int interval, int roadWidth) { + if (interval <= 0) return false; + return mod(chunk, interval + roadWidth) == 0; + } + + private static boolean isPreviousBoundaryChunk(int chunk, int interval, int roadWidth) { + if (interval <= 0) return false; + int period = interval + roadWidth; + if (roadWidth > 0) return mod(chunk, period) == interval - 1; + return mod(chunk + 1, period) == 0; + } + + /* ================= 工具 ================= */ + + public static int mod(int value, int divisor) { + int result = value % divisor; + return result < 0 ? result + divisor : result; + } + + public static int clampY(int y, int minY, int maxY) { + return Math.max(minY, Math.min(maxY - 1, y)); + } +} diff --git a/src/main/java/com/sorrowmist/useless/world/dimension/PlatformStyle.java b/src/main/java/com/sorrowmist/useless/world/dimension/PlatformStyle.java new file mode 100644 index 00000000..8c925653 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/world/dimension/PlatformStyle.java @@ -0,0 +1,197 @@ +package com.sorrowmist.useless.world.dimension; + +/** + * 每个维度各自的平台样式参数。 + * + *

这些参数原先分散在 {@code UselessDimGen} / {@code UselessDimGen2} / {@code UselessDimGen3} + * 的覆写方法里。集中到本枚举后有两个好处: + *

    + *
  • 布局算法({@link PlatformLayout})只需要一个 {@code PlatformStyle} 参数, + * 生成器与客户端预览可以共用同一份实现,不会各自漂移;
  • + *
  • 枚举是纯数据、双侧安全,客户端无需实例化 {@code ChunkGenerator} 即可取到样式。
  • + *
+ * + *

默认实现对应原 {@code AbstractPlasticPlatformGenerator} 里的默认方法体(二维度风格)。 + */ +public enum PlatformStyle { + + /** 一维度:中心 1 格 (8,8),边框为 x==0 或 z==0,多联边框 1 格厚。 */ + STYLE_1 { + @Override + public PlatformLayout.Role platformRole(int localX, int localZ) { + if (localX == 8 && localZ == 8) { + return PlatformLayout.Role.CENTER; + } + return localX == 0 || localZ == 0 + ? PlatformLayout.Role.BORDER : PlatformLayout.Role.FILL; + } + + @Override + public int roadStartBoundaryWidth() { + return 1; + } + + @Override + public int roadCenterLineWidth() { + return 1; + } + + @Override + public boolean unshiftedRoadIntersectionLayout() { + return true; + } + + @Override + public String debugName() { + return "Plastic Platform - Style 1 (2x2 Center)"; + } + }, + + /** 二维度:中心 2×2 (7..8,7..8),边框为最外一圈,多联边框四边各 1 格。 */ + STYLE_2 { + @Override + public PlatformLayout.Role platformRole(int localX, int localZ) { + if (localX >= 7 && localX <= 8 && localZ >= 7 && localZ <= 8) { + return PlatformLayout.Role.CENTER; + } + if (localX >= 1 && localX <= 14 && localZ >= 1 && localZ <= 14) { + return PlatformLayout.Role.FILL; + } + return PlatformLayout.Role.BORDER; + } + + /** 二维度多联边框四边环绕,每条边 1 格;相邻两组交界处合成 2 格宽。 */ + @Override + public int multiBorderThickness() { + return 1; + } + + @Override + public boolean multiBorderSurrounds() { + return true; + } + + @Override + public boolean isCenterMarkerPosition(int areaX, int areaZ, int centerX, int centerZ) { + // An even boundary interval places the area center on a chunk seam. + // Cover the complete 2x2 center in that case (for example, 2x3). + if ((centerX & 15) == 0 || (centerZ & 15) == 0) { + return areaX >= centerX - 1 && areaX <= centerX + && areaZ >= centerZ - 1 && areaZ <= centerZ; + } + + // Keep the two-block marker centered on a fixed row. + return (areaX == centerX - 1 || areaX == centerX) && areaZ == centerZ; + } + + @Override + public String debugName() { + return "Plastic Platform - Style 2 (L-Shaped Border)"; + } + }, + + /** 三维度:中心 1 格 (8,8),边框为 2 格厚外圈,多联边框 3 格厚。 */ + STYLE_3 { + @Override + public PlatformLayout.Role platformRole(int localX, int localZ) { + if (localX == 8 && localZ == 8) { + return PlatformLayout.Role.CENTER; + } + return localX <= 1 || localX == 15 || localZ <= 1 || localZ == 15 + ? PlatformLayout.Role.BORDER : PlatformLayout.Role.FILL; + } + + /** 三维度多联边框固定为 3 格宽。 */ + @Override + public int multiBorderThickness() { + return 3; + } + + @Override + public int roadStartBoundaryWidth() { + return 1; + } + + @Override + public int roadCenterLineWidth() { + return 1; + } + + @Override + public boolean unshiftedRoadIntersectionLayout() { + return true; + } + + @Override + public String debugName() { + return "Plastic Platform - Style 3 (Thick Border)"; + } + }; + + /** 区块内的平台角色,决定该列用边框 / 填充 / 中心方块。 */ + public abstract PlatformLayout.Role platformRole(int localX, int localZ); + + /** 道路起始侧保留给边界的宽度(格)。 */ + public int roadStartBoundaryWidth() { + return 0; + } + + /** 道路中心标线的宽度(格)。 */ + public int roadCenterLineWidth() { + return 2; + } + + /** 交叉口是否忽略道路起始侧的边界内缩。 */ + public boolean unshiftedRoadIntersectionLayout() { + return false; + } + + /** 多联模式下合并组起始侧的边框宽度(格)。 */ + public int multiBorderThickness() { + return 1; + } + + /** + * 多联边框是否四边环绕。 + * + *

{@code false}(默认)时只在合并组的起始列与起始行各铺一条边框带, + * 相邻合并组共用它,交界处只有一条边框;{@code true} 时四条边各铺一条, + * 相邻合并组在交界处各出一条边,合成一条两倍于 + * {@link #multiBorderThickness()} 宽的边框带,使边框均分到交界两侧。 + */ + public boolean multiBorderSurrounds() { + return false; + } + + /** + * 多联合并组的中心标记判定,以填充区(合并区域去掉起始侧边框后的部分)居中。 + * + *

填充区宽度为奇数时中心格唯一,取 1 格;为偶数时中心落在两格之间, + * 两格一并取用以保持左右对称。两轴各自独立判定,因此非正方形的合并尺寸 + * 也能在每条轴上分别取到正确的中心位置。 + * + * @param thickness 合并组起始侧的边框宽度(格),与布局算法使用同一数值 + */ + public boolean isMultiCenterMarker(int groupX, int groupZ, + int widthX, int widthZ, int thickness) { + // 四边环绕时填充区两侧各内缩一条边框,总内缩量为两倍厚度; + // 默认单侧边框模式只内缩一条。 + int inset = multiBorderSurrounds() ? thickness * 2 : thickness; + int innerX = Math.max(1, widthX - inset); + int innerZ = Math.max(1, widthZ - inset); + int centerX = thickness + innerX / 2; + int centerZ = thickness + innerZ / 2; + int minX = innerX % 2 == 0 ? centerX - 1 : centerX; + int minZ = innerZ % 2 == 0 ? centerZ - 1 : centerZ; + return groupX >= minX && groupX <= centerX + && groupZ >= minZ && groupZ <= centerZ; + } + + /** 马路模式下单个平台区域内的中心标记判定。默认只占正中心 1 格。 */ + public boolean isCenterMarkerPosition(int areaX, int areaZ, int centerX, int centerZ) { + return areaX == centerX && areaZ == centerZ; + } + + /** 调试界面显示的名称。 */ + public abstract String debugName(); +} diff --git a/src/main/java/com/sorrowmist/useless/world/dimension/UselessDimGen.java b/src/main/java/com/sorrowmist/useless/world/dimension/UselessDimGen.java new file mode 100644 index 00000000..0c61fe6c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/world/dimension/UselessDimGen.java @@ -0,0 +1,28 @@ +// UselessDimGen.java +package com.sorrowmist.useless.world.dimension; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.chunk.ChunkGenerator; +import org.jetbrains.annotations.NotNull; + +/** 一维度生成器。布局细节见 {@link PlatformStyle#STYLE_1} 与 {@link PlatformLayout}。 */ +public class UselessDimGen extends AbstractPlasticPlatformGenerator { + static final Codec CODEC = RecordCodecBuilder.create( + instance -> + instance.group(BiomeSource.CODEC.fieldOf("biome_source") + .forGetter(g -> g.biomeSource)) + .apply(instance, UselessDimGen::new)); + + private UselessDimGen(BiomeSource biomeSource) { + super(biomeSource); + } + + @Override protected @NotNull Codec codec() {return CODEC;} + + @Override + protected PlatformStyle style() { + return PlatformStyle.STYLE_1; + } +} diff --git a/src/main/java/com/sorrowmist/useless/world/dimension/UselessDimGen2.java b/src/main/java/com/sorrowmist/useless/world/dimension/UselessDimGen2.java new file mode 100644 index 00000000..9fa8c57b --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/world/dimension/UselessDimGen2.java @@ -0,0 +1,28 @@ +// UselessDimGen2.java +package com.sorrowmist.useless.world.dimension; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.chunk.ChunkGenerator; +import org.jetbrains.annotations.NotNull; + +/** 二维度生成器。布局细节见 {@link PlatformStyle#STYLE_2} 与 {@link PlatformLayout}。 */ +public class UselessDimGen2 extends AbstractPlasticPlatformGenerator { + static final Codec CODEC = RecordCodecBuilder.create( + instance -> + instance.group(BiomeSource.CODEC.fieldOf("biome_source") + .forGetter(g -> g.biomeSource)) + .apply(instance, UselessDimGen2::new)); + + private UselessDimGen2(BiomeSource biomeSource) { + super(biomeSource); + } + + @Override protected @NotNull Codec codec() {return CODEC;} + + @Override + protected PlatformStyle style() { + return PlatformStyle.STYLE_2; + } +} diff --git a/src/main/java/com/sorrowmist/useless/world/dimension/UselessDimGen3.java b/src/main/java/com/sorrowmist/useless/world/dimension/UselessDimGen3.java new file mode 100644 index 00000000..c6f3ea76 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/world/dimension/UselessDimGen3.java @@ -0,0 +1,28 @@ +// UselessDimGen3.java +package com.sorrowmist.useless.world.dimension; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.chunk.ChunkGenerator; +import org.jetbrains.annotations.NotNull; + +/** 三维度生成器。布局细节见 {@link PlatformStyle#STYLE_3} 与 {@link PlatformLayout}。 */ +public class UselessDimGen3 extends AbstractPlasticPlatformGenerator { + static final Codec CODEC = RecordCodecBuilder.create( + instance -> + instance.group(BiomeSource.CODEC.fieldOf("biome_source") + .forGetter(g -> g.biomeSource)) + .apply(instance, UselessDimGen3::new)); + + private UselessDimGen3(BiomeSource biomeSource) { + super(biomeSource); + } + + @Override protected @NotNull Codec codec() {return CODEC;} + + @Override + protected PlatformStyle style() { + return PlatformStyle.STYLE_3; + } +} diff --git a/src/main/java/com/sorrowmist/useless/world/dimension/UselessDimensionConfigManager.java b/src/main/java/com/sorrowmist/useless/world/dimension/UselessDimensionConfigManager.java new file mode 100644 index 00000000..d29682d7 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/world/dimension/UselessDimensionConfigManager.java @@ -0,0 +1,51 @@ +package com.sorrowmist.useless.world.dimension; + +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.ChunkGenerator; + +public final class UselessDimensionConfigManager { + private UselessDimensionConfigManager() { + } + + public static boolean isConfigured(MinecraftServer server, ResourceKey dimension) { + return UselessDimensionConfigSavedData.get(server).isConfigured(dimension); + } + + public static DimensionGenerationConfig get( + MinecraftServer server, ResourceKey dimension) { + return UselessDimensionConfigSavedData.get(server).get(dimension) + .orElseGet(DimensionGenerationConfig::defaults); + } + + public static DimensionGenerationConfig save( + MinecraftServer server, + ResourceKey dimension, + DimensionGenerationConfig config, + boolean firstSetup) { + UselessDimensionConfigSavedData data = UselessDimensionConfigSavedData.get(server); + DimensionGenerationConfig applied = firstSetup + ? data.putIfAbsent(dimension, config) + : data.put(dimension, config); + ServerLevel level = server.getLevel(dimension); + if (level != null) apply(level); + return applied; + } + + public static void apply(ServerLevel level) { + if (level == null) return; + if (!UselessDimensions.isUselessDimension(level.dimension())) return; + ChunkGenerator generator = level.getChunkSource().getGenerator(); + if (generator instanceof AbstractPlasticPlatformGenerator platformGenerator) { + platformGenerator.setConfiguration(get(level.getServer(), level.dimension())); + } + } + + public static void applyAll(MinecraftServer server) { + for (ServerLevel level : server.getAllLevels()) { + apply(level); + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/world/dimension/UselessDimensionConfigSavedData.java b/src/main/java/com/sorrowmist/useless/world/dimension/UselessDimensionConfigSavedData.java new file mode 100644 index 00000000..c097779d --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/world/dimension/UselessDimensionConfigSavedData.java @@ -0,0 +1,89 @@ +package com.sorrowmist.useless.world.dimension; + +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.saveddata.SavedData; +import net.minecraft.server.MinecraftServer; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public final class UselessDimensionConfigSavedData extends SavedData { + private static final String FILE_ID = "useless_mod_dimension_generation"; + private static final String DIMENSIONS_TAG = "dimensions"; + + private final Map configurations = new HashMap<>(); + + UselessDimensionConfigSavedData() { + } + + public static UselessDimensionConfigSavedData get(MinecraftServer server) { + return server.overworld().getDataStorage().computeIfAbsent( + tag -> load(tag, server.registryAccess()), UselessDimensionConfigSavedData::new, FILE_ID); + } + + public Optional get(ResourceKey dimension) { + return Optional.ofNullable(configurations.get(dimension.location())); + } + + public boolean isConfigured(ResourceKey dimension) { + return configurations.containsKey(dimension.location()); + } + + public DimensionGenerationConfig putIfAbsent( + ResourceKey dimension, DimensionGenerationConfig config) { + DimensionGenerationConfig existing = configurations.get(dimension.location()); + if (existing != null) return existing; + DimensionGenerationConfig normalized = normalizeAndValidate(config); + configurations.put(dimension.location(), normalized); + setDirty(); + return normalized; + } + + public DimensionGenerationConfig put( + ResourceKey dimension, DimensionGenerationConfig config) { + DimensionGenerationConfig normalized = normalizeAndValidate(config); + configurations.put(dimension.location(), normalized); + setDirty(); + return normalized; + } + + private static DimensionGenerationConfig normalizeAndValidate(DimensionGenerationConfig config) { + DimensionGenerationConfig normalized = config.normalized(); + if (!normalized.hasAllowedBlockIds()) { + throw new IllegalArgumentException("Useless Dimension configuration contains a blocked block"); + } + return normalized; + } + + @Override + public CompoundTag save(CompoundTag tag) { + return save(tag, net.minecraft.core.RegistryAccess.EMPTY); + } + + public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) { + CompoundTag dimensions = new CompoundTag(); + configurations.forEach((id, config) -> dimensions.put(id.toString(), config.save(new CompoundTag()))); + tag.put(DIMENSIONS_TAG, dimensions); + return tag; + } + + static UselessDimensionConfigSavedData load( + CompoundTag tag, HolderLookup.Provider registries) { + UselessDimensionConfigSavedData data = new UselessDimensionConfigSavedData(); + if (!tag.contains(DIMENSIONS_TAG)) return data; + + CompoundTag dimensions = tag.getCompound(DIMENSIONS_TAG); + for (String key : dimensions.getAllKeys()) { + ResourceLocation id = ResourceLocation.tryParse(key); + if (id != null) { + data.configurations.put(id, DimensionGenerationConfig.load(dimensions.getCompound(key))); + } + } + return data; + } +} diff --git a/src/main/java/com/sorrowmist/useless/world/dimension/UselessDimensions.java b/src/main/java/com/sorrowmist/useless/world/dimension/UselessDimensions.java new file mode 100644 index 00000000..984bd155 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/world/dimension/UselessDimensions.java @@ -0,0 +1,69 @@ +package com.sorrowmist.useless.world.dimension; + +import com.mojang.serialization.Codec; +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.world.teleport.AbstractDimensionTeleporter; +import com.sorrowmist.useless.world.teleport.UselessDimTeleporter; +import com.sorrowmist.useless.world.teleport.UselessDimTeleporter2; +import com.sorrowmist.useless.world.teleport.UselessDimTeleporter3; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraftforge.eventbus.api.IEventBus; +import com.sorrowmist.useless.compat.forge1201.registry.DeferredRegister; + +import java.util.function.Supplier; + +public class UselessDimensions { + + public static final ResourceKey USELESSDIM_KEY = key("uselessdim"); + public static final ResourceKey USELESSDIM_2_KEY = key("uselessdim2"); + public static final ResourceKey USELESSDIM_3_KEY = key("uselessdim3"); + + private static final DeferredRegister> CHUNK_GENERATORS = + DeferredRegister.create(Registries.CHUNK_GENERATOR, UselessMod.MODID); + + public static final Supplier> USELESSDIM_GEN = + CHUNK_GENERATORS.register("uselessdim_gen", () -> UselessDimGen.CODEC); + + public static final Supplier> USELESSDIM_GEN_2 = + CHUNK_GENERATORS.register("uselessdim_gen_2", () -> UselessDimGen2.CODEC); + + public static final Supplier> USELESSDIM_GEN_3 = + CHUNK_GENERATORS.register("uselessdim_gen_3", () -> UselessDimGen3.CODEC); + + public static boolean isUselessDimension(ResourceKey dimension) { + return USELESSDIM_KEY.equals(dimension) + || USELESSDIM_2_KEY.equals(dimension) + || USELESSDIM_3_KEY.equals(dimension); + } + + public static AbstractDimensionTeleporter teleporterFor(ResourceKey dimension) { + if (USELESSDIM_KEY.equals(dimension)) return new UselessDimTeleporter(); + if (USELESSDIM_2_KEY.equals(dimension)) return new UselessDimTeleporter2(); + if (USELESSDIM_3_KEY.equals(dimension)) return new UselessDimTeleporter3(); + return null; + } + + /** + * 该维度的平台样式。样式是纯数据、双侧安全,客户端可直接用它驱动生成预览, + * 无需实例化 {@code ChunkGenerator}。非本模组维度返回 {@code null}。 + */ + public static PlatformStyle styleFor(ResourceKey dimension) { + if (USELESSDIM_KEY.equals(dimension)) return PlatformStyle.STYLE_1; + if (USELESSDIM_2_KEY.equals(dimension)) return PlatformStyle.STYLE_2; + if (USELESSDIM_3_KEY.equals(dimension)) return PlatformStyle.STYLE_3; + return null; + } + + private static ResourceKey key(String path) { + return ResourceKey.create(Registries.DIMENSION, + new ResourceLocation(UselessMod.MODID, path)); + } + + public static void init(IEventBus modEventBus) { + CHUNK_GENERATORS.register(modEventBus); + } +} diff --git a/src/main/java/com/sorrowmist/useless/world/inventory/ExternalInventorySavedData.java b/src/main/java/com/sorrowmist/useless/world/inventory/ExternalInventorySavedData.java new file mode 100644 index 00000000..5c67b71c --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/world/inventory/ExternalInventorySavedData.java @@ -0,0 +1,162 @@ +package com.sorrowmist.useless.world.inventory; + +import com.sorrowmist.useless.core.component.ExternalInventoryReference; +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.server.MinecraftServer; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.saveddata.SavedData; +import net.minecraftforge.items.ItemStackHandler; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +public final class ExternalInventorySavedData extends SavedData { + private static final int CURRENT_VERSION = 1; + private static final String INVENTORY = "Inventory"; + private static final String PAYLOAD = "Payload"; + private static final String KIND = "Kind"; + private static final String VERSION = "Version"; + private static final String BOUND = "Bound"; + private static final String OWNER_DIMENSION = "OwnerDimension"; + private static final String OWNER_POS = "OwnerPos"; + + private final ExternalInventoryReference reference; + private CompoundTag inventory = new CompoundTag(); + /** + * 非物品栏的通用载荷:给「一整块任意 NBT」用(紧凑 F9 的 176 条影子样板总线数据)。 + * 它和 {@link #inventory} 互不影响,同一个文件里只会用到其中一种。 + */ + private CompoundTag payload = new CompoundTag(); + private boolean hasPayload; + private boolean initialized; + private boolean claimed; + private String ownerDimension; + private long ownerPos; + + private ExternalInventorySavedData(ExternalInventoryReference reference) { + this.reference = Objects.requireNonNull(reference, "reference"); + } + + public static ExternalInventorySavedData get(MinecraftServer server, + ExternalInventoryReference reference) { + return server.overworld().getDataStorage().computeIfAbsent( + tag -> load(reference, tag), + () -> new ExternalInventorySavedData(reference), + fileId(reference)); + } + + private static ExternalInventorySavedData load(ExternalInventoryReference reference, CompoundTag tag) { + ExternalInventorySavedData data = new ExternalInventorySavedData(reference); + if (tag.contains(INVENTORY, Tag.TAG_COMPOUND)) { + data.inventory = tag.getCompound(INVENTORY).copy(); + data.initialized = true; + } + if (tag.contains(PAYLOAD, Tag.TAG_COMPOUND)) { + data.payload = tag.getCompound(PAYLOAD).copy(); + data.hasPayload = true; + } + data.claimed = tag.getBoolean(BOUND); + if (tag.contains(OWNER_DIMENSION, Tag.TAG_STRING)) { + data.ownerDimension = tag.getString(OWNER_DIMENSION); + data.ownerPos = tag.getLong(OWNER_POS); + } + return data; + } + + private static String fileId(ExternalInventoryReference reference) { + return "useless_mod_inventory_" + reference.kind().serializedName() + "_" + reference.id(); + } + + public boolean claimAt(Level level, BlockPos pos) { + String dimension = level.dimension().location().toString(); + if (claimed) { + return ownerDimension != null + && dimension.equals(ownerDimension) + && pos.asLong() == ownerPos; + } + claimed = true; + ownerDimension = dimension; + ownerPos = pos.asLong(); + setDirty(); + return true; + } + + public void releaseAt(Level level, BlockPos pos) { + if (!claimed + || ownerDimension == null + || !ownerDimension.equals(level.dimension().location().toString()) + || ownerPos != pos.asLong()) { + return; + } + claimed = false; + ownerDimension = null; + ownerPos = 0L; + setDirty(); + } + + public void loadInto(ItemStackHandler target, HolderLookup.Provider registries) { + CompoundTag tag = inventory.copy(); + tag.remove("Size"); + target.deserializeNBT(tag); + } + + public void saveFrom(ItemStackHandler source, HolderLookup.Provider registries) { + inventory = source.serializeNBT().copy(); + initialized = true; + setDirty(); + } + + // ---------------------------------------------------------------- 通用载荷 + + /** 写入一段原始 NBT(调用方保证它已经是完整的一份,内部会再拷一次避免外部改动)。 */ + public void saveRawPayload(CompoundTag value) { + payload = value.copy(); + hasPayload = true; + initialized = true; + setDirty(); + } + + /** 取出上次写入的原始 NBT;没写过时返回 {@code null}。 */ + @Nullable + public CompoundTag rawPayload() { + return hasPayload ? payload.copy() : null; + } + + public void copyInventoryTo(ExternalInventorySavedData target) { + target.inventory = inventory.copy(); + target.initialized = initialized; + if (hasPayload) { + target.payload = payload.copy(); + target.hasPayload = true; + } + target.setDirty(); + } + + public boolean isInitialized() { + return initialized; + } + + @Override + public CompoundTag save(CompoundTag tag) { + return save(tag, net.minecraft.core.RegistryAccess.EMPTY); + } + + public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) { + tag.putInt(VERSION, CURRENT_VERSION); + tag.putString(KIND, reference.kind().serializedName()); + tag.putBoolean("Initialized", initialized); + tag.putBoolean(BOUND, claimed); + if (claimed && ownerDimension != null) { + tag.putString(OWNER_DIMENSION, ownerDimension); + tag.putLong(OWNER_POS, ownerPos); + } + tag.put(INVENTORY, inventory.copy()); + if (hasPayload) { + tag.put(PAYLOAD, payload.copy()); + } + return tag; + } +} diff --git a/src/main/java/com/sorrowmist/useless/world/inventory/ExternalInventoryStore.java b/src/main/java/com/sorrowmist/useless/world/inventory/ExternalInventoryStore.java new file mode 100644 index 00000000..d6761f05 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/world/inventory/ExternalInventoryStore.java @@ -0,0 +1,127 @@ +package com.sorrowmist.useless.world.inventory; + +import com.sorrowmist.useless.core.component.ExternalInventoryKind; +import com.sorrowmist.useless.core.component.ExternalInventoryReference; +import com.sorrowmist.useless.core.component.UComponents; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.Level; +import net.minecraftforge.items.ItemStackHandler; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; + +public final class ExternalInventoryStore { + private ExternalInventoryStore() { + } + + public static ExternalInventoryReference bindAt( + Level level, + ExternalInventoryKind kind, + @Nullable ExternalInventoryReference requested, + BlockPos pos, + ItemStackHandler inventory, + @Nullable CompoundTag legacyInventory, + HolderLookup.Provider registries) { + ExternalInventoryReference reference = requested != null && requested.kind() == kind + ? requested + : new ExternalInventoryReference(kind, UUID.randomUUID()); + if (level.isClientSide() || level.getServer() == null) return reference; + + ExternalInventorySavedData data = ExternalInventorySavedData.get(level.getServer(), reference); + if (!data.claimAt(level, pos)) { + ExternalInventoryReference fork = new ExternalInventoryReference(kind, UUID.randomUUID()); + ExternalInventorySavedData forked = ExternalInventorySavedData.get(level.getServer(), fork); + data.copyInventoryTo(forked); + forked.claimAt(level, pos); + forked.loadInto(inventory, registries); + return fork; + } + + if (data.isInitialized()) { + data.loadInto(inventory, registries); + } else if (legacyInventory != null) { + inventory.deserializeNBT(legacyInventory.copy()); + data.saveFrom(inventory, registries); + } else { + data.saveFrom(inventory, registries); + } + return reference; + } + + /** + * 认领一份「通用载荷」外置数据(不是物品栏,而是任意一段 NBT,例如紧凑 F9 的全部样板)。 + * + *

占位规则与 {@link #bindAt} 完全一致:目标坐标已被别的方块占着就分叉出一份新 UUID, + * 把老数据整份复制过去,避免复制出来的两台机器互相踩。

+ */ + public static ExternalInventoryReference bindRawAt( + Level level, + ExternalInventoryKind kind, + @Nullable ExternalInventoryReference requested, + BlockPos pos) { + ExternalInventoryReference reference = requested != null && requested.kind() == kind + ? requested + : new ExternalInventoryReference(kind, UUID.randomUUID()); + if (level.isClientSide() || level.getServer() == null) return reference; + + ExternalInventorySavedData data = ExternalInventorySavedData.get(level.getServer(), reference); + if (!data.claimAt(level, pos)) { + ExternalInventoryReference fork = new ExternalInventoryReference(kind, UUID.randomUUID()); + ExternalInventorySavedData forked = ExternalInventorySavedData.get(level.getServer(), fork); + data.copyInventoryTo(forked); + forked.claimAt(level, pos); + return fork; + } + return reference; + } + + /** 读出外置存储里的通用载荷;从没写过时返回 {@code null}。 */ + public static @Nullable CompoundTag loadRawPayload(Level level, @Nullable ExternalInventoryReference reference) { + if (reference == null || level.isClientSide() || level.getServer() == null) return null; + return ExternalInventorySavedData.get(level.getServer(), reference).rawPayload(); + } + + /** 把通用载荷写进外置存储。调用方负责控制写入频率——这份载荷可能有 MB 级。 */ + public static void saveRawPayload(Level level, @Nullable ExternalInventoryReference reference, + CompoundTag payload) { + if (reference == null || level.isClientSide() || level.getServer() == null) return; + ExternalInventorySavedData.get(level.getServer(), reference).saveRawPayload(payload); + } + + public static @Nullable ExternalInventoryReference getReference(BlockEntity entity) { + return com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.readTag( + entity.getPersistentData(), UComponents.EXTERNAL_INVENTORY_REFERENCE.get()); + } + + public static void setReference(BlockEntity entity, ExternalInventoryReference reference) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.writeTag( + entity.getPersistentData(), UComponents.EXTERNAL_INVENTORY_REFERENCE.get(), reference); + entity.setChanged(); + } + + public static void clearLegacyComponent(BlockEntity entity, ExternalInventoryKind kind) { + if (kind == ExternalInventoryKind.FURNACE) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.writeTag( + entity.getPersistentData(), UComponents.FURNACE_DATA.get(), null); + } else { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.writeTag( + entity.getPersistentData(), UComponents.MULTIBLOCK_PART_DATA.get(), null); + } + entity.setChanged(); + } + + public static void save(Level level, @Nullable ExternalInventoryReference reference, + ItemStackHandler inventory, HolderLookup.Provider registries) { + if (reference == null || level.isClientSide() || level.getServer() == null) return; + ExternalInventorySavedData.get(level.getServer(), reference).saveFrom(inventory, registries); + } + + public static void release(Level level, BlockPos pos, + @Nullable ExternalInventoryReference reference) { + if (reference == null || level.isClientSide() || level.getServer() == null) return; + ExternalInventorySavedData.get(level.getServer(), reference).releaseAt(level, pos); + } +} diff --git a/src/main/java/com/sorrowmist/useless/world/stafflink/StaffLinkManager.java b/src/main/java/com/sorrowmist/useless/world/stafflink/StaffLinkManager.java new file mode 100644 index 00000000..86819bbd --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/world/stafflink/StaffLinkManager.java @@ -0,0 +1,383 @@ +package com.sorrowmist.useless.world.stafflink; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.compat.teams.TeamOwners; +import com.sorrowmist.useless.core.component.UComponents; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 无线物流网络的门面。 + * + *

归属跟着玩家/队伍,不跟着杖

+ * + *

「谁拥有哪些网络、当前用的是哪一张」存在 {@link StaffLinkSavedData} 的归属登记里, + * 键是归属者 ID:玩家组了队就是队伍 UUID,没组队就是他自己的 UUID + * (见 {@link TeamOwners})。

+ * + *

早先这些信息挂在杖的物品组件上({@code STAFF_LINK_NETWORKS} / {@code STAFF_LINK_ACTIVE}), + * 于是换一把杖、把杖放进箱子,网络就跟着丢了或停摆。现在网络跟着归属者走:

+ * + *
    + *
  • 队友之间天然共享同一批网络(同一个队伍 ID);
  • + *
  • 归属者只要有成员在线,网络就照常被引擎遍历,与「杖在哪」无关。
  • + *
+ * + *

组队时的并入

+ * + *

组队之前各人已经建好的网络,会在解析出队伍之后并入队伍 + * (见 {@link #mergePersonalIntoTeam})——你把人拉进队伍,第一眼就该看见彼此的机器。 + * 这是不可逆的:并进去之后网络属于队伍,退队不会带走。

+ * + *

杖仍然是操作工具:绑定容器、切网络、看高亮、开配置界面都要手持它。 + * 杖上残留的旧登记只用于一次性迁移,见 {@link #migrateLegacyRegistrations}。

+ * + *

权限

+ * + *

判据只有一条:{@link #canAccess} —— 玩家当前解析出的归属 ID 必须与该网络的归属 ID 相同。 + * 退队自然失去访问权,入队自然获得,不需要额外的成员表。

+ */ +public final class StaffLinkManager { + /** + * 本局「有归属者在线」的网络。 + * + *

这是防孤儿网络的关键。存档里的网络不会因为没人引用就消失——比如早期版本用单 + * UUID 组件,改成列表组件之后,旧网络就成了没人引用的孤儿,可它仍然躺在 + * {@link StaffLinkSavedData} 里、仍然会被引擎遍历到。孤儿的配置可能停在 + * 「数量 65536 / 周期 1」,于是它会把箱子瞬间抽干,而玩家正在编辑的那张网络只看到 + * 「源已空」,完全无从判断。

+ * + *

只让「归属者本局有在线成员」的网络参与搬运,孤儿自然就停摆了。

+ */ + private static final Set LIVE_NETWORKS = ConcurrentHashMap.newKeySet(); + + private StaffLinkManager() { + } + + // ------------------------------------------------------------------ 归属 + + /** 该玩家当前的归属 ID:有队伍就是队伍,否则是他自己。 */ + public static UUID ownerIdOf(ServerPlayer player) { + return TeamOwners.ownerIdOf(player); + } + + /** 一张网络的归属者;没有归属登记时返回 {@code null}。 */ + @Nullable + public static UUID ownerOf(MinecraftServer server, UUID networkId) { + return server == null || networkId == null + ? null : StaffLinkSavedData.get(server).ownerOfNetwork(networkId); + } + + /** 该玩家有没有权限操作这张网络。 */ + public static boolean canAccess(MinecraftServer server, ServerPlayer player, UUID networkId) { + UUID owner = ownerOf(server, networkId); + return owner != null && owner.equals(ownerIdOf(player)); + } + + // ------------------------------------------------------------------ 活跃闸门 + + /** 标记某张网络本局活跃。 */ + public static void markLive(UUID networkId) { + if (networkId != null) { + LIVE_NETWORKS.add(networkId); + } + } + + public static boolean isLive(UUID networkId) { + return networkId != null && LIVE_NETWORKS.contains(networkId); + } + + public static void clearLive() { + LIVE_NETWORKS.clear(); + } + + /** + * 把「在线玩家所属归属者名下的网络」标记为活跃。 + * + *

不再靠扫背包找杖:网络跟着归属者走,只要归属者有成员在线就该跑。每隔一会儿跑一次即可。

+ * + *

顺带做两件收尾的事:

+ *
    + *
  • 并入队伍({@link #mergePersonalIntoTeam}):玩家刚组队时把他个人名下的网络 + * 交给队伍。放在这里而不是只挂在组队事件上,是因为「组队」可能发生在他离线的时候 + * (被管理员拉进队伍),下次登录才解析得出队伍,没有事件可听。
  • + *
  • 迁移旧登记:玩家可能在本局中途才从箱子里翻出一把老存档留下的杖, + * 只在登录时迁一次会漏掉这种。没有残留时就是每格一次组件查询,代价可以忽略。
  • + *
+ */ + public static void refreshLiveNetworks(MinecraftServer server) { + if (server == null) { + return; + } + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + migrateLegacyRegistrations(server, player); + mergePersonalIntoTeam(server, player); + LIVE_NETWORKS.addAll(networkIds(server, ownerIdOf(player))); + } + } + + /** + * 玩家在队伍里、而个人名下还留着网络时,把这些网络并入队伍。 + * + *

这是不可逆的:并进去之后网络就属于队伍了,退队不会带走。设计取舍是 + * 「组队共享」优先——你把人拉进队伍,第一眼就该看见彼此的机器,而不是对着空列表发呆。

+ * + *

判据只有一个:个人登记非空,且当前归属不是他自己。它同时覆盖「在线组队」与 + * 「离线期间被拉进队伍、下次登录才解析出队伍」;而队→队、队→个人这两种切换下个人登记 + * 本来就是空的,自然不会误触发。

+ * + *

并入时把个人登记清空:不清的话下次刷新会再并一遍,而且他退队后又会「复活」一批 + * 早就交给队伍的网络。

+ */ + public static void mergePersonalIntoTeam(MinecraftServer server, ServerPlayer player) { + if (server == null || player == null) { + return; + } + UUID personalId = player.getUUID(); + UUID ownerId = ownerIdOf(player); + if (personalId.equals(ownerId)) { + // 没队伍:个人登记就是他的当前登记,不用动。 + return; + } + StaffLinkSavedData data = StaffLinkSavedData.get(server); + StaffLinkSavedData.OwnerRegistration personal = data.ownerOrNull(personalId); + if (personal == null || personal.isEmpty()) { + return; + } + + List merged = new ArrayList<>(networkIds(server, ownerId)); + boolean teamAlreadyHad = !merged.isEmpty(); + int moved = 0; + for (UUID id : personal.networks()) { + if (data.get(id) != null && !merged.contains(id)) { + merged.add(id); + moved++; + } + } + if (!merged.isEmpty()) { + // 队伍本来就有网络时保留队伍的当前项;第一次并进来才沿用个人那边的下标。 + int active = teamAlreadyHad + ? Math.min(activeIndex(server, ownerId), merged.size() - 1) + : Math.min(personal.activeIndex(), merged.size() - 1); + data.setOwnerNetworks(ownerId, merged, Math.max(0, active)); + } + data.setOwnerNetworks(personalId, List.of(), 0); + data.dropOwnerIfEmpty(personalId); + + if (moved > 0) { + UselessMod.LOGGER.info("无线物流:把 {} 名下的 {} 张网络并入队伍 {}", + player.getGameProfile().getName(), moved, ownerId); + } + } + + // ------------------------------------------------------------------ 归属登记读写 + + /** + * 该归属者名下的网络,按登记顺序。 + * + *

顺手剔掉「已经被解散」的 id,避免登记里攒死链接;只有真的剔掉了才写回。

+ */ + public static List networkIds(MinecraftServer server, UUID ownerId) { + if (server == null || ownerId == null) { + return List.of(); + } + StaffLinkSavedData data = StaffLinkSavedData.get(server); + StaffLinkSavedData.OwnerRegistration registration = data.ownerOrNull(ownerId); + if (registration == null) { + return List.of(); + } + List ids = new ArrayList<>(registration.networks()); + if (ids.removeIf(id -> data.get(id) == null)) { + data.setOwnerNetworks(ownerId, ids, registration.activeIndex()); + } + return List.copyOf(ids); + } + + public static int activeIndex(MinecraftServer server, UUID ownerId) { + if (server == null || ownerId == null) { + return 0; + } + StaffLinkSavedData.OwnerRegistration registration = + StaffLinkSavedData.get(server).ownerOrNull(ownerId); + return registration == null ? 0 : registration.activeIndex(); + } + + /** 当前生效的网络;该归属者名下一张都没有时返回 {@code null}。 */ + @Nullable + public static StaffLinkNetwork activeNetwork(MinecraftServer server, UUID ownerId) { + return activeNetwork(server, ownerId, false); + } + + /** + * 取当前生效的网络。 + * + * @param createIfMissing 该归属者一张网络都没有时是否就地新建一张并切过去 + */ + @Nullable + public static StaffLinkNetwork activeNetwork(MinecraftServer server, UUID ownerId, boolean createIfMissing) { + if (server == null || ownerId == null) { + return null; + } + StaffLinkSavedData data = StaffLinkSavedData.get(server); + List ids = new ArrayList<>(networkIds(server, ownerId)); + + if (ids.isEmpty()) { + if (!createIfMissing) { + return null; + } + StaffLinkNetwork created = data.getOrCreate(UUID.randomUUID()); + data.setOwnerNetworks(ownerId, List.of(created.id()), 0); + markLive(created.id()); + return created; + } + + int raw = activeIndex(server, ownerId); + int clamped = Math.floorMod(raw, ids.size()); + if (clamped != raw) { + // 只在真的越界时才写回,否则每 tick 都会把存档标脏。 + data.setOwnerNetworks(ownerId, ids, clamped); + } + markLive(ids.get(clamped)); + return data.get(ids.get(clamped)); + } + + /** + * 在当前网络之间循环(手持杖 Shift+滚轮,或界面上的 {@code <} / {@code >})。 + * + * @param delta +1 下一张,-1 上一张 + * @return 是否成功切换(一张都没有时会顺手建一张) + */ + public static boolean cycleActive(MinecraftServer server, UUID ownerId, int delta) { + if (server == null || ownerId == null) { + return false; + } + StaffLinkSavedData data = StaffLinkSavedData.get(server); + List ids = new ArrayList<>(networkIds(server, ownerId)); + + if (ids.isEmpty()) { + StaffLinkNetwork created = data.getOrCreate(UUID.randomUUID()); + data.setOwnerNetworks(ownerId, List.of(created.id()), 0); + markLive(created.id()); + return true; + } + + int next = Math.floorMod(activeIndex(server, ownerId) + delta, ids.size()); + data.setOwnerNetworks(ownerId, ids, next); + markLive(ids.get(next)); + return true; + } + + /** 新建一张空网络并切过去。 */ + @Nullable + public static StaffLinkNetwork createNetwork(MinecraftServer server, UUID ownerId) { + if (server == null || ownerId == null) { + return null; + } + StaffLinkSavedData data = StaffLinkSavedData.get(server); + StaffLinkNetwork created = data.getOrCreate(UUID.randomUUID()); + List ids = new ArrayList<>(networkIds(server, ownerId)); + ids.add(created.id()); + data.setOwnerNetworks(ownerId, ids, ids.size() - 1); + markLive(created.id()); + return created; + } + + /** 解散当前网络:从存档删掉,并从归属登记里摘掉、切到相邻的一张。 */ + public static void dissolveActive(MinecraftServer server, UUID ownerId) { + if (server == null || ownerId == null) { + return; + } + StaffLinkSavedData data = StaffLinkSavedData.get(server); + List ids = new ArrayList<>(networkIds(server, ownerId)); + if (ids.isEmpty()) { + return; + } + int index = Math.floorMod(activeIndex(server, ownerId), ids.size()); + data.remove(ids.remove(index)); + + if (ids.isEmpty()) { + data.setOwnerNetworks(ownerId, List.of(), 0); + data.dropOwnerIfEmpty(ownerId); + return; + } + data.setOwnerNetworks(ownerId, ids, Math.floorMod(index, ids.size())); + } + + @Nullable + public static StaffLinkNetwork networkById(MinecraftServer server, UUID id) { + return server == null || id == null ? null : StaffLinkSavedData.get(server).get(id); + } + + // ------------------------------------------------------------------ 旧存档迁移 + + /** + * 把该玩家身上所有造化杖登记的旧网络迁到他的归属名下,并清空杖上的组件。 + * + *

登录时跑一次即可;迁移后组件就清掉了,所以重复调用是幂等的。

+ */ + public static void migrateLegacyRegistrations(MinecraftServer server, ServerPlayer player) { + if (server == null || player == null) { + return; + } + UUID ownerId = ownerIdOf(player); + Inventory inventory = player.getInventory(); + // 背包 41 格包含主手与副手,所以不用再单独看双手。 + for (int slot = 0; slot < inventory.getContainerSize(); slot++) { + migrateStaff(server, ownerId, inventory.getItem(slot)); + } + } + + private static void migrateStaff(MinecraftServer server, UUID ownerId, ItemStack staff) { + List legacy = legacyNetworkIds(staff); + if (legacy.isEmpty()) { + return; + } + StaffLinkSavedData data = StaffLinkSavedData.get(server); + List ids = new ArrayList<>(networkIds(server, ownerId)); + boolean alreadyHad = !ids.isEmpty(); + for (UUID id : legacy) { + if (data.get(id) != null && !ids.contains(id)) { + ids.add(id); + } + } + if (!ids.isEmpty()) { + // 本来就有网络时保留原来的当前项;第一次迁移才沿用杖上的下标。 + int active = alreadyHad + ? Math.min(activeIndex(server, ownerId), ids.size() - 1) + : Math.min(legacyActiveIndex(staff), ids.size() - 1); + data.setOwnerNetworks(ownerId, ids, Math.max(0, active)); + } + clearLegacyRegistration(staff); + } + + /** 旧存档:杖上登记的网络 ID(只用于迁移)。 */ + public static List legacyNetworkIds(ItemStack staff) { + if (staff == null || staff.isEmpty()) { + return List.of(); + } + List ids = com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(staff, UComponents.STAFF_LINK_NETWORKS.get()); + return ids == null ? List.of() : ids; + } + + /** 旧存档:杖上的当前下标(只用于迁移)。 */ + public static int legacyActiveIndex(ItemStack staff) { + Integer index = staff == null || staff.isEmpty() + ? null : com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.get(staff, UComponents.STAFF_LINK_ACTIVE.get()); + return index == null ? 0 : index; + } + + /** 迁移完成后把杖上的登记清掉,免得下次登录又迁一遍。 */ + private static void clearLegacyRegistration(ItemStack staff) { + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.remove(staff, UComponents.STAFF_LINK_NETWORKS.get()); + com.sorrowmist.useless.compat.forge1201.component.ComponentAccess.remove(staff, UComponents.STAFF_LINK_ACTIVE.get()); + } +} diff --git a/src/main/java/com/sorrowmist/useless/world/stafflink/StaffLinkNetwork.java b/src/main/java/com/sorrowmist/useless/world/stafflink/StaffLinkNetwork.java new file mode 100644 index 00000000..6b3ec5cb --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/world/stafflink/StaffLinkNetwork.java @@ -0,0 +1,342 @@ +package com.sorrowmist.useless.world.stafflink; + +import com.sorrowmist.useless.content.stafflink.LinkFlow; +import com.sorrowmist.useless.content.stafflink.ResourceFamily; +import com.sorrowmist.useless.content.stafflink.StaffLinkRoute; +import net.minecraft.core.BlockPos; +import net.minecraft.core.GlobalPos; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.FriendlyByteBuf; +import com.sorrowmist.useless.compat.forge1201.network.ByteBufCodecs; +import com.sorrowmist.useless.compat.forge1201.network.StreamCodec; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +/** + * 造化杖的一张无线物流网络。 + * + *

网络由若干条 {@link StaffLinkRoute} 组成,每条记录「某个锚点的某条线路怎么搬运」。 + * 同一个锚点可以在多条线路上各有一条配置,所以这里是扁平存储而不是「锚点 → 配置」的嵌套。

+ * + *

物品上只保存 {@link #id()};网络本体存在 {@link StaffLinkSavedData} 里。

+ */ +public final class StaffLinkNetwork { + public static final int ROUTE_COUNT = StaffLinkRoute.ROUTE_COUNT; + /** 锚点自定义名的长度上限。 */ + public static final int MAX_ANCHOR_NAME = 32; + /** 网络名的长度上限。 */ + public static final int MAX_NAME = 32; + + private static final String TAG_ID = "Id"; + private static final String TAG_NAME = "Name"; + private static final String TAG_ROUTES = "Routes"; + private static final String TAG_ANCHOR_NAMES = "AnchorNames"; + private static final String TAG_DIMENSION = "Dimension"; + private static final String TAG_POS = "Pos"; + private static final String TAG_LABEL = "Label"; + + private static final StreamCodec> ROUTES_CODEC = + StaffLinkRoute.STREAM_CODEC.apply(ByteBufCodecs.list()); + + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, network) -> { + buf.writeUUID(network.id); + buf.writeUtf(network.name); + ROUTES_CODEC.encode(buf, network.routes); + buf.writeVarInt(network.anchorNames.size()); + network.anchorNames.forEach((anchor, anchorName) -> { + StaffLinkRoute.writeAnchor(buf, anchor); + buf.writeUtf(anchorName); + }); + }, + buf -> { + UUID id = buf.readUUID(); + String name = buf.readUtf(); + List routes = ROUTES_CODEC.decode(buf); + StaffLinkNetwork network = new StaffLinkNetwork(id, name); + network.routes.addAll(routes); + int nameCount = buf.readVarInt(); + for (int i = 0; i < nameCount; i++) { + GlobalPos anchor = StaffLinkRoute.readAnchor(buf); + network.anchorNames.put(anchor, buf.readUtf()); + } + return network; + }); + + private final UUID id; + private String name; + private final List routes = new ArrayList<>(); + /** 玩家给锚点起的名字;没起名时不出现在表里,界面回落到方块名。 */ + private final Map anchorNames = new LinkedHashMap<>(); + /** 「这张网络里有没有应力线路」的缓存;{@code null} 表示还没算过。 */ + @Nullable + private Boolean hasStressRoute; + + public StaffLinkNetwork(UUID id) { + this(id, ""); + } + + private StaffLinkNetwork(UUID id, String name) { + this.id = Objects.requireNonNull(id, "id"); + this.name = name == null ? "" : name; + } + + public UUID id() { + return id; + } + + public String name() { + return name; + } + + public void setName(String name) { + if (name == null || name.isBlank()) { + this.name = ""; + return; + } + String trimmed = name.trim(); + this.name = trimmed.length() > MAX_NAME ? trimmed.substring(0, MAX_NAME) : trimmed; + } + + /** 锚点的自定义名;没起过名时返回 {@code null}。 */ + @Nullable + public String anchorName(GlobalPos anchor) { + String value = anchorNames.get(anchor); + return value == null || value.isEmpty() ? null : value; + } + + public void setAnchorName(GlobalPos anchor, @Nullable String anchorName) { + if (anchorName == null || anchorName.isBlank()) { + anchorNames.remove(anchor); + return; + } + String trimmed = anchorName.trim(); + anchorNames.put(anchor, trimmed.length() > MAX_ANCHOR_NAME + ? trimmed.substring(0, MAX_ANCHOR_NAME) : trimmed); + } + + public List routes() { + return List.copyOf(routes); + } + + /** + * 这张网络里有没有应力线路。 + * + *

应力线路不受周期与退避调度(它承载的是网络状态而不是一轮搬运量), + * 因此引擎在「这张网络这一 tick 要不要跑」的判断里要额外看它一眼。这个询问每 tick 都会发生, + * 而答案只在网络被改动时才变,所以缓存起来。

+ */ + public boolean hasStressRoute() { + if (hasStressRoute == null) { + boolean found = false; + for (StaffLinkRoute route : routes) { + if (route.medium().family() == ResourceFamily.STRESS) { + found = true; + break; + } + } + hasStressRoute = found; + } + return hasStressRoute; + } + + /** 同一线路号下的全部配置。 */ + public List routesOn(int route) { + List result = new ArrayList<>(); + for (StaffLinkRoute candidate : routes) { + if (candidate.route() == route) { + result.add(candidate); + } + } + return result; + } + + /** 去重后的已绑定锚点,按绑定先后排列。 */ + public List anchors() { + Map seen = new LinkedHashMap<>(); + for (StaffLinkRoute route : routes) { + seen.putIfAbsent(route.anchor(), Boolean.TRUE); + } + return List.copyOf(seen.keySet()); + } + + /** + * 把某个锚点挪到列表里的指定位置。 + * + *

次序不是纯装饰:引擎在同一条线路上先按权重降序排,权重相同时保持这里的次序, + * 而 {@code distribute} 会把除不尽的余数留给靠前的那个 —— 所以同权重下「谁在前谁先拿」。

+ * + *

参数是目标下标而不是「上移/下移一格」:界面上可能开着搜索过滤,可见邻居未必是 + * 列表里的邻居。由客户端把「越过上一个可见行」翻译成目标下标,服务端只管照做, + * 这样过滤状态下也是「眼睛看到的那样动」。

+ * + *

实现上是重排 {@code routes} 本身(锚点列表就是从它去重出来的):这样不必再维护 + * 一份独立的「顺序」数据,也就不会和增删锚点走岔;存档与网络同步本来就按列表顺序走, + * 于是自动一起生效。

+ * + * @param targetIndex 目标位置,会被夹到合法范围内 + * @return 是否真的移动了 + */ + public boolean moveAnchorTo(GlobalPos anchor, int targetIndex) { + List order = new ArrayList<>(anchors()); + int index = order.indexOf(anchor); + if (index < 0 || order.isEmpty()) { + return false; + } + int target = Math.max(0, Math.min(targetIndex, order.size() - 1)); + if (target == index) { + return false; + } + order.remove(index); + order.add(target, anchor); + + // 按新的锚点顺序把各锚点的线路重新拼起来;锚点内部各线路的相对次序保持不变。 + Map> grouped = new LinkedHashMap<>(); + for (GlobalPos position : order) { + grouped.put(position, new ArrayList<>()); + } + List leftovers = new ArrayList<>(); + for (StaffLinkRoute route : routes) { + List bucket = grouped.get(route.anchor()); + if (bucket == null) { + // 理论上不会发生(anchors() 就是从 routes 去重出来的);真发生了也绝不能丢。 + leftovers.add(route); + } else { + bucket.add(route); + } + } + List reordered = new ArrayList<>(routes.size()); + for (List bucket : grouped.values()) { + reordered.addAll(bucket); + } + reordered.addAll(leftovers); + + routes.clear(); + routes.addAll(reordered); + return true; + } + + public boolean isBound(GlobalPos anchor) { + for (StaffLinkRoute route : routes) { + if (route.anchor().equals(anchor)) { + return true; + } + } + return false; + } + + @Nullable + public StaffLinkRoute routeAt(GlobalPos anchor, int route) { + for (StaffLinkRoute candidate : routes) { + if (candidate.route() == route && candidate.anchor().equals(anchor)) { + return candidate; + } + } + return null; + } + + /** + * 写入/覆盖「该锚点的该线路」配置。 + * + *

原地替换而不是先删后加:锚点列表是按线路出现顺序去重出来的,若把编辑过的线路挪到 + * 末尾,界面上这个容器就会「跳」到列表最下面。

+ */ + public void putRoute(StaffLinkRoute route) { + hasStressRoute = null; + for (int index = 0; index < routes.size(); index++) { + StaffLinkRoute existing = routes.get(index); + if (existing.route() == route.route() && existing.anchor().equals(route.anchor())) { + routes.set(index, route); + return; + } + } + routes.add(route); + } + + /** 解绑:移除该锚点的全部线路配置与自定义名。 */ + public void detach(GlobalPos anchor) { + hasStressRoute = null; + routes.removeIf(candidate -> candidate.anchor().equals(anchor)); + anchorNames.remove(anchor); + } + + public boolean isEmpty() { + return routes.isEmpty(); + } + + /** 该网络里是否已经有释放端——绑定新锚点时用它决定默认流向。 */ + public boolean hasReleaseRoute() { + for (StaffLinkRoute route : routes) { + if (route.flow() == LinkFlow.RELEASE) { + return true; + } + } + return false; + } + + // ------------------------------------------------------------------ 持久化 + + public CompoundTag save(HolderLookup.Provider registries) { + CompoundTag tag = new CompoundTag(); + tag.putUUID(TAG_ID, id); + tag.putString(TAG_NAME, name); + + ListTag routeList = new ListTag(); + for (StaffLinkRoute route : routes) { + routeList.add(route.save(registries)); + } + tag.put(TAG_ROUTES, routeList); + + ListTag nameList = new ListTag(); + anchorNames.forEach((anchor, anchorName) -> { + CompoundTag entry = new CompoundTag(); + entry.putString(TAG_DIMENSION, anchor.dimension().location().toString()); + entry.putLong(TAG_POS, anchor.pos().asLong()); + entry.putString(TAG_LABEL, anchorName); + nameList.add(entry); + }); + tag.put(TAG_ANCHOR_NAMES, nameList); + return tag; + } + + public static StaffLinkNetwork load(CompoundTag tag, HolderLookup.Provider registries) { + StaffLinkNetwork network = new StaffLinkNetwork( + tag.hasUUID(TAG_ID) ? tag.getUUID(TAG_ID) : UUID.randomUUID(), + tag.getString(TAG_NAME)); + + ListTag routeList = tag.getList(TAG_ROUTES, Tag.TAG_COMPOUND); + for (int i = 0; i < routeList.size(); i++) { + StaffLinkRoute route = StaffLinkRoute.load(routeList.getCompound(i), registries); + if (route != null) { + network.routes.add(route); + } + } + + ListTag nameList = tag.getList(TAG_ANCHOR_NAMES, Tag.TAG_COMPOUND); + for (int i = 0; i < nameList.size(); i++) { + CompoundTag entry = nameList.getCompound(i); + ResourceLocation dimensionId = ResourceLocation.tryParse(entry.getString(TAG_DIMENSION)); + String anchorName = entry.getString(TAG_LABEL); + if (dimensionId == null || anchorName.isEmpty()) { + continue; + } + network.anchorNames.put( + GlobalPos.of(ResourceKey.create(Registries.DIMENSION, dimensionId), + BlockPos.of(entry.getLong(TAG_POS))), + anchorName); + } + return network; + } +} diff --git a/src/main/java/com/sorrowmist/useless/world/stafflink/StaffLinkSavedData.java b/src/main/java/com/sorrowmist/useless/world/stafflink/StaffLinkSavedData.java new file mode 100644 index 00000000..b9a3c097 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/world/stafflink/StaffLinkSavedData.java @@ -0,0 +1,267 @@ +package com.sorrowmist.useless.world.stafflink; + +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.saveddata.SavedData; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * 无线物流的全局存档:网络本体 + 归属登记。 + * + *

与 {@code AeConnectLinkSavedData} 同一套路:存在主世界的 {@code DimensionDataStorage} 上, + * 与「谁此刻拿着哪把杖」无关。

+ * + *

归属登记({@link #owners})

+ * + *

「这些网络是谁的」记在这里,而不是记在杖的物品组件上。早先网络列表挂在杖上, + * 于是换一把杖、把杖放进箱子,网络就跟着丢了或停摆。现在网络跟着归属者走: + * 归属者通常是玩家本人,玩家在队伍里时则是队伍 —— 队友之间因此天然共享同一批网络。

+ * + *

网络本体与归属分开存,所以删网络时两边都要清:{@link #remove} 会顺手把 id 从所有 + * 归属登记里摘掉,免得留下「登记了但不存在」的死链接。

+ */ +public final class StaffLinkSavedData extends SavedData { + private static final String DATA_NAME = "useless_mod_staff_link"; + private static final int CURRENT_VERSION = 2; + private static final String TAG_VERSION = "Version"; + private static final String TAG_NETWORKS = "Networks"; + private static final String TAG_OWNERS = "Owners"; + private static final String TAG_OWNER_ID = "Owner"; + private static final String TAG_OWNER_ACTIVE = "Active"; + private static final String TAG_OWNER_REGISTERED = "Registered"; + private static final String TAG_OWNER_ENTRY_ID = "Id"; + + private final Map networks = new LinkedHashMap<>(); + /** 归属者 UUID → 它登记的网络。 */ + private final Map owners = new LinkedHashMap<>(); + + /** + * 一个归属者登记的网络。 + * + *

列表顺序就是 Shift+滚轮的切换顺序;{@link #activeIndex} 是「当前用的是第几张」。

+ */ + public static final class OwnerRegistration { + private final List networks = new ArrayList<>(); + private int activeIndex; + + public List networks() { + return List.copyOf(networks); + } + + public int activeIndex() { + return activeIndex; + } + + public void setActiveIndex(int index) { + activeIndex = Math.max(0, index); + } + + public boolean isEmpty() { + return networks.isEmpty(); + } + + boolean register(UUID id) { + return !networks.contains(id) && networks.add(id); + } + + boolean unregister(UUID id) { + return networks.remove(id); + } + } + + public static StaffLinkSavedData get(MinecraftServer server) { + return server.overworld().getDataStorage().computeIfAbsent( + tag -> load(tag, server.registryAccess()), + StaffLinkSavedData::new, + DATA_NAME); + } + + private static StaffLinkSavedData load(CompoundTag tag, HolderLookup.Provider registries) { + StaffLinkSavedData data = new StaffLinkSavedData(); + ListTag list = tag.getList(TAG_NETWORKS, Tag.TAG_COMPOUND); + for (int i = 0; i < list.size(); i++) { + StaffLinkNetwork network = StaffLinkNetwork.load(list.getCompound(i), registries); + data.networks.put(network.id(), network); + } + + ListTag ownerList = tag.getList(TAG_OWNERS, Tag.TAG_COMPOUND); + for (int i = 0; i < ownerList.size(); i++) { + CompoundTag entry = ownerList.getCompound(i); + UUID ownerId = readUuid(entry.getString(TAG_OWNER_ID)); + if (ownerId == null) { + continue; + } + OwnerRegistration registration = new OwnerRegistration(); + ListTag registered = entry.getList(TAG_OWNER_REGISTERED, Tag.TAG_COMPOUND); + for (int n = 0; n < registered.size(); n++) { + UUID networkId = readUuid(registered.getCompound(n).getString(TAG_OWNER_ENTRY_ID)); + if (networkId != null && !registration.networks.contains(networkId)) { + registration.networks.add(networkId); + } + } + registration.activeIndex = Math.max(0, entry.getInt(TAG_OWNER_ACTIVE)); + data.owners.put(ownerId, registration); + } + return data; + } + + // ------------------------------------------------------------------ 网络本体 + + public StaffLinkNetwork getOrCreate(UUID id) { + StaffLinkNetwork existing = networks.get(id); + if (existing != null) { + return existing; + } + StaffLinkNetwork created = new StaffLinkNetwork(id); + networks.put(id, created); + setDirty(); + return created; + } + + @Nullable + public StaffLinkNetwork get(UUID id) { + return networks.get(id); + } + + /** + * 全部网络的一份快照。 + * + *

返回 {@link List} 而不是 {@code Collection}:调用方(引擎 tick)本来就要按顺序遍历, + * 声明成 List 就不必在那边再 {@code List.copyOf} 复制一次。

+ */ + public List all() { + return List.copyOf(networks.values()); + } + + /** 删一张网络,并从所有归属登记里摘掉它。 */ + public void remove(UUID id) { + boolean changed = networks.remove(id) != null; + for (OwnerRegistration registration : owners.values()) { + changed |= registration.unregister(id); + } + if (changed) { + setDirty(); + } + } + + /** 网络内容被改动后调用(网络对象本身是可变类,脏标记由它这里统一打)。 */ + public void markDirty() { + setDirty(); + } + + // ------------------------------------------------------------------ 归属登记 + + /** 该归属者的登记;没有则返回 {@code null}(只读,不会顺手建一条)。 */ + @Nullable + public OwnerRegistration ownerOrNull(UUID ownerId) { + return owners.get(ownerId); + } + + /** 该归属者的登记,没有就建一条空的。 */ + public OwnerRegistration owner(UUID ownerId) { + return owners.computeIfAbsent(ownerId, ignored -> new OwnerRegistration()); + } + + /** 整份覆盖某个归属者的登记(写回时用)。 */ + public void setOwnerNetworks(UUID ownerId, List networkIds, int activeIndex) { + OwnerRegistration registration = owner(ownerId); + registration.networks.clear(); + for (UUID id : networkIds) { + if (id != null && !registration.networks.contains(id)) { + registration.networks.add(id); + } + } + registration.setActiveIndex(activeIndex); + setDirty(); + } + + /** 把一张网络登记到该归属者名下(已在列表里则什么都不做)。 */ + public void registerNetwork(UUID ownerId, UUID networkId) { + if (networkId != null && owner(ownerId).register(networkId)) { + setDirty(); + } + } + + /** + * 反查一张网络的归属者。 + * + *

归属者数量很少(在线玩家/队伍级别),遍历足够;维护反向索引反而容易和 + * {@link #remove} / {@link #setOwnerNetworks} 走岔。

+ */ + @Nullable + public UUID ownerOfNetwork(UUID networkId) { + if (networkId == null) { + return null; + } + for (Map.Entry entry : owners.entrySet()) { + if (entry.getValue().networks.contains(networkId)) { + return entry.getKey(); + } + } + return null; + } + + /** 某归属者名下的一张都不剩时,把这条空登记删掉,别让存档里攒空壳。 */ + public void dropOwnerIfEmpty(UUID ownerId) { + OwnerRegistration registration = owners.get(ownerId); + if (registration != null && registration.isEmpty()) { + owners.remove(ownerId); + setDirty(); + } + } + + // ------------------------------------------------------------------ 持久化 + + @Override + public CompoundTag save(CompoundTag tag) { + return save(tag, net.minecraft.core.RegistryAccess.EMPTY); + } + + public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) { + tag.putInt(TAG_VERSION, CURRENT_VERSION); + + ListTag list = new ListTag(); + for (StaffLinkNetwork network : networks.values()) { + list.add(network.save(registries)); + } + tag.put(TAG_NETWORKS, list); + + ListTag ownerList = new ListTag(); + owners.forEach((ownerId, registration) -> { + CompoundTag entry = new CompoundTag(); + entry.putString(TAG_OWNER_ID, ownerId.toString()); + entry.putInt(TAG_OWNER_ACTIVE, registration.activeIndex); + ListTag registered = new ListTag(); + for (UUID networkId : registration.networks) { + CompoundTag item = new CompoundTag(); + item.putString(TAG_OWNER_ENTRY_ID, networkId.toString()); + registered.add(item); + } + entry.put(TAG_OWNER_REGISTERED, registered); + ownerList.add(entry); + }); + tag.put(TAG_OWNERS, ownerList); + return tag; + } + + @Nullable + private static UUID readUuid(String value) { + if (value == null || value.isEmpty()) { + return null; + } + try { + return UUID.fromString(value); + } catch (IllegalArgumentException malformed) { + return null; + } + } +} diff --git a/src/main/java/com/sorrowmist/useless/world/teleport/AbstractDimensionTeleporter.java b/src/main/java/com/sorrowmist/useless/world/teleport/AbstractDimensionTeleporter.java new file mode 100644 index 00000000..683c5a08 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/world/teleport/AbstractDimensionTeleporter.java @@ -0,0 +1,207 @@ +package com.sorrowmist.useless.world.teleport; + +import com.sorrowmist.useless.content.menus.DimensionConfigMenu; +import com.sorrowmist.useless.world.dimension.UselessDimensionConfigManager; +import com.sorrowmist.useless.world.dimension.UselessDimensions; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.Vec3i; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.TicketType; +import net.minecraft.tags.BlockTags; +import net.minecraft.world.entity.ai.village.poi.PoiManager; +import net.minecraft.world.entity.ai.village.poi.PoiRecord; +import net.minecraft.world.entity.ai.village.poi.PoiType; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.border.WorldBorder; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.material.FluidState; + +import java.util.Comparator; +import java.util.Optional; +import java.util.function.Supplier; + +/** + * Abstract Dimension Teleporter + *

+ * Code reference from AllTheModium (ATM) mod - TeleportPad class + * GitHub: https://github.com/AllTheMods/AllTheModium/blob/1.21.x/src/main/java/com/thevortex/allthemodium/blocks/TeleportPad.java + *

+ * 参考内容包括: + * - POI系统查找传送垫 + * - 螺旋搜索算法 (BlockPos.spiralAround) + * - 安全检查 (isSafeSpot) + * - 天花板维度Y轴搜索 (findSafeY) + */ +@SuppressWarnings("all") +public abstract class AbstractDimensionTeleporter { + + /* ================= 子类提供 ================= */ + + protected abstract ResourceKey getDimensionKey(); + + protected abstract Supplier getTeleportBlock(); + + protected abstract net.minecraft.core.Holder getPOI(); + + public final ResourceKey dimensionKey() { + return getDimensionKey(); + } + + public final ResourceKey targetDimensionFor(ResourceKey sourceDimension) { + return sourceDimension.equals(getDimensionKey()) ? Level.OVERWORLD : getDimensionKey(); + } + + public final Block getTeleportBlockForValidation() { + return getTeleportBlock().get(); + } + + /* ================= 入口 ================= */ + + public void handleTeleport(ServerPlayer player, BlockPos sourcePos) { + ServerLevel from = (ServerLevel) player.level(); + + ResourceKey targetKey = targetDimensionFor(from.dimension()); + + if (UselessDimensions.isUselessDimension(targetKey) + && !UselessDimensionConfigManager.isConfigured(player.server, targetKey)) { + DimensionConfigMenu.openForTeleport(player, this, sourcePos); + return; + } + + teleportAfterConfiguration(player, from.dimension(), sourcePos); + } + + public void teleportAfterConfiguration(ServerPlayer player, + ResourceKey sourceDimension, + BlockPos sourcePos) { + if (!player.level().dimension().equals(sourceDimension)) return; + if (!player.level().getBlockState(sourcePos).is(getTeleportBlock().get())) return; + ResourceKey targetKey = targetDimensionFor(sourceDimension); + + ServerLevel target = player.server.getLevel(targetKey); + if (target == null) return; + + BlockPos targetBlock = findSafeExit(target, sourcePos); + if (targetBlock == null) return; + + // 轻量级区块票证 + target.getChunkSource().addRegionTicket( + TicketType.PORTAL, + new ChunkPos(targetBlock), + 1, + targetBlock + ); + + teleport(player, target, targetBlock); + } + + /// 查找安全的出口位置,参考ATM的findSafeExit + private BlockPos findSafeExit(ServerLevel level, BlockPos entryPos) { + // 1. 使用POI系统查找现有的传送方块 + Optional existing = findClosestTeleportBlock( + level, entryPos, 32, level.getWorldBorder() + ); + if (existing.isPresent()) { + return existing.get(); + } + + // 2. 使用螺旋搜索查找安全位置 + for (BlockPos.MutableBlockPos candidate : BlockPos.spiralAround(entryPos, 32, Direction.EAST, Direction.SOUTH)) { + if (!level.getWorldBorder().isWithinBounds(candidate)) continue; + + // ServerLevel#getHeight returns the minimum build height for an + // unloaded chunk, so generate the destination chunk first. + level.getChunk(candidate.getX() >> 4, candidate.getZ() >> 4); + int y = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, candidate.getX(), candidate.getZ()); + if (level.dimensionType().hasCeiling()) { + y = findSafeY(level, candidate.getX(), y, candidate.getZ(), candidate); + } + + BlockPos spot = new BlockPos(candidate.getX(), y, candidate.getZ()); + if (isSafeSpot(level, spot)) { + // 创建新的传送方块 + BlockState teleportState = getTeleportBlock().get().defaultBlockState(); + level.setBlockAndUpdate(spot, teleportState); + return spot; + } + } + + // 3. 回退到原始位置 + // Never reuse the source dimension's Y coordinate as a destination. + return null; + } + + // 在天花板维度(如下界)中查找安全的Y坐标 + private int findSafeY(ServerLevel level, int x, int y, int z, BlockPos.MutableBlockPos pos) { + int minY = level.getMinBuildHeight(); + pos.set(x, y - 1, z); + pos.move(Direction.DOWN); + + while (pos.getY() > minY) { + if (isSafeSpot(level, pos.immutable())) { + return pos.getY(); + } + pos.move(Direction.DOWN); + } + + return level.getChunkSource().getGenerator().getSpawnHeight(level.getChunk(pos).getHeightAccessorForGeneration()); + } + + // 安全检查:确保1x2空间 + 固体地面 + private boolean isSafeSpot(ServerLevel level, BlockPos pos) { + BlockState here = level.getBlockState(pos); + FluidState hereFluid = level.getFluidState(pos); + BlockState below = level.getBlockState(pos.below()); + FluidState belowFluid = level.getFluidState(pos.below()); + BlockState above = level.getBlockState(pos.above()); + FluidState aboveFluid = level.getFluidState(pos.above()); + + return ((here.isAir() || here.is(BlockTags.REPLACEABLE)) && hereFluid.isEmpty()) && + ((above.isAir() || above.is(BlockTags.REPLACEABLE)) && aboveFluid.isEmpty()) && + (!(below.isAir() || below.is(BlockTags.REPLACEABLE) || below.is(Blocks.BEDROCK)) && belowFluid.isEmpty()); + } + + // 使用POI系统查找最近的传送方块 + public Optional findClosestTeleportBlock( + ServerLevel level, + BlockPos origin, + int radius, + WorldBorder border + ) { + PoiManager poiManager = level.getPoiManager(); + poiManager.ensureLoadedAndValid(level, origin, radius); + + return poiManager.getInSquare( + record -> record.equals(getPOI()), + origin, + radius, + PoiManager.Occupancy.ANY + ) + .map(PoiRecord::getPos) + .filter(border::isWithinBounds) + .filter(pos -> level.getBlockState(pos).is(getTeleportBlock().get())) + .min(Comparator. + comparingDouble(pos -> pos.distSqr(origin)) + .thenComparingInt(Vec3i::getY) + ); + } + + /// 执行传送 + private void teleport(ServerPlayer player, ServerLevel level, BlockPos targetPos) { + player.teleportTo( + level, + targetPos.getX() + 0.5D, + targetPos.getY() + 1.25D, + targetPos.getZ() + 0.5D, + player.getYRot(), + player.getXRot() + ); + } +} diff --git a/src/main/java/com/sorrowmist/useless/world/teleport/UselessDimTeleporter.java b/src/main/java/com/sorrowmist/useless/world/teleport/UselessDimTeleporter.java new file mode 100644 index 00000000..7e1d4717 --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/world/teleport/UselessDimTeleporter.java @@ -0,0 +1,45 @@ +package com.sorrowmist.useless.world.teleport; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.init.ModBlocks; +import com.sorrowmist.useless.init.ModPOIs; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.ai.village.poi.PoiType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; + +import java.util.function.Supplier; + +public class UselessDimTeleporter extends AbstractDimensionTeleporter { + + private static final ResourceKey DIMENSION_KEY = + ResourceKey.create(net.minecraft.core.registries.Registries.DIMENSION, + new ResourceLocation(UselessMod.MODID, "uselessdim") + ); + + public static void teleport(ServerPlayer player, BlockPos pos) { + new UselessDimTeleporter().handleTeleport(player, pos); + } + + @Override + protected ResourceKey getDimensionKey() { + return DIMENSION_KEY; + } + + @Override + protected Supplier getTeleportBlock() { + return ModBlocks.TELEPORT_BLOCK; + } + + @Override + protected Holder getPOI() { + return BuiltInRegistries.POINT_OF_INTEREST_TYPE.getHolderOrThrow( + ResourceKey.create(Registries.POINT_OF_INTEREST_TYPE, ModPOIs.TELEPORT_PAD_POI.getId())); + } +} diff --git a/src/main/java/com/sorrowmist/useless/world/teleport/UselessDimTeleporter2.java b/src/main/java/com/sorrowmist/useless/world/teleport/UselessDimTeleporter2.java new file mode 100644 index 00000000..3471a73a --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/world/teleport/UselessDimTeleporter2.java @@ -0,0 +1,45 @@ +package com.sorrowmist.useless.world.teleport; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.init.ModBlocks; +import com.sorrowmist.useless.init.ModPOIs; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.ai.village.poi.PoiType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; + +import java.util.function.Supplier; + +public class UselessDimTeleporter2 extends AbstractDimensionTeleporter { + + private static final ResourceKey DIMENSION_KEY = + ResourceKey.create(net.minecraft.core.registries.Registries.DIMENSION, + new ResourceLocation(UselessMod.MODID, "uselessdim2") + ); + + public static void teleport(ServerPlayer player, BlockPos pos) { + new UselessDimTeleporter2().handleTeleport(player, pos); + } + + @Override + protected ResourceKey getDimensionKey() { + return DIMENSION_KEY; + } + + @Override + protected Supplier getTeleportBlock() { + return ModBlocks.TELEPORT_BLOCK_2; + } + + @Override + protected Holder getPOI() { + return BuiltInRegistries.POINT_OF_INTEREST_TYPE.getHolderOrThrow( + ResourceKey.create(Registries.POINT_OF_INTEREST_TYPE, ModPOIs.TELEPORT_PAD_2_POI.getId())); + } +} diff --git a/src/main/java/com/sorrowmist/useless/world/teleport/UselessDimTeleporter3.java b/src/main/java/com/sorrowmist/useless/world/teleport/UselessDimTeleporter3.java new file mode 100644 index 00000000..6393f09f --- /dev/null +++ b/src/main/java/com/sorrowmist/useless/world/teleport/UselessDimTeleporter3.java @@ -0,0 +1,45 @@ +package com.sorrowmist.useless.world.teleport; + +import com.sorrowmist.useless.UselessMod; +import com.sorrowmist.useless.init.ModBlocks; +import com.sorrowmist.useless.init.ModPOIs; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.ai.village.poi.PoiType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; + +import java.util.function.Supplier; + +public class UselessDimTeleporter3 extends AbstractDimensionTeleporter { + + private static final ResourceKey DIMENSION_KEY = + ResourceKey.create(net.minecraft.core.registries.Registries.DIMENSION, + new ResourceLocation(UselessMod.MODID, "uselessdim3") + ); + + public static void teleport(ServerPlayer player, BlockPos pos) { + new UselessDimTeleporter3().handleTeleport(player, pos); + } + + @Override + protected ResourceKey getDimensionKey() { + return DIMENSION_KEY; + } + + @Override + protected Supplier getTeleportBlock() { + return ModBlocks.TELEPORT_BLOCK_3; + } + + @Override + protected Holder getPOI() { + return BuiltInRegistries.POINT_OF_INTEREST_TYPE.getHolderOrThrow( + ResourceKey.create(Registries.POINT_OF_INTEREST_TYPE, ModPOIs.TELEPORT_PAD_3_POI.getId())); + } +} diff --git a/src/main/java/com/sorrowmist/useless/worldgen/dimension/UselessDimGen.java b/src/main/java/com/sorrowmist/useless/worldgen/dimension/UselessDimGen.java deleted file mode 100644 index 0840dfb2..00000000 --- a/src/main/java/com/sorrowmist/useless/worldgen/dimension/UselessDimGen.java +++ /dev/null @@ -1,158 +0,0 @@ -// UselessDimGen.java -package com.sorrowmist.useless.worldgen.dimension; - -import com.mojang.serialization.Codec; -import com.mojang.serialization.codecs.RecordCodecBuilder; -import com.sorrowmist.useless.config.ConfigManager; -import net.minecraft.core.BlockPos; -import net.minecraft.server.level.WorldGenRegion; -import net.minecraft.world.level.LevelHeightAccessor; -import net.minecraft.world.level.NoiseColumn; -import net.minecraft.world.level.StructureManager; -import net.minecraft.world.level.biome.BiomeManager; -import net.minecraft.world.level.biome.BiomeSource; -import net.minecraft.world.level.block.Blocks; -import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.level.chunk.ChunkAccess; -import net.minecraft.world.level.chunk.ChunkGenerator; -import net.minecraft.world.level.levelgen.GenerationStep; -import net.minecraft.world.level.levelgen.Heightmap; -import net.minecraft.world.level.levelgen.RandomState; -import net.minecraft.world.level.levelgen.blending.Blender; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; - -public class UselessDimGen extends ChunkGenerator { - - // 注册Codec用于数据序列化 - public static final Codec CODEC = RecordCodecBuilder.create(instance -> - instance.group( - BiomeSource.CODEC.fieldOf("biome_source").forGetter(generator -> generator.biomeSource) - ).apply(instance, UselessDimGen::new) - ); - - public UselessDimGen(BiomeSource biomeSource) { - super(biomeSource); - // 不再需要加载配置,ConfigManager 会处理 - } - - @Override - public void spawnOriginalMobs(WorldGenRegion region) { - // 超平坦世界不生成原始怪物 - } - - @Override - public int getSeaLevel() { - return 0; - } - - @Override - public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor level, RandomState randomState) { - BlockState[] states = new BlockState[level.getHeight()]; - int platformLayers = ConfigManager.getPlatformLayers(); - int startY = ConfigManager.getPlatformStartY(); - int maxY = startY + platformLayers; - - for (int y = level.getMinBuildHeight(); y < level.getMaxBuildHeight(); y++) { - int index = y - level.getMinBuildHeight(); - - if (y == startY && ConfigManager.shouldGenerateBedrock()) { - states[index] = Blocks.BEDROCK.defaultBlockState(); - } else if (y > startY && y <= maxY) { - // 使用 ConfigManager 获取填充方块 - states[index] = ConfigManager.getFillBlock().defaultBlockState(); - } else { - states[index] = Blocks.AIR.defaultBlockState(); - } - } - - return new NoiseColumn(level.getMinBuildHeight(), states); - } - - @Override - protected Codec codec() { - return CODEC; - } - - @Override - public void buildSurface(WorldGenRegion region, StructureManager structureManager, RandomState randomState, ChunkAccess chunk) { - BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); - - Heightmap oceanFloorMap = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG); - Heightmap surfaceMap = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG); - int platformLayers = ConfigManager.getPlatformLayers(); - int startY = ConfigManager.getPlatformStartY(); - int maxY = startY + platformLayers; - int airStartY = maxY + 1; - - for (int x = 0; x < 16; x++) { - for (int z = 0; z < 16; z++) { - // 设置基岩底层 - if (ConfigManager.shouldGenerateBedrock()) { - chunk.setBlockState(pos.set(x, startY, z), Blocks.BEDROCK.defaultBlockState(), false); - } - - // 生成塑料平台 (Y=startY+1 到 Y=maxY) - for (int y = startY + 1; y <= maxY; y++) { - BlockState blockState; - - // 判断是否在中心2x2区域内 (坐标7-8) - if (x >= 7 && x <= 8 && z >= 7 && z <= 8) { - blockState = ConfigManager.getCenterBlock().defaultBlockState(); - } - // 判断是否在中心14x14区域内 - else if (x >= 1 && x <= 14 && z >= 1 && z <= 14) { - blockState = ConfigManager.getFillBlock().defaultBlockState(); - } else { - blockState = ConfigManager.getBorderBlock().defaultBlockState(); - } - - chunk.setBlockState(pos.set(x, y, z), blockState, false); - } - - // 空气层设置 - for (int y = airStartY; y < 320; y++) { - chunk.setBlockState(pos.set(x, y, z), Blocks.AIR.defaultBlockState(), false); - } - - - } - } - } - - @Override - public CompletableFuture fillFromNoise(Executor executor, Blender blender, RandomState randomState, StructureManager structureManager, ChunkAccess chunk) { - return CompletableFuture.completedFuture(chunk); - } - - @Override - public int getBaseHeight(int x, int z, Heightmap.Types heightmap, LevelHeightAccessor level, RandomState randomState) { - return ConfigManager.getPlatformLayers()+1; - } - - @Override - public int getMinY() { - return ConfigManager.getPlatformStartY(); - } - - @Override - public int getGenDepth() { - return 384; - } - - @Override - public void applyCarvers(WorldGenRegion chunkRegion, long seed, RandomState randomState, BiomeManager biomeAccess, StructureManager structureManager, ChunkAccess chunk, GenerationStep.Carving generationStep) { - // 不需要洞穴雕刻 - } - - @Override - public void addDebugScreenInfo(java.util.List list, RandomState randomState, BlockPos pos) { - list.add("Useless Dimension - Plastic Platform"); - list.add("边框方块: " + ConfigManager.getBorderBlock()); - list.add("填充方块: " + ConfigManager.getFillBlock()); - list.add("中心方块: " + ConfigManager.getCenterBlock()); - } - - // 移除 getConfigPath 方法,因为不再需要 -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/worldgen/dimension/UselessDimGen2.java b/src/main/java/com/sorrowmist/useless/worldgen/dimension/UselessDimGen2.java deleted file mode 100644 index 49a54761..00000000 --- a/src/main/java/com/sorrowmist/useless/worldgen/dimension/UselessDimGen2.java +++ /dev/null @@ -1,157 +0,0 @@ -package com.sorrowmist.useless.worldgen.dimension; - -import com.mojang.serialization.Codec; -import com.mojang.serialization.codecs.RecordCodecBuilder; -import com.sorrowmist.useless.config.ConfigManager; -import net.minecraft.core.BlockPos; -import net.minecraft.server.level.WorldGenRegion; -import net.minecraft.world.level.LevelHeightAccessor; -import net.minecraft.world.level.NoiseColumn; -import net.minecraft.world.level.StructureManager; -import net.minecraft.world.level.biome.BiomeManager; -import net.minecraft.world.level.biome.BiomeSource; -import net.minecraft.world.level.block.Blocks; -import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.level.chunk.ChunkAccess; -import net.minecraft.world.level.chunk.ChunkGenerator; -import net.minecraft.world.level.levelgen.GenerationStep; -import net.minecraft.world.level.levelgen.Heightmap; -import net.minecraft.world.level.levelgen.RandomState; -import net.minecraft.world.level.levelgen.blending.Blender; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; - -public class UselessDimGen2 extends ChunkGenerator { - - // 注册Codec用于数据序列化 - public static final Codec CODEC = RecordCodecBuilder.create(instance -> - instance.group( - BiomeSource.CODEC.fieldOf("biome_source").forGetter(generator -> generator.biomeSource) - ).apply(instance, UselessDimGen2::new) - ); - - public UselessDimGen2(BiomeSource biomeSource) { - super(biomeSource); - // 不再需要加载配置,ConfigManager 会处理 - } - - @Override - public void spawnOriginalMobs(WorldGenRegion region) { - // 超平坦世界不生成原始怪物 - } - - @Override - public int getSeaLevel() { - return 0; - } - - @Override - public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor level, RandomState randomState) { - BlockState[] states = new BlockState[level.getHeight()]; - int platformLayers = ConfigManager.getPlatformLayers(); - int startY = ConfigManager.getPlatformStartY(); - int maxY = startY + platformLayers; - - for (int y = level.getMinBuildHeight(); y < level.getMaxBuildHeight(); y++) { - int index = y - level.getMinBuildHeight(); - - if (y == startY && ConfigManager.shouldGenerateBedrock()) { - states[index] = Blocks.BEDROCK.defaultBlockState(); - } else if (y > startY && y <= maxY) { - // 使用 ConfigManager 获取填充方块 - states[index] = ConfigManager.getFillBlock().defaultBlockState(); - } else { - states[index] = Blocks.AIR.defaultBlockState(); - } - } - - return new NoiseColumn(level.getMinBuildHeight(), states); - } - - @Override - protected Codec codec() { - return CODEC; - } - - @Override - public void buildSurface(WorldGenRegion region, StructureManager structureManager, RandomState randomState, ChunkAccess chunk) { - BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); - - Heightmap oceanFloorMap = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG); - Heightmap surfaceMap = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG); - int platformLayers = ConfigManager.getPlatformLayers(); - int startY = ConfigManager.getPlatformStartY(); - int maxY = startY + platformLayers; - int airStartY = maxY + 1; - - for (int x = 0; x < 16; x++) { - for (int z = 0; z < 16; z++) { - // 设置基岩底层 - if (ConfigManager.shouldGenerateBedrock()) { - chunk.setBlockState(pos.set(x, startY, z), Blocks.BEDROCK.defaultBlockState(), false); - } - - // 生成平台 (Y=startY+1 到 Y=maxY) - for (int y = startY + 1; y <= maxY; y++) { - BlockState blockState; - - // 判断是否在坐标(8,8)的中心方块 - if (x == 8 && z == 8) { - blockState = ConfigManager.getCenterBlock().defaultBlockState(); - } - // 判断是否在最北边一行(z=0)或最西边一列(x=0) - else if (x == 0 || z == 0) { - blockState = ConfigManager.getBorderBlock().defaultBlockState(); - } else { - blockState = ConfigManager.getFillBlock().defaultBlockState(); - } - - chunk.setBlockState(pos.set(x, y, z), blockState, false); - } - - // 空气层设置 - for (int y = airStartY; y < 320; y++) { - chunk.setBlockState(pos.set(x, y, z), Blocks.AIR.defaultBlockState(), false); - } - - - } - } - } - - @Override - public CompletableFuture fillFromNoise(Executor executor, Blender blender, RandomState randomState, StructureManager structureManager, ChunkAccess chunk) { - return CompletableFuture.completedFuture(chunk); - } - - @Override - public int getBaseHeight(int x, int z, Heightmap.Types heightmap, LevelHeightAccessor level, RandomState randomState) { - return ConfigManager.getPlatformLayers()+1; - } - - @Override - public int getMinY() { - return ConfigManager.getPlatformStartY(); - } - - @Override - public int getGenDepth() { - return 384; - } - - @Override - public void applyCarvers(WorldGenRegion chunkRegion, long seed, RandomState randomState, BiomeManager biomeAccess, StructureManager structureManager, ChunkAccess chunk, GenerationStep.Carving generationStep) { - // 不需要洞穴雕刻 - } - - @Override - public void addDebugScreenInfo(java.util.List list, RandomState randomState, BlockPos pos) { - list.add("Useless Dimension - Plastic Platform"); - list.add("边框方块: " + ConfigManager.getBorderBlock()); - list.add("填充方块: " + ConfigManager.getFillBlock()); - list.add("中心方块: " + ConfigManager.getCenterBlock()); - } - - // 移除 getConfigPath 方法,因为不再需要 -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/worldgen/dimension/UselessDimGen3.java b/src/main/java/com/sorrowmist/useless/worldgen/dimension/UselessDimGen3.java deleted file mode 100644 index 49183315..00000000 --- a/src/main/java/com/sorrowmist/useless/worldgen/dimension/UselessDimGen3.java +++ /dev/null @@ -1,157 +0,0 @@ -package com.sorrowmist.useless.worldgen.dimension; - -import com.mojang.serialization.Codec; -import com.mojang.serialization.codecs.RecordCodecBuilder; -import com.sorrowmist.useless.config.ConfigManager; -import net.minecraft.core.BlockPos; -import net.minecraft.server.level.WorldGenRegion; -import net.minecraft.world.level.LevelHeightAccessor; -import net.minecraft.world.level.NoiseColumn; -import net.minecraft.world.level.StructureManager; -import net.minecraft.world.level.biome.BiomeManager; -import net.minecraft.world.level.biome.BiomeSource; -import net.minecraft.world.level.block.Blocks; -import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.level.chunk.ChunkAccess; -import net.minecraft.world.level.chunk.ChunkGenerator; -import net.minecraft.world.level.levelgen.GenerationStep; -import net.minecraft.world.level.levelgen.Heightmap; -import net.minecraft.world.level.levelgen.RandomState; -import net.minecraft.world.level.levelgen.blending.Blender; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; - -public class UselessDimGen3 extends ChunkGenerator { - - // 注册Codec用于数据序列化 - public static final Codec CODEC = RecordCodecBuilder.create(instance -> - instance.group( - BiomeSource.CODEC.fieldOf("biome_source").forGetter(generator -> generator.biomeSource) - ).apply(instance, UselessDimGen3::new) - ); - - public UselessDimGen3(BiomeSource biomeSource) { - super(biomeSource); - // 不再需要加载配置,ConfigManager 会处理 - } - - @Override - public void spawnOriginalMobs(WorldGenRegion region) { - // 超平坦世界不生成原始怪物 - } - - @Override - public int getSeaLevel() { - return 0; - } - - @Override - public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor level, RandomState randomState) { - BlockState[] states = new BlockState[level.getHeight()]; - int platformLayers = ConfigManager.getPlatformLayers(); - int startY = ConfigManager.getPlatformStartY(); - int maxY = startY + platformLayers; - - for (int y = level.getMinBuildHeight(); y < level.getMaxBuildHeight(); y++) { - int index = y - level.getMinBuildHeight(); - - if (y == startY && ConfigManager.shouldGenerateBedrock()) { - states[index] = Blocks.BEDROCK.defaultBlockState(); - } else if (y > startY && y <= maxY) { - // 使用 ConfigManager 获取填充方块 - states[index] = ConfigManager.getFillBlock().defaultBlockState(); - } else { - states[index] = Blocks.AIR.defaultBlockState(); - } - } - - return new NoiseColumn(level.getMinBuildHeight(), states); - } - - @Override - protected Codec codec() { - return CODEC; - } - - @Override - public void buildSurface(WorldGenRegion region, StructureManager structureManager, RandomState randomState, ChunkAccess chunk) { - BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); - - Heightmap oceanFloorMap = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG); - Heightmap surfaceMap = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG); - int platformLayers = ConfigManager.getPlatformLayers(); - int startY = ConfigManager.getPlatformStartY(); - int maxY = startY + platformLayers; - int airStartY = maxY + 1; - - for (int x = 0; x < 16; x++) { - for (int z = 0; z < 16; z++) { - // 设置基岩底层 - if (ConfigManager.shouldGenerateBedrock()) { - chunk.setBlockState(pos.set(x, startY, z), Blocks.BEDROCK.defaultBlockState(), false); - } - - // 生成平台 (Y=startY+1 到 Y=maxY) - for (int y = startY + 1; y <= maxY; y++) { - BlockState blockState; - - // 判断是否在坐标(8,8)的中心方块 - if (x == 8 && z == 8) { - blockState = ConfigManager.getCenterBlock().defaultBlockState(); - } - // 判断是否在边框3格范围内 (x < 2 或 x > 14 或 z < 4 或 z > 14) - else if (x < 2 || x > 14 || z < 2 || z > 14) { - blockState = ConfigManager.getBorderBlock().defaultBlockState(); - } else { - blockState = ConfigManager.getFillBlock().defaultBlockState(); - } - - chunk.setBlockState(pos.set(x, y, z), blockState, false); - } - - // 空气层设置 - for (int y = airStartY; y < 320; y++) { - chunk.setBlockState(pos.set(x, y, z), Blocks.AIR.defaultBlockState(), false); - } - - - } - } - } - - @Override - public CompletableFuture fillFromNoise(Executor executor, Blender blender, RandomState randomState, StructureManager structureManager, ChunkAccess chunk) { - return CompletableFuture.completedFuture(chunk); - } - - @Override - public int getBaseHeight(int x, int z, Heightmap.Types heightmap, LevelHeightAccessor level, RandomState randomState) { - return ConfigManager.getPlatformLayers()+1; - } - - @Override - public int getMinY() { - return ConfigManager.getPlatformStartY(); - } - - @Override - public int getGenDepth() { - return 384; - } - - @Override - public void applyCarvers(WorldGenRegion chunkRegion, long seed, RandomState randomState, BiomeManager biomeAccess, StructureManager structureManager, ChunkAccess chunk, GenerationStep.Carving generationStep) { - // 不需要洞穴雕刻 - } - - @Override - public void addDebugScreenInfo(java.util.List list, RandomState randomState, BlockPos pos) { - list.add("Useless Dimension - Plastic Platform"); - list.add("边框方块: " + ConfigManager.getBorderBlock()); - list.add("填充方块: " + ConfigManager.getFillBlock()); - list.add("中心方块: " + ConfigManager.getCenterBlock()); - } - - // 移除 getConfigPath 方法,因为不再需要 -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/worldgen/dimension/UselessDimension.java b/src/main/java/com/sorrowmist/useless/worldgen/dimension/UselessDimension.java deleted file mode 100644 index 74983670..00000000 --- a/src/main/java/com/sorrowmist/useless/worldgen/dimension/UselessDimension.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.sorrowmist.useless.worldgen.dimension; - -import com.mojang.serialization.Codec; -import com.sorrowmist.useless.UselessMod; -import net.minecraft.world.level.chunk.ChunkGenerator; -import net.minecraftforge.eventbus.api.IEventBus; -import net.minecraftforge.registries.DeferredRegister; -import net.minecraftforge.registries.RegistryObject; - -public class UselessDimension { - - // 只需要注册区块生成器 - public static final DeferredRegister> CHUNK_GENERATORS = - DeferredRegister.create(net.minecraft.core.registries.Registries.CHUNK_GENERATOR, UselessMod.MOD_ID); - - public static final RegistryObject> USELESSDIM_GEN_CODEC = - CHUNK_GENERATORS.register("uselessdim_gen", () -> UselessDimGen.CODEC); - -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/worldgen/dimension/UselessDimension2.java b/src/main/java/com/sorrowmist/useless/worldgen/dimension/UselessDimension2.java deleted file mode 100644 index 060b4c1d..00000000 --- a/src/main/java/com/sorrowmist/useless/worldgen/dimension/UselessDimension2.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.sorrowmist.useless.worldgen.dimension; - -import com.mojang.serialization.Codec; -import com.sorrowmist.useless.UselessMod; -import net.minecraft.world.level.chunk.ChunkGenerator; -import net.minecraftforge.eventbus.api.IEventBus; -import net.minecraftforge.registries.DeferredRegister; -import net.minecraftforge.registries.RegistryObject; - -public class UselessDimension2 { - - // 只需要注册区块生成器 - public static final DeferredRegister> CHUNK_GENERATORS = - DeferredRegister.create(net.minecraft.core.registries.Registries.CHUNK_GENERATOR, UselessMod.MOD_ID); - - public static final RegistryObject> USELESSDIM_GEN_CODEC = - CHUNK_GENERATORS.register("uselessdim_gen_2", () -> UselessDimGen2.CODEC); - -} \ No newline at end of file diff --git a/src/main/java/com/sorrowmist/useless/worldgen/dimension/UselessDimension3.java b/src/main/java/com/sorrowmist/useless/worldgen/dimension/UselessDimension3.java deleted file mode 100644 index aed0d910..00000000 --- a/src/main/java/com/sorrowmist/useless/worldgen/dimension/UselessDimension3.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.sorrowmist.useless.worldgen.dimension; - -import com.mojang.serialization.Codec; -import com.sorrowmist.useless.UselessMod; -import net.minecraft.world.level.chunk.ChunkGenerator; -import net.minecraftforge.eventbus.api.IEventBus; -import net.minecraftforge.registries.DeferredRegister; -import net.minecraftforge.registries.RegistryObject; - -public class UselessDimension3 { - - // 只需要注册区块生成器 - public static final DeferredRegister> CHUNK_GENERATORS = - DeferredRegister.create(net.minecraft.core.registries.Registries.CHUNK_GENERATOR, UselessMod.MOD_ID); - - public static final RegistryObject> USELESSDIM_GEN_CODEC = - CHUNK_GENERATORS.register("uselessdim3_gen", () -> UselessDimGen3.CODEC); - -} \ No newline at end of file diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg new file mode 100644 index 00000000..0939b45e --- /dev/null +++ b/src/main/resources/META-INF/accesstransformer.cfg @@ -0,0 +1,15 @@ +# Native 1.20.1 SRG names. Applied by Gradle and Forge at runtime. +public net.minecraft.world.entity.LivingEntity f_20898_ # lastHurt +public net.minecraft.world.entity.LivingEntity f_20890_ # dead +public net.minecraft.world.entity.LivingEntity f_20897_ # deathScore +public net.minecraft.world.entity.LivingEntity m_6668_(Lnet/minecraft/world/damagesource/DamageSource;)V # dropAllDeathLoot +public net.minecraft.world.entity.animal.Fox m_6668_(Lnet/minecraft/world/damagesource/DamageSource;)V # dropAllDeathLoot +public net.minecraft.server.level.ServerLevel f_143243_ # entityTickList +public net.minecraft.server.level.ServerLevel f_143244_ # entityManager +public net.minecraft.world.level.entity.PersistentEntitySectionManager f_157491_ # knownUuids +public net.minecraft.world.level.entity.PersistentEntitySectionManager f_157494_ # visibleEntityStorage +public net.minecraft.world.level.entity.PersistentEntitySectionManager f_157496_ # entityGetter +public net.minecraft.world.level.entity.EntityLookup f_156807_ # byId +public net.minecraft.world.level.entity.EntityLookup f_156808_ # byUuid +public net.minecraft.world.level.entity.LevelEntityGetterAdapter f_156940_ # visibleEntities +protected net.minecraft.client.gui.screens.inventory.AbstractContainerScreen m_280092_(Lnet/minecraft/client/gui/GuiGraphics;Lnet/minecraft/world/inventory/Slot;)V # renderSlot diff --git a/src/main/resources/META-INF/mods.toml b/src/main/resources/META-INF/mods.toml index 3b4a9826..3b70b202 100644 --- a/src/main/resources/META-INF/mods.toml +++ b/src/main/resources/META-INF/mods.toml @@ -1,100 +1,26 @@ -# This is an example mods.toml file. It contains the data relating to the loading mods. -# There are several mandatory fields (#mandatory), and many more that are optional (#optional). -# The overall format is standard TOML format, v0.5.0. -# Note that there are a couple of TOML lists in this file. -# Find more information on toml format here: https://github.com/toml-lang/toml -# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml -modLoader="javafml" #mandatory -# A version range to match for said mod loader - for regular FML @Mod it will be the forge version -loaderVersion="${loader_version_range}" #mandatory This is typically bumped every Minecraft version by Forge. See our download page for lists of versions. -# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties. -# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here. -license="${mod_license}" -# A URL to refer people to when problems occur with this mod -#issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional -# If your mod is purely client-side and has no multiplayer functionality (be it dedicated servers or Open to LAN), -# set this to true, and Forge will set the correct displayTest for you and skip loading your mod on dedicated servers. -#clientSideOnly=true #optional - defaults to false if absent -# A list of mods - how many allowed here is determined by the individual mod loader -[[mods]] #mandatory -# The modid of the mod -modId="useless_mod" #mandatory -# The version number of the mod -version="${mod_version}" #mandatory -# A display name for the mod -displayName="UselessMod" #mandatory -# A URL to query for updates for this mod. See the JSON update specification https://docs.minecraftforge.net/en/latest/misc/updatechecker/ -#updateJSONURL="https://change.me.example.invalid/updates.json" #optional -# A URL for the "homepage" for this mod, displayed in the mod UI -#displayURL="https://change.me.to.your.mods.homepage.example.invalid/" #optional -# A file name (in the root of the mod JAR) containing a logo for display -#logoFile="examplemod.png" #optional -# A text field displayed in the mod UI -#credits="" #optional -# A text field displayed in the mod UI -authors="水幕忘忧" #optional -# Display Test controls the display for your mod in the server connection screen -# MATCH_VERSION means that your mod will cause a red X if the versions on client and server differ. This is the default behaviour and should be what you choose if you have server and client elements to your mod. -# IGNORE_SERVER_VERSION means that your mod will not cause a red X if it's present on the server but not on the client. This is what you should use if you're a server only mod. -# IGNORE_ALL_VERSION means that your mod will not cause a red X if it's present on the client or the server. This is a special case and should only be used if your mod has no server component. -# NONE means that no display test is set on your mod. You need to do this yourself, see IExtensionPoint.DisplayTest for more information. You can define any scheme you wish with this value. -# IMPORTANT NOTE: this is NOT an instruction as to which environments (CLIENT or DEDICATED SERVER) your mod loads on. Your mod should load (and maybe do nothing!) whereever it finds itself. -#displayTest="MATCH_VERSION" # if nothing is specified, MATCH_VERSION is the default when clientSideOnly=false, otherwise IGNORE_ALL_VERSION when clientSideOnly=true (#optional) - -# The description text for the mod (multi line!) (#mandatory) -description='''这个mod可能很有用,但这个mod很有用不太可能。''' -# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional. -[[dependencies.${mod_id}]] #optional - # the modid of the dependency - modId="forge" #mandatory - # Does this dependency have to exist - if not, ordering below must be specified - mandatory=true #mandatory - # The version range of the dependency - versionRange="[47.4.0,)" #mandatory - # An ordering relationship for the dependency - BEFORE or AFTER required if the dependency is not mandatory - # BEFORE - This mod is loaded BEFORE the dependency - # AFTER - This mod is loaded AFTER the dependency - ordering="NONE" - # Side this dependency is applied on - BOTH, CLIENT, or SERVER - side="BOTH" -# Here's another dependency -[[dependencies.${mod_id}]] - modId="minecraft" - mandatory=true - # This version range declares a minimum of the current minecraft version up to but not including the next major version - versionRange="${minecraft_version_range}" - ordering="NONE" - side="BOTH" -[[mods.mixinConfigs]] -config = "mixin.useless.json" -minVersion = "0.8" - -# EMI 作为可选依赖 -[[dependencies.${mod_id}]] - modId="emi" - mandatory=false - versionRange="[1.0.0,)" - ordering="NONE" - side="BOTH" - -# AdvancedAE 作为可选依赖 -[[dependencies.${mod_id}]] - modId="advancedae" - mandatory=false - versionRange="[1.0.0,)" - ordering="NONE" - side="BOTH" - -# ExtendedAE 作为可选依赖 -[[dependencies.${mod_id}]] - modId="expatternprovider" - mandatory=true - versionRange="[1.0.0,)" - ordering="NONE" - side="BOTH" - -# Features are specific properties of the game environment, that you may want to declare you require. This example declares -# that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't -# stop your mod loading on the server for example. -#[features.${mod_id}] -#openGLVersion="[3.2,)" +modLoader="javafml" +loaderVersion="[47,)" +license="MIT" +[[mods]] +modId="useless_mod" +version="${mod_version}" +displayName="Useless Mod" +description="Utilities, tools, logistics and omniversal alloy-furnace recipe integrations for Forge 1.20.1." +[[dependencies.useless_mod]] +modId="forge" +mandatory=true +versionRange="[47.3.10,48)" +ordering="NONE" +side="BOTH" +[[dependencies.useless_mod]] +modId="minecraft" +mandatory=true +versionRange="[1.20.1,1.20.2)" +ordering="NONE" +side="BOTH" +[[dependencies.useless_mod]] +modId="ae2" +mandatory=true +versionRange="[15.4.8,16)" +ordering="AFTER" +side="BOTH" diff --git a/src/main/resources/assets/useless_mod/blockstates/aqua_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/aqua_glow_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/aqua_glow_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/aqua_plastic.json b/src/main/resources/assets/useless_mod/blockstates/aqua_plastic.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/aqua_plastic.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/aqua_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/aqua_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/aqua_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/black_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/black_glow_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/black_glow_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/black_plastic.json b/src/main/resources/assets/useless_mod/blockstates/black_plastic.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/black_plastic.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/black_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/black_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/black_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/blue_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/blue_glow_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/blue_glow_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/blue_plastic.json b/src/main/resources/assets/useless_mod/blockstates/blue_plastic.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/blue_plastic.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/blue_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/blue_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/blue_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/brown_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/brown_glow_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/brown_glow_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/brown_plastic.json b/src/main/resources/assets/useless_mod/blockstates/brown_plastic.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/brown_plastic.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/brown_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/brown_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/brown_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/cyan_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/cyan_glow_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/cyan_glow_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/cyan_plastic.json b/src/main/resources/assets/useless_mod/blockstates/cyan_plastic.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/cyan_plastic.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/cyan_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/cyan_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/cyan_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/dark_red_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/dark_red_glow_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/dark_red_glow_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/dark_red_plastic.json b/src/main/resources/assets/useless_mod/blockstates/dark_red_plastic.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/dark_red_plastic.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/dark_red_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/dark_red_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/dark_red_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/gray_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/gray_glow_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/gray_glow_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/gray_plastic.json b/src/main/resources/assets/useless_mod/blockstates/gray_plastic.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/gray_plastic.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/gray_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/gray_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/gray_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/green_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/green_glow_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/green_glow_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/green_plastic.json b/src/main/resources/assets/useless_mod/blockstates/green_plastic.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/green_plastic.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/green_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/green_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/green_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/light_blue_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/light_blue_glow_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/light_blue_glow_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/light_blue_plastic.json b/src/main/resources/assets/useless_mod/blockstates/light_blue_plastic.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/light_blue_plastic.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/light_blue_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/light_blue_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/light_blue_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/light_gray_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/light_gray_glow_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/light_gray_glow_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/light_gray_plastic.json b/src/main/resources/assets/useless_mod/blockstates/light_gray_plastic.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/light_gray_plastic.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/light_gray_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/light_gray_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/light_gray_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/lime_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/lime_glow_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/lime_glow_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/lime_plastic.json b/src/main/resources/assets/useless_mod/blockstates/lime_plastic.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/lime_plastic.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/lime_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/lime_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/lime_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/magenta_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/magenta_glow_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/magenta_glow_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/magenta_plastic.json b/src/main/resources/assets/useless_mod/blockstates/magenta_plastic.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/magenta_plastic.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/magenta_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/magenta_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/magenta_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/me_pattern_assembly.json b/src/main/resources/assets/useless_mod/blockstates/me_pattern_assembly.json new file mode 100644 index 00000000..5a5d59a7 --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/me_pattern_assembly.json @@ -0,0 +1,10 @@ +{ + "variants": { + "facing=down": { "model": "useless_mod:block/me_pattern_assembly", "x": 90, "uvlock": true }, + "facing=up": { "model": "useless_mod:block/me_pattern_assembly", "x": 270, "uvlock": true }, + "facing=north": { "model": "useless_mod:block/me_pattern_assembly", "uvlock": true }, + "facing=east": { "model": "useless_mod:block/me_pattern_assembly", "y": 90, "uvlock": true }, + "facing=south": { "model": "useless_mod:block/me_pattern_assembly", "y": 180, "uvlock": true }, + "facing=west": { "model": "useless_mod:block/me_pattern_assembly", "y": 270, "uvlock": true } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/multiblock_alloy_furnace_core.json b/src/main/resources/assets/useless_mod/blockstates/multiblock_alloy_furnace_core.json new file mode 100644 index 00000000..488a2b16 --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/multiblock_alloy_furnace_core.json @@ -0,0 +1,20 @@ +{ + "variants": { + "facing=north,formed=false": { "model": "useless_mod:block/multiblock_alloy_furnace_core_base", "uvlock": true }, + "facing=east,formed=false": { "model": "useless_mod:block/multiblock_alloy_furnace_core_base", "y": 90, "uvlock": true }, + "facing=south,formed=false": { "model": "useless_mod:block/multiblock_alloy_furnace_core_base", "y": 180, "uvlock": true }, + "facing=west,formed=false": { "model": "useless_mod:block/multiblock_alloy_furnace_core_base", "y": 270, "uvlock": true }, + "activity=idle,facing=north,formed=true": { "model": "useless_mod:block/multiblock_alloy_furnace_core_idle", "uvlock": true }, + "activity=idle,facing=east,formed=true": { "model": "useless_mod:block/multiblock_alloy_furnace_core_idle", "y": 90, "uvlock": true }, + "activity=idle,facing=south,formed=true": { "model": "useless_mod:block/multiblock_alloy_furnace_core_idle", "y": 180, "uvlock": true }, + "activity=idle,facing=west,formed=true": { "model": "useless_mod:block/multiblock_alloy_furnace_core_idle", "y": 270, "uvlock": true }, + "activity=run,facing=north,formed=true": { "model": "useless_mod:block/multiblock_alloy_furnace_core_run", "uvlock": true }, + "activity=run,facing=east,formed=true": { "model": "useless_mod:block/multiblock_alloy_furnace_core_run", "y": 90, "uvlock": true }, + "activity=run,facing=south,formed=true": { "model": "useless_mod:block/multiblock_alloy_furnace_core_run", "y": 180, "uvlock": true }, + "activity=run,facing=west,formed=true": { "model": "useless_mod:block/multiblock_alloy_furnace_core_run", "y": 270, "uvlock": true }, + "activity=wait,facing=north,formed=true": { "model": "useless_mod:block/multiblock_alloy_furnace_core_wait", "uvlock": true }, + "activity=wait,facing=east,formed=true": { "model": "useless_mod:block/multiblock_alloy_furnace_core_wait", "y": 90, "uvlock": true }, + "activity=wait,facing=south,formed=true": { "model": "useless_mod:block/multiblock_alloy_furnace_core_wait", "y": 180, "uvlock": true }, + "activity=wait,facing=west,formed=true": { "model": "useless_mod:block/multiblock_alloy_furnace_core_wait", "y": 270, "uvlock": true } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/omniversal_furnace_casing.json b/src/main/resources/assets/useless_mod/blockstates/omniversal_furnace_casing.json new file mode 100644 index 00000000..2ea41b93 --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/omniversal_furnace_casing.json @@ -0,0 +1,5 @@ +{ + "variants": { + "": { "model": "useless_mod:block/omniversal_furnace_casing" } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/omniversal_mold_hub.json b/src/main/resources/assets/useless_mod/blockstates/omniversal_mold_hub.json new file mode 100644 index 00000000..bbb9b42c --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/omniversal_mold_hub.json @@ -0,0 +1,10 @@ +{ + "variants": { + "facing=down": { "model": "useless_mod:block/omniversal_mold_hub", "x": 90, "uvlock": true }, + "facing=up": { "model": "useless_mod:block/omniversal_mold_hub", "x": 270, "uvlock": true }, + "facing=north": { "model": "useless_mod:block/omniversal_mold_hub", "uvlock": true }, + "facing=east": { "model": "useless_mod:block/omniversal_mold_hub", "y": 90, "uvlock": true }, + "facing=south": { "model": "useless_mod:block/omniversal_mold_hub", "y": 180, "uvlock": true }, + "facing=west": { "model": "useless_mod:block/omniversal_mold_hub", "y": 270, "uvlock": true } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/orange_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/orange_glow_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/orange_glow_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/orange_plastic.json b/src/main/resources/assets/useless_mod/blockstates/orange_plastic.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/orange_plastic.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/orange_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/orange_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/orange_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/passive_crafting_hatch.json b/src/main/resources/assets/useless_mod/blockstates/passive_crafting_hatch.json new file mode 100644 index 00000000..db42ca9d --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/passive_crafting_hatch.json @@ -0,0 +1,10 @@ +{ + "variants": { + "facing=down": { "model": "useless_mod:block/passive_crafting_hatch", "x": 90, "uvlock": true }, + "facing=up": { "model": "useless_mod:block/passive_crafting_hatch", "x": 270, "uvlock": true }, + "facing=north": { "model": "useless_mod:block/passive_crafting_hatch", "uvlock": true }, + "facing=east": { "model": "useless_mod:block/passive_crafting_hatch", "y": 90, "uvlock": true }, + "facing=south": { "model": "useless_mod:block/passive_crafting_hatch", "y": 180, "uvlock": true }, + "facing=west": { "model": "useless_mod:block/passive_crafting_hatch", "y": 270, "uvlock": true } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/pink_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/pink_glow_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/pink_glow_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/pink_plastic.json b/src/main/resources/assets/useless_mod/blockstates/pink_plastic.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/pink_plastic.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/pink_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/pink_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/pink_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/purple_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/purple_glow_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/purple_glow_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/purple_plastic.json b/src/main/resources/assets/useless_mod/blockstates/purple_plastic.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/purple_plastic.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/purple_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/purple_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/purple_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/red_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/red_glow_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/red_glow_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/red_plastic.json b/src/main/resources/assets/useless_mod/blockstates/red_plastic.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/red_plastic.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/red_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/red_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/red_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/supervisor.json b/src/main/resources/assets/useless_mod/blockstates/supervisor.json new file mode 100644 index 00000000..14a3b7f7 --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/supervisor.json @@ -0,0 +1,19 @@ +{ + "variants": { + "facing=north": { + "model": "useless_mod:block/supervisor" + }, + "facing=east": { + "model": "useless_mod:block/supervisor", + "y": 90 + }, + "facing=south": { + "model": "useless_mod:block/supervisor", + "y": 180 + }, + "facing=west": { + "model": "useless_mod:block/supervisor", + "y": 270 + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/temperature_regulator.json b/src/main/resources/assets/useless_mod/blockstates/temperature_regulator.json new file mode 100644 index 00000000..bf6405b0 --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/temperature_regulator.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/temperature_regulator" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/useful_coil.json b/src/main/resources/assets/useless_mod/blockstates/useful_coil.json new file mode 100644 index 00000000..2be56f83 --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/useful_coil.json @@ -0,0 +1,6 @@ +{ + "variants": { + "active=false": { "model": "useless_mod:block/useful_coil" }, + "active=true": { "model": "useless_mod:block/useful_coil_active" } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/useless_coil_tier_1.json b/src/main/resources/assets/useless_mod/blockstates/useless_coil_tier_1.json new file mode 100644 index 00000000..63683217 --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/useless_coil_tier_1.json @@ -0,0 +1,6 @@ +{ + "variants": { + "active=false": { "model": "useless_mod:block/useless_coil_tier_1" }, + "active=true": { "model": "useless_mod:block/useless_coil_tier_1_active" } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/useless_coil_tier_2.json b/src/main/resources/assets/useless_mod/blockstates/useless_coil_tier_2.json new file mode 100644 index 00000000..8429975b --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/useless_coil_tier_2.json @@ -0,0 +1,6 @@ +{ + "variants": { + "active=false": { "model": "useless_mod:block/useless_coil_tier_2" }, + "active=true": { "model": "useless_mod:block/useless_coil_tier_2_active" } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/useless_coil_tier_3.json b/src/main/resources/assets/useless_mod/blockstates/useless_coil_tier_3.json new file mode 100644 index 00000000..56fa4d59 --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/useless_coil_tier_3.json @@ -0,0 +1,6 @@ +{ + "variants": { + "active=false": { "model": "useless_mod:block/useless_coil_tier_3" }, + "active=true": { "model": "useless_mod:block/useless_coil_tier_3_active" } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/useless_coil_tier_4.json b/src/main/resources/assets/useless_mod/blockstates/useless_coil_tier_4.json new file mode 100644 index 00000000..2e7db4c8 --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/useless_coil_tier_4.json @@ -0,0 +1,6 @@ +{ + "variants": { + "active=false": { "model": "useless_mod:block/useless_coil_tier_4" }, + "active=true": { "model": "useless_mod:block/useless_coil_tier_4_active" } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/useless_coil_tier_5.json b/src/main/resources/assets/useless_mod/blockstates/useless_coil_tier_5.json new file mode 100644 index 00000000..3d47505a --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/useless_coil_tier_5.json @@ -0,0 +1,6 @@ +{ + "variants": { + "active=false": { "model": "useless_mod:block/useless_coil_tier_5" }, + "active=true": { "model": "useless_mod:block/useless_coil_tier_5_active" } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/useless_coil_tier_6.json b/src/main/resources/assets/useless_mod/blockstates/useless_coil_tier_6.json new file mode 100644 index 00000000..fd8ca841 --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/useless_coil_tier_6.json @@ -0,0 +1,6 @@ +{ + "variants": { + "active=false": { "model": "useless_mod:block/useless_coil_tier_6" }, + "active=true": { "model": "useless_mod:block/useless_coil_tier_6_active" } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/useless_coil_tier_7.json b/src/main/resources/assets/useless_mod/blockstates/useless_coil_tier_7.json new file mode 100644 index 00000000..b7d72640 --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/useless_coil_tier_7.json @@ -0,0 +1,6 @@ +{ + "variants": { + "active=false": { "model": "useless_mod:block/useless_coil_tier_7" }, + "active=true": { "model": "useless_mod:block/useless_coil_tier_7_active" } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/useless_coil_tier_8.json b/src/main/resources/assets/useless_mod/blockstates/useless_coil_tier_8.json new file mode 100644 index 00000000..b1d7387d --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/useless_coil_tier_8.json @@ -0,0 +1,6 @@ +{ + "variants": { + "active=false": { "model": "useless_mod:block/useless_coil_tier_8" }, + "active=true": { "model": "useless_mod:block/useless_coil_tier_8_active" } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/useless_coil_tier_9.json b/src/main/resources/assets/useless_mod/blockstates/useless_coil_tier_9.json new file mode 100644 index 00000000..da380f0d --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/useless_coil_tier_9.json @@ -0,0 +1,6 @@ +{ + "variants": { + "active=false": { "model": "useless_mod:block/useless_coil_tier_9" }, + "active=true": { "model": "useless_mod:block/useless_coil_tier_9_active" } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/useless_compact_c9.json b/src/main/resources/assets/useless_mod/blockstates/useless_compact_c9.json new file mode 100644 index 00000000..7bd745f8 --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/useless_compact_c9.json @@ -0,0 +1,39 @@ +{ + "multipart": [ + { + "when": { + "facing": "north" + }, + "apply": { + "model": "useless_mod:block/useless_compact_c9" + } + }, + { + "when": { + "facing": "east" + }, + "apply": { + "model": "useless_mod:block/useless_compact_c9", + "y": 90 + } + }, + { + "when": { + "facing": "south" + }, + "apply": { + "model": "useless_mod:block/useless_compact_c9", + "y": 180 + } + }, + { + "when": { + "facing": "west" + }, + "apply": { + "model": "useless_mod:block/useless_compact_c9", + "y": 270 + } + } + ] +} diff --git a/src/main/resources/assets/useless_mod/blockstates/useless_compact_f9.json b/src/main/resources/assets/useless_mod/blockstates/useless_compact_f9.json new file mode 100644 index 00000000..70334d45 --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/useless_compact_f9.json @@ -0,0 +1,39 @@ +{ + "multipart": [ + { + "when": { + "facing": "north" + }, + "apply": { + "model": "useless_mod:block/useless_compact_f9" + } + }, + { + "when": { + "facing": "east" + }, + "apply": { + "model": "useless_mod:block/useless_compact_f9", + "y": 90 + } + }, + { + "when": { + "facing": "south" + }, + "apply": { + "model": "useless_mod:block/useless_compact_f9", + "y": 180 + } + }, + { + "when": { + "facing": "west" + }, + "apply": { + "model": "useless_mod:block/useless_compact_f9", + "y": 270 + } + } + ] +} diff --git a/src/main/resources/assets/useless_mod/blockstates/useless_compact_l9.json b/src/main/resources/assets/useless_mod/blockstates/useless_compact_l9.json new file mode 100644 index 00000000..608d0455 --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/useless_compact_l9.json @@ -0,0 +1,39 @@ +{ + "multipart": [ + { + "when": { + "facing": "north" + }, + "apply": { + "model": "useless_mod:block/useless_compact_l9" + } + }, + { + "when": { + "facing": "east" + }, + "apply": { + "model": "useless_mod:block/useless_compact_l9", + "y": 90 + } + }, + { + "when": { + "facing": "south" + }, + "apply": { + "model": "useless_mod:block/useless_compact_l9", + "y": 180 + } + }, + { + "when": { + "facing": "west" + }, + "apply": { + "model": "useless_mod:block/useless_compact_l9", + "y": 270 + } + } + ] +} diff --git a/src/main/resources/assets/useless_mod/blockstates/useless_glass_tier_1.json b/src/main/resources/assets/useless_mod/blockstates/useless_glass_tier_1.json new file mode 100644 index 00000000..4cc5b02c --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/useless_glass_tier_1.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/useless_glass_tier_1" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/useless_glass_tier_2.json b/src/main/resources/assets/useless_mod/blockstates/useless_glass_tier_2.json new file mode 100644 index 00000000..1e6e93f1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/useless_glass_tier_2.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/useless_glass_tier_2" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/useless_glass_tier_3.json b/src/main/resources/assets/useless_mod/blockstates/useless_glass_tier_3.json new file mode 100644 index 00000000..0e82a765 --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/useless_glass_tier_3.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/useless_glass_tier_3" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/useless_glass_tier_4.json b/src/main/resources/assets/useless_mod/blockstates/useless_glass_tier_4.json new file mode 100644 index 00000000..e12d63e2 --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/useless_glass_tier_4.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/useless_glass_tier_4" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/useless_glass_tier_5.json b/src/main/resources/assets/useless_mod/blockstates/useless_glass_tier_5.json new file mode 100644 index 00000000..d89f16d6 --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/useless_glass_tier_5.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/useless_glass_tier_5" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/useless_glass_tier_6.json b/src/main/resources/assets/useless_mod/blockstates/useless_glass_tier_6.json new file mode 100644 index 00000000..a929d031 --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/useless_glass_tier_6.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/useless_glass_tier_6" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/useless_glass_tier_7.json b/src/main/resources/assets/useless_mod/blockstates/useless_glass_tier_7.json new file mode 100644 index 00000000..309786d3 --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/useless_glass_tier_7.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/useless_glass_tier_7" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/useless_glass_tier_8.json b/src/main/resources/assets/useless_mod/blockstates/useless_glass_tier_8.json new file mode 100644 index 00000000..4b691011 --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/useless_glass_tier_8.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/useless_glass_tier_8" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/useless_glass_tier_9.json b/src/main/resources/assets/useless_mod/blockstates/useless_glass_tier_9.json new file mode 100644 index 00000000..ab0fe6e8 --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/useless_glass_tier_9.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/useless_glass_tier_9" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/white_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/white_glow_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/white_glow_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/white_plastic.json b/src/main/resources/assets/useless_mod/blockstates/white_plastic.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/white_plastic.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/white_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/white_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/white_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/yellow_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/yellow_glow_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/yellow_glow_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/yellow_plastic.json b/src/main/resources/assets/useless_mod/blockstates/yellow_plastic.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/yellow_plastic.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/blockstates/yellow_plastic_ctm.json b/src/main/resources/assets/useless_mod/blockstates/yellow_plastic_ctm.json new file mode 100644 index 00000000..d2079d5e --- /dev/null +++ b/src/main/resources/assets/useless_mod/blockstates/yellow_plastic_ctm.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "useless_mod:block/plastic/glow" + } + } +} diff --git a/src/main/resources/assets/useless_mod/lang/en_us.json b/src/main/resources/assets/useless_mod/lang/en_us.json index 3ed8048a..2fb39f47 100644 --- a/src/main/resources/assets/useless_mod/lang/en_us.json +++ b/src/main/resources/assets/useless_mod/lang/en_us.json @@ -1,9 +1,190 @@ { - "item.useless_mod.endless_beaf_item": "Endless Beef Multi-tool", - "block.useless_mod.teleport_block": "Even Dimension Teleport Block", + "item.useless_mod.omniversal_pattern_converter": "Omniversal Pattern Converter", + "tooltip.useless_mod.pattern_converter.unlinked": "No furnace or mold hub linked", + "tooltip.useless_mod.pattern_converter.bind_hint": "Shift+right-click a furnace or mold hub to bind", + "tooltip.useless_mod.pattern_converter.linked": "Bound target: %s [%s, %s, %s]", + "gui.useless_mod.paged.search": "Search", + "gui.useless_mod.paged.search.hint": "Search by name", + "gui.useless_mod.paged.search.hint.in": "Find patterns by input", + "gui.useless_mod.paged.search.hint.out": "Find patterns by output", + "gui.useless_mod.paged.search.hint.mold": "Find patterns by mold", + "gui.useless_mod.paged.search.hint.all": "Find patterns by name", + "gui.useless_mod.paged.search_mode.in": "In", + "gui.useless_mod.paged.search_mode.out": "Out", + "gui.useless_mod.paged.search_mode.mold": "Mold", + "gui.useless_mod.paged.search_mode.all": "All", + "gui.useless_mod.pattern_converter.convert": "Convert", + "message.useless_mod.pattern_converter.linked": "Linked to %s", + "message.useless_mod.pattern_converter.binding": "Bound target: %s [%s, %s, %s]", + "message.useless_mod.pattern_converter.unlinked": "Binding cleared", + "message.useless_mod.pattern_converter.unbound": "Link this converter to a furnace or mold hub first", + "message.useless_mod.pattern_converter.target_unavailable": "The linked furnace or mold hub is unavailable", + "message.useless_mod.pattern_converter.no_patterns": "No patterns to convert", + "message.useless_mod.pattern_converter.invalid_context": "The converter cannot be used here", + "message.useless_mod.pattern_converter.result": "Converted %s pattern(s); skipped %s", + "useless_mod.configuration.ae2_gift_package": "AE2 Gift Package", + "useless_mod.configuration.ae2_gift_package.items": "Gift Package Items", + "useless_mod.configuration.ae2_gift_package.items.comment": "Items granted by the AE2 gift package. Format: modid:item_id,count. Missing items and invalid quantities are skipped.", + "useless_mod.configuration.ae2_gift_package.items.tooltip": "One entry per item: modid:item_id,count. The server must have the referenced mod and item.", + "useless_mod.configuration.enable_kaleidoscope_tavern_recipe_conversion": "Enable Kaleidoscope Tavern Recipe Conversion", + "useless_mod.configuration.enable_kaleidoscope_tavern_recipe_conversion.comment": "Whether to enable conversion of Kaleidoscope Tavern barrel, pressing-tub, and shaker recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_kaleidoscope_tavern_recipe_conversion.tooltip": "Whether to enable conversion of Kaleidoscope Tavern barrel, pressing-tub, and shaker recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_kaleidoscope_grilling_recipe_conversion": "Enable Kaleidoscope Grilling Recipe Conversion", + "useless_mod.configuration.enable_kaleidoscope_grilling_recipe_conversion.comment": "Whether to enable conversion of Kaleidoscope Grilling skewer-cooking recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_kaleidoscope_grilling_recipe_conversion.tooltip": "Whether to enable conversion of Kaleidoscope Grilling skewer-cooking recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_farmersdelight_recipe_conversion": "Enable Farmer's Delight Recipe Conversion", + "useless_mod.configuration.enable_farmersdelight_recipe_conversion.comment": "Whether to enable conversion of Farmer's Delight Cooking Pot and Cutting Board recipes, including addon recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_farmersdelight_recipe_conversion.tooltip": "Whether to enable conversion of Farmer's Delight Cooking Pot and Cutting Board recipes, including addon recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_extradelight_recipe_conversion": "Enable Extra Delight Recipe Conversion", + "useless_mod.configuration.enable_extradelight_recipe_conversion.comment": "Whether to enable conversion of Extra Delight workstation recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_extradelight_recipe_conversion.tooltip": "Whether to enable conversion of Extra Delight workstation recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_crabbersdelight_recipe_conversion": "Enable Crabbers Delight Recipe Conversion", + "useless_mod.configuration.enable_crabbersdelight_recipe_conversion.comment": "Whether to enable conversion of Crabbers Delight crab-trap recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_crabbersdelight_recipe_conversion.tooltip": "Whether to enable conversion of Crabbers Delight crab-trap recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_casualnessdelight_recipe_conversion": "Enable Casualness Delight Recipe Conversion", + "useless_mod.configuration.enable_casualnessdelight_recipe_conversion.comment": "Whether to enable conversion of Casualness Delight deep-frying recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_casualnessdelight_recipe_conversion.tooltip": "Whether to enable conversion of Casualness Delight deep-frying recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_expandeddelight_recipe_conversion": "Enable Expanded Delight Recipe Conversion", + "useless_mod.configuration.enable_expandeddelight_recipe_conversion.comment": "Whether to enable conversion of Expanded Delight juicing recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_expandeddelight_recipe_conversion.tooltip": "Whether to enable conversion of Expanded Delight juicing recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_brewinandchewin_recipe_conversion": "Enable Brewin And Chewin Recipe Conversion", + "useless_mod.configuration.enable_brewinandchewin_recipe_conversion.comment": "Whether to enable conversion of Brewin And Chewin keg fermentation recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_brewinandchewin_recipe_conversion.tooltip": "Whether to enable conversion of Brewin And Chewin keg fermentation recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_nomadsdelight_recipe_conversion": "Enable Nomad's Delight Recipe Conversion", + "useless_mod.configuration.enable_nomadsdelight_recipe_conversion.comment": "Whether to enable conversion of Nomad's Delight butter churn and curd bag recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_nomadsdelight_recipe_conversion.tooltip": "Whether to enable conversion of Nomad's Delight butter churn and curd bag recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_ubesdelight_recipe_conversion": "Enable Ube's Delight Recipe Conversion", + "useless_mod.configuration.enable_ubesdelight_recipe_conversion.comment": "Whether to enable conversion of Ube's Delight baking-mat recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_ubesdelight_recipe_conversion.tooltip": "Whether to enable conversion of Ube's Delight baking-mat recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_barbequesdelight_recipe_conversion": "Enable Barbeque's Delight Recipe Conversion", + "useless_mod.configuration.enable_barbequesdelight_recipe_conversion.comment": "Whether to enable conversion of Barbeque's Delight grill and skewer recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_barbequesdelight_recipe_conversion.tooltip": "Whether to enable conversion of Barbeque's Delight grill and skewer recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_youkaishomecoming_recipe_conversion": "Enable Youkai's Homecoming Recipe Conversion", + "useless_mod.configuration.enable_youkaishomecoming_recipe_conversion.comment": "Whether to enable conversion of Youkai's Homecoming cooking, drying, and fermentation recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_youkaishomecoming_recipe_conversion.tooltip": "Whether to enable conversion of Youkai's Homecoming cooking, drying, and fermentation recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_kaleidoscope_cookery_recipe_conversion": "Enable Kaleidoscope Cookery Recipe Conversion", + "useless_mod.configuration.enable_kaleidoscope_cookery_recipe_conversion.comment": "Whether to enable conversion of Kaleidoscope Cookery kitchen recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_kaleidoscope_cookery_recipe_conversion.tooltip": "Whether to enable conversion of Kaleidoscope Cookery kitchen recipes. Takes effect after restarting the game.", + "itemGroup.useless_mod.main": "Useless", + "block.useless_mod.multiblock_alloy_furnace_core": "Omniversal Alloy Furnace Core", + "config.jade.plugin_useless_mod.multiblock_alloy_furnace_core_name": "Show Omniversal Alloy Furnace Core name", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.1": "Immortal god of synthesis, hear our prayer.", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.2": "We are your children, engineers walking the paths of machinery and arcana.", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.3": "We treasure recipes above all, for they are your most eternal gift.", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.4": "We yearn for the blessing of full-speed parallelism and utmost efficiency,", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.5": "May automation never cease, and every process accelerate within your order.", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.6": "We are sheltered by redstone and energy, guided by coils and patterns,", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.7": "Between molds, catalysts, and material flows, we seek synthesis's final answer.", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.8": "We spare no effort, traveling between mods and dimensions,", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.9": "Turning JEI's pages, deciphering ancient recipes scattered through the pack.", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.10": "With caches and networks, sort our chaos and bear ten thousand processes.", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.11": "Ring the great bell once! Pull the lever and connect the energy,", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.12": "Start the pistons and pumps; let the basic materials surge!", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.13": "Ring the great bell twice! Press the button, load the pattern, start the engine,", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.14": "Inject mana, essentia, chemicals, and the force of life!", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.15": "Ring the great bell thrice! Heat the coils and awaken the structure,", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.16": "Let the flood of parallelism course through every production chain!", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.17": "Sing together, praise the Omniversal Alloy Furnace!", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.18": "Before it, technology and magic are no longer divided,", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.19": "And the source of a recipe is no longer an obstacle.", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.20": "Praise the Omniversal Alloy Furnace, praise the all-powerful seat of synthesis!", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.21": "May its queue never clog, may our caches never overflow,", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.22": "May our automation never stop turning, and may our TPS never fall.", + "block.useless_mod.me_pattern_assembly": "ME Pattern Assembly", + "block.useless_mod.omniversal_mold_hub": "Omniversal Mold Hub", + "block.useless_mod.passive_crafting_hatch": "Passive Crafting Hatch", + "block.useless_mod.omniversal_furnace_casing": "Omniversal Furnace Casing", + "block.useless_mod.temperature_regulator": "Temperature Regulator", + "block.useless_mod.useless_coil_tier_1": "Useless Coil Tier 1", + "block.useless_mod.useless_coil_tier_2": "Useless Coil Tier 2", + "block.useless_mod.useless_coil_tier_3": "Useless Coil Tier 3", + "block.useless_mod.useless_coil_tier_4": "Useless Coil Tier 4", + "block.useless_mod.useless_coil_tier_5": "Useless Coil Tier 5", + "block.useless_mod.useless_coil_tier_6": "Useless Coil Tier 6", + "block.useless_mod.useless_coil_tier_7": "Useless Coil Tier 7", + "block.useless_mod.useless_coil_tier_8": "Useless Coil Tier 8", + "block.useless_mod.useless_coil_tier_9": "Useless Coil Tier 9", + "block.useless_mod.useful_coil": "Useful Coil", + "death.attack.useless_mod.beef_tool": "%1$s was eliminated by the Beef Tool", + "death.attack.useless_mod.beef_tool.player": "%1$s was eliminated by %2$s using the Beef Tool", + "item.useless_mod.omniversal_pattern": "Omniversal Pattern", + "tooltip.useless_mod.omniversal_pattern.recipe": "Bound recipe: %s", + "tooltip.useless_mod.omniversal_pattern.mold": "Mold marker: %s", + "tooltip.useless_mod.omniversal_pattern.no_mold": "No mold required", + "tooltip.useless_mod.omniversal_pattern.unknown_mold": "Recipe-defined mold", + "gui.useless_mod.multiblock_alloy_furnace.formed": "Structure formed", + "gui.useless_mod.multiblock_alloy_furnace.invalid": "Invalid structure", + "gui.useless_mod.multiblock_alloy_furnace.invalid_count": "Invalid structure: %s issue(s)", + "gui.useless_mod.multiblock_alloy_furnace.coil": "Coil tier: %s", + "gui.useless_mod.multiblock_alloy_furnace.redstone": "Redstone: %s", + "gui.useless_mod.multiblock_alloy_furnace.tasks": "Tasks: %s / %s", + "gui.useless_mod.multiblock_alloy_furnace.energy_limit": "FE refill limit", + "gui.useless_mod.multiblock_alloy_furnace.energy_limit.desc": "Stops AE/AppliedFlux energy draw at this stored FE amount. Set to 0 to disable automatic refill; existing energy is not removed.", + "gui.useless_mod.multiblock_alloy_furnace.energy_limit.units": "Accepts K/M/G/T/P/E suffixes and is capped by the furnace's FE capacity.", + "gui.useless_mod.multiblock_alloy_furnace.preview.correct": "Correct", + "gui.useless_mod.multiblock_alloy_furnace.preview.incorrect": "Incorrect", + "gui.useless_mod.multiblock_alloy_furnace.preview.unavailable": "Chunk not loaded", + "gui.useless_mod.multiblock_alloy_furnace.preview.expected": "Expected: %s", + "gui.useless_mod.multiblock_alloy_furnace.preview.actual": "Found: %s", + "gui.useless_mod.multiblock_alloy_furnace.preview.position": "Position: %s, %s, %s", + "gui.useless_mod.multiblock_alloy_furnace.preview.casing_with_hatch": "%s (one position may use %s)", + "gui.useless_mod.multiblock_alloy_furnace.preview.casing_with_functional_parts": "%s (among the outer positions, use exactly one %s and one %s; optionally one %s. Pattern assemblies and passive hatches are exclusive; mold hubs may be shared)", + "gui.useless_mod.multiblock_alloy_furnace.preview.coil_any": "%s through %s (all coils must use one tier)", + "gui.useless_mod.passive_crafting.interval": "Interval (ticks)", + "gui.useless_mod.passive_crafting.multiplier": "Craft multiplier", + "gui.useless_mod.passive_crafting.max_multiplier": "Maximum: %s", + "gui.useless_mod.passive_crafting.countdown": "Next cycle: %s t", + "gui.useless_mod.passive_crafting.apply": "Apply", + "gui.useless_mod.passive_crafting.apply_slot": "Slot global", + "gui.useless_mod.passive_crafting.apply_all": "All global", + "gui.useless_mod.passive_crafting.slot_global_target": "Global", + "gui.useless_mod.passive_crafting.slot_select_hint": "Ctrl-click slot", + "gui.useless_mod.passive_crafting.slot_wheel_hint": "Wheel = set", + "gui.useless_mod.passive_crafting.slot_label": "Slot %s", + "gui.useless_mod.passive_crafting.slot_label_selected": "Editing %s", + "gui.useless_mod.passive_crafting.slot_selected_hint": "Selected - ctrl-click again to deselect", + "gui.useless_mod.passive_crafting.slot_multiplier_value": "Mult. %s", + "gui.useless_mod.passive_crafting.slot_multiplier_state_custom": "Custom", + "gui.useless_mod.passive_crafting.slot_multiplier_state_global": "Global %s", + "gui.useless_mod.passive_crafting.slot_multiplier_custom": "Batch size: %s (custom, global %s)", + "gui.useless_mod.passive_crafting.slot_multiplier_global": "Batch size: %s (global)", + "gui.useless_mod.passive_crafting.slot_multiplier_edit_hint": "Ctrl-click a slot to edit its batch size on the right; scroll to adjust (Shift doubles/halves)", + "gui.useless_mod.passive_crafting.progress": "Progress: %s / %s", + "gui.useless_mod.passive_crafting.connected": "Structure linked", + "gui.useless_mod.passive_crafting.unformed": "Structure not formed", + "gui.useless_mod.passive_crafting.active_slots": "Active slots: %s / %s", + "gui.useless_mod.passive_crafting.locked_slot": "Locked recovery slot", + "gui.useless_mod.passive_crafting.status_detail": "%s: %s", + "gui.useless_mod.passive_crafting.status.empty": "Empty", + "gui.useless_mod.passive_crafting.status.ready": "Ready", + "gui.useless_mod.passive_crafting.status.running": "Running", + "gui.useless_mod.passive_crafting.status.paused": "Paused", + "gui.useless_mod.passive_crafting.status.missing_input": "Missing input", + "gui.useless_mod.passive_crafting.status.missing_mold": "Missing mold", + "gui.useless_mod.passive_crafting.status.ae_offline": "AE network offline", + "gui.useless_mod.passive_crafting.status.invalid_pattern": "Invalid pattern", + "gui.useless_mod.passive_crafting.status.waiting_output": "Waiting for output storage", + "gui.useless_mod.multiblock_builder.invalid_core": "This block is not an omniversal furnace core", + "gui.useless_mod.multiblock_builder.invalid_functional_parts": "Functional parts are duplicated, claimed by another furnace, or cannot be completed without replacing a block entity", + "gui.useless_mod.multiblock_builder.mixed_coils": "Existing coils must all have the same tier", + "gui.useless_mod.multiblock_builder.missing_coils": "No complete coil tier is available", + "gui.useless_mod.multiblock_builder.no_permission": "You cannot build in part of the structure area", + "gui.useless_mod.multiblock_builder.blocked": "The structure is blocked at %s", + "gui.useless_mod.multiblock_builder.missing_materials": "The linked AE network and inventory do not contain all materials", + "gui.useless_mod.multiblock_builder.commit_failed": "Construction failed; materials and blocks were restored", + "gui.useless_mod.multiblock_builder.success": "Built an omniversal furnace with tier %s coils", + "gui.useless_mod.omniversal_pattern.too_many_slots": "Recipe has more ingredients than the terminal has slots", + "biome.useless_mod:useless_biome": "Useless Biome", + "dimension.useless_mod.uselessdim": "Even Dimension", + "dimension.useless_mod.uselessdim2": "Odd Dimension", + "dimension.useless_mod.uselessdim3": "Triple Dimension", "block.useless_mod.ore_generator_block": "Ore Generator", + "block.useless_mod.teleport_block": "Even Dimension Teleport Block", "block.useless_mod.teleport_block_2": "Odd Dimension Teleport Block", - "biome.useless_mod:useless_biome": "Useless Biome", + "block.useless_mod.teleport_block_3": "Triple Dimension Teleport Block", + "block.useless_mod.advanced_alloy_furnace_block": "Advanced Alloy Furnace", + "block.useless_mod.creator_doll_block": "Creator Skin Doll", + "block.useless_mod.supervisor": "Supervisor", "block.useless_mod.black_glow_plastic": "Black Glow Plastic", "block.useless_mod.red_glow_plastic": "Red Glow Plastic", "block.useless_mod.green_glow_plastic": "Green Glow Plastic", @@ -22,61 +203,76 @@ "block.useless_mod.white_glow_plastic": "White Glow Plastic", "block.useless_mod.dark_red_glow_plastic": "Dark Red Glow Plastic", "block.useless_mod.aqua_glow_plastic": "Aqua Glow Plastic", - "key.category.useless_mod.useless": "Useless Mod", - "key.useless_mod.switch_silk_touch": "Switch to Silk Touch", - "key.useless_mod.switch_fortune": "Switch to Fortune", - "key.useless_mod.chain_mining": "Chain Mining Toggle", - "key.useless_mod.enhanced_chain_mining": "Enhanced Chain Mining Toggle", - "key.useless_mod.force_mining": "Force Mining Toggle", - "key.useless_mod.switch_mode_wheel": "Open Mode Selection Wheel", - "key.useless_mod.trigger_force_mining": "Trigger Force Mining", - "key.useless_mod.set_master_pattern": "Set Master Pattern Provider", - "key.useless_mod.set_slave_pattern": "Set Slave Pattern Provider", - "category.useless_mod.chain_mining": "Useless Mod Controls", - "tooltip.useless_mod.silk_touch_mode": "Mode: Silk Touch", - "tooltip.useless_mod.fortune_mode": "Mode: Fortune", - "tooltip.useless_mod.silk_touch_level": "Silk Touch %s", - "tooltip.useless_mod.fortune_level": "Fortune %s", - "tooltip.useless_mod.external_enchantment": "(Enhanced by external enchantments)", - "tooltip.useless_mod.switch_enchantment": "Use PAGE_UP/PAGE_DOWN to switch modes", - "tooltip.useless_mod.wrench_function": "Can be used as a wrench (compatible with other mods)", - "tooltip.useless_mod.fast_break_plastic": "Shift + Right Click to quickly break plastic blocks", + "block.useless_mod.black_plastic": "Black Plastic", + "block.useless_mod.black_plastic_ctm": "Black Plastic (Connected)", + "block.useless_mod.black_glow_plastic_ctm": "Black Glow Plastic (Connected)", + "block.useless_mod.red_plastic": "Red Plastic", + "block.useless_mod.red_plastic_ctm": "Red Plastic (Connected)", + "block.useless_mod.red_glow_plastic_ctm": "Red Glow Plastic (Connected)", + "block.useless_mod.green_plastic": "Green Plastic", + "block.useless_mod.green_plastic_ctm": "Green Plastic (Connected)", + "block.useless_mod.green_glow_plastic_ctm": "Green Glow Plastic (Connected)", + "block.useless_mod.brown_plastic": "Brown Plastic", + "block.useless_mod.brown_plastic_ctm": "Brown Plastic (Connected)", + "block.useless_mod.brown_glow_plastic_ctm": "Brown Glow Plastic (Connected)", + "block.useless_mod.blue_plastic": "Blue Plastic", + "block.useless_mod.blue_plastic_ctm": "Blue Plastic (Connected)", + "block.useless_mod.blue_glow_plastic_ctm": "Blue Glow Plastic (Connected)", + "block.useless_mod.purple_plastic": "Purple Plastic", + "block.useless_mod.purple_plastic_ctm": "Purple Plastic (Connected)", + "block.useless_mod.purple_glow_plastic_ctm": "Purple Glow Plastic (Connected)", + "block.useless_mod.cyan_plastic": "Cyan Plastic", + "block.useless_mod.cyan_plastic_ctm": "Cyan Plastic (Connected)", + "block.useless_mod.cyan_glow_plastic_ctm": "Cyan Glow Plastic (Connected)", + "block.useless_mod.light_gray_plastic": "Light Gray Plastic", + "block.useless_mod.light_gray_plastic_ctm": "Light Gray Plastic (Connected)", + "block.useless_mod.light_gray_glow_plastic_ctm": "Light Gray Glow Plastic (Connected)", + "block.useless_mod.gray_plastic": "Gray Plastic", + "block.useless_mod.gray_plastic_ctm": "Gray Plastic (Connected)", + "block.useless_mod.gray_glow_plastic_ctm": "Gray Glow Plastic (Connected)", + "block.useless_mod.pink_plastic": "Pink Plastic", + "block.useless_mod.pink_plastic_ctm": "Pink Plastic (Connected)", + "block.useless_mod.pink_glow_plastic_ctm": "Pink Glow Plastic (Connected)", + "block.useless_mod.lime_plastic": "Lime Plastic", + "block.useless_mod.lime_plastic_ctm": "Lime Plastic (Connected)", + "block.useless_mod.lime_glow_plastic_ctm": "Lime Glow Plastic (Connected)", + "block.useless_mod.yellow_plastic": "Yellow Plastic", + "block.useless_mod.yellow_plastic_ctm": "Yellow Plastic (Connected)", + "block.useless_mod.yellow_glow_plastic_ctm": "Yellow Glow Plastic (Connected)", + "block.useless_mod.light_blue_plastic": "Light Blue Plastic", + "block.useless_mod.light_blue_plastic_ctm": "Light Blue Plastic (Connected)", + "block.useless_mod.light_blue_glow_plastic_ctm": "Light Blue Glow Plastic (Connected)", + "block.useless_mod.magenta_plastic": "Magenta Plastic", + "block.useless_mod.magenta_plastic_ctm": "Magenta Plastic (Connected)", + "block.useless_mod.magenta_glow_plastic_ctm": "Magenta Glow Plastic (Connected)", + "block.useless_mod.orange_plastic": "Orange Plastic", + "block.useless_mod.orange_plastic_ctm": "Orange Plastic (Connected)", + "block.useless_mod.orange_glow_plastic_ctm": "Orange Glow Plastic (Connected)", + "block.useless_mod.white_plastic": "White Plastic", + "block.useless_mod.white_plastic_ctm": "White Plastic (Connected)", + "block.useless_mod.white_glow_plastic_ctm": "White Glow Plastic (Connected)", + "block.useless_mod.dark_red_plastic": "Dark Red Plastic", + "block.useless_mod.dark_red_plastic_ctm": "Dark Red Plastic (Connected)", + "block.useless_mod.dark_red_glow_plastic_ctm": "Dark Red Glow Plastic (Connected)", + "block.useless_mod.aqua_plastic": "Aqua Plastic", + "block.useless_mod.aqua_plastic_ctm": "Aqua Plastic (Connected)", + "block.useless_mod.aqua_glow_plastic_ctm": "Aqua Glow Plastic (Connected)", + "block.useless_mod.useless_glass_tier_1": "Useless Glass Tier 1", + "block.useless_mod.useless_glass_tier_2": "Useless Glass Tier 2", + "block.useless_mod.useless_glass_tier_3": "Useless Glass Tier 3", + "block.useless_mod.useless_glass_tier_4": "Useless Glass Tier 4", + "block.useless_mod.useless_glass_tier_5": "Useless Glass Tier 5", + "block.useless_mod.useless_glass_tier_6": "Useless Glass Tier 6", + "block.useless_mod.useless_glass_tier_7": "Useless Glass Tier 7", + "block.useless_mod.useless_glass_tier_8": "Useless Glass Tier 8", + "block.useless_mod.useless_glass_tier_9": "Useless Glass Tier 9", "item.useless_mod.endless_beaf_item.silk_touch": "Endless Beef Multi-tool (Silk Touch)", "item.useless_mod.endless_beaf_item.fortune": "Endless Beef Multi-tool (Fortune)", - "text.useless_mod.silk_touch": "Silk Touch", - "text.useless_mod.fortune": "Fortune", - "message.useless_mod.switched_to_silk_touch": "§bSwitched to Silk Touch mode", - "message.useless_mod.switched_to_fortune": "§6Switched to Fortune mode", - "message.useless_mod.festive_triggered": "§6Loot explosion!", - "tooltip.useless_mod.festive_affix": "5% chance to trigger loot explosion on kill", - "tooltip.useless_mod.auto_collect": "Built-in magnet function", - "config.useless_mod.botany_pot_growth_multiplier": "Botany Pot Growth Multiplier", - "config.useless_mod.matrix_pattern_count": "Matrix Pattern Count Multiplier", - "item.useless_mod.useless_integral_component_tier_1": "Useless Integral Component Tier 1", - "item.useless_mod.useless_integral_component_tier_2": "Useless Integral Component Tier 2", - "item.useless_mod.useless_integral_component_tier_3": "Useless Integral Component Tier 3", - "item.useless_mod.useless_integral_component_tier_4": "Useless Integral Component Tier 4", - "item.useless_mod.useless_integral_component_tier_5": "Useless Integral Component Tier 5", - "item.useless_mod.useless_integral_component_tier_6": "Useless Integral Component Tier 6", - "item.useless_mod.useless_integral_component_tier_7": "Useless Integral Component Tier 7", - "item.useless_mod.useless_integral_component_tier_8": "Useless Integral Component Tier 8", - "item.useless_mod.useless_integral_component_tier_9": "Useless Integral Component Tier 9", - "useless_mod.augment.upgrade": "Upgrade Augment", - "info.useless_mod.lava_gen_nether": "Works in the Nether", - "info.thermal.augment.attr.MachineParallel": "Maximum Parallel", - "item.useless_mod.augment_parallel_1": "Parallel Processing Augment I", - "item.useless_mod.augment_parallel_2": "Parallel Processing Augment II", - "item.useless_mod.augment_parallel_3": "Parallel Processing Augment III", - "config.useless_mod.parallel_increase_energy_consumption": "Parallel Increases Energy Consumption", - "config.useless_mod.base_mod_affect_parallel": "Base Multiplier Affects Parallel", - "config.useless_mod.add_extra_parallel_augments_to_tab": "Add Extra Parallel Augments to Tab", - "config.useless_mod.parallel_increase_item_cap": "Parallel Increases Item Capacity", - "config.useless_mod.parallel_increase_fluid_cap": "Parallel Increases Fluid Capacity", - "config.useless_mod.parallel_increase_energy_cap": "Parallel Increases Energy Capacity", - "config.useless_mod.parallel_increase_item_transfer": "Parallel Increases Item Transfer", - "config.useless_mod.parallel_increase_fluid_transfer": "Parallel Increases Fluid Transfer", - "config.useless_mod.parallel_increase_energy_transfer": "Parallel Increases Energy Transfer", - "block.useless_mod.advanced_alloy_furnace_block": "Advanced Alloy Furnace", + "item.useless_mod.endless_beaf_item": "Endless Beef Multi-tool", + "advancements.useless_mod.grass_wand_drop.title": "This Time It Really Dropped from Grass", + "advancements.useless_mod.grass_wand_drop.description": "You mined a Fortune wand from grass.", + "item.useless_mod.creator_doll_block": "Creator Skin Doll", + "item.useless_mod.supervisor": "Supervisor", "item.useless_mod.useless_ingot_tier_1": "Useless Ingot Tier 1", "item.useless_mod.useless_ingot_tier_2": "Useless Ingot Tier 2", "item.useless_mod.useless_ingot_tier_3": "Useless Ingot Tier 3", @@ -86,25 +282,6 @@ "item.useless_mod.useless_ingot_tier_7": "Useless Ingot Tier 7", "item.useless_mod.useless_ingot_tier_8": "Useless Ingot Tier 8", "item.useless_mod.useless_ingot_tier_9": "Useless Ingot Tier 9", - "message.useless_mod.chain_mining_off": "§cChain mining disabled", - "config.useless_mod.chain_mining_range_x": "Chain Mining Range X", - "config.useless_mod.chain_mining_range_y": "Chain Mining Range Y", - "config.useless_mod.chain_mining_range_z": "Chain Mining Range Z", - "config.useless_mod.chain_mining_max_blocks": "Chain Mining Maximum Blocks", - "message.useless_mod.enhanced_chain_on": "§eEnhanced chain mining enabled", - "message.useless_mod.enhanced_chain_off": "§cEnhanced chain mining disabled", - "message.useless_mod.force_mining_on": "§eForce mining enabled", - "message.useless_mod.force_mining_off": "§cForce mining disabled", - "tooltip.useless_mod.enhanced_chain_mining_on": "Enhanced Chain Mining: On", - "tooltip.useless_mod.enhanced_chain_mining_off": "Enhanced Chain Mining: Off", - "tooltip.useless_mod.chain_mining_enhanced": "Enhanced Chain Mode", - "tooltip.useless_mod.chain_mining_normal": "Normal Chain Mode", - "tooltip.useless_mod.switch_enhanced_chain": "Toggle Enhanced Chain Mining: %s", - "tooltip.useless_mod.enhanced_chain_description": "Enhanced Chain Mining: Removes the adjacent-only limitation for chain mining", - "tooltip.useless_mod.omnitool_mode": "Mode: Omnitool", - "tooltip.useless_mod.force_mining_mode": "Mode: Force Mining", - "tooltip.useless_mod.ae_storage_priority_mode": "Mode: AE Storage Priority", - "jei.useless_mod.catalyst_info": "Catalyst Parallel Info", "item.useless_mod.possible_useful_ingot": "Possible Useful Ingot", "item.useless_mod.useful_ingot": "Useful Ingot", "item.useless_mod.metal_mold_plate": "Metal Mold (Plate)", @@ -112,8 +289,6 @@ "item.useless_mod.metal_mold_gear": "Metal Mold (Gear)", "item.useless_mod.metal_mold_wire": "Metal Mold (Wire)", "item.useless_mod.metal_mold_block": "Metal Mold (Block)", - "block.useless_mod.creator_doll_block": "Creator Skin Doll", - "item.useless_mod.creator_doll_block": "Creator Skin Doll", "item.useless_mod.useless_gear_tier_1": "Useless Gear Tier 1", "item.useless_mod.useless_gear_tier_2": "Useless Gear Tier 2", "item.useless_mod.useless_gear_tier_3": "Useless Gear Tier 3", @@ -123,17 +298,1015 @@ "item.useless_mod.useless_gear_tier_7": "Useless Gear Tier 7", "item.useless_mod.useless_gear_tier_8": "Useless Gear Tier 8", "item.useless_mod.useless_gear_tier_9": "Useless Gear Tier 9", - "item.useless_mod.useless_glass_tier_1": "Useless Glass Tier 1", - "item.useless_mod.useless_glass_tier_2": "Useless Glass Tier 2", - "item.useless_mod.useless_glass_tier_3": "Useless Glass Tier 3", - "item.useless_mod.useless_glass_tier_4": "Useless Glass Tier 4", - "item.useless_mod.useless_glass_tier_5": "Useless Glass Tier 5", - "item.useless_mod.useless_glass_tier_6": "Useless Glass Tier 6", - "item.useless_mod.useless_glass_tier_7": "Useless Glass Tier 7", - "item.useless_mod.useless_glass_tier_8": "Useless Glass Tier 8", - "item.useless_mod.useless_glass_tier_9": "Useless Glass Tier 9", + "item.useless_mod.ae2_gift_package": "AE pack", "emi.category.useless_mod.advanced_alloy_furnace": "Advanced Alloy Furnace", "emi.category.useless_mod.catalyst_info": "Catalyst Parallel Info", + + "gui.useless_mod.advanced_alloy_furnace.energy": "Energy: %s / %s FE", + "gui.useless_mod.advanced_alloy_furnace.progress": "Progress: %s/%s (%s%%)", + "gui.useless_mod.advanced_alloy_furnace.status": "Status: %s", + "gui.useless_mod.advanced_alloy_furnace.status.active": "Active", + "gui.useless_mod.advanced_alloy_furnace.status.idle": "Idle", + "gui.useless_mod.advanced_alloy_furnace.current_parallel": "Current Parallel: %s", + "gui.useless_mod.advanced_alloy_furnace.max_parallel": "Max Parallel: %s", + "gui.useless_mod.advanced_alloy_furnace.ae_tasks": "AE Tasks: %s", + "gui.useless_mod.advanced_alloy_furnace.ae_progress": "AE Progress: %s/%s (%s%%)", + "gui.useless_mod.advanced_alloy_furnace.ae_task_progress": " - %s (x%d): %d/%d (%s%%) [%s]", + "gui.useless_mod.advanced_alloy_furnace.ae_task_progress_ticks": " - %s (x%d): %dt / %dt [%s]", + "gui.useless_mod.advanced_alloy_furnace.ae_task_waiting": " - %s (x%d): %s", + "gui.useless_mod.advanced_alloy_furnace.ae_task_waiting_detail": " - %s (x%d): %s, needs %s", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.processing": "Processing", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.queued": "Queued", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_mold": "Waiting for mold/condition", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_structure": "Waiting for multiblock structure", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_mold_hub": "Waiting for mold hub", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_missing_mold": "Missing required mold", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_tier": "Waiting for required furnace/coil tier", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_output_mismatch": "Pattern output does not match", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_invalid_pattern": "Invalid crafting pattern", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_conditions": "Waiting for runtime conditions", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_recipe": "Waiting for an exact recipe", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_ambiguous_recipe": "Waiting: recipe is not unique", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_energy": "Waiting for energy", + "gui.useless_mod.advanced_alloy_furnace.ae_task_recipe_not_found": "No exact recipe matches the pattern inputs", + "gui.useless_mod.advanced_alloy_furnace.ae_task_recipe_ambiguous": "Multiple exact recipes match; waiting", + "gui.useless_mod.advanced_alloy_furnace.ae_task_waiting_energy_detail": "Furnace energy is insufficient", + "gui.useless_mod.advanced_alloy_furnace.recipe_priority": "Machine recipe priority", + "gui.useless_mod.advanced_alloy_furnace.recipe_priority_up": "Move up", + "gui.useless_mod.advanced_alloy_furnace.recipe_priority_down": "Move down", + "gui.useless_mod.advanced_alloy_furnace.recipe_priority_reset": "Reset automatic order", + "gui.useless_mod.advanced_alloy_furnace.ae_task_missing_correct_mold": "correct mold", + "gui.useless_mod.advanced_alloy_furnace.view_recipes": "Click to view recipes", + "gui.useless_mod.advanced_alloy_furnace.no_process": "No active process", + "gui.useless_mod.advanced_alloy_furnace.parallel_info": "Parallel Info", + "gui.useless_mod.advanced_alloy_furnace.catalyst": "Catalyst: %s (%sx)", + "gui.useless_mod.advanced_alloy_furnace.parallel_description.title": "Parallel Description:", + "gui.useless_mod.advanced_alloy_furnace.parallel_description.1": "• Consumption and output multiplied by parallel", + "gui.useless_mod.advanced_alloy_furnace.parallel_description.2": "• Processing time remains unchanged", + "gui.useless_mod.advanced_alloy_furnace.parallel_description.3": "• Energy consumption multiplied by parallel", + "gui.useless_mod.advanced_alloy_furnace.parallel_description.4": "• Catalyst is optional, can increase parallel", + "gui.useless_mod.advanced_alloy_furnace.catalyst_warning": "⚠ Normal catalyst will be consumed", + "gui.useless_mod.advanced_alloy_furnace.fluid_amount": "%s / %s mB", + "gui.useless_mod.advanced_alloy_furnace.chemical_amount": "%s / %s", + "gui.useless_mod.advanced_alloy_furnace.face_mode.disabled": "Disabled", + "gui.useless_mod.advanced_alloy_furnace.face_mode.material_input": "Material Input", + "gui.useless_mod.advanced_alloy_furnace.face_mode.material_output": "Material Output", + "gui.useless_mod.advanced_alloy_furnace.face_mode.material_input_output": "Material I/O", + "gui.useless_mod.advanced_alloy_furnace.face_mode.catalyst_input": "Catalyst Input", + "gui.useless_mod.advanced_alloy_furnace.face_mode.mold_input": "Mold Input", + "gui.useless_mod.advanced_alloy_furnace.face.top": "Top", + "gui.useless_mod.advanced_alloy_furnace.face.left": "Left", + "gui.useless_mod.advanced_alloy_furnace.face.front": "Front", + "gui.useless_mod.advanced_alloy_furnace.face.right": "Right", + "gui.useless_mod.advanced_alloy_furnace.face.bottom": "Bottom", + "gui.useless_mod.advanced_alloy_furnace.face.back": "Back", + "gui.useless_mod.advanced_alloy_furnace.auto_output": "Auto Output", + "gui.useless_mod.advanced_alloy_furnace.auto_input": "Auto Input", + "gui.useless_mod.advanced_alloy_furnace.redstone_control": "Redstone Control", + "gui.useless_mod.advanced_alloy_furnace.redstone_control.disabled": "Disabled", + "gui.useless_mod.advanced_alloy_furnace.redstone_control.high_active": "High Active", + "gui.useless_mod.advanced_alloy_furnace.redstone_control.low_active": "Low Active", + "gui.useless_mod.advanced_alloy_furnace.cancel_ae_tasks": "Cancel AE Tasks", + "gui.useless_mod.advanced_alloy_furnace.cancel_ae_tasks.desc": "Cancel all active tasks and refund materials", + "gui.useless_mod.advanced_alloy_furnace.ae_return_output": "Return to AE", + "gui.useless_mod.enabled": "Enabled", + "gui.useless_mod.disabled": "Disabled", + "container.useless_mod.advanced_alloy_furnace": "Advanced Alloy Furnace", + "pack.useless_mod.god.name": "Useless Mod God Pack", - "pack.useless_mod.god.description": "A built-in resource pack for Useless Mod" -} \ No newline at end of file + "pack.useless_mod.god.description": "A built-in resource pack for Useless Mod", + + "gui.useless_mod.chain_mining_status": "Chain: %s", + "gui.useless_mod.force_mining_label": "Force Mining: ", + "pack.useless_mod.xia.name": "Useless Mod - Xia Texture Optimization Pack", + "pack.useless_mod.xia.description": "Textures from AE2 & Mekanism, partially edited by Xiabishi Lidie, Useless Mod texture optimization V2.1", + "gui.useless_mod.mining_count": "Mining Count: %s", + "gui.useless_mod.status.enhanced": "Enhanced Chain", + "gui.useless_mod.status.normal": "Normal Chain", + "gui.useless_mod.shape_label": "Shape: %s (%s)", + "gui.useless_mod.shape_hint": "Hold chain key + Shift + scroll to switch shape", + "gui.useless_mod.shape_switched": "Chain Shape: %s (%s)", + "gui.useless_mod.mining_count_limit": "Mining Count: %s / %s", + "gui.useless_mod.shape.shapeless": "Shapeless Chain", + "gui.useless_mod.shape.shapeless.desc": "Adjacent-spread scan; range and limit follow config", + "gui.useless_mod.shape.small_tunnel": "1x1 Tunnel", + "gui.useless_mod.shape.small_tunnel.desc": "Extends straight inward from the clicked face", + "gui.useless_mod.shape.small_square": "3x3 Plane", + "gui.useless_mod.shape.small_square.desc": "Single 3x3 layer on the clicked face", + "gui.useless_mod.shape.large_tunnel": "3x3 Tunnel", + "gui.useless_mod.shape.large_tunnel.desc": "3x3 cross-section advancing inward from the clicked face", + "gui.useless_mod.shape.mining_tunnel": "Downward Diagonal", + "gui.useless_mod.shape.mining_tunnel.desc": "Descends one block per step from the clicked face", + "gui.useless_mod.shape.escape_tunnel": "Upward Diagonal", + "gui.useless_mod.shape.escape_tunnel.desc": "Ascends one block per step from the clicked face", + "gui.useless_mod.force.enabled": "Enabled", + "gui.useless_mod.force.disabled": "Disabled", + "gui.useless_mod.festive_triggered": "§6Loot explosion!", + "gui.useless_mod.chain_mining_result": "Chain Mining: Mined %s blocks", + "gui.useless_mod.enhanced_chain_mining_result": "Enhanced Chain Mining: Mined %s blocks", + "gui.useless_mod.force_chain_mining_result": "Force Chain Mining: Mined %s blocks", + "gui.useless_mod.force_enhanced_chain_mining_result": "Force Enhanced Chain Mining: Mined %s blocks", + "gui.useless_mod.wireless_access_point_bound": "Successfully bound wireless access point: %s", + "gui.useless_mod.ae2.not_bound": "Not bound to a wireless access point", + "gui.useless_mod.ae2.chunk_not_loaded": "Target area is not loaded", + "gui.useless_mod.ae2.invalid_target": "Linked target is not a valid wireless access point", + "gui.useless_mod.ae2.offline": "Wireless access point is offline", + "gui.useless_mod.ae_connect.not_bound": "Not bound to a wireless access point (Shift + right-click an ME Wireless Access Point)", + "gui.useless_mod.ae_connect.access_point": "AE Connection Mode cannot target the access point itself", + "gui.useless_mod.ae_connect.access_point_unloaded": "The bound wireless access point is not loaded", + "gui.useless_mod.ae_connect.invalid_access_point": "The bound position no longer holds a wireless access point", + "gui.useless_mod.ae_connect.access_point_offline": "The bound wireless access point is offline", + "gui.useless_mod.ae_connect.not_ready": "This block has no usable AE grid node yet", + "gui.useless_mod.ae_connect.already_networked": "%s already belongs to another AE network; disconnect it first", + "gui.useless_mod.ae_connect.linked": "%s is now connected to the tool's AE network", + "gui.useless_mod.ae_connect.unlinked": "%s has been disconnected from the AE network", + "screen.useless_mod.title": "Useless Mod Configuration", + "gui.useless_mod.mode_config.title": "Beef Tool Configuration", + "gui.useless_mod.mode_config.tools": "Tools", + "gui.useless_mod.mode_config.mining": "Mining", + "gui.useless_mod.mode_config.combat": "Combat", + "gui.useless_mod.mode_config.vanilla": "Vanilla", + "gui.useless_mod.mode_config.auxiliary": "Auxiliary", + "gui.useless_mod.mode_config.current": "%s (Current)", + "gui.useless_mod.mode_config.state": "%s: %s", + "gui.useless_mod.mode_config.page_name": "Page Name", + "gui.useless_mod.mode_config.group_name": "Group Name", + "gui.useless_mod.mode_config.edit": "Edit Layout", + "gui.useless_mod.mode_config.done": "Done", + "gui.useless_mod.mode_config.add_page": "+Page", + "gui.useless_mod.mode_config.delete_page": "Delete Page", + "gui.useless_mod.mode_config.add_group": "+Group", + "gui.useless_mod.mode_config.export": "Export", + "gui.useless_mod.mode_config.import": "Import", + "gui.useless_mod.mode_config.unassigned": "Unassigned Modules", + "gui.useless_mod.mode_config.empty": "No available modules", + "gui.useless_mod.mode_config.loading": "Loading layout...", + "gui.useless_mod.mode_config.exported": "Layout copied to clipboard", + "gui.useless_mod.mode_config.importing": "Importing layout...", + "gui.useless_mod.mode_config.import_confirm": "Import replaces layout + chain groups. Continue?", + "gui.useless_mod.mode_config.confirm": "Confirm", + "gui.useless_mod.mode_config.cancel": "Cancel", + "gui.useless_mod.mode_config.error.invalid_text": "Invalid layout text", + "gui.useless_mod.mode_config.error.unsupported_version": "Unsupported layout version", + "gui.useless_mod.mode_config.error.invalid_structure": "Invalid layout structure", + "gui.useless_mod.mode_config.error.invalid_name": "Invalid page or group name", + "gui.useless_mod.mode_config.error.duplicate_module": "The layout contains a duplicate module", + "gui.useless_mod.mode_config.error.unknown_module": "The layout contains an unknown module", + "gui.useless_mod.mode_config.error.limit": "The layout exceeds its size limit", + "gui.useless_mod.chain_group.title": "Chain Equivalence Groups", + "gui.useless_mod.chain_group.group_label": "Groups (blocks in a group chain together)", + "gui.useless_mod.chain_group.entry_label": "Blocks in the selected group", + "gui.useless_mod.chain_group.add_group": "New group", + "gui.useless_mod.chain_group.remove_group": "Delete", + "gui.useless_mod.chain_group.add_entry": "Add", + "gui.useless_mod.chain_group.entry_hint": "#tag / id / *wild", + "gui.useless_mod.chain_group.empty": "No groups yet - click \"New group\" to start", + "gui.useless_mod.chain_group.entry_empty": "This group has no blocks yet", + "gui.useless_mod.chain_group.drop_hint": "Drag a block here from JEI to add it to this group", + "gui.useless_mod.chain_group.gear_tooltip": "Click to configure chain equivalence groups", + "gui.useless_mod.mode_config.beef_protect_mode.title": "Protection List Mode: while on, the Kill Aura and AoE Damage stand down so you can add creatures to the list at leisure.", + "gui.useless_mod.mode_config.beef_protect_mode.ctrl": "Ctrl + right-click a creature: toggle protection for its TYPE (e.g. all villagers)", + "gui.useless_mod.mode_config.beef_protect_mode.shift": "Shift + right-click a creature: toggle protection for that SINGLE entity", + "gui.useless_mod.mode_config.beef_protect_mode.auto": "Your own pets and mounts are always protected. Turn this mode off when you are done.", + "gui.useless_mod.mode_config.wireless_logistics.title": "Wireless Logistics: sneak-right-click a container block to bind it to this staff's network.", + "gui.useless_mod.mode_config.wireless_logistics.key": "Press %s to open the logistics config screen (only while holding the staff).", + "gui.useless_mod.chain_group.error.no_group": "Select a group first", + "gui.useless_mod.chain_group.error.invalid": "Invalid entry (use a block id, #tag, or * wildcard)", + "gui.useless_mod.chain_group.error.duplicate": "That entry is already in this group", + "useless_mod.configuration.game_mechanics": "Game Mechanics Settings", + "useless_mod.configuration.botany_pot_growth_multiplier": "Botany Pot Growth Multiplier", + "useless_mod.configuration.botany_pot_growth_multiplier.comment": "Botany pot growth multiplier - 1.0 is vanilla speed, 2.0 is 2x speed", + "useless_mod.configuration.enable_botany_pot_rendering": "Enable Botany Pot Rendering", + "useless_mod.configuration.enable_botany_pot_rendering.comment": "Whether to enable botany pot crop rendering, disabling may improve performance", + "useless_mod.configuration.matrix_pattern_count": "Matrix Pattern Slot Multiplier", + "useless_mod.configuration.matrix_pattern_count.comment": "Matrix pattern slot multiplier - Keep slots empty when reducing! Otherwise patterns may be lost", + "useless_mod.configuration.beef_tool": "Beef Tool Settings", + "useless_mod.configuration.enable_potion_effects": "Enable Potion Effects", + "useless_mod.configuration.enable_potion_effects.comment": "Whether to enable potion effects", + "useless_mod.configuration.enable_potion_effects.tooltip": "Whether to enable potion effects", + "useless_mod.configuration.enable_flight_effect": "Enable Flight Effect", + "useless_mod.configuration.enable_flight_effect.comment": "Whether to enable flight effect", + "useless_mod.configuration.enable_flight_effect.tooltip": "Whether to enable flight effect", + "useless_mod.configuration.beef_tool_flight_speed": "Beef Tool Flight Speed", + "useless_mod.configuration.beef_tool_flight_speed.comment": "Flight speed provided by the beef tool", + "useless_mod.configuration.beef_tool_flight_speed.tooltip": "Flight speed provided by the beef tool", + "useless_mod.configuration.chain_mining_range_x": "Chain Mining X Range", + "useless_mod.configuration.chain_mining_range_x.comment": "Chain mining X-axis range radius", + "useless_mod.configuration.chain_mining_range_x.tooltip": "Chain mining X-axis range radius", + "useless_mod.configuration.chain_mining_range_y": "Chain Mining Y Range", + "useless_mod.configuration.chain_mining_range_y.comment": "Chain mining Y-axis range radius", + "useless_mod.configuration.chain_mining_range_y.tooltip": "Chain mining Y-axis range radius", + "useless_mod.configuration.chain_mining_range_z": "Chain Mining Z Range", + "useless_mod.configuration.chain_mining_range_z.comment": "Chain mining Z-axis range radius", + "useless_mod.configuration.chain_mining_range_z.tooltip": "Chain mining Z-axis range radius", + "useless_mod.configuration.beef_aoe_damage_range_x": "AoE Damage X Range", + "useless_mod.configuration.beef_aoe_damage_range_x.comment": "AoE damage X-axis range radius", + "useless_mod.configuration.beef_aoe_damage_range_x.tooltip": "AoE damage X-axis range radius", + "useless_mod.configuration.beef_aoe_damage_range_y": "AoE Damage Y Range", + "useless_mod.configuration.beef_aoe_damage_range_y.comment": "AoE damage Y-axis range radius", + "useless_mod.configuration.beef_aoe_damage_range_y.tooltip": "AoE damage Y-axis range radius", + "useless_mod.configuration.beef_aoe_damage_range_z": "AoE Damage Z Range", + "useless_mod.configuration.beef_aoe_damage_range_z.comment": "AoE damage Z-axis range radius", + "useless_mod.configuration.beef_aoe_damage_range_z.tooltip": "AoE damage Z-axis range radius", + "useless_mod.configuration.beef_aoe_damage_max_targets": "AoE Damage Max Targets", + "useless_mod.configuration.beef_aoe_damage_max_targets.comment": "Maximum number of entities hit by a single AoE attack", + "useless_mod.configuration.beef_aoe_damage_max_targets.tooltip": "Maximum number of entities hit by a single AoE attack (high values may cause lag)", + "useless_mod.configuration.beef_kill_aura_interval": "Kill Aura Interval (ticks)", + "useless_mod.configuration.beef_kill_aura_interval.comment": "How many ticks between kill aura damage sweeps (20 ticks = 1 second)", + "useless_mod.configuration.beef_kill_aura_interval.tooltip": "Smaller = faster sweeps, but server-side entity query load rises linearly", + "useless_mod.configuration.beef_magnet_range_x": "Magnet X Range", + "useless_mod.configuration.beef_magnet_range_x.comment": "X-axis range radius of the post-kill magnet", + "useless_mod.configuration.beef_magnet_range_x.tooltip": "X-axis range radius of the post-kill magnet", + "useless_mod.configuration.beef_magnet_range_y": "Magnet Y Range", + "useless_mod.configuration.beef_magnet_range_y.comment": "Y-axis range radius of the post-kill magnet", + "useless_mod.configuration.beef_magnet_range_y.tooltip": "Y-axis range radius of the post-kill magnet", + "useless_mod.configuration.beef_magnet_range_z": "Magnet Z Range", + "useless_mod.configuration.beef_magnet_range_z.comment": "Z-axis range radius of the post-kill magnet", + "useless_mod.configuration.beef_magnet_range_z.tooltip": "Z-axis range radius of the post-kill magnet", + "useless_mod.configuration.chain_mining_max_blocks": "Chain Mining Max Blocks", + "useless_mod.configuration.chain_mining_max_blocks.comment": "Maximum number of blocks for chain mining", + "useless_mod.configuration.chain_mining_max_blocks.tooltip": "Maximum number of blocks for chain mining (high values may cause lag)", + "useless_mod.configuration.fortune_level": "Beef Tool Fortune Level", + "useless_mod.configuration.fortune_level.comment": "Fortune enchantment level for beef tool", + "useless_mod.configuration.fortune_level.tooltip": "Fortune enchantment level for beef tool", + "useless_mod.configuration.looting_level": "Beef Tool Looting Level", + "useless_mod.configuration.looting_level.comment": "Looting enchantment level for beef tool", + "useless_mod.configuration.looting_level.tooltip": "Looting enchantment level for beef tool", + "useless_mod.configuration.beef_tool_mining_speed": "Beef Tool Mining Speed", + "useless_mod.configuration.beef_tool_mining_speed.comment": "Base mining speed for the beef tool", + "useless_mod.configuration.beef_tool_mining_speed.tooltip": "Base mining speed for the beef tool", + "useless_mod.configuration.beef_tool_ignores_tool_tier": "Staff Ignores Tool Tier", + "useless_mod.configuration.beef_tool_ignores_tool_tier.comment": "Whether the creation staff ignores every tool mining tier restriction (including custom tiers added by other mods). On: the staff can harvest any block that requires a specific tool tier. Off: falls back to the vanilla netherite tier rules. Blocks in useless_mod:beef_tool_tier_locked always keep tier checks.", + "useless_mod.configuration.beef_tool_ignores_tool_tier.tooltip": "Whether the creation staff ignores every tool mining tier restriction", + "useless_mod.configuration.beef_tool_pseudo_hardness": "Pseudo-Unbreakable Block Nominal Hardness", + "useless_mod.configuration.beef_tool_pseudo_hardness.comment": "Nominal hardness the staff substitutes for hardness = -1 when mining 'pseudo-unbreakable' blocks (unbreakable in vanilla terms, but supplying their own getDestroyProgress - e.g. Allthemodium ores and ancient stone). Dig speed = beef_tool_mining_speed x this value. 15 roughly matches the feel of mining a normal block; raise it to mine faster.", + "useless_mod.configuration.beef_tool_pseudo_hardness.tooltip": "Nominal hardness used for pseudo-unbreakable blocks (hardness = -1)", + "useless_mod.configuration.beef_tool_entity_interaction_range": "Beef Tool Entity Reach", + "useless_mod.configuration.beef_tool_entity_interaction_range.comment": "Entity interaction range bonus for the beef tool, requires game restart", + "useless_mod.configuration.beef_tool_entity_interaction_range.tooltip": "Entity interaction range bonus for the beef tool, requires game restart", + "useless_mod.configuration.beef_tool_block_interaction_range": "Beef Tool Block Reach", + "useless_mod.configuration.beef_tool_block_interaction_range.comment": "Block interaction range bonus for the beef tool, requires game restart", + "useless_mod.configuration.beef_tool_block_interaction_range.tooltip": "Block interaction range bonus for the beef tool, requires game restart", + "useless_mod.configuration.beef_short_teleport_range": "Beef Tool Short Teleport Range", + "useless_mod.configuration.beef_short_teleport_range.comment": "Maximum distance in blocks for the beef tool's short teleport; this is an upper bound rather than a guaranteed distance, and the destination moves closer when the ray hits a block or the landing spot has no room to stand", + "useless_mod.configuration.beef_short_teleport_range.tooltip": "Maximum distance in blocks for the beef tool's short teleport", + "useless_mod.configuration.beef_construction_wand_build_limit": "Construction Wand Build Limit", + "useless_mod.configuration.beef_construction_wand_angel_limit": "Construction Wand Angel Limit", + "useless_mod.configuration.beef_construction_wand_destruction_limit": "Construction Wand Destruction Limit", + "useless_mod.configuration.beef_tool_force_mining_blacklist": "Beef Tool Force Mining Blacklist", + "useless_mod.configuration.beef_tool_force_mining_blacklist.comment": "Blocks that cannot be force-mined by the beef tool; supports block IDs, #block tags, and * wildcards", + "useless_mod.configuration.beef_tool_force_mining_blacklist.tooltip": "Blocks that cannot be force-mined by the beef tool; supports block IDs, #block tags, and * wildcards", + "useless_mod.configuration.beef_tool_force_kill_blacklist": "Beef Tool Force Kill Blacklist", + "useless_mod.configuration.beef_tool_force_kill_blacklist.comment": "Entity IDs that cannot be force-killed by the beef tool, separated by semicolons", + "useless_mod.configuration.beef_tool_force_kill_blacklist.tooltip": "Entity IDs that cannot be force-killed by the beef tool, separated by semicolons", + "useless_mod.configuration.beef_tool_force_kill_non_living_whitelist": "Non-Living Force Kill Whitelist", + "useless_mod.configuration.beef_tool_force_kill_non_living_whitelist.comment": "Non-living entity IDs that can be force-killed by the beef tool, separated by semicolons", + "useless_mod.configuration.beef_tool_force_kill_non_living_whitelist.tooltip": "Non-living entity IDs that can be force-killed by the beef tool, separated by semicolons", + "useless_mod.configuration.beef_capture_strip_keys": "Mob Capture: Extra NBT Keys to Drop", + "useless_mod.configuration.beef_capture_strip_keys.comment": "Extra NBT key paths dropped before writing the spawn egg, format container.childKey; keys not listed are always kept (avoids breaking other modpacks)", + "useless_mod.configuration.beef_capture_strip_keys.tooltip": "Extra NBT key paths dropped before writing the spawn egg, format container.childKey; keys not listed are always kept (avoids breaking other modpacks)", + "useless_mod.configuration.beef_capture_strip_attachment_namespaces": "Mob Capture: Extra Attachment Namespaces to Drop", + "useless_mod.configuration.beef_capture_strip_attachment_namespaces.comment": "Extra data attachment namespaces dropped before writing the spawn egg; everything else (including unknown mods) is kept (avoids breaking other modpacks)", + "useless_mod.configuration.beef_capture_strip_attachment_namespaces.tooltip": "Extra data attachment namespaces dropped before writing the spawn egg; everything else (including unknown mods) is kept (avoids breaking other modpacks)", + "useless_mod.configuration.festive_drop_chance": "Festive Drop Chance", + "useless_mod.configuration.festive_drop_chance.tooltip": "Chance to trigger festive loot drops, as a percentage from 1 to 100%", + "useless_mod.configuration.enable_grass_wand_drop": "Enable Grass Wand Drop Easter Egg", + "useless_mod.configuration.enable_grass_wand_drop.comment": "Whether breaking grass can trigger the wand drop easter egg", + "useless_mod.configuration.enable_grass_wand_drop.tooltip": "Whether breaking grass can trigger the wand drop easter egg", + "useless_mod.configuration.grass_wand_drop_probability": "Grass Wand Drop Probability", + "useless_mod.configuration.grass_wand_drop_probability.comment": "Probability of a wand dropping from grass; 0.00001 is one in one hundred thousand", + "useless_mod.configuration.grass_wand_drop_probability.tooltip": "Probability from 0 to 1; 0.00001 is one in one hundred thousand", + "useless_mod.configuration.custom_potion_effects": "Custom Potion Effects", + "useless_mod.configuration.custom_potion_effects.comment": "One effect per list entry, format: modid:effect_id,amplifier. Amplifier starts at 1; missing effects and invalid levels are skipped.", + "useless_mod.configuration.custom_potion_effects.tooltip": "One effect per entry: modid:effect_id,amplifier. The duration is fixed at 20000 ticks.", + "useless_mod.configuration.mekanism_upgrade": "Mekanism Upgrade Settings", + "useless_mod.configuration.time_multiplier": "Speed Upgrade Multiplier", + "useless_mod.configuration.time_multiplier.comment": "Speed upgrade multiplier applied every 8 upgrades", + "useless_mod.configuration.electricity_multiplier": "Energy Upgrade Efficiency Multiplier", + "useless_mod.configuration.electricity_multiplier.comment": "Energy efficiency multiplier applied every 8 upgrades", + "useless_mod.configuration.capacity_multiplier": "Energy Upgrade Capacity Multiplier", + "useless_mod.configuration.capacity_multiplier.comment": "Energy capacity multiplier applied every 8 upgrades", + "useless_mod.configuration.max_upgrade": "Max Upgrade Count", + "useless_mod.configuration.max_upgrade.comment": "Maximum number of speed/energy upgrades a machine can accept, requires game restart", + "useless_mod.configuration.omniversal_multiblock_alloy_furnace": "Omniversal Multiblock Alloy Furnace", + "useless_mod.configuration.omniversal_pattern_upload": "Omniversal Pattern Auto Upload", + "useless_mod.configuration.omniversal_pattern_auto_upload": "Enable Auto Upload", + "useless_mod.configuration.omniversal_pattern_auto_upload.comment": "Whether a freshly encoded omniversal pattern is uploaded to a multiblock pattern assembly on the same AE grid. When disabled, the pattern stays in the encoding terminal for the player to move manually, matching the behavior before auto upload existed.", + "useless_mod.configuration.omniversal_pattern_auto_upload.tooltip": "Whether a freshly encoded omniversal pattern is uploaded to a multiblock pattern assembly on the same AE grid. When disabled, the pattern stays in the encoding terminal for the player to move manually, matching the behavior before auto upload existed.", + "useless_mod.configuration.ore_generator": "Ore Generator", + "useless_mod.configuration.pattern_slots": "ME Pattern Assembly Slots", + "useless_mod.configuration.pattern_slots.comment": "Number of active slots in the ME pattern assembly. Values are rounded down to complete pages of 27; contents above a reduced limit remain available on withdraw-only recovery pages.", + "useless_mod.configuration.pattern_slots.tooltip": "Number of active slots in the ME pattern assembly. Values are rounded down to complete pages of 27; contents above a reduced limit remain available on withdraw-only recovery pages.", + "useless_mod.configuration.mold_slots": "Omniversal Mold Hub Slots", + "useless_mod.configuration.mold_slots.comment": "Number of active slots in the omniversal mold hub. Values are rounded down to complete pages of 27; contents above a reduced limit remain available on withdraw-only recovery pages.", + "useless_mod.configuration.mold_slots.tooltip": "Number of active slots in the omniversal mold hub. Values are rounded down to complete pages of 27; contents above a reduced limit remain available on withdraw-only recovery pages.", + "useless_mod.configuration.passive_pattern_slots": "Passive Crafting Hatch Slots", + "useless_mod.configuration.passive_pattern_slots.comment": "Maximum passive-pattern slots. Tier 10 coils unlock all configured slots; lower tiers unlock ceil(slots x tier / 10). Patterns above a reduced limit remain available on withdraw-only recovery slots.", + "useless_mod.configuration.passive_pattern_slots.tooltip": "Maximum passive-pattern slots. Tier 10 coils unlock all configured slots; lower tiers unlock ceil(slots x tier / 10). Patterns above a reduced limit remain available on withdraw-only recovery slots.", + "useless_mod.configuration.decode_cache_capacity": "Omniversal Decode Cache Capacity", + "useless_mod.configuration.decode_cache_capacity.comment": "Maximum decoded omniversal pattern results kept per level. Takes effect after restart; valid range is 64 to 16384.", + "useless_mod.configuration.decode_cache_capacity.tooltip": "Maximum decoded omniversal pattern results kept per level. Takes effect after restart; valid range is 64 to 16384.", + "useless_mod.configuration.advanced_alloy_furnace": "Advanced Alloy Furnace Settings", + "useless_mod.configuration.advanced_alloy_furnace.recipe_tier_rules": "Alloy Furnace Recipe Tier Rules", + "useless_mod.configuration.advanced_alloy_furnace.recipe_tier_rules.comment": "One entry per line: recipe ID pattern,tier. Supports * wildcards; tiers range from 0 to 10; unmatched recipes are unrestricted.", + "useless_mod.configuration.advanced_alloy_furnace.recipe_tier_rules.tooltip": "Exact IDs take priority over wildcards; when specificity is equal, the later rule overrides the earlier rule.", + "useless_mod.configuration.advanced_alloy_furnace.recipe_conversion": "Recipe Conversion", + "useless_mod.configuration.advanced_alloy_furnace.recipe_conversion.minecraft": "Minecraft", + "useless_mod.configuration.advanced_alloy_furnace.recipe_conversion.farmers_delight": "Farmer's Delight and Addons", + "useless_mod.configuration.advanced_alloy_furnace.recipe_conversion.kaleidoscope": "Kaleidoscope", + "useless_mod.configuration.advanced_alloy_furnace.recipe_conversion.ae": "Applied Energistics and Addons", + "useless_mod.configuration.advanced_alloy_furnace.recipe_conversion.mekanism": "Mekanism and Addons", + "useless_mod.configuration.advanced_alloy_furnace.recipe_conversion.justdirethings": "Just Dire Things and Addons", + "useless_mod.configuration.advanced_alloy_furnace.recipe_conversion.other": "Other Mods", + "useless_mod.configuration.draw_appflux_energy": "Draw AppliedFlux Network Energy", + "useless_mod.configuration.draw_appflux_energy.comment": "Whether the Advanced Alloy Furnace automatically draws FE stored by AppliedFlux from its AE network. Requires AppliedFlux and flux cells in the network; per-tick amount is limited by the furnace's max input rate", + "useless_mod.configuration.draw_appflux_energy.tooltip": "Whether the Advanced Alloy Furnace automatically draws FE stored by AppliedFlux from its AE network. Requires AppliedFlux and flux cells in the network; per-tick amount is limited by the furnace's max input rate", + "useless_mod.configuration.draw_ae_energy": "Draw AE Network Energy", + "useless_mod.configuration.draw_ae_energy.comment": "Whether the Advanced Alloy Furnace draws the AE network's own energy (converted at 1 AE = 2 FE). Warning: competes with other devices for power and may cause devices to go offline when network storage is low. Applied after AppliedFlux extraction as a supplement", + "useless_mod.configuration.draw_ae_energy.tooltip": "Whether the Advanced Alloy Furnace draws the AE network's own energy (converted at 1 AE = 2 FE). Warning: competes with other devices for power and may cause devices to go offline when network storage is low. Applied after AppliedFlux extraction as a supplement", + "useless_mod.configuration.ae_batch_ripe_ticks": "AE Batch Ripening Wait (ticks)", + "useless_mod.configuration.ae_batch_ripe_ticks.comment": "How many ticks the Advanced Alloy Furnace waits after receiving a pushed AE batch before starting it. This window merges consecutive pushes into a single task; the task shows as \"Queued\" while waiting. Set to 0 to start immediately; the window is always skipped when the machine is completely idle (no running task)", + "useless_mod.configuration.ae_batch_ripe_ticks.tooltip": "How many ticks the Advanced Alloy Furnace waits after receiving a pushed AE batch before starting it. This window merges consecutive pushes into a single task; the task shows as \"Queued\" while waiting. Set to 0 to start immediately; the window is always skipped when the machine is completely idle (no running task)", + "useless_mod.configuration.ae_output_return_budget_millis": "Output Return Time Budget (ms/tick)", + "useless_mod.configuration.ae_output_return_budget_millis.comment": "How many milliseconds per tick the Advanced Alloy Furnace may spend writing outputs back into the ME network. This directly sets the sustainable crafting speed: AE2 storage accepts only one long chunk per call, so outputs must be inserted chunk by chunk and the per-tick insert count is what limits throughput. Higher = faster crafting but a heavier tick (may cost TPS); lower = cheaper tick but slower crafting. The budget is further scaled down by the global throttle when the machine or server is overloaded (floor 250 microseconds)", + "useless_mod.configuration.ae_output_return_budget_millis.tooltip": "How many milliseconds per tick the Advanced Alloy Furnace may spend writing outputs back into the ME network. This directly sets the sustainable crafting speed: AE2 storage accepts only one long chunk per call, so outputs must be inserted chunk by chunk and the per-tick insert count is what limits throughput. Higher = faster crafting but a heavier tick (may cost TPS); lower = cheaper tick but slower crafting. The budget is further scaled down by the global throttle when the machine or server is overloaded (floor 250 microseconds)", + "useless_mod.configuration.ae_unlimited_bigint_parallelism": "Unlimited Batch Parallelism", + "useless_mod.configuration.ae_unlimited_bigint_parallelism.comment": "When enabled, removes the material-window cap on a single batch (the threads x Long.MAX gate). Batch size is then decided solely by output delivery capacity, so one dispatch can absorb arbitrarily large counts without stacking thread counts. Trade-off: a single admission may take an extremely large amount of materials (deducted by the caller's own BigInteger ledger); low-tier coils then fall back to the energy gate instead, while the useful coil (no energy gate) is unaffected", + "useless_mod.configuration.ae_unlimited_bigint_parallelism.tooltip": "When enabled, removes the material-window cap on a single batch (the threads x Long.MAX gate). Batch size is then decided solely by output delivery capacity, so one dispatch can absorb arbitrarily large counts without stacking thread counts. Trade-off: a single admission may take an extremely large amount of materials (deducted by the caller's own BigInteger ledger); low-tier coils then fall back to the energy gate instead, while the useful coil (no energy gate) is unaffected", + "useless_mod.configuration.stafflink": "Wireless Logistics", + "useless_mod.configuration.stafflink_idle_backoff_enabled": "Idle Exponential Backoff", + "useless_mod.configuration.stafflink_idle_backoff_enabled.comment": "Wireless Logistics: whether an idle route uses exponential backoff. Enabled (default): when a round moves nothing, the next interval doubles each time up to \"interval x backoff cap\", and a single successful move resets it immediately. This keeps large numbers of empty channels from running a full pass every tick. Disabled: routes always run on their configured interval regardless of movement, which is the most responsive but keeps idle channels costing time", + "useless_mod.configuration.stafflink_idle_backoff_enabled.tooltip": "Wireless Logistics: whether an idle route uses exponential backoff", + "useless_mod.configuration.stafflink_idle_backoff_cap": "Idle Backoff Multiplier Cap", + "useless_mod.configuration.stafflink_idle_backoff_cap.comment": "Wireless Logistics: cap on the idle backoff multiplier (next interval grows to at most interval x this value). Prefer powers of two (1/2/4/8/16/32/64); other values are rounded down. 1 means no growth (same as disabling backoff). The default 32 caps at roughly 32 minutes. Only takes effect when idle exponential backoff is enabled", + "useless_mod.configuration.stafflink_idle_backoff_cap.tooltip": "Wireless Logistics: cap on the idle backoff multiplier", + "useless_mod.configuration.enable_crafting_recipe_conversion": "Enable Crafting Recipe Conversion", + "useless_mod.configuration.enable_crafting_recipe_conversion.comment": "Whether to enable conversion of crafting-table recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_crafting_recipe_conversion.tooltip": "Whether to enable conversion of crafting-table recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_smelting_recipe_conversion": "Enable Smelting Recipe Conversion", + "useless_mod.configuration.enable_smelting_recipe_conversion.comment": "Whether to enable conversion of vanilla furnace recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_smelting_recipe_conversion.tooltip": "Whether to enable conversion of vanilla furnace recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_brewing_recipe_conversion": "Enable Brewing Recipe Conversion", + "useless_mod.configuration.enable_brewing_recipe_conversion.comment": "Whether to enable conversion of brewing-stand recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_brewing_recipe_conversion.tooltip": "Whether to enable conversion of brewing-stand recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_fishing_recipe_conversion": "Enable Fishing Recipe Conversion", + "useless_mod.configuration.enable_fishing_recipe_conversion.comment": "Whether to enable conversion of the vanilla fishing loot table (read dynamically, excluding enchanted books and equipment). Takes effect after restarting the game.", + "useless_mod.configuration.enable_fishing_recipe_conversion.tooltip": "Whether to enable conversion of the vanilla fishing loot table (read dynamically, excluding enchanted books and equipment). Takes effect after restarting the game.", + "useless_mod.configuration.enable_villager_trade_recipe_conversion": "Enable Villager Trade Recipe Conversion", + "useless_mod.configuration.enable_villager_trade_recipe_conversion.comment": "Whether to enable conversion of vanilla villager trades. The lowest base emerald cost is used. Takes effect after restarting the game.", + "useless_mod.configuration.enable_villager_trade_recipe_conversion.tooltip": "Whether to enable conversion of vanilla villager trades. The lowest base emerald cost is used. Takes effect after restarting the game.", + "useless_mod.configuration.enable_extendedae_recipe_conversion": "Enable ExtendedAE Recipe Conversion", + "useless_mod.configuration.enable_extendedae_recipe_conversion.comment": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_extendedae_recipe_conversion.tooltip": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_advanced_ae_recipe_conversion": "Enable Advanced AE Recipe Conversion", + "useless_mod.configuration.enable_advanced_ae_recipe_conversion.comment": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_advanced_ae_recipe_conversion.tooltip": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_mekanism_recipe_conversion": "Enable Mekanism Recipe Conversion", + "useless_mod.configuration.enable_mekanism_recipe_conversion.comment": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_mekanism_recipe_conversion.tooltip": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_mekanism_generators_recipe_conversion": "Enable Mekanism Generators Recipe Conversion", + "useless_mod.configuration.enable_mekanism_generators_recipe_conversion.comment": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_mekanism_generators_recipe_conversion.tooltip": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_appmek_recipe_conversion": "Enable AppMek Recipe Conversion", + "useless_mod.configuration.enable_appmek_recipe_conversion.comment": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_appmek_recipe_conversion.tooltip": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_ae2_recipe_conversion": "Enable AE2 Recipe Conversion", + "useless_mod.configuration.enable_ae2_recipe_conversion.comment": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_ae2_recipe_conversion.tooltip": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_ae2cs_recipe_conversion": "Enable AE2 Crystal Science Recipe Conversion", + "useless_mod.configuration.enable_ae2cs_recipe_conversion.comment": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_ae2cs_recipe_conversion.tooltip": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_industrial_foregoing_recipe_conversion": "Enable Industrial Foregoing Recipe Conversion", + "useless_mod.configuration.enable_industrial_foregoing_recipe_conversion.comment": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_industrial_foregoing_recipe_conversion.tooltip": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_actually_additions_recipe_conversion": "Enable Actually Additions Recipe Conversion", + "useless_mod.configuration.enable_actually_additions_recipe_conversion.comment": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_actually_additions_recipe_conversion.tooltip": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_ars_nouveau_recipe_conversion": "Enable Ars Nouveau Recipe Conversion", + "useless_mod.configuration.enable_ars_nouveau_recipe_conversion.comment": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_ars_nouveau_recipe_conversion.tooltip": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_mystical_agriculture_recipe_conversion": "Enable Mystical Agriculture Recipe Conversion", + "useless_mod.configuration.enable_mystical_agriculture_recipe_conversion.comment": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_mystical_agriculture_recipe_conversion.tooltip": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_ae2lt_recipe_conversion": "Enable AE2 Lightning Tech Recipe Conversion", + "useless_mod.configuration.enable_ae2lt_recipe_conversion.comment": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_ae2lt_recipe_conversion.tooltip": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_data_energistics_recipe_conversion": "Enable Data Energistics Recipe Conversion", + "useless_mod.configuration.enable_data_energistics_recipe_conversion.comment": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_data_energistics_recipe_conversion.tooltip": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_productive_bees_recipe_conversion": "Enable Productive Bees Recipe Conversion", + "useless_mod.configuration.enable_productive_bees_recipe_conversion.comment": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_productive_bees_recipe_conversion.tooltip": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_draconic_evolution_recipe_conversion": "Enable Draconic Evolution Recipe Conversion", + "useless_mod.configuration.enable_draconic_evolution_recipe_conversion.comment": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_draconic_evolution_recipe_conversion.tooltip": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_powah_recipe_conversion": "Enable Powah Recipe Conversion", + "useless_mod.configuration.enable_powah_recipe_conversion.comment": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_powah_recipe_conversion.tooltip": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_extended_crafting_recipe_conversion": "Enable Extended Crafting Recipe Conversion", + "useless_mod.configuration.enable_extended_crafting_recipe_conversion.comment": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_extended_crafting_recipe_conversion.tooltip": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_avaritia_recipe_conversion": "Enable Re-Avaritia Recipe Conversion", + "useless_mod.configuration.enable_avaritia_recipe_conversion.comment": "Whether to enable conversion of Re-Avaritia crafting, extreme smithing, neutron compressor, and neutron collector recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_avaritia_recipe_conversion.tooltip": "Whether to enable conversion of Re-Avaritia crafting, extreme smithing, neutron compressor, and neutron collector recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_neo_eco_ae_recipe_conversion": "Enable NeoECOAE Recipe Conversion", + "useless_mod.configuration.enable_neo_eco_ae_recipe_conversion.comment": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_neo_eco_ae_recipe_conversion.tooltip": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_natures_aura_recipe_conversion": "Enable Nature's Aura Recipe Conversion", + "useless_mod.configuration.enable_natures_aura_recipe_conversion.comment": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_natures_aura_recipe_conversion.tooltip": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_forbidden_arcanus_recipe_conversion": "Enable Forbidden Arcanus Recipe Conversion", + "useless_mod.configuration.enable_forbidden_arcanus_recipe_conversion.comment": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_forbidden_arcanus_recipe_conversion.tooltip": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_occultism_recipe_conversion": "Enable Occultism Recipe Conversion", + "useless_mod.configuration.enable_occultism_recipe_conversion.comment": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_occultism_recipe_conversion.tooltip": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_summoningrituals_recipe_conversion": "Enable Summoning Rituals Recipe Conversion", + "useless_mod.configuration.enable_summoningrituals_recipe_conversion.comment": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_summoningrituals_recipe_conversion.tooltip": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_malum_recipe_conversion": "Enable Malum Recipe Conversion", + "useless_mod.configuration.enable_malum_recipe_conversion.comment": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_malum_recipe_conversion.tooltip": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_ender_io_recipe_conversion": "Enable Ender IO Recipe Conversion", + "useless_mod.configuration.enable_ender_io_recipe_conversion.comment": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_ender_io_recipe_conversion.tooltip": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_create_recipe_conversion": "Enable Create Recipe Conversion", + "useless_mod.configuration.enable_create_recipe_conversion.comment": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_create_recipe_conversion.tooltip": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_astralsorcery_recipe_conversion": "Enable Astral Sorcery Recipe Conversion", + "useless_mod.configuration.enable_astralsorcery_recipe_conversion.comment": "Whether to enable conversion of Astral Sorcery infusion, lightwell and altar-crafting recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_astralsorcery_recipe_conversion.tooltip": "Whether to enable conversion of Astral Sorcery infusion, lightwell and altar-crafting recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_oritech_recipe_conversion": "Enable Oritech Recipe Conversion", + "useless_mod.configuration.enable_oritech_recipe_conversion.comment": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_oritech_recipe_conversion.tooltip": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_neovitae_recipe_conversion": "Enable Neo Vitae Recipe Conversion", + "useless_mod.configuration.enable_neovitae_recipe_conversion.comment": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_neovitae_recipe_conversion.tooltip": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_ufo_recipe_conversion": "Enable UFO Future Recipe Conversion", + "useless_mod.configuration.enable_ufo_recipe_conversion.comment": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_ufo_recipe_conversion.tooltip": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_modern_industrialization_recipe_conversion": "Enable Modern Industrialization Recipe Conversion", + "useless_mod.configuration.enable_modern_industrialization_recipe_conversion.comment": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_modern_industrialization_recipe_conversion.tooltip": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_immersiveengineering_recipe_conversion": "Enable Immersive Engineering Recipe Conversion", + "useless_mod.configuration.enable_immersiveengineering_recipe_conversion.comment": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_immersiveengineering_recipe_conversion.tooltip": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_pneumaticcraft_recipe_conversion": "Enable PneumaticCraft Recipe Conversion", + "useless_mod.configuration.enable_pneumaticcraft_recipe_conversion.comment": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_pneumaticcraft_recipe_conversion.tooltip": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_bigreactors_recipe_conversion": "Enable Extreme Reactors Recipe Conversion", + "useless_mod.configuration.enable_bigreactors_recipe_conversion.comment": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_bigreactors_recipe_conversion.tooltip": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_irons_spellbooks_recipe_conversion": "Enable Iron's Spells 'n Spellbooks Recipe Conversion", + "useless_mod.configuration.enable_irons_spellbooks_recipe_conversion.comment": "Whether to enable conversion of Iron's Spells 'n Spellbooks alchemist cauldron recipes. Fluids are converted to their item equivalents.", + "useless_mod.configuration.enable_irons_spellbooks_recipe_conversion.tooltip": "Whether to enable conversion of Iron's Spells 'n Spellbooks alchemist cauldron recipes. Fluids are converted to their item equivalents.", + "useless_mod.configuration.enable_hostilenetworks_recipe_conversion": "Enable Hostile Neural Networks Recipe Conversion", + "useless_mod.configuration.enable_hostilenetworks_recipe_conversion.comment": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_hostilenetworks_recipe_conversion.tooltip": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_apothic_flux_recipe_conversion": "Enable Apothic Flux Recipe Conversion", + "useless_mod.configuration.enable_apothic_flux_recipe_conversion.comment": "Whether to enable conversion of Apothic Flux ritual-crafting recipes and Apothic Enchanting infusion recipes using Flux bookshelves. Takes effect after restarting the game.", + "useless_mod.configuration.enable_apothic_flux_recipe_conversion.tooltip": "Whether to enable conversion of Apothic Flux ritual-crafting recipes and Apothic Enchanting infusion recipes using Flux bookshelves. Takes effect after restarting the game.", + "useless_mod.configuration.enable_justdirethings_recipe_conversion": "Enable Just Dire Things Recipe Conversion", + "useless_mod.configuration.enable_justdirethings_recipe_conversion.comment": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_justdirethings_recipe_conversion.tooltip": "Whether to enable conversion of recipes from this source. Takes effect after restarting the game.", + "useless_mod.configuration.enable_jdte_recipe_conversion": "Enable Just Dire Things Extra Recipe Conversion", + "useless_mod.configuration.enable_jdte_recipe_conversion.comment": "Whether to enable conversion of Just Dire Things Extra recipes. Disabling Just Dire Things conversion also disables this addon. Takes effect after restarting the game.", + "useless_mod.configuration.enable_jdte_recipe_conversion.tooltip": "Whether to enable conversion of Just Dire Things Extra recipes. Disabling Just Dire Things conversion also disables this addon. Takes effect after restarting the game.", + "useless_mod.configuration.enable_exdeorum_recipe_conversion": "Enable Ex Deorum Recipe Conversion", + "useless_mod.configuration.enable_exdeorum_recipe_conversion.comment": "Whether to enable conversion of Ex Deorum barrel composting, barrel item-fluid mixing, fluid transformation, and crucible melting recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_exdeorum_recipe_conversion.tooltip": "Whether to enable conversion of Ex Deorum barrel composting, barrel item-fluid mixing, fluid transformation, and crucible melting recipes. Takes effect after restarting the game.", + "useless_mod.configuration.enable_botany_pots_recipe_conversion": "Enable Botany Pots Recipe Conversion", + "useless_mod.configuration.enable_botany_pots_recipe_conversion.comment": "Whether to enable conversion of Botany Pots crop-growing recipes: water is the fluid input and the molds are a botany pot plus the corresponding crop. Takes effect after restarting the game.", + "useless_mod.configuration.enable_botany_pots_recipe_conversion.tooltip": "Whether to enable conversion of Botany Pots crop-growing recipes: water is the fluid input and the molds are a botany pot plus the corresponding crop. Takes effect after restarting the game.", + "useless_mod.configuration.beef_autoclick_clicks_per_tick": "Auto Click Rate", + "useless_mod.configuration.beef_autoclick_clicks_per_tick.comment": "How many right-clicks the staff's Auto Click mode triggers per client tick; higher = faster (4 is roughly 80 clicks per second), lower = less server load", + "useless_mod.configuration.beef_autoclick_clicks_per_tick.tooltip": "How many right-clicks the staff's Auto Click mode triggers per client tick; higher = faster (4 is roughly 80 clicks per second), lower = less server load", + "key.category.useless_mod.useless": "Useless Mod", + "key.useless_mod.switch_silk_touch": "Switch to Silk Touch", + "key.useless_mod.switch_fortune": "Switch to Fortune", + "key.useless_mod.trigger_chain_mining": "Trigger Chain Mining", + "key.useless_mod.toggle_chain_mode": "Toggle Chain Mode", + "key.useless_mod.force_mining": "Force Mining Toggle", + "key.useless_mod.switch_mode_wheel": "Open Mode Selection Wheel", + "key.useless_mod.trigger_force_mining": "Trigger Force Mining", + "key.useless_mod.switch_farmland_mode": "Toggle Farmland / Path Mode", + "key.useless_mod.toggle_crop_harvest": "Toggle Quick Harvest", + "key.useless_mod.toggle_shears": "Toggle Shears", + "key.useless_mod.toggle_flint_and_steel": "Toggle Flint and Steel", + "key.useless_mod.short_teleport": "Short-Range Teleport", + "key.useless_mod.toggle_auto_click": "Toggle Auto Click", + "key.useless_mod.toggle_kill_aura": "Toggle Kill Aura", + "tooltip.useless_mod.fast_break_plastic": "Shift + Right Click to quickly break plastic blocks", + "tooltip.useless_mod.ae_storage_priority_bind_hint": "§aShift+Right-click§rME Wireless Access Point to bind", + "tooltip.useless_mod.ae_network_connect_hint": "In §9AE Connection Mode§r, right-click a machine with an AE node to toggle its link; linked machines are outlined and ignore channel limits", + "tooltip.useless_mod.festive_affix": "5% chance to trigger loot explosion on kill", + "tooltip.useless_mod.auto_collect": "Built-in magnet function", + "tooltip.useless_mod.time_acceleration_mode": "Time Acceleration", + "tooltip.useless_mod.time_acceleration_hint": "Hold Shift and right-click to accelerate ticking blocks and machines when enabled; use the separate Entity Acceleration mode for mobs", + "tooltip.useless_mod.beef_teleport_mode": "Teleport", + "tooltip.useless_mod.beef_aoe_damage_mode": "AoE Damage", + "tooltip.useless_mod.beef_magnet_mode": "Ranged Magnet", + "tooltip.useless_mod.beef_kill_aura_mode": "Kill Aura", + "tooltip.useless_mod.beef_kill_aura_hint": "While holding the staff, deals attack damage to creatures within the AoE Damage range every 20 ticks; hits are registered for Ranged Magnet pickup", + "tooltip.useless_mod.beef_protect_mode": "Protection List", + "tooltip.useless_mod.beef_protect_hint": "Ctrl+right-click a creature to toggle protection for its type; Shift+right-click for that single entity (requires the staff with Kill Aura or AoE Damage on). Protected creatures are skipped by the aura, AoE damage and force kill; your own pets and mounts are always protected", + "tooltip.useless_mod.beef_protect_count": "Protected: %s type(s) / %s entity(ies)", + "tooltip.useless_mod.beef_protect_toggle_mode": "Protection List Mode", + "tooltip.useless_mod.beef_entity_time_acceleration_mode": "Entity Acceleration", + "tooltip.useless_mod.beef_entity_time_acceleration_hint": "Sneak + right-click a mob to accelerate its timers: baby growth / post-breeding cooldown, chicken egg laying, sheep wool regrowth, villager restocking, bee nectar and turtle egg laying; with Ars Nouveau installed it also speeds up the Drygmy / Alakarkinos / Whirlisprig work rate. Only counters advance, movement and AI are untouched. Mutually exclusive with the Protection List Shift gesture (Ctrl by-type still works)", + "tooltip.useless_mod.beef_protect_toggle_hint": "While Protection List Mode is on, the Kill Aura and AoE Damage stand down so you can add entries at leisure; remember to turn it off", + "tooltip.useless_mod.beef_farmland_mode": "Right-Click", + "tooltip.useless_mod.beef_farmland_state.hoe": "Hoe Priority", + "tooltip.useless_mod.beef_farmland_state.shovel": "Shovel Priority", + "tooltip.useless_mod.beef_farmland_hint": "Which tool wins when right-clicking dirt / grass: hoe priority makes farmland, shovel priority makes a dirt path", + "tooltip.useless_mod.beef_crop_harvest_mode": "Quick Harvest", + "tooltip.useless_mod.beef_crop_harvest_hint": "Right-click a mature crop to harvest it and leave the seed in the ground; sneak + right-click hands the interaction back to other mods", + "tooltip.useless_mod.beef_shears_mode": "Shears", + "tooltip.useless_mod.beef_shears_hint": "Right-click sheep, mooshrooms and other shearable entities to shear them, and expose shears abilities to other mods", + "tooltip.useless_mod.beef_flint_and_steel_mode": "Flint and Steel", + "tooltip.useless_mod.beef_flint_and_steel_hint": "Right-click campfires and candles to light them, or any other block to place fire on the clicked face; sneak + right-click hands the interaction back to other mods", + "tooltip.useless_mod.beef_ritual_satchel_mode": "Ritual Satchel", + "tooltip.useless_mod.beef_ritual_satchel_hint": "Right-click a multiblock previewed in the guide book to build the whole ritual, using blocks from your inventory first and the bound AE network when needed", + "gui.useless_mod.ritual_satchel.unknown_multiblock": "This is not a recognizable ritual structure", + "gui.useless_mod.ritual_satchel.no_ae_items": "The bound AE network has no usable blocks", + "gui.useless_mod.ritual_satchel.no_matching_item": "Neither your inventory nor the AE network has the blocks this ritual needs", + "tooltip.useless_mod.beef_ripen_mode": "Ripening", + "tooltip.useless_mod.beef_ripen_hint": "Right-click to instantly ripen: crops, saplings and fungi grow to full maturity in one click, pumpkin and melon stems bear their fruit, and baby animals become adults; sugar cane / cactus / bamboo need the separate Force Growth mode; hold the chain key (Tab) to affect a whole field; sneak + right-click hands the interaction back to other mods; vines spread forcibly (ignoring the doVinesSpread game rule and the 5-vine cap) and flowers yield their drops (routed through AE storage priority / range magnet) while the flower itself stays", + "tooltip.useless_mod.beef_force_grow_mode": "Force Growth", + "tooltip.useless_mod.beef_force_grow_hint": "Ripening additionally advances blocks by random ticks, which covers sugar cane, cactus, bamboo and anything else bone meal cannot affect; but it does not discriminate - leaf decay, fire spread, melting snow and ice, and farmland turning to dirt get advanced too, so decide for yourself whether to enable it", + "tooltip.useless_mod.beef_auto_click_mode": "Auto Click", + "tooltip.useless_mod.beef_auto_click_hint": "While holding the staff, right-click is triggered repeatedly as fast as possible (rate is set in the client config); press the bound key again to stop. Beware of combining it with Flint and Steel", + "tooltip.useless_mod.beef_teleport_hint": "Hold Shift and right-click to blink forward; with Ender IO, right-click to travel to an anchor", + "gui.useless_mod.beef_protect.add_type": "Protected creature type: %s", + "gui.useless_mod.beef_protect.remove_type": "Unprotected creature type: %s", + "gui.useless_mod.beef_protect.add_entity": "Protected creature: %s", + "gui.useless_mod.beef_protect.remove_entity": "Unprotected creature: %s", + "subtitles.useless_mod.kill_aura_on": "Kill Aura: On", + "subtitles.useless_mod.kill_aura_off": "Kill Aura: Off", + "tooltip.useless_mod.current_tool_mode": "Current Tool Mode", + "tooltip.useless_mod.none_mode": "None", + "tooltip.useless_mod.wrench_mode": "Wrench", + "tooltip.useless_mod.screwdriver_mode": "Screwdriver", + "tooltip.useless_mod.mallet_mode": "Mallet", + "tooltip.useless_mod.crowbar_mode": "Crowbar", + "tooltip.useless_mod.hammer_mode": "Hammer", + "tooltip.useless_mod.omnitool_mode": "Omnitool", + "tooltip.useless_mod.disable": "Disabled", + "tooltip.useless_mod.enable": "Enabled", + "tooltip.useless_mod.force_mining_mode": "Force Mining", + "tooltip.useless_mod.ae_storage_priority_mode": "AE Storage Priority", + "tooltip.useless_mod.ae_network_connect_mode": "AE Connection Mode", + "tooltip.useless_mod.wrench_tag_mode": "Wrench Tag", + "tooltip.useless_mod.construction_wand_mode": "Construction Wand", + "tooltip.useless_mod.construction_wand_default_core": "Construction Core", + "tooltip.useless_mod.construction_wand_angel_core": "Angel Core", + "tooltip.useless_mod.construction_wand_destruction_core": "Destruction Core", + "tooltip.useless_mod.enhanced_chain_mining_mode": "Enhanced Chain Mining", + "tooltip.useless_mod.auto_smelt_mode": "Auto Smelting", + "tooltip.useless_mod.force_kill_enabled_mode": "Force Kill", + "tooltip.useless_mod.beef_malum_spirit_mode": "Spirit", + "tooltip.useless_mod.beef_mystical_agriculture_mode": "Awakening Powder", + "tooltip.useless_mod.beef_beheading_mode": "Beheading", + "tooltip.useless_mod.beef_invulnerability_mode": "Player Protection", + "tooltip.useless_mod.beef_advanced_stealth_mode": "Advanced Stealth", + "tooltip.useless_mod.beef_capture_mode": "Mob Capture", + "tooltip.useless_mod.exdeorum_crook_mode": "Crook", + "tooltip.useless_mod.exdeorum_hammer_mode": "Hammer", + "tooltip.useless_mod.exdeorum_compressed_hammer_mode": "Compressed Hammer", + "tooltip.useless_mod.press_shift_for_keys": "Hold Shift to show key bindings", + "tooltip.useless_mod.tooltip_page": "Page %s/%s Press %s %s to turn pages", + "tooltip.useless_mod.silk_touch_mode": "Silk Touch", + "tooltip.useless_mod.fortune_mode": "Fortune", + "tooltip.useless_mod.key.switch_silk_touch": "§dPress %s to switch to Silk Touch mode", + "tooltip.useless_mod.key.switch_fortune": "§dPress %s to switch to Fortune mode", + "tooltip.useless_mod.key.toggle_chain_mode": "§dPress %s to toggle Chain Mining (Normal ↔ Enhanced)", + "tooltip.useless_mod.key.switch_force_mining": "§dPress %s to toggle Force Mining mode", + "tooltip.useless_mod.key.switch_farmland_mode": "§dPress %s to switch the Right-Click Dirt priority (Hoe / Shovel)", + "tooltip.useless_mod.key.toggle_crop_harvest": "§dPress %s to toggle Quick Harvest", + "tooltip.useless_mod.key.toggle_shears": "§dPress %s to toggle Shears", + "tooltip.useless_mod.key.toggle_flint_and_steel": "§dPress %s to toggle Flint and Steel", + "tooltip.useless_mod.key.toggle_auto_click": "§dPress %s to toggle Auto Click", + "tooltip.useless_mod.key.toggle_kill_aura": "§dPress %s to toggle Kill Aura", + "tooltip.useless_mod.key.trigger_chain_mining": "§cHold %s to temporarily activate Chain Mining", + "tooltip.useless_mod.key.chain_use": "§cHold %s to chain right-click actions too (soil, stripping/scraping/wax off, harvesting, ripening)", + "tooltip.useless_mod.key.trigger_force_mining": "§cHold %s to temporarily activate Force Mining", + "tooltip.useless_mod.key.open_mode_wheel": "§ePress %s to open Mode Selection Wheel", + "tooltip.useless_mod.furnace_tier": "Tier: %s", + "tooltip.useless_mod.furnace_upgrade_hint": "§eSneak + Right-click§r: use §6Useless Ingots§r for tiers 1-9, then a §aUseful Ingot§r for tier 10", + "tooltip.useless_mod.furnace_upgrade_effect": "§7Tier 10: §aenergy capacity and input rate§7 reach the long limit", + "tooltip.useless_mod.ae2_gift_package.description": "All your hard work has finally paid off.", + + "jei.useless_mod.catalyst_info": "Catalyst and Coil Info", + "jei.useless_mod.tooltip.amount": "Amount: %s", + "jei.useless_mod.tooltip.amount.mb": "Amount: %s mB", + "jei.useless_mod.tooltip.catalyst": "Catalyst", + "jei.useless_mod.tooltip.catalyst.useless_ingot.title": "Useless Ingot Recipe Info:", + "jei.useless_mod.tooltip.catalyst.useless_ingot.desc1": "• Crafting Tier N ingot requires Tier N-1 ingot as input", + "jei.useless_mod.tooltip.catalyst.useless_ingot.desc2": "• Parallel count is 2^N (N is target tier)", + "jei.useless_mod.tooltip.catalyst.useless_ingot.desc3": "• Processing time is reduced by catalyst tier", + "jei.useless_mod.tooltip.catalyst.useless_ingot.warning": "• Catalyst not consumed for useless ingot recipes", + "jei.useless_mod.tooltip.catalyst.normal.desc1": "• Useless ingots can increase parallel count", + "jei.useless_mod.tooltip.catalyst.normal.desc2": "• Different tiers of useless ingots provide different parallel counts", + "jei.useless_mod.tooltip.catalyst.normal.warning": "• Catalyst will be consumed", + "jei.useless_mod.tooltip.catalyst.normal.desc3": "• Different tiers provide different parallel counts", + "jei.useless_mod.tooltip.catalyst.normal.speed_bonus": "Processing time: %d0%%", + "jei.useless_mod.tooltip.key_input": "Key Input (AE2 Special Data Type)", + "jei.useless_mod.tooltip.key_output": "Key Output (AE2 Special Data Type)", + "jei.useless_mod.tooltip.energy.base": "Base Energy Cost: %s", + "jei.useless_mod.tooltip.energy.actual": "Actual Energy = Base Energy × Parallel", + "jei.useless_mod.tooltip.energy.note_useful": "Energy does not multiply when using useful ingot as catalyst", + "jei.useless_mod.tooltip.process_time": "Process Time: %s ticks", + "jei.useless_mod.tooltip.process_time.note": "Process time is not affected by parallel", + "jei.useless_mod.tooltip.process_time.catalyst_bonus": "• Tier 1 catalyst: 90%% time", + "jei.useless_mod.tooltip.process_time.catalyst_bonus_2": "• Tier 2 catalyst: 80%% time", + "jei.useless_mod.tooltip.process_time.useful_ingot": "• Useful ingot: fixed at 1 tick", + "jei.useless_mod.tooltip.process_time.coil_bonus": "• Multiblock coils halve processing time per tier", + "jei.useless_mod.tooltip.process_time.coil_useful": "• Multiblock useful coil: fixed at 1 tick", + "jei.useless_mod.tooltip.parallel.title": "Parallel Effect Description", + "jei.useless_mod.tooltip.parallel.desc1": "• Input item consumption × parallel", + "jei.useless_mod.tooltip.parallel.desc2": "• Output item count × parallel", + "jei.useless_mod.tooltip.parallel.desc3": "• Energy consumption × parallel", + "jei.useless_mod.tooltip.parallel.desc4": "• Process time remains unchanged", + "jei.useless_mod.tooltip.parallel.warning": "⚠ Catalyst will be consumed", + "jei.useless_mod.tooltip.catalyst.parallel.unlimited": "Parallel: Unlimited", + "jei.useless_mod.tooltip.catalyst.effect.title": "Effect Description:", + "jei.useless_mod.tooltip.catalyst.effect.not_consumed": "• Catalyst will not be consumed", + "jei.useless_mod.tooltip.catalyst.effect.unlimited": "• No parallel limit", + "jei.useless_mod.tooltip.catalyst.effect.no_energy_mult": "• Energy consumption will not multiply", + "jei.useless_mod.tooltip.catalyst.effect.time_unchanged": "• Process time remains unchanged", + "jei.useless_mod.tooltip.catalyst.effect.useless_warning": "Note: Catalyst is invalid when crafting useless ingots", + "jei.useless_mod.tooltip.catalyst.normal.parallel": "Normal Recipe Parallel: %sx", + "jei.useless_mod.tooltip.catalyst.cross_tier.title": "Cross-Tier Synthesis:", + "jei.useless_mod.tooltip.catalyst.cross_tier.format": "• Craft Tier %s Useless Ingot: %sx parallel", + "jei.useless_mod.tooltip.recipe_id": "Recipe ID: %s", + "jei.useless_mod.gui.tier_required": "Tier requirement: %s (single furnace / multiblock coil)", + "jei.useless_mod.gui.tier_unrestricted": "No tier requirement", + "jei.useless_mod.catalyst_info.title": "Catalyst and Coil Effects", + "jei.useless_mod.catalyst_info.desc1": "• Catalyst is optional, will be consumed (except useful ingot)", + "jei.useless_mod.catalyst_info.desc2": "• Adding catalyst greatly increases parallel count", + "jei.useless_mod.catalyst_info.desc3": "• Parallel affects consumption, output and energy", + "jei.useless_mod.catalyst_info.desc4": "• Higher tier catalysts work for lower tier useless ingots", + "jei.useless_mod.catalyst_info.desc5": "• Tier 1 uses 90%% time, tier 2 uses 80%% time, and so on", + "jei.useless_mod.catalyst_info.desc6": "• Useful ingot is fixed at 1 tick and does not multiply energy", + "jei.useless_mod.catalyst_info.desc7": "• Multiblock coils: 50%% time remains per tier", + "jei.useless_mod.catalyst_info.desc8": "• Multiblock useful coil: fixed at 1 tick", + + "config.jade.plugin_useless_mod.advanced_alloy_furnace_tasks": "Advanced Alloy Furnace Tasks", + "config.jade.plugin_useless_mod.advanced_alloy_furnace_tasks.comment": "Display AE tasks for Advanced Alloy Furnace", + "item.useless_mod.ritual_blueprint": "Ritual Blueprint", + "item.useless_mod.ritual_blueprint.blank": "Blank: use on a completed ritual pentacle", + "item.useless_mod.ritual_blueprint.pentacle": "Imprinted pentacle: %s", + "item.useless_mod.ritual_blueprint.pentacles": "Imprinted pentacles (%s):", + "item.useless_mod.ritual_blueprint.pentacle_entry": " %s", + "item.useless_mod.ritual_blueprint.imprinted": "Ritual blueprint imprinted for %s", + "item.useless_mod.ritual_blueprint.imprinted_multiple": "Ritual blueprint imprinted with %s pentacles", + "item.useless_mod.ritual_blueprint.no_pentacle": "No completed ritual pentacle found", + "item.useless_mod.ritual_blueprint.ambiguous": "Multiple pentacles found; hold the activation item in the offhand" + ,"container.useless_mod.ore_generator": "Ore Generator" + ,"gui.useless_mod.ore_generator.rate": "Output rate" + ,"gui.useless_mod.ore_generator.rate_unit": "items / slot / 20 ticks" + ,"gui.useless_mod.ore_generator.apply": "Apply" + ,"gui.useless_mod.ore_generator.ae_toggle": "AE output" + ,"gui.useless_mod.ore_generator.ae_online": "AE online" + ,"gui.useless_mod.ore_generator.ae_offline": "AE offline" + ,"gui.useless_mod.ore_generator.ae_enabled": "AE output: enabled" + ,"gui.useless_mod.ore_generator.ae_disabled": "AE output: disabled" + ,"gui.useless_mod.ore_generator.countdown": "Next output: %s ticks" + ,"gui.useless_mod.ore_generator.active_slots": "Active slots: %s / %s" + ,"gui.useless_mod.ore_generator.locked_slot": "Locked sample slot" + ,"useless_mod.configuration.ore_generator_slots": "Ore Generator Sample Slots" + ,"useless_mod.configuration.ore_generator_slots.comment": "Number of active ore generator sample slots. Contents above a reduced limit remain available on recovery pages." + ,"useless_mod.configuration.ore_generator_slots.tooltip": "Number of active ore generator sample slots. Contents above a reduced limit remain available on recovery pages." + ,"useless_mod.configuration.useless_dimension": "Useless Dimension" + ,"useless_mod.configuration.useless_dimension_floor_block_blacklist": "Useless Dimension Floor Block Blacklist" + ,"useless_mod.configuration.useless_dimension_floor_block_blacklist.comment": "Blocks that cannot be used for layers, borders, fills, roads, or center markers. Blacklist entries always win. Supports exact IDs, #block tags, and * wildcard patterns." + ,"useless_mod.configuration.useless_dimension_floor_block_blacklist.tooltip": "Exact IDs, #block tags, or * wildcard patterns for blocks excluded from Useless Dimension layers, borders, roads, and center markers." + ,"useless_mod.configuration.useless_dimension_floor_block_whitelist": "Useless Dimension Floor Block Whitelist" + ,"useless_mod.configuration.useless_dimension_floor_block_whitelist.comment": "When non-empty, only matching blocks can be used for layers, borders, fills, roads, or center markers. Leave empty to disable the whitelist. Supports exact IDs, #block tags, and * wildcard patterns." + ,"useless_mod.configuration.useless_dimension_floor_block_whitelist.tooltip": "When non-empty, only matching blocks can be used for Useless Dimension layers, borders, roads, and center markers; the blacklist still takes priority." + ,"menu.useless_mod.dimension_config": "Useless Dimension Configuration" + ,"gui.useless_mod.dimension_config.border_block": "Border" + ,"gui.useless_mod.dimension_config.fill_block": "Fill" + ,"gui.useless_mod.dimension_config.center_block": "Center" + ,"gui.useless_mod.dimension_config.layers": "Layers" + ,"gui.useless_mod.dimension_config.start_y": "Start Y" + ,"gui.useless_mod.dimension_config.boundary_alternate_1": "Boundary 1" + ,"gui.useless_mod.dimension_config.boundary_alternate_2": "Boundary 2" + ,"gui.useless_mod.dimension_config.road_surface": "Body" + ,"gui.useless_mod.dimension_config.road_edge": "Edge" + ,"gui.useless_mod.dimension_config.road_center_marking": "Center line" + ,"gui.useless_mod.dimension_config.center_marker": "Center marker" + ,"gui.useless_mod.dimension_config.boundary_interval_x": "Boundary interval X" + ,"gui.useless_mod.dimension_config.boundary_interval_z": "Boundary interval Z" + ,"gui.useless_mod.dimension_config.road_width": "Road width" + ,"gui.useless_mod.dimension_config.road_options": "Road options" + ,"gui.useless_mod.dimension_config.center_options": "Center options" + ,"gui.useless_mod.dimension_config.mode": "Mode" + ,"gui.useless_mod.dimension_config.mode.road": "Road" + ,"gui.useless_mod.dimension_config.mode.multi": "Multi" + ,"gui.useless_mod.dimension_config.center_marker_on": "Center marker: on" + ,"gui.useless_mod.dimension_config.center_marker_off": "Center marker: off" + ,"gui.useless_mod.dimension_config.tooltip.decrease": "Decrease value by 1" + ,"gui.useless_mod.dimension_config.tooltip.increase": "Increase value by 1" + ,"gui.useless_mod.dimension_config.tooltip.bedrock": "Toggle generation of the bedrock layer" + ,"gui.useless_mod.dimension_config.tooltip.bottom": "Toggle placing the bedrock layer at the bottom" + ,"gui.useless_mod.dimension_config.tooltip.mode": "Cycle the platform mode: road / multi" + ,"gui.useless_mod.dimension_config.tooltip.center_marker": "Toggle the center marker" + ,"gui.useless_mod.dimension_config.tooltip.apply": "Save this dimension configuration" + ,"gui.useless_mod.dimension_config.tooltip.apply_and_teleport": "Save the configuration and teleport" + ,"gui.useless_mod.dimension_config.tooltip.cancel": "Close without saving" + ,"gui.useless_mod.dimension_config.bedrock_on": "Generate bedrock: on" + ,"gui.useless_mod.dimension_config.bedrock_off": "Generate bedrock: off" + ,"gui.useless_mod.dimension_config.bottom_on": "Bedrock at bottom: on" + ,"gui.useless_mod.dimension_config.bottom_off": "Bedrock at bottom: off" + ,"gui.useless_mod.dimension_config.apply": "Apply" + ,"gui.useless_mod.dimension_config.apply_and_teleport": "Apply + teleport" + ,"gui.useless_mod.dimension_config.cancel": "Cancel" + ,"gui.useless_mod.dimension_config.invalid": "Invalid dimension configuration" + ,"gui.useless_mod.dimension_config.export": "Export" + ,"gui.useless_mod.dimension_config.import": "Import" + ,"gui.useless_mod.dimension_config.tooltip.export": "Copy the current configuration to the clipboard" + ,"gui.useless_mod.dimension_config.tooltip.import": "Import a configuration from the clipboard" + ,"gui.useless_mod.dimension_config.exported": "Configuration copied to the clipboard" + ,"gui.useless_mod.dimension_config.export_invalid": "The configuration is incomplete and cannot be exported" + ,"gui.useless_mod.dimension_config.imported": "Configuration imported; click Apply to commit it" + ,"gui.useless_mod.dimension_config.import_confirm": "Importing will replace the current configuration. Continue?" + ,"gui.useless_mod.dimension_config.confirm": "Confirm" + ,"gui.useless_mod.dimension_config.cancel_import": "Cancel" + ,"gui.useless_mod.dimension_config.error.invalid_text": "The clipboard does not contain valid configuration text" + ,"gui.useless_mod.dimension_config.error.unsupported_version": "Unsupported configuration version" + ,"gui.useless_mod.dimension_config.error.invalid_structure": "The configuration structure is incomplete" + ,"gui.useless_mod.dimension_config.error.blocked_block": "The configuration contains a blocked block" + ,"gui.useless_mod.dimension_config.error.limit": "The configuration text is too long" + ,"useless_mod.configuration.furnace_tier_0_threads": "Single-block Alloy Furnace Tier 0 Thread Count" + ,"useless_mod.configuration.furnace_tier_1_threads": "Single-block Alloy Furnace Tier 1 Thread Count" + ,"useless_mod.configuration.furnace_tier_2_threads": "Single-block Alloy Furnace Tier 2 Thread Count" + ,"useless_mod.configuration.furnace_tier_3_threads": "Single-block Alloy Furnace Tier 3 Thread Count" + ,"useless_mod.configuration.furnace_tier_4_threads": "Single-block Alloy Furnace Tier 4 Thread Count" + ,"useless_mod.configuration.furnace_tier_5_threads": "Single-block Alloy Furnace Tier 5 Thread Count" + ,"useless_mod.configuration.furnace_tier_6_threads": "Single-block Alloy Furnace Tier 6 Thread Count" + ,"useless_mod.configuration.furnace_tier_7_threads": "Single-block Alloy Furnace Tier 7 Thread Count" + ,"useless_mod.configuration.furnace_tier_8_threads": "Single-block Alloy Furnace Tier 8 Thread Count" + ,"useless_mod.configuration.furnace_tier_9_threads": "Single-block Alloy Furnace Tier 9 Thread Count" + ,"useless_mod.configuration.furnace_tier_10_threads": "Single-block Alloy Furnace Tier 10 Thread Count" + ,"useless_mod.configuration.catalyst_tier_0_parallel": "Catalyst Tier 0 Parallelism" + ,"useless_mod.configuration.catalyst_tier_0_energy_divisor": "Catalyst Tier 0 Energy Divisor" + ,"useless_mod.configuration.catalyst_tier_0_time_multiplier": "Catalyst Tier 0 Time Multiplier" + ,"useless_mod.configuration.catalyst_tier_1_parallel": "Catalyst Tier 1 Parallelism" + ,"useless_mod.configuration.catalyst_tier_1_energy_divisor": "Catalyst Tier 1 Energy Divisor" + ,"useless_mod.configuration.catalyst_tier_1_time_multiplier": "Catalyst Tier 1 Time Multiplier" + ,"useless_mod.configuration.catalyst_tier_2_parallel": "Catalyst Tier 2 Parallelism" + ,"useless_mod.configuration.catalyst_tier_2_energy_divisor": "Catalyst Tier 2 Energy Divisor" + ,"useless_mod.configuration.catalyst_tier_2_time_multiplier": "Catalyst Tier 2 Time Multiplier" + ,"useless_mod.configuration.catalyst_tier_3_parallel": "Catalyst Tier 3 Parallelism" + ,"useless_mod.configuration.catalyst_tier_3_energy_divisor": "Catalyst Tier 3 Energy Divisor" + ,"useless_mod.configuration.catalyst_tier_3_time_multiplier": "Catalyst Tier 3 Time Multiplier" + ,"useless_mod.configuration.catalyst_tier_4_parallel": "Catalyst Tier 4 Parallelism" + ,"useless_mod.configuration.catalyst_tier_4_energy_divisor": "Catalyst Tier 4 Energy Divisor" + ,"useless_mod.configuration.catalyst_tier_4_time_multiplier": "Catalyst Tier 4 Time Multiplier" + ,"useless_mod.configuration.catalyst_tier_5_parallel": "Catalyst Tier 5 Parallelism" + ,"useless_mod.configuration.catalyst_tier_5_energy_divisor": "Catalyst Tier 5 Energy Divisor" + ,"useless_mod.configuration.catalyst_tier_5_time_multiplier": "Catalyst Tier 5 Time Multiplier" + ,"useless_mod.configuration.catalyst_tier_6_parallel": "Catalyst Tier 6 Parallelism" + ,"useless_mod.configuration.catalyst_tier_6_energy_divisor": "Catalyst Tier 6 Energy Divisor" + ,"useless_mod.configuration.catalyst_tier_6_time_multiplier": "Catalyst Tier 6 Time Multiplier" + ,"useless_mod.configuration.catalyst_tier_7_parallel": "Catalyst Tier 7 Parallelism" + ,"useless_mod.configuration.catalyst_tier_7_energy_divisor": "Catalyst Tier 7 Energy Divisor" + ,"useless_mod.configuration.catalyst_tier_7_time_multiplier": "Catalyst Tier 7 Time Multiplier" + ,"useless_mod.configuration.catalyst_tier_8_parallel": "Catalyst Tier 8 Parallelism" + ,"useless_mod.configuration.catalyst_tier_8_energy_divisor": "Catalyst Tier 8 Energy Divisor" + ,"useless_mod.configuration.catalyst_tier_8_time_multiplier": "Catalyst Tier 8 Time Multiplier" + ,"useless_mod.configuration.catalyst_tier_9_parallel": "Catalyst Tier 9 Parallelism" + ,"useless_mod.configuration.catalyst_tier_9_energy_divisor": "Catalyst Tier 9 Energy Divisor" + ,"useless_mod.configuration.catalyst_tier_9_time_multiplier": "Catalyst Tier 9 Time Multiplier" + ,"useless_mod.configuration.coil_tier_1_threads": "Multiblock Coil Tier 1 Thread Count" + ,"useless_mod.configuration.coil_tier_1_single_task_parallel": "Multiblock Coil Tier 1 Single-task Parallelism" + ,"useless_mod.configuration.coil_tier_1_energy_divisor": "Multiblock Coil Tier 1 Energy Divisor" + ,"useless_mod.configuration.coil_tier_1_time_multiplier": "Multiblock Coil Tier 1 Time Multiplier" + ,"useless_mod.configuration.coil_tier_2_threads": "Multiblock Coil Tier 2 Thread Count" + ,"useless_mod.configuration.coil_tier_2_single_task_parallel": "Multiblock Coil Tier 2 Single-task Parallelism" + ,"useless_mod.configuration.coil_tier_2_energy_divisor": "Multiblock Coil Tier 2 Energy Divisor" + ,"useless_mod.configuration.coil_tier_2_time_multiplier": "Multiblock Coil Tier 2 Time Multiplier" + ,"useless_mod.configuration.coil_tier_3_threads": "Multiblock Coil Tier 3 Thread Count" + ,"useless_mod.configuration.coil_tier_3_single_task_parallel": "Multiblock Coil Tier 3 Single-task Parallelism" + ,"useless_mod.configuration.coil_tier_3_energy_divisor": "Multiblock Coil Tier 3 Energy Divisor" + ,"useless_mod.configuration.coil_tier_3_time_multiplier": "Multiblock Coil Tier 3 Time Multiplier" + ,"useless_mod.configuration.coil_tier_4_threads": "Multiblock Coil Tier 4 Thread Count" + ,"useless_mod.configuration.coil_tier_4_single_task_parallel": "Multiblock Coil Tier 4 Single-task Parallelism" + ,"useless_mod.configuration.coil_tier_4_energy_divisor": "Multiblock Coil Tier 4 Energy Divisor" + ,"useless_mod.configuration.coil_tier_4_time_multiplier": "Multiblock Coil Tier 4 Time Multiplier" + ,"useless_mod.configuration.coil_tier_5_threads": "Multiblock Coil Tier 5 Thread Count" + ,"useless_mod.configuration.coil_tier_5_single_task_parallel": "Multiblock Coil Tier 5 Single-task Parallelism" + ,"useless_mod.configuration.coil_tier_5_energy_divisor": "Multiblock Coil Tier 5 Energy Divisor" + ,"useless_mod.configuration.coil_tier_5_time_multiplier": "Multiblock Coil Tier 5 Time Multiplier" + ,"useless_mod.configuration.coil_tier_6_threads": "Multiblock Coil Tier 6 Thread Count" + ,"useless_mod.configuration.coil_tier_6_single_task_parallel": "Multiblock Coil Tier 6 Single-task Parallelism" + ,"useless_mod.configuration.coil_tier_6_energy_divisor": "Multiblock Coil Tier 6 Energy Divisor" + ,"useless_mod.configuration.coil_tier_6_time_multiplier": "Multiblock Coil Tier 6 Time Multiplier" + ,"useless_mod.configuration.coil_tier_7_threads": "Multiblock Coil Tier 7 Thread Count" + ,"useless_mod.configuration.coil_tier_7_single_task_parallel": "Multiblock Coil Tier 7 Single-task Parallelism" + ,"useless_mod.configuration.coil_tier_7_energy_divisor": "Multiblock Coil Tier 7 Energy Divisor" + ,"useless_mod.configuration.coil_tier_7_time_multiplier": "Multiblock Coil Tier 7 Time Multiplier" + ,"useless_mod.configuration.coil_tier_8_threads": "Multiblock Coil Tier 8 Thread Count" + ,"useless_mod.configuration.coil_tier_8_single_task_parallel": "Multiblock Coil Tier 8 Single-task Parallelism" + ,"useless_mod.configuration.coil_tier_8_energy_divisor": "Multiblock Coil Tier 8 Energy Divisor" + ,"useless_mod.configuration.coil_tier_8_time_multiplier": "Multiblock Coil Tier 8 Time Multiplier" + ,"useless_mod.configuration.coil_tier_9_threads": "Multiblock Coil Tier 9 Thread Count" + ,"useless_mod.configuration.coil_tier_9_single_task_parallel": "Multiblock Coil Tier 9 Single-task Parallelism" + ,"useless_mod.configuration.coil_tier_9_energy_divisor": "Multiblock Coil Tier 9 Energy Divisor" + ,"useless_mod.configuration.coil_tier_9_time_multiplier": "Multiblock Coil Tier 9 Time Multiplier" + ,"useless_mod.configuration.useful_tier_threads": "Multiblock Useful Tier Thread Count", + "block.useless_mod.useless_compact_c9": "Useless Compact C9", + "block.useless_mod.useless_compact_f9": "Useless Compact F9", + "block.useless_mod.useless_compact_l9": "Useless Compact L9", + "tooltip.useless_mod.useless_compact_c9": "Single block equivalent to eight fully configured C9 computation hosts (endgame mode).", + "tooltip.useless_mod.useless_compact_f9": "Single block equivalent to eight fully extended F9 crafting hosts (virtual crafting).", + "tooltip.useless_mod.useless_compact_l9": "Single block L9 storage host running ECO infinite storage.", + "gui.useless_mod.compact.c9.title": "Compact C9 Overview", + "gui.useless_mod.compact.c9.overview": "Fixed eight-host computation network; CPU, threads, and storage are provided by built-in shadow cores.", + "gui.useless_mod.compact.c9.hardware": "Built-in hardware cannot be removed; sneak-right-click opens the ECO interface manager.", + "gui.useless_mod.compact.f9.title": "Compact F9 Overview", + "gui.useless_mod.compact.f9.overview": "Fixed eight-host virtual crafting network with 88 FX lanes and merged pattern buses.", + "gui.useless_mod.compact.f9.interaction": "Right-click encoded patterns to insert; fluid containers fill, extract output, or recover input coolant.", + "gui.useless_mod.compact.f9.pattern_result.already_present": "This pattern is already installed.", + "gui.useless_mod.compact.f9.pattern_result.no_space": "No pattern slot is available.", + "gui.useless_mod.compact.f9.pattern_result.incompatible": "This item is not a compatible ECO pattern.", + "gui.useless_mod.compact.f9.pattern_result.no_target": "The compact F9 pattern buses are not ready.", + "gui.useless_mod.compact.l9.title": "Compact L9 Overview", + "gui.useless_mod.compact.l9.overview": "Fixed infinite storage domain managed by ECO with 64 built-in infinite components.", + "gui.useless_mod.compact.l9.interaction": "Built-in components are read-only; sneak-right-click opens transfer and filter settings.", + "gui.useless_mod.dimension_config.preview": "Preview", + "gui.useless_mod.dimension_config.preview.top": "Top view", + "gui.useless_mod.dimension_config.preview.side": "Section", + "gui.useless_mod.dimension_config.preview.period": "Period: %s x %s blocks", + "gui.useless_mod.dimension_config.preview.textured": "Render: real textures", + "gui.useless_mod.dimension_config.preview.color": "Render: color blocks", + "gui.useless_mod.dimension_config.preview.blocks": "Render: color blocks (1 px = %s blocks)", + "gui.useless_mod.dimension_config.preview.range": "Y %s ~ %s, %s layers", + "gui.useless_mod.dimension_config.preview.bedrock": "Bedrock Y=%s", + "gui.useless_mod.dimension_config.preview.empty": "Current configuration cannot be previewed", + "gui.useless_mod.dimension_config.preview.role.road": "Road surface", + "gui.useless_mod.dimension_config.preview.role.road_edge": "Road edge", + "gui.useless_mod.dimension_config.preview.role.road_marking": "Road center line", + "gui.useless_mod.dimension_config.preview.role.marker": "Center marker", + "gui.useless_mod.dimension_config.preview.role.boundary": "Boundary", + "gui.useless_mod.dimension_config.preview.role.border": "Border", + "gui.useless_mod.dimension_config.preview.role.center": "Center", + "gui.useless_mod.dimension_config.preview.role.fill": "Fill", + "gui.useless_mod.dimension_config.preview.close": "Close", + "gui.useless_mod.dimension_config.tooltip.preview": "Preview the generation result of the current configuration", + "tooltip.useless_mod.beef_wireless_logistics_mode": "Wireless Logistics Mode", + "key.useless_mod.open_wireless_logistics": "Open Wireless Logistics GUI", + "menu.useless_mod.wireless_logistics": "Wireless Logistics Network", + "gui.useless_mod.wireless_logistics.anchors": "Bound Containers (%s)", + "gui.useless_mod.wireless_logistics.network_position": "Net %s/%s", + "gui.useless_mod.wireless_logistics.network_name_hint": "Network name", + "gui.useless_mod.wireless_logistics.network_owned_hint": "Networks belong to your account (shared with your team once you join one), not to the staff you hold", + "gui.useless_mod.wireless_logistics.network_new": "New", + "gui.useless_mod.wireless_logistics.network_switch_hint": "Switch network (Shift+scroll with the staff works too)", + "gui.useless_mod.wireless_logistics.network_switched": "Switched to network %s/%s: %s", + "gui.useless_mod.wireless_logistics.network_unnamed": "Unnamed network", + "gui.useless_mod.wireless_logistics.search_hint": "Search containers (pinyin supported)", + "gui.useless_mod.wireless_logistics.search_no_match": "No matching container", + "gui.useless_mod.wireless_logistics.empty": "No containers bound (Shift + right-click a container)", + "gui.useless_mod.wireless_logistics.route_label": "Route %s", + "gui.useless_mod.wireless_logistics.filter": "Filter", + "gui.useless_mod.wireless_logistics.flow_label": "Flow: %s", + "gui.useless_mod.wireless_logistics.medium_label": "Resource: %s", + "gui.useless_mod.wireless_logistics.enabled_label": "Enabled: %s", + "gui.useless_mod.wireless_logistics.side_label": "Side: %s", + "gui.useless_mod.wireless_logistics.weight_label": "Wt", + "gui.useless_mod.wireless_logistics.weight_label_hint": "Higher weight is served first", + "gui.useless_mod.wireless_logistics.amount_label": "Qty", + "gui.useless_mod.wireless_logistics.amount_label_hint": "How much each output receives; with N outputs a transfer moves up to Qty x N", + "gui.useless_mod.wireless_logistics.interval_label": "Period", + "gui.useless_mod.wireless_logistics.interval_label_hint": "Ticks between two transfers", + "gui.useless_mod.wireless_logistics.release_only_hint": "Only Output nodes start a transfer; this is ignored on Input nodes", + "gui.useless_mod.wireless_logistics.range": "Range: %s - %s", + "gui.useless_mod.wireless_logistics.range_min": "%s or more; K/M/G/T/P/E supported", + "gui.useless_mod.wireless_logistics.rename": "Container Name", + "gui.useless_mod.wireless_logistics.highlight_hint": "While holding the staff with Wireless Logistics on, network containers highlight automatically (warm = output / cool = input / gray = disabled)", + "gui.useless_mod.wireless_logistics.multi_select_hint": "Ctrl+click to multi-select, Shift+click to select a range, then edit them together", + "gui.useless_mod.wireless_logistics.anchor_order_hint": "The ▲▼ at the row start reorders: with equal weight the earlier one is served first (and takes the remainder)", + "gui.useless_mod.wireless_logistics.multi_select_active": "Added to batch edit", + "gui.useless_mod.wireless_logistics.multi_select_count": "%s selected", + "gui.useless_mod.wireless_logistics.config_copied": "Route config copied", + "gui.useless_mod.wireless_logistics.config_pasted": "Route config pasted", + "gui.useless_mod.wireless_logistics.config_copy_hint": "Route %s · Ctrl+C copies the config / Ctrl+V pastes it", + "gui.useless_mod.wireless_logistics.select_all": "All", + "gui.useless_mod.wireless_logistics.clear_selection": "Clear", + "gui.useless_mod.wireless_logistics.apply_all": "Apply All", + "gui.useless_mod.wireless_logistics.apply_all_same_name": "Apply: same name", + "gui.useless_mod.wireless_logistics.apply_all_hint": "Apply the current route config to the listed containers (only matches while searching)", + "gui.useless_mod.wireless_logistics.apply_all_same_name_hint": "Applies the current route config to every container whose displayed name matches this one", + "gui.useless_mod.wireless_logistics.filter_button": "Filter (%s)", + "gui.useless_mod.wireless_logistics.filter_button_none": "Filter", + "gui.useless_mod.wireless_logistics.filter_panel_title": "Filter - %s slots used", + "gui.useless_mod.wireless_logistics.filter_panel_hint": "Right-click a slot to edit it in detail", + "gui.useless_mod.wireless_logistics.filter_close": "Close", + "gui.useless_mod.wireless_logistics.filter_close_hint": "Close the filter panel (Esc also works)", + "gui.useless_mod.wireless_logistics.filter_clear": "Clear", + "gui.useless_mod.wireless_logistics.filter_clear_hint": "Clear every filter slot on this route", + "gui.useless_mod.wireless_logistics.filter_hint": "Items for item routes, the fluid itself (or a container) for fluid routes, a tank for chemical routes; drag from JEI too", + "gui.useless_mod.wireless_logistics.filter_slot_title": "Slot %s", + "gui.useless_mod.wireless_logistics.filter_detail_done": "Done", + "gui.useless_mod.wireless_logistics.filter_detail_hint": "Right-click a slot to edit it in detail", + "gui.useless_mod.wireless_logistics.filter_exclude_off": "Whitelist", + "gui.useless_mod.wireless_logistics.filter_exclude_on": "Blacklist", + "gui.useless_mod.wireless_logistics.filter_exclude_hint": "Blacklist: matching stacks are blocked; Whitelist: only matching stacks move", + "gui.useless_mod.wireless_logistics.filter_cond_out_label": "Output", + "gui.useless_mod.wireless_logistics.filter_cond_in_label": "Input", + "gui.useless_mod.wireless_logistics.filter_cond_out_hint": "Measured at the output container", + "gui.useless_mod.wireless_logistics.filter_cond_in_hint": "Measured at the input container", + "gui.useless_mod.wireless_logistics.filter_cond_op_off": "Off", + "gui.useless_mod.wireless_logistics.filter_cond_op_at_least": "≥", + "gui.useless_mod.wireless_logistics.filter_cond_op_at_most": "≤", + "gui.useless_mod.wireless_logistics.filter_cond_self": "Self", + "gui.useless_mod.wireless_logistics.filter_cond_value_hint": "Blank = off; K/M/G/T/P/E supported", + "gui.useless_mod.wireless_logistics.filter_pattern_hint": "#tag or a wildcard, e.g. #c:ingots or *iron*", + "gui.useless_mod.wireless_logistics.filter_pattern_tag": "Matches by item/fluid tag", + "gui.useless_mod.wireless_logistics.filter_pattern_glob": "Match by registry id / display name; * any run, ? one char", + "gui.useless_mod.wireless_logistics.filter_pattern_unresolved": "No resource uses this tag; nothing will move", + "gui.useless_mod.wireless_logistics.filter_pattern_unsupported": "Tags are not supported for chemicals; use a wildcard instead", + "gui.useless_mod.wireless_logistics.filter_pattern_invalid": "\"%s\" is not a valid pattern: use #tag or include a * / ? wildcard", + "gui.useless_mod.wireless_logistics.filter_right_click_hint": "Right-click a slot to type a #tag or wildcard", + "gui.useless_mod.wireless_logistics.ae_whitelist_warning": "AE output + no filter: nothing will move", + "gui.useless_mod.wireless_logistics.state_on": "ON", + "gui.useless_mod.wireless_logistics.state_off": "OFF", + "gui.useless_mod.wireless_logistics.flow.absorb": "Input", + "gui.useless_mod.wireless_logistics.flow.release": "Output", + "gui.useless_mod.wireless_logistics.medium.item": "Item", + "gui.useless_mod.wireless_logistics.medium.ae_item": "AE Item", + "gui.useless_mod.wireless_logistics.medium.fluid": "Fluid", + "gui.useless_mod.wireless_logistics.medium.ae_fluid": "AE Fluid", + "gui.useless_mod.wireless_logistics.medium.energy": "Energy", + "gui.useless_mod.wireless_logistics.medium.chemical": "Chemical", + "gui.useless_mod.wireless_logistics.medium.ae_chemical": "AE Chemical", + "gui.useless_mod.wireless_logistics.medium.source": "Source", + "gui.useless_mod.wireless_logistics.medium.ae_source": "AE Source", + "gui.useless_mod.wireless_logistics.medium.ae_energy": "AE Energy", + "gui.useless_mod.wireless_logistics.medium.stress": "Stress", + "gui.useless_mod.wireless_logistics.medium.pressure": "Pressure", + "gui.useless_mod.wireless_logistics.medium.ae_pressure": "AE Pressure", + "gui.useless_mod.wireless_logistics.pressure.target_label": "Target", + "gui.useless_mod.wireless_logistics.pressure.target_hint": "Target pressure the receiving block is servoed to (bar)", + "gui.useless_mod.wireless_logistics.pressure.range": "Range: -1.0 ~ 20.0 bar (-1 = hard vacuum)", + "gui.useless_mod.thermostat.title": "Temperature Regulator", + "gui.useless_mod.thermostat.temperature": "Temperature (K)", + "gui.useless_mod.thermostat.kelvin": "Temperature (K)", + "gui.useless_mod.thermostat.celsius": "= %s C", + "gui.useless_mod.thermostat.hint": "Supports K/M/G suffixes, e.g. 1M = 1000000 K", + "gui.useless_mod.thermostat.enabled": "Thermostat: ON (click to disable)", + "gui.useless_mod.thermostat.disabled": "Thermostat: OFF (click to enable)", + "gui.useless_mod.thermostat.done": "Done", + "jade.useless_mod.thermostat.temperature": "Temperature: %s K (%s C)", + "jade.useless_mod.thermostat.disabled": "Thermostat: OFF", + "config.jade.plugin_useless_mod.temperature_regulator": "Temperature Regulator", + "config.jade.plugin_useless_mod.temperature_regulator.comment": "Show the configured temperature of the Temperature Regulator", + "gui.useless_mod.wireless_logistics.stress.rpm_label": "RPM", + "gui.useless_mod.wireless_logistics.stress.rpm_hint": "Rotation speed the target network is driven at", + "gui.useless_mod.wireless_logistics.stress.direction_label": "Dir", + "gui.useless_mod.wireless_logistics.stress.clockwise": "CW", + "gui.useless_mod.wireless_logistics.stress.counter_clockwise": "CCW", + "gui.useless_mod.wireless_logistics.stress.input_side_hint": "Speed and direction are configured on the receiving end", + "gui.useless_mod.wireless_logistics.stress.status_breakdown": "Self %s Net %s Free %s SU", + "gui.useless_mod.wireless_logistics.stress.status": "Status: %s", + "gui.useless_mod.wireless_logistics.stress.state.ok": "Connected", + "gui.useless_mod.wireless_logistics.stress.state.disabled": "Route disabled", + "gui.useless_mod.wireless_logistics.stress.state.idle": "Source network is not turning", + "gui.useless_mod.wireless_logistics.stress.state.no_network": "Source block is not in a kinetic network", + "gui.useless_mod.wireless_logistics.stress.state.not_kinetic": "Not a kinetic block", + "gui.useless_mod.wireless_logistics.stress.state.missing_io": "Missing an input or an output", + "gui.useless_mod.wireless_logistics.stress.state.no_power": "The source network has no stress to spare", + "gui.useless_mod.wireless_logistics.stress.state.no_share": "No share left: higher-priority outputs took the surplus", + "gui.useless_mod.wireless_logistics.stress.state.fed_elsewhere": "Another wireless output already supplies this network", + "gui.useless_mod.wireless_logistics.stress.state.self_fed": "This network has its own power source", + "gui.useless_mod.wireless_logistics.stress.state.real_source": "The target network already has a real power source", + "gui.useless_mod.wireless_logistics.stress.state.speed_conflict": "Another output drives this network at a different speed", + "gui.useless_mod.wireless_logistics.stress.state.shared": "This network is already driven by another route", + "gui.useless_mod.wireless_logistics.stress.chat.real_source": "Wireless stress paused: the target kinetic network already has a real power source.", + "gui.useless_mod.wireless_logistics.stress.chat.speed_conflict": "Wireless stress paused: another output drives the same network at a different speed.", + "gui.useless_mod.wireless_logistics.stress.chat.shared": "Wireless stress paused: this network is already driven by another route.", + "gui.useless_mod.wireless_logistics.stress.chat.conflict": "Wireless stress output paused due to a conflict.", + "gui.useless_mod.wireless_logistics.side.null": "Any Side", + "gui.useless_mod.wireless_logistics.side.up": "Top", + "gui.useless_mod.wireless_logistics.side.down": "Bottom", + "gui.useless_mod.wireless_logistics.side.north": "North", + "gui.useless_mod.wireless_logistics.side.south": "South", + "gui.useless_mod.wireless_logistics.side.west": "West", + "gui.useless_mod.wireless_logistics.side.east": "East", + "gui.useless_mod.wireless_logistics.dissolve": "Dissolve", + "gui.useless_mod.wireless_logistics.bound": "Bound %s to the wireless logistics network", + "gui.useless_mod.wireless_logistics.unbound": "Unbound %s from the wireless logistics network", + "gui.useless_mod.wireless_logistics.not_bindable": "This block has no movable storage", + "gui.useless_mod.omniversal_pattern.uploaded": "Omniversal pattern uploaded to the multiblock alloy furnace at: %s, Dim: %s (%s blocks away)", + "gui.useless_mod.omniversal_pattern.duplicate": "An identical omniversal pattern already exists in the multiblock alloy furnace; it was not uploaded and the blank pattern was refunded", + "gui.useless_mod.omniversal_pattern.uploaded.tooltip": "Click to teleport to that multiblock alloy furnace", + "commands.useless_mod.patterns.repair.player_only": "This command can only be run by a player: repairing needs a bound wireless terminal in the player's inventory to resolve the network", + "commands.useless_mod.patterns.repair.unbound": "No bound wireless terminal was found in your inventory, so the AE network to repair could not be determined", + "commands.useless_mod.patterns.repair.grid_unavailable": "A wireless terminal was found, but its AE network could not be resolved (out of range or not connected to an access point)", + "commands.useless_mod.patterns.repair.no_machines": "This network has no powered omniversal furnaces or ME pattern assemblies, so there is nothing to repair", + "commands.useless_mod.patterns.repair.summary": "Scanned %s omniversal patterns, found %s invalid; repaired %s, %s could not be repaired (recipe removed or ambiguous).", + "commands.useless_mod.patterns.repair.summary.preview": "Scanned %s omniversal patterns, found %s invalid; %s are repairable and %s could not be repaired (recipe removed or ambiguous). This was a preview; nothing was written back.", + "commands.useless_mod.patterns.repair.detail": "Could not repair: %s, bound recipe %s" +} diff --git a/src/main/resources/assets/useless_mod/lang/zh_cn.json b/src/main/resources/assets/useless_mod/lang/zh_cn.json index d3a1f109..50f5401a 100644 --- a/src/main/resources/assets/useless_mod/lang/zh_cn.json +++ b/src/main/resources/assets/useless_mod/lang/zh_cn.json @@ -1,10 +1,190 @@ { - "item.useless_mod.endless_beaf_item": "造化垂青之杖", - "block.useless_mod.teleport_block": "偶数维度传送方块", + "item.useless_mod.omniversal_pattern_converter": "万象样板转换器", + "tooltip.useless_mod.pattern_converter.unlinked": "未绑定万象炉或模具仓", + "tooltip.useless_mod.pattern_converter.bind_hint": "Shift+右键万象炉或模具仓进行绑定", + "tooltip.useless_mod.pattern_converter.linked": "绑定目标:%s [%s, %s, %s]", + "gui.useless_mod.paged.search": "搜索", + "gui.useless_mod.paged.search.hint": "按名称搜索", + "gui.useless_mod.paged.search.hint.in": "按输入查找样板", + "gui.useless_mod.paged.search.hint.out": "按输出查找样板", + "gui.useless_mod.paged.search.hint.mold": "按模具查找样板", + "gui.useless_mod.paged.search.hint.all": "按名称查找样板", + "gui.useless_mod.paged.search_mode.in": "输入", + "gui.useless_mod.paged.search_mode.out": "输出", + "gui.useless_mod.paged.search_mode.mold": "模具", + "gui.useless_mod.paged.search_mode.all": "全部", + "gui.useless_mod.pattern_converter.convert": "转换", + "message.useless_mod.pattern_converter.linked": "已绑定:%s", + "message.useless_mod.pattern_converter.binding": "绑定目标:%s [%s, %s, %s]", + "message.useless_mod.pattern_converter.unlinked": "已清除绑定目标", + "message.useless_mod.pattern_converter.unbound": "请先将转换器绑定到万象炉或模具仓", + "message.useless_mod.pattern_converter.target_unavailable": "绑定的万象炉或模具仓不可用", + "message.useless_mod.pattern_converter.no_patterns": "没有可转换的样板", + "message.useless_mod.pattern_converter.invalid_context": "无法在当前环境使用转换器", + "message.useless_mod.pattern_converter.result": "已转换 %s 个样板,跳过 %s 个", + "useless_mod.configuration.ae2_gift_package": "AE2大礼包", + "useless_mod.configuration.ae2_gift_package.items": "礼包物品", + "useless_mod.configuration.ae2_gift_package.items.comment": "AE2大礼包发放的物品。格式:modid:item_id,数量。不存在的物品和无效数量会跳过。", + "useless_mod.configuration.ae2_gift_package.items.tooltip": "每行一个物品:modid:item_id,数量。服务器必须安装对应模组并注册该物品。", + "useless_mod.configuration.enable_kaleidoscope_tavern_recipe_conversion": "启用森罗物语酒馆配方转换", + "useless_mod.configuration.enable_kaleidoscope_tavern_recipe_conversion.comment": "是否启用森罗物语酒桶、果盆和雪克杯配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_kaleidoscope_tavern_recipe_conversion.tooltip": "是否启用森罗物语酒桶、果盆和雪克杯配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_kaleidoscope_grilling_recipe_conversion": "启用森罗物语烧烤配方转换", + "useless_mod.configuration.enable_kaleidoscope_grilling_recipe_conversion.comment": "是否启用森罗物语串签烧烤配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_kaleidoscope_grilling_recipe_conversion.tooltip": "是否启用森罗物语串签烧烤配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_farmersdelight_recipe_conversion": "启用Farmer's Delight配方转换", + "useless_mod.configuration.enable_farmersdelight_recipe_conversion.comment": "是否启用Farmer's Delight烹饪锅、砧板配方及其附属模组配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_farmersdelight_recipe_conversion.tooltip": "是否启用Farmer's Delight烹饪锅、砧板配方及其附属模组配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_extradelight_recipe_conversion": "启用Extra Delight配方转换", + "useless_mod.configuration.enable_extradelight_recipe_conversion.comment": "是否启用Extra Delight工作站配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_extradelight_recipe_conversion.tooltip": "是否启用Extra Delight工作站配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_crabbersdelight_recipe_conversion": "启用Crabbers Delight配方转换", + "useless_mod.configuration.enable_crabbersdelight_recipe_conversion.comment": "是否启用Crabbers Delight捕蟹笼配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_crabbersdelight_recipe_conversion.tooltip": "是否启用Crabbers Delight捕蟹笼配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_casualnessdelight_recipe_conversion": "启用Casualness Delight配方转换", + "useless_mod.configuration.enable_casualnessdelight_recipe_conversion.comment": "是否启用Casualness Delight炸锅配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_casualnessdelight_recipe_conversion.tooltip": "是否启用Casualness Delight炸锅配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_expandeddelight_recipe_conversion": "启用Expanded Delight配方转换", + "useless_mod.configuration.enable_expandeddelight_recipe_conversion.comment": "是否启用Expanded Delight榨汁机配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_expandeddelight_recipe_conversion.tooltip": "是否启用Expanded Delight榨汁机配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_brewinandchewin_recipe_conversion": "启用Brewin And Chewin配方转换", + "useless_mod.configuration.enable_brewinandchewin_recipe_conversion.comment": "是否启用Brewin And Chewin发酵桶配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_brewinandchewin_recipe_conversion.tooltip": "是否启用Brewin And Chewin发酵桶配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_nomadsdelight_recipe_conversion": "启用Nomad's Delight配方转换", + "useless_mod.configuration.enable_nomadsdelight_recipe_conversion.comment": "是否启用Nomad's Delight黄油搅拌器和凝乳过滤袋配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_nomadsdelight_recipe_conversion.tooltip": "是否启用Nomad's Delight黄油搅拌器和凝乳过滤袋配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_ubesdelight_recipe_conversion": "启用Ube's Delight配方转换", + "useless_mod.configuration.enable_ubesdelight_recipe_conversion.comment": "是否启用Ube's Delight烘焙垫配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_ubesdelight_recipe_conversion.tooltip": "是否启用Ube's Delight烘焙垫配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_barbequesdelight_recipe_conversion": "启用Barbeque's Delight配方转换", + "useless_mod.configuration.enable_barbequesdelight_recipe_conversion.comment": "是否启用Barbeque's Delight烧烤架和串签配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_barbequesdelight_recipe_conversion.tooltip": "是否启用Barbeque's Delight烧烤架和串签配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_youkaishomecoming_recipe_conversion": "启用Youkai's Homecoming配方转换", + "useless_mod.configuration.enable_youkaishomecoming_recipe_conversion.comment": "是否启用Youkai's Homecoming烹饪、晾晒和发酵配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_youkaishomecoming_recipe_conversion.tooltip": "是否启用Youkai's Homecoming烹饪、晾晒和发酵配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_kaleidoscope_cookery_recipe_conversion": "启用森罗物语厨艺配方转换", + "useless_mod.configuration.enable_kaleidoscope_cookery_recipe_conversion.comment": "是否启用森罗物语厨艺厨房配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_kaleidoscope_cookery_recipe_conversion.tooltip": "是否启用森罗物语厨艺厨房配方转换,修改后重启游戏生效", + "itemGroup.useless_mod.main": "Useless", + "block.useless_mod.multiblock_alloy_furnace_core": "多方块万象合金炉核心", + "config.jade.plugin_useless_mod.multiblock_alloy_furnace_core_name": "显示多方块万象合金炉核心名称", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.1": "不朽的合成之神,聆听我们的祈祷。", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.2": "我们是您的孩子,是行走于机械与奥术道路上的工程师。", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.3": "我们珍视配方胜过一切,因为那是您赐予最永恒的礼物。", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.4": "我们渴望全速并行与极致效率的祝福,", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.5": "愿自动化永不停息,愿每一道工序都在您的秩序中加速运转。", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.6": "我们受红石与能量庇护,受线圈与样板引导,", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.7": "在模具、催化剂与物质流之间,追寻合成的终极答案。", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.8": "我们不辞辛劳,穿行于模组与维度之间,", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.9": "翻阅 JEI 的页册,解析散落在整合包中的古老配方。", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.10": "以缓存与网络,替我们整理混乱,承载万千工序。", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.11": "鸣大钟一次!推动拉杆,接通能量,", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.12": "启动活塞与泵,让基础材料开始奔涌!", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.13": "鸣大钟两次!按下按钮,载入样板,发动引擎,", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.14": "注入魔力、源质、化学物质与生命之力!", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.15": "鸣大钟三次!让线圈升温,让结构觉醒,", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.16": "让并行的洪流贯穿每一条生产链!", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.17": "齐声歌唱,赞美万象合金炉!", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.18": "科技与魔法在它面前不再彼此隔绝,", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.19": "配方的来源也不再成为阻碍。", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.20": "赞美万象合金炉,赞美全能的合成圣座!", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.21": "愿它的队列永不堵塞,愿我们的缓存永不溢出,", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.22": "愿我们的自动化永不停转,愿我们的 TPS 永不坠落。", + "block.useless_mod.me_pattern_assembly": "ME样板总成", + "block.useless_mod.omniversal_mold_hub": "万象模具集散中心", + "block.useless_mod.passive_crafting_hatch": "被动合成仓", + "block.useless_mod.omniversal_furnace_casing": "万象炉装饰外壳", + "block.useless_mod.temperature_regulator": "温度调节器", + "block.useless_mod.useless_coil_tier_1": "一阶无用线圈", + "block.useless_mod.useless_coil_tier_2": "二阶无用线圈", + "block.useless_mod.useless_coil_tier_3": "三阶无用线圈", + "block.useless_mod.useless_coil_tier_4": "四阶无用线圈", + "block.useless_mod.useless_coil_tier_5": "五阶无用线圈", + "block.useless_mod.useless_coil_tier_6": "六阶无用线圈", + "block.useless_mod.useless_coil_tier_7": "七阶无用线圈", + "block.useless_mod.useless_coil_tier_8": "八阶无用线圈", + "block.useless_mod.useless_coil_tier_9": "九阶无用线圈", + "block.useless_mod.useful_coil": "有用线圈", + "death.attack.useless_mod.beef_tool": "%1$s 被牛排工具强制击杀", + "death.attack.useless_mod.beef_tool.player": "%1$s 被 %2$s 使用牛排工具强制击杀", + "item.useless_mod.omniversal_pattern": "万象样板", + "tooltip.useless_mod.omniversal_pattern.recipe": "绑定配方:%s", + "tooltip.useless_mod.omniversal_pattern.mold": "模具标记:%s", + "tooltip.useless_mod.omniversal_pattern.no_mold": "无需模具", + "tooltip.useless_mod.omniversal_pattern.unknown_mold": "由配方定义的模具", + "gui.useless_mod.multiblock_alloy_furnace.formed": "结构已成型", + "gui.useless_mod.multiblock_alloy_furnace.invalid": "结构无效", + "gui.useless_mod.multiblock_alloy_furnace.invalid_count": "结构无效:%s处错误", + "gui.useless_mod.multiblock_alloy_furnace.coil": "线圈阶级:%s", + "gui.useless_mod.multiblock_alloy_furnace.redstone": "红石:%s", + "gui.useless_mod.multiblock_alloy_furnace.tasks": "任务:%s / %s", + "gui.useless_mod.multiblock_alloy_furnace.energy_limit": "FE补能上限", + "gui.useless_mod.multiblock_alloy_furnace.energy_limit.desc": "核心储能达到该数值后停止从AE/AppliedFlux网络抽取能量。设为0可禁用自动补能;已有能量不会被删除。", + "gui.useless_mod.multiblock_alloy_furnace.energy_limit.units": "支持K/M/G/T/P/E缩写,最大不超过炉子的FE容量。", + "gui.useless_mod.multiblock_alloy_furnace.preview.correct": "正确", + "gui.useless_mod.multiblock_alloy_furnace.preview.incorrect": "错误", + "gui.useless_mod.multiblock_alloy_furnace.preview.unavailable": "区块未加载", + "gui.useless_mod.multiblock_alloy_furnace.preview.expected": "需要:%s", + "gui.useless_mod.multiblock_alloy_furnace.preview.actual": "当前:%s", + "gui.useless_mod.multiblock_alloy_furnace.preview.position": "位置:%s, %s, %s", + "gui.useless_mod.multiblock_alloy_furnace.preview.casing_with_hatch": "%s(其中一个位置可使用%s)", + "gui.useless_mod.multiblock_alloy_furnace.preview.casing_with_functional_parts": "%s(17个外层位置中须各放置一个%s和%s;可选一个%s。样板总成和被动合成仓不可共享,模具集散中心可共享)", + "gui.useless_mod.multiblock_alloy_furnace.preview.coil_any": "%s至%s(所有线圈必须为同一阶级)", + "gui.useless_mod.passive_crafting.interval": "合成间隔(tick)", + "gui.useless_mod.passive_crafting.multiplier": "合成倍率", + "gui.useless_mod.passive_crafting.max_multiplier": "最大倍率:%s", + "gui.useless_mod.passive_crafting.countdown": "下次运行:%s tick", + "gui.useless_mod.passive_crafting.apply": "应用", + "gui.useless_mod.passive_crafting.apply_slot": "本槽跟随全局", + "gui.useless_mod.passive_crafting.apply_all": "全部跟随全局", + "gui.useless_mod.passive_crafting.slot_global_target": "全局倍率", + "gui.useless_mod.passive_crafting.slot_select_hint": "Ctrl 点击选中", + "gui.useless_mod.passive_crafting.slot_wheel_hint": "滚轮调整", + "gui.useless_mod.passive_crafting.slot_label": "槽位 %s", + "gui.useless_mod.passive_crafting.slot_label_selected": "编辑槽位 %s", + "gui.useless_mod.passive_crafting.slot_selected_hint": "已选中(Ctrl 再点一次取消)", + "gui.useless_mod.passive_crafting.slot_multiplier_value": "倍率 %s", + "gui.useless_mod.passive_crafting.slot_multiplier_state_custom": "单独设置", + "gui.useless_mod.passive_crafting.slot_multiplier_state_global": "跟随全局 %s", + "gui.useless_mod.passive_crafting.slot_multiplier_custom": "批次倍率:%s(单独设置,全局 %s)", + "gui.useless_mod.passive_crafting.slot_multiplier_global": "批次倍率:%s(跟随全局)", + "gui.useless_mod.passive_crafting.slot_multiplier_edit_hint": "Ctrl+左键 选中槽位后在右侧编辑;滚轮快速调整(Shift 加倍/减半)", + "gui.useless_mod.passive_crafting.progress": "进度:%s / %s", + "gui.useless_mod.passive_crafting.connected": "结构已连接", + "gui.useless_mod.passive_crafting.unformed": "结构未成型", + "gui.useless_mod.passive_crafting.active_slots": "可用栏位:%s / %s", + "gui.useless_mod.passive_crafting.locked_slot": "锁定的恢复栏位", + "gui.useless_mod.passive_crafting.status_detail": "%s:%s", + "gui.useless_mod.passive_crafting.status.empty": "空", + "gui.useless_mod.passive_crafting.status.ready": "就绪", + "gui.useless_mod.passive_crafting.status.running": "运行中", + "gui.useless_mod.passive_crafting.status.paused": "已暂停", + "gui.useless_mod.passive_crafting.status.missing_input": "材料不足", + "gui.useless_mod.passive_crafting.status.missing_mold": "缺少模具", + "gui.useless_mod.passive_crafting.status.ae_offline": "AE 网络断开", + "gui.useless_mod.passive_crafting.status.invalid_pattern": "无效样板", + "gui.useless_mod.passive_crafting.status.waiting_output": "等待产物写回", + "gui.useless_mod.multiblock_builder.invalid_core": "该方块不是多方块万象合金炉核心", + "gui.useless_mod.multiblock_builder.invalid_functional_parts": "功能件重复、已被其他合金炉占用,或无法在不替换方块实体的情况下补齐功能件", + "gui.useless_mod.multiblock_builder.mixed_coils": "已有线圈必须全部同阶", + "gui.useless_mod.multiblock_builder.missing_coils": "没有任何阶级的线圈足以完成结构", + "gui.useless_mod.multiblock_builder.no_permission": "你无权在部分结构区域内搭建", + "gui.useless_mod.multiblock_builder.blocked": "结构在 %s 处被阻挡", + "gui.useless_mod.multiblock_builder.missing_materials": "绑定的AE网络与背包材料合计不足", + "gui.useless_mod.multiblock_builder.commit_failed": "搭建失败,材料与方块已还原", + "gui.useless_mod.multiblock_builder.success": "已使用%s阶线圈搭建万象合金炉", + "gui.useless_mod.omniversal_pattern.too_many_slots": "配方的原料数量超过了终端的槽位数量", + "biome.useless_mod:useless_biome": "无用之地", + "dimension.useless_mod.uselessdim": "奇数维度", + "dimension.useless_mod.uselessdim2": "偶数维度", + "dimension.useless_mod.uselessdim3": "三维度", "block.useless_mod.ore_generator_block": "矿石生成器", - "block.useless_mod.teleport_block_2": "奇数维度传送方块", + "block.useless_mod.teleport_block": "奇数维度传送方块", + "block.useless_mod.teleport_block_2": "偶数维度传送方块", "block.useless_mod.teleport_block_3": "三维度传送方块", - "biome.useless_mod:useless_biome": "无用之地", + "block.useless_mod.advanced_alloy_furnace_block": "万象合金炉", + "block.useless_mod.creator_doll_block": "制作者皮肤玩偶", + "block.useless_mod.supervisor": "监工", "block.useless_mod.black_glow_plastic": "黑色荧光塑料", "block.useless_mod.red_glow_plastic": "红色荧光塑料", "block.useless_mod.green_glow_plastic": "绿色荧光塑料", @@ -23,60 +203,77 @@ "block.useless_mod.white_glow_plastic": "白色荧光塑料", "block.useless_mod.dark_red_glow_plastic": "深红色荧光塑料", "block.useless_mod.aqua_glow_plastic": "水色荧光塑料", - "key.category.useless_mod.useless": "无用模组", - "key.useless_mod.switch_silk_touch": "切换到精准采集", - "key.useless_mod.switch_fortune": "切换到时运", - "key.useless_mod.chain_mining": "连锁挖掘(需要按住)", - "key.useless_mod.enhanced_chain_mining": "增强连锁挖掘切换", - "key.useless_mod.force_mining": "强制挖掘模式切换", - "key.useless_mod.switch_mode_wheel": "打开模式选择轮盘", - "key.useless_mod.trigger_force_mining": "触发强制挖掘", - "key.useless_mod.set_master_pattern": "设置主扩展样板供应器", - "key.useless_mod.set_slave_pattern": "设置从扩展样板供应器", - "category.useless_mod.chain_mining": "无用模组控制", - "tooltip.useless_mod.silk_touch_mode": "精准采集", - "tooltip.useless_mod.fortune_mode": "时运", - "tooltip.useless_mod.silk_touch_level": "精准采集 %s", - "tooltip.useless_mod.fortune_level": "时运 %s", - "tooltip.useless_mod.external_enchantment": "(受外部附魔增强)", - "tooltip.useless_mod.switch_enchantment": "使用 PAGE_UP/PAGE_DOWN 切换模式", - "tooltip.useless_mod.fast_break_plastic": "Shift + 右键点击快速破坏塑料方块", + "block.useless_mod.black_plastic": "黑色塑料", + "block.useless_mod.black_plastic_ctm": "黑色塑料(连接材质)", + "block.useless_mod.black_glow_plastic_ctm": "黑色荧光塑料(连接材质)", + "block.useless_mod.red_plastic": "红色塑料", + "block.useless_mod.red_plastic_ctm": "红色塑料(连接材质)", + "block.useless_mod.red_glow_plastic_ctm": "红色荧光塑料(连接材质)", + "block.useless_mod.green_plastic": "绿色塑料", + "block.useless_mod.green_plastic_ctm": "绿色塑料(连接材质)", + "block.useless_mod.green_glow_plastic_ctm": "绿色荧光塑料(连接材质)", + "block.useless_mod.brown_plastic": "棕色塑料", + "block.useless_mod.brown_plastic_ctm": "棕色塑料(连接材质)", + "block.useless_mod.brown_glow_plastic_ctm": "棕色荧光塑料(连接材质)", + "block.useless_mod.blue_plastic": "蓝色塑料", + "block.useless_mod.blue_plastic_ctm": "蓝色塑料(连接材质)", + "block.useless_mod.blue_glow_plastic_ctm": "蓝色荧光塑料(连接材质)", + "block.useless_mod.purple_plastic": "紫色塑料", + "block.useless_mod.purple_plastic_ctm": "紫色塑料(连接材质)", + "block.useless_mod.purple_glow_plastic_ctm": "紫色荧光塑料(连接材质)", + "block.useless_mod.cyan_plastic": "青色塑料", + "block.useless_mod.cyan_plastic_ctm": "青色塑料(连接材质)", + "block.useless_mod.cyan_glow_plastic_ctm": "青色荧光塑料(连接材质)", + "block.useless_mod.light_gray_plastic": "浅灰色塑料", + "block.useless_mod.light_gray_plastic_ctm": "浅灰色塑料(连接材质)", + "block.useless_mod.light_gray_glow_plastic_ctm": "浅灰色荧光塑料(连接材质)", + "block.useless_mod.gray_plastic": "灰色塑料", + "block.useless_mod.gray_plastic_ctm": "灰色塑料(连接材质)", + "block.useless_mod.gray_glow_plastic_ctm": "灰色荧光塑料(连接材质)", + "block.useless_mod.pink_plastic": "粉色塑料", + "block.useless_mod.pink_plastic_ctm": "粉色塑料(连接材质)", + "block.useless_mod.pink_glow_plastic_ctm": "粉色荧光塑料(连接材质)", + "block.useless_mod.lime_plastic": "黄绿色塑料", + "block.useless_mod.lime_plastic_ctm": "黄绿色塑料(连接材质)", + "block.useless_mod.lime_glow_plastic_ctm": "黄绿色荧光塑料(连接材质)", + "block.useless_mod.yellow_plastic": "黄色塑料", + "block.useless_mod.yellow_plastic_ctm": "黄色塑料(连接材质)", + "block.useless_mod.yellow_glow_plastic_ctm": "黄色荧光塑料(连接材质)", + "block.useless_mod.light_blue_plastic": "浅蓝色塑料", + "block.useless_mod.light_blue_plastic_ctm": "浅蓝色塑料(连接材质)", + "block.useless_mod.light_blue_glow_plastic_ctm": "浅蓝色荧光塑料(连接材质)", + "block.useless_mod.magenta_plastic": "品红色塑料", + "block.useless_mod.magenta_plastic_ctm": "品红色塑料(连接材质)", + "block.useless_mod.magenta_glow_plastic_ctm": "品红色荧光塑料(连接材质)", + "block.useless_mod.orange_plastic": "橙色塑料", + "block.useless_mod.orange_plastic_ctm": "橙色塑料(连接材质)", + "block.useless_mod.orange_glow_plastic_ctm": "橙色荧光塑料(连接材质)", + "block.useless_mod.white_plastic": "白色塑料", + "block.useless_mod.white_plastic_ctm": "白色塑料(连接材质)", + "block.useless_mod.white_glow_plastic_ctm": "白色荧光塑料(连接材质)", + "block.useless_mod.dark_red_plastic": "深红色塑料", + "block.useless_mod.dark_red_plastic_ctm": "深红色塑料(连接材质)", + "block.useless_mod.dark_red_glow_plastic_ctm": "深红色荧光塑料(连接材质)", + "block.useless_mod.aqua_plastic": "水色塑料", + "block.useless_mod.aqua_plastic_ctm": "水色塑料(连接材质)", + "block.useless_mod.aqua_glow_plastic_ctm": "水色荧光塑料(连接材质)", + "block.useless_mod.useless_glass_tier_1": "一阶无用玻璃", + "block.useless_mod.useless_glass_tier_2": "二阶无用玻璃", + "block.useless_mod.useless_glass_tier_3": "三阶无用玻璃", + "block.useless_mod.useless_glass_tier_4": "四阶无用玻璃", + "block.useless_mod.useless_glass_tier_5": "五阶无用玻璃", + "block.useless_mod.useless_glass_tier_6": "六阶无用玻璃", + "block.useless_mod.useless_glass_tier_7": "七阶无用玻璃", + "block.useless_mod.useless_glass_tier_8": "八阶无用玻璃", + "block.useless_mod.useless_glass_tier_9": "九阶无用玻璃", + "item.useless_mod.endless_beaf_item.silk_touch": "太初洞见之杖", "item.useless_mod.endless_beaf_item.fortune": "造化垂青之杖", - "text.useless_mod.silk_touch": "精准采集", - "text.useless_mod.fortune": "时运", - "message.useless_mod.switched_to_silk_touch": "§b已切换到精准采集模式", - "message.useless_mod.switched_to_fortune": "§6已切换到时运模式", - "message.useless_mod.festive_triggered": "§6战利品大爆发!", - "tooltip.useless_mod.festive_affix": "5% 概率在击杀时触发战利品大爆发", - "tooltip.useless_mod.auto_collect": "自带磁力功能", - "config.useless_mod.botany_pot_growth_multiplier": "植物盆生长倍率", - "config.useless_mod.matrix_pattern_count": "矩阵样板数量倍率", - "item.useless_mod.useless_integral_component_tier_1": "无用整合组件 等级 1", - "item.useless_mod.useless_integral_component_tier_2": "无用整合组件 等级 2", - "item.useless_mod.useless_integral_component_tier_3": "无用整合组件 等级 3", - "item.useless_mod.useless_integral_component_tier_4": "无用整合组件 等级 4", - "item.useless_mod.useless_integral_component_tier_5": "无用整合组件 等级 5", - "item.useless_mod.useless_integral_component_tier_6": "无用整合组件 等级 6", - "item.useless_mod.useless_integral_component_tier_7": "无用整合组件 等级 7", - "item.useless_mod.useless_integral_component_tier_8": "无用整合组件 等级 8", - "item.useless_mod.useless_integral_component_tier_9": "无用整合组件 等级 9", - "useless_mod.augment.upgrade": "升级增强", - "info.useless_mod.lava_gen_nether": "在下界工作", - "info.thermal.augment.attr.MachineParallel": "最大并行数", - "item.useless_mod.augment_parallel_1": "并行处理增强 I", - "item.useless_mod.augment_parallel_2": "并行处理增强 II", - "item.useless_mod.augment_parallel_3": "并行处理增强 III", - "config.useless_mod.parallel_increase_energy_consumption": "并行增加能耗", - "config.useless_mod.base_mod_affect_parallel": "基础倍率影响并行", - "config.useless_mod.add_extra_parallel_augments_to_tab": "添加额外并行增强到标签", - "config.useless_mod.parallel_increase_item_cap": "并行增加物品容量", - "config.useless_mod.parallel_increase_fluid_cap": "并行增加流体容量", - "config.useless_mod.parallel_increase_energy_cap": "并行增加能量容量", - "config.useless_mod.parallel_increase_item_transfer": "并行增加物品传输", - "config.useless_mod.parallel_increase_fluid_transfer": "并行增加流体传输", - "config.useless_mod.parallel_increase_energy_transfer": "并行增加能量传输", - "block.useless_mod.advanced_alloy_furnace_block": "万象合金炉", + "item.useless_mod.endless_beaf_item": "造化垂青之杖", + "advancements.useless_mod.grass_wand_drop.title": "这回真是打草掉的", + "advancements.useless_mod.grass_wand_drop.description": "从草里打出了一把造化杖。", + "item.useless_mod.creator_doll_block": "制作者皮肤玩偶", + "item.useless_mod.supervisor": "监工", "item.useless_mod.useless_ingot_tier_1": "一阶无用锭", "item.useless_mod.useless_ingot_tier_2": "二阶无用锭", "item.useless_mod.useless_ingot_tier_3": "三阶无用锭", @@ -86,34 +283,6 @@ "item.useless_mod.useless_ingot_tier_7": "七阶无用锭", "item.useless_mod.useless_ingot_tier_8": "八阶无用锭", "item.useless_mod.useless_ingot_tier_9": "九阶无用锭", - "message.useless_mod.chain_mining_off": "§c已关闭连锁挖掘模式", - "config.useless_mod.chain_mining_range_x": "X轴连锁挖掘范围", - "config.useless_mod.chain_mining_range_y": "Y轴连锁挖掘范围", - "config.useless_mod.chain_mining_range_z": "Z轴连锁挖掘范围", - "config.useless_mod.chain_mining_max_blocks": "连锁挖掘最大方块数量", - "message.useless_mod.enhanced_chain_on": "§e已开启增强连锁挖掘模式", - "message.useless_mod.enhanced_chain_off": "§c已关闭增强连锁挖掘模式", - "message.useless_mod.force_mining_on": "§e已开启强制挖掘模式", - "message.useless_mod.force_mining_off": "§c已关闭强制挖掘模式", - "tooltip.useless_mod.enhanced_chain_mining_on": "增强连锁:开启", - "tooltip.useless_mod.enhanced_chain_mining_off": "增强连锁:关闭", - "tooltip.useless_mod.chain_mining_enhanced": "增强连锁模式", - "tooltip.useless_mod.chain_mining_normal": "普通连锁模式", - "tooltip.useless_mod.switch_enhanced_chain": "切换增强连锁: %s", - "tooltip.useless_mod.enhanced_chain_description": "增强连锁模式:取消相邻才能连锁的限制", - "tooltip.useless_mod.mode_with_status": "%s %s", - "text.useless_mod.active": "已激活", - "tooltip.useless_mod.wrench_mode": "扳手", - "tooltip.useless_mod.screwdriver_mode": "螺丝刀", - "tooltip.useless_mod.mallet_mode": "软锤", - "tooltip.useless_mod.crowbar_mode": "撬棍", - "tooltip.useless_mod.hammer_mode": "锻造锤", - "tooltip.useless_mod.enhanced_chain_mining_mode": "增强连锁", - "tooltip.useless_mod.chain_mining_mode": "连锁挖掘", - "tooltip.useless_mod.omnitool_mode": "全能工具", - "tooltip.useless_mod.force_mining_mode": "强制挖掘", - "tooltip.useless_mod.ae_storage_priority_mode": "AE存储优先", - "jei.useless_mod.catalyst_info": "催化剂并行数信息", "item.useless_mod.possible_useful_ingot": "可能有用锭", "item.useless_mod.useful_ingot": "有用锭", "item.useless_mod.metal_mold_plate": "金属模具(板)", @@ -121,8 +290,6 @@ "item.useless_mod.metal_mold_gear": "金属模具(齿轮)", "item.useless_mod.metal_mold_wire": "金属模具(线)", "item.useless_mod.metal_mold_block": "金属模具(方块)", - "block.useless_mod.creator_doll_block": "制作者皮肤玩偶", - "item.useless_mod.creator_doll_block": "制作者皮肤玩偶", "item.useless_mod.useless_gear_tier_1": "一阶无用齿轮", "item.useless_mod.useless_gear_tier_2": "二阶无用齿轮", "item.useless_mod.useless_gear_tier_3": "三阶无用齿轮", @@ -132,17 +299,1018 @@ "item.useless_mod.useless_gear_tier_7": "七阶无用齿轮", "item.useless_mod.useless_gear_tier_8": "八阶无用齿轮", "item.useless_mod.useless_gear_tier_9": "九阶无用齿轮", - "item.useless_mod.useless_glass_tier_1": "一阶无用玻璃", - "item.useless_mod.useless_glass_tier_2": "二阶无用玻璃", - "item.useless_mod.useless_glass_tier_3": "三阶无用玻璃", - "item.useless_mod.useless_glass_tier_4": "四阶无用玻璃", - "item.useless_mod.useless_glass_tier_5": "五阶无用玻璃", - "item.useless_mod.useless_glass_tier_6": "六阶无用玻璃", - "item.useless_mod.useless_glass_tier_7": "七阶无用玻璃", - "item.useless_mod.useless_glass_tier_8": "八阶无用玻璃", - "item.useless_mod.useless_glass_tier_9": "九阶无用玻璃", + "item.useless_mod.ae2_gift_package": "AE大礼包", + "emi.category.useless_mod.advanced_alloy_furnace": "万象合金炉", "emi.category.useless_mod.catalyst_info": "催化剂并行数信息", + + "gui.useless_mod.advanced_alloy_furnace.energy": "能量: %s / %s FE", + "gui.useless_mod.advanced_alloy_furnace.progress": "进度: %s/%s (%s%%)", + "gui.useless_mod.advanced_alloy_furnace.status": "状态: %s", + "gui.useless_mod.advanced_alloy_furnace.status.active": "工作中", + "gui.useless_mod.advanced_alloy_furnace.status.idle": "空闲", + "gui.useless_mod.advanced_alloy_furnace.current_parallel": "本次并行数: %s", + "gui.useless_mod.advanced_alloy_furnace.max_parallel": "当前最大并行数: %s", + "gui.useless_mod.advanced_alloy_furnace.ae_tasks": "AE任务: %s", + "gui.useless_mod.advanced_alloy_furnace.ae_progress": "AE进度: %s/%s (%s%%)", + "gui.useless_mod.advanced_alloy_furnace.ae_task_progress": " - %s (×%d): %d/%d (%s%%) [%s]", + "gui.useless_mod.advanced_alloy_furnace.ae_task_progress_ticks": " - %s (×%d): %dt / %dt [%s]", + "gui.useless_mod.advanced_alloy_furnace.ae_task_waiting": " - %s (×%d): %s", + "gui.useless_mod.advanced_alloy_furnace.ae_task_waiting_detail": " - %s (×%d): %s,需要 %s", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.processing": "处理中", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.queued": "排队中", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_mold": "等待模具/条件", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_structure": "等待多方块结构", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_mold_hub": "等待模具集散中心", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_missing_mold": "缺少所需模具", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_tier": "等待万象炉等级", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_output_mismatch": "样板输出不匹配", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_invalid_pattern": "无效合成样板", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_conditions": "等待运行条件", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_recipe": "等待严格匹配配方", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_ambiguous_recipe": "等待配方不唯一", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_energy": "等待能量", + "gui.useless_mod.advanced_alloy_furnace.ae_task_recipe_not_found": "没有严格匹配配方", + "gui.useless_mod.advanced_alloy_furnace.ae_task_recipe_ambiguous": "严格匹配到多个配方,等待处理", + "gui.useless_mod.advanced_alloy_furnace.ae_task_waiting_energy_detail": "机器能量不足", + "gui.useless_mod.advanced_alloy_furnace.recipe_priority": "机器配方优先级", + "gui.useless_mod.advanced_alloy_furnace.recipe_priority_up": "上移", + "gui.useless_mod.advanced_alloy_furnace.recipe_priority_down": "下移", + "gui.useless_mod.advanced_alloy_furnace.recipe_priority_reset": "恢复自动排序", + "gui.useless_mod.advanced_alloy_furnace.ae_task_missing_correct_mold": "正确模具", + "gui.useless_mod.advanced_alloy_furnace.view_recipes": "点击查看配方", + "gui.useless_mod.advanced_alloy_furnace.no_process": "没有活动进程", + "gui.useless_mod.advanced_alloy_furnace.parallel_info": "并行数信息", + "gui.useless_mod.advanced_alloy_furnace.catalyst": "催化剂: %s (%s倍)", + "gui.useless_mod.advanced_alloy_furnace.parallel_description.title": "并行数说明:", + "gui.useless_mod.advanced_alloy_furnace.parallel_description.1": "• 消耗和产出乘以并行数", + "gui.useless_mod.advanced_alloy_furnace.parallel_description.2": "• 处理时间随催化剂等级降低", + "gui.useless_mod.advanced_alloy_furnace.parallel_description.3": "• 能量消耗乘以并行数", + "gui.useless_mod.advanced_alloy_furnace.parallel_description.4": "• 催化剂为可选项,可提高并行数", + "gui.useless_mod.advanced_alloy_furnace.catalyst_warning": "催化剂不会被消耗", + "gui.useless_mod.advanced_alloy_furnace.fluid_amount": "%s / %s mB", + "gui.useless_mod.advanced_alloy_furnace.chemical_amount": "%s / %s", + "gui.useless_mod.advanced_alloy_furnace.face_mode.disabled": "禁止输入输出", + "gui.useless_mod.advanced_alloy_furnace.face_mode.material_input": "输入", + "gui.useless_mod.advanced_alloy_furnace.face_mode.material_output": "输出", + "gui.useless_mod.advanced_alloy_furnace.face_mode.material_input_output": "输入和输出", + "gui.useless_mod.advanced_alloy_furnace.face_mode.catalyst_input": "催化剂输入", + "gui.useless_mod.advanced_alloy_furnace.face_mode.mold_input": "模具输入", + "gui.useless_mod.advanced_alloy_furnace.face.top": "顶面", + "gui.useless_mod.advanced_alloy_furnace.face.left": "左面", + "gui.useless_mod.advanced_alloy_furnace.face.front": "前面", + "gui.useless_mod.advanced_alloy_furnace.face.right": "右面", + "gui.useless_mod.advanced_alloy_furnace.face.bottom": "底面", + "gui.useless_mod.advanced_alloy_furnace.face.back": "后面", + "gui.useless_mod.advanced_alloy_furnace.auto_output": "自动输出", + "gui.useless_mod.advanced_alloy_furnace.auto_input": "自动输入", + "gui.useless_mod.advanced_alloy_furnace.redstone_control": "红石控制", + "gui.useless_mod.advanced_alloy_furnace.redstone_control.disabled": "无红石控制", + "gui.useless_mod.advanced_alloy_furnace.redstone_control.high_active": "高电平激活", + "gui.useless_mod.advanced_alloy_furnace.redstone_control.low_active": "低电平激活", + "gui.useless_mod.advanced_alloy_furnace.cancel_ae_tasks": "取消AE合成任务", + "gui.useless_mod.advanced_alloy_furnace.cancel_ae_tasks.desc": "取消所有当前合成任务并返还材料", + "gui.useless_mod.advanced_alloy_furnace.ae_return_output": "产物回AE", + "gui.useless_mod.enabled": "已开启", + "gui.useless_mod.disabled": "已关闭", + "container.useless_mod.advanced_alloy_furnace": "万象合金炉", + "pack.useless_mod.god.name": "无用模组 - 神权杖材质包", - "pack.useless_mod.god.description": "麦淇淋制作的立体神权杖材质包" -} \ No newline at end of file + "pack.useless_mod.god.description": "麦淇淋制作的立体神权杖材质包", + "pack.useless_mod.xia.name": "无用模组 - 虾材质优化包", + "pack.useless_mod.xia.description": "材质来源:ae2&mek丨部分二改 by 虾比事李跌丨$丨无用模组材质优化V2.1", + "gui.useless_mod.chain_mining_status": "连锁: %s", + "gui.useless_mod.force_mining_label": "强制挖掘: ", + "gui.useless_mod.mining_count": "挖掘数量: %s", + "gui.useless_mod.status.enhanced": "增强连锁", + "gui.useless_mod.status.normal": "普通连锁", + "gui.useless_mod.shape_label": "形状: %s(%s)", + "gui.useless_mod.shape_hint": "按住连锁键 + Shift + 滚轮 切换形状", + "gui.useless_mod.shape_switched": "连锁形状: %s(%s)", + "gui.useless_mod.mining_count_limit": "挖掘数量: %s / %s", + "gui.useless_mod.shape.shapeless": "原版连锁", + "gui.useless_mod.shape.shapeless.desc": "沿用相邻扩散扫描,范围与数量由配置决定", + "gui.useless_mod.shape.small_tunnel": "单格隧道", + "gui.useless_mod.shape.small_tunnel.desc": "自点击面向方块内部逐格直线延伸", + "gui.useless_mod.shape.small_square": "3×3 平面", + "gui.useless_mod.shape.small_square.desc": "以点击面所在平面展开单层 3×3", + "gui.useless_mod.shape.large_tunnel": "3×3 隧道", + "gui.useless_mod.shape.large_tunnel.desc": "以点击面为截面,逐层向方块内部推进", + "gui.useless_mod.shape.mining_tunnel": "向下斜隧道", + "gui.useless_mod.shape.mining_tunnel.desc": "自点击面起,每前进一格下降一格", + "gui.useless_mod.shape.escape_tunnel": "向上斜隧道", + "gui.useless_mod.shape.escape_tunnel.desc": "自点击面起,每前进一格上升一格", + "gui.useless_mod.force.enabled": "开启", + "gui.useless_mod.force.disabled": "关闭", + "gui.useless_mod.festive_triggered": "§6战利品大爆发!", + "gui.useless_mod.chain_mining_result": "连锁挖掘: 破坏了 %s 个方块", + "gui.useless_mod.enhanced_chain_mining_result": "增强连锁挖掘: 破坏了 %s 个方块", + "gui.useless_mod.force_chain_mining_result": "强制连锁挖掘: 破坏了 %s 个方块", + "gui.useless_mod.force_enhanced_chain_mining_result": "强制增强连锁挖掘: 破坏了 %s 个方块", + "gui.useless_mod.wireless_access_point_bound": "已成功绑定无线接入点: %s", + "gui.useless_mod.ae2.not_bound": "未绑定无线访问点", + "gui.useless_mod.ae2.chunk_not_loaded": "目标区域未加载", + "gui.useless_mod.ae2.invalid_target": "链接的目标不是有效的无线访问点", + "gui.useless_mod.ae2.offline": "无线访问点已掉线", + "gui.useless_mod.ae_connect.not_bound": "尚未绑定无线访问点(Shift + 右键 ME 无线访问点绑定)", + "gui.useless_mod.ae_connect.access_point": "不能对无线访问点自身使用 AE 连接模式", + "gui.useless_mod.ae_connect.access_point_unloaded": "绑定的无线访问点未加载", + "gui.useless_mod.ae_connect.invalid_access_point": "绑定坐标上的方块已经不是无线访问点", + "gui.useless_mod.ae_connect.access_point_offline": "绑定的无线访问点已离线", + "gui.useless_mod.ae_connect.not_ready": "该方块还没有可用的 AE 网络节点", + "gui.useless_mod.ae_connect.already_networked": "%s 已经接入了别的 AE 网络,先断开再连", + "gui.useless_mod.ae_connect.linked": "%s 已接入工具绑定的 AE 网络", + "gui.useless_mod.ae_connect.unlinked": "%s 已从 AE 网络断开", + "screen.useless_mod.title": "Useless Mod 配置", + "gui.useless_mod.mode_config.title": "牛排工具配置", + "gui.useless_mod.mode_config.tools": "工具", + "gui.useless_mod.mode_config.mining": "挖掘", + "gui.useless_mod.mode_config.combat": "战斗", + "gui.useless_mod.mode_config.vanilla": "原版", + "gui.useless_mod.mode_config.auxiliary": "辅助", + "gui.useless_mod.mode_config.current": "%s(当前)", + "gui.useless_mod.mode_config.state": "%s:%s", + "gui.useless_mod.mode_config.page_name": "页面名称", + "gui.useless_mod.mode_config.group_name": "分组名称", + "gui.useless_mod.mode_config.edit": "编辑布局", + "gui.useless_mod.mode_config.done": "完成", + "gui.useless_mod.mode_config.add_page": "+页面", + "gui.useless_mod.mode_config.delete_page": "删除页", + "gui.useless_mod.mode_config.add_group": "+分组", + "gui.useless_mod.mode_config.export": "导出", + "gui.useless_mod.mode_config.import": "导入", + "gui.useless_mod.mode_config.unassigned": "未分配模块", + "gui.useless_mod.mode_config.empty": "无可用模块", + "gui.useless_mod.mode_config.loading": "正在加载布局...", + "gui.useless_mod.mode_config.exported": "布局已复制到剪贴板", + "gui.useless_mod.mode_config.importing": "正在导入布局...", + "gui.useless_mod.mode_config.import_confirm": "导入将覆盖「轮盘布局」与「连锁等价组」,是否继续?", + "gui.useless_mod.mode_config.confirm": "确认", + "gui.useless_mod.mode_config.cancel": "取消", + "gui.useless_mod.mode_config.error.invalid_text": "布局文本无效", + "gui.useless_mod.mode_config.error.unsupported_version": "不支持的布局版本", + "gui.useless_mod.mode_config.error.invalid_structure": "布局结构无效", + "gui.useless_mod.mode_config.error.invalid_name": "页面或分组名称无效", + "gui.useless_mod.mode_config.error.duplicate_module": "布局中存在重复模块", + "gui.useless_mod.mode_config.error.unknown_module": "布局中存在未知模块", + "gui.useless_mod.mode_config.error.limit": "布局超出允许的大小限制", + "gui.useless_mod.chain_group.title": "连锁等价组", + "gui.useless_mod.chain_group.group_label": "等价组(组内方块互相连锁)", + "gui.useless_mod.chain_group.entry_label": "该组包含的方块", + "gui.useless_mod.chain_group.add_group": "新建组", + "gui.useless_mod.chain_group.remove_group": "删除组", + "gui.useless_mod.chain_group.add_entry": "添加", + "gui.useless_mod.chain_group.entry_hint": "#标签 / ID / *通配", + "gui.useless_mod.chain_group.empty": "还没有等价组,点「新建组」开始", + "gui.useless_mod.chain_group.entry_empty": "这一组还没有方块", + "gui.useless_mod.chain_group.drop_hint": "从 JEI 拖一个方块到这里加进本组", + "gui.useless_mod.chain_group.gear_tooltip": "点击配置连锁等价组", + "gui.useless_mod.mode_config.beef_protect_mode.title": "保护名单模式:开启后杀戮光环与范围伤害会暂时停手,方便你从容地给生物加保护。", + "gui.useless_mod.mode_config.beef_protect_mode.ctrl": "Ctrl + 右键生物:按「种类」加入/移出保护(例如所有村民)", + "gui.useless_mod.mode_config.beef_protect_mode.shift": "Shift + 右键生物:按「个体」加入/移出保护(只保护这一只)", + "gui.useless_mod.mode_config.beef_protect_mode.auto": "你自己的宠物与坐骑始终自动受保护。加完记得关掉本模式。", + "gui.useless_mod.mode_config.wireless_logistics.title": "无线物流:潜行右键容器方块可把它绑定到这把杖的物流网络。", + "gui.useless_mod.mode_config.wireless_logistics.key": "按 %s 打开物流配置界面(仅手持造化杖时生效)。", + "gui.useless_mod.chain_group.error.no_group": "请先选中一个等价组", + "gui.useless_mod.chain_group.error.invalid": "条目格式无效(支持方块ID、#标签、*通配符)", + "gui.useless_mod.chain_group.error.duplicate": "这一组里已经有这个条目了", + "useless_mod.configuration.game_mechanics": "游戏机制设置", + "useless_mod.configuration.botany_pot_growth_multiplier": "植物盆生长倍率", + "useless_mod.configuration.botany_pot_growth_multiplier.comment": "植物盆生长倍率 - 1.0为原版速度,2.0为2倍速度", + "useless_mod.configuration.enable_botany_pot_rendering": "启用植物盆作物渲染", + "useless_mod.configuration.enable_botany_pot_rendering.comment": "是否启用植物盆作物渲染,关闭后可提升性能", + "useless_mod.configuration.matrix_pattern_count": "矩阵样板槽位倍数", + "useless_mod.configuration.matrix_pattern_count.comment": "矩阵样板槽位倍数 - 减少数量时请保持槽位空!否则可能会造成样板丢失", + "useless_mod.configuration.beef_tool": "牛排工具设置", + "useless_mod.configuration.enable_potion_effects": "启用药水效果", + "useless_mod.configuration.enable_potion_effects.comment": "是否启用药水效果", + "useless_mod.configuration.enable_potion_effects.tooltip": "是否启用药水效果", + "useless_mod.configuration.enable_flight_effect": "启用飞行效果", + "useless_mod.configuration.enable_flight_effect.comment": "是否启用飞行效果", + "useless_mod.configuration.enable_flight_effect.tooltip": "是否启用飞行效果", + "useless_mod.configuration.beef_tool_flight_speed": "牛排工具飞行速度", + "useless_mod.configuration.beef_tool_flight_speed.comment": "牛排工具提供的飞行速度", + "useless_mod.configuration.beef_tool_flight_speed.tooltip": "牛排工具提供的飞行速度", + "useless_mod.configuration.chain_mining_range_x": "连锁挖掘X轴范围", + "useless_mod.configuration.chain_mining_range_x.comment": "连锁挖掘的X轴范围半径", + "useless_mod.configuration.chain_mining_range_x.tooltip": "连锁挖掘的X轴范围半径", + "useless_mod.configuration.chain_mining_range_y": "连锁挖掘Y轴范围", + "useless_mod.configuration.chain_mining_range_y.comment": "连锁挖掘的Y轴范围半径", + "useless_mod.configuration.chain_mining_range_y.tooltip": "连锁挖掘的Y轴范围半径", + "useless_mod.configuration.chain_mining_range_z": "连锁挖掘Z轴范围", + "useless_mod.configuration.chain_mining_range_z.comment": "连锁挖掘的Z轴范围半径", + "useless_mod.configuration.chain_mining_range_z.tooltip": "连锁挖掘的Z轴范围半径", + "useless_mod.configuration.beef_aoe_damage_range_x": "范围伤害X轴范围", + "useless_mod.configuration.beef_aoe_damage_range_x.comment": "范围伤害的X轴范围半径", + "useless_mod.configuration.beef_aoe_damage_range_x.tooltip": "范围伤害的X轴范围半径", + "useless_mod.configuration.beef_aoe_damage_range_y": "范围伤害Y轴范围", + "useless_mod.configuration.beef_aoe_damage_range_y.comment": "范围伤害的Y轴范围半径", + "useless_mod.configuration.beef_aoe_damage_range_y.tooltip": "范围伤害的Y轴范围半径", + "useless_mod.configuration.beef_aoe_damage_range_z": "范围伤害Z轴范围", + "useless_mod.configuration.beef_aoe_damage_range_z.comment": "范围伤害的Z轴范围半径", + "useless_mod.configuration.beef_aoe_damage_range_z.tooltip": "范围伤害的Z轴范围半径", + "useless_mod.configuration.beef_aoe_damage_max_targets": "范围伤害最大目标数", + "useless_mod.configuration.beef_aoe_damage_max_targets.comment": "范围伤害单次最多命中的实体数量", + "useless_mod.configuration.beef_aoe_damage_max_targets.tooltip": "范围伤害单次最多命中的实体数量(数值过大可能导致卡顿)", + "useless_mod.configuration.beef_kill_aura_interval": "杀戮光环间隔(tick)", + "useless_mod.configuration.beef_kill_aura_interval.comment": "杀戮光环每隔多少 tick 结算一次(20 tick = 1 秒)", + "useless_mod.configuration.beef_kill_aura_interval.tooltip": "调小 = 更快,但服务端实体查询压力会线性上升", + "useless_mod.configuration.beef_magnet_range_x": "磁力吸附X轴范围", + "useless_mod.configuration.beef_magnet_range_x.comment": "击杀后范围磁力吸附的X轴范围半径", + "useless_mod.configuration.beef_magnet_range_x.tooltip": "击杀后范围磁力吸附的X轴范围半径", + "useless_mod.configuration.beef_magnet_range_y": "磁力吸附Y轴范围", + "useless_mod.configuration.beef_magnet_range_y.comment": "击杀后范围磁力吸附的Y轴范围半径", + "useless_mod.configuration.beef_magnet_range_y.tooltip": "击杀后范围磁力吸附的Y轴范围半径", + "useless_mod.configuration.beef_magnet_range_z": "磁力吸附Z轴范围", + "useless_mod.configuration.beef_magnet_range_z.comment": "击杀后范围磁力吸附的Z轴范围半径", + "useless_mod.configuration.beef_magnet_range_z.tooltip": "击杀后范围磁力吸附的Z轴范围半径", + "useless_mod.configuration.chain_mining_max_blocks": "连锁挖掘最大方块数量", + "useless_mod.configuration.chain_mining_max_blocks.comment": "连锁挖掘的最大方块数量", + "useless_mod.configuration.chain_mining_max_blocks.tooltip": "连锁挖掘的最大方块数量(数值过大可能导致卡死)", + "useless_mod.configuration.fortune_level": "牛排工具时运等级", + "useless_mod.configuration.fortune_level.comment": "牛排工具时运附魔等级", + "useless_mod.configuration.fortune_level.tooltip": "牛排工具时运附魔等级", + "useless_mod.configuration.looting_level": "牛排工具抢夺等级", + "useless_mod.configuration.looting_level.comment": "牛排工具抢夺附魔等级", + "useless_mod.configuration.looting_level.tooltip": "牛排工具抢夺附魔等级", + "useless_mod.configuration.beef_tool_mining_speed": "牛排工具挖掘速度", + "useless_mod.configuration.beef_tool_mining_speed.comment": "牛排工具基础挖掘速度", + "useless_mod.configuration.beef_tool_mining_speed.tooltip": "牛排工具基础挖掘速度", + "useless_mod.configuration.beef_tool_ignores_tool_tier": "造化杖无视工具挖掘等级", + "useless_mod.configuration.beef_tool_ignores_tool_tier.comment": "造化杖是否无视所有工具挖掘等级限制(含其它模组自定义的等级)。开启:可正常挖掘任何需要特定工具等级的方块;关闭:回到下界合金层级的原版语义。useless_mod:beef_tool_tier_locked 标签内的方块始终按等级判定。", + "useless_mod.configuration.beef_tool_ignores_tool_tier.tooltip": "造化杖是否无视所有工具挖掘等级限制", + "useless_mod.configuration.beef_tool_pseudo_hardness": "伪不可破坏方块名义硬度", + "useless_mod.configuration.beef_tool_pseudo_hardness.comment": "造化杖挖掘「伪不可破坏」方块(hardness = -1,如 AllTheModium 的三矿与远古石)时使用的名义硬度。挖掘速度 = beef_tool_mining_speed × 本值。15 约等于挖普通方块的手感;调大 = 更快。", + "useless_mod.configuration.beef_tool_pseudo_hardness.tooltip": "造化杖挖掘伪不可破坏方块时使用的名义硬度", + "useless_mod.configuration.beef_tool_entity_interaction_range": "牛排工具实体触及范围", + "useless_mod.configuration.beef_tool_entity_interaction_range.comment": "牛排工具实体触及范围加成,重启游戏生效", + "useless_mod.configuration.beef_tool_entity_interaction_range.tooltip": "牛排工具实体触及范围加成,重启游戏生效", + "useless_mod.configuration.beef_tool_block_interaction_range": "牛排工具方块触及范围", + "useless_mod.configuration.beef_tool_block_interaction_range.comment": "牛排工具方块触及范围加成,重启游戏生效", + "useless_mod.configuration.beef_tool_block_interaction_range.tooltip": "牛排工具方块触及范围加成,重启游戏生效", + "useless_mod.configuration.beef_short_teleport_range": "造化杖短距闪现距离", + "useless_mod.configuration.beef_short_teleport_range.comment": "造化杖短距闪现的最大距离(格);该值为上限而非保证距离,中途命中方块或缺少可站立空间时落点相应前移", + "useless_mod.configuration.beef_short_teleport_range.tooltip": "造化杖短距闪现的最大距离(格)", + "useless_mod.configuration.beef_construction_wand_build_limit": "建筑手杖普通建造上限", + "useless_mod.configuration.beef_construction_wand_angel_limit": "建筑手杖天使核心上限", + "useless_mod.configuration.beef_construction_wand_destruction_limit": "建筑手杖破坏核心上限", + "useless_mod.configuration.beef_tool_force_mining_blacklist": "牛排工具强制挖掘黑名单", + "useless_mod.configuration.beef_tool_force_mining_blacklist.comment": "不会被牛排工具强制挖掘的方块,支持方块ID、#方块标签和*通配符", + "useless_mod.configuration.beef_tool_force_mining_blacklist.tooltip": "不会被牛排工具强制挖掘的方块,支持方块ID、#方块标签和*通配符", + "useless_mod.configuration.beef_tool_force_kill_blacklist": "牛排工具强制击杀黑名单", + "useless_mod.configuration.beef_tool_force_kill_blacklist.comment": "不会被牛排工具强制击杀的实体ID,多个实体ID用分号分隔", + "useless_mod.configuration.beef_tool_force_kill_blacklist.tooltip": "不会被牛排工具强制击杀的实体ID,多个实体ID用分号分隔", + "useless_mod.configuration.beef_tool_force_kill_non_living_whitelist": "非生物强制击杀白名单", + "useless_mod.configuration.beef_tool_force_kill_non_living_whitelist.comment": "允许牛排工具强制击杀的非生物实体ID,多个实体ID用分号分隔", + "useless_mod.configuration.beef_tool_force_kill_non_living_whitelist.tooltip": "允许牛排工具强制击杀的非生物实体ID,多个实体ID用分号分隔", + "useless_mod.configuration.beef_capture_strip_keys": "生物捕捉:额外丢弃的 NBT 键", + "useless_mod.configuration.beef_capture_strip_keys.comment": "写进刷怪蛋前额外丢弃的 NBT 键路径,格式为 容器.子键;未列出的键一律保留(避免误伤其它整合包)", + "useless_mod.configuration.beef_capture_strip_keys.tooltip": "写进刷怪蛋前额外丢弃的 NBT 键路径,格式为 容器.子键;未列出的键一律保留(避免误伤其它整合包)", + "useless_mod.configuration.beef_capture_strip_attachment_namespaces": "生物捕捉:额外丢弃的附件命名空间", + "useless_mod.configuration.beef_capture_strip_attachment_namespaces.comment": "写进刷怪蛋前额外丢弃的数据附件命名空间;其余(含未知模组)一律保留(避免误伤其它整合包)", + "useless_mod.configuration.beef_capture_strip_attachment_namespaces.tooltip": "写进刷怪蛋前额外丢弃的数据附件命名空间;其余(含未知模组)一律保留(避免误伤其它整合包)", + "useless_mod.configuration.festive_drop_chance": "战利品大爆发概率", + "useless_mod.configuration.festive_drop_chance.tooltip": "战利品大爆发触发概率(百分比,1-100%)", + "useless_mod.configuration.enable_grass_wand_drop": "启用打草掉落造化杖彩蛋", + "useless_mod.configuration.enable_grass_wand_drop.comment": "是否启用打草掉落造化杖彩蛋", + "useless_mod.configuration.enable_grass_wand_drop.tooltip": "是否启用打草掉落造化杖彩蛋", + "useless_mod.configuration.grass_wand_drop_probability": "打草掉落造化杖概率", + "useless_mod.configuration.grass_wand_drop_probability.comment": "打草掉落造化杖的概率,0.00001为十万分之一", + "useless_mod.configuration.grass_wand_drop_probability.tooltip": "打草掉落造化杖的概率,取值范围为0到1,0.00001为十万分之一", + "useless_mod.configuration.custom_potion_effects": "自定义药水效果", + "useless_mod.configuration.custom_potion_effects.comment": "每行一个效果,格式:modid:effect_id,等级。等级从1开始;不存在的效果和无效等级会跳过。", + "useless_mod.configuration.custom_potion_effects.tooltip": "每行一个效果:modid:effect_id,等级。持续时间固定为20000 tick。", + "useless_mod.configuration.mekanism_upgrade": "Mekanism升级设置", + "useless_mod.configuration.time_multiplier": "速度升级增强倍率", + "useless_mod.configuration.time_multiplier.comment": "速度平均每8个升级最终效果乘以一次该倍率", + "useless_mod.configuration.electricity_multiplier": "能量升级节电增强倍率", + "useless_mod.configuration.electricity_multiplier.comment": "节电平均每8个升级最终效果乘以一次该倍率", + "useless_mod.configuration.capacity_multiplier": "能量升级储电增强倍率", + "useless_mod.configuration.capacity_multiplier.comment": "储电平均每8个升级最终效果乘以一次该倍率", + "useless_mod.configuration.max_upgrade": "最大升级数量", + "useless_mod.configuration.max_upgrade.comment": "机器可接受的最大速度/能量升级数量,重启游戏生效", + "useless_mod.configuration.omniversal_multiblock_alloy_furnace": "多方块万象合金炉", + "useless_mod.configuration.omniversal_pattern_upload": "万象样板自动上传", + "useless_mod.configuration.omniversal_pattern_auto_upload": "启用自动上传", + "useless_mod.configuration.omniversal_pattern_auto_upload.comment": "编码产出的万象样板是否自动上传到同一AE网络中的多方块样板总成。关闭后样板保留在编码终端内,由玩家自行取出,与引入自动上传之前的行为一致。", + "useless_mod.configuration.omniversal_pattern_auto_upload.tooltip": "编码产出的万象样板是否自动上传到同一AE网络中的多方块样板总成。关闭后样板保留在编码终端内,由玩家自行取出,与引入自动上传之前的行为一致。", + "useless_mod.configuration.ore_generator": "矿石生成器", + "useless_mod.configuration.pattern_slots": "ME样板总成槽位", + "useless_mod.configuration.pattern_slots.comment": "ME样板总成的可用槽位数量。数值会向下取整为27的整页;降低配置后,超出范围的已有内容仍可在只取恢复页中取出。", + "useless_mod.configuration.pattern_slots.tooltip": "ME样板总成的可用槽位数量。数值会向下取整为27的整页;降低配置后,超出范围的已有内容仍可在只取恢复页中取出。", + "useless_mod.configuration.mold_slots": "万象模具集散中心槽位", + "useless_mod.configuration.mold_slots.comment": "万象模具集散中心的可用槽位数量。数值会向下取整为27的整页;降低配置后,超出范围的已有内容仍可在只取恢复页中取出。", + "useless_mod.configuration.mold_slots.tooltip": "万象模具集散中心的可用槽位数量。数值会向下取整为27的整页;降低配置后,超出范围的已有内容仍可在只取恢复页中取出。", + "useless_mod.configuration.passive_pattern_slots": "被动合成仓样板槽位", + "useless_mod.configuration.passive_pattern_slots.comment": "被动合成仓的最大样板槽位。10级线圈解锁全部配置槽位,低等级解锁 ceil(槽位数 x 等级 / 10);降低配置后,超出范围的样板仍可在只取恢复槽位中取出。", + "useless_mod.configuration.passive_pattern_slots.tooltip": "被动合成仓的最大样板槽位。10级线圈解锁全部配置槽位,低等级解锁 ceil(槽位数 x 等级 / 10);降低配置后,超出范围的样板仍可在只取恢复槽位中取出。", + "useless_mod.configuration.decode_cache_capacity": "万象样板解码缓存容量", + "useless_mod.configuration.decode_cache_capacity.comment": "每个世界保留的已解码万象样板最大数量,重启后生效,范围为64至16384。", + "useless_mod.configuration.decode_cache_capacity.tooltip": "每个世界保留的已解码万象样板最大数量,重启后生效,范围为64至16384。", + "useless_mod.configuration.advanced_alloy_furnace": "万象炉设置", + "useless_mod.configuration.advanced_alloy_furnace.recipe_tier_rules": "万象炉配方等级限制", + "useless_mod.configuration.advanced_alloy_furnace.recipe_tier_rules.comment": "每行格式:配方ID通配符,等级。支持*通配符;等级范围为0-10;未匹配配方不受限制。", + "useless_mod.configuration.advanced_alloy_furnace.recipe_tier_rules.tooltip": "每行格式:配方ID通配符,等级。精确ID优先于通配符,相同具体度时后面的规则覆盖前面的规则。", + "useless_mod.configuration.advanced_alloy_furnace.recipe_conversion": "配方转换", + "useless_mod.configuration.advanced_alloy_furnace.recipe_conversion.minecraft": "Minecraft", + "useless_mod.configuration.advanced_alloy_furnace.recipe_conversion.farmers_delight": "Farmer's Delight及其附属", + "useless_mod.configuration.advanced_alloy_furnace.recipe_conversion.kaleidoscope": "森罗物语", + "useless_mod.configuration.advanced_alloy_furnace.recipe_conversion.ae": "Applied Energistics及其附属", + "useless_mod.configuration.advanced_alloy_furnace.recipe_conversion.mekanism": "Mekanism及其附属", + "useless_mod.configuration.advanced_alloy_furnace.recipe_conversion.justdirethings": "Just Dire Things及其附属", + "useless_mod.configuration.advanced_alloy_furnace.recipe_conversion.other": "其他模组", + "useless_mod.configuration.draw_appflux_energy": "自动抽取AppFlux网络储电", + "useless_mod.configuration.draw_appflux_energy.comment": "万象炉是否自动从所在AE网络抽取AppliedFlux(应用通量)存储的FE能量,需要安装AppliedFlux且网络中有通量元件,每tick抽取量受熔炉最大输入速率限制", + "useless_mod.configuration.draw_appflux_energy.tooltip": "万象炉是否自动从所在AE网络抽取AppliedFlux(应用通量)存储的FE能量,需要安装AppliedFlux且网络中有通量元件,每tick抽取量受熔炉最大输入速率限制", + "useless_mod.configuration.draw_ae_energy": "抽取AE网络自身能量", + "useless_mod.configuration.draw_ae_energy.comment": "万象炉是否直接抽取AE网络自身的能量(按 1 AE = 2 FE 折算)。警告:会与网络中其他设备争抢供电,网络储能不足时可能导致设备频繁掉线。在AppliedFlux抽取之后作为补充", + "useless_mod.configuration.draw_ae_energy.tooltip": "万象炉是否直接抽取AE网络自身的能量(按 1 AE = 2 FE 折算)。警告:会与网络中其他设备争抢供电,网络储能不足时可能导致设备频繁掉线。在AppliedFlux抽取之后作为补充", + "useless_mod.configuration.ae_batch_ripe_ticks": "AE批次成熟等待(tick)", + "useless_mod.configuration.ae_batch_ripe_ticks.comment": "万象炉收到AE推送的批次后先等待多少个tick再投入执行。这个窗口用于把连续推送合并成一个任务,等待期间GUI显示为「排队」。设为0表示推送即刻投入执行;机器完全空闲(无任何运行中任务)时会跳过该窗口", + "useless_mod.configuration.ae_batch_ripe_ticks.tooltip": "万象炉收到AE推送的批次后先等待多少个tick再投入执行。这个窗口用于把连续推送合并成一个任务,等待期间GUI显示为「排队」。设为0表示推送即刻投入执行;机器完全空闲(无任何运行中任务)时会跳过该窗口", + "useless_mod.configuration.ae_output_return_budget_millis": "万象炉产物回网时间预算(毫秒)", + "useless_mod.configuration.ae_output_return_budget_millis.comment": "万象炉每tick最多花多少毫秒把产物写回ME网络。它直接决定可持续合成速度:AE2存储接口单次只能写一个long分段,产物必须逐段插入,所以每tick能插多少次就决定了能跑多快。调大=合成更快但单tick更重(可能掉TPS);调小=更省tick但合成变慢。机器或服务端过载时本预算还会被全局降频系数按比例收窄(下限250微秒)", + "useless_mod.configuration.ae_output_return_budget_millis.tooltip": "万象炉每tick最多花多少毫秒把产物写回ME网络。它直接决定可持续合成速度:AE2存储接口单次只能写一个long分段,产物必须逐段插入,所以每tick能插多少次就决定了能跑多快。调大=合成更快但单tick更重(可能掉TPS);调小=更省tick但合成变慢。机器或服务端过载时本预算还会被全局降频系数按比例收窄(下限250微秒)", + "useless_mod.configuration.ae_unlimited_bigint_parallelism": "万象炉单批并行上限解除", + "useless_mod.configuration.ae_unlimited_bigint_parallelism.comment": "开启后解除「材料窗口」对单批规模的限制(即去掉 线程数×Long.MAX 这道闸)。单批规模将只由产物交付能力决定,可以一次吃下任意大的份数,不必再靠堆线程数抬高上限。代价:单次准入可能吃下极大量材料(由调用方自己的BigInteger账本扣除);低档线圈会转而受能量闸限制,有用线圈无能量闸、不受影响", + "useless_mod.configuration.ae_unlimited_bigint_parallelism.tooltip": "开启后解除「材料窗口」对单批规模的限制(即去掉 线程数×Long.MAX 这道闸)。单批规模将只由产物交付能力决定,可以一次吃下任意大的份数,不必再靠堆线程数抬高上限。代价:单次准入可能吃下极大量材料(由调用方自己的BigInteger账本扣除);低档线圈会转而受能量闸限制,有用线圈无能量闸、不受影响", + "useless_mod.configuration.stafflink": "无线物流", + "useless_mod.configuration.stafflink_idle_backoff_enabled": "空转指数退避", + "useless_mod.configuration.stafflink_idle_backoff_enabled.comment": "无线物流:空转的线路是否启用指数退避。开启(默认):某一轮搬运量为0时把下一轮间隔逐次翻倍,最多放大到「周期×退避倍率上限」,搬动一次立即恢复。这样可以避免大量空通道每tick白跑一遍。关闭:无论搬动与否都严格按线路配置的周期运行,搬运响应最及时,代价是空通道持续产生开销", + "useless_mod.configuration.stafflink_idle_backoff_enabled.tooltip": "无线物流:空转的线路是否启用指数退避", + "useless_mod.configuration.stafflink_idle_backoff_cap": "空转退避倍率上限", + "useless_mod.configuration.stafflink_idle_backoff_cap.comment": "无线物流:空转退避的倍率上限(下一轮间隔最多放大到 配置周期×本值)。建议取2的幂(1/2/4/8/16/32/64),非2的幂会向下取整。设为1表示不放大(等同关闭退避)。默认32折合约32分钟封顶。仅在「空转指数退避」开启时生效", + "useless_mod.configuration.stafflink_idle_backoff_cap.tooltip": "无线物流:空转退避的倍率上限(下一轮间隔最多放大到 配置周期×本值)", + "useless_mod.configuration.enable_crafting_recipe_conversion": "启用工作台配方转换", + "useless_mod.configuration.enable_crafting_recipe_conversion.comment": "是否启用工作台配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_crafting_recipe_conversion.tooltip": "是否启用工作台配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_smelting_recipe_conversion": "启用原版熔炉配方转换", + "useless_mod.configuration.enable_smelting_recipe_conversion.comment": "是否启用原版熔炉配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_smelting_recipe_conversion.tooltip": "是否启用原版熔炉配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_brewing_recipe_conversion": "启用原版炼药台配方转换", + "useless_mod.configuration.enable_brewing_recipe_conversion.comment": "是否启用原版炼药台配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_brewing_recipe_conversion.tooltip": "是否启用原版炼药台配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_fishing_recipe_conversion": "启用原版钓鱼配方转换", + "useless_mod.configuration.enable_fishing_recipe_conversion.comment": "是否启用原版钓鱼战利品表配方转换(动态读取,不含附魔书与装备),修改后重启游戏生效", + "useless_mod.configuration.enable_fishing_recipe_conversion.tooltip": "是否启用原版钓鱼战利品表配方转换(动态读取,不含附魔书与装备),修改后重启游戏生效", + "useless_mod.configuration.enable_villager_trade_recipe_conversion": "启用原版村民交易配方转换", + "useless_mod.configuration.enable_villager_trade_recipe_conversion.comment": "是否启用原版村民交易配方转换,使用基础绿宝石消耗的最低值,修改后重启游戏生效", + "useless_mod.configuration.enable_villager_trade_recipe_conversion.tooltip": "是否启用原版村民交易配方转换,使用基础绿宝石消耗的最低值,修改后重启游戏生效", + "useless_mod.configuration.enable_extendedae_recipe_conversion": "启用ExtendedAE配方转换", + "useless_mod.configuration.enable_extendedae_recipe_conversion.comment": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_extendedae_recipe_conversion.tooltip": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_advanced_ae_recipe_conversion": "启用Advanced AE配方转换", + "useless_mod.configuration.enable_advanced_ae_recipe_conversion.comment": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_advanced_ae_recipe_conversion.tooltip": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_mekanism_recipe_conversion": "启用Mekanism配方转换", + "useless_mod.configuration.enable_mekanism_recipe_conversion.comment": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_mekanism_recipe_conversion.tooltip": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_mekanism_generators_recipe_conversion": "启用Mekanism Generators配方转换", + "useless_mod.configuration.enable_mekanism_generators_recipe_conversion.comment": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_mekanism_generators_recipe_conversion.tooltip": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_appmek_recipe_conversion": "启用AppMek配方转换", + "useless_mod.configuration.enable_appmek_recipe_conversion.comment": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_appmek_recipe_conversion.tooltip": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_ae2_recipe_conversion": "启用AE2配方转换", + "useless_mod.configuration.enable_ae2_recipe_conversion.comment": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_ae2_recipe_conversion.tooltip": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_ae2cs_recipe_conversion": "启用AE2 Crystal Science配方转换", + "useless_mod.configuration.enable_ae2cs_recipe_conversion.comment": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_ae2cs_recipe_conversion.tooltip": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_industrial_foregoing_recipe_conversion": "启用Industrial Foregoing配方转换", + "useless_mod.configuration.enable_industrial_foregoing_recipe_conversion.comment": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_industrial_foregoing_recipe_conversion.tooltip": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_actually_additions_recipe_conversion": "启用Actually Additions配方转换", + "useless_mod.configuration.enable_actually_additions_recipe_conversion.comment": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_actually_additions_recipe_conversion.tooltip": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_ars_nouveau_recipe_conversion": "启用Ars Nouveau配方转换", + "useless_mod.configuration.enable_ars_nouveau_recipe_conversion.comment": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_ars_nouveau_recipe_conversion.tooltip": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_mystical_agriculture_recipe_conversion": "启用Mystical Agriculture配方转换", + "useless_mod.configuration.enable_mystical_agriculture_recipe_conversion.comment": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_mystical_agriculture_recipe_conversion.tooltip": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_ae2lt_recipe_conversion": "启用AE2 Lightning Tech配方转换", + "useless_mod.configuration.enable_ae2lt_recipe_conversion.comment": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_ae2lt_recipe_conversion.tooltip": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_data_energistics_recipe_conversion": "启用Data Energistics配方转换", + "useless_mod.configuration.enable_data_energistics_recipe_conversion.comment": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_data_energistics_recipe_conversion.tooltip": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_productive_bees_recipe_conversion": "启用Productive Bees配方转换", + "useless_mod.configuration.enable_productive_bees_recipe_conversion.comment": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_productive_bees_recipe_conversion.tooltip": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_draconic_evolution_recipe_conversion": "启用Draconic Evolution配方转换", + "useless_mod.configuration.enable_draconic_evolution_recipe_conversion.comment": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_draconic_evolution_recipe_conversion.tooltip": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_powah_recipe_conversion": "启用Powah配方转换", + "useless_mod.configuration.enable_powah_recipe_conversion.comment": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_powah_recipe_conversion.tooltip": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_extended_crafting_recipe_conversion": "启用Extended Crafting配方转换", + "useless_mod.configuration.enable_extended_crafting_recipe_conversion.comment": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_extended_crafting_recipe_conversion.tooltip": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_avaritia_recipe_conversion": "启用Re-Avaritia配方转换", + "useless_mod.configuration.enable_avaritia_recipe_conversion.comment": "是否启用Re-Avaritia合成、终焉锻造、中子压缩和中子态素收集器配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_avaritia_recipe_conversion.tooltip": "是否启用Re-Avaritia合成、终焉锻造、中子压缩和中子态素收集器配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_neo_eco_ae_recipe_conversion": "启用NeoECOAE配方转换", + "useless_mod.configuration.enable_neo_eco_ae_recipe_conversion.comment": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_neo_eco_ae_recipe_conversion.tooltip": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_natures_aura_recipe_conversion": "启用Nature's Aura配方转换", + "useless_mod.configuration.enable_natures_aura_recipe_conversion.comment": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_natures_aura_recipe_conversion.tooltip": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_forbidden_arcanus_recipe_conversion": "启用Forbidden Arcanus配方转换", + "useless_mod.configuration.enable_forbidden_arcanus_recipe_conversion.comment": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_forbidden_arcanus_recipe_conversion.tooltip": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_occultism_recipe_conversion": "启用Occultism配方转换", + "useless_mod.configuration.enable_occultism_recipe_conversion.comment": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_occultism_recipe_conversion.tooltip": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_summoningrituals_recipe_conversion": "启用Summoning Rituals配方转换", + "useless_mod.configuration.enable_summoningrituals_recipe_conversion.comment": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_summoningrituals_recipe_conversion.tooltip": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_malum_recipe_conversion": "启用Malum配方转换", + "useless_mod.configuration.enable_malum_recipe_conversion.comment": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_malum_recipe_conversion.tooltip": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_ender_io_recipe_conversion": "启用Ender IO配方转换", + "useless_mod.configuration.enable_ender_io_recipe_conversion.comment": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_ender_io_recipe_conversion.tooltip": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_create_recipe_conversion": "启用Create配方转换", + "useless_mod.configuration.enable_create_recipe_conversion.comment": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_create_recipe_conversion.tooltip": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_astralsorcery_recipe_conversion": "启用星辉魔法配方转换", + "useless_mod.configuration.enable_astralsorcery_recipe_conversion.comment": "是否启用星辉魔法的星能注入、聚星缸与祭坛合成配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_astralsorcery_recipe_conversion.tooltip": "是否启用星辉魔法的星能注入、聚星缸与祭坛合成配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_botany_pots_recipe_conversion": "启用植物盆配方转换", + "useless_mod.configuration.enable_botany_pots_recipe_conversion.comment": "是否启用植物盆作物生长配方的转换,输入水,模具为植物盆与对应作物,修改后重启游戏生效", + "useless_mod.configuration.enable_botany_pots_recipe_conversion.tooltip": "是否启用植物盆作物生长配方的转换,输入水,模具为植物盆与对应作物,修改后重启游戏生效", + "useless_mod.configuration.enable_oritech_recipe_conversion": "启用Oritech配方转换", + "useless_mod.configuration.enable_oritech_recipe_conversion.comment": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_oritech_recipe_conversion.tooltip": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_neovitae_recipe_conversion": "启用Neo Vitae配方转换", + "useless_mod.configuration.enable_neovitae_recipe_conversion.comment": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_neovitae_recipe_conversion.tooltip": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_ufo_recipe_conversion": "启用UFO Future配方转换", + "useless_mod.configuration.enable_ufo_recipe_conversion.comment": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_ufo_recipe_conversion.tooltip": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_modern_industrialization_recipe_conversion": "启用Modern Industrialization配方转换", + "useless_mod.configuration.enable_modern_industrialization_recipe_conversion.comment": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_modern_industrialization_recipe_conversion.tooltip": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_immersiveengineering_recipe_conversion": "启用Immersive Engineering配方转换", + "useless_mod.configuration.enable_immersiveengineering_recipe_conversion.comment": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_immersiveengineering_recipe_conversion.tooltip": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_pneumaticcraft_recipe_conversion": "启用PneumaticCraft配方转换", + "useless_mod.configuration.enable_pneumaticcraft_recipe_conversion.comment": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_pneumaticcraft_recipe_conversion.tooltip": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_bigreactors_recipe_conversion": "启用Extreme Reactors配方转换", + "useless_mod.configuration.enable_bigreactors_recipe_conversion.comment": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_bigreactors_recipe_conversion.tooltip": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_irons_spellbooks_recipe_conversion": "启用铁魔法炼金锅配方转换", + "useless_mod.configuration.enable_irons_spellbooks_recipe_conversion.comment": "是否启用铁魔法炼金锅配方转换,并将流体转换为对应物品,修改后重启游戏生效", + "useless_mod.configuration.enable_irons_spellbooks_recipe_conversion.tooltip": "是否启用铁魔法炼金锅配方转换,并将流体转换为对应物品,修改后重启游戏生效", + "useless_mod.configuration.enable_hostilenetworks_recipe_conversion": "启用敌对神经网络配方转换", + "useless_mod.configuration.enable_hostilenetworks_recipe_conversion.comment": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_hostilenetworks_recipe_conversion.tooltip": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_apothic_flux_recipe_conversion": "启用Apothic Flux配方转换", + "useless_mod.configuration.enable_apothic_flux_recipe_conversion.comment": "是否启用Apothic Flux聚灵配方及使用通量调整书架的Apothic Enchanting灌注配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_apothic_flux_recipe_conversion.tooltip": "是否启用Apothic Flux聚灵配方及使用通量调整书架的Apothic Enchanting灌注配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_justdirethings_recipe_conversion": "启用Just Dire Things配方转换", + "useless_mod.configuration.enable_justdirethings_recipe_conversion.comment": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_justdirethings_recipe_conversion.tooltip": "是否启用该配方来源的配方转换,修改后重启游戏生效", + "useless_mod.configuration.enable_jdte_recipe_conversion": "启用Just Dire Things Extra配方转换", + "useless_mod.configuration.enable_jdte_recipe_conversion.comment": "是否启用JDTE附属的配方转换;关闭Just Dire Things本体转换时该附属一并停用,修改后重启游戏生效", + "useless_mod.configuration.enable_jdte_recipe_conversion.tooltip": "是否启用JDTE附属的配方转换;关闭Just Dire Things本体转换时该附属一并停用,修改后重启游戏生效", + "useless_mod.configuration.enable_exdeorum_recipe_conversion": "启用Ex Deorum配方转换", + "useless_mod.configuration.enable_exdeorum_recipe_conversion.comment": "是否启用Ex Deorum石桶堆肥、桶内物品与流体混合、流体转化及坩埚熔化配方的转换,修改后重启游戏生效", + "useless_mod.configuration.enable_exdeorum_recipe_conversion.tooltip": "是否启用Ex Deorum石桶堆肥、桶内物品与流体混合、流体转化及坩埚熔化配方的转换,修改后重启游戏生效", + "useless_mod.configuration.beef_autoclick_clicks_per_tick": "连点模式触发速率", + "useless_mod.configuration.beef_autoclick_clicks_per_tick.comment": "造化杖连点模式每个客户端 tick 触发多少次右键;调大 = 更快(默认 4 约等于 80 次/秒),调小 = 降低服务端压力", + "useless_mod.configuration.beef_autoclick_clicks_per_tick.tooltip": "造化杖连点模式每个客户端 tick 触发多少次右键;调大 = 更快(默认 4 约等于 80 次/秒),调小 = 降低服务端压力", + "key.category.useless_mod.useless": "无用模组", + "key.useless_mod.switch_silk_touch": "切换到精准采集", + "key.useless_mod.switch_fortune": "切换到时运", + "key.useless_mod.trigger_chain_mining": "连锁挖掘(需要按住)", + "key.useless_mod.toggle_chain_mode": "增强连锁挖掘切换", + "key.useless_mod.force_mining": "强制挖掘模式切换", + "key.useless_mod.switch_mode_wheel": "打开模式选择轮盘", + "key.useless_mod.trigger_force_mining": "触发强制挖掘", + "key.useless_mod.switch_farmland_mode": "切换耕地/草径模式", + "key.useless_mod.toggle_crop_harvest": "顺手收菜开关", + "key.useless_mod.toggle_shears": "剪刀功能开关", + "key.useless_mod.toggle_flint_and_steel": "打火石功能开关", + "key.useless_mod.short_teleport": "短距传送", + "key.useless_mod.toggle_auto_click": "连点模式开关", + "key.useless_mod.toggle_kill_aura": "杀戮光环开关", + "tooltip.useless_mod.festive_affix": "5% 概率在击杀时触发战利品大爆发", + "tooltip.useless_mod.auto_collect": "自带磁力功能", + "tooltip.useless_mod.time_acceleration_mode": "时间加速模式", + "tooltip.useless_mod.time_acceleration_hint": "开启后按住 Shift 右键加速方块和机器;加速生物请改用「生物加速」模式", + "tooltip.useless_mod.beef_teleport_mode": "传送", + "tooltip.useless_mod.beef_aoe_damage_mode": "范围伤害", + "tooltip.useless_mod.beef_magnet_mode": "范围磁力", + "tooltip.useless_mod.beef_kill_aura_mode": "杀戮光环", + "tooltip.useless_mod.beef_kill_aura_hint": "开启后手持造化杖每 20 tick 对「范围伤害」配置范围内生物结算一次攻击伤害;命中后按「范围磁力」登记吸附", + "tooltip.useless_mod.beef_protect_mode": "生物保护名单", + "tooltip.useless_mod.beef_protect_hint": "Ctrl+右键生物按「种类」加入/移出保护;Shift+右键生物按「个体」加入/移出保护(需手持造化杖并开启杀戮光环或范围伤害)。受保护的生物不会被光环、范围伤害与强制击杀命中;你自己的宠物与坐骑始终自动受保护", + "tooltip.useless_mod.beef_protect_count": "已保护 %s 种 / %s 只", + "tooltip.useless_mod.beef_protect_toggle_mode": "保护名单模式", + "tooltip.useless_mod.beef_entity_time_acceleration_mode": "生物加速", + "tooltip.useless_mod.beef_entity_time_acceleration_hint": "开启后潜行右键生物可加速其计时:幼年成长/繁殖后冷却、鸡下蛋间隔、羊剪毛后长毛、村民补货、蜜蜂采蜜、海龟下蛋;装了新生魔艺时还可加速德格米/探宝蟹/风转草的工作速度。只推数值,不改变移动与 AI。与「保护名单」的 Shift 手势互斥(Ctrl 按种类仍可用)", + "tooltip.useless_mod.beef_protect_toggle_hint": "开启「保护名单模式」后杀戮光环与范围伤害会暂时停手,方便从容添加保护名单;加完记得关掉", + "tooltip.useless_mod.beef_farmland_mode": "右键", + "tooltip.useless_mod.beef_farmland_state.hoe": "锄头优先", + "tooltip.useless_mod.beef_farmland_state.shovel": "铲子优先", + "tooltip.useless_mod.beef_farmland_hint": "右键泥土 / 草方块时优先用哪个工具:锄头优先变耕地,铲子优先变草径", + "tooltip.useless_mod.beef_crop_harvest_mode": "顺手收菜", + "tooltip.useless_mod.beef_crop_harvest_hint": "右键成熟作物直接收获、种子留在地里;潜行右键把这次交互让给其它模组", + "tooltip.useless_mod.beef_shears_mode": "剪刀功能", + "tooltip.useless_mod.beef_shears_hint": "开启后右键羊、哞菇等可剪实体直接剪毛/剪掉落,并对外声明剪刀能力", + "tooltip.useless_mod.beef_flint_and_steel_mode": "打火石功能", + "tooltip.useless_mod.beef_flint_and_steel_hint": "开启后右键营火、蜡烛可直接点亮,右键其它方块在点击面放火;潜行右键把这次交互让给其它模组", + "tooltip.useless_mod.beef_ritual_satchel_mode": "匠心仪式挎包", + "tooltip.useless_mod.beef_ritual_satchel_hint": "开启后右键魔典预览的仪式多方块,优先用背包里的方块,不足时从绑定的 AE 网络取,一次摆出整座仪式", + "gui.useless_mod.ritual_satchel.unknown_multiblock": "这里不是可识别的仪式结构", + "gui.useless_mod.ritual_satchel.no_ae_items": "绑定的 AE 网络里没有可用的方块", + "gui.useless_mod.ritual_satchel.no_matching_item": "背包与 AE 网络里都没有这座仪式需要的方块", + "tooltip.useless_mod.beef_ripen_mode": "催熟模式", + "tooltip.useless_mod.beef_ripen_hint": "开启后右键可一键催熟:作物、树苗、菌类等直接催到成熟,南瓜西瓜茎会结果实,右键幼年动物直接催至成年;甘蔗/仙人掌/竹子等骨粉无效的方块需另开「强制生长」;按住连锁键(Tab)可整片作用;潜行右键把这次交互让给其它模组;对藤蔓可强制蔓延(无视游戏规则与数量上限),对花可产出掉落物(走 AE 存储优先/范围磁力)且花本体保留", + "tooltip.useless_mod.beef_force_grow_mode": "强制生长", + "tooltip.useless_mod.beef_force_grow_hint": "开启后催熟会额外用随机刻推进方块,可作用于甘蔗、仙人掌、竹子等骨粉无效的方块;但它不挑方块——树叶枯萎、火蔓延、雪冰融化、耕地退化同样会被推,请自行判断是否开启", + "tooltip.useless_mod.beef_auto_click_mode": "连点模式", + "tooltip.useless_mod.beef_auto_click_hint": "开启后手持造化杖会自动以最快速度重复触发右键(速率见客户端配置);再按一次绑定按键关闭,注意与打火石等功能同时开启时的后果", + "tooltip.useless_mod.beef_teleport_hint": "开启后按住 Shift 右键进行短距离传送;安装 Ender IO 时可传送至旅行锚", + "gui.useless_mod.beef_protect.add_type": "已保护生物种类:%s", + "gui.useless_mod.beef_protect.remove_type": "已取消保护生物种类:%s", + "gui.useless_mod.beef_protect.add_entity": "已保护该生物:%s", + "gui.useless_mod.beef_protect.remove_entity": "已取消保护该生物:%s", + "subtitles.useless_mod.kill_aura_on": "杀戮光环:开启", + "subtitles.useless_mod.kill_aura_off": "杀戮光环:关闭", + "tooltip.useless_mod.current_tool_mode": "当前工具模式", + "tooltip.useless_mod.none_mode": "未选择", + "tooltip.useless_mod.wrench_mode": "扳手", + "tooltip.useless_mod.screwdriver_mode": "螺丝刀", + "tooltip.useless_mod.mallet_mode": "软锤", + "tooltip.useless_mod.crowbar_mode": "撬棍", + "tooltip.useless_mod.hammer_mode": "锻造锤", + "tooltip.useless_mod.omnitool_mode": "全能工具", + "tooltip.useless_mod.disable": "关闭", + "tooltip.useless_mod.enable": "开启", + "tooltip.useless_mod.force_mining_mode": "强制挖掘模式", + "tooltip.useless_mod.ae_storage_priority_mode": "AE存储优先", + "tooltip.useless_mod.ae_network_connect_mode": "AE连接模式", + "tooltip.useless_mod.wrench_tag_mode": "扳手标签", + "tooltip.useless_mod.construction_wand_mode": "建筑手杖", + "tooltip.useless_mod.construction_wand_default_core": "建筑核心", + "tooltip.useless_mod.construction_wand_angel_core": "天使核心", + "tooltip.useless_mod.construction_wand_destruction_core": "破坏核心", + "tooltip.useless_mod.enhanced_chain_mining_mode": "增强连锁挖掘", + "tooltip.useless_mod.auto_smelt_mode": "自动熔炼", + "tooltip.useless_mod.force_kill_enabled_mode": "强制击杀", + "tooltip.useless_mod.beef_malum_spirit_mode": "精魂掉落", + "tooltip.useless_mod.beef_mystical_agriculture_mode": "觉醒粉掉落", + "tooltip.useless_mod.beef_beheading_mode": "斩首", + "tooltip.useless_mod.beef_invulnerability_mode": "玩家保护", + "tooltip.useless_mod.beef_advanced_stealth_mode": "高级隐身", + "tooltip.useless_mod.beef_capture_mode": "生物捕捉", + "tooltip.useless_mod.exdeorum_crook_mode": "钩子", + "tooltip.useless_mod.exdeorum_hammer_mode": "锤子", + "tooltip.useless_mod.exdeorum_compressed_hammer_mode": "压缩锤", + "tooltip.useless_mod.press_shift_for_keys": "按住 Shift 显示按键绑定", + "tooltip.useless_mod.tooltip_page": "第 %s/%s 页 按 %s %s 翻页", + "tooltip.useless_mod.silk_touch_mode": "精准采集", + "tooltip.useless_mod.fortune_mode": "时运", + "tooltip.useless_mod.key.switch_silk_touch": "§d按下 %s 切换到精准采集模式", + "tooltip.useless_mod.key.switch_fortune": "§d按下 %s 切换到时运模式", + "tooltip.useless_mod.key.toggle_chain_mode": "§d按下 %s 切换连锁挖掘(普通 ↔ 增强)", + "tooltip.useless_mod.key.switch_force_mining": "§d按下 %s 切换强制挖掘模式", + "tooltip.useless_mod.key.switch_farmland_mode": "§d按下 %s 切换泥土右键优先(锄头 / 铲子)", + "tooltip.useless_mod.key.toggle_crop_harvest": "§d按下 %s 切换顺手收菜", + "tooltip.useless_mod.key.toggle_shears": "§d按下 %s 切换剪刀功能", + "tooltip.useless_mod.key.toggle_flint_and_steel": "§d按下 %s 切换打火石功能", + "tooltip.useless_mod.key.toggle_auto_click": "§d按下 %s 切换连点模式", + "tooltip.useless_mod.key.toggle_kill_aura": "§d按下 %s 切换杀戮光环", + "tooltip.useless_mod.key.trigger_chain_mining": "§c按住 %s 临时激活连锁挖掘", + "tooltip.useless_mod.key.chain_use": "§c按住 %s 右键操作(耕地/铺路、剥皮/刮铜/去蜡、收菜、催熟)同样连锁", + "tooltip.useless_mod.key.trigger_force_mining": "§c按住 %s 临时激活强制挖掘", + "tooltip.useless_mod.key.open_mode_wheel": "§e按下 %s 打开模式选择轮盘", + "tooltip.useless_mod.furnace_tier": "阶级: %s", + "tooltip.useless_mod.furnace_upgrade_hint": "§e潜行右键§r:1-9阶使用§6无用系列锭§r,之后使用§a有用锭§r升至10阶", + "tooltip.useless_mod.furnace_upgrade_effect": "§7第10阶:§a能量容量与输入速率§7提升至 long 上限", + "tooltip.useless_mod.fast_break_plastic": "Shift + 右键点击快速破坏塑料方块", + "tooltip.useless_mod.ae_storage_priority_bind_hint": "§aShift + 右键§rME无线访问点进行绑定", + "tooltip.useless_mod.ae_network_connect_hint": "§9AE连接模式§r下右键有 AE 节点的机器接入/再右键断开,已连接的会显示边框,且不受通道上限约束", + "jei.useless_mod.catalyst_info": "催化剂与线圈信息", + "jei.useless_mod.tooltip.amount": "数量: %s", + "jei.useless_mod.tooltip.amount.mb": "数量: %s mB", + "jei.useless_mod.tooltip.catalyst": "催化剂", + "tooltip.useless_mod.ae2_gift_package.description": "一路来的努力与汗水终于有了回报。", + "jei.useless_mod.tooltip.catalyst.useless_ingot.title": "无用锭配方说明:", + "jei.useless_mod.tooltip.catalyst.useless_ingot.desc1": "• 合成N阶无用锭需要N-1阶无用锭作为输入", + "jei.useless_mod.tooltip.catalyst.useless_ingot.desc2": "• 并行数为2^N(N为目标阶数)", + "jei.useless_mod.tooltip.catalyst.useless_ingot.desc3": "• 处理时间会随催化剂等级缩短", + "jei.useless_mod.tooltip.catalyst.useless_ingot.warning": "• 合成无用锭时催化剂不会被消耗", + "jei.useless_mod.tooltip.catalyst.normal.desc1": "• 可以使用无用锭提高并行数", + "jei.useless_mod.tooltip.catalyst.normal.desc2": "• 不同等级的无用锭提供不同的并行数", + "jei.useless_mod.tooltip.catalyst.normal.warning": "• 催化剂会被消耗", + "jei.useless_mod.tooltip.catalyst.normal.desc3": "• 催化剂等级越高,处理时间越短", + "jei.useless_mod.tooltip.key_input": "数据输入(AE2特殊数据类型)", + "jei.useless_mod.tooltip.key_output": "数据输出(AE2特殊数据类型)", + "jei.useless_mod.tooltip.energy.base": "基础能量消耗: %s", + "jei.useless_mod.tooltip.energy.actual": "实际能量 = 基础能量 × 并行数", + "jei.useless_mod.tooltip.energy.note_useful": "有用锭催化时能量不倍增", + "jei.useless_mod.tooltip.process_time": "处理时间: %s ticks", + "jei.useless_mod.tooltip.process_time.note": "处理时间受催化剂等级影响", + "jei.useless_mod.tooltip.process_time.catalyst_bonus": "• 一阶催化剂: 90%%时间", + "jei.useless_mod.tooltip.process_time.catalyst_bonus_2": "• 二阶催化剂: 80%%时间", + "jei.useless_mod.tooltip.process_time.useful_ingot": "• 有用锭: 固定1 tick", + "jei.useless_mod.tooltip.process_time.coil_bonus": "• 多方块万象炉:线圈每升一级,处理时间减半", + "jei.useless_mod.tooltip.process_time.coil_useful": "• 多方块万象炉有用线圈:固定1 tick", + "jei.useless_mod.tooltip.parallel.title": "并行数效果说明", + "jei.useless_mod.tooltip.parallel.desc1": "• 输入物品消耗 × 并行数", + "jei.useless_mod.tooltip.parallel.desc2": "• 输出物品数量 × 并行数", + "jei.useless_mod.tooltip.parallel.desc3": "• 能量消耗 × 并行数", + "jei.useless_mod.tooltip.parallel.desc4": "• 处理时间受催化剂等级影响", + "jei.useless_mod.tooltip.parallel.warning": "⚠ 催化剂会被消耗", + "jei.useless_mod.tooltip.catalyst.parallel.unlimited": "并行数: 无上限", + "jei.useless_mod.tooltip.catalyst.effect.title": "效果说明:", + "jei.useless_mod.tooltip.catalyst.effect.not_consumed": "• 催化剂不会被消耗", + "jei.useless_mod.tooltip.catalyst.effect.unlimited": "• 无并行数上限", + "jei.useless_mod.tooltip.catalyst.effect.no_energy_mult": "• 能量消耗不会倍增", + "jei.useless_mod.tooltip.catalyst.effect.time_unchanged": "• 处理时间固定为1 tick", + "jei.useless_mod.tooltip.catalyst.effect.useless_warning": "注意: 合成无用锭时催化剂无效(无用锭配方已内置低一级无用锭作为输入)", + "jei.useless_mod.tooltip.catalyst.normal.parallel": "普通配方并行数: %s倍", + "jei.useless_mod.tooltip.catalyst.normal.speed_bonus": "处理时间: %d0%%", + "jei.useless_mod.tooltip.catalyst.cross_tier.title": "跨阶合成效果:", + "jei.useless_mod.tooltip.catalyst.cross_tier.format": "• 合成%s阶无用锭: %s倍并行", + "jei.useless_mod.tooltip.recipe_id": "配方 ID: %s", + "jei.useless_mod.gui.tier_required": "等级要求: %s(单方块炉/多方块线圈)", + "jei.useless_mod.gui.tier_unrestricted": "无等级要求", + "jei.useless_mod.catalyst_info.title": "催化剂与线圈效果信息", + "jei.useless_mod.catalyst_info.desc1": "• 催化剂为可选项,不会被消耗", + "jei.useless_mod.catalyst_info.desc2": "• 放入催化剂可大幅提高并行数", + "jei.useless_mod.catalyst_info.desc3": "• 并行数影响消耗、产出和能量", + "jei.useless_mod.catalyst_info.desc4": "• 催化剂等级越高,处理时间越短", + "jei.useless_mod.catalyst_info.desc5": "• 一阶90%%时间,二阶80%%时间,依此类推", + "jei.useless_mod.catalyst_info.desc6": "• 有用锭固定1 tick且能量不倍增", + "jei.useless_mod.catalyst_info.desc7": "• 多方块炉线圈:每级剩余50%%处理时间", + "jei.useless_mod.catalyst_info.desc8": "• 多方块炉有用线圈:固定1 tick", + + "config.jade.plugin_useless_mod.advanced_alloy_furnace_tasks": "万象合金炉任务", + "config.jade.plugin_useless_mod.advanced_alloy_furnace_tasks.comment": "显示万象合金炉的AE任务", + "item.useless_mod.ritual_blueprint": "仪式蓝图", + "item.useless_mod.ritual_blueprint.blank": "空白:对已完成的仪式五芒星阵使用", + "item.useless_mod.ritual_blueprint.pentacle": "已刻印五芒星阵:%s", + "item.useless_mod.ritual_blueprint.pentacles": "已刻印五芒星阵(%s个):", + "item.useless_mod.ritual_blueprint.pentacle_entry": " %s", + "item.useless_mod.ritual_blueprint.imprinted": "已为 %s 刻印仪式蓝图", + "item.useless_mod.ritual_blueprint.imprinted_multiple": "已将%s个五芒星阵刻印到仪式蓝图", + "item.useless_mod.ritual_blueprint.no_pentacle": "未找到已完成的仪式五芒星阵", + "item.useless_mod.ritual_blueprint.ambiguous": "找到多个五芒星阵;请在副手持有启动物品" + ,"container.useless_mod.ore_generator": "矿石生成器" + ,"gui.useless_mod.ore_generator.rate": "输出速率" + ,"gui.useless_mod.ore_generator.rate_unit": "每槽 / 20 tick" + ,"gui.useless_mod.ore_generator.apply": "应用" + ,"gui.useless_mod.ore_generator.ae_toggle": "AE 输出" + ,"gui.useless_mod.ore_generator.ae_online": "AE 在线" + ,"gui.useless_mod.ore_generator.ae_offline": "AE 离线" + ,"gui.useless_mod.ore_generator.ae_enabled": "AE 输出:启用" + ,"gui.useless_mod.ore_generator.ae_disabled": "AE 输出:关闭" + ,"gui.useless_mod.ore_generator.countdown": "下次输出:%s tick" + ,"gui.useless_mod.ore_generator.active_slots": "活动槽位:%s / %s" + ,"gui.useless_mod.ore_generator.locked_slot": "锁定的样板槽位" + ,"useless_mod.configuration.ore_generator_slots": "矿石生成器样板槽位" + ,"useless_mod.configuration.ore_generator_slots.comment": "矿石生成器的活动样板槽位数量。降低配置后,超出范围的内容仍可在恢复页取出。" + ,"useless_mod.configuration.ore_generator_slots.tooltip": "矿石生成器的活动样板槽位数量。降低配置后,超出范围的内容仍可在恢复页取出。" + ,"useless_mod.configuration.useless_dimension": "无用维度" + ,"useless_mod.configuration.useless_dimension_floor_block_blacklist": "无用维度地板方块黑名单" + ,"useless_mod.configuration.useless_dimension_floor_block_blacklist.comment": "不能用于边框、填充、层、道路或中心标记的方块。黑名单优先于白名单。支持精确 ID、#方块标签和 * 通配符。" + ,"useless_mod.configuration.useless_dimension_floor_block_blacklist.tooltip": "排除出无用维度层、边界、道路和中心标记的方块,支持精确 ID、#方块标签或 * 通配符。" + ,"useless_mod.configuration.useless_dimension_floor_block_whitelist": "无用维度地板方块白名单" + ,"useless_mod.configuration.useless_dimension_floor_block_whitelist.comment": "非空时只有匹配的方块可以用于边框、填充、层、道路或中心标记。留空可关闭白名单。支持精确 ID、#方块标签和 * 通配符。" + ,"useless_mod.configuration.useless_dimension_floor_block_whitelist.tooltip": "非空时仅允许匹配的方块用于无用维度层、边界、道路和中心标记,黑名单仍然优先。" + ,"menu.useless_mod.dimension_config": "无用维度配置" + ,"gui.useless_mod.dimension_config.border_block": "边框方块" + ,"gui.useless_mod.dimension_config.fill_block": "填充方块" + ,"gui.useless_mod.dimension_config.center_block": "中心方块" + ,"gui.useless_mod.dimension_config.layers": "层数" + ,"gui.useless_mod.dimension_config.start_y": "起始 Y" + ,"gui.useless_mod.dimension_config.boundary_alternate_1": "边界1" + ,"gui.useless_mod.dimension_config.boundary_alternate_2": "边界2" + ,"gui.useless_mod.dimension_config.road_surface": "主体" + ,"gui.useless_mod.dimension_config.road_edge": "边缘" + ,"gui.useless_mod.dimension_config.road_center_marking": "道路中心线" + ,"gui.useless_mod.dimension_config.center_marker": "中心标记" + ,"gui.useless_mod.dimension_config.boundary_interval_x": "边界间隔 X" + ,"gui.useless_mod.dimension_config.boundary_interval_z": "边界间隔 Z" + ,"gui.useless_mod.dimension_config.road_width": "道路宽度" + ,"gui.useless_mod.dimension_config.road_options": "道路选项" + ,"gui.useless_mod.dimension_config.center_options": "中心选项" + ,"gui.useless_mod.dimension_config.mode": "模式" + ,"gui.useless_mod.dimension_config.mode.road": "马路" + ,"gui.useless_mod.dimension_config.mode.multi": "多联" + ,"gui.useless_mod.dimension_config.center_marker_on": "中心标记:开" + ,"gui.useless_mod.dimension_config.center_marker_off": "中心标记:关" + ,"gui.useless_mod.dimension_config.tooltip.decrease": "数值减少 1" + ,"gui.useless_mod.dimension_config.tooltip.increase": "数值增加 1" + ,"gui.useless_mod.dimension_config.tooltip.bedrock": "切换是否生成基岩层" + ,"gui.useless_mod.dimension_config.tooltip.bottom": "切换是否将基岩层放在底部" + ,"gui.useless_mod.dimension_config.tooltip.mode": "切换平台模式:马路 / 多联" + ,"gui.useless_mod.dimension_config.tooltip.center_marker": "切换中心标记" + ,"gui.useless_mod.dimension_config.tooltip.apply": "保存当前维度配置" + ,"gui.useless_mod.dimension_config.tooltip.apply_and_teleport": "保存配置并传送" + ,"gui.useless_mod.dimension_config.tooltip.cancel": "关闭且不保存" + ,"gui.useless_mod.dimension_config.bedrock_on": "生成基岩层:开" + ,"gui.useless_mod.dimension_config.bedrock_off": "生成基岩层:关" + ,"gui.useless_mod.dimension_config.bottom_on": "基岩层置底:开" + ,"gui.useless_mod.dimension_config.bottom_off": "基岩层置底:关" + ,"gui.useless_mod.dimension_config.apply": "应用" + ,"gui.useless_mod.dimension_config.apply_and_teleport": "应用并传送" + ,"gui.useless_mod.dimension_config.cancel": "取消" + ,"gui.useless_mod.dimension_config.invalid": "无效的维度配置" + ,"gui.useless_mod.dimension_config.export": "导出" + ,"gui.useless_mod.dimension_config.import": "导入" + ,"gui.useless_mod.dimension_config.tooltip.export": "把当前配置导出到剪贴板" + ,"gui.useless_mod.dimension_config.tooltip.import": "从剪贴板导入配置" + ,"gui.useless_mod.dimension_config.exported": "配置已复制到剪贴板" + ,"gui.useless_mod.dimension_config.export_invalid": "当前配置不完整,无法导出" + ,"gui.useless_mod.dimension_config.imported": "已导入配置,点击应用后生效" + ,"gui.useless_mod.dimension_config.import_confirm": "导入将替换当前配置,是否继续?" + ,"gui.useless_mod.dimension_config.confirm": "确认" + ,"gui.useless_mod.dimension_config.cancel_import": "取消导入" + ,"gui.useless_mod.dimension_config.error.invalid_text": "剪贴板内容不是有效的配置文本" + ,"gui.useless_mod.dimension_config.error.unsupported_version": "配置版本不受支持" + ,"gui.useless_mod.dimension_config.error.invalid_structure": "配置结构不完整" + ,"gui.useless_mod.dimension_config.error.blocked_block": "配置包含被禁用的方块" + ,"gui.useless_mod.dimension_config.error.limit": "配置文本过长" + ,"useless_mod.configuration.furnace_tier_0_threads": "单方块万象炉 0 阶线程数" + ,"useless_mod.configuration.furnace_tier_1_threads": "单方块万象炉 1 阶线程数" + ,"useless_mod.configuration.furnace_tier_2_threads": "单方块万象炉 2 阶线程数" + ,"useless_mod.configuration.furnace_tier_3_threads": "单方块万象炉 3 阶线程数" + ,"useless_mod.configuration.furnace_tier_4_threads": "单方块万象炉 4 阶线程数" + ,"useless_mod.configuration.furnace_tier_5_threads": "单方块万象炉 5 阶线程数" + ,"useless_mod.configuration.furnace_tier_6_threads": "单方块万象炉 6 阶线程数" + ,"useless_mod.configuration.furnace_tier_7_threads": "单方块万象炉 7 阶线程数" + ,"useless_mod.configuration.furnace_tier_8_threads": "单方块万象炉 8 阶线程数" + ,"useless_mod.configuration.furnace_tier_9_threads": "单方块万象炉 9 阶线程数" + ,"useless_mod.configuration.furnace_tier_10_threads": "单方块万象炉 10 阶线程数" + ,"useless_mod.configuration.catalyst_tier_0_parallel": "催化剂 0 阶并行数" + ,"useless_mod.configuration.catalyst_tier_0_energy_divisor": "催化剂 0 阶能耗除数" + ,"useless_mod.configuration.catalyst_tier_0_time_multiplier": "催化剂 0 阶时间倍率" + ,"useless_mod.configuration.catalyst_tier_1_parallel": "催化剂 1 阶并行数" + ,"useless_mod.configuration.catalyst_tier_1_energy_divisor": "催化剂 1 阶能耗除数" + ,"useless_mod.configuration.catalyst_tier_1_time_multiplier": "催化剂 1 阶时间倍率" + ,"useless_mod.configuration.catalyst_tier_2_parallel": "催化剂 2 阶并行数" + ,"useless_mod.configuration.catalyst_tier_2_energy_divisor": "催化剂 2 阶能耗除数" + ,"useless_mod.configuration.catalyst_tier_2_time_multiplier": "催化剂 2 阶时间倍率" + ,"useless_mod.configuration.catalyst_tier_3_parallel": "催化剂 3 阶并行数" + ,"useless_mod.configuration.catalyst_tier_3_energy_divisor": "催化剂 3 阶能耗除数" + ,"useless_mod.configuration.catalyst_tier_3_time_multiplier": "催化剂 3 阶时间倍率" + ,"useless_mod.configuration.catalyst_tier_4_parallel": "催化剂 4 阶并行数" + ,"useless_mod.configuration.catalyst_tier_4_energy_divisor": "催化剂 4 阶能耗除数" + ,"useless_mod.configuration.catalyst_tier_4_time_multiplier": "催化剂 4 阶时间倍率" + ,"useless_mod.configuration.catalyst_tier_5_parallel": "催化剂 5 阶并行数" + ,"useless_mod.configuration.catalyst_tier_5_energy_divisor": "催化剂 5 阶能耗除数" + ,"useless_mod.configuration.catalyst_tier_5_time_multiplier": "催化剂 5 阶时间倍率" + ,"useless_mod.configuration.catalyst_tier_6_parallel": "催化剂 6 阶并行数" + ,"useless_mod.configuration.catalyst_tier_6_energy_divisor": "催化剂 6 阶能耗除数" + ,"useless_mod.configuration.catalyst_tier_6_time_multiplier": "催化剂 6 阶时间倍率" + ,"useless_mod.configuration.catalyst_tier_7_parallel": "催化剂 7 阶并行数" + ,"useless_mod.configuration.catalyst_tier_7_energy_divisor": "催化剂 7 阶能耗除数" + ,"useless_mod.configuration.catalyst_tier_7_time_multiplier": "催化剂 7 阶时间倍率" + ,"useless_mod.configuration.catalyst_tier_8_parallel": "催化剂 8 阶并行数" + ,"useless_mod.configuration.catalyst_tier_8_energy_divisor": "催化剂 8 阶能耗除数" + ,"useless_mod.configuration.catalyst_tier_8_time_multiplier": "催化剂 8 阶时间倍率" + ,"useless_mod.configuration.catalyst_tier_9_parallel": "催化剂 9 阶并行数" + ,"useless_mod.configuration.catalyst_tier_9_energy_divisor": "催化剂 9 阶能耗除数" + ,"useless_mod.configuration.catalyst_tier_9_time_multiplier": "催化剂 9 阶时间倍率" + ,"useless_mod.configuration.coil_tier_1_threads": "多方块线圈 1 阶线程数" + ,"useless_mod.configuration.coil_tier_1_single_task_parallel": "多方块线圈 1 阶单任务并行数" + ,"useless_mod.configuration.coil_tier_1_energy_divisor": "多方块线圈 1 阶能耗除数" + ,"useless_mod.configuration.coil_tier_1_time_multiplier": "多方块线圈 1 阶时间倍率" + ,"useless_mod.configuration.coil_tier_2_threads": "多方块线圈 2 阶线程数" + ,"useless_mod.configuration.coil_tier_2_single_task_parallel": "多方块线圈 2 阶单任务并行数" + ,"useless_mod.configuration.coil_tier_2_energy_divisor": "多方块线圈 2 阶能耗除数" + ,"useless_mod.configuration.coil_tier_2_time_multiplier": "多方块线圈 2 阶时间倍率" + ,"useless_mod.configuration.coil_tier_3_threads": "多方块线圈 3 阶线程数" + ,"useless_mod.configuration.coil_tier_3_single_task_parallel": "多方块线圈 3 阶单任务并行数" + ,"useless_mod.configuration.coil_tier_3_energy_divisor": "多方块线圈 3 阶能耗除数" + ,"useless_mod.configuration.coil_tier_3_time_multiplier": "多方块线圈 3 阶时间倍率" + ,"useless_mod.configuration.coil_tier_4_threads": "多方块线圈 4 阶线程数" + ,"useless_mod.configuration.coil_tier_4_single_task_parallel": "多方块线圈 4 阶单任务并行数" + ,"useless_mod.configuration.coil_tier_4_energy_divisor": "多方块线圈 4 阶能耗除数" + ,"useless_mod.configuration.coil_tier_4_time_multiplier": "多方块线圈 4 阶时间倍率" + ,"useless_mod.configuration.coil_tier_5_threads": "多方块线圈 5 阶线程数" + ,"useless_mod.configuration.coil_tier_5_single_task_parallel": "多方块线圈 5 阶单任务并行数" + ,"useless_mod.configuration.coil_tier_5_energy_divisor": "多方块线圈 5 阶能耗除数" + ,"useless_mod.configuration.coil_tier_5_time_multiplier": "多方块线圈 5 阶时间倍率" + ,"useless_mod.configuration.coil_tier_6_threads": "多方块线圈 6 阶线程数" + ,"useless_mod.configuration.coil_tier_6_single_task_parallel": "多方块线圈 6 阶单任务并行数" + ,"useless_mod.configuration.coil_tier_6_energy_divisor": "多方块线圈 6 阶能耗除数" + ,"useless_mod.configuration.coil_tier_6_time_multiplier": "多方块线圈 6 阶时间倍率" + ,"useless_mod.configuration.coil_tier_7_threads": "多方块线圈 7 阶线程数" + ,"useless_mod.configuration.coil_tier_7_single_task_parallel": "多方块线圈 7 阶单任务并行数" + ,"useless_mod.configuration.coil_tier_7_energy_divisor": "多方块线圈 7 阶能耗除数" + ,"useless_mod.configuration.coil_tier_7_time_multiplier": "多方块线圈 7 阶时间倍率" + ,"useless_mod.configuration.coil_tier_8_threads": "多方块线圈 8 阶线程数" + ,"useless_mod.configuration.coil_tier_8_single_task_parallel": "多方块线圈 8 阶单任务并行数" + ,"useless_mod.configuration.coil_tier_8_energy_divisor": "多方块线圈 8 阶能耗除数" + ,"useless_mod.configuration.coil_tier_8_time_multiplier": "多方块线圈 8 阶时间倍率" + ,"useless_mod.configuration.coil_tier_9_threads": "多方块线圈 9 阶线程数" + ,"useless_mod.configuration.coil_tier_9_single_task_parallel": "多方块线圈 9 阶单任务并行数" + ,"useless_mod.configuration.coil_tier_9_energy_divisor": "多方块线圈 9 阶能耗除数" + ,"useless_mod.configuration.coil_tier_9_time_multiplier": "多方块线圈 9 阶时间倍率" + ,"useless_mod.configuration.useful_tier_threads": "多方块有用级线程数", + "block.useless_mod.useless_compact_c9": "无用型紧凑C9", + "block.useless_mod.useless_compact_f9": "无用型紧凑F9", + "block.useless_mod.useless_compact_l9": "无用型紧凑L9", + "tooltip.useless_mod.useless_compact_c9": "单方块等价于八联满配 C9:8 台满长高能网络交换主机合并后的极限模式。", + "tooltip.useless_mod.useless_compact_f9": "单方块等价于八联满配 F9:8 台满长高能网络交换主机,88 条 FX 执行通道,虚拟合成。", + "tooltip.useless_mod.useless_compact_l9": "单方块 L9 存储主机,直接运行 ECO 无限存储。", + "gui.useless_mod.compact.c9.title": "紧凑 C9 总览", + "gui.useless_mod.compact.c9.overview": "固定八联计算网络;CPU、线程和存储容量由内建影子核心统一提供。", + "gui.useless_mod.compact.c9.hardware": "内建硬件不可拆卸;潜行右键进入 ECO 通讯接口管理页。", + "gui.useless_mod.compact.f9.title": "紧凑 F9 总览", + "gui.useless_mod.compact.f9.overview": "固定八联虚拟合成网络;88 条 FX 通道和全部样板总线已合并。", + "gui.useless_mod.compact.f9.interaction": "编码样板可直接右键插入;流体容器可填充、取出输出或恢复输入冷却液。", + "gui.useless_mod.compact.f9.pattern_result.already_present": "样板已存在", + "gui.useless_mod.compact.f9.pattern_result.no_space": "没有可用的样板槽位", + "gui.useless_mod.compact.f9.pattern_result.incompatible": "该物品不是可用的 ECO 样板", + "gui.useless_mod.compact.f9.pattern_result.no_target": "紧凑 F9 尚未完成样板总线装配", + "gui.useless_mod.compact.l9.title": "紧凑 L9 总览", + "gui.useless_mod.compact.l9.overview": "固定无限存储域;64 个内建无限组件和域状态由 ECO 管理。", + "gui.useless_mod.compact.l9.interaction": "内建组件只读;潜行右键进入通讯接口的搬入、搬出和过滤设置。", + "gui.useless_mod.dimension_config.preview": "预览", + "gui.useless_mod.dimension_config.preview.top": "俯视", + "gui.useless_mod.dimension_config.preview.side": "剖面", + "gui.useless_mod.dimension_config.preview.period": "周期:%s × %s 格", + "gui.useless_mod.dimension_config.preview.textured": "渲染:真实材质", + "gui.useless_mod.dimension_config.preview.color": "渲染:色块", + "gui.useless_mod.dimension_config.preview.blocks": "渲染:色块(1 像素 = %s 格)", + "gui.useless_mod.dimension_config.preview.range": "Y %s ~ %s,共 %s 层", + "gui.useless_mod.dimension_config.preview.bedrock": "基岩 Y=%s", + "gui.useless_mod.dimension_config.preview.empty": "当前配置无法预览", + "gui.useless_mod.dimension_config.preview.role.road": "道路主体", + "gui.useless_mod.dimension_config.preview.role.road_edge": "道路边缘", + "gui.useless_mod.dimension_config.preview.role.road_marking": "道路中心线", + "gui.useless_mod.dimension_config.preview.role.marker": "中心标记", + "gui.useless_mod.dimension_config.preview.role.boundary": "边界", + "gui.useless_mod.dimension_config.preview.role.border": "边框", + "gui.useless_mod.dimension_config.preview.role.center": "中心", + "gui.useless_mod.dimension_config.preview.role.fill": "填充", + "gui.useless_mod.dimension_config.preview.close": "关闭", + "gui.useless_mod.dimension_config.tooltip.preview": "预览当前配置的生成结果", + "tooltip.useless_mod.beef_wireless_logistics_mode": "无线物流模式", + "key.useless_mod.open_wireless_logistics": "打开无线物流界面", + "menu.useless_mod.wireless_logistics": "无线物流网络", + "gui.useless_mod.wireless_logistics.anchors": "已绑定容器 (%s)", + "gui.useless_mod.wireless_logistics.network_position": "网络 %s/%s", + "gui.useless_mod.wireless_logistics.network_name_hint": "网络名", + "gui.useless_mod.wireless_logistics.network_owned_hint": "网络挂在你的账号名下(组队后由队伍共享),与手持哪把杖无关", + "gui.useless_mod.wireless_logistics.network_new": "新建", + "gui.useless_mod.wireless_logistics.network_switch_hint": "切换网络(手持杖 Shift+滚轮同效)", + "gui.useless_mod.wireless_logistics.network_switched": "已切换到网络 %s/%s:%s", + "gui.useless_mod.wireless_logistics.network_unnamed": "未命名网络", + "gui.useless_mod.wireless_logistics.search_hint": "搜索容器(支持拼音)", + "gui.useless_mod.wireless_logistics.search_no_match": "没有匹配的容器", + "gui.useless_mod.wireless_logistics.empty": "尚未绑定任何容器(潜行右键容器绑定)", + "gui.useless_mod.wireless_logistics.route_label": "线路 %s", + "gui.useless_mod.wireless_logistics.filter": "过滤器", + "gui.useless_mod.wireless_logistics.flow_label": "流向:%s", + "gui.useless_mod.wireless_logistics.medium_label": "资源:%s", + "gui.useless_mod.wireless_logistics.enabled_label": "启用:%s", + "gui.useless_mod.wireless_logistics.side_label": "面:%s", + "gui.useless_mod.wireless_logistics.trigger_label": "红石:%s", + "gui.useless_mod.wireless_logistics.weight_label": "权重", + "gui.useless_mod.wireless_logistics.weight_label_hint": "数值大的输出优先分到东西", + "gui.useless_mod.wireless_logistics.amount_label": "数量", + "gui.useless_mod.wireless_logistics.amount_label_hint": "每个输出各搬多少;一个输入接 N 个输出时这一轮最多搬「数量 × N」", + "gui.useless_mod.wireless_logistics.interval_label": "周期", + "gui.useless_mod.wireless_logistics.interval_label_hint": "两次搬运之间间隔的 tick 数", + "gui.useless_mod.wireless_logistics.release_only_hint": "只有「输出」端发起搬运,输入端的这项不生效", + "gui.useless_mod.wireless_logistics.rename": "容器名", + "gui.useless_mod.wireless_logistics.range": "可填 %s ~ %s", + "gui.useless_mod.wireless_logistics.range_min": "可填 %s 以上,支持 K/M/G/T/P/E", + "gui.useless_mod.wireless_logistics.highlight_hint": "手持造化杖并开启无线物流模式时,网络内容器会自动高亮(暖色=输出 / 冷色=输入 / 灰色=已停用)", + "gui.useless_mod.wireless_logistics.multi_select_hint": "Ctrl+点击多选,Shift+点击选中整段,批量编辑选中的容器", + "gui.useless_mod.wireless_logistics.anchor_order_hint": "行首 ▲▼ 调整次序:权重相同时靠前的先拿(余数也留给靠前的)", + "gui.useless_mod.wireless_logistics.multi_select_active": "已加入批量编辑", + "gui.useless_mod.wireless_logistics.multi_select_count": "已选 %s 台", + "gui.useless_mod.wireless_logistics.config_copied": "已复制当前线路配置", + "gui.useless_mod.wireless_logistics.config_pasted": "已粘贴到当前线路", + "gui.useless_mod.wireless_logistics.config_copy_hint": "线路 %s · Ctrl+C 复制配置 / Ctrl+V 粘贴配置", + "gui.useless_mod.wireless_logistics.select_all": "全选", + "gui.useless_mod.wireless_logistics.clear_selection": "清空", + "gui.useless_mod.wireless_logistics.apply_all": "应用到全部", + "gui.useless_mod.wireless_logistics.apply_all_hint": "把当前线路配置套用到列表里的容器(搜索时只作用于匹配项)", + "gui.useless_mod.wireless_logistics.apply_all_same_name": "应用: 同名容器", + "gui.useless_mod.wireless_logistics.apply_all_same_name_hint": "把当前线路配置套用给所有「界面显示名」与当前容器相同的容器", + "gui.useless_mod.wireless_logistics.filter_button": "过滤器 (%s)", + "gui.useless_mod.wireless_logistics.filter_button_none": "过滤器", + "gui.useless_mod.wireless_logistics.filter_panel_title": "过滤器 —— 已用 %s 格", + "gui.useless_mod.wireless_logistics.filter_panel_hint": "右键格子可打开详细设置", + "gui.useless_mod.wireless_logistics.filter_close": "关闭", + "gui.useless_mod.wireless_logistics.filter_close_hint": "关闭过滤器面板(Esc 也可以)", + "gui.useless_mod.wireless_logistics.filter_clear": "清空", + "gui.useless_mod.wireless_logistics.filter_clear_hint": "清掉这条线路上的全部过滤格", + "gui.useless_mod.wireless_logistics.filter_hint": "物品线路放物品,流体线路放流体(或容器),化学品线路放储罐;可直接从 JEI 拖入", + "gui.useless_mod.wireless_logistics.filter_slot_title": "槽位 %s", + "gui.useless_mod.wireless_logistics.filter_detail_done": "完成", + "gui.useless_mod.wireless_logistics.filter_detail_hint": "右键格子可打开详细设置", + "gui.useless_mod.wireless_logistics.filter_exclude_off": "白名单", + "gui.useless_mod.wireless_logistics.filter_exclude_on": "黑名单", + "gui.useless_mod.wireless_logistics.filter_exclude_hint": "黑名单:标记命中的资源不搬运;白名单:只搬标记命中的", + "gui.useless_mod.wireless_logistics.filter_cond_out_label": "输出端", + "gui.useless_mod.wireless_logistics.filter_cond_in_label": "输入端", + "gui.useless_mod.wireless_logistics.filter_cond_out_hint": "在输出容器上测量", + "gui.useless_mod.wireless_logistics.filter_cond_in_hint": "在输入容器上测量", + "gui.useless_mod.wireless_logistics.filter_cond_op_off": "关", + "gui.useless_mod.wireless_logistics.filter_cond_op_at_least": "≥", + "gui.useless_mod.wireless_logistics.filter_cond_op_at_most": "≤", + "gui.useless_mod.wireless_logistics.filter_cond_self": "自身", + "gui.useless_mod.wireless_logistics.filter_cond_value_hint": "留空 = 关闭;支持 K / M / G / T / P / E", + "gui.useless_mod.wireless_logistics.filter_pattern_hint": "#标签 或通配符,例如 #c:ingots 、 *iron*", + "gui.useless_mod.wireless_logistics.filter_pattern_tag": "按物品 / 流体标签匹配", + "gui.useless_mod.wireless_logistics.filter_pattern_glob": "按注册名 / 显示名匹配;* 任意长,? 一个字符", + "gui.useless_mod.wireless_logistics.filter_pattern_unresolved": "没有资源使用这条标签,一个物品都不会搬", + "gui.useless_mod.wireless_logistics.filter_pattern_unsupported": "化学品不支持标签,请改用通配符", + "gui.useless_mod.wireless_logistics.filter_pattern_invalid": "「%s」不是合法模式:请用 #标签,或带上 * / ? 通配符", + "gui.useless_mod.wireless_logistics.filter_right_click_hint": "右键格子可输入 #标签 或通配符", + "gui.useless_mod.wireless_logistics.ae_whitelist_warning": "AE 输出端 + 无过滤:不会搬出任何物品", + "gui.useless_mod.wireless_logistics.state_on": "开", + "gui.useless_mod.wireless_logistics.state_off": "关", + "gui.useless_mod.wireless_logistics.flow.absorb": "输入", + "gui.useless_mod.wireless_logistics.flow.release": "输出", + "gui.useless_mod.wireless_logistics.medium.item": "物品", + "gui.useless_mod.wireless_logistics.medium.ae_item": "AE 物品", + "gui.useless_mod.wireless_logistics.medium.fluid": "流体", + "gui.useless_mod.wireless_logistics.medium.ae_fluid": "AE 流体", + "gui.useless_mod.wireless_logistics.medium.energy": "能量", + "gui.useless_mod.wireless_logistics.medium.chemical": "化学品", + "gui.useless_mod.wireless_logistics.medium.ae_chemical": "AE 化学品", + "gui.useless_mod.wireless_logistics.medium.source": "魔源", + "gui.useless_mod.wireless_logistics.medium.ae_source": "AE 魔源", + "gui.useless_mod.wireless_logistics.medium.ae_energy": "AE 能量", + "gui.useless_mod.wireless_logistics.medium.stress": "应力", + "gui.useless_mod.wireless_logistics.medium.pressure": "气压", + "gui.useless_mod.wireless_logistics.medium.ae_pressure": "AE 气压", + "gui.useless_mod.wireless_logistics.pressure.target_label": "目标气压", + "gui.useless_mod.wireless_logistics.pressure.target_hint": "输入端被输出到的目标气压(bar)", + "gui.useless_mod.wireless_logistics.pressure.range": "范围:-1.0 ~ 20.0 bar(-1 为绝对真空)", + "gui.useless_mod.thermostat.title": "温度调节器", + "gui.useless_mod.thermostat.temperature": "温度 (K)", + "gui.useless_mod.thermostat.kelvin": "温度 (K)", + "gui.useless_mod.thermostat.celsius": "= %s °C", + "gui.useless_mod.thermostat.hint": "支持 K/M/G 缩写,例如 1M = 1000000 K", + "gui.useless_mod.thermostat.enabled": "恒温:开(点击关闭)", + "gui.useless_mod.thermostat.disabled": "恒温:关(点击开启)", + "gui.useless_mod.thermostat.done": "完成", + "jade.useless_mod.thermostat.temperature": "温度:%s K(%s °C)", + "jade.useless_mod.thermostat.disabled": "恒温:关闭", + "config.jade.plugin_useless_mod.temperature_regulator": "温度调节器", + "config.jade.plugin_useless_mod.temperature_regulator.comment": "显示温度调节器的设定温度", + "gui.useless_mod.wireless_logistics.stress.rpm_label": "转速", + "gui.useless_mod.wireless_logistics.stress.rpm_hint": "目标网络被驱动的旋转速度", + "gui.useless_mod.wireless_logistics.stress.direction_label": "方向", + "gui.useless_mod.wireless_logistics.stress.clockwise": "顺时针", + "gui.useless_mod.wireless_logistics.stress.counter_clockwise": "逆时针", + "gui.useless_mod.wireless_logistics.stress.input_side_hint": "转速与方向配在输入端", + "gui.useless_mod.wireless_logistics.stress.status_breakdown": "自身 %s 网络总计 %s 剩余 %s SU", + "gui.useless_mod.wireless_logistics.stress.status": "状态:%s", + "gui.useless_mod.wireless_logistics.stress.state.ok": "已连接", + "gui.useless_mod.wireless_logistics.stress.state.disabled": "线路已关闭", + "gui.useless_mod.wireless_logistics.stress.state.idle": "源网络没有转动", + "gui.useless_mod.wireless_logistics.stress.state.no_network": "源方块不在任何动力网络里", + "gui.useless_mod.wireless_logistics.stress.state.not_kinetic": "不是动力学方块", + "gui.useless_mod.wireless_logistics.stress.state.missing_io": "缺少输入或输出", + "gui.useless_mod.wireless_logistics.stress.state.no_power": "源网络没有可用的应力", + "gui.useless_mod.wireless_logistics.stress.state.no_share": "分到的应力为 0:富余已被优先级更高的输出分完", + "gui.useless_mod.wireless_logistics.stress.state.fed_elsewhere": "该网络的应力已由其它无线输出补足", + "gui.useless_mod.wireless_logistics.stress.state.self_fed": "该网络自身就带着动力源", + "gui.useless_mod.wireless_logistics.stress.state.real_source": "目标网络已有真实动力源", + "gui.useless_mod.wireless_logistics.stress.state.speed_conflict": "该网络已被另一条输出用别的转速驱动", + "gui.useless_mod.wireless_logistics.stress.state.shared": "该网络已被另一条线路驱动", + "gui.useless_mod.wireless_logistics.stress.chat.real_source": "无线应力已暂停:目标动力网络已有真实动力源。", + "gui.useless_mod.wireless_logistics.stress.chat.speed_conflict": "无线应力已暂停:同一网络上存在冲突的目标转速。", + "gui.useless_mod.wireless_logistics.stress.chat.shared": "无线应力已暂停:该网络已由另一条线路驱动。", + "gui.useless_mod.wireless_logistics.stress.chat.conflict": "无线应力输出因冲突已暂停。", + "gui.useless_mod.wireless_logistics.trigger.always": "忽略红石", + "gui.useless_mod.wireless_logistics.trigger.low": "低电平", + "gui.useless_mod.wireless_logistics.trigger.high": "高电平", + "gui.useless_mod.wireless_logistics.side.null": "所有面", + "gui.useless_mod.wireless_logistics.side.up": "上面", + "gui.useless_mod.wireless_logistics.side.down": "下面", + "gui.useless_mod.wireless_logistics.side.north": "北面", + "gui.useless_mod.wireless_logistics.side.south": "南面", + "gui.useless_mod.wireless_logistics.side.west": "西面", + "gui.useless_mod.wireless_logistics.side.east": "东面", + "gui.useless_mod.wireless_logistics.dissolve": "解散网络", + "gui.useless_mod.wireless_logistics.bound": "已绑定 %s 到无线物流网络", + "gui.useless_mod.wireless_logistics.unbound": "已从无线物流网络解绑 %s", + "gui.useless_mod.wireless_logistics.not_bindable": "该方块没有可搬运的库存", + "gui.useless_mod.omniversal_pattern.uploaded": "万象样板已上传至多方块合金炉:%s,维度:%s(%s 格外)", + "gui.useless_mod.omniversal_pattern.uploaded.tooltip": "点击传送到该多方块合金炉", + "gui.useless_mod.omniversal_pattern.duplicate": "网络中已存在相同的万象样板,本次未上传,已退回空白样板", + "commands.useless_mod.patterns.repair.player_only": "该命令仅玩家可执行:修复需要玩家身上已绑定的无线终端来解析网络", + "commands.useless_mod.patterns.repair.unbound": "背包中未找到已绑定的无线终端,无法确定要修复的 AE 网络", + "commands.useless_mod.patterns.repair.grid_unavailable": "已找到无线终端,但无法解析到 AE 网络(可能超出范围或未连接接入点)", + "commands.useless_mod.patterns.repair.no_machines": "该网络上没有已通电的万象炉或 ME 样板总成,无需修复", + "commands.useless_mod.patterns.repair.summary": "已扫描 %s 张万象样板,发现 %s 张失效;已修复 %s 张,%s 张无法修复(配方已移除或存在歧义)。", + "commands.useless_mod.patterns.repair.summary.preview": "已扫描 %s 张万象样板,发现 %s 张失效;其中 %s 张可修复,%s 张无法修复(配方已移除或存在歧义)。本次为预览,未写回任何槽位。", + "commands.useless_mod.patterns.repair.detail": "无法修复:%s,绑定配方 %s" +} diff --git a/src/main/resources/assets/useless_mod/lang/zh_tw.json b/src/main/resources/assets/useless_mod/lang/zh_tw.json new file mode 100644 index 00000000..9b392edc --- /dev/null +++ b/src/main/resources/assets/useless_mod/lang/zh_tw.json @@ -0,0 +1,1301 @@ +{ + "item.useless_mod.omniversal_pattern_converter": "萬象樣板轉換器", + "tooltip.useless_mod.pattern_converter.unlinked": "未綁定萬象爐或模具倉", + "tooltip.useless_mod.pattern_converter.bind_hint": "Shift+右鍵萬象爐或模具倉以綁定", + "tooltip.useless_mod.pattern_converter.linked": "綁定目標:%s [%s, %s, %s]", + "gui.useless_mod.paged.search": "搜尋", + "gui.useless_mod.paged.search.hint": "按名稱搜尋", + "gui.useless_mod.paged.search.hint.in": "按輸入查找樣板", + "gui.useless_mod.paged.search.hint.out": "按輸出查找樣板", + "gui.useless_mod.paged.search.hint.mold": "按模具查找樣板", + "gui.useless_mod.paged.search.hint.all": "按名稱查找樣板", + "gui.useless_mod.paged.search_mode.in": "輸入", + "gui.useless_mod.paged.search_mode.out": "輸出", + "gui.useless_mod.paged.search_mode.mold": "模具", + "gui.useless_mod.paged.search_mode.all": "全部", + "gui.useless_mod.pattern_converter.convert": "轉換", + "message.useless_mod.pattern_converter.linked": "已綁定:%s", + "message.useless_mod.pattern_converter.binding": "綁定目標:%s [%s, %s, %s]", + "message.useless_mod.pattern_converter.unlinked": "已清除綁定目標", + "message.useless_mod.pattern_converter.unbound": "請先將轉換器綁定到萬象爐或模具倉", + "message.useless_mod.pattern_converter.target_unavailable": "綁定的萬象爐或模具倉不可用", + "message.useless_mod.pattern_converter.no_patterns": "沒有可轉換的樣板", + "message.useless_mod.pattern_converter.invalid_context": "無法在當前環境使用轉換器", + "message.useless_mod.pattern_converter.result": "已轉換 %s 個樣板,跳過 %s 個", + "useless_mod.configuration.ae2_gift_package": "AE2超值大福袋", + "useless_mod.configuration.ae2_gift_package.items": "福袋物品", + "useless_mod.configuration.ae2_gift_package.items.comment": "AE2超值大福袋發放的物品。格式:modid:item_id, 數量。不存在的物品和無效數量會跳過", + "useless_mod.configuration.ae2_gift_package.items.tooltip": "每行一個物品:modid:item_id, 數量。伺服器必須安裝對應模組並註冊該物品。", + "useless_mod.configuration.enable_kaleidoscope_tavern_recipe_conversion": "啟用森羅物語酒館配方轉換", + "useless_mod.configuration.enable_kaleidoscope_tavern_recipe_conversion.comment": "是否啟用森羅物語酒桶、果盆和雪克杯配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_kaleidoscope_tavern_recipe_conversion.tooltip": "是否啟用森羅物語酒桶、果盆和雪克杯配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_kaleidoscope_grilling_recipe_conversion": "啟用森羅物語燒烤配方轉換", + "useless_mod.configuration.enable_kaleidoscope_grilling_recipe_conversion.comment": "是否啟用森羅物語串籤燒烤配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_kaleidoscope_grilling_recipe_conversion.tooltip": "是否啟用森羅物語串籤燒烤配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_farmersdelight_recipe_conversion": "啟用Farmer's Delight配方轉換", + "useless_mod.configuration.enable_farmersdelight_recipe_conversion.comment": "是否啟用Farmer's Delight烹飪锅、砧板配方及其附屬模組配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_farmersdelight_recipe_conversion.tooltip": "是否啟用Farmer's Delight烹飪锅、砧板配方及其附屬模組配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_extradelight_recipe_conversion": "啟用Extra Delight配方轉換", + "useless_mod.configuration.enable_extradelight_recipe_conversion.comment": "是否啟用Extra Delight工作站配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_extradelight_recipe_conversion.tooltip": "是否啟用Extra Delight工作站配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_crabbersdelight_recipe_conversion": "啟用Crabbers Delight配方轉換", + "useless_mod.configuration.enable_crabbersdelight_recipe_conversion.comment": "是否啟用Crabbers Delight捕蟹籠配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_crabbersdelight_recipe_conversion.tooltip": "是否啟用Crabbers Delight捕蟹籠配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_casualnessdelight_recipe_conversion": "啟用Casualness Delight配方轉換", + "useless_mod.configuration.enable_casualnessdelight_recipe_conversion.comment": "是否啟用Casualness Delight深煎鍋配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_casualnessdelight_recipe_conversion.tooltip": "是否啟用Casualness Delight深煎鍋配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_expandeddelight_recipe_conversion": "啟用Expanded Delight配方轉換", + "useless_mod.configuration.enable_expandeddelight_recipe_conversion.comment": "是否啟用Expanded Delight榨汁機配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_expandeddelight_recipe_conversion.tooltip": "是否啟用Expanded Delight榨汁機配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_brewinandchewin_recipe_conversion": "啟用Brewin And Chewin配方轉換", + "useless_mod.configuration.enable_brewinandchewin_recipe_conversion.comment": "是否啟用Brewin And Chewin發酵桶配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_brewinandchewin_recipe_conversion.tooltip": "是否啟用Brewin And Chewin發酵桶配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_nomadsdelight_recipe_conversion": "啟用Nomad's Delight配方轉換", + "useless_mod.configuration.enable_nomadsdelight_recipe_conversion.comment": "是否啟用Nomad's Delight奶油攪拌器和凝乳過濾袋配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_nomadsdelight_recipe_conversion.tooltip": "是否啟用Nomad's Delight奶油攪拌器和凝乳過濾袋配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_ubesdelight_recipe_conversion": "啟用Ube's Delight配方轉換", + "useless_mod.configuration.enable_ubesdelight_recipe_conversion.comment": "是否啟用Ube's Delight烘焙墊配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_ubesdelight_recipe_conversion.tooltip": "是否啟用Ube's Delight烘焙墊配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_barbequesdelight_recipe_conversion": "啟用Barbeque's Delight配方轉換", + "useless_mod.configuration.enable_barbequesdelight_recipe_conversion.comment": "是否啟用Barbeque's Delight燒烤架和串籤配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_barbequesdelight_recipe_conversion.tooltip": "是否啟用Barbeque's Delight燒烤架和串籤配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_youkaishomecoming_recipe_conversion": "啟用Youkai's Homecoming配方轉換", + "useless_mod.configuration.enable_youkaishomecoming_recipe_conversion.comment": "是否啟用Youkai's Homecoming烹飪、晾曬和發酵配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_youkaishomecoming_recipe_conversion.tooltip": "是否啟用Youkai's Homecoming烹飪、晾曬和發酵配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_kaleidoscope_cookery_recipe_conversion": "啟用森羅物語廚藝配方轉換", + "useless_mod.configuration.enable_kaleidoscope_cookery_recipe_conversion.comment": "是否啟用森羅物語廚藝廚房配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_kaleidoscope_cookery_recipe_conversion.tooltip": "是否啟用森羅物語廚藝廚房配方轉換,修改後重啟遊戲生效", + "itemGroup.useless_mod.main": "Useless", + "block.useless_mod.multiblock_alloy_furnace_core": "多方塊萬象合金爐核心", + "config.jade.plugin_useless_mod.multiblock_alloy_furnace_core_name": "顯示多方塊萬象合金爐核心名稱", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.1": "不朽的合成之神,聆聽我們的祈禱。", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.2": "我們是您的孩子,是行走於機械與奧術道路上的工程師。", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.3": "我們珍視配方勝過一切,因為那是您賜予最永恆的禮物。", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.4": "我們渴望全速並行與極致效率的祝福,", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.5": "願自動化永不停息,願每一道工序都在您的秩序中加速運轉。", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.6": "我們受紅石與能量庇護,受線圈與樣板引導,", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.7": "在模具、催化劑與物質流之間,追尋合成的終極答案。", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.8": "我們不辭辛勞,穿行於模組與維度之間,", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.9": "翻閱 JEI 的頁冊,解析散落在整合包中的古老配方。", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.10": "以快取與網路,替我們整理混亂,承載萬千工序。", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.11": "鳴大鐘一次!推動拉桿,接通能量,", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.12": "啟動活塞與泵,讓基礎材料開始奔湧!", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.13": "鳴大鐘兩次!按下按鈕,載入樣板,發動引擎,", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.14": "注入魔力、源質、化學物質與生命之力!", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.15": "鳴大鐘三次!讓線圈升溫,讓結構覺醒,", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.16": "讓並行的洪流貫穿每一條生產鏈!", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.17": "齊聲歌唱,讚美萬象合金爐!", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.18": "科技與魔法在它面前不再彼此隔絕,", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.19": "配方的來源也不再成為阻礙。", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.20": "讚美萬象合金爐,讚美全能的合成聖座!", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.21": "願它的佇列永不堵塞,願我們的快取永不溢出,", + "tooltip.useless_mod.multiblock_alloy_furnace_core.prayer.22": "願我們的自動化永不停轉,願我們的 TPS 永不墜落。", + "block.useless_mod.me_pattern_assembly": "ME 樣板組件", + "block.useless_mod.omniversal_mold_hub": "萬象模具中心", + "block.useless_mod.passive_crafting_hatch": "被動合成倉", + "block.useless_mod.omniversal_furnace_casing": "萬象爐裝飾外殼", + "block.useless_mod.temperature_regulator": "溫度調節器", + "block.useless_mod.useless_coil_tier_1": "一階無用線圈", + "block.useless_mod.useless_coil_tier_2": "二階無用線圈", + "block.useless_mod.useless_coil_tier_3": "三階無用線圈", + "block.useless_mod.useless_coil_tier_4": "四階無用線圈", + "block.useless_mod.useless_coil_tier_5": "五階無用線圈", + "block.useless_mod.useless_coil_tier_6": "六階無用線圈", + "block.useless_mod.useless_coil_tier_7": "七階無用線圈", + "block.useless_mod.useless_coil_tier_8": "八階無用線圈", + "block.useless_mod.useless_coil_tier_9": "九階無用線圈", + "block.useless_mod.useful_coil": "有用線圈", + "death.attack.useless_mod.beef_tool": "%1$s 被牛排工具強制擊殺", + "death.attack.useless_mod.beef_tool.player": "%1$s 被 %2$s 使用牛排工具強制擊殺", + "item.useless_mod.omniversal_pattern": "萬象樣板", + "tooltip.useless_mod.omniversal_pattern.recipe": "綁定配方:%s", + "tooltip.useless_mod.omniversal_pattern.mold": "模具標記:%s", + "tooltip.useless_mod.omniversal_pattern.no_mold": "無需模具", + "tooltip.useless_mod.omniversal_pattern.unknown_mold": "由配方定義的模具", + "gui.useless_mod.multiblock_alloy_furnace.formed": "結構已成型", + "gui.useless_mod.multiblock_alloy_furnace.invalid": "結構無效", + "gui.useless_mod.multiblock_alloy_furnace.invalid_count": "結構無效:%s處錯誤", + "gui.useless_mod.multiblock_alloy_furnace.coil": "線圈層級:%s", + "gui.useless_mod.multiblock_alloy_furnace.redstone": "紅石:%s", + "gui.useless_mod.multiblock_alloy_furnace.tasks": "任務:%s / %s", + "gui.useless_mod.multiblock_alloy_furnace.energy_limit": "FE充能上限", + "gui.useless_mod.multiblock_alloy_furnace.energy_limit.desc": "核心儲能達到該數值後停止從AE/AppliedFlux網路中抽取能量。設為0可進用自動充能;已存入之能量不會被刪去。", + "gui.useless_mod.multiblock_alloy_furnace.energy_limit.units": "支持K/M/G/T/P/E縮寫,最大不超過合金爐的FE能量上限。", + "gui.useless_mod.multiblock_alloy_furnace.preview.correct": "正確", + "gui.useless_mod.multiblock_alloy_furnace.preview.incorrect": "錯誤", + "gui.useless_mod.multiblock_alloy_furnace.preview.unavailable": "區塊未加載", + "gui.useless_mod.multiblock_alloy_furnace.preview.expected": "需要:%s", + "gui.useless_mod.multiblock_alloy_furnace.preview.actual": "當前:%s", + "gui.useless_mod.multiblock_alloy_furnace.preview.position": "位置:%s, %s, %s", + "gui.useless_mod.multiblock_alloy_furnace.preview.casing_with_hatch": "%s(其中一個位置可使用%s)", + "gui.useless_mod.multiblock_alloy_furnace.preview.casing_with_functional_parts": "%s(17個外層位置中須各放置一個%s和%s;可選一個%s。樣板組件和被動合成倉不可共享,模具中心可共享)", + "gui.useless_mod.multiblock_alloy_furnace.preview.coil_any": "%s至%s(所有線圈必須為同一層級)", + "gui.useless_mod.passive_crafting.interval": "合成間隔(tick)", + "gui.useless_mod.passive_crafting.multiplier": "合成倍率", + "gui.useless_mod.passive_crafting.max_multiplier": "最大倍率:%s", + "gui.useless_mod.passive_crafting.countdown": "下次運行:%s tick", + "gui.useless_mod.passive_crafting.apply": "應用", + "gui.useless_mod.passive_crafting.apply_slot": "本槽跟隨全域", + "gui.useless_mod.passive_crafting.apply_all": "全部跟隨全域", + "gui.useless_mod.passive_crafting.slot_global_target": "全域倍率", + "gui.useless_mod.passive_crafting.slot_select_hint": "Ctrl 點擊選取", + "gui.useless_mod.passive_crafting.slot_wheel_hint": "滾輪調整", + "gui.useless_mod.passive_crafting.slot_label": "欄位 %s", + "gui.useless_mod.passive_crafting.slot_label_selected": "編輯欄位 %s", + "gui.useless_mod.passive_crafting.slot_selected_hint": "已選取(Ctrl 再點一次取消)", + "gui.useless_mod.passive_crafting.slot_multiplier_value": "倍率 %s", + "gui.useless_mod.passive_crafting.slot_multiplier_state_custom": "單獨設定", + "gui.useless_mod.passive_crafting.slot_multiplier_state_global": "跟隨全域 %s", + "gui.useless_mod.passive_crafting.slot_multiplier_custom": "批次倍率:%s(單獨設定,全域 %s)", + "gui.useless_mod.passive_crafting.slot_multiplier_global": "批次倍率:%s(跟隨全域)", + "gui.useless_mod.passive_crafting.slot_multiplier_edit_hint": "Ctrl+左鍵 選取欄位後在右側編輯;滾輪快速調整(Shift 加倍/減半)", + "gui.useless_mod.passive_crafting.progress": "進度:%s / %s", + "gui.useless_mod.passive_crafting.connected": "結構已連接", + "gui.useless_mod.passive_crafting.unformed": "結構未成型", + "gui.useless_mod.passive_crafting.active_slots": "可用欄位:%s / %s", + "gui.useless_mod.passive_crafting.locked_slot": "鎖定的恢復欄位", + "gui.useless_mod.passive_crafting.status_detail": "%s:%s", + "gui.useless_mod.passive_crafting.status.empty": "空", + "gui.useless_mod.passive_crafting.status.ready": "就緒", + "gui.useless_mod.passive_crafting.status.running": "運行中", + "gui.useless_mod.passive_crafting.status.paused": "已暫停", + "gui.useless_mod.passive_crafting.status.missing_input": "材料不足", + "gui.useless_mod.passive_crafting.status.missing_mold": "缺少模具", + "gui.useless_mod.passive_crafting.status.ae_offline": "AE 網路斷開", + "gui.useless_mod.passive_crafting.status.invalid_pattern": "無效樣板", + "gui.useless_mod.passive_crafting.status.waiting_output": "等待產物返回", + "gui.useless_mod.multiblock_builder.invalid_core": "該方塊非多方快萬象合金爐核心", + "gui.useless_mod.multiblock_builder.invalid_functional_parts": "功能組鍵重複、已被其他合金爐占用,或無法在不替換方塊實體之情況下補齊功能組鍵", + "gui.useless_mod.multiblock_builder.mixed_coils": "已有線圈必須全為同一層級", + "gui.useless_mod.multiblock_builder.missing_coils": "無任一層級之線圈足以滿足結構構築所需", + "gui.useless_mod.multiblock_builder.no_permission": "你無權於部分結構區域內搭建", + "gui.useless_mod.multiblock_builder.blocked": "結構在 %s 處被阻擋", + "gui.useless_mod.multiblock_builder.missing_materials": "綁定的AE網路與背包內材料總量不足", + "gui.useless_mod.multiblock_builder.commit_failed": "搭建失敗,材料與方塊已退還", + "gui.useless_mod.multiblock_builder.success": "已使用 %s 階線圈搭建萬象合金爐", + "gui.useless_mod.omniversal_pattern.too_many_slots": "配方的原料數量超出終端機之槽位數量", + "biome.useless_mod:useless_biome": "無用生態域", + "dimension.useless_mod.uselessdim": "單數維度", + "dimension.useless_mod.uselessdim2": "偶數維度", + "dimension.useless_mod.uselessdim3": "三維度", + "block.useless_mod.ore_generator_block": "礦物產生器", + "block.useless_mod.teleport_block": "單數維度傳送方塊", + "block.useless_mod.teleport_block_2": "偶數維度傳送方塊", + "block.useless_mod.teleport_block_3": "三維度傳送方塊", + "block.useless_mod.advanced_alloy_furnace_block": "萬象合金爐", + "block.useless_mod.creator_doll_block": "創作者造型玩偶", + "block.useless_mod.supervisor": "監管者", + "block.useless_mod.black_glow_plastic": "黑色螢光塑膠", + "block.useless_mod.red_glow_plastic": "紅色螢光塑膠", + "block.useless_mod.green_glow_plastic": "綠色螢光塑膠", + "block.useless_mod.brown_glow_plastic": "棕色螢光塑膠", + "block.useless_mod.blue_glow_plastic": "藍色螢光塑膠", + "block.useless_mod.purple_glow_plastic": "紫色螢光塑膠", + "block.useless_mod.cyan_glow_plastic": "青色螢光塑膠", + "block.useless_mod.light_gray_glow_plastic": "淺灰色螢光塑膠", + "block.useless_mod.gray_glow_plastic": "灰色螢光塑膠", + "block.useless_mod.pink_glow_plastic": "粉色螢光塑膠", + "block.useless_mod.lime_glow_plastic": "萊姆色螢光塑膠", + "block.useless_mod.yellow_glow_plastic": "黃色螢光塑膠", + "block.useless_mod.light_blue_glow_plastic": "淡藍色螢光塑膠", + "block.useless_mod.magenta_glow_plastic": "紫紅色螢光塑膠", + "block.useless_mod.orange_glow_plastic": "橘色螢光塑膠", + "block.useless_mod.white_glow_plastic": "白色螢光塑膠", + "block.useless_mod.dark_red_glow_plastic": "深紅色螢光塑膠", + "block.useless_mod.aqua_glow_plastic": "水藍色螢光塑膠", + "block.useless_mod.black_plastic": "黑色塑膠", + "block.useless_mod.black_plastic_ctm": "黑色塑膠(連接材質)", + "block.useless_mod.black_glow_plastic_ctm": "黑色螢光塑膠(連接材質)", + "block.useless_mod.red_plastic": "紅色塑膠", + "block.useless_mod.red_plastic_ctm": "紅色塑膠(連接材質)", + "block.useless_mod.red_glow_plastic_ctm": "紅色螢光塑膠(連接材質)", + "block.useless_mod.green_plastic": "綠色塑膠", + "block.useless_mod.green_plastic_ctm": "綠色塑膠(連接材質)", + "block.useless_mod.green_glow_plastic_ctm": "綠色螢光塑膠(連接材質)", + "block.useless_mod.brown_plastic": "棕色塑膠", + "block.useless_mod.brown_plastic_ctm": "棕色塑膠(連接材質)", + "block.useless_mod.brown_glow_plastic_ctm": "棕色螢光塑膠(連接材質)", + "block.useless_mod.blue_plastic": "藍色塑膠", + "block.useless_mod.blue_plastic_ctm": "藍色塑膠(連接材質)", + "block.useless_mod.blue_glow_plastic_ctm": "藍色螢光塑膠(連接材質)", + "block.useless_mod.purple_plastic": "紫色塑膠", + "block.useless_mod.purple_plastic_ctm": "紫色塑膠(連接材質)", + "block.useless_mod.purple_glow_plastic_ctm": "紫色螢光塑膠(連接材質)", + "block.useless_mod.cyan_plastic": "青色塑膠", + "block.useless_mod.cyan_plastic_ctm": "青色塑膠(連接材質)", + "block.useless_mod.cyan_glow_plastic_ctm": "青色螢光塑膠(連接材質)", + "block.useless_mod.light_gray_plastic": "淺灰色塑膠", + "block.useless_mod.light_gray_plastic_ctm": "淺灰色塑膠(連接材質)", + "block.useless_mod.light_gray_glow_plastic_ctm": "淺灰色螢光塑膠(連接材質)", + "block.useless_mod.gray_plastic": "灰色塑膠", + "block.useless_mod.gray_plastic_ctm": "灰色塑膠(連接材質)", + "block.useless_mod.gray_glow_plastic_ctm": "灰色螢光塑膠(連接材質)", + "block.useless_mod.pink_plastic": "粉色塑膠", + "block.useless_mod.pink_plastic_ctm": "粉色塑膠(連接材質)", + "block.useless_mod.pink_glow_plastic_ctm": "粉色螢光塑膠(連接材質)", + "block.useless_mod.lime_plastic": "萊姆色塑膠", + "block.useless_mod.lime_plastic_ctm": "萊姆色塑膠(連接材質)", + "block.useless_mod.lime_glow_plastic_ctm": "萊姆色螢光塑膠(連接材質)", + "block.useless_mod.yellow_plastic": "黄色塑膠", + "block.useless_mod.yellow_plastic_ctm": "黄色塑膠(連接材質)", + "block.useless_mod.yellow_glow_plastic_ctm": "黄色螢光塑膠(連接材質)", + "block.useless_mod.light_blue_plastic": "淡藍色塑膠", + "block.useless_mod.light_blue_plastic_ctm": "淡藍色塑膠(連接材質)", + "block.useless_mod.light_blue_glow_plastic_ctm": "淡藍色螢光塑膠(連接材質)", + "block.useless_mod.magenta_plastic": " 紫紅色塑膠", + "block.useless_mod.magenta_plastic_ctm": " 紫紅色塑膠(連接材質)", + "block.useless_mod.magenta_glow_plastic_ctm": "紫紅色螢光塑膠(連接材質)", + "block.useless_mod.orange_plastic": "橙色塑膠", + "block.useless_mod.orange_plastic_ctm": "橙色塑膠(連接材質)", + "block.useless_mod.orange_glow_plastic_ctm": "橙色螢光塑膠(連接材質)", + "block.useless_mod.white_plastic": "白色塑膠", + "block.useless_mod.white_plastic_ctm": "白色塑膠(連接材質)", + "block.useless_mod.white_glow_plastic_ctm": "白色螢光塑膠(連接材質)", + "block.useless_mod.dark_red_plastic": "深紅色塑膠", + "block.useless_mod.dark_red_plastic_ctm": "深紅色塑膠(連接材質)", + "block.useless_mod.dark_red_glow_plastic_ctm": "深紅色螢光塑膠(連接材質)", + "block.useless_mod.aqua_plastic": "水藍色塑膠", + "block.useless_mod.aqua_plastic_ctm": "水藍色塑膠(連接材質)", + "block.useless_mod.aqua_glow_plastic_ctm": "水藍色螢光塑膠(連接材質)", + "block.useless_mod.useless_glass_tier_1": "一階無用玻璃", + "block.useless_mod.useless_glass_tier_2": "二階無用玻璃", + "block.useless_mod.useless_glass_tier_3": "三階無用玻璃", + "block.useless_mod.useless_glass_tier_4": "四階無用玻璃", + "block.useless_mod.useless_glass_tier_5": "五階無用玻璃", + "block.useless_mod.useless_glass_tier_6": "六階無用玻璃", + "block.useless_mod.useless_glass_tier_7": "七階無用玻璃", + "block.useless_mod.useless_glass_tier_8": "八階無用玻璃", + "block.useless_mod.useless_glass_tier_9": "九階無用玻璃", + "item.useless_mod.endless_beaf_item.silk_touch": "太初洞見之杖(絲綢之觸)", + "item.useless_mod.endless_beaf_item.fortune": "造化垂青之杖(幸運)", + "item.useless_mod.endless_beaf_item": "造化垂青之杖", + "advancements.useless_mod.grass_wand_drop.title": "這回真是打草掉的", + "advancements.useless_mod.grass_wand_drop.description": "從草裡打出了一把造化杖。", + "item.useless_mod.creator_doll_block": "創作者造型玩偶", + "item.useless_mod.supervisor": "監管者", + "item.useless_mod.useless_ingot_tier_1": "一階無用錠", + "item.useless_mod.useless_ingot_tier_2": "二階無用錠", + "item.useless_mod.useless_ingot_tier_3": "三階無用錠", + "item.useless_mod.useless_ingot_tier_4": "四階無用錠", + "item.useless_mod.useless_ingot_tier_5": "五階無用錠", + "item.useless_mod.useless_ingot_tier_6": "六階無用錠", + "item.useless_mod.useless_ingot_tier_7": "七階無用錠", + "item.useless_mod.useless_ingot_tier_8": "八階無用錠", + "item.useless_mod.useless_ingot_tier_9": "九階無用錠", + "item.useless_mod.possible_useful_ingot": "可能有用之錠", + "item.useless_mod.useful_ingot": "有用錠", + "item.useless_mod.metal_mold_plate": "金屬模具(板)", + "item.useless_mod.metal_mold_rod": "金屬模具(棒)", + "item.useless_mod.metal_mold_gear": "金屬模具(齒輪)", + "item.useless_mod.metal_mold_wire": "金屬模具(線)", + "item.useless_mod.metal_mold_block": "金屬模具(方塊)", + "item.useless_mod.useless_gear_tier_1": "一階無用齒輪", + "item.useless_mod.useless_gear_tier_2": "二階無用齒輪", + "item.useless_mod.useless_gear_tier_3": "三階無用齒輪", + "item.useless_mod.useless_gear_tier_4": "四階無用齒輪", + "item.useless_mod.useless_gear_tier_5": "五階無用齒輪", + "item.useless_mod.useless_gear_tier_6": "六階無用齒輪", + "item.useless_mod.useless_gear_tier_7": "七階無用齒輪", + "item.useless_mod.useless_gear_tier_8": "八階無用齒輪", + "item.useless_mod.useless_gear_tier_9": "九階無用齒輪", + "item.useless_mod.ae2_gift_package": "AE大禮包", + "emi.category.useless_mod.advanced_alloy_furnace": "萬象合金爐", + "emi.category.useless_mod.catalyst_info": "催化劑並行資訊", + "gui.useless_mod.advanced_alloy_furnace.energy": "能量: %s / %s FE", + "gui.useless_mod.advanced_alloy_furnace.progress": "進度: %s/%s (%s%%)", + "gui.useless_mod.advanced_alloy_furnace.status": "狀態: %s", + "gui.useless_mod.advanced_alloy_furnace.status.active": "工作中", + "gui.useless_mod.advanced_alloy_furnace.status.idle": "閒置", + "gui.useless_mod.advanced_alloy_furnace.current_parallel": "本次並行數: %s", + "gui.useless_mod.advanced_alloy_furnace.max_parallel": "當前最大並行數: %s", + "gui.useless_mod.advanced_alloy_furnace.ae_tasks": "AE任務: %s", + "gui.useless_mod.advanced_alloy_furnace.ae_progress": "AE進度: %s/%s (%s%%)", + "gui.useless_mod.advanced_alloy_furnace.ae_task_progress": " - %s (×%d): %d/%d (%s%%) [%s]", + "gui.useless_mod.advanced_alloy_furnace.ae_task_progress_ticks": " - %s (×%d): %dt / %dt [%s]", + "gui.useless_mod.advanced_alloy_furnace.ae_task_waiting": " - %s (×%d): %s", + "gui.useless_mod.advanced_alloy_furnace.ae_task_waiting_detail": " - %s (×%d): %s,需要 %s", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.processing": "處理中", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.queued": "排隊中", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_mold": "等待模具/條件", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_structure": "等待多方塊結構", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_mold_hub": "等待萬象模具中心", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_missing_mold": "缺少所需模具", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_tier": "等待萬象爐等級", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_output_mismatch": "樣板輸出不匹配", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_invalid_pattern": "無效合成樣板", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_conditions": "等待運行條件", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_recipe": "等待嚴格匹配配方", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_ambiguous_recipe": "等待配方衝突排除", + "gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_energy": "等待能量", + "gui.useless_mod.advanced_alloy_furnace.ae_task_recipe_not_found": "未嚴格匹配到配方", + "gui.useless_mod.advanced_alloy_furnace.ae_task_recipe_ambiguous": "嚴格匹配到多個配方,等待處理", + "gui.useless_mod.advanced_alloy_furnace.ae_task_waiting_energy_detail": "機器能量不足", + "gui.useless_mod.advanced_alloy_furnace.recipe_priority": "機器配方優先級", + "gui.useless_mod.advanced_alloy_furnace.recipe_priority_up": "上移", + "gui.useless_mod.advanced_alloy_furnace.recipe_priority_down": "下移", + "gui.useless_mod.advanced_alloy_furnace.recipe_priority_reset": "恢復自動排序", + "gui.useless_mod.advanced_alloy_furnace.ae_task_missing_correct_mold": "正確模具", + "gui.useless_mod.advanced_alloy_furnace.view_recipes": "點擊查看配方", + "gui.useless_mod.advanced_alloy_furnace.no_process": "沒有運作中的進程", + "gui.useless_mod.advanced_alloy_furnace.parallel_info": "並行數資訊", + "gui.useless_mod.advanced_alloy_furnace.catalyst": "催化劑: %s (%s倍)", + "gui.useless_mod.advanced_alloy_furnace.parallel_description.title": "並行數說明:", + "gui.useless_mod.advanced_alloy_furnace.parallel_description.1": "• 消耗與產出皆會乘以並行數", + "gui.useless_mod.advanced_alloy_furnace.parallel_description.2": "• 處理時間隨催化劑等級提升而降低", + "gui.useless_mod.advanced_alloy_furnace.parallel_description.3": "• 能量消耗乘以並行數", + "gui.useless_mod.advanced_alloy_furnace.parallel_description.4": "• 催化劑為選填項,可提高並行數", + "gui.useless_mod.advanced_alloy_furnace.catalyst_warning": "催化劑不會被消耗", + "gui.useless_mod.advanced_alloy_furnace.fluid_amount": "%s / %s mB", + "gui.useless_mod.advanced_alloy_furnace.chemical_amount": "%s / %s", + "gui.useless_mod.advanced_alloy_furnace.face_mode.disabled": "停用輸入與輸出", + "gui.useless_mod.advanced_alloy_furnace.face_mode.material_input": "輸入", + "gui.useless_mod.advanced_alloy_furnace.face_mode.material_output": "輸出", + "gui.useless_mod.advanced_alloy_furnace.face_mode.material_input_output": "輸入與輸出", + "gui.useless_mod.advanced_alloy_furnace.face_mode.catalyst_input": "催化劑輸入", + "gui.useless_mod.advanced_alloy_furnace.face_mode.mold_input": "模具輸入", + "gui.useless_mod.advanced_alloy_furnace.face.top": "頂面", + "gui.useless_mod.advanced_alloy_furnace.face.left": "左面", + "gui.useless_mod.advanced_alloy_furnace.face.front": "前面", + "gui.useless_mod.advanced_alloy_furnace.face.right": "右面", + "gui.useless_mod.advanced_alloy_furnace.face.bottom": "底面", + "gui.useless_mod.advanced_alloy_furnace.face.back": "後面", + "gui.useless_mod.advanced_alloy_furnace.auto_output": "自動輸出", + "gui.useless_mod.advanced_alloy_furnace.auto_input": "自動輸入", + "gui.useless_mod.advanced_alloy_furnace.redstone_control": "紅石控制", + "gui.useless_mod.advanced_alloy_furnace.redstone_control.disabled": "無紅石控制", + "gui.useless_mod.advanced_alloy_furnace.redstone_control.high_active": "高能水平啟用", + "gui.useless_mod.advanced_alloy_furnace.redstone_control.low_active": "低能水平啟用", + "gui.useless_mod.advanced_alloy_furnace.cancel_ae_tasks": "取消 AE 合成任務", + "gui.useless_mod.advanced_alloy_furnace.cancel_ae_tasks.desc": "取消當前所有合成任務並返還材料", + "gui.useless_mod.advanced_alloy_furnace.ae_return_output": "產物回傳至 AE", + "gui.useless_mod.enabled": "已開啟", + "gui.useless_mod.disabled": "已關閉", + "container.useless_mod.advanced_alloy_furnace": "萬象合金爐", + "pack.useless_mod.god.name": "無用模組 - 神權杖資源包", + "pack.useless_mod.god.description": "由麥淇淋製作的立體神權杖資源包", + "pack.useless_mod.xia.name": "無用模組 - 蝦材質優化包", + "pack.useless_mod.xia.description": "材質來源:ae2 & mek丨部分二創由 蝦比事李跌 製作丨$丨無用模組材質優化 V2.1", + "gui.useless_mod.chain_mining_status": "連鎖: %s", + "gui.useless_mod.force_mining_label": "強制挖掘: ", + "gui.useless_mod.mining_count": "挖掘數量: %s", + "gui.useless_mod.status.enhanced": "強化連鎖", + "gui.useless_mod.status.normal": "普通連鎖", + "gui.useless_mod.shape_label": "形狀: %s(%s)", + "gui.useless_mod.shape_hint": "按住連鎖鍵 + Shift + 滾輪 切換形狀", + "gui.useless_mod.shape_switched": "連鎖形狀: %s(%s)", + "gui.useless_mod.mining_count_limit": "挖掘數量: %s / %s", + "gui.useless_mod.shape.shapeless": "原版連鎖", + "gui.useless_mod.shape.shapeless.desc": "沿用相鄰擴散掃描,範圍與數量由設定決定", + "gui.useless_mod.shape.small_tunnel": "單格隧道", + "gui.useless_mod.shape.small_tunnel.desc": "自點擊面向方塊內部逐格直線延伸", + "gui.useless_mod.shape.small_square": "3×3 平面", + "gui.useless_mod.shape.small_square.desc": "以點擊面所在平面展開單層 3×3", + "gui.useless_mod.shape.large_tunnel": "3×3 隧道", + "gui.useless_mod.shape.large_tunnel.desc": "以點擊面為截面,逐層向方塊內部推進", + "gui.useless_mod.shape.mining_tunnel": "向下斜隧道", + "gui.useless_mod.shape.mining_tunnel.desc": "自點擊面起,每前進一格下降一格", + "gui.useless_mod.shape.escape_tunnel": "向上斜隧道", + "gui.useless_mod.shape.escape_tunnel.desc": "自點擊面起,每前進一格上升一格", + "gui.useless_mod.force.enabled": "開啟", + "gui.useless_mod.force.disabled": "關閉", + "gui.useless_mod.festive_triggered": "§6戰利品大爆發!", + "gui.useless_mod.chain_mining_result": "連鎖挖掘: 破壞了 %s 個方塊", + "gui.useless_mod.enhanced_chain_mining_result": "強化連鎖挖掘: 破壞了 %s 個方塊", + "gui.useless_mod.force_chain_mining_result": "強制連鎖挖掘: 破壞了 %s 個方塊", + "gui.useless_mod.force_enhanced_chain_mining_result": "強制強化連鎖挖掘: 破壞了 %s 個方塊", + "gui.useless_mod.wireless_access_point_bound": "已成功綁定無線存取點: %s", + "gui.useless_mod.ae2.not_bound": "未綁定無線存取點", + "gui.useless_mod.ae2.chunk_not_loaded": "目標區塊未載入", + "gui.useless_mod.ae2.invalid_target": "連結的目標不是有效的無線存取點", + "gui.useless_mod.ae2.offline": "無線存取點已離線", + "gui.useless_mod.ae_connect.not_bound": "尚未綁定無線存取點(Shift + 右鍵 ME 無線存取點綁定)", + "gui.useless_mod.ae_connect.access_point": "不能對無線存取點自身使用 AE 連接模式", + "gui.useless_mod.ae_connect.access_point_unloaded": "綁定的無線存取點未載入", + "gui.useless_mod.ae_connect.invalid_access_point": "綁定座標上的方塊已不是無線存取點", + "gui.useless_mod.ae_connect.access_point_offline": "綁定的無線存取點已離線", + "gui.useless_mod.ae_connect.not_ready": "該方塊還沒有可用的 AE 網路節點", + "gui.useless_mod.ae_connect.already_networked": "%s 已經接入其他的 AE 網路,請先斷開", + "gui.useless_mod.ae_connect.linked": "%s 已接入工具綁定的 AE 網路", + "gui.useless_mod.ae_connect.unlinked": "%s 已從 AE 網路斷開", + "screen.useless_mod.title": "Useless Mod 配置", + "gui.useless_mod.mode_config.title": "牛排工具配置", + "gui.useless_mod.mode_config.tools": "工具", + "gui.useless_mod.mode_config.mining": "挖掘", + "gui.useless_mod.mode_config.combat": "戰鬥", + "gui.useless_mod.mode_config.auxiliary": "輔助", + "gui.useless_mod.mode_config.current": "%s(目前)", + "gui.useless_mod.mode_config.state": "%s:%s", + "gui.useless_mod.mode_config.page_name": "頁面名稱", + "gui.useless_mod.mode_config.group_name": "分組名稱", + "gui.useless_mod.mode_config.edit": "編輯佈局", + "gui.useless_mod.mode_config.done": "完成", + "gui.useless_mod.mode_config.add_page": "+頁面", + "gui.useless_mod.mode_config.delete_page": "刪除頁", + "gui.useless_mod.mode_config.add_group": "+分組", + "gui.useless_mod.mode_config.export": "匯出", + "gui.useless_mod.mode_config.import": "匯入", + "gui.useless_mod.mode_config.unassigned": "未分配模組", + "gui.useless_mod.mode_config.empty": "沒有可用模組", + "gui.useless_mod.mode_config.loading": "正在載入佈局...", + "gui.useless_mod.mode_config.exported": "佈局已複製到剪貼簿", + "gui.useless_mod.mode_config.importing": "正在匯入佈局...", + "gui.useless_mod.mode_config.import_confirm": "匯入將覆蓋「輪盤佈局」與「連鎖等價組」,是否繼續?", + "gui.useless_mod.mode_config.confirm": "確認", + "gui.useless_mod.mode_config.cancel": "取消", + "gui.useless_mod.mode_config.error.invalid_text": "佈局文字無效", + "gui.useless_mod.mode_config.error.unsupported_version": "不支援的佈局版本", + "gui.useless_mod.mode_config.error.invalid_structure": "佈局結構無效", + "gui.useless_mod.mode_config.error.invalid_name": "頁面或分組名稱無效", + "gui.useless_mod.mode_config.error.duplicate_module": "佈局中存在重複模組", + "gui.useless_mod.mode_config.error.unknown_module": "佈局中存在未知模組", + "gui.useless_mod.mode_config.error.limit": "佈局超出允許的大小限制", + "gui.useless_mod.chain_group.title": "連鎖等價組", + "gui.useless_mod.chain_group.group_label": "等價組(組內方塊互相連鎖)", + "gui.useless_mod.chain_group.entry_label": "該組包含的方塊", + "gui.useless_mod.chain_group.add_group": "新增組", + "gui.useless_mod.chain_group.remove_group": "刪除組", + "gui.useless_mod.chain_group.add_entry": "新增", + "gui.useless_mod.chain_group.entry_hint": "#標籤 / ID / *萬用", + "gui.useless_mod.chain_group.empty": "還沒有等價組,點「新增組」開始", + "gui.useless_mod.chain_group.entry_empty": "這一組還沒有方塊", + "gui.useless_mod.chain_group.drop_hint": "從 JEI 拖一個方塊到這裡加進本組", + "gui.useless_mod.chain_group.gear_tooltip": "點擊設定連鎖等價組", + "gui.useless_mod.mode_config.beef_protect_mode.title": "保護名單模式:開啟後殺戮光環與範圍傷害會暫時停手,方便你從容地給生物加保護。", + "gui.useless_mod.mode_config.beef_protect_mode.ctrl": "Ctrl + 右鍵生物:依「種類」加入/移出保護(例如所有村民)", + "gui.useless_mod.mode_config.beef_protect_mode.shift": "Shift + 右鍵生物:依「個體」加入/移出保護(只保護這一隻)", + "gui.useless_mod.mode_config.beef_protect_mode.auto": "你自己的寵物與坐騎一律自動受保護。加完記得關掉本模式。", + "gui.useless_mod.mode_config.wireless_logistics.title": "無線物流:潛行右鍵容器方塊可把它綁定到這把杖的物流網路。", + "gui.useless_mod.mode_config.wireless_logistics.key": "按 %s 開啟物流設定介面(僅手持造化杖時生效)。", + "gui.useless_mod.chain_group.error.no_group": "請先選中一個等價組", + "gui.useless_mod.chain_group.error.invalid": "條目格式無效(支持方塊ID、#標籤、*萬用字元)", + "gui.useless_mod.chain_group.error.duplicate": "這一組裡已經有這個條目了", + "useless_mod.configuration.game_mechanics": "遊戲機制設置", + "useless_mod.configuration.botany_pot_growth_multiplier": "植物盆栽生長倍率", + "useless_mod.configuration.botany_pot_growth_multiplier.comment": "植物盆栽生長倍率 - 1.0 為原版速度,2.0 為 2 倍速度", + "useless_mod.configuration.enable_botany_pot_rendering": "啟用植物盆栽作物渲染", + "useless_mod.configuration.enable_botany_pot_rendering.comment": "是否啟用植物盆栽作物渲染,關閉後可提升效能", + "useless_mod.configuration.matrix_pattern_count": "矩陣樣板槽位倍數", + "useless_mod.configuration.matrix_pattern_count.comment": "矩陣樣板槽位倍數 - 減少數量時請保持槽位空!否則可能會造成樣板丟失", + "useless_mod.configuration.beef_tool": "牛排工具設置", + "useless_mod.configuration.enable_potion_effects": "啟用藥水效果", + "useless_mod.configuration.enable_potion_effects.comment": "是否啟用藥水效果", + "useless_mod.configuration.enable_potion_effects.tooltip": "是否啟用藥水效果", + "useless_mod.configuration.enable_flight_effect": "啟用飛行效果", + "useless_mod.configuration.enable_flight_effect.comment": "是否啟用飛行效果", + "useless_mod.configuration.enable_flight_effect.tooltip": "是否啟用飛行效果", + "useless_mod.configuration.beef_tool_flight_speed": "牛排工具飛行速度", + "useless_mod.configuration.beef_tool_flight_speed.comment": "牛排工具提供的飛行速度", + "useless_mod.configuration.beef_tool_flight_speed.tooltip": "牛排工具提供的飛行速度", + "useless_mod.configuration.chain_mining_range_x": "連鎖挖掘 X 軸範圍", + "useless_mod.configuration.chain_mining_range_x.comment": "連鎖挖掘的 X 軸範圍半徑", + "useless_mod.configuration.chain_mining_range_x.tooltip": "連鎖挖掘的 X 軸範圍半徑", + "useless_mod.configuration.chain_mining_range_y": "連鎖挖掘 Y 軸範圍", + "useless_mod.configuration.chain_mining_range_y.comment": "連鎖挖掘的 Y 軸範圍半徑", + "useless_mod.configuration.chain_mining_range_y.tooltip": "連鎖挖掘的 Y 軸範圍半徑", + "useless_mod.configuration.chain_mining_range_z": "連鎖挖掘 Z 軸範圍", + "useless_mod.configuration.chain_mining_range_z.comment": "連鎖挖掘的 Z 軸範圍半徑", + "useless_mod.configuration.chain_mining_range_z.tooltip": "連鎖挖掘的 Z 軸範圍半徑", + "useless_mod.configuration.beef_aoe_damage_range_x": "範圍傷害 X 軸範圍", + "useless_mod.configuration.beef_aoe_damage_range_x.comment": "範圍傷害的 X 軸範圍半徑", + "useless_mod.configuration.beef_aoe_damage_range_x.tooltip": "範圍傷害的 X 軸範圍半徑", + "useless_mod.configuration.beef_aoe_damage_range_y": "範圍傷害 Y 軸範圍", + "useless_mod.configuration.beef_aoe_damage_range_y.comment": "範圍傷害的 Y 軸範圍半徑", + "useless_mod.configuration.beef_aoe_damage_range_y.tooltip": "範圍傷害的 Y 軸範圍半徑", + "useless_mod.configuration.beef_aoe_damage_range_z": "範圍傷害 Z 軸範圍", + "useless_mod.configuration.beef_aoe_damage_range_z.comment": "範圍傷害的 Z 軸範圍半徑", + "useless_mod.configuration.beef_aoe_damage_range_z.tooltip": "範圍傷害的 Z 軸範圍半徑", + "useless_mod.configuration.beef_aoe_damage_max_targets": "範圍傷害最大目標數", + "useless_mod.configuration.beef_aoe_damage_max_targets.comment": "範圍傷害單次最多命中的實體數量", + "useless_mod.configuration.beef_aoe_damage_max_targets.tooltip": "範圍傷害單次最多命中的實體數量(數值過大可能導致卡頓)", + "useless_mod.configuration.beef_kill_aura_interval": "殺戮光環間隔(tick)", + "useless_mod.configuration.beef_kill_aura_interval.comment": "殺戮光環每隔多少 tick 結算一次(20 tick = 1 秒)", + "useless_mod.configuration.beef_kill_aura_interval.tooltip": "調小 = 更快,但伺服端實體查詢壓力會線性上升", + "useless_mod.configuration.beef_magnet_range_x": "磁力吸附 X 軸範圍", + "useless_mod.configuration.beef_magnet_range_x.comment": "擊殺後範圍磁力吸附的 X 軸範圍半徑", + "useless_mod.configuration.beef_magnet_range_x.tooltip": "擊殺後範圍磁力吸附的 X 軸範圍半徑", + "useless_mod.configuration.beef_magnet_range_y": "磁力吸附 Y 軸範圍", + "useless_mod.configuration.beef_magnet_range_y.comment": "擊殺後範圍磁力吸附的 Y 軸範圍半徑", + "useless_mod.configuration.beef_magnet_range_y.tooltip": "擊殺後範圍磁力吸附的 Y 軸範圍半徑", + "useless_mod.configuration.beef_magnet_range_z": "磁力吸附 Z 軸範圍", + "useless_mod.configuration.beef_magnet_range_z.comment": "擊殺後範圍磁力吸附的 Z 軸範圍半徑", + "useless_mod.configuration.beef_magnet_range_z.tooltip": "擊殺後範圍磁力吸附的 Z 軸範圍半徑", + "useless_mod.configuration.chain_mining_max_blocks": "連鎖挖掘最大方塊數量", + "useless_mod.configuration.chain_mining_max_blocks.comment": "連鎖挖掘的最大方塊數量", + "useless_mod.configuration.chain_mining_max_blocks.tooltip": "連鎖挖掘的最大方塊數量", + "useless_mod.configuration.fortune_level": "牛排工具時運等級", + "useless_mod.configuration.fortune_level.comment": "牛排工具時運附魔等級", + "useless_mod.configuration.fortune_level.tooltip": "牛排工具時運附魔等級", + "useless_mod.configuration.looting_level": "牛排工具搶奪等級", + "useless_mod.configuration.looting_level.comment": "牛排工具搶奪附魔等級", + "useless_mod.configuration.looting_level.tooltip": "牛排工具搶奪附魔等級", + "useless_mod.configuration.beef_tool_mining_speed": "牛排工具挖掘速度", + "useless_mod.configuration.beef_tool_mining_speed.comment": "牛排工具基礎挖掘速度", + "useless_mod.configuration.beef_tool_mining_speed.tooltip": "牛排工具基礎挖掘速度", + "useless_mod.configuration.beef_tool_ignores_tool_tier": "造化杖無視工具挖掘等級", + "useless_mod.configuration.beef_tool_ignores_tool_tier.comment": "造化杖是否無視所有工具挖掘等級限制(含其它模組自訂的等級)。開啟:可正常挖掘任何需要特定工具等級的方塊;關閉:回到獄髓等級的原版語意。useless_mod:beef_tool_tier_locked 標籤內的方塊始終按等級判定。", + "useless_mod.configuration.beef_tool_ignores_tool_tier.tooltip": "造化杖是否無視所有工具挖掘等級限制", + "useless_mod.configuration.beef_tool_pseudo_hardness": "偽不可破壞方塊名義硬度", + "useless_mod.configuration.beef_tool_pseudo_hardness.comment": "造化杖挖掘「偽不可破壞」方塊(hardness = -1,如 AllTheModium 的三礦與遠古石)時使用的名義硬度。挖掘速度 = beef_tool_mining_speed × 本值。15 約等於挖普通方塊的手感;調大 = 更快。", + "useless_mod.configuration.beef_tool_pseudo_hardness.tooltip": "造化杖挖掘偽不可破壞方塊時使用的名義硬度", + "useless_mod.configuration.beef_tool_entity_interaction_range": "牛排工具實體互動距離", + "useless_mod.configuration.beef_tool_entity_interaction_range.comment": "牛排工具實體互動距離加成,重啟遊戲生效", + "useless_mod.configuration.beef_tool_entity_interaction_range.tooltip": "牛排工具實體互動距離加成,重啟遊戲生效", + "useless_mod.configuration.beef_tool_block_interaction_range": "牛排工具方塊互動距離", + "useless_mod.configuration.beef_tool_block_interaction_range.comment": "牛排工具方塊互動距離加成,重啟遊戲生效", + "useless_mod.configuration.beef_tool_block_interaction_range.tooltip": "牛排工具方塊互動距離加成,重啟遊戲生效", + "useless_mod.configuration.beef_short_teleport_range": "造化杖短距閃現距離", + "useless_mod.configuration.beef_short_teleport_range.comment": "造化杖短距閃現的最大距離(格);該值為上限而非保證距離,中途命中方塊或缺少可站立空間時落點相應前移", + "useless_mod.configuration.beef_short_teleport_range.tooltip": "造化杖短距閃現的最大距離(格)", + "useless_mod.configuration.beef_construction_wand_build_limit": "建築手杖普通建造上限", + "useless_mod.configuration.beef_construction_wand_angel_limit": "建築手杖天使核心上限", + "useless_mod.configuration.beef_construction_wand_destruction_limit": "建築手杖破壞核心上限", + "useless_mod.configuration.beef_tool_force_mining_blacklist": "牛排工具強制挖掘黑名單", + "useless_mod.configuration.beef_tool_force_mining_blacklist.comment": "不會被牛排工具強制挖掘的方塊,支持方塊ID、方塊標籤和*通配符", + "useless_mod.configuration.beef_tool_force_mining_blacklist.tooltip": "不會被牛排工具強制挖掘的方塊,支持方塊ID、方塊標籤和*通配符", + "useless_mod.configuration.beef_tool_force_kill_blacklist": "牛排工具強制擊殺黑名單", + "useless_mod.configuration.beef_tool_force_kill_blacklist.comment": "不會被牛排工具強制擊殺的實體 ID,多個實體 ID 用分號分隔", + "useless_mod.configuration.beef_tool_force_kill_blacklist.tooltip": "不會被牛排工具強制擊殺的實體 ID,多個實體 ID 用分號分隔", + "useless_mod.configuration.beef_tool_force_kill_non_living_whitelist": "非生物強制擊殺白名單", + "useless_mod.configuration.beef_tool_force_kill_non_living_whitelist.comment": "允許牛排工具強制擊殺的非生物實體 ID,多個實體 ID 用分號分隔", + "useless_mod.configuration.beef_tool_force_kill_non_living_whitelist.tooltip": "允許牛排工具強制擊殺的非生物實體 ID,多個實體 ID 用分號分隔", + "useless_mod.configuration.beef_capture_strip_keys": "生物捕捉:額外丟棄的 NBT 鍵", + "useless_mod.configuration.beef_capture_strip_keys.comment": "寫進生怪蛋前額外丟棄的 NBT 鍵路徑,格式為 容器.子鍵;未列出的鍵一律保留(避免誤傷其它整合包)", + "useless_mod.configuration.beef_capture_strip_keys.tooltip": "寫進生怪蛋前額外丟棄的 NBT 鍵路徑,格式為 容器.子鍵;未列出的鍵一律保留(避免誤傷其它整合包)", + "useless_mod.configuration.beef_capture_strip_attachment_namespaces": "生物捕捉:額外丟棄的附件命名空間", + "useless_mod.configuration.beef_capture_strip_attachment_namespaces.comment": "寫進生怪蛋前額外丟棄的資料附件命名空間;其餘(含未知模組)一律保留(避免誤傷其它整合包)", + "useless_mod.configuration.beef_capture_strip_attachment_namespaces.tooltip": "寫進生怪蛋前額外丟棄的資料附件命名空間;其餘(含未知模組)一律保留(避免誤傷其它整合包)", + "useless_mod.configuration.festive_drop_chance": "戰利品大爆發機率", + "useless_mod.configuration.festive_drop_chance.tooltip": "戰利品大爆發觸發機率(百分比,1-100%)", + "useless_mod.configuration.enable_grass_wand_drop": "啟用打草掉落造化杖彩蛋", + "useless_mod.configuration.enable_grass_wand_drop.comment": "是否啟用打草掉落造化杖彩蛋", + "useless_mod.configuration.enable_grass_wand_drop.tooltip": "是否啟用打草掉落造化杖彩蛋", + "useless_mod.configuration.grass_wand_drop_probability": "打草掉落造化杖機率", + "useless_mod.configuration.grass_wand_drop_probability.comment": "打草掉落造化杖的機率,0.00001為十萬分之一", + "useless_mod.configuration.grass_wand_drop_probability.tooltip": "打草掉落造化杖的機率,取值範圍為0到1,0.00001為十萬分之一", + "useless_mod.configuration.custom_potion_effects": "自定義藥水效果", + "useless_mod.configuration.custom_potion_effects.comment": "每行一個效果,格式:modid:effect_id,等級。等級從1開始;不存在的效果和無效的等級會跳過。", + "useless_mod.configuration.custom_potion_effects.tooltip": "自定義藥水效果列表,格式:modid:effect_name,amplifier;多個效果用分號分隔", + "useless_mod.configuration.mekanism_upgrade": "Mekanism 升級設置", + "useless_mod.configuration.time_multiplier": "速度升級增強倍率", + "useless_mod.configuration.time_multiplier.comment": "速度平均每 8 個升級最終效果乘以一次該倍率", + "useless_mod.configuration.electricity_multiplier": "能量升級節省電量增強倍率", + "useless_mod.configuration.electricity_multiplier.comment": "節省電量平均每 8 個升級最終效果乘以一次該倍率", + "useless_mod.configuration.capacity_multiplier": "能量升級儲存電量增強倍率", + "useless_mod.configuration.capacity_multiplier.comment": "儲存電量平均每 8 個升級最終效果乘以一次該倍率", + "useless_mod.configuration.max_upgrade": "最大升級數量", + "useless_mod.configuration.max_upgrade.comment": "機器可接受的最大速度/能量升級數量,重啟遊戲生效", + "useless_mod.configuration.omniversal_multiblock_alloy_furnace": "多方塊萬象合金爐", + "useless_mod.configuration.omniversal_pattern_upload": "萬象樣板自動上傳", + "useless_mod.configuration.omniversal_pattern_auto_upload": "啟用自動上傳", + "useless_mod.configuration.omniversal_pattern_auto_upload.comment": "編碼產出的萬象樣板是否自動上傳至同一AE網路中的多方塊樣板組件。關閉後樣板保留在編碼終端內,由玩家自行取出,與引入自動上傳之前的行為一致。", + "useless_mod.configuration.omniversal_pattern_auto_upload.tooltip": "編碼產出的萬象樣板是否自動上傳至同一AE網路中的多方塊樣板組件。關閉後樣板保留在編碼終端內,由玩家自行取出,與引入自動上傳之前的行為一致。", + "useless_mod.configuration.ore_generator": "礦石產生器", + "useless_mod.configuration.pattern_slots": "ME樣板組件槽位", + "useless_mod.configuration.pattern_slots.comment": "ME樣板組件的可用槽位數量。數值會向下取整為27的整頁;降低配置後,超出範圍的已有內容仍可在只取恢復頁中取出。", + "useless_mod.configuration.pattern_slots.tooltip": "ME樣板組件的可用槽位數量。數值會向下取整為27的整頁;降低配置後,超出範圍的已有內容仍可在只取恢復頁中取出。", + "useless_mod.configuration.mold_slots": "萬象模具中心槽位", + "useless_mod.configuration.mold_slots.comment": "萬象模具中心的可用槽位數量。數值會向下取整為27的整頁;降低配置後,超出範圍的已有內容仍可在只取恢復頁中取出。", + "useless_mod.configuration.mold_slots.tooltip": "萬象模具中心的可用槽位數量。數值會向下取整為27的整頁;降低配置後,超出範圍的已有內容仍可在只取恢復頁中取出。", + "useless_mod.configuration.passive_pattern_slots": "被動合成倉樣板槽位", + "useless_mod.configuration.passive_pattern_slots.comment": "被動合成倉的最大樣板槽位。10級線圈解鎖全部配置槽位,低等級解鎖 ceil(槽位數 x 等級 / 10);降低配置後,超出範圍的樣板仍可在只取恢復槽位中取出。", + "useless_mod.configuration.passive_pattern_slots.tooltip": "被動合成倉的最大樣板槽位。10級線圈解鎖全部配置槽位,低等級解鎖 ceil(槽位數 x 等級 / 10);降低配置後,超出範圍的樣板仍可在只取恢復槽位中取出。", + "useless_mod.configuration.decode_cache_capacity": "萬象樣板解碼緩存容量", + "useless_mod.configuration.decode_cache_capacity.comment": "每個世界保留的已解碼萬象樣板最大數量,重啟後生效,範圍為64至16384。", + "useless_mod.configuration.decode_cache_capacity.tooltip": "每個世界保留的已解碼萬象樣板最大數量,重啟後生效,範圍為64至16384。", + "useless_mod.configuration.advanced_alloy_furnace": "萬象爐設置", + "useless_mod.configuration.advanced_alloy_furnace.recipe_tier_rules": "萬象爐配方等級限制", + "useless_mod.configuration.advanced_alloy_furnace.recipe_tier_rules.comment": "每行格式:配方 ID 通配符,等級。支援 * 通配符;等級範圍為 0-10;未匹配配方不受限制。", + "useless_mod.configuration.advanced_alloy_furnace.recipe_tier_rules.tooltip": "每行格式:配方 ID 通配符,等級。精確 ID 優先於通配符,相同具體度時後面的規則覆蓋前面的規則。", + "useless_mod.configuration.advanced_alloy_furnace.recipe_conversion": "配方轉換", + "useless_mod.configuration.advanced_alloy_furnace.recipe_conversion.minecraft": "Minecraft", + "useless_mod.configuration.advanced_alloy_furnace.recipe_conversion.farmers_delight": "Farmer's Delight及其附屬", + "useless_mod.configuration.advanced_alloy_furnace.recipe_conversion.kaleidoscope": "森羅物語", + "useless_mod.configuration.advanced_alloy_furnace.recipe_conversion.ae": "Applied Energistics及其附屬", + "useless_mod.configuration.advanced_alloy_furnace.recipe_conversion.mekanism": "Mekanism及其附屬", + "useless_mod.configuration.advanced_alloy_furnace.recipe_conversion.justdirethings": "Just Dire Things及其附屬", + "useless_mod.configuration.advanced_alloy_furnace.recipe_conversion.other": "其他模組", + "useless_mod.configuration.draw_appflux_energy": "自動抽取 AppFlux 網路儲電", + "useless_mod.configuration.draw_appflux_energy.comment": "萬象爐是否自動從所在 AE 網路抽取 AppliedFlux (應用通量) 儲存的 FE 能量,需要安裝 AppliedFlux 且網路中有通量元件,每 tick 抽取量受熔爐最大輸入速率限制", + "useless_mod.configuration.draw_appflux_energy.tooltip": "萬象爐是否自動從所在 AE 網路抽取 AppliedFlux (應用通量) 儲存的 FE 能量,需要安裝 AppliedFlux 且網路中有通量元件,每 tick 抽取量受熔爐最大輸入速率限制", + "useless_mod.configuration.draw_ae_energy": "抽取 AE 網路自身能量", + "useless_mod.configuration.draw_ae_energy.comment": "萬象爐是否直接抽取 AE 網路自身的能量(按 1 AE = 2 FE 折算)。警告:會與網路中其他設備爭搶供電,網路儲能不足時可能導致設備頻繁掉線。在抽取 AppliedFlux 之後作為補充", + "useless_mod.configuration.draw_ae_energy.tooltip": "萬象爐是否直接抽取 AE 網路自身的能量(按 1 AE = 2 FE 折算)。警告:會與網路中其他設備爭搶供電,網路儲能不足時可能導致設備頻繁掉線。在抽取 AppliedFlux 之後作為補充", + "useless_mod.configuration.ae_batch_ripe_ticks": "AE 批次成熟等待(tick)", + "useless_mod.configuration.ae_batch_ripe_ticks.comment": "萬象爐收到 AE 推送的批次後先等待多少個 tick 再投入執行。這個窗口用於把連續推送合併成一個任務,等待期間 GUI 顯示為「排隊」。設為 0 表示推送即刻投入執行;機器完全空閒(無任何運行中任務)時會跳過該窗口", + "useless_mod.configuration.ae_batch_ripe_ticks.tooltip": "萬象爐收到 AE 推送的批次後先等待多少個 tick 再投入執行。這個窗口用於把連續推送合併成一個任務,等待期間 GUI 顯示為「排隊」。設為 0 表示推送即刻投入執行;機器完全空閒(無任何運行中任務)時會跳過該窗口", + "useless_mod.configuration.ae_output_return_budget_millis": "萬象爐產物回網時間預算(毫秒)", + "useless_mod.configuration.ae_output_return_budget_millis.comment": "萬象爐每 tick 最多花多少毫秒把產物寫回 ME 網路。它直接決定可持續合成速度:AE2 儲存介面單次只能寫一個 long 分段,產物必須逐段插入,所以每 tick 能插多少次就決定了能跑多快。調大=合成更快但單 tick 更重(可能掉 TPS);調小=更省 tick 但合成變慢。機器或伺服器過載時本預算還會被全域降頻係數按比例收窄(下限 250 微秒)", + "useless_mod.configuration.ae_output_return_budget_millis.tooltip": "萬象爐每 tick 最多花多少毫秒把產物寫回 ME 網路。它直接決定可持續合成速度:AE2 儲存介面單次只能寫一個 long 分段,產物必須逐段插入,所以每 tick 能插多少次就決定了能跑多快。調大=合成更快但單 tick 更重(可能掉 TPS);調小=更省 tick 但合成變慢。機器或伺服器過載時本預算還會被全域降頻係數按比例收窄(下限 250 微秒)", + "useless_mod.configuration.ae_unlimited_bigint_parallelism": "萬象爐單批並行上限解除", + "useless_mod.configuration.ae_unlimited_bigint_parallelism.comment": "開啟後解除「材料窗口」對單批規模的限制(即去掉 執行緒數×Long.MAX 這道閘)。單批規模將只由產物交付能力決定,可以一次吃下任意大的份數,不必再靠堆執行緒數抬高上限。代價:單次准入可能吃下極大量材料(由呼叫方自己的BigInteger帳本扣除);低檔線圈會轉而受能量閘限制,有用線圈無能量閘、不受影響", + "useless_mod.configuration.ae_unlimited_bigint_parallelism.tooltip": "開啟後解除「材料窗口」對單批規模的限制(即去掉 執行緒數×Long.MAX 這道閘)。單批規模將只由產物交付能力決定,可以一次吃下任意大的份數,不必再靠堆執行緒數抬高上限。代價:單次准入可能吃下極大量材料(由呼叫方自己的BigInteger帳本扣除);低檔線圈會轉而受能量閘限制,有用線圈無能量閘、不受影響", + "useless_mod.configuration.stafflink": "無線物流", + "useless_mod.configuration.stafflink_idle_backoff_enabled": "空轉指數退避", + "useless_mod.configuration.stafflink_idle_backoff_enabled.comment": "無線物流:空轉的線路是否啟用指數退避。開啟(預設):某一輪搬運量為0時把下一輪間隔逐次翻倍,最多放大到「週期×退避倍率上限」,搬動一次立即恢復。這樣可以避免大量空通道每 tick 白跑一遍。關閉:無論搬動與否都嚴格按線路配置的週期執行,搬運響應最即時,代價是空通道持續產生開銷", + "useless_mod.configuration.stafflink_idle_backoff_enabled.tooltip": "無線物流:空轉的線路是否啟用指數退避", + "useless_mod.configuration.stafflink_idle_backoff_cap": "空轉退避倍率上限", + "useless_mod.configuration.stafflink_idle_backoff_cap.comment": "無線物流:空轉退避的倍率上限(下一輪間隔最多放大到 配置週期×本值)。建議取2的冪(1/2/4/8/16/32/64),非2的冪會向下取整。設為1表示不放大(等同關閉退避)。預設32折合約32分鐘封頂。僅在「空轉指數退避」開啟時生效", + "useless_mod.configuration.stafflink_idle_backoff_cap.tooltip": "無線物流:空轉退避的倍率上限(下一輪間隔最多放大到 配置週期×本值)", + "useless_mod.configuration.enable_crafting_recipe_conversion": "啟用工作台配方轉換", + "useless_mod.configuration.enable_crafting_recipe_conversion.comment": "是否啟用工作台配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_crafting_recipe_conversion.tooltip": "是否啟用工作台配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_smelting_recipe_conversion": "啟用原版熔爐配方轉換", + "useless_mod.configuration.enable_smelting_recipe_conversion.comment": "是否啟用原版熔爐配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_smelting_recipe_conversion.tooltip": "是否啟用原版熔爐配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_brewing_recipe_conversion": "啟用原版釀造台配方轉換", + "useless_mod.configuration.enable_brewing_recipe_conversion.comment": "是否啟用原版釀造台配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_brewing_recipe_conversion.tooltip": "是否啟用原版釀造台配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_fishing_recipe_conversion": "啟用原版釣魚配方轉換", + "useless_mod.configuration.enable_fishing_recipe_conversion.comment": "是否啟用原版釣魚戰利品表配方轉換(動態讀取,不含附魔書與裝備),修改後重啟遊戲生效", + "useless_mod.configuration.enable_fishing_recipe_conversion.tooltip": "是否啟用原版釣魚戰利品表配方轉換(動態讀取,不含附魔書與裝備),修改後重啟遊戲生效", + "useless_mod.configuration.enable_villager_trade_recipe_conversion": "啟用原版村民交易配方轉換", + "useless_mod.configuration.enable_villager_trade_recipe_conversion.comment": "是否啟用原版村民交易配方轉換,使用基礎綠寶石消耗的最低值,修改後重啟遊戲生效", + "useless_mod.configuration.enable_villager_trade_recipe_conversion.tooltip": "是否啟用原版村民交易配方轉換,使用基礎綠寶石消耗的最低值,修改後重啟遊戲生效", + "useless_mod.configuration.enable_extendedae_recipe_conversion": "啟用ExtendedAE配方轉換", + "useless_mod.configuration.enable_extendedae_recipe_conversion.comment": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_extendedae_recipe_conversion.tooltip": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_advanced_ae_recipe_conversion": "啟用Advanced AE配方轉換", + "useless_mod.configuration.enable_advanced_ae_recipe_conversion.comment": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_advanced_ae_recipe_conversion.tooltip": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_mekanism_recipe_conversion": "啟用Mekanism配方轉換", + "useless_mod.configuration.enable_mekanism_recipe_conversion.comment": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_mekanism_recipe_conversion.tooltip": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_mekanism_generators_recipe_conversion": "啟用Mekanism Generators配方轉換", + "useless_mod.configuration.enable_mekanism_generators_recipe_conversion.comment": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_mekanism_generators_recipe_conversion.tooltip": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_appmek_recipe_conversion": "啟用AppMek配方轉換", + "useless_mod.configuration.enable_appmek_recipe_conversion.comment": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_appmek_recipe_conversion.tooltip": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_ae2_recipe_conversion": "啟用AE2配方轉換", + "useless_mod.configuration.enable_ae2_recipe_conversion.comment": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_ae2_recipe_conversion.tooltip": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_ae2cs_recipe_conversion": "啟用AE2 Crystal Science配方轉換", + "useless_mod.configuration.enable_ae2cs_recipe_conversion.comment": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_ae2cs_recipe_conversion.tooltip": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_industrial_foregoing_recipe_conversion": "啟用Industrial Foregoing配方轉換", + "useless_mod.configuration.enable_industrial_foregoing_recipe_conversion.comment": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_industrial_foregoing_recipe_conversion.tooltip": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_actually_additions_recipe_conversion": "啟用Actually Additions配方轉換", + "useless_mod.configuration.enable_actually_additions_recipe_conversion.comment": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_actually_additions_recipe_conversion.tooltip": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_ars_nouveau_recipe_conversion": "啟用Ars Nouveau配方轉換", + "useless_mod.configuration.enable_ars_nouveau_recipe_conversion.comment": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_ars_nouveau_recipe_conversion.tooltip": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_mystical_agriculture_recipe_conversion": "啟用Mystical Agriculture配方轉換", + "useless_mod.configuration.enable_mystical_agriculture_recipe_conversion.comment": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_mystical_agriculture_recipe_conversion.tooltip": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_ae2lt_recipe_conversion": "啟用AE2 Lightning Tech配方轉換", + "useless_mod.configuration.enable_ae2lt_recipe_conversion.comment": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_ae2lt_recipe_conversion.tooltip": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_data_energistics_recipe_conversion": "啟用Data Energistics配方轉換", + "useless_mod.configuration.enable_data_energistics_recipe_conversion.comment": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_data_energistics_recipe_conversion.tooltip": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_productive_bees_recipe_conversion": "啟用Productive Bees配方轉換", + "useless_mod.configuration.enable_productive_bees_recipe_conversion.comment": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_productive_bees_recipe_conversion.tooltip": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_draconic_evolution_recipe_conversion": "啟用Draconic Evolution配方轉換", + "useless_mod.configuration.enable_draconic_evolution_recipe_conversion.comment": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_draconic_evolution_recipe_conversion.tooltip": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_powah_recipe_conversion": "啟用Powah配方轉換", + "useless_mod.configuration.enable_powah_recipe_conversion.comment": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_powah_recipe_conversion.tooltip": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_extended_crafting_recipe_conversion": "啟用Extended Crafting配方轉換", + "useless_mod.configuration.enable_extended_crafting_recipe_conversion.comment": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_extended_crafting_recipe_conversion.tooltip": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_avaritia_recipe_conversion": "啟用Re-Avaritia配方轉換", + "useless_mod.configuration.enable_avaritia_recipe_conversion.comment": "是否啟用Re-Avaritia合成、終焉鍛造、中子壓縮和中子態素收集器配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_avaritia_recipe_conversion.tooltip": "是否啟用Re-Avaritia合成、終焉鍛造、中子壓縮和中子態素收集器配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_neo_eco_ae_recipe_conversion": "啟用NeoECOAE配方轉換", + "useless_mod.configuration.enable_neo_eco_ae_recipe_conversion.comment": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_neo_eco_ae_recipe_conversion.tooltip": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_natures_aura_recipe_conversion": "啟用Nature's Aura配方轉換", + "useless_mod.configuration.enable_natures_aura_recipe_conversion.comment": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_natures_aura_recipe_conversion.tooltip": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_forbidden_arcanus_recipe_conversion": "啟用Forbidden Arcanus配方轉換", + "useless_mod.configuration.enable_forbidden_arcanus_recipe_conversion.comment": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_forbidden_arcanus_recipe_conversion.tooltip": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_occultism_recipe_conversion": "啟用Occultism配方轉換", + "useless_mod.configuration.enable_occultism_recipe_conversion.comment": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_occultism_recipe_conversion.tooltip": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_summoningrituals_recipe_conversion": "啟用Summoning Rituals配方轉換", + "useless_mod.configuration.enable_summoningrituals_recipe_conversion.comment": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_summoningrituals_recipe_conversion.tooltip": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_malum_recipe_conversion": "啟用Malum配方轉換", + "useless_mod.configuration.enable_malum_recipe_conversion.comment": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_malum_recipe_conversion.tooltip": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_ender_io_recipe_conversion": "啟用Ender IO配方轉換", + "useless_mod.configuration.enable_ender_io_recipe_conversion.comment": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_ender_io_recipe_conversion.tooltip": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_create_recipe_conversion": "啟用Create配方轉換", + "useless_mod.configuration.enable_create_recipe_conversion.comment": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_create_recipe_conversion.tooltip": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_astralsorcery_recipe_conversion": "啟用星輝魔法配方轉換", + "useless_mod.configuration.enable_astralsorcery_recipe_conversion.comment": "是否啟用星輝魔法的星能注入、聚星缸與祭壇合成配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_astralsorcery_recipe_conversion.tooltip": "是否啟用星輝魔法的星能注入、聚星缸與祭壇合成配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_oritech_recipe_conversion": "啟用Oritech配方轉換", + "useless_mod.configuration.enable_oritech_recipe_conversion.comment": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_oritech_recipe_conversion.tooltip": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_neovitae_recipe_conversion": "啟用Neo Vitae配方轉換", + "useless_mod.configuration.enable_neovitae_recipe_conversion.comment": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_neovitae_recipe_conversion.tooltip": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_ufo_recipe_conversion": "啟用UFO Future配方轉換", + "useless_mod.configuration.enable_ufo_recipe_conversion.comment": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_ufo_recipe_conversion.tooltip": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_botany_pots_recipe_conversion": "啟用植物盆栽配方轉換", + "useless_mod.configuration.enable_botany_pots_recipe_conversion.comment": "是否啟用植物盆栽作物生長配方的轉換,輸入為水,模具為植物盆栽與對應作物,修改後重啟遊戲生效", + "useless_mod.configuration.enable_botany_pots_recipe_conversion.tooltip": "是否啟用植物盆栽作物生長配方的轉換,輸入為水,模具為植物盆栽與對應作物,修改後重啟遊戲生效", + "useless_mod.configuration.enable_modern_industrialization_recipe_conversion": "啟用Modern Industrialization配方轉換", + "useless_mod.configuration.enable_modern_industrialization_recipe_conversion.comment": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_modern_industrialization_recipe_conversion.tooltip": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_immersiveengineering_recipe_conversion": "啟用Immersive Engineering配方轉換", + "useless_mod.configuration.enable_immersiveengineering_recipe_conversion.comment": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_immersiveengineering_recipe_conversion.tooltip": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_pneumaticcraft_recipe_conversion": "啟用PneumaticCraft配方轉換", + "useless_mod.configuration.enable_pneumaticcraft_recipe_conversion.comment": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_pneumaticcraft_recipe_conversion.tooltip": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_bigreactors_recipe_conversion": "啟用Extreme Reactors配方轉換", + "useless_mod.configuration.enable_bigreactors_recipe_conversion.comment": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_bigreactors_recipe_conversion.tooltip": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_irons_spellbooks_recipe_conversion": "啟用鐵魔法煉金鍋配方轉換", + "useless_mod.configuration.enable_irons_spellbooks_recipe_conversion.comment": "是否啟用鐵魔法煉金鍋配方轉換,並將流體轉換為對應物品,修改後重啟遊戲生效", + "useless_mod.configuration.enable_irons_spellbooks_recipe_conversion.tooltip": "是否啟用鐵魔法煉金鍋配方轉換,並將流體轉換為對應物品,修改後重啟遊戲生效", + "useless_mod.configuration.enable_hostilenetworks_recipe_conversion": "啟用敵對神經網絡配方轉換", + "useless_mod.configuration.enable_hostilenetworks_recipe_conversion.comment": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_hostilenetworks_recipe_conversion.tooltip": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_apothic_flux_recipe_conversion": "啟用Apothic Flux配方轉換", + "useless_mod.configuration.enable_apothic_flux_recipe_conversion.comment": "是否啟用Apothic Flux聚靈配方及使用通量調整書架的Apothic Enchanting灌注配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_apothic_flux_recipe_conversion.tooltip": "是否啟用Apothic Flux聚靈配方及使用通量調整書架的Apothic Enchanting灌注配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_justdirethings_recipe_conversion": "啟用Just Dire Things配方轉換", + "useless_mod.configuration.enable_justdirethings_recipe_conversion.comment": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_justdirethings_recipe_conversion.tooltip": "是否啟用該配方來源的配方轉換,修改後重啟遊戲生效", + "useless_mod.configuration.enable_jdte_recipe_conversion": "啟用Just Dire Things Extra配方轉換", + "useless_mod.configuration.enable_jdte_recipe_conversion.comment": "是否啟用JDTE附屬的配方轉換;關閉Just Dire Things本體轉換時該附屬一併停用,修改後重啟遊戲生效", + "useless_mod.configuration.enable_jdte_recipe_conversion.tooltip": "是否啟用JDTE附屬的配方轉換;關閉Just Dire Things本體轉換時該附屬一併停用,修改後重啟遊戲生效", + "useless_mod.configuration.beef_autoclick_clicks_per_tick": "連點模式觸發速率", + "useless_mod.configuration.beef_autoclick_clicks_per_tick.comment": "造化杖連點模式每個客戶端 tick 觸發多少次右鍵;調大 = 更快(預設 4 約等於 80 次/秒),調小 = 降低伺服器壓力", + "useless_mod.configuration.beef_autoclick_clicks_per_tick.tooltip": "造化杖連點模式每個客戶端 tick 觸發多少次右鍵;調大 = 更快(預設 4 約等於 80 次/秒),調小 = 降低伺服器壓力", + "key.category.useless_mod.useless": "無用模組", + "key.useless_mod.switch_silk_touch": "切換至絲綢之觸", + "key.useless_mod.switch_fortune": "切換至幸運", + "key.useless_mod.trigger_chain_mining": "連鎖挖掘(需要按住按鍵)", + "key.useless_mod.toggle_chain_mode": "強化連鎖挖掘切換", + "key.useless_mod.force_mining": "強制挖掘模式切換", + "key.useless_mod.switch_mode_wheel": "開啟模式選擇輪盤", + "key.useless_mod.trigger_force_mining": "觸發強制挖掘", + "key.useless_mod.switch_farmland_mode": "切換耕地/草徑模式", + "key.useless_mod.toggle_crop_harvest": "順手收菜開關", + "key.useless_mod.toggle_shears": "剪刀功能開關", + "key.useless_mod.toggle_flint_and_steel": "打火石功能開關", + "key.useless_mod.short_teleport": "短距傳送", + "key.useless_mod.toggle_auto_click": "連點模式開關", + "key.useless_mod.toggle_kill_aura": "殺戮光環開關", + "tooltip.useless_mod.festive_affix": "擊殺實體時有 5% 機率觸發戰利品大爆發", + "tooltip.useless_mod.auto_collect": "內建磁鐵功能", + "tooltip.useless_mod.time_acceleration_mode": "時間加速模式", + "tooltip.useless_mod.time_acceleration_hint": "開啟後按住 Shift 右鍵加速方塊和機器;加速生物請改用「生物加速」模式", + "tooltip.useless_mod.beef_teleport_mode": "傳送", + "tooltip.useless_mod.beef_aoe_damage_mode": "範圍傷害", + "tooltip.useless_mod.beef_magnet_mode": "範圍磁力", + "tooltip.useless_mod.beef_kill_aura_mode": "殺戮光環", + "tooltip.useless_mod.beef_kill_aura_hint": "開啟後手持造化杖每 20 tick 對「範圍傷害」設定範圍內生物結算一次攻擊傷害;命中後依「範圍磁力」登記吸附", + "tooltip.useless_mod.beef_protect_mode": "生物保護名單", + "tooltip.useless_mod.beef_protect_hint": "Ctrl+右鍵生物依「種類」加入/移出保護;Shift+右鍵生物依「個體」加入/移出保護(需手持造化杖並開啟殺戮光環或範圍傷害)。受保護的生物不會被光環、範圍傷害與強制擊殺命中;你自己的寵物與坐騎一律自動受保護", + "tooltip.useless_mod.beef_protect_count": "已保護 %s 種 / %s 隻", + "tooltip.useless_mod.beef_protect_toggle_mode": "保護名單模式", + "tooltip.useless_mod.beef_entity_time_acceleration_mode": "生物加速", + "tooltip.useless_mod.beef_entity_time_acceleration_hint": "開啟後潛行右鍵生物可加速其計時:幼年成長/繁殖後冷卻、雞下蛋間隔、羊剪毛後長毛、村民補貨、蜜蜂採蜜、海龜下蛋;裝了新生魔藝時還可加速德格米/探寶蟹/旋風精靈的工作速度。只推數值,不改變移動與 AI。與「保護名單」的 Shift 手勢互斥(Ctrl 按種類仍可用)", + "tooltip.useless_mod.beef_protect_toggle_hint": "開啟「保護名單模式」後殺戮光環與範圍傷害會暫時停手,方便從容加入保護名單;加完記得關掉", + "tooltip.useless_mod.beef_farmland_mode": "右鍵", + "tooltip.useless_mod.beef_farmland_state.hoe": "鋤頭優先", + "tooltip.useless_mod.beef_farmland_state.shovel": "鏟子優先", + "tooltip.useless_mod.beef_farmland_hint": "右鍵泥土 / 草方塊時優先使用哪個工具:鋤頭優先變耕地,鏟子優先變草徑", + "tooltip.useless_mod.beef_shears_mode": "剪刀功能", + "tooltip.useless_mod.beef_shears_hint": "開啟後右鍵羊、哞菇等可剪實體直接剪毛/剪掉落,並對外聲明剪刀能力", + "tooltip.useless_mod.beef_flint_and_steel_mode": "打火石功能", + "tooltip.useless_mod.beef_flint_and_steel_hint": "開啟後右鍵營火、蠟燭可直接點亮,右鍵其它方塊在點擊面放火;潛行右鍵把這次互動交給其他模組", + "tooltip.useless_mod.beef_ritual_satchel_mode": "匠心儀式挎包", + "tooltip.useless_mod.beef_ritual_satchel_hint": "開啟後右鍵魔典預覽的儀式多方塊,優先使用背包裡的方塊,不足時從綁定的 AE 網路取,一次擺出整座儀式", + "gui.useless_mod.ritual_satchel.unknown_multiblock": "這裡不是可識別的儀式結構", + "gui.useless_mod.ritual_satchel.no_ae_items": "綁定的 AE 網路裡沒有可用的方塊", + "gui.useless_mod.ritual_satchel.no_matching_item": "背包與 AE 網路裡都沒有這座儀式需要的方塊", + "tooltip.useless_mod.beef_ripen_mode": "催熟模式", + "tooltip.useless_mod.beef_ripen_hint": "開啟後右鍵可一鍵催熟:作物、樹苗、菌類等直接催到成熟,南瓜西瓜莖會結果實,右鍵幼年動物直接催至成年;甘蔗/仙人掌/竹子等骨粉無效的方塊需另開「強制生長」;按住連鎖鍵(Tab)可整片作用;潛行右鍵把這次互動交給其他模組;對藤蔓可強制蔓延(無視遊戲規則與數量上限),對花可產出掉落物(走 AE 儲存優先/範圍磁力)且花本體保留", + "tooltip.useless_mod.beef_force_grow_mode": "強制生長", + "tooltip.useless_mod.beef_force_grow_hint": "開啟後催熟會額外用隨機刻推進方塊,可作用於甘蔗、仙人掌、竹子等骨粉無效的方塊;但它不挑方塊——樹葉枯萎、火蔓延、雪冰融化、耕地退化同樣會被推,請自行判斷是否開啟", + "tooltip.useless_mod.beef_auto_click_mode": "連點模式", + "tooltip.useless_mod.beef_auto_click_hint": "開啟後手持造化杖會自動以最快速度重複觸發右鍵(速率見客戶端設定);再按一次綁定按鍵關閉,注意與打火石等功能同時開啟時的後果", + "tooltip.useless_mod.beef_crop_harvest_mode": "順手收菜", + "tooltip.useless_mod.beef_crop_harvest_hint": "右鍵成熟作物直接收穫、種子留在地裡;潛行右鍵把這次互動交給其他模組", + "tooltip.useless_mod.beef_teleport_hint": "開啟後按住 Shift 右鍵進行短距離傳送;安裝 Ender IO 時可傳送至旅行錨", + "gui.useless_mod.beef_protect.add_type": "已保護生物種類:%s", + "gui.useless_mod.beef_protect.remove_type": "已取消保護生物種類:%s", + "gui.useless_mod.beef_protect.add_entity": "已保護該生物:%s", + "gui.useless_mod.beef_protect.remove_entity": "已取消保護該生物:%s", + "subtitles.useless_mod.kill_aura_on": "殺戮光環:開啟", + "subtitles.useless_mod.kill_aura_off": "殺戮光環:關閉", + "tooltip.useless_mod.current_tool_mode": "當前工具模式", + "tooltip.useless_mod.none_mode": "未選擇", + "tooltip.useless_mod.wrench_mode": "扳手模式", + "tooltip.useless_mod.screwdriver_mode": "螺絲起子模式", + "tooltip.useless_mod.mallet_mode": "軟錘模式", + "tooltip.useless_mod.crowbar_mode": "撬棍模式", + "tooltip.useless_mod.hammer_mode": "鍛造錘模式", + "tooltip.useless_mod.omnitool_mode": "全能工具", + "tooltip.useless_mod.disable": "關閉", + "tooltip.useless_mod.enable": "開啟", + "tooltip.useless_mod.force_mining_mode": "強制挖掘", + "tooltip.useless_mod.ae_storage_priority_mode": "AE 儲存優先", + "tooltip.useless_mod.ae_network_connect_mode": "AE 連接模式", + "tooltip.useless_mod.wrench_tag_mode": "扳手標籤", + "tooltip.useless_mod.construction_wand_mode": "建築手杖", + "tooltip.useless_mod.construction_wand_default_core": "建築核心", + "tooltip.useless_mod.construction_wand_angel_core": "天使核心", + "tooltip.useless_mod.construction_wand_destruction_core": "破壞核心", + "tooltip.useless_mod.enhanced_chain_mining_mode": "強化連鎖挖掘", + "tooltip.useless_mod.auto_smelt_mode": "自動熔煉", + "tooltip.useless_mod.force_kill_enabled_mode": "強制擊殺", + "tooltip.useless_mod.beef_malum_spirit_mode": "精魂掉落", + "tooltip.useless_mod.beef_mystical_agriculture_mode": "觉醒粉掉落", + "tooltip.useless_mod.beef_beheading_mode": "斬首", + "tooltip.useless_mod.beef_invulnerability_mode": "玩家保護", + "tooltip.useless_mod.beef_advanced_stealth_mode": "進階隱身", + "tooltip.useless_mod.beef_capture_mode": "生物捕捉", + "tooltip.useless_mod.exdeorum_crook_mode": "鉤子", + "tooltip.useless_mod.exdeorum_hammer_mode": "錘子", + "tooltip.useless_mod.exdeorum_compressed_hammer_mode": "壓縮錘", + "tooltip.useless_mod.press_shift_for_keys": "按住 Shift 顯示按鍵綁定", + "tooltip.useless_mod.tooltip_page": "第 %s/%s 頁 按 %s %s 翻頁", + "tooltip.useless_mod.silk_touch_mode": "絲綢之觸", + "tooltip.useless_mod.fortune_mode": "幸運", + "tooltip.useless_mod.key.switch_silk_touch": "§d按下 %s 切換至絲綢之觸模式", + "tooltip.useless_mod.key.switch_fortune": "§d按下 %s 切換至幸運模式", + "tooltip.useless_mod.key.toggle_chain_mode": "§d按下 %s 切換連鎖挖掘(普通 ↔ 增強)", + "tooltip.useless_mod.key.switch_force_mining": "§d按下 %s 切換強制挖掘模式", + "tooltip.useless_mod.key.switch_farmland_mode": "§d按下 %s 切換泥土右鍵優先(鋤頭 / 鏟子)", + "tooltip.useless_mod.key.toggle_crop_harvest": "§d按下 %s 切換順手收菜", + "tooltip.useless_mod.key.toggle_shears": "§d按下 %s 切換剪刀功能", + "tooltip.useless_mod.key.toggle_flint_and_steel": "§d按下 %s 切換打火石功能", + "tooltip.useless_mod.key.toggle_auto_click": "§d按下 %s 切換連點模式", + "tooltip.useless_mod.key.toggle_kill_aura": "§d按下 %s 切換殺戮光環", + "tooltip.useless_mod.key.trigger_chain_mining": "§c按住 %s 臨時啟用連鎖挖掘", + "tooltip.useless_mod.key.chain_use": "§c按住 %s 右鍵操作(耕地/鋪路、剝皮/刮銅/除蠟、收菜、催熟)同樣連鎖", + "tooltip.useless_mod.key.trigger_force_mining": "§c按住 %s 臨時啟用強制挖掘", + "tooltip.useless_mod.key.open_mode_wheel": "§e按下 %s 打開模式選擇輪盤", + "tooltip.useless_mod.furnace_tier": "階級: %s", + "tooltip.useless_mod.furnace_upgrade_hint": "§e潛行右鍵§r:1-9 階使用§6無用錠系列§r,之後使用§a有用錠§r升至 10 階", + "tooltip.useless_mod.furnace_upgrade_effect": "§7第 10 階:§a能量容量與輸入速率§7提升至 long 上限", + "tooltip.useless_mod.fast_break_plastic": "Shift + 右鍵以快速破壞塑膠方塊", + "tooltip.useless_mod.ae_storage_priority_bind_hint": "§aShift + 右鍵§r ME 無線存取點進行綁定", + "tooltip.useless_mod.ae_network_connect_hint": "§9AE 連接模式§r下右鍵有 AE 節點的機器接入/再右鍵斷開,已連接的會顯示邊框,且不受通道上限約束", + "jei.useless_mod.catalyst_info": "催化劑並行數資訊", + "jei.useless_mod.tooltip.amount": "數量: %s", + "jei.useless_mod.tooltip.amount.mb": "數量: %s mB", + "jei.useless_mod.tooltip.catalyst": "催化劑", + "tooltip.useless_mod.ae2_gift_package.description": "一路以來的努力與汗水終於有了回報。", + "jei.useless_mod.tooltip.catalyst.useless_ingot.title": "無用錠配方說明:", + "jei.useless_mod.tooltip.catalyst.useless_ingot.desc1": "• 合成 N 階無用錠需要 N-1 階無用錠作為輸入", + "jei.useless_mod.tooltip.catalyst.useless_ingot.desc2": "• 並行數為 2^N(N 為目標階數)", + "jei.useless_mod.tooltip.catalyst.useless_ingot.desc3": "• 處理時間會隨催化劑等級縮短", + "jei.useless_mod.tooltip.catalyst.useless_ingot.warning": "• 合成無用錠時催化劑不會被消耗", + "jei.useless_mod.tooltip.catalyst.normal.desc1": "• 可以使用無用錠提高並行數", + "jei.useless_mod.tooltip.catalyst.normal.desc2": "• 不同等級的無用錠提供不同的並行數", + "jei.useless_mod.tooltip.catalyst.normal.warning": "• 催化劑會被消耗", + "jei.useless_mod.tooltip.catalyst.normal.desc3": "• 催化劑等級越高,處理時間越短", + "jei.useless_mod.tooltip.key_input": "數據輸入(AE2 特殊數據類型)", + "jei.useless_mod.tooltip.key_output": "數據輸出(AE2 特殊數據類型)", + "jei.useless_mod.tooltip.energy.base": "基礎能量消耗: %s", + "jei.useless_mod.tooltip.energy.actual": "實際能量消耗 = 基礎能量消耗 × 並行數", + "jei.useless_mod.tooltip.energy.note_useful": "有用錠催化時能量不倍增", + "jei.useless_mod.tooltip.process_time": "處理時間: %s ticks", + "jei.useless_mod.tooltip.process_time.note": "處理時間受催化劑等級影響", + "jei.useless_mod.tooltip.process_time.catalyst_bonus": "• 一階催化劑: 90%%時間", + "jei.useless_mod.tooltip.process_time.catalyst_bonus_2": "• 二階催化劑: 80%%時間", + "jei.useless_mod.tooltip.process_time.useful_ingot": "• 有用錠: 固定 1 tick", + "jei.useless_mod.tooltip.process_time.coil_bonus": "• 多方塊萬象爐:線圈每升一級,處理時間减半", + "jei.useless_mod.tooltip.process_time.coil_useful": "• 多方塊萬象爐有用線圈:固定1 tick", + "jei.useless_mod.tooltip.parallel.title": "並行數效果說明", + "jei.useless_mod.tooltip.parallel.desc1": "• 輸入物品消耗 × 並行數", + "jei.useless_mod.tooltip.parallel.desc2": "• 輸出物品數量 × 並行數", + "jei.useless_mod.tooltip.parallel.desc3": "• 能量消耗 × 並行數", + "jei.useless_mod.tooltip.parallel.desc4": "• 處理時間受催化劑等級影響", + "jei.useless_mod.tooltip.parallel.warning": "⚠ 催化劑會被消耗", + "jei.useless_mod.tooltip.catalyst.parallel.unlimited": "並行數: 無上限", + "jei.useless_mod.tooltip.catalyst.effect.title": "效果說明:", + "jei.useless_mod.tooltip.catalyst.effect.not_consumed": "• 催化劑不會被消耗", + "jei.useless_mod.tooltip.catalyst.effect.unlimited": "• 無並行數上限", + "jei.useless_mod.tooltip.catalyst.effect.no_energy_mult": "• 能量消耗不會加倍", + "jei.useless_mod.tooltip.catalyst.effect.time_unchanged": "• 處理時間固定為 1 tick", + "jei.useless_mod.tooltip.catalyst.effect.useless_warning": "注意:合成無用錠時催化劑無效(無用錠配方已內建低一階無用錠作為輸入)", + "jei.useless_mod.tooltip.catalyst.normal.parallel": "普通配方並行數: %s倍", + "jei.useless_mod.tooltip.catalyst.normal.speed_bonus": "處理時間: %d0%%", + "jei.useless_mod.tooltip.catalyst.cross_tier.title": "跨階合成效果:", + "jei.useless_mod.tooltip.catalyst.cross_tier.format": "• 合成 %s 階無用錠: %s 倍並行", + "jei.useless_mod.tooltip.recipe_id": "配方 ID:%s", + "jei.useless_mod.gui.tier_required": "等級要求: %s(單方塊爐/多方塊線圈)", + "jei.useless_mod.gui.tier_unrestricted": "無等級要求", + "jei.useless_mod.catalyst_info.title": "催化劑效果資訊", + "jei.useless_mod.catalyst_info.desc1": "• 催化劑為可選項,不會被消耗", + "jei.useless_mod.catalyst_info.desc2": "• 放入催化劑可大幅提高並行數", + "jei.useless_mod.catalyst_info.desc3": "• 並行數影響消耗、產出和能量", + "jei.useless_mod.catalyst_info.desc4": "• 催化劑等級越高,處理時間越短", + "jei.useless_mod.catalyst_info.desc5": "• 一階 90%% 時間,二階 80%% 時間,以此類推", + "jei.useless_mod.catalyst_info.desc6": "• 有用錠固定1 tick且能量消耗不倍增", + "jei.useless_mod.catalyst_info.desc7": "• 多方塊爐線圈:每級剩餘50%%處理時間", + "jei.useless_mod.catalyst_info.desc8": "• 多方塊爐有用線圈:固定1 tick", + "config.jade.plugin_useless_mod.advanced_alloy_furnace_tasks": "萬象合金爐進行中任務", + "config.jade.plugin_useless_mod.advanced_alloy_furnace_tasks.comment": "顯示萬象合金爐中的AE任務", + "item.useless_mod.ritual_blueprint": "儀式藍圖", + "item.useless_mod.ritual_blueprint.blank": "空白:對已完成的儀式五芒星陣使用", + "item.useless_mod.ritual_blueprint.pentacle": "已刻印五芒星陣:%s", + "item.useless_mod.ritual_blueprint.pentacles": "已刻印五芒星陣(%s個):", + "item.useless_mod.ritual_blueprint.pentacle_entry": " %s", + "item.useless_mod.ritual_blueprint.imprinted": "已為 %s 刻印儀式藍圖", + "item.useless_mod.ritual_blueprint.imprinted_multiple": "已將%s個五芒星陣刻印到儀式藍圖", + "item.useless_mod.ritual_blueprint.no_pentacle": "未找到已完成的儀式五芒星陣", + "item.useless_mod.ritual_blueprint.ambiguous": "找到多個五芒星陣;請在副手持有啟動物品", + "container.useless_mod.ore_generator": "礦石產生器", + "gui.useless_mod.ore_generator.rate": "輸出速率", + "gui.useless_mod.ore_generator.rate_unit": "每槽 / 20 tick", + "gui.useless_mod.ore_generator.apply": "應用", + "gui.useless_mod.ore_generator.ae_toggle": "AE 輸出", + "gui.useless_mod.ore_generator.ae_online": "AE 在線", + "gui.useless_mod.ore_generator.ae_offline": "AE 離線", + "gui.useless_mod.ore_generator.ae_enabled": "AE 輸出:啟用", + "gui.useless_mod.ore_generator.ae_disabled": "AE 輸出:關閉", + "gui.useless_mod.ore_generator.countdown": "下次輸出:%s tick", + "gui.useless_mod.ore_generator.active_slots": "激活槽位:%s / %s", + "gui.useless_mod.ore_generator.locked_slot": "鎖定的樣板槽位", + "useless_mod.configuration.ore_generator_slots": "礦石產生器樣板槽位", + "useless_mod.configuration.ore_generator_slots.comment": "礦石產生器的激活樣板槽位數量。降低配置後,超出範圍的內容仍可在恢復頁取出。", + "useless_mod.configuration.ore_generator_slots.tooltip": "礦石產生器的激活樣板槽位數量。降低配置後,超出範圍的內容仍可在恢復頁取出。", + "useless_mod.configuration.useless_dimension": "無用維度", + "useless_mod.configuration.useless_dimension_floor_block_blacklist": "無用維度地板方塊黑名單", + "useless_mod.configuration.useless_dimension_floor_block_blacklist.comment": "不能用於邊框、填充、層、道路或中心標記的方塊。黑名單優先於白名單。支持精確 ID、方塊標籤和 * 通配符。", + "useless_mod.configuration.useless_dimension_floor_block_blacklist.tooltip": "排除出無用維度層、邊界、道路和中心標記的方塊,支持精確 ID、方塊標籤或 * 通配符。", + "useless_mod.configuration.useless_dimension_floor_block_whitelist": "無用維度地板方塊白名單", + "useless_mod.configuration.useless_dimension_floor_block_whitelist.comment": "非空時只有匹配的方塊可以用於邊框、填充、層、道路或中心標記。留空可關閉白名單。支持精確 ID、方塊標籤和 * 通配符。", + "useless_mod.configuration.useless_dimension_floor_block_whitelist.tooltip": "非空時僅允許匹配的方塊用於無用維度層、邊界、道路和中心標記,黑名單仍然優先。", + "menu.useless_mod.dimension_config": "無用維度配置", + "gui.useless_mod.dimension_config.border_block": "邊框方塊", + "gui.useless_mod.dimension_config.fill_block": "填充方塊", + "gui.useless_mod.dimension_config.center_block": "中心方塊", + "gui.useless_mod.dimension_config.layers": "層數", + "gui.useless_mod.dimension_config.start_y": "起始 Y", + "gui.useless_mod.dimension_config.boundary_alternate_1": "邊界1", + "gui.useless_mod.dimension_config.boundary_alternate_2": "邊界2", + "gui.useless_mod.dimension_config.road_surface": "主體", + "gui.useless_mod.dimension_config.road_edge": "邊緣", + "gui.useless_mod.dimension_config.road_center_marking": "道路中心線", + "gui.useless_mod.dimension_config.center_marker": "中心標記", + "gui.useless_mod.dimension_config.boundary_interval_x": "邊界間隔 X", + "gui.useless_mod.dimension_config.boundary_interval_z": "邊界間隔 Z", + "gui.useless_mod.dimension_config.road_width": "道路寬度", + "gui.useless_mod.dimension_config.road_options": "道路選項", + "gui.useless_mod.dimension_config.center_options": "中心選項", + "gui.useless_mod.dimension_config.mode": "模式", + "gui.useless_mod.dimension_config.mode.road": "馬路", + "gui.useless_mod.dimension_config.mode.multi": "多聯", + "gui.useless_mod.dimension_config.center_marker_on": "中心標記:開", + "gui.useless_mod.dimension_config.center_marker_off": "中心標記:關", + "gui.useless_mod.dimension_config.tooltip.decrease": "數值減少 1", + "gui.useless_mod.dimension_config.tooltip.increase": "數值增加 1", + "gui.useless_mod.dimension_config.tooltip.bedrock": "切換是否生成基岩層", + "gui.useless_mod.dimension_config.tooltip.bottom": "切換是否將基岩層放在底部", + "gui.useless_mod.dimension_config.tooltip.mode": "切換平台模式:馬路 / 多聯", + "gui.useless_mod.dimension_config.tooltip.center_marker": "切換中心標記", + "gui.useless_mod.dimension_config.tooltip.apply": "儲存目前維度配置", + "gui.useless_mod.dimension_config.tooltip.apply_and_teleport": "儲存配置並傳送", + "gui.useless_mod.dimension_config.tooltip.cancel": "關閉且不儲存", + "gui.useless_mod.dimension_config.bedrock_on": "生成基岩層:開", + "gui.useless_mod.dimension_config.bedrock_off": "生成基岩層:關", + "gui.useless_mod.dimension_config.bottom_on": "基岩層置底:開", + "gui.useless_mod.dimension_config.bottom_off": "基岩層置底:關", + "gui.useless_mod.dimension_config.apply": "應用", + "gui.useless_mod.dimension_config.apply_and_teleport": "應用並傳送", + "gui.useless_mod.dimension_config.cancel": "取消", + "gui.useless_mod.dimension_config.invalid": "無效的維度配置", + "gui.useless_mod.dimension_config.export": "匯出", + "gui.useless_mod.dimension_config.import": "匯入", + "gui.useless_mod.dimension_config.tooltip.export": "將目前設定複製到剪貼簿", + "gui.useless_mod.dimension_config.tooltip.import": "從剪貼簿匯入設定", + "gui.useless_mod.dimension_config.exported": "設定已複製到剪貼簿", + "gui.useless_mod.dimension_config.export_invalid": "目前設定不完整,無法匯出", + "gui.useless_mod.dimension_config.imported": "已匯入設定,點擊套用後生效", + "gui.useless_mod.dimension_config.import_confirm": "匯入將取代目前設定,是否繼續?", + "gui.useless_mod.dimension_config.confirm": "確認", + "gui.useless_mod.dimension_config.cancel_import": "取消匯入", + "gui.useless_mod.dimension_config.error.invalid_text": "剪貼簿內容不是有效的設定文字", + "gui.useless_mod.dimension_config.error.unsupported_version": "設定版本不受支援", + "gui.useless_mod.dimension_config.error.invalid_structure": "設定結構不完整", + "gui.useless_mod.dimension_config.error.blocked_block": "設定包含被停用的方塊", + "gui.useless_mod.dimension_config.error.limit": "設定文字過長", + "useless_mod.configuration.furnace_tier_0_threads": "單方塊萬象爐 0 階執行緒數", + "useless_mod.configuration.furnace_tier_1_threads": "單方塊萬象爐 1 階執行緒數", + "useless_mod.configuration.furnace_tier_2_threads": "單方塊萬象爐 2 階執行緒數", + "useless_mod.configuration.furnace_tier_3_threads": "單方塊萬象爐 3 階執行緒數", + "useless_mod.configuration.furnace_tier_4_threads": "單方塊萬象爐 4 階執行緒數", + "useless_mod.configuration.furnace_tier_5_threads": "單方塊萬象爐 5 階執行緒數", + "useless_mod.configuration.furnace_tier_6_threads": "單方塊萬象爐 6 階執行緒數", + "useless_mod.configuration.furnace_tier_7_threads": "單方塊萬象爐 7 階執行緒數", + "useless_mod.configuration.furnace_tier_8_threads": "單方塊萬象爐 8 階執行緒數", + "useless_mod.configuration.furnace_tier_9_threads": "單方塊萬象爐 9 階執行緒數", + "useless_mod.configuration.furnace_tier_10_threads": "單方塊萬象爐 10 階執行緒數", + "useless_mod.configuration.catalyst_tier_0_parallel": "催化劑 0 階並行數", + "useless_mod.configuration.catalyst_tier_0_energy_divisor": "催化劑 0 階能耗除數", + "useless_mod.configuration.catalyst_tier_0_time_multiplier": "催化劑 0 階時間倍率", + "useless_mod.configuration.catalyst_tier_1_parallel": "催化劑 1 階並行數", + "useless_mod.configuration.catalyst_tier_1_energy_divisor": "催化劑 1 階能耗除數", + "useless_mod.configuration.catalyst_tier_1_time_multiplier": "催化劑 1 階時間倍率", + "useless_mod.configuration.catalyst_tier_2_parallel": "催化劑 2 階並行數", + "useless_mod.configuration.catalyst_tier_2_energy_divisor": "催化劑 2 階能耗除數", + "useless_mod.configuration.catalyst_tier_2_time_multiplier": "催化劑 2 階時間倍率", + "useless_mod.configuration.catalyst_tier_3_parallel": "催化劑 3 階並行數", + "useless_mod.configuration.catalyst_tier_3_energy_divisor": "催化劑 3 階能耗除數", + "useless_mod.configuration.catalyst_tier_3_time_multiplier": "催化劑 3 階時間倍率", + "useless_mod.configuration.catalyst_tier_4_parallel": "催化劑 4 階並行數", + "useless_mod.configuration.catalyst_tier_4_energy_divisor": "催化劑 4 階能耗除數", + "useless_mod.configuration.catalyst_tier_4_time_multiplier": "催化劑 4 階時間倍率", + "useless_mod.configuration.catalyst_tier_5_parallel": "催化劑 5 階並行數", + "useless_mod.configuration.catalyst_tier_5_energy_divisor": "催化劑 5 階能耗除數", + "useless_mod.configuration.catalyst_tier_5_time_multiplier": "催化劑 5 階時間倍率", + "useless_mod.configuration.catalyst_tier_6_parallel": "催化劑 6 階並行數", + "useless_mod.configuration.catalyst_tier_6_energy_divisor": "催化劑 6 階能耗除數", + "useless_mod.configuration.catalyst_tier_6_time_multiplier": "催化劑 6 階時間倍率", + "useless_mod.configuration.catalyst_tier_7_parallel": "催化劑 7 階並行數", + "useless_mod.configuration.catalyst_tier_7_energy_divisor": "催化劑 7 階能耗除數", + "useless_mod.configuration.catalyst_tier_7_time_multiplier": "催化劑 7 階時間倍率", + "useless_mod.configuration.catalyst_tier_8_parallel": "催化劑 8 階並行數", + "useless_mod.configuration.catalyst_tier_8_energy_divisor": "催化劑 8 階能耗除數", + "useless_mod.configuration.catalyst_tier_8_time_multiplier": "催化劑 8 階時間倍率", + "useless_mod.configuration.catalyst_tier_9_parallel": "催化劑 9 階並行數", + "useless_mod.configuration.catalyst_tier_9_energy_divisor": "催化劑 9 階能耗除數", + "useless_mod.configuration.catalyst_tier_9_time_multiplier": "催化劑 9 階時間倍率", + "useless_mod.configuration.coil_tier_1_threads": "多方塊線圈 1 階執行緒數", + "useless_mod.configuration.coil_tier_1_single_task_parallel": "多方塊線圈 1 階單任務並行數", + "useless_mod.configuration.coil_tier_1_energy_divisor": "多方塊線圈 1 階能耗除數", + "useless_mod.configuration.coil_tier_1_time_multiplier": "多方塊線圈 1 階時間倍率", + "useless_mod.configuration.coil_tier_2_threads": "多方塊線圈 2 階執行緒數", + "useless_mod.configuration.coil_tier_2_single_task_parallel": "多方塊線圈 2 階單任務並行數", + "useless_mod.configuration.coil_tier_2_energy_divisor": "多方塊線圈 2 階能耗除數", + "useless_mod.configuration.coil_tier_2_time_multiplier": "多方塊線圈 2 階時間倍率", + "useless_mod.configuration.coil_tier_3_threads": "多方塊線圈 3 階執行緒數", + "useless_mod.configuration.coil_tier_3_single_task_parallel": "多方塊線圈 3 階單任務並行數", + "useless_mod.configuration.coil_tier_3_energy_divisor": "多方塊線圈 3 階能耗除數", + "useless_mod.configuration.coil_tier_3_time_multiplier": "多方塊線圈 3 階時間倍率", + "useless_mod.configuration.coil_tier_4_threads": "多方塊線圈 4 階執行緒數", + "useless_mod.configuration.coil_tier_4_single_task_parallel": "多方塊線圈 4 階單任務並行數", + "useless_mod.configuration.coil_tier_4_energy_divisor": "多方塊線圈 4 階能耗除數", + "useless_mod.configuration.coil_tier_4_time_multiplier": "多方塊線圈 4 階時間倍率", + "useless_mod.configuration.coil_tier_5_threads": "多方塊線圈 5 階執行緒數", + "useless_mod.configuration.coil_tier_5_single_task_parallel": "多方塊線圈 5 階單任務並行數", + "useless_mod.configuration.coil_tier_5_energy_divisor": "多方塊線圈 5 階能耗除數", + "useless_mod.configuration.coil_tier_5_time_multiplier": "多方塊線圈 5 階時間倍率", + "useless_mod.configuration.coil_tier_6_threads": "多方塊線圈 6 階執行緒數", + "useless_mod.configuration.coil_tier_6_single_task_parallel": "多方塊線圈 6 階單任務並行數", + "useless_mod.configuration.coil_tier_6_energy_divisor": "多方塊線圈 6 階能耗除數", + "useless_mod.configuration.coil_tier_6_time_multiplier": "多方塊線圈 6 階時間倍率", + "useless_mod.configuration.coil_tier_7_threads": "多方塊線圈 7 階執行緒數", + "useless_mod.configuration.coil_tier_7_single_task_parallel": "多方塊線圈 7 階單任務並行數", + "useless_mod.configuration.coil_tier_7_energy_divisor": "多方塊線圈 7 階能耗除數", + "useless_mod.configuration.coil_tier_7_time_multiplier": "多方塊線圈 7 階時間倍率", + "useless_mod.configuration.coil_tier_8_threads": "多方塊線圈 8 階執行緒數", + "useless_mod.configuration.coil_tier_8_single_task_parallel": "多方塊線圈 8 階單任務並行數", + "useless_mod.configuration.coil_tier_8_energy_divisor": "多方塊線圈 8 階能耗除數", + "useless_mod.configuration.coil_tier_8_time_multiplier": "多方塊線圈 8 階時間倍率", + "useless_mod.configuration.coil_tier_9_threads": "多方塊線圈 9 階執行緒數", + "useless_mod.configuration.coil_tier_9_single_task_parallel": "多方塊線圈 9 階單任務並行數", + "useless_mod.configuration.coil_tier_9_energy_divisor": "多方塊線圈 9 階能耗除數", + "useless_mod.configuration.coil_tier_9_time_multiplier": "多方塊線圈 9 階時間倍率", + "useless_mod.configuration.useful_tier_threads": "多方塊有用級執行緒數", + "block.useless_mod.useless_compact_c9": "無用型緊湊C9", + "block.useless_mod.useless_compact_f9": "無用型緊湊F9", + "block.useless_mod.useless_compact_l9": "無用型緊湊L9", + "tooltip.useless_mod.useless_compact_c9": "單方塊等價於八聯滿配 C9:8 台滿長高能網路交換主機合併後的極限模式。", + "tooltip.useless_mod.useless_compact_f9": "單方塊等價於八聯滿配 F9:8 台滿長高能網路交換主機,88 條 FX 執行通道,虛擬合成。", + "tooltip.useless_mod.useless_compact_l9": "單方塊 L9 儲存主機,直接執行 ECO 無限儲存。", + "gui.useless_mod.dimension_config.preview": "預覽", + "gui.useless_mod.dimension_config.preview.top": "俯視", + "gui.useless_mod.dimension_config.preview.side": "剖面", + "gui.useless_mod.dimension_config.preview.period": "週期:%s × %s 格", + "gui.useless_mod.dimension_config.preview.textured": "渲染:真實材質", + "gui.useless_mod.dimension_config.preview.color": "渲染:色塊", + "gui.useless_mod.dimension_config.preview.blocks": "渲染:色塊(1 像素 = %s 格)", + "gui.useless_mod.dimension_config.preview.range": "Y %s ~ %s,共 %s 層", + "gui.useless_mod.dimension_config.preview.bedrock": "基岩 Y=%s", + "gui.useless_mod.dimension_config.preview.empty": "目前設定無法預覽", + "gui.useless_mod.dimension_config.preview.role.road": "道路主體", + "gui.useless_mod.dimension_config.preview.role.road_edge": "道路邊緣", + "gui.useless_mod.dimension_config.preview.role.road_marking": "道路中心線", + "gui.useless_mod.dimension_config.preview.role.marker": "中心標記", + "gui.useless_mod.dimension_config.preview.role.boundary": "邊界", + "gui.useless_mod.dimension_config.preview.role.border": "邊框", + "gui.useless_mod.dimension_config.preview.role.center": "中心", + "gui.useless_mod.dimension_config.preview.role.fill": "填充", + "gui.useless_mod.dimension_config.preview.close": "關閉", + "gui.useless_mod.dimension_config.tooltip.preview": "預覽目前設定的生成結果", + "tooltip.useless_mod.beef_wireless_logistics_mode": "無線物流模式", + "key.useless_mod.open_wireless_logistics": "開啟無線物流介面", + "menu.useless_mod.wireless_logistics": "無線物流網路", + "gui.useless_mod.wireless_logistics.anchors": "已綁定容器 (%s)", + "gui.useless_mod.wireless_logistics.network_position": "網路 %s/%s", + "gui.useless_mod.wireless_logistics.network_name_hint": "網路名", + "gui.useless_mod.wireless_logistics.network_owned_hint": "網路掛在你的帳號名下(組隊後由隊伍共享),與手持哪把杖無關", + "gui.useless_mod.wireless_logistics.network_new": "新建", + "gui.useless_mod.wireless_logistics.network_switch_hint": "切換網路(手持杖 Shift+滾輪同效)", + "gui.useless_mod.wireless_logistics.network_switched": "已切換到網路 %s/%s:%s", + "gui.useless_mod.wireless_logistics.network_unnamed": "未命名網路", + "gui.useless_mod.wireless_logistics.search_hint": "搜尋容器(支援拼音)", + "gui.useless_mod.wireless_logistics.search_no_match": "沒有符合的容器", + "gui.useless_mod.wireless_logistics.empty": "尚未綁定任何容器(潛行右鍵容器綁定)", + "gui.useless_mod.wireless_logistics.route_label": "線路 %s", + "gui.useless_mod.wireless_logistics.filter": "過濾器", + "gui.useless_mod.wireless_logistics.stats": "上次 %s/%s", + "gui.useless_mod.wireless_logistics.stats_none": "上次 未搬運", + "gui.useless_mod.wireless_logistics.stats_hint": "上次搬運:實際搬走 %s,請求 %s,分給 %s 個輸出", + "gui.useless_mod.wireless_logistics.stats_blocked": "上次 %s/%s · %s", + "gui.useless_mod.wireless_logistics.blocker.source_unreachable": "來源不可達", + "gui.useless_mod.wireless_logistics.blocker.target_unreachable": "目標不可達", + "gui.useless_mod.wireless_logistics.blocker.filtered": "被過濾", + "gui.useless_mod.wireless_logistics.blocker.source_empty": "來源已空", + "gui.useless_mod.wireless_logistics.blocker.source_rejected": "來源不給抽", + "gui.useless_mod.wireless_logistics.blocker.target_rejected": "目標不收", + "gui.useless_mod.wireless_logistics.flow_label": "流向:%s", + "gui.useless_mod.wireless_logistics.medium_label": "資源:%s", + "gui.useless_mod.wireless_logistics.enabled_label": "啟用:%s", + "gui.useless_mod.wireless_logistics.side_label": "面:%s", + "gui.useless_mod.wireless_logistics.weight_label": "權重", + "gui.useless_mod.wireless_logistics.weight_label_hint": "數值大的輸出優先分到東西", + "gui.useless_mod.wireless_logistics.amount_label": "數量", + "gui.useless_mod.wireless_logistics.amount_label_hint": "每個輸出各搬多少;一個輸入接 N 個輸出時這一輪最多搬「數量 × N」", + "gui.useless_mod.wireless_logistics.interval_label": "週期", + "gui.useless_mod.wireless_logistics.interval_label_hint": "兩次搬運之間間隔的 tick 數", + "gui.useless_mod.wireless_logistics.release_only_hint": "只有「輸出」端發起搬運,輸入端的這項不生效", + "gui.useless_mod.wireless_logistics.rename": "容器名", + "gui.useless_mod.wireless_logistics.range": "可填 %s ~ %s", + "gui.useless_mod.wireless_logistics.range_min": "可填 %s 以上,支援 K/M/G/T/P/E", + + "gui.useless_mod.wireless_logistics.highlight_hint": "手持造化杖並開啟無線物流模式時,網路內容器會自動高亮(暖色=輸出 / 冷色=輸入 / 灰色=已停用)", + "gui.useless_mod.wireless_logistics.multi_select_hint": "Ctrl+點擊多選,Shift+點擊選取整段,批量編輯選中的容器", + "gui.useless_mod.wireless_logistics.anchor_order_hint": "行首 ▲▼ 調整次序:權重相同時靠前的先拿(餘數也留給靠前的)", + "gui.useless_mod.wireless_logistics.multi_select_active": "已加入批量編輯", + "gui.useless_mod.wireless_logistics.multi_select_count": "已選 %s 台", + "gui.useless_mod.wireless_logistics.config_copied": "已複製當前線路設定", + "gui.useless_mod.wireless_logistics.config_pasted": "已貼到當前線路", + "gui.useless_mod.wireless_logistics.config_copy_hint": "線路 %s · Ctrl+C 複製設定 / Ctrl+V 貼上設定", + "gui.useless_mod.wireless_logistics.select_all": "全選", + "gui.useless_mod.wireless_logistics.clear_selection": "清空", + "gui.useless_mod.wireless_logistics.apply_all": "應用到全部", + "gui.useless_mod.wireless_logistics.apply_all_same_name": "套用: 同名容器", + "gui.useless_mod.wireless_logistics.apply_all_hint": "把當前線路配置套用到列表裡的容器(搜尋時只作用於符合的項目)", + "gui.useless_mod.wireless_logistics.apply_all_same_name_hint": "把當前線路配置套用給所有「介面顯示名」與當前容器相同的容器", + "gui.useless_mod.wireless_logistics.filter_button": "過濾器 (%s)", + "gui.useless_mod.wireless_logistics.filter_button_none": "過濾器", + "gui.useless_mod.wireless_logistics.filter_panel_title": "過濾器 —— 已用 %s 格", + "gui.useless_mod.wireless_logistics.filter_panel_hint": "右鍵格子可開啟詳細設定", + "gui.useless_mod.wireless_logistics.filter_close": "關閉", + "gui.useless_mod.wireless_logistics.filter_close_hint": "關閉過濾器面板(按 Esc 也可以)", + "gui.useless_mod.wireless_logistics.filter_clear": "清空", + "gui.useless_mod.wireless_logistics.filter_clear_hint": "清掉這條線路上的全部過濾格", + "gui.useless_mod.wireless_logistics.filter_hint": "物品線路放物品,流體線路放流體(或容器),化學品線路放儲罐;可直接從 JEI 拖入", + "gui.useless_mod.wireless_logistics.filter_slot_title": "槽位 %s", + "gui.useless_mod.wireless_logistics.filter_detail_done": "完成", + "gui.useless_mod.wireless_logistics.filter_detail_hint": "右鍵格子可開啟詳細設定", + "gui.useless_mod.wireless_logistics.filter_exclude_off": "白名單", + "gui.useless_mod.wireless_logistics.filter_exclude_on": "黑名單", + "gui.useless_mod.wireless_logistics.filter_exclude_hint": "黑名單:標記命中的資源不搬運;白名單:只搬標記命中的", + "gui.useless_mod.wireless_logistics.filter_cond_out_label": "輸出端", + "gui.useless_mod.wireless_logistics.filter_cond_in_label": "輸入端", + "gui.useless_mod.wireless_logistics.filter_cond_out_hint": "在輸出容器上測量", + "gui.useless_mod.wireless_logistics.filter_cond_in_hint": "在輸入容器上測量", + "gui.useless_mod.wireless_logistics.filter_cond_op_off": "關", + "gui.useless_mod.wireless_logistics.filter_cond_op_at_least": "≥", + "gui.useless_mod.wireless_logistics.filter_cond_op_at_most": "≤", + "gui.useless_mod.wireless_logistics.filter_cond_self": "自身", + "gui.useless_mod.wireless_logistics.filter_cond_value_hint": "留空 = 關閉;支援 K / M / G / T / P / E", + "gui.useless_mod.wireless_logistics.filter_pattern_hint": "#標籤 或通配符,例如 #c:ingots 、 *iron*", + "gui.useless_mod.wireless_logistics.filter_pattern_tag": "按物品 / 流體標籤匹配", + "gui.useless_mod.wireless_logistics.filter_pattern_glob": "按註冊名 / 顯示名匹配;* 任意長,? 一個字元", + "gui.useless_mod.wireless_logistics.filter_pattern_unresolved": "沒有資源使用這條標籤,一個物品都不會搬", + "gui.useless_mod.wireless_logistics.filter_pattern_unsupported": "化學品不支援標籤,請改用通配符", + "gui.useless_mod.wireless_logistics.filter_pattern_invalid": "「%s」不是合法模式:請用 #標籤,或帶上 * / ? 通配符", + "gui.useless_mod.wireless_logistics.filter_right_click_hint": "右鍵格子可輸入 #標籤 或通配符", + "gui.useless_mod.wireless_logistics.ae_whitelist_warning": "AE 輸出端 + 無過濾:不會搬出任何物品", + "gui.useless_mod.wireless_logistics.state_on": "開", + "gui.useless_mod.wireless_logistics.state_off": "關", + "gui.useless_mod.wireless_logistics.flow.absorb": "輸入", + "gui.useless_mod.wireless_logistics.flow.release": "輸出", + "gui.useless_mod.wireless_logistics.medium.item": "物品", + "gui.useless_mod.wireless_logistics.medium.ae_item": "AE 物品", + "gui.useless_mod.wireless_logistics.medium.fluid": "流體", + "gui.useless_mod.wireless_logistics.medium.ae_fluid": "AE 流體", + "gui.useless_mod.wireless_logistics.medium.energy": "能量", + "gui.useless_mod.wireless_logistics.medium.chemical": "化學品", + "gui.useless_mod.wireless_logistics.medium.ae_chemical": "AE 化學品", + "gui.useless_mod.wireless_logistics.medium.source": "魔源", + "gui.useless_mod.wireless_logistics.medium.ae_source": "AE 魔源", + "gui.useless_mod.wireless_logistics.medium.ae_energy": "AE 能量", + "gui.useless_mod.wireless_logistics.medium.stress": "應力", + "gui.useless_mod.wireless_logistics.medium.pressure": "氣壓", + "gui.useless_mod.wireless_logistics.medium.ae_pressure": "AE 氣壓", + "gui.useless_mod.wireless_logistics.pressure.target_label": "目標氣壓", + "gui.useless_mod.wireless_logistics.pressure.target_hint": "輸入端被伺服到的目標氣壓(bar)", + "gui.useless_mod.wireless_logistics.pressure.range": "範圍:-1.0 ~ 20.0 bar(-1 為絕對真空)", + "gui.useless_mod.thermostat.title": "溫度調節器", + "gui.useless_mod.thermostat.temperature": "溫度 (K)", + "gui.useless_mod.thermostat.kelvin": "溫度 (K)", + "gui.useless_mod.thermostat.celsius": "= %s °C", + "gui.useless_mod.thermostat.hint": "支援 K/M/G 縮寫,例如 1M = 1000000 K", + "gui.useless_mod.thermostat.enabled": "恆溫:開(點擊關閉)", + "gui.useless_mod.thermostat.disabled": "恆溫:關(點擊開啟)", + "gui.useless_mod.thermostat.done": "完成", + "jade.useless_mod.thermostat.temperature": "溫度:%s K(%s °C)", + "jade.useless_mod.thermostat.disabled": "恆溫:關閉", + "config.jade.plugin_useless_mod.temperature_regulator": "溫度調節器", + "config.jade.plugin_useless_mod.temperature_regulator.comment": "顯示溫度調節器的設定溫度", + "gui.useless_mod.wireless_logistics.stress.rpm_label": "轉速", + "gui.useless_mod.wireless_logistics.stress.rpm_hint": "目標網路被驅動的旋轉速度", + "gui.useless_mod.wireless_logistics.stress.direction_label": "方向", + "gui.useless_mod.wireless_logistics.stress.clockwise": "順時針", + "gui.useless_mod.wireless_logistics.stress.counter_clockwise": "逆時針", + "gui.useless_mod.wireless_logistics.stress.input_side_hint": "轉速與方向配在輸入端", + "gui.useless_mod.wireless_logistics.stress.status_breakdown": "自身 %s 網路總計 %s 剩餘 %s SU", + "gui.useless_mod.wireless_logistics.stress.status": "狀態:%s", + "gui.useless_mod.wireless_logistics.stress.state.ok": "已連接", + "gui.useless_mod.wireless_logistics.stress.state.disabled": "線路已關閉", + "gui.useless_mod.wireless_logistics.stress.state.idle": "來源網路沒有轉動", + "gui.useless_mod.wireless_logistics.stress.state.no_network": "來源方塊不在任何動力網路裡", + "gui.useless_mod.wireless_logistics.stress.state.not_kinetic": "不是動力學方塊", + "gui.useless_mod.wireless_logistics.stress.state.missing_io": "缺少輸入或輸出", + "gui.useless_mod.wireless_logistics.stress.state.no_power": "來源網路沒有可用的應力", + "gui.useless_mod.wireless_logistics.stress.state.no_share": "分到的應力為 0:富餘已被優先級更高的輸出分完", + "gui.useless_mod.wireless_logistics.stress.state.fed_elsewhere": "該網路的應力已由其它無線輸出補足", + "gui.useless_mod.wireless_logistics.stress.state.self_fed": "該網路自身就帶著動力來源", + "gui.useless_mod.wireless_logistics.stress.state.real_source": "目標網路已有真實動力來源", + "gui.useless_mod.wireless_logistics.stress.state.speed_conflict": "該網路已被另一條輸出用別的轉速驅動", + "gui.useless_mod.wireless_logistics.stress.state.shared": "該網路已被另一條線路驅動", + "gui.useless_mod.wireless_logistics.stress.chat.real_source": "無線應力已暫停:目標動力網路已有真實動力來源。", + "gui.useless_mod.wireless_logistics.stress.chat.speed_conflict": "無線應力已暫停:同一網路上存在衝突的目標轉速。", + "gui.useless_mod.wireless_logistics.stress.chat.shared": "無線應力已暫停:該網路已由另一條線路驅動。", + "gui.useless_mod.wireless_logistics.stress.chat.conflict": "無線應力輸出因衝突已暫停。", + "gui.useless_mod.wireless_logistics.side.null": "所有面", + "gui.useless_mod.wireless_logistics.side.up": "上面", + "gui.useless_mod.wireless_logistics.side.down": "下面", + "gui.useless_mod.wireless_logistics.side.north": "北面", + "gui.useless_mod.wireless_logistics.side.south": "南面", + "gui.useless_mod.wireless_logistics.side.west": "西面", + "gui.useless_mod.wireless_logistics.side.east": "東面", + "gui.useless_mod.wireless_logistics.dissolve": "解散網路", + "gui.useless_mod.wireless_logistics.bound": "已綁定 %s 到無線物流網路", + "gui.useless_mod.wireless_logistics.unbound": "已從無線物流網路解綁 %s", + "gui.useless_mod.wireless_logistics.not_bindable": "該方塊沒有可搬運的庫存", + "gui.useless_mod.omniversal_pattern.uploaded": "萬象樣板已上傳至多方塊合金爐:%s,維度:%s(%s 格外)", + "gui.useless_mod.omniversal_pattern.duplicate": "多方塊合金爐中已存在相同的萬象樣板,本次不再上傳,已退回空白樣板", + "gui.useless_mod.omniversal_pattern.uploaded.tooltip": "點擊傳送至該多方塊合金爐", + "commands.useless_mod.patterns.repair.player_only": "該指令僅玩家可執行:修復需要玩家身上已綁定的無線終端來解析網路", + "commands.useless_mod.patterns.repair.unbound": "背包中未找到已綁定的無線終端,無法確定要修復的 AE 網路", + "commands.useless_mod.patterns.repair.grid_unavailable": "已找到無線終端,但無法解析到 AE 網路(可能超出範圍或未連接接入點)", + "commands.useless_mod.patterns.repair.no_machines": "該網路上沒有已通電的萬象爐或 ME 樣板總成,無需修復", + "commands.useless_mod.patterns.repair.summary": "已掃描 %s 張萬象樣板,發現 %s 張失效;已修復 %s 張,%s 張無法修復(配方已移除或存在歧義)。", + "commands.useless_mod.patterns.repair.summary.preview": "已掃描 %s 張萬象樣板,發現 %s 張失效;其中 %s 張可修復,%s 張無法修復(配方已移除或存在歧義)。本次為預覽,未寫回任何槽位。", + "commands.useless_mod.patterns.repair.detail": "無法修復:%s,綁定配方 %s" +} diff --git a/src/main/resources/assets/useless_mod/models/block/advanced_alloy_furnace.json b/src/main/resources/assets/useless_mod/models/block/advanced_alloy_furnace.json index 8c563c46..732a3eb2 100644 --- a/src/main/resources/assets/useless_mod/models/block/advanced_alloy_furnace.json +++ b/src/main/resources/assets/useless_mod/models/block/advanced_alloy_furnace.json @@ -1,1085 +1,299 @@ { - "format_version": "1.9.0", - "credit": "Made with _leng", + "format_version": "1.21.6", + "credit": "Made with blockbench,made by xiabi418", "render_type": "translucent", + "texture_size": [64, 64], "textures": { - "0": "useless_mod:block/advanced_alloy_furnace", - "1": "useless_mod:block/advanced_alloy_furnace_flame" + "12": "useless_mod:block/advanced_alloy_furnace", + "particle": "useless_mod:block/advanced_alloy_furnace" }, "elements": [ { - "from": [12, 1, 0], - "to": [13, 6, 2], + "from": [3, 0, 0], + "to": [13, 1, 1], "faces": { - "north": {"uv": [8.375, 9.75, 8.5, 10.375], "texture": "#0"}, - "east": {"uv": [3.625, 5.75, 3.875, 6.375], "texture": "#0"}, - "south": {"uv": [9.125, 9.75, 9.25, 10.375], "texture": "#0"}, - "west": {"uv": [3.75, 8.25, 4, 8.875], "texture": "#0"}, - "up": {"uv": [6.125, 4, 6, 3.75], "texture": "#0"}, - "down": {"uv": [8, 10.75, 7.875, 11], "texture": "#0"} + "north": {"uv": [0.75, 3.75, 3.25, 4], "texture": "#12"}, + "up": {"uv": [8, 1.25, 10.5, 1.5], "texture": "#12"}, + "down": {"uv": [1, 11.75, 3, 12], "texture": "#12"} } }, { - "from": [1.001, 4.95312, 13.6803], - "to": [4.001, 7.95312, 15.6803], - "rotation": {"angle": -45, "axis": "x", "origin": [8, 4.65, 14.1]}, + "from": [13, 0, 0], + "to": [16, 1, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [3, 0, 0]}, "faces": { - "north": {"uv": [4.875, 9.125, 5.25, 9.5], "texture": "#0"}, - "east": {"uv": [4.125, 9.625, 4.375, 10], "texture": "#0"}, - "south": {"uv": [5.375, 9.125, 5.75, 9.5], "texture": "#0"}, - "west": {"uv": [4.5, 9.625, 4.75, 10], "texture": "#0"}, - "up": {"uv": [5.25, 9.875, 4.875, 9.625], "texture": "#0"}, - "down": {"uv": [5.75, 9.625, 5.375, 9.875], "texture": "#0"} - } - }, - { - "from": [3, 1, 14], - "to": [4, 6, 16], - "faces": { - "north": {"uv": [1.25, 10, 1.375, 10.625], "texture": "#0"}, - "east": {"uv": [1.875, 9, 2.125, 9.625], "texture": "#0"}, - "south": {"uv": [1.5, 10, 1.625, 10.625], "texture": "#0"}, - "west": {"uv": [2.25, 9, 2.5, 9.625], "texture": "#0"}, - "up": {"uv": [11.125, 0.625, 11, 0.375], "texture": "#0"}, - "down": {"uv": [11.125, 0.75, 11, 1], "texture": "#0"} - } - }, - { - "from": [1, 1, 13], - "to": [15, 6, 14], - "faces": { - "north": {"uv": [6.25, 3.75, 8, 4.375], "texture": "#0"}, - "east": {"uv": [1.75, 10, 1.875, 10.625], "texture": "#0"}, - "south": {"uv": [6.25, 4.5, 8, 5.125], "texture": "#0"}, - "west": {"uv": [2, 10, 2.125, 10.625], "texture": "#0"}, - "up": {"uv": [1.75, 8.75, 0, 8.625], "texture": "#0"}, - "down": {"uv": [10.375, 0.75, 8.625, 0.875], "texture": "#0"} - } - }, - { - "from": [1, 1, 2], - "to": [15, 6, 3], - "faces": { - "north": {"uv": [1.75, 6.625, 3.5, 7.25], "texture": "#0"}, - "east": {"uv": [5.625, 10, 5.75, 10.625], "texture": "#0"}, - "south": {"uv": [7, 2, 8.75, 2.625], "texture": "#0"}, - "west": {"uv": [6.625, 10, 6.75, 10.625], "texture": "#0"}, - "up": {"uv": [10.375, 1.125, 8.625, 1], "texture": "#0"}, - "down": {"uv": [10.375, 1.25, 8.625, 1.375], "texture": "#0"} - } - }, - { - "from": [1, 1, 14], - "to": [2, 6, 16], - "faces": { - "north": {"uv": [0.75, 10, 0.875, 10.625], "texture": "#0"}, - "east": {"uv": [4.5, 8.875, 4.75, 9.5], "texture": "#0"}, - "south": {"uv": [1, 10, 1.125, 10.625], "texture": "#0"}, - "west": {"uv": [8.875, 8.625, 9.125, 9.25], "texture": "#0"}, - "up": {"uv": [10.25, 11.125, 10.125, 10.875], "texture": "#0"}, - "down": {"uv": [11, 10.625, 10.875, 10.875], "texture": "#0"} - } - }, - { - "from": [11.999, 4.95312, 13.6803], - "to": [14.999, 7.95312, 15.6803], - "rotation": {"angle": -45, "axis": "x", "origin": [8, 4.65, 14.1]}, - "faces": { - "north": {"uv": [7.875, 9, 8.25, 9.375], "texture": "#0"}, - "east": {"uv": [0, 9.625, 0.25, 10], "texture": "#0"}, - "south": {"uv": [8.375, 9, 8.75, 9.375], "texture": "#0"}, - "west": {"uv": [0.375, 9.625, 0.625, 10], "texture": "#0"}, - "up": {"uv": [1.125, 9.875, 0.75, 9.625], "texture": "#0"}, - "down": {"uv": [1.625, 9.625, 1.25, 9.875], "texture": "#0"} - } - }, - { - "from": [14, 1, 14], - "to": [15, 6, 16], - "faces": { - "north": {"uv": [9.625, 9.875, 9.75, 10.5], "texture": "#0"}, - "east": {"uv": [8.875, 2, 9.125, 2.625], "texture": "#0"}, - "south": {"uv": [9.875, 9.875, 10, 10.5], "texture": "#0"}, - "west": {"uv": [4.125, 8.875, 4.375, 9.5], "texture": "#0"}, - "up": {"uv": [11, 7.875, 10.875, 7.625], "texture": "#0"}, - "down": {"uv": [11, 9.25, 10.875, 9.5], "texture": "#0"} - } - }, - { - "from": [12, 1, 14], - "to": [13, 6, 16], - "faces": { - "north": {"uv": [8.875, 9.875, 9, 10.5], "texture": "#0"}, - "east": {"uv": [1.125, 8.875, 1.375, 9.5], "texture": "#0"}, - "south": {"uv": [9.375, 9.875, 9.5, 10.5], "texture": "#0"}, - "west": {"uv": [1.5, 8.875, 1.75, 9.5], "texture": "#0"}, - "up": {"uv": [4.375, 11.125, 4.25, 10.875], "texture": "#0"}, - "down": {"uv": [4.625, 10.875, 4.5, 11.125], "texture": "#0"} - } - }, - { - "from": [10, 1, 14], - "to": [11, 6, 15], - "faces": { - "north": {"uv": [2.25, 10, 2.375, 10.625], "texture": "#0"}, - "east": {"uv": [3.625, 10, 3.75, 10.625], "texture": "#0"}, - "south": {"uv": [10, 3.625, 10.125, 4.25], "texture": "#0"}, - "west": {"uv": [3.875, 10, 4, 10.625], "texture": "#0"}, - "up": {"uv": [11.125, 5.625, 11, 5.5], "texture": "#0"}, - "down": {"uv": [11.125, 6.625, 11, 6.75], "texture": "#0"} - } - }, - { - "from": [5, 1, 14], - "to": [6, 6, 15], - "faces": { - "north": {"uv": [10, 4.375, 10.125, 5], "texture": "#0"}, - "east": {"uv": [4.875, 10, 5, 10.625], "texture": "#0"}, - "south": {"uv": [5.125, 10, 5.25, 10.625], "texture": "#0"}, - "west": {"uv": [5.375, 10, 5.5, 10.625], "texture": "#0"}, - "up": {"uv": [7, 11.125, 6.875, 11], "texture": "#0"}, - "down": {"uv": [7.25, 11, 7.125, 11.125], "texture": "#0"} - } - }, - { - "from": [10, 1, 1], - "to": [11, 6, 2], - "faces": { - "north": {"uv": [7.875, 10, 8, 10.625], "texture": "#0"}, - "east": {"uv": [8.125, 10, 8.25, 10.625], "texture": "#0"}, - "south": {"uv": [10, 8.625, 10.125, 9.25], "texture": "#0"}, - "west": {"uv": [0, 10.125, 0.125, 10.75], "texture": "#0"}, - "up": {"uv": [11, 11.125, 10.875, 11], "texture": "#0"}, - "down": {"uv": [11.25, 0, 11.125, 0.125], "texture": "#0"} - } - }, - { - "from": [6, 5, 1], - "to": [10, 6, 2], - "faces": { - "north": {"uv": [10, 1.75, 10.5, 1.875], "texture": "#0"}, - "east": {"uv": [11, 10.125, 11.125, 10.25], "texture": "#0"}, - "south": {"uv": [10.125, 2.75, 10.625, 2.875], "texture": "#0"}, - "west": {"uv": [11, 10.375, 11.125, 10.5], "texture": "#0"}, - "up": {"uv": [10.875, 10.25, 10.375, 10.125], "texture": "#0"}, - "down": {"uv": [10.875, 10.375, 10.375, 10.5], "texture": "#0"} - } - }, - { - "from": [6, 5, 14], - "to": [10, 6, 15], - "faces": { - "north": {"uv": [10.125, 2.75, 10.625, 2.875], "texture": "#0"}, - "east": {"uv": [11, 10.375, 11.125, 10.5], "texture": "#0"}, - "south": {"uv": [10, 1.75, 10.5, 1.875], "texture": "#0"}, - "west": {"uv": [11, 10.125, 11.125, 10.25], "texture": "#0"}, - "up": {"uv": [10.875, 10.25, 10.375, 10.125], "rotation": 180, "texture": "#0"}, - "down": {"uv": [10.875, 10.375, 10.375, 10.5], "rotation": 180, "texture": "#0"} - } - }, - { - "from": [9, 6, 1], - "to": [10, 7, 2], - "faces": { - "north": {"uv": [11.125, 1.375, 11.25, 1.5], "texture": "#0"}, - "east": {"uv": [11.125, 3.625, 11.25, 3.75], "texture": "#0"}, - "south": {"uv": [11.125, 7.625, 11.25, 7.75], "texture": "#0"}, - "west": {"uv": [7.875, 11.125, 8, 11.25], "texture": "#0"}, - "up": {"uv": [8.25, 11.25, 8.125, 11.125], "texture": "#0"}, - "down": {"uv": [8.5, 11.125, 8.375, 11.25], "texture": "#0"} - } - }, - { - "from": [5, 6, 2], - "to": [11, 7, 3], - "faces": { - "north": {"uv": [7.5, 9.75, 8.25, 9.875], "texture": "#0"}, - "east": {"uv": [5.75, 11.375, 5.875, 11.5], "texture": "#0"}, - "south": {"uv": [9.875, 2, 10.625, 2.125], "texture": "#0"}, - "west": {"uv": [11.375, 5.875, 11.5, 6], "texture": "#0"}, - "up": {"uv": [10.625, 2.375, 9.875, 2.25], "texture": "#0"}, - "down": {"uv": [10.625, 2.5, 9.875, 2.625], "texture": "#0"} - } - }, - { - "from": [4.6, 7, 2.625], - "to": [5.6, 7, 3.625], - "rotation": {"angle": -45, "axis": "y", "origin": [5.1, 7, 3.125]}, - "faces": { - "north": {"uv": [0, 15.625, 0.375, 16], "texture": "#0"}, - "east": {"uv": [0, 15.625, 0.375, 16], "texture": "#0"}, - "south": {"uv": [0, 15.625, 0.375, 16], "texture": "#0"}, - "west": {"uv": [0, 15.625, 0.375, 16], "texture": "#0"}, - "up": {"uv": [6.125, 11.5, 6, 11.375], "texture": "#0"}, - "down": {"uv": [6.375, 11.375, 6.25, 11.5], "texture": "#0"} - } - }, - { - "from": [10.275, 7, 12.5], - "to": [11.275, 7, 13.5], - "rotation": {"angle": -45, "axis": "y", "origin": [10.775, 7, 13]}, - "faces": { - "north": {"uv": [0, 15.5, 0.375, 16], "texture": "#0"}, - "east": {"uv": [0, 15.625, 0.375, 16], "texture": "#0"}, - "south": {"uv": [0, 15.375, 0.625, 16], "texture": "#0"}, - "west": {"uv": [0, 15.625, 0.375, 16], "texture": "#0"}, - "up": {"uv": [6.625, 11.5, 6.5, 11.375], "texture": "#0"}, - "down": {"uv": [11.5, 6.75, 11.375, 6.875], "texture": "#0"} - } - }, - { - "from": [4.725, 7, 12.5], - "to": [5.725, 7, 13.5], - "rotation": {"angle": 45, "axis": "y", "origin": [5.225, 7, 13]}, - "faces": { - "north": {"uv": [0, 15.625, 0.375, 16], "texture": "#0"}, - "east": {"uv": [0, 15.5, 0.5, 16], "texture": "#0"}, - "south": {"uv": [0, 15.625, 0.375, 16], "texture": "#0"}, - "west": {"uv": [0, 15.625, 0.375, 16], "texture": "#0"}, - "up": {"uv": [11.5, 7.125, 11.375, 7], "texture": "#0"}, - "down": {"uv": [11.5, 7.25, 11.375, 7.375], "texture": "#0"} - } - }, - { - "from": [10.275, 7, 2.5], - "to": [11.275, 7, 3.5], - "rotation": {"angle": 45, "axis": "y", "origin": [10.775, 7, 3]}, - "faces": { - "north": {"uv": [0, 16, 0.25, 15.75], "texture": "#0"}, - "east": {"uv": [0, 16, 0.25, 15.75], "texture": "#0"}, - "south": {"uv": [0, 15.625, 0.375, 16], "texture": "#0"}, - "west": {"uv": [0, 16, 0.375, 15.625], "texture": "#0"}, - "up": {"uv": [11.5, 7.625, 11.375, 7.5], "texture": "#0"}, - "down": {"uv": [8, 11.375, 7.875, 11.5], "texture": "#0"} - } - }, - { - "from": [6, 6, 1], - "to": [7, 7, 2], - "faces": { - "north": {"uv": [9.125, 11.125, 9.25, 11.25], "texture": "#0"}, - "east": {"uv": [11.125, 9.25, 11.25, 9.375], "texture": "#0"}, - "south": {"uv": [11.125, 10.625, 11.25, 10.75], "texture": "#0"}, - "west": {"uv": [11.125, 10.875, 11.25, 11], "texture": "#0"}, - "up": {"uv": [11.25, 11.25, 11.125, 11.125], "texture": "#0"}, - "down": {"uv": [0.125, 11.25, 0, 11.375], "texture": "#0"} - } - }, - { - "from": [6, 6, 14.001], - "to": [10, 10, 16.001], - "faces": { - "north": {"uv": [5, 8, 5.5, 8.5], "texture": "#0"}, - "east": {"uv": [10.5, 0.75, 10.75, 1.25], "texture": "#0"}, - "south": {"uv": [8.375, 2.75, 8.875, 3.25], "texture": "#0"}, - "west": {"uv": [10.5, 3, 10.75, 3.5], "texture": "#0"}, - "up": {"uv": [11, 1.625, 10.5, 1.375], "texture": "#0"}, - "down": {"uv": [11, 3.5, 10.5, 3.75], "texture": "#0"} - } - }, - { - "from": [5, 6, 13], - "to": [11, 7, 14], - "faces": { - "north": {"uv": [9.625, 1.5, 10.375, 1.625], "texture": "#0"}, - "east": {"uv": [5.5, 11.375, 5.625, 11.5], "texture": "#0"}, - "south": {"uv": [9.625, 7.625, 10.375, 7.75], "texture": "#0"}, - "west": {"uv": [11.375, 5.625, 11.5, 5.75], "texture": "#0"}, - "up": {"uv": [2.5, 9.875, 1.75, 9.75], "texture": "#0"}, - "down": {"uv": [7.375, 9.75, 6.625, 9.875], "texture": "#0"} - } - }, - { - "from": [2, 5, 15], - "to": [3, 6, 16], - "faces": { - "north": {"uv": [8.875, 11.25, 9, 11.375], "texture": "#0"}, - "east": {"uv": [9.375, 11.25, 9.5, 11.375], "texture": "#0"}, - "south": {"uv": [11.25, 9.5, 11.375, 9.625], "texture": "#0"}, - "west": {"uv": [9.625, 11.25, 9.75, 11.375], "texture": "#0"}, - "up": {"uv": [11.375, 9.875, 11.25, 9.75], "texture": "#0"}, - "down": {"uv": [10, 11.25, 9.875, 11.375], "texture": "#0"} - } - }, - { - "from": [13, 5, 15], - "to": [14, 6, 16], - "faces": { - "north": {"uv": [11.25, 10, 11.375, 10.125], "texture": "#0"}, - "east": {"uv": [10.125, 11.25, 10.25, 11.375], "texture": "#0"}, - "south": {"uv": [11.25, 10.25, 11.375, 10.375], "texture": "#0"}, - "west": {"uv": [10.375, 11.25, 10.5, 11.375], "texture": "#0"}, - "up": {"uv": [10.75, 11.375, 10.625, 11.25], "texture": "#0"}, - "down": {"uv": [11, 11.25, 10.875, 11.375], "texture": "#0"} - } - }, - { - "from": [2, 5, 0], - "to": [3, 6, 1], - "faces": { - "north": {"uv": [1.5, 11.375, 1.625, 11.5], "texture": "#0"}, - "east": {"uv": [1.75, 11.375, 1.875, 11.5], "texture": "#0"}, - "south": {"uv": [2, 11.375, 2.125, 11.5], "texture": "#0"}, - "west": {"uv": [2.25, 11.375, 2.375, 11.5], "texture": "#0"}, - "up": {"uv": [11.5, 3.25, 11.375, 3.125], "texture": "#0"}, - "down": {"uv": [11.5, 3.375, 11.375, 3.5], "texture": "#0"} - } - }, - { - "from": [13, 5, 0], - "to": [14, 6, 1], - "faces": { - "north": {"uv": [11.375, 0, 11.5, 0.125], "texture": "#0"}, - "east": {"uv": [0.75, 11.375, 0.875, 11.5], "texture": "#0"}, - "south": {"uv": [1, 11.375, 1.125, 11.5], "texture": "#0"}, - "west": {"uv": [11.375, 1, 11.5, 1.125], "texture": "#0"}, - "up": {"uv": [1.375, 11.5, 1.25, 11.375], "texture": "#0"}, - "down": {"uv": [11.5, 1.25, 11.375, 1.375], "texture": "#0"} - } - }, - { - "from": [5, 1, 1], - "to": [6, 6, 2], - "faces": { - "north": {"uv": [6.875, 10, 7, 10.625], "texture": "#0"}, - "east": {"uv": [7.125, 10, 7.25, 10.625], "texture": "#0"}, - "south": {"uv": [7.375, 10, 7.5, 10.625], "texture": "#0"}, - "west": {"uv": [7.625, 10, 7.75, 10.625], "texture": "#0"}, - "up": {"uv": [7.5, 11.125, 7.375, 11], "texture": "#0"}, - "down": {"uv": [7.75, 11, 7.625, 11.125], "texture": "#0"} - } - }, - { - "from": [1.001, 4.8, 1.75], - "to": [4.001, 7.8, 3.75], - "rotation": {"angle": 45, "axis": "x", "origin": [2.5, 6.3, 2.8]}, - "faces": { - "north": {"uv": [3.125, 9, 3.5, 9.375], "texture": "#0"}, - "east": {"uv": [3.75, 9.5, 4, 9.875], "texture": "#0"}, - "south": {"uv": [3.625, 9, 4, 9.375], "texture": "#0"}, - "west": {"uv": [9.5, 8.625, 9.75, 9], "texture": "#0"}, - "up": {"uv": [9.875, 9.375, 9.5, 9.125], "texture": "#0"}, - "down": {"uv": [9.875, 9.5, 9.5, 9.75], "texture": "#0"} - } - }, - { - "from": [3, 1, 0], - "to": [4, 6, 2], - "faces": { - "north": {"uv": [9.875, 7.875, 10, 8.5], "texture": "#0"}, - "east": {"uv": [0.375, 8.875, 0.625, 9.5], "texture": "#0"}, - "south": {"uv": [8.625, 9.875, 8.75, 10.5], "texture": "#0"}, - "west": {"uv": [0.75, 8.875, 1, 9.5], "texture": "#0"}, - "up": {"uv": [0.625, 11.125, 0.5, 10.875], "texture": "#0"}, - "down": {"uv": [4.125, 10.875, 4, 11.125], "texture": "#0"} - } - }, - { - "from": [2, 1, 1], - "to": [3, 6, 2], - "faces": { - "north": {"uv": [0.25, 10.125, 0.375, 10.75], "texture": "#0"}, - "east": {"uv": [0.5, 10.125, 0.625, 10.75], "texture": "#0"}, - "south": {"uv": [4.125, 10.125, 4.25, 10.75], "texture": "#0"}, - "west": {"uv": [4.375, 10.125, 4.5, 10.75], "texture": "#0"}, - "up": {"uv": [11.375, 6.625, 11.25, 6.5], "texture": "#0"}, - "down": {"uv": [7, 11.25, 6.875, 11.375], "texture": "#0"} - } - }, - { - "from": [13, 1, 1], - "to": [14, 6, 2], - "faces": { - "north": {"uv": [4.625, 10.125, 4.75, 10.75], "texture": "#0"}, - "east": {"uv": [10.125, 7.875, 10.25, 8.5], "texture": "#0"}, - "south": {"uv": [10.125, 9.375, 10.25, 10], "texture": "#0"}, - "west": {"uv": [10.125, 10.125, 10.25, 10.75], "texture": "#0"}, - "up": {"uv": [7.25, 11.375, 7.125, 11.25], "texture": "#0"}, - "down": {"uv": [7.5, 11.25, 7.375, 11.375], "texture": "#0"} - } - }, - { - "from": [2, 1, 14], - "to": [3, 6, 15], - "faces": { - "north": {"uv": [10.25, 3.625, 10.375, 4.25], "texture": "#0"}, - "east": {"uv": [10.25, 4.375, 10.375, 5], "texture": "#0"}, - "south": {"uv": [5.875, 10.25, 6, 10.875], "texture": "#0"}, - "west": {"uv": [6.125, 10.25, 6.25, 10.875], "texture": "#0"}, - "up": {"uv": [7.75, 11.375, 7.625, 11.25], "texture": "#0"}, - "down": {"uv": [11.375, 7.875, 11.25, 8], "texture": "#0"} - } - }, - { - "from": [13, 1, 14], - "to": [14, 6, 15], - "faces": { - "north": {"uv": [6.375, 10.25, 6.5, 10.875], "texture": "#0"}, - "east": {"uv": [10.25, 8.625, 10.375, 9.25], "texture": "#0"}, - "south": {"uv": [10.375, 7.875, 10.5, 8.5], "texture": "#0"}, - "west": {"uv": [10.375, 9.375, 10.5, 10], "texture": "#0"}, - "up": {"uv": [11.375, 8.25, 11.25, 8.125], "texture": "#0"}, - "down": {"uv": [8.75, 11.25, 8.625, 11.375], "texture": "#0"} - } - }, - { - "from": [1, 1, 0], - "to": [2, 6, 2], - "faces": { - "north": {"uv": [3.125, 9.875, 3.25, 10.5], "texture": "#0"}, - "east": {"uv": [7.5, 8.75, 7.75, 9.375], "texture": "#0"}, - "south": {"uv": [3.375, 9.875, 3.5, 10.5], "texture": "#0"}, - "west": {"uv": [0, 8.875, 0.25, 9.5], "texture": "#0"}, - "up": {"uv": [11, 0.25, 10.875, 0], "texture": "#0"}, - "down": {"uv": [0.375, 10.875, 0.25, 11.125], "texture": "#0"} - } - }, - { - "from": [0, 1, 1], - "to": [1, 5, 2], - "faces": { - "north": {"uv": [10.5, 3.875, 10.625, 4.375], "texture": "#0"}, - "east": {"uv": [10.5, 4.5, 10.625, 5], "texture": "#0"}, - "south": {"uv": [10.5, 5.125, 10.625, 5.625], "texture": "#0"}, - "west": {"uv": [10.5, 5.75, 10.625, 6.25], "texture": "#0"}, - "up": {"uv": [0.375, 11.375, 0.25, 11.25], "texture": "#0"}, - "down": {"uv": [11.375, 0.25, 11.25, 0.375], "texture": "#0"} - } - }, - { - "from": [15, 4, 12], - "to": [16, 5, 14], - "faces": { - "north": {"uv": [11.25, 5.375, 11.375, 5.5], "texture": "#0"}, - "east": {"uv": [11, 4.625, 11.25, 4.75], "texture": "#0"}, - "south": {"uv": [11.25, 6.25, 11.375, 6.375], "texture": "#0"}, - "west": {"uv": [11, 4.875, 11.25, 5], "texture": "#0"}, - "up": {"uv": [11.125, 5.375, 11, 5.125], "texture": "#0"}, - "down": {"uv": [6, 11, 5.875, 11.25], "texture": "#0"} - } - }, - { - "from": [15, 1, 14], - "to": [16, 5, 15], - "faces": { - "north": {"uv": [10.75, 1.625, 10.875, 2.125], "texture": "#0"}, - "east": {"uv": [1.75, 10.75, 1.875, 11.25], "texture": "#0"}, - "south": {"uv": [2, 10.75, 2.125, 11.25], "texture": "#0"}, - "west": {"uv": [2.25, 10.75, 2.375, 11.25], "texture": "#0"}, - "up": {"uv": [4.625, 11.375, 4.5, 11.25], "texture": "#0"}, - "down": {"uv": [11.375, 5.125, 11.25, 5.25], "texture": "#0"} - } - }, - { - "from": [15, 1, 11], - "to": [16, 5, 12], - "faces": { - "north": {"uv": [0.75, 10.75, 0.875, 11.25], "texture": "#0"}, - "east": {"uv": [1, 10.75, 1.125, 11.25], "texture": "#0"}, - "south": {"uv": [1.25, 10.75, 1.375, 11.25], "texture": "#0"}, - "west": {"uv": [1.5, 10.75, 1.625, 11.25], "texture": "#0"}, - "up": {"uv": [4.375, 11.375, 4.25, 11.25], "texture": "#0"}, - "down": {"uv": [11.375, 4.375, 11.25, 4.5], "texture": "#0"} - } - }, - { - "from": [0, 1, 6], - "to": [1, 10, 10], - "faces": { - "north": {"uv": [5.875, 9.125, 6, 10.25], "texture": "#0"}, - "east": {"uv": [0, 7.125, 0.5, 8.25], "texture": "#0"}, - "south": {"uv": [6.125, 9.125, 6.25, 10.25], "texture": "#0"}, - "west": {"uv": [7.125, 0, 7.625, 1.125], "texture": "#0"}, - "up": {"uv": [10.875, 2.75, 10.75, 2.25], "texture": "#0"}, - "down": {"uv": [10.875, 2.875, 10.75, 3.375], "texture": "#0"} - } - }, - { - "from": [15, 1, 6], - "to": [16, 10, 10], - "faces": { - "north": {"uv": [6.375, 9.125, 6.5, 10.25], "texture": "#0"}, - "east": {"uv": [0.625, 7.125, 1.125, 8.25], "texture": "#0"}, - "south": {"uv": [9.25, 8.625, 9.375, 9.75], "texture": "#0"}, - "west": {"uv": [3.625, 7.125, 4.125, 8.25], "texture": "#0"}, - "up": {"uv": [3.625, 11.25, 3.5, 10.75], "texture": "#0"}, - "down": {"uv": [3.875, 10.75, 3.75, 11.25], "texture": "#0"} - } - }, - { - "from": [14.001, 7, 6], - "to": [15.001, 10, 10], - "faces": { - "north": {"uv": [6.125, 11, 6.25, 11.25], "texture": "#0"}, - "east": {"uv": [9.25, 2, 9.75, 2.25], "texture": "#0"}, - "south": {"uv": [11, 6.25, 11.125, 6.5], "texture": "#0"}, - "west": {"uv": [9.25, 2.375, 9.75, 2.625], "texture": "#0"}, - "up": {"uv": [10.875, 4.375, 10.75, 3.875], "texture": "#0"}, - "down": {"uv": [10.875, 4.5, 10.75, 5], "texture": "#0"} - } - }, - { - "from": [15, 14, 6], - "to": [16, 15, 10], - "faces": { - "north": {"uv": [3.5, 11.375, 3.625, 11.5], "texture": "#0"}, - "east": {"uv": [10.625, 8.25, 11.125, 8.375], "texture": "#0"}, - "south": {"uv": [11.375, 3.625, 11.5, 3.75], "texture": "#0"}, - "west": {"uv": [10.625, 9.875, 11.125, 10], "texture": "#0"}, - "up": {"uv": [10.875, 5.625, 10.75, 5.125], "texture": "#0"}, - "down": {"uv": [5.5, 10.75, 5.375, 11.25], "texture": "#0"} - } - }, - { - "from": [0, 14, 6], - "to": [1, 15, 10], - "faces": { - "north": {"uv": [3.75, 11.375, 3.875, 11.5], "texture": "#0"}, - "east": {"uv": [10.75, 5.75, 11.25, 5.875], "texture": "#0"}, - "south": {"uv": [11.375, 4.625, 11.5, 4.75], "texture": "#0"}, - "west": {"uv": [10.75, 6, 11.25, 6.125], "texture": "#0"}, - "up": {"uv": [5.75, 11.25, 5.625, 10.75], "texture": "#0"}, - "down": {"uv": [10.875, 6.25, 10.75, 6.75], "texture": "#0"} - } - }, - { - "from": [6, 14, 0], - "to": [10, 15, 1], - "faces": { - "north": {"uv": [6.625, 10.75, 7.125, 10.875], "texture": "#0"}, - "east": {"uv": [4.75, 11.375, 4.875, 11.5], "texture": "#0"}, - "south": {"uv": [10.75, 6.875, 11.25, 7], "texture": "#0"}, - "west": {"uv": [11.375, 4.875, 11.5, 5], "texture": "#0"}, - "up": {"uv": [11.25, 7.25, 10.75, 7.125], "texture": "#0"}, - "down": {"uv": [7.75, 10.75, 7.25, 10.875], "texture": "#0"} - } - }, - { - "from": [6, 14, 15], - "to": [10, 15, 16], - "faces": { - "north": {"uv": [10.75, 7.375, 11.25, 7.5], "texture": "#0"}, - "east": {"uv": [5, 11.375, 5.125, 11.5], "texture": "#0"}, - "south": {"uv": [10.75, 8.5, 11.25, 8.625], "texture": "#0"}, - "west": {"uv": [5.25, 11.375, 5.375, 11.5], "texture": "#0"}, - "up": {"uv": [11.25, 8.875, 10.75, 8.75], "texture": "#0"}, - "down": {"uv": [11.25, 9, 10.75, 9.125], "texture": "#0"} - } - }, - { - "from": [1, 7, 6], - "to": [2, 10, 10], - "faces": { - "north": {"uv": [6.375, 11, 6.5, 11.25], "texture": "#0"}, - "east": {"uv": [2.625, 9.5, 3.125, 9.75], "texture": "#0"}, - "south": {"uv": [6.625, 11, 6.75, 11.25], "texture": "#0"}, - "west": {"uv": [9.5, 2.75, 10, 3], "texture": "#0"}, - "up": {"uv": [5, 11.25, 4.875, 10.75], "texture": "#0"}, - "down": {"uv": [5.25, 10.75, 5.125, 11.25], "texture": "#0"} - } - }, - { - "from": [1, 1.0001, 3], - "to": [15, 7.0001, 13], - "faces": { - "north": {"uv": [2.125, 4.875, 3.875, 5.625], "texture": "#0"}, - "east": {"uv": [7, 2.75, 8.25, 3.5], "texture": "#0"}, - "south": {"uv": [5.125, 2, 6.875, 2.75], "texture": "#0"}, - "west": {"uv": [5.75, 7, 7, 7.75], "texture": "#0"}, - "up": {"uv": [3.875, 1.25, 2.125, 0], "texture": "#0"}, - "down": {"uv": [3.875, 1.375, 2.125, 2.625], "texture": "#0"} - } - }, - { - "from": [15, 1, 4], - "to": [16, 5, 5], - "faces": { - "north": {"uv": [9.875, 10.625, 10, 11.125], "texture": "#0"}, - "east": {"uv": [10.375, 10.625, 10.5, 11.125], "texture": "#0"}, - "south": {"uv": [10.625, 10.625, 10.75, 11.125], "texture": "#0"}, - "west": {"uv": [10.75, 0.625, 10.875, 1.125], "texture": "#0"}, - "up": {"uv": [4.125, 11.375, 4, 11.25], "texture": "#0"}, - "down": {"uv": [11.375, 4.125, 11.25, 4.25], "texture": "#0"} - } - }, - { - "from": [15, 4, 2], - "to": [16, 5, 4], - "faces": { - "north": {"uv": [3.25, 11.25, 3.375, 11.375], "texture": "#0"}, - "east": {"uv": [11, 3.125, 11.25, 3.25], "texture": "#0"}, - "south": {"uv": [11.25, 3.875, 11.375, 4], "texture": "#0"}, - "west": {"uv": [11, 3.375, 11.25, 3.5], "texture": "#0"}, - "up": {"uv": [11.125, 4.125, 11, 3.875], "texture": "#0"}, - "down": {"uv": [11.125, 4.25, 11, 4.5], "texture": "#0"} - } - }, - { - "from": [15, 1, 1], - "to": [16, 5, 2], - "faces": { - "north": {"uv": [8.875, 10.625, 9, 11.125], "texture": "#0"}, - "east": {"uv": [10.625, 9.25, 10.75, 9.75], "texture": "#0"}, - "south": {"uv": [9.375, 10.625, 9.5, 11.125], "texture": "#0"}, - "west": {"uv": [9.625, 10.625, 9.75, 11.125], "texture": "#0"}, - "up": {"uv": [11.375, 3, 11.25, 2.875], "texture": "#0"}, - "down": {"uv": [3.125, 11.25, 3, 11.375], "texture": "#0"} - } - }, - { - "from": [0, 4, 2], - "to": [1, 5, 4], - "faces": { - "north": {"uv": [11.25, 2.375, 11.375, 2.5], "texture": "#0"}, - "east": {"uv": [1.25, 7.125, 1.5, 7.25], "texture": "#0"}, - "south": {"uv": [2.5, 11.25, 2.625, 11.375], "texture": "#0"}, - "west": {"uv": [10.875, 8, 11.125, 8.125], "texture": "#0"}, - "up": {"uv": [11.125, 1.875, 11, 1.625], "texture": "#0"}, - "down": {"uv": [11.125, 2, 11, 2.25], "texture": "#0"} - } - }, - { - "from": [0, 4, 12], - "to": [1, 5, 14], - "faces": { - "north": {"uv": [11.25, 2.625, 11.375, 2.75], "texture": "#0"}, - "east": {"uv": [10.875, 9.625, 11.125, 9.75], "texture": "#0"}, - "south": {"uv": [2.75, 11.25, 2.875, 11.375], "texture": "#0"}, - "west": {"uv": [11, 1.125, 11.25, 1.25], "texture": "#0"}, - "up": {"uv": [11.125, 2.625, 11, 2.375], "texture": "#0"}, - "down": {"uv": [11.125, 2.75, 11, 3], "texture": "#0"} + "north": {"uv": [0, 3.75, 0.75, 4], "texture": "#12"}, + "east": {"uv": [0, 7.75, 0.25, 8], "texture": "#12"}, + "down": {"uv": [0, 11.75, 1, 12], "texture": "#12"} } }, { "from": [0, 0, 0], - "to": [16, 1, 16], - "faces": { - "north": {"uv": [7.875, 1.75, 9.875, 1.875], "texture": "#0"}, - "east": {"uv": [8.375, 3.375, 10.375, 3.5], "texture": "#0"}, - "south": {"uv": [8.375, 5.125, 10.375, 5.25], "texture": "#0"}, - "west": {"uv": [8.375, 5.375, 10.375, 5.5], "texture": "#0"}, - "up": {"uv": [2, 2, 0, 0], "texture": "#0"}, - "down": {"uv": [2, 2.125, 0, 4.125], "texture": "#0"} - } - }, - { - "from": [0, 1, 14], - "to": [1, 5, 15], + "to": [3, 1, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [-10, 0, 0]}, "faces": { - "north": {"uv": [3, 10.625, 3.125, 11.125], "texture": "#0"}, - "east": {"uv": [3.25, 10.625, 3.375, 11.125], "texture": "#0"}, - "south": {"uv": [10.625, 7.625, 10.75, 8.125], "texture": "#0"}, - "west": {"uv": [8.625, 10.625, 8.75, 11.125], "texture": "#0"}, - "up": {"uv": [11.375, 2, 11.25, 1.875], "texture": "#0"}, - "down": {"uv": [11.375, 2.125, 11.25, 2.25], "texture": "#0"} + "north": {"uv": [3.25, 3.75, 4, 4], "texture": "#12"}, + "west": {"uv": [0, 7.75, 0.25, 8], "texture": "#12"}, + "down": {"uv": [3, 11.75, 4, 12], "texture": "#12"} } }, { - "from": [0, 1, 11], - "to": [1, 5, 12], + "from": [3, 14, 0], + "to": [13, 16, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [5, 14, 0]}, "faces": { - "north": {"uv": [9.125, 10.5, 9.25, 11], "texture": "#0"}, - "east": {"uv": [10.625, 0, 10.75, 0.5], "texture": "#0"}, - "south": {"uv": [2.5, 10.625, 2.625, 11.125], "texture": "#0"}, - "west": {"uv": [2.75, 10.625, 2.875, 11.125], "texture": "#0"}, - "up": {"uv": [11.375, 0.875, 11.25, 0.75], "texture": "#0"}, - "down": {"uv": [11.375, 1.625, 11.25, 1.75], "texture": "#0"} + "north": {"uv": [0.75, 0, 3.25, 0.5], "texture": "#12"}, + "up": {"uv": [4.75, 4, 7.25, 4.25], "texture": "#12"}, + "down": {"uv": [7, 10.25, 9.5, 10.5], "texture": "#12"} } }, { - "from": [0, 1, 4], - "to": [1, 5, 5], + "from": [0, 1, 0], + "to": [3, 5, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 1, 0]}, "faces": { - "north": {"uv": [10.5, 6.375, 10.625, 6.875], "texture": "#0"}, - "east": {"uv": [10.5, 7, 10.625, 7.5], "texture": "#0"}, - "south": {"uv": [8.375, 10.5, 8.5, 11], "texture": "#0"}, - "west": {"uv": [10.5, 8.625, 10.625, 9.125], "texture": "#0"}, - "up": {"uv": [0.625, 11.375, 0.5, 11.25], "texture": "#0"}, - "down": {"uv": [11.375, 0.5, 11.25, 0.625], "texture": "#0"} + "north": {"uv": [3.25, 2.75, 4, 3.75], "texture": "#12"}, + "east": {"uv": [10.5, 0, 10.75, 1], "texture": "#12"}, + "west": {"uv": [0, 6.75, 0.25, 7.75], "texture": "#12"} } }, { - "from": [14, 1, 0], - "to": [15, 6, 2], + "from": [13, 1, 0], + "to": [16, 5, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [14, 1, 0]}, "faces": { - "north": {"uv": [2.625, 9.875, 2.75, 10.5], "texture": "#0"}, - "east": {"uv": [6.75, 8.75, 7, 9.375], "texture": "#0"}, - "south": {"uv": [2.875, 9.875, 3, 10.5], "texture": "#0"}, - "west": {"uv": [7.125, 8.75, 7.375, 9.375], "texture": "#0"}, - "up": {"uv": [8.25, 11, 8.125, 10.75], "texture": "#0"}, - "down": {"uv": [0.125, 10.875, 0, 11.125], "texture": "#0"} + "north": {"uv": [0, 2.75, 0.75, 3.75], "texture": "#12"}, + "east": {"uv": [0, 6.75, 0.25, 7.75], "texture": "#12"}, + "west": {"uv": [10.5, 0, 10.75, 1], "texture": "#12"} } }, { - "from": [11.999, 4.8, 1.75], - "to": [14.999, 7.8, 3.75], - "rotation": {"angle": 45, "axis": "x", "origin": [13.5, 6.3, 2.8]}, + "from": [13, 8, 0], + "to": [16, 14, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [14, 8, 0]}, "faces": { - "north": {"uv": [2.625, 9, 3, 9.375], "texture": "#0"}, - "east": {"uv": [1.75, 5.25, 2, 5.625], "texture": "#0"}, - "south": {"uv": [9, 2.75, 9.375, 3.125], "texture": "#0"}, - "west": {"uv": [3.625, 6.5, 3.875, 6.875], "texture": "#0"}, - "up": {"uv": [3.625, 9.75, 3.25, 9.5], "texture": "#0"}, - "down": {"uv": [9, 9.5, 8.625, 9.75], "texture": "#0"} + "north": {"uv": [0, 0.5, 0.75, 2], "texture": "#12"}, + "east": {"uv": [0, 4.5, 0.25, 6], "texture": "#12"}, + "west": {"uv": [9.5, 8.75, 9.75, 10.25], "texture": "#12"} } }, { - "name": "Octagon", - "from": [5.51472, 7, 14], - "to": [10.48528, 13, 14], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 7.5, 8]}, + "from": [0, 8, 0], + "to": [3, 14, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [1, 8, 0]}, "faces": { - "north": {"uv": [7.125, 1.125, 7.75, 1.875], "texture": "#0"}, - "east": {"uv": [0, 15.25, 0.375, 16], "texture": "#0"}, - "south": {"uv": [4.25, 7.125, 4.875, 7.875], "texture": "#0"}, - "west": {"uv": [0, 15.25, 0.25, 16], "texture": "#0"}, - "up": {"uv": [0.625, 15.625, 0, 16], "texture": "#0"}, - "down": {"uv": [0.625, 15.625, 0, 16], "texture": "#0"} + "north": {"uv": [3.25, 0.5, 4, 2], "texture": "#12"}, + "east": {"uv": [9.5, 8.75, 9.75, 10.25], "texture": "#12"}, + "west": {"uv": [0, 4.5, 0.25, 6], "texture": "#12"} } }, { - "name": "Octagon", - "from": [14, 7, 5.51472], - "to": [14, 13, 10.48528], + "from": [3, 5, 0], + "to": [13, 8, 1], "faces": { - "north": {"uv": [0, 15.25, 0.375, 16], "texture": "#0"}, - "east": {"uv": [7.25, 7.875, 7.875, 8.625], "texture": "#0"}, - "south": {"uv": [0, 15.25, 0.375, 16], "texture": "#0"}, - "west": {"uv": [4.25, 8, 4.875, 8.75], "texture": "#0"}, - "up": {"uv": [0, 16, 0.375, 15.375], "texture": "#0"}, - "down": {"uv": [0, 15.375, 0.5, 16], "texture": "#0"} + "north": {"uv": [0.75, 2, 3.25, 2.75], "texture": "#12"}, + "up": {"uv": [7, 10.5, 9.5, 10.75], "texture": "#12"}, + "down": {"uv": [8, 1.75, 10.5, 1.5], "texture": "#12"} } }, { - "name": "Octagon", - "from": [14, 7, 5.51472], - "to": [14, 13, 10.48528], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 12.5, 8]}, + "from": [0, 5, 0], + "to": [3, 8, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 5, 0]}, "faces": { - "north": {"uv": [0.375, 15.25, 0, 16], "texture": "#0"}, - "east": {"uv": [6.5, 7.875, 7.125, 8.625], "texture": "#0"}, - "south": {"uv": [0, 15.25, 0.625, 16], "texture": "#0"}, - "west": {"uv": [7.875, 7, 8.5, 7.75], "texture": "#0"}, - "up": {"uv": [0, 16, 0.5, 15.375], "texture": "#0"}, - "down": {"uv": [0, 15.375, 0.375, 16], "texture": "#0"} + "north": {"uv": [3.25, 2, 4, 2.75], "texture": "#12"}, + "west": {"uv": [0, 6, 0.25, 6.75], "texture": "#12"} } }, { - "name": "Octagon", - "from": [5.51472, 7, 2], - "to": [10.48528, 13, 2], - "faces": { - "north": {"uv": [7.875, 0.875, 8.5, 1.625], "texture": "#0"}, - "east": {"uv": [0, 15.25, 0.625, 16], "texture": "#0"}, - "south": {"uv": [5.75, 7.875, 6.375, 8.625], "texture": "#0"}, - "west": {"uv": [0, 15.25, 0.5, 16], "texture": "#0"}, - "up": {"uv": [0.625, 15.25, 0, 16], "texture": "#0"}, - "down": {"uv": [0.625, 15.25, 0, 16], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [5.51472, 7, 2], - "to": [10.48528, 13, 2], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 12.5, 8]}, - "faces": { - "north": {"uv": [7.625, 6.125, 8.25, 6.875], "texture": "#0"}, - "east": {"uv": [0, 15.25, 0.5, 16], "texture": "#0"}, - "south": {"uv": [7.75, 0, 8.375, 0.75], "texture": "#0"}, - "west": {"uv": [0, 15.25, 0.625, 16], "texture": "#0"}, - "up": {"uv": [0.625, 15.25, 0, 16], "texture": "#0"}, - "down": {"uv": [0.625, 15.25, 0, 16], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [5.51472, 7, 14], - "to": [10.48528, 13, 14], - "faces": { - "north": {"uv": [5, 7.125, 5.625, 7.875], "texture": "#0"}, - "east": {"uv": [0, 15.25, 0.375, 16], "texture": "#0"}, - "south": {"uv": [7.125, 7, 7.75, 7.75], "texture": "#0"}, - "west": {"uv": [0, 15.25, 0.375, 16], "texture": "#0"}, - "up": {"uv": [0.625, 15.25, 0, 16], "texture": "#0"}, - "down": {"uv": [0.625, 15.375, 0, 16], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [2, 7, 5.51472], - "to": [2, 13, 10.48528], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 7.5, 8]}, - "faces": { - "north": {"uv": [0.5, 15.25, 0, 16], "texture": "#0"}, - "east": {"uv": [1.25, 7.375, 1.875, 8.125], "texture": "#0"}, - "south": {"uv": [0, 15.25, 0.375, 16], "texture": "#0"}, - "west": {"uv": [2, 7.375, 2.625, 8.125], "texture": "#0"}, - "up": {"uv": [0, 16, 0.5, 15.375], "texture": "#0"}, - "down": {"uv": [0, 15.375, 0.5, 16], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [2, 7, 5.51472], - "to": [2, 13, 10.48528], - "faces": { - "north": {"uv": [0, 15.25, 0.375, 16], "texture": "#0"}, - "east": {"uv": [2.75, 7.375, 3.375, 8.125], "texture": "#0"}, - "south": {"uv": [0, 15.25, 0.625, 16], "texture": "#0"}, - "west": {"uv": [7.625, 5.25, 8.25, 6], "texture": "#0"}, - "up": {"uv": [0, 16, 0.375, 15.375], "texture": "#0"}, - "down": {"uv": [0, 15.375, 0.5, 16], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [5.10051, 13, 1], - "to": [10.89949, 15, 15], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 14, 8]}, + "from": [13, 5, 0], + "to": [16, 8, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [13, 5, 0]}, "faces": { - "north": {"uv": [5.125, 3.75, 5.875, 4], "texture": "#0"}, - "east": {"uv": [8, 7.875, 9.75, 8.125], "texture": "#0"}, - "south": {"uv": [1.875, 8.625, 2.625, 8.875], "texture": "#0"}, - "west": {"uv": [8.125, 3.625, 9.875, 3.875], "texture": "#0"}, - "up": {"uv": [0.75, 7, 0, 5.25], "texture": "#0"}, - "down": {"uv": [1.625, 5.25, 0.875, 7], "texture": "#0"} + "north": {"uv": [0, 2, 0.75, 2.75], "texture": "#12"}, + "east": {"uv": [0, 6, 0.25, 6.75], "texture": "#12"} } }, { - "name": "Octagon", - "from": [5.10051, 12.999, 1], - "to": [10.89949, 15, 15], + "from": [0, 14, 0], + "to": [3, 16, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 14, 0]}, "faces": { - "north": {"uv": [2.75, 8.625, 3.5, 8.875], "texture": "#0"}, - "east": {"uv": [8.125, 4, 9.875, 4.25], "texture": "#0"}, - "south": {"uv": [8.625, 6.875, 9.375, 7.125], "texture": "#0"}, - "west": {"uv": [8.125, 4.375, 9.875, 4.625], "texture": "#0"}, - "up": {"uv": [4.75, 7, 4, 5.25], "texture": "#0"}, - "down": {"uv": [5.625, 5.25, 4.875, 7], "texture": "#0"} + "north": {"uv": [0, 0, 0.75, 0.5], "texture": "#12"}, + "west": {"uv": [0, 4, 0.25, 4.5], "texture": "#12"}, + "up": {"uv": [7.25, 4, 8, 4.25], "texture": "#12"} } }, { - "name": "Octagon", - "from": [1, 13, 5.10051], - "to": [15, 15, 10.89949], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 14, 8]}, + "from": [13, 14, 0], + "to": [16, 16, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [13, 14, 0]}, "faces": { - "north": {"uv": [8.125, 4.75, 9.875, 5], "texture": "#0"}, - "east": {"uv": [8.625, 7.25, 9.375, 7.5], "texture": "#0"}, - "south": {"uv": [0, 8.25, 1.75, 8.5], "texture": "#0"}, - "west": {"uv": [8, 8.625, 8.75, 8.875], "texture": "#0"}, - "up": {"uv": [6.875, 3.625, 5.125, 2.875], "texture": "#0"}, - "down": {"uv": [3.5, 5.75, 1.75, 6.5], "texture": "#0"} + "north": {"uv": [0, 0, 0.75, 0.5], "texture": "#12"}, + "east": {"uv": [0, 4, 0.25, 4.5], "texture": "#12"}, + "up": {"uv": [4, 4, 4.75, 4.25], "texture": "#12"} } }, { - "name": "Octagon", - "from": [1, 12.999, 5.10051], - "to": [15, 15, 10.89949], + "from": [3, 1, 1], + "to": [13, 5, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [3, 1, 1]}, "faces": { - "north": {"uv": [1.875, 8.25, 3.625, 8.5], "texture": "#0"}, - "east": {"uv": [5, 8.75, 5.75, 9], "texture": "#0"}, - "south": {"uv": [8, 8.25, 9.75, 8.5], "texture": "#0"}, - "west": {"uv": [5.875, 8.75, 6.625, 9], "texture": "#0"}, - "up": {"uv": [7.5, 6, 5.75, 5.25], "texture": "#0"}, - "down": {"uv": [7.5, 6.125, 5.75, 6.875], "texture": "#0"} + "north": {"uv": [8, 0, 10.5, 1], "texture": "#12"} } }, { - "name": "Octagon", - "from": [4.68629, 15, 0], - "to": [11.31371, 16, 16], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 16, 8]}, + "from": [3, 8, 1], + "to": [13, 14, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [3, 8, 1]}, "faces": { - "north": {"uv": [8.625, 1.5, 9.5, 1.625], "texture": "#0"}, - "east": {"uv": [8.375, 5.625, 10.375, 5.75], "texture": "#0"}, - "south": {"uv": [8.625, 7.625, 9.5, 7.75], "texture": "#0"}, - "west": {"uv": [8.375, 5.875, 10.375, 6], "texture": "#0"}, - "up": {"uv": [3, 4.70938, 2.125, 2.7875], "texture": "#0"}, - "down": {"uv": [4, 2.75, 3.125, 4.75], "texture": "#0"} + "north": {"uv": [7, 8.75, 9.5, 10.25], "texture": "#12"} } }, { - "name": "Octagon", - "from": [4.68629, 15.001, 0], - "to": [11.31371, 16.001, 16], + "from": [12, 0, 1], + "to": [16, 9, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [12, 0, 1]}, "faces": { - "north": {"uv": [9.5, 3.125, 10.375, 3.25], "texture": "#0"}, - "east": {"uv": [8.375, 6.125, 10.375, 6.25], "texture": "#0"}, - "south": {"uv": [6.625, 9.5, 7.5, 9.625], "texture": "#0"}, - "west": {"uv": [8.375, 6.375, 10.375, 6.5], "texture": "#0"}, - "up": {"uv": [4.875, 2, 4, 0], "texture": "#0"}, - "down": {"uv": [5, 2.125, 4.125, 4.125], "texture": "#0"} + "east": {"uv": [4, 5.75, 0.25, 8], "texture": "#12"}, + "south": {"uv": [7, 1.75, 8, 4], "texture": "#12"}, + "down": {"uv": [3, 8, 4, 11.75], "texture": "#12"} } }, { - "name": "Octagon", - "from": [0, 15, 4.68629], - "to": [16, 16, 11.31371], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 16, 8]}, + "from": [0, 0, 1], + "to": [4, 9, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 0, 1]}, "faces": { - "north": {"uv": [8.375, 6.625, 10.375, 6.75], "texture": "#0"}, - "east": {"uv": [9.5, 6.875, 10.375, 7], "texture": "#0"}, - "south": {"uv": [8.5, 0, 10.5, 0.125], "texture": "#0"}, - "west": {"uv": [9.5, 7.125, 10.375, 7.25], "texture": "#0"}, - "up": {"uv": [6.9625, 0.875, 5.04062, 0], "texture": "#0"}, - "down": {"uv": [6.125, 4.25, 4.125, 5.125], "texture": "#0"} + "south": {"uv": [4, 1.75, 5, 4], "texture": "#12"}, + "west": {"uv": [0.25, 5.75, 4, 8], "texture": "#12"}, + "down": {"uv": [0, 8, 1, 11.75], "texture": "#12"} } }, { - "name": "Octagon", - "from": [0, 15.002, 4.68629], - "to": [16, 16.002, 11.31371], + "from": [0, 9, 1], + "to": [3, 16, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 9, 1]}, "faces": { - "north": {"uv": [8.5, 0.25, 10.5, 0.375], "texture": "#0"}, - "east": {"uv": [4.775, 11.75, 5.60343, 11.875], "texture": "#0"}, - "south": {"uv": [8.5, 0.5, 10.5, 0.625], "texture": "#0"}, - "west": {"uv": [4.775, 12.125, 5.60343, 12.25], "texture": "#0"}, - "up": {"uv": [2, 12.6625, 0, 11.83408], "texture": "#0"}, - "down": {"uv": [4.375, 11.75, 2.375, 12.625], "texture": "#0"} + "east": {"uv": [16, 3.75, 12.25, 5.75], "texture": "#12"}, + "south": {"uv": [4, 0, 4.75, 1.75], "texture": "#12"}, + "west": {"uv": [0.25, 4, 4, 5.75], "texture": "#12"}, + "up": {"uv": [4, 4.25, 4.75, 8], "texture": "#12"} } }, { - "from": [6.042, 7.5525, 6.042], - "to": [9.958, 12.4475, 9.958], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 10, 8]}, + "from": [13, 9, 1], + "to": [16, 16, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [13, 9, 1]}, "faces": { - "north": {"uv": [6, 5, 14, 15], "texture": "#1"}, - "east": {"uv": [6, 5, 14, 15], "texture": "#1"}, - "south": {"uv": [6, 5, 14, 15], "texture": "#1"}, - "west": {"uv": [6, 5, 14, 15], "texture": "#1"}, - "up": {"uv": [14, 14, 6, 5], "texture": "#1"}, - "down": {"uv": [14, 14, 6, 6], "texture": "#1"} + "east": {"uv": [4, 4, 0.25, 5.75], "texture": "#12"}, + "south": {"uv": [7.25, 0, 8, 1.75], "texture": "#12"}, + "west": {"uv": [12.25, 3.75, 16, 5.75], "texture": "#12"}, + "up": {"uv": [7.25, 4.25, 8, 8], "texture": "#12"} } }, { - "from": [9.958, 8.042, 6.042], - "to": [10.4475, 11.958, 9.958], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 10, 8]}, + "from": [4, 0, 1], + "to": [12, 4, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [4, 0, 1]}, "faces": { - "north": {"uv": [5, 6, 6, 14], "texture": "#1"}, - "east": {"uv": [6, 6, 14, 14], "texture": "#1"}, - "south": {"uv": [14, 6, 15, 14], "texture": "#1"}, - "west": {"uv": [5, 6, 6, 14], "texture": "#1"}, - "up": {"uv": [5, 6, 6, 7], "texture": "#1"}, - "down": {"uv": [5, 14, 6, 6], "texture": "#1"} + "south": {"uv": [5, 3, 7, 4], "texture": "#12"}, + "down": {"uv": [1, 8, 3, 11.75], "texture": "#12"} } }, { - "from": [5.5525, 8.042, 6.042], - "to": [6.042, 11.958, 9.958], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 10, 8]}, + "from": [3, 9.05, 13.05], + "to": [13, 16.05, 16.05], + "rotation": {"angle": 0, "axis": "y", "origin": [3, 14.05, 13.05]}, "faces": { - "north": {"uv": [14, 6, 15, 14], "texture": "#1"}, - "east": {"uv": [14, 6, 15, 14], "texture": "#1"}, - "south": {"uv": [5, 6, 6, 14], "texture": "#1"}, - "west": {"uv": [6, 6, 14, 14], "texture": "#1"}, - "up": {"uv": [14, 6, 15, 7], "texture": "#1"}, - "down": {"uv": [14, 14, 15, 6], "texture": "#1"} + "south": {"uv": [4, 8.75, 6.5, 10.5], "texture": "#12"}, + "up": {"uv": [4, 8, 6.5, 8.75], "texture": "#12"} } }, { - "from": [5.063, 9.021, 7.021], - "to": [5.5525, 10.979, 8.979], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 10, 8]}, + "from": [3, 12, 1], + "to": [13, 16, 13], + "rotation": {"angle": 0, "axis": "y", "origin": [3, 14, 1]}, "faces": { - "north": {"uv": [15, 8, 16, 12], "texture": "#1"}, - "east": {"uv": [15, 8, 16, 12], "texture": "#1"}, - "south": {"uv": [4, 8, 5, 12], "texture": "#1"}, - "west": {"uv": [8, 8, 12, 12], "texture": "#1"}, - "up": {"uv": [15, 12, 16, 8], "texture": "#1"}, - "down": {"uv": [15, 12, 16, 8], "texture": "#1"} + "south": {"uv": [8.75, 3.75, 11.25, 4.75], "texture": "#12"}, + "up": {"uv": [4.75, 4.25, 7.25, 7.25], "texture": "#12"} } }, { - "from": [10.4475, 9.021, 7.021], - "to": [10.937, 10.979, 8.979], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 10, 8]}, + "from": [4, 4, 12], + "to": [12, 12, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [5, 9, 14]}, "faces": { - "north": {"uv": [4, 8, 5, 12], "texture": "#1"}, - "east": {"uv": [8, 8, 12, 12], "texture": "#1"}, - "south": {"uv": [15, 8, 16, 12], "texture": "#1"}, - "west": {"uv": [4, 8, 5, 12], "texture": "#1"}, - "up": {"uv": [4, 12, 5, 8], "texture": "#1"}, - "down": {"uv": [4, 8, 5, 12], "texture": "#1"} + "east": {"uv": [8, 1.75, 9, 3.75], "texture": "#12"}, + "south": {"uv": [5, 1, 7, 3], "texture": "#12"}, + "west": {"uv": [8, 1.75, 9, 3.75], "texture": "#12"}, + "up": {"uv": [8, 2.75, 10, 3.75], "texture": "#12"} } }, { - "from": [7.021, 12.4475, 7.021], - "to": [8.979, 12.937, 8.979], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 10, 8]}, + "from": [5, 7.9, 11.875], + "to": [6, 15.9, 15.875], + "rotation": {"angle": 0, "axis": "y", "origin": [5, 7.9, 10.875]}, "faces": { - "north": {"uv": [8, 4, 12, 5], "texture": "#1"}, - "east": {"uv": [8, 4, 12, 5], "texture": "#1"}, - "south": {"uv": [8, 4, 12, 5], "texture": "#1"}, - "west": {"uv": [8, 4, 12, 5], "texture": "#1"}, - "up": {"uv": [12, 12, 8, 8], "texture": "#1"}, - "down": {"uv": [8, 4, 12, 5], "texture": "#1"} + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} } }, { - "from": [7.021, 7.063, 7.021], - "to": [8.979, 7.5525, 8.979], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 10, 8]}, + "from": [7, 7.9, 11.875], + "to": [8, 15.9, 15.875], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 7.9, 10.875]}, "faces": { - "north": {"uv": [8, 15, 12, 16], "texture": "#1"}, - "east": {"uv": [8, 15, 12, 16], "texture": "#1"}, - "south": {"uv": [8, 15, 12, 16], "texture": "#1"}, - "west": {"uv": [8, 15, 12, 16], "texture": "#1"}, - "up": {"uv": [8, 15, 12, 16], "texture": "#1"}, - "down": {"uv": [12, 12, 8, 8], "texture": "#1"} + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} } }, { - "from": [6.042, 8.042, 9.958], - "to": [9.958, 11.958, 10.4475], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 10, 8]}, + "from": [10, 7.9, 11.825], + "to": [11, 15.9, 15.825], + "rotation": {"angle": 0, "axis": "y", "origin": [9, 7.9, 10.825]}, "faces": { - "north": {"uv": [6, 6, 14, 14], "texture": "#1"}, - "east": {"uv": [5, 6, 6, 14], "texture": "#1"}, - "south": {"uv": [6, 6, 14, 14], "texture": "#1"}, - "west": {"uv": [13, 6, 14, 14], "texture": "#1"}, - "up": {"uv": [14, 5, 6, 6], "texture": "#1"}, - "down": {"uv": [14, 14, 6, 15], "texture": "#1"} + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} } }, { - "from": [6.042, 8.042, 5.5525], - "to": [9.958, 11.958, 6.042], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 10, 8]}, + "from": [12, 7.9, 11.9], + "to": [13, 15.9, 15.9], + "rotation": {"angle": 0, "axis": "y", "origin": [11, 7.9, 10.9]}, "faces": { - "north": {"uv": [6, 6, 14, 14], "texture": "#1"}, - "east": {"uv": [14, 6, 15, 14], "texture": "#1"}, - "south": {"uv": [6, 6, 14, 14], "texture": "#1"}, - "west": {"uv": [14, 6, 15, 14], "texture": "#1"}, - "up": {"uv": [14, 15, 6, 14], "texture": "#1"}, - "down": {"uv": [6, 5, 14, 6], "texture": "#1"} + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} } }, { - "from": [7.021, 9.021, 5.063], - "to": [8.979, 10.979, 5.5525], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 10, 8]}, + "from": [3, 7.9, 11.9], + "to": [4, 15.9, 15.9], + "rotation": {"angle": 0, "axis": "y", "origin": [2, 7.9, 10.9]}, "faces": { - "north": {"uv": [8, 8, 12, 12], "texture": "#1"}, - "east": {"uv": [15, 8, 16, 12], "texture": "#1"}, - "south": {"uv": [8, 8, 12, 12], "texture": "#1"}, - "west": {"uv": [4, 8, 5, 12], "texture": "#1"}, - "up": {"uv": [12, 15, 8, 16], "texture": "#1"}, - "down": {"uv": [12, 4, 8, 5], "texture": "#1"} + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} } }, { - "from": [7.021, 9.021, 10.4475], - "to": [8.979, 10.979, 10.937], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 10, 8]}, + "from": [8, 7.9, 11.825], + "to": [9, 15.9, 15.825], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 7.9, 10.825]}, "faces": { - "north": {"uv": [8, 8, 12, 12], "texture": "#1"}, - "east": {"uv": [4, 8, 5, 12], "texture": "#1"}, - "south": {"uv": [8, 8, 12, 12], "texture": "#1"}, - "west": {"uv": [15, 8, 16, 12], "texture": "#1"}, - "up": {"uv": [12, 4, 8, 5], "texture": "#1"}, - "down": {"uv": [12, 16, 8, 15], "texture": "#1"} + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8.5, 6.75, 8.75, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.5, 6.75, 9.75, 7.75], "texture": "#12"} } } ], @@ -1116,5 +330,60 @@ "fixed": { "scale": [0.5, 0.5, 0.5] } - } + }, + "groups": [ + { + "name": "allcube", + "origin": [0, 0, 0], + "color": 0, + "children": [ + { + "name": "baseBlock", + "origin": [0, 0, 0], + "color": 0, + "children": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + { + "name": "back", + "origin": [7, 0.025, -2], + "color": 0, + "children": [20] + }, + 21 + ] + }, + { + "name": "connect", + "origin": [5, 10, 14], + "color": 0, + "children": [22] + }, + { + "name": "copper_past", + "origin": [3, 9, -2], + "color": 0, + "children": [23, 24, 25, 26, 27, 28] + } + ] + } + ] } \ No newline at end of file diff --git a/src/main/resources/assets/useless_mod/models/block/advanced_alloy_furnace_active.json b/src/main/resources/assets/useless_mod/models/block/advanced_alloy_furnace_active.json index f7a9c146..de2242e2 100644 --- a/src/main/resources/assets/useless_mod/models/block/advanced_alloy_furnace_active.json +++ b/src/main/resources/assets/useless_mod/models/block/advanced_alloy_furnace_active.json @@ -1,1097 +1,300 @@ { - "format_version": "1.9.0", - "credit": "Made with _leng", + "format_version": "1.21.6", + "credit": "Made with blockbench,made by xiabi418", "render_type": "translucent", + "texture_size": [64, 64], "textures": { - "0": "useless_mod:block/advanced_alloy_furnace", - "1": "useless_mod:block/advanced_alloy_furnace_flame" + "1": "useless_mod:block/advanced_alloy_furnace_active", + "12": "useless_mod:block/advanced_alloy_furnace", + "particle": "useless_mod:block/advanced_alloy_furnace" }, "elements": [ { - "from": [12, 1, 0], - "to": [13, 6, 2], + "from": [3, 0, 0], + "to": [13, 1, 1], "faces": { - "north": {"uv": [8.375, 9.75, 8.5, 10.375], "texture": "#0"}, - "east": {"uv": [3.625, 5.75, 3.875, 6.375], "texture": "#0"}, - "south": {"uv": [9.125, 9.75, 9.25, 10.375], "texture": "#0"}, - "west": {"uv": [3.75, 8.25, 4, 8.875], "texture": "#0"}, - "up": {"uv": [6.125, 4, 6, 3.75], "texture": "#0"}, - "down": {"uv": [8, 10.75, 7.875, 11], "texture": "#0"} + "north": {"uv": [0.75, 3.75, 3.25, 4], "texture": "#12"}, + "up": {"uv": [8, 1.25, 10.5, 1.5], "texture": "#12"}, + "down": {"uv": [1, 11.75, 3, 12], "texture": "#12"} } }, { - "from": [1.001, 4.95312, 13.6803], - "to": [4.001, 7.95312, 15.6803], - "rotation": {"angle": -45, "axis": "x", "origin": [8, 4.65, 14.1]}, + "from": [13, 0, 0], + "to": [16, 1, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [3, 0, 0]}, "faces": { - "north": {"uv": [4.875, 9.125, 5.25, 9.5], "texture": "#0"}, - "east": {"uv": [4.125, 9.625, 4.375, 10], "texture": "#0"}, - "south": {"uv": [5.375, 9.125, 5.75, 9.5], "texture": "#0"}, - "west": {"uv": [4.5, 9.625, 4.75, 10], "texture": "#0"}, - "up": {"uv": [5.25, 9.875, 4.875, 9.625], "texture": "#0"}, - "down": {"uv": [5.75, 9.625, 5.375, 9.875], "texture": "#0"} - } - }, - { - "from": [3, 1, 14], - "to": [4, 6, 16], - "faces": { - "north": {"uv": [1.25, 10, 1.375, 10.625], "texture": "#0"}, - "east": {"uv": [1.875, 9, 2.125, 9.625], "texture": "#0"}, - "south": {"uv": [1.5, 10, 1.625, 10.625], "texture": "#0"}, - "west": {"uv": [2.25, 9, 2.5, 9.625], "texture": "#0"}, - "up": {"uv": [11.125, 0.625, 11, 0.375], "texture": "#0"}, - "down": {"uv": [11.125, 0.75, 11, 1], "texture": "#0"} - } - }, - { - "from": [1, 1, 13], - "to": [15, 6, 14], - "faces": { - "north": {"uv": [6.25, 3.75, 8, 4.375], "texture": "#0"}, - "east": {"uv": [1.75, 10, 1.875, 10.625], "texture": "#0"}, - "south": {"uv": [6.25, 4.5, 8, 5.125], "texture": "#0"}, - "west": {"uv": [2, 10, 2.125, 10.625], "texture": "#0"}, - "up": {"uv": [1.75, 8.75, 0, 8.625], "texture": "#0"}, - "down": {"uv": [10.375, 0.75, 8.625, 0.875], "texture": "#0"} - } - }, - { - "from": [1, 1, 2], - "to": [15, 6, 3], - "faces": { - "north": {"uv": [1.75, 6.625, 3.5, 7.25], "texture": "#0"}, - "east": {"uv": [5.625, 10, 5.75, 10.625], "texture": "#0"}, - "south": {"uv": [7, 2, 8.75, 2.625], "texture": "#0"}, - "west": {"uv": [6.625, 10, 6.75, 10.625], "texture": "#0"}, - "up": {"uv": [10.375, 1.125, 8.625, 1], "texture": "#0"}, - "down": {"uv": [10.375, 1.25, 8.625, 1.375], "texture": "#0"} - } - }, - { - "from": [1, 1, 14], - "to": [2, 6, 16], - "faces": { - "north": {"uv": [0.75, 10, 0.875, 10.625], "texture": "#0"}, - "east": {"uv": [4.5, 8.875, 4.75, 9.5], "texture": "#0"}, - "south": {"uv": [1, 10, 1.125, 10.625], "texture": "#0"}, - "west": {"uv": [8.875, 8.625, 9.125, 9.25], "texture": "#0"}, - "up": {"uv": [10.25, 11.125, 10.125, 10.875], "texture": "#0"}, - "down": {"uv": [11, 10.625, 10.875, 10.875], "texture": "#0"} - } - }, - { - "from": [11.999, 4.95312, 13.6803], - "to": [14.999, 7.95312, 15.6803], - "rotation": {"angle": -45, "axis": "x", "origin": [8, 4.65, 14.1]}, - "faces": { - "north": {"uv": [7.875, 9, 8.25, 9.375], "texture": "#0"}, - "east": {"uv": [0, 9.625, 0.25, 10], "texture": "#0"}, - "south": {"uv": [8.375, 9, 8.75, 9.375], "texture": "#0"}, - "west": {"uv": [0.375, 9.625, 0.625, 10], "texture": "#0"}, - "up": {"uv": [1.125, 9.875, 0.75, 9.625], "texture": "#0"}, - "down": {"uv": [1.625, 9.625, 1.25, 9.875], "texture": "#0"} - } - }, - { - "from": [14, 1, 14], - "to": [15, 6, 16], - "faces": { - "north": {"uv": [9.625, 9.875, 9.75, 10.5], "texture": "#0"}, - "east": {"uv": [8.875, 2, 9.125, 2.625], "texture": "#0"}, - "south": {"uv": [9.875, 9.875, 10, 10.5], "texture": "#0"}, - "west": {"uv": [4.125, 8.875, 4.375, 9.5], "texture": "#0"}, - "up": {"uv": [11, 7.875, 10.875, 7.625], "texture": "#0"}, - "down": {"uv": [11, 9.25, 10.875, 9.5], "texture": "#0"} - } - }, - { - "from": [12, 1, 14], - "to": [13, 6, 16], - "faces": { - "north": {"uv": [8.875, 9.875, 9, 10.5], "texture": "#0"}, - "east": {"uv": [1.125, 8.875, 1.375, 9.5], "texture": "#0"}, - "south": {"uv": [9.375, 9.875, 9.5, 10.5], "texture": "#0"}, - "west": {"uv": [1.5, 8.875, 1.75, 9.5], "texture": "#0"}, - "up": {"uv": [4.375, 11.125, 4.25, 10.875], "texture": "#0"}, - "down": {"uv": [4.625, 10.875, 4.5, 11.125], "texture": "#0"} - } - }, - { - "from": [10, 1, 14], - "to": [11, 6, 15], - "faces": { - "north": {"uv": [2.25, 10, 2.375, 10.625], "texture": "#0"}, - "east": {"uv": [3.625, 10, 3.75, 10.625], "texture": "#0"}, - "south": {"uv": [10, 3.625, 10.125, 4.25], "texture": "#0"}, - "west": {"uv": [3.875, 10, 4, 10.625], "texture": "#0"}, - "up": {"uv": [11.125, 5.625, 11, 5.5], "texture": "#0"}, - "down": {"uv": [11.125, 6.625, 11, 6.75], "texture": "#0"} - } - }, - { - "from": [5, 1, 14], - "to": [6, 6, 15], - "faces": { - "north": {"uv": [10, 4.375, 10.125, 5], "texture": "#0"}, - "east": {"uv": [4.875, 10, 5, 10.625], "texture": "#0"}, - "south": {"uv": [5.125, 10, 5.25, 10.625], "texture": "#0"}, - "west": {"uv": [5.375, 10, 5.5, 10.625], "texture": "#0"}, - "up": {"uv": [7, 11.125, 6.875, 11], "texture": "#0"}, - "down": {"uv": [7.25, 11, 7.125, 11.125], "texture": "#0"} - } - }, - { - "from": [10, 1, 1], - "to": [11, 6, 2], - "faces": { - "north": {"uv": [7.875, 10, 8, 10.625], "texture": "#0"}, - "east": {"uv": [8.125, 10, 8.25, 10.625], "texture": "#0"}, - "south": {"uv": [10, 8.625, 10.125, 9.25], "texture": "#0"}, - "west": {"uv": [0, 10.125, 0.125, 10.75], "texture": "#0"}, - "up": {"uv": [11, 11.125, 10.875, 11], "texture": "#0"}, - "down": {"uv": [11.25, 0, 11.125, 0.125], "texture": "#0"} - } - }, - { - "from": [6, 5, 1], - "to": [10, 6, 2], - "faces": { - "north": {"uv": [10, 1.75, 10.5, 1.875], "texture": "#0"}, - "east": {"uv": [11, 10.125, 11.125, 10.25], "texture": "#0"}, - "south": {"uv": [10.125, 2.75, 10.625, 2.875], "texture": "#0"}, - "west": {"uv": [11, 10.375, 11.125, 10.5], "texture": "#0"}, - "up": {"uv": [10.875, 10.25, 10.375, 10.125], "texture": "#0"}, - "down": {"uv": [10.875, 10.375, 10.375, 10.5], "texture": "#0"} - } - }, - { - "from": [6, 5, 14], - "to": [10, 6, 15], - "faces": { - "north": {"uv": [10.125, 2.75, 10.625, 2.875], "texture": "#0"}, - "east": {"uv": [11, 10.375, 11.125, 10.5], "texture": "#0"}, - "south": {"uv": [10, 1.75, 10.5, 1.875], "texture": "#0"}, - "west": {"uv": [11, 10.125, 11.125, 10.25], "texture": "#0"}, - "up": {"uv": [10.875, 10.25, 10.375, 10.125], "rotation": 180, "texture": "#0"}, - "down": {"uv": [10.875, 10.375, 10.375, 10.5], "rotation": 180, "texture": "#0"} - } - }, - { - "from": [9, 6, 1], - "to": [10, 7, 2], - "faces": { - "north": {"uv": [11.125, 1.375, 11.25, 1.5], "texture": "#0"}, - "east": {"uv": [11.125, 3.625, 11.25, 3.75], "texture": "#0"}, - "south": {"uv": [11.125, 7.625, 11.25, 7.75], "texture": "#0"}, - "west": {"uv": [7.875, 11.125, 8, 11.25], "texture": "#0"}, - "up": {"uv": [8.25, 11.25, 8.125, 11.125], "texture": "#0"}, - "down": {"uv": [8.5, 11.125, 8.375, 11.25], "texture": "#0"} - } - }, - { - "from": [5, 6, 2], - "to": [11, 7, 3], - "faces": { - "north": {"uv": [7.5, 9.75, 8.25, 9.875], "texture": "#0"}, - "east": {"uv": [5.75, 11.375, 5.875, 11.5], "texture": "#0"}, - "south": {"uv": [9.875, 2, 10.625, 2.125], "texture": "#0"}, - "west": {"uv": [11.375, 5.875, 11.5, 6], "texture": "#0"}, - "up": {"uv": [10.625, 2.375, 9.875, 2.25], "texture": "#0"}, - "down": {"uv": [10.625, 2.5, 9.875, 2.625], "texture": "#0"} - } - }, - { - "from": [4.6, 7, 2.625], - "to": [5.6, 7, 3.625], - "rotation": {"angle": -45, "axis": "y", "origin": [5.1, 7, 3.125]}, - "faces": { - "north": {"uv": [0, 15.625, 0.375, 16], "texture": "#0"}, - "east": {"uv": [0, 15.625, 0.375, 16], "texture": "#0"}, - "south": {"uv": [0, 15.625, 0.375, 16], "texture": "#0"}, - "west": {"uv": [0, 15.625, 0.375, 16], "texture": "#0"}, - "up": {"uv": [6.125, 11.5, 6, 11.375], "texture": "#0"}, - "down": {"uv": [6.375, 11.375, 6.25, 11.5], "texture": "#0"} - } - }, - { - "from": [10.275, 7, 12.5], - "to": [11.275, 7, 13.5], - "rotation": {"angle": -45, "axis": "y", "origin": [10.775, 7, 13]}, - "faces": { - "north": {"uv": [0, 15.5, 0.375, 16], "texture": "#0"}, - "east": {"uv": [0, 15.625, 0.375, 16], "texture": "#0"}, - "south": {"uv": [0, 15.375, 0.625, 16], "texture": "#0"}, - "west": {"uv": [0, 15.625, 0.375, 16], "texture": "#0"}, - "up": {"uv": [6.625, 11.5, 6.5, 11.375], "texture": "#0"}, - "down": {"uv": [11.5, 6.75, 11.375, 6.875], "texture": "#0"} - } - }, - { - "from": [4.725, 7, 12.5], - "to": [5.725, 7, 13.5], - "rotation": {"angle": 45, "axis": "y", "origin": [5.225, 7, 13]}, - "faces": { - "north": {"uv": [0, 15.625, 0.375, 16], "texture": "#0"}, - "east": {"uv": [0, 15.5, 0.5, 16], "texture": "#0"}, - "south": {"uv": [0, 15.625, 0.375, 16], "texture": "#0"}, - "west": {"uv": [0, 15.625, 0.375, 16], "texture": "#0"}, - "up": {"uv": [11.5, 7.125, 11.375, 7], "texture": "#0"}, - "down": {"uv": [11.5, 7.25, 11.375, 7.375], "texture": "#0"} - } - }, - { - "from": [10.275, 7, 2.5], - "to": [11.275, 7, 3.5], - "rotation": {"angle": 45, "axis": "y", "origin": [10.775, 7, 3]}, - "faces": { - "north": {"uv": [0, 16, 0.25, 15.75], "texture": "#0"}, - "east": {"uv": [0, 16, 0.25, 15.75], "texture": "#0"}, - "south": {"uv": [0, 15.625, 0.375, 16], "texture": "#0"}, - "west": {"uv": [0, 16, 0.375, 15.625], "texture": "#0"}, - "up": {"uv": [11.5, 7.625, 11.375, 7.5], "texture": "#0"}, - "down": {"uv": [8, 11.375, 7.875, 11.5], "texture": "#0"} - } - }, - { - "from": [6, 6, 1], - "to": [7, 7, 2], - "faces": { - "north": {"uv": [9.125, 11.125, 9.25, 11.25], "texture": "#0"}, - "east": {"uv": [11.125, 9.25, 11.25, 9.375], "texture": "#0"}, - "south": {"uv": [11.125, 10.625, 11.25, 10.75], "texture": "#0"}, - "west": {"uv": [11.125, 10.875, 11.25, 11], "texture": "#0"}, - "up": {"uv": [11.25, 11.25, 11.125, 11.125], "texture": "#0"}, - "down": {"uv": [0.125, 11.25, 0, 11.375], "texture": "#0"} - } - }, - { - "from": [6, 6, 14.001], - "to": [10, 10, 16.001], - "faces": { - "north": {"uv": [5, 8, 5.5, 8.5], "texture": "#0"}, - "east": {"uv": [10.5, 0.75, 10.75, 1.25], "texture": "#0"}, - "south": {"uv": [8.375, 2.75, 8.875, 3.25], "texture": "#0"}, - "west": {"uv": [10.5, 3, 10.75, 3.5], "texture": "#0"}, - "up": {"uv": [11, 1.625, 10.5, 1.375], "texture": "#0"}, - "down": {"uv": [11, 3.5, 10.5, 3.75], "texture": "#0"} - } - }, - { - "from": [5, 6, 13], - "to": [11, 7, 14], - "faces": { - "north": {"uv": [9.625, 1.5, 10.375, 1.625], "texture": "#0"}, - "east": {"uv": [5.5, 11.375, 5.625, 11.5], "texture": "#0"}, - "south": {"uv": [9.625, 7.625, 10.375, 7.75], "texture": "#0"}, - "west": {"uv": [11.375, 5.625, 11.5, 5.75], "texture": "#0"}, - "up": {"uv": [2.5, 9.875, 1.75, 9.75], "texture": "#0"}, - "down": {"uv": [7.375, 9.75, 6.625, 9.875], "texture": "#0"} - } - }, - { - "from": [2, 5, 15], - "to": [3, 6, 16], - "faces": { - "north": {"uv": [8.875, 11.25, 9, 11.375], "texture": "#0"}, - "east": {"uv": [9.375, 11.25, 9.5, 11.375], "texture": "#0"}, - "south": {"uv": [11.25, 9.5, 11.375, 9.625], "texture": "#0"}, - "west": {"uv": [9.625, 11.25, 9.75, 11.375], "texture": "#0"}, - "up": {"uv": [11.375, 9.875, 11.25, 9.75], "texture": "#0"}, - "down": {"uv": [10, 11.25, 9.875, 11.375], "texture": "#0"} - } - }, - { - "from": [13, 5, 15], - "to": [14, 6, 16], - "faces": { - "north": {"uv": [11.25, 10, 11.375, 10.125], "texture": "#0"}, - "east": {"uv": [10.125, 11.25, 10.25, 11.375], "texture": "#0"}, - "south": {"uv": [11.25, 10.25, 11.375, 10.375], "texture": "#0"}, - "west": {"uv": [10.375, 11.25, 10.5, 11.375], "texture": "#0"}, - "up": {"uv": [10.75, 11.375, 10.625, 11.25], "texture": "#0"}, - "down": {"uv": [11, 11.25, 10.875, 11.375], "texture": "#0"} - } - }, - { - "from": [2, 5, 0], - "to": [3, 6, 1], - "faces": { - "north": {"uv": [1.5, 11.375, 1.625, 11.5], "texture": "#0"}, - "east": {"uv": [1.75, 11.375, 1.875, 11.5], "texture": "#0"}, - "south": {"uv": [2, 11.375, 2.125, 11.5], "texture": "#0"}, - "west": {"uv": [2.25, 11.375, 2.375, 11.5], "texture": "#0"}, - "up": {"uv": [11.5, 3.25, 11.375, 3.125], "texture": "#0"}, - "down": {"uv": [11.5, 3.375, 11.375, 3.5], "texture": "#0"} - } - }, - { - "from": [13, 5, 0], - "to": [14, 6, 1], - "faces": { - "north": {"uv": [11.375, 0, 11.5, 0.125], "texture": "#0"}, - "east": {"uv": [0.75, 11.375, 0.875, 11.5], "texture": "#0"}, - "south": {"uv": [1, 11.375, 1.125, 11.5], "texture": "#0"}, - "west": {"uv": [11.375, 1, 11.5, 1.125], "texture": "#0"}, - "up": {"uv": [1.375, 11.5, 1.25, 11.375], "texture": "#0"}, - "down": {"uv": [11.5, 1.25, 11.375, 1.375], "texture": "#0"} - } - }, - { - "from": [5, 1, 1], - "to": [6, 6, 2], - "faces": { - "north": {"uv": [6.875, 10, 7, 10.625], "texture": "#0"}, - "east": {"uv": [7.125, 10, 7.25, 10.625], "texture": "#0"}, - "south": {"uv": [7.375, 10, 7.5, 10.625], "texture": "#0"}, - "west": {"uv": [7.625, 10, 7.75, 10.625], "texture": "#0"}, - "up": {"uv": [7.5, 11.125, 7.375, 11], "texture": "#0"}, - "down": {"uv": [7.75, 11, 7.625, 11.125], "texture": "#0"} - } - }, - { - "from": [1.001, 4.8, 1.75], - "to": [4.001, 7.8, 3.75], - "rotation": {"angle": 45, "axis": "x", "origin": [2.5, 6.3, 2.8]}, - "faces": { - "north": {"uv": [3.125, 9, 3.5, 9.375], "texture": "#0"}, - "east": {"uv": [3.75, 9.5, 4, 9.875], "texture": "#0"}, - "south": {"uv": [3.625, 9, 4, 9.375], "texture": "#0"}, - "west": {"uv": [9.5, 8.625, 9.75, 9], "texture": "#0"}, - "up": {"uv": [9.875, 9.375, 9.5, 9.125], "texture": "#0"}, - "down": {"uv": [9.875, 9.5, 9.5, 9.75], "texture": "#0"} - } - }, - { - "from": [3, 1, 0], - "to": [4, 6, 2], - "faces": { - "north": {"uv": [9.875, 7.875, 10, 8.5], "texture": "#0"}, - "east": {"uv": [0.375, 8.875, 0.625, 9.5], "texture": "#0"}, - "south": {"uv": [8.625, 9.875, 8.75, 10.5], "texture": "#0"}, - "west": {"uv": [0.75, 8.875, 1, 9.5], "texture": "#0"}, - "up": {"uv": [0.625, 11.125, 0.5, 10.875], "texture": "#0"}, - "down": {"uv": [4.125, 10.875, 4, 11.125], "texture": "#0"} - } - }, - { - "from": [2, 1, 1], - "to": [3, 6, 2], - "faces": { - "north": {"uv": [0.25, 10.125, 0.375, 10.75], "texture": "#0"}, - "east": {"uv": [0.5, 10.125, 0.625, 10.75], "texture": "#0"}, - "south": {"uv": [4.125, 10.125, 4.25, 10.75], "texture": "#0"}, - "west": {"uv": [4.375, 10.125, 4.5, 10.75], "texture": "#0"}, - "up": {"uv": [11.375, 6.625, 11.25, 6.5], "texture": "#0"}, - "down": {"uv": [7, 11.25, 6.875, 11.375], "texture": "#0"} - } - }, - { - "from": [13, 1, 1], - "to": [14, 6, 2], - "faces": { - "north": {"uv": [4.625, 10.125, 4.75, 10.75], "texture": "#0"}, - "east": {"uv": [10.125, 7.875, 10.25, 8.5], "texture": "#0"}, - "south": {"uv": [10.125, 9.375, 10.25, 10], "texture": "#0"}, - "west": {"uv": [10.125, 10.125, 10.25, 10.75], "texture": "#0"}, - "up": {"uv": [7.25, 11.375, 7.125, 11.25], "texture": "#0"}, - "down": {"uv": [7.5, 11.25, 7.375, 11.375], "texture": "#0"} - } - }, - { - "from": [2, 1, 14], - "to": [3, 6, 15], - "faces": { - "north": {"uv": [10.25, 3.625, 10.375, 4.25], "texture": "#0"}, - "east": {"uv": [10.25, 4.375, 10.375, 5], "texture": "#0"}, - "south": {"uv": [5.875, 10.25, 6, 10.875], "texture": "#0"}, - "west": {"uv": [6.125, 10.25, 6.25, 10.875], "texture": "#0"}, - "up": {"uv": [7.75, 11.375, 7.625, 11.25], "texture": "#0"}, - "down": {"uv": [11.375, 7.875, 11.25, 8], "texture": "#0"} - } - }, - { - "from": [13, 1, 14], - "to": [14, 6, 15], - "faces": { - "north": {"uv": [6.375, 10.25, 6.5, 10.875], "texture": "#0"}, - "east": {"uv": [10.25, 8.625, 10.375, 9.25], "texture": "#0"}, - "south": {"uv": [10.375, 7.875, 10.5, 8.5], "texture": "#0"}, - "west": {"uv": [10.375, 9.375, 10.5, 10], "texture": "#0"}, - "up": {"uv": [11.375, 8.25, 11.25, 8.125], "texture": "#0"}, - "down": {"uv": [8.75, 11.25, 8.625, 11.375], "texture": "#0"} - } - }, - { - "from": [1, 1, 0], - "to": [2, 6, 2], - "faces": { - "north": {"uv": [3.125, 9.875, 3.25, 10.5], "texture": "#0"}, - "east": {"uv": [7.5, 8.75, 7.75, 9.375], "texture": "#0"}, - "south": {"uv": [3.375, 9.875, 3.5, 10.5], "texture": "#0"}, - "west": {"uv": [0, 8.875, 0.25, 9.5], "texture": "#0"}, - "up": {"uv": [11, 0.25, 10.875, 0], "texture": "#0"}, - "down": {"uv": [0.375, 10.875, 0.25, 11.125], "texture": "#0"} - } - }, - { - "from": [0, 1, 1], - "to": [1, 5, 2], - "faces": { - "north": {"uv": [10.5, 3.875, 10.625, 4.375], "texture": "#0"}, - "east": {"uv": [10.5, 4.5, 10.625, 5], "texture": "#0"}, - "south": {"uv": [10.5, 5.125, 10.625, 5.625], "texture": "#0"}, - "west": {"uv": [10.5, 5.75, 10.625, 6.25], "texture": "#0"}, - "up": {"uv": [0.375, 11.375, 0.25, 11.25], "texture": "#0"}, - "down": {"uv": [11.375, 0.25, 11.25, 0.375], "texture": "#0"} - } - }, - { - "from": [15, 4, 12], - "to": [16, 5, 14], - "faces": { - "north": {"uv": [11.25, 5.375, 11.375, 5.5], "texture": "#0"}, - "east": {"uv": [11, 4.625, 11.25, 4.75], "texture": "#0"}, - "south": {"uv": [11.25, 6.25, 11.375, 6.375], "texture": "#0"}, - "west": {"uv": [11, 4.875, 11.25, 5], "texture": "#0"}, - "up": {"uv": [11.125, 5.375, 11, 5.125], "texture": "#0"}, - "down": {"uv": [6, 11, 5.875, 11.25], "texture": "#0"} - } - }, - { - "from": [15, 1, 14], - "to": [16, 5, 15], - "faces": { - "north": {"uv": [10.75, 1.625, 10.875, 2.125], "texture": "#0"}, - "east": {"uv": [1.75, 10.75, 1.875, 11.25], "texture": "#0"}, - "south": {"uv": [2, 10.75, 2.125, 11.25], "texture": "#0"}, - "west": {"uv": [2.25, 10.75, 2.375, 11.25], "texture": "#0"}, - "up": {"uv": [4.625, 11.375, 4.5, 11.25], "texture": "#0"}, - "down": {"uv": [11.375, 5.125, 11.25, 5.25], "texture": "#0"} - } - }, - { - "from": [15, 1, 11], - "to": [16, 5, 12], - "faces": { - "north": {"uv": [0.75, 10.75, 0.875, 11.25], "texture": "#0"}, - "east": {"uv": [1, 10.75, 1.125, 11.25], "texture": "#0"}, - "south": {"uv": [1.25, 10.75, 1.375, 11.25], "texture": "#0"}, - "west": {"uv": [1.5, 10.75, 1.625, 11.25], "texture": "#0"}, - "up": {"uv": [4.375, 11.375, 4.25, 11.25], "texture": "#0"}, - "down": {"uv": [11.375, 4.375, 11.25, 4.5], "texture": "#0"} - } - }, - { - "from": [0, 1, 6], - "to": [1, 10, 10], - "faces": { - "north": {"uv": [5.875, 9.125, 6, 10.25], "texture": "#0"}, - "east": {"uv": [0, 7.125, 0.5, 8.25], "texture": "#0"}, - "south": {"uv": [6.125, 9.125, 6.25, 10.25], "texture": "#0"}, - "west": {"uv": [7.125, 0, 7.625, 1.125], "texture": "#0"}, - "up": {"uv": [10.875, 2.75, 10.75, 2.25], "texture": "#0"}, - "down": {"uv": [10.875, 2.875, 10.75, 3.375], "texture": "#0"} - } - }, - { - "from": [15, 1, 6], - "to": [16, 10, 10], - "faces": { - "north": {"uv": [6.375, 9.125, 6.5, 10.25], "texture": "#0"}, - "east": {"uv": [0.625, 7.125, 1.125, 8.25], "texture": "#0"}, - "south": {"uv": [9.25, 8.625, 9.375, 9.75], "texture": "#0"}, - "west": {"uv": [3.625, 7.125, 4.125, 8.25], "texture": "#0"}, - "up": {"uv": [3.625, 11.25, 3.5, 10.75], "texture": "#0"}, - "down": {"uv": [3.875, 10.75, 3.75, 11.25], "texture": "#0"} - } - }, - { - "from": [14.001, 7, 6], - "to": [15.001, 10, 10], - "faces": { - "north": {"uv": [6.125, 11, 6.25, 11.25], "texture": "#0"}, - "east": {"uv": [9.25, 2, 9.75, 2.25], "texture": "#0"}, - "south": {"uv": [11, 6.25, 11.125, 6.5], "texture": "#0"}, - "west": {"uv": [9.25, 2.375, 9.75, 2.625], "texture": "#0"}, - "up": {"uv": [10.875, 4.375, 10.75, 3.875], "texture": "#0"}, - "down": {"uv": [10.875, 4.5, 10.75, 5], "texture": "#0"} - } - }, - { - "from": [15, 14, 6], - "to": [16, 15, 10], - "faces": { - "north": {"uv": [3.5, 11.375, 3.625, 11.5], "texture": "#0"}, - "east": {"uv": [10.625, 8.25, 11.125, 8.375], "texture": "#0"}, - "south": {"uv": [11.375, 3.625, 11.5, 3.75], "texture": "#0"}, - "west": {"uv": [10.625, 9.875, 11.125, 10], "texture": "#0"}, - "up": {"uv": [10.875, 5.625, 10.75, 5.125], "texture": "#0"}, - "down": {"uv": [5.5, 10.75, 5.375, 11.25], "texture": "#0"} - } - }, - { - "from": [0, 14, 6], - "to": [1, 15, 10], - "faces": { - "north": {"uv": [3.75, 11.375, 3.875, 11.5], "texture": "#0"}, - "east": {"uv": [10.75, 5.75, 11.25, 5.875], "texture": "#0"}, - "south": {"uv": [11.375, 4.625, 11.5, 4.75], "texture": "#0"}, - "west": {"uv": [10.75, 6, 11.25, 6.125], "texture": "#0"}, - "up": {"uv": [5.75, 11.25, 5.625, 10.75], "texture": "#0"}, - "down": {"uv": [10.875, 6.25, 10.75, 6.75], "texture": "#0"} - } - }, - { - "from": [6, 14, 0], - "to": [10, 15, 1], - "faces": { - "north": {"uv": [6.625, 10.75, 7.125, 10.875], "texture": "#0"}, - "east": {"uv": [4.75, 11.375, 4.875, 11.5], "texture": "#0"}, - "south": {"uv": [10.75, 6.875, 11.25, 7], "texture": "#0"}, - "west": {"uv": [11.375, 4.875, 11.5, 5], "texture": "#0"}, - "up": {"uv": [11.25, 7.25, 10.75, 7.125], "texture": "#0"}, - "down": {"uv": [7.75, 10.75, 7.25, 10.875], "texture": "#0"} - } - }, - { - "from": [6, 14, 15], - "to": [10, 15, 16], - "faces": { - "north": {"uv": [10.75, 7.375, 11.25, 7.5], "texture": "#0"}, - "east": {"uv": [5, 11.375, 5.125, 11.5], "texture": "#0"}, - "south": {"uv": [10.75, 8.5, 11.25, 8.625], "texture": "#0"}, - "west": {"uv": [5.25, 11.375, 5.375, 11.5], "texture": "#0"}, - "up": {"uv": [11.25, 8.875, 10.75, 8.75], "texture": "#0"}, - "down": {"uv": [11.25, 9, 10.75, 9.125], "texture": "#0"} - } - }, - { - "from": [1, 7, 6], - "to": [2, 10, 10], - "faces": { - "north": {"uv": [6.375, 11, 6.5, 11.25], "texture": "#0"}, - "east": {"uv": [2.625, 9.5, 3.125, 9.75], "texture": "#0"}, - "south": {"uv": [6.625, 11, 6.75, 11.25], "texture": "#0"}, - "west": {"uv": [9.5, 2.75, 10, 3], "texture": "#0"}, - "up": {"uv": [5, 11.25, 4.875, 10.75], "texture": "#0"}, - "down": {"uv": [5.25, 10.75, 5.125, 11.25], "texture": "#0"} - } - }, - { - "from": [1, 1.0001, 3], - "to": [15, 7.0001, 13], - "faces": { - "north": {"uv": [2.125, 4.875, 3.875, 5.625], "texture": "#0"}, - "east": {"uv": [7, 2.75, 8.25, 3.5], "texture": "#0"}, - "south": {"uv": [5.125, 2, 6.875, 2.75], "texture": "#0"}, - "west": {"uv": [5.75, 7, 7, 7.75], "texture": "#0"}, - "up": {"uv": [3.875, 1.25, 2.125, 0], "texture": "#0"}, - "down": {"uv": [3.875, 1.375, 2.125, 2.625], "texture": "#0"} - } - }, - { - "from": [15, 1, 4], - "to": [16, 5, 5], - "faces": { - "north": {"uv": [9.875, 10.625, 10, 11.125], "texture": "#0"}, - "east": {"uv": [10.375, 10.625, 10.5, 11.125], "texture": "#0"}, - "south": {"uv": [10.625, 10.625, 10.75, 11.125], "texture": "#0"}, - "west": {"uv": [10.75, 0.625, 10.875, 1.125], "texture": "#0"}, - "up": {"uv": [4.125, 11.375, 4, 11.25], "texture": "#0"}, - "down": {"uv": [11.375, 4.125, 11.25, 4.25], "texture": "#0"} - } - }, - { - "from": [15, 4, 2], - "to": [16, 5, 4], - "faces": { - "north": {"uv": [3.25, 11.25, 3.375, 11.375], "texture": "#0"}, - "east": {"uv": [11, 3.125, 11.25, 3.25], "texture": "#0"}, - "south": {"uv": [11.25, 3.875, 11.375, 4], "texture": "#0"}, - "west": {"uv": [11, 3.375, 11.25, 3.5], "texture": "#0"}, - "up": {"uv": [11.125, 4.125, 11, 3.875], "texture": "#0"}, - "down": {"uv": [11.125, 4.25, 11, 4.5], "texture": "#0"} - } - }, - { - "from": [15, 1, 1], - "to": [16, 5, 2], - "faces": { - "north": {"uv": [8.875, 10.625, 9, 11.125], "texture": "#0"}, - "east": {"uv": [10.625, 9.25, 10.75, 9.75], "texture": "#0"}, - "south": {"uv": [9.375, 10.625, 9.5, 11.125], "texture": "#0"}, - "west": {"uv": [9.625, 10.625, 9.75, 11.125], "texture": "#0"}, - "up": {"uv": [11.375, 3, 11.25, 2.875], "texture": "#0"}, - "down": {"uv": [3.125, 11.25, 3, 11.375], "texture": "#0"} - } - }, - { - "from": [0, 4, 2], - "to": [1, 5, 4], - "faces": { - "north": {"uv": [11.25, 2.375, 11.375, 2.5], "texture": "#0"}, - "east": {"uv": [1.25, 7.125, 1.5, 7.25], "texture": "#0"}, - "south": {"uv": [2.5, 11.25, 2.625, 11.375], "texture": "#0"}, - "west": {"uv": [10.875, 8, 11.125, 8.125], "texture": "#0"}, - "up": {"uv": [11.125, 1.875, 11, 1.625], "texture": "#0"}, - "down": {"uv": [11.125, 2, 11, 2.25], "texture": "#0"} - } - }, - { - "from": [0, 4, 12], - "to": [1, 5, 14], - "faces": { - "north": {"uv": [11.25, 2.625, 11.375, 2.75], "texture": "#0"}, - "east": {"uv": [10.875, 9.625, 11.125, 9.75], "texture": "#0"}, - "south": {"uv": [2.75, 11.25, 2.875, 11.375], "texture": "#0"}, - "west": {"uv": [11, 1.125, 11.25, 1.25], "texture": "#0"}, - "up": {"uv": [11.125, 2.625, 11, 2.375], "texture": "#0"}, - "down": {"uv": [11.125, 2.75, 11, 3], "texture": "#0"} + "north": {"uv": [0, 3.75, 0.75, 4], "texture": "#12"}, + "east": {"uv": [0, 7.75, 0.25, 8], "texture": "#12"}, + "down": {"uv": [0, 11.75, 1, 12], "texture": "#12"} } }, { "from": [0, 0, 0], - "to": [16, 1, 16], + "to": [3, 1, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [-10, 0, 0]}, "faces": { - "north": {"uv": [7.875, 1.75, 9.875, 1.875], "texture": "#0"}, - "east": {"uv": [8.375, 3.375, 10.375, 3.5], "texture": "#0"}, - "south": {"uv": [8.375, 5.125, 10.375, 5.25], "texture": "#0"}, - "west": {"uv": [8.375, 5.375, 10.375, 5.5], "texture": "#0"}, - "up": {"uv": [2, 2, 0, 0], "texture": "#0"}, - "down": {"uv": [2, 2.125, 0, 4.125], "texture": "#0"} + "north": {"uv": [3.25, 3.75, 4, 4], "texture": "#12"}, + "west": {"uv": [0, 7.75, 0.25, 8], "texture": "#12"}, + "down": {"uv": [3, 11.75, 4, 12], "texture": "#12"} } }, { - "from": [0, 1, 14], - "to": [1, 5, 15], + "from": [3, 14, 0], + "to": [13, 16, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [5, 14, 0]}, "faces": { - "north": {"uv": [3, 10.625, 3.125, 11.125], "texture": "#0"}, - "east": {"uv": [3.25, 10.625, 3.375, 11.125], "texture": "#0"}, - "south": {"uv": [10.625, 7.625, 10.75, 8.125], "texture": "#0"}, - "west": {"uv": [8.625, 10.625, 8.75, 11.125], "texture": "#0"}, - "up": {"uv": [11.375, 2, 11.25, 1.875], "texture": "#0"}, - "down": {"uv": [11.375, 2.125, 11.25, 2.25], "texture": "#0"} + "north": {"uv": [0.75, 0, 3.25, 0.5], "texture": "#12"}, + "up": {"uv": [4.75, 4, 7.25, 4.25], "texture": "#12"}, + "down": {"uv": [7, 10.25, 9.5, 10.5], "texture": "#12"} } }, { - "from": [0, 1, 11], - "to": [1, 5, 12], + "from": [0, 1, 0], + "to": [3, 5, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 1, 0]}, "faces": { - "north": {"uv": [9.125, 10.5, 9.25, 11], "texture": "#0"}, - "east": {"uv": [10.625, 0, 10.75, 0.5], "texture": "#0"}, - "south": {"uv": [2.5, 10.625, 2.625, 11.125], "texture": "#0"}, - "west": {"uv": [2.75, 10.625, 2.875, 11.125], "texture": "#0"}, - "up": {"uv": [11.375, 0.875, 11.25, 0.75], "texture": "#0"}, - "down": {"uv": [11.375, 1.625, 11.25, 1.75], "texture": "#0"} + "north": {"uv": [3.25, 2.75, 4, 3.75], "texture": "#12"}, + "east": {"uv": [10.5, 0, 10.75, 1], "texture": "#12"}, + "west": {"uv": [0, 6.75, 0.25, 7.75], "texture": "#12"} } }, { - "from": [0, 1, 4], - "to": [1, 5, 5], + "from": [13, 1, 0], + "to": [16, 5, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [14, 1, 0]}, "faces": { - "north": {"uv": [10.5, 6.375, 10.625, 6.875], "texture": "#0"}, - "east": {"uv": [10.5, 7, 10.625, 7.5], "texture": "#0"}, - "south": {"uv": [8.375, 10.5, 8.5, 11], "texture": "#0"}, - "west": {"uv": [10.5, 8.625, 10.625, 9.125], "texture": "#0"}, - "up": {"uv": [0.625, 11.375, 0.5, 11.25], "texture": "#0"}, - "down": {"uv": [11.375, 0.5, 11.25, 0.625], "texture": "#0"} + "north": {"uv": [0, 2.75, 0.75, 3.75], "texture": "#12"}, + "east": {"uv": [0, 6.75, 0.25, 7.75], "texture": "#12"}, + "west": {"uv": [10.5, 0, 10.75, 1], "texture": "#12"} } }, { - "from": [14, 1, 0], - "to": [15, 6, 2], + "from": [13, 8, 0], + "to": [16, 14, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [14, 8, 0]}, "faces": { - "north": {"uv": [2.625, 9.875, 2.75, 10.5], "texture": "#0"}, - "east": {"uv": [6.75, 8.75, 7, 9.375], "texture": "#0"}, - "south": {"uv": [2.875, 9.875, 3, 10.5], "texture": "#0"}, - "west": {"uv": [7.125, 8.75, 7.375, 9.375], "texture": "#0"}, - "up": {"uv": [8.25, 11, 8.125, 10.75], "texture": "#0"}, - "down": {"uv": [0.125, 10.875, 0, 11.125], "texture": "#0"} + "north": {"uv": [0, 0.5, 0.75, 2], "texture": "#12"}, + "east": {"uv": [0, 4.5, 0.25, 6], "texture": "#12"}, + "west": {"uv": [9.5, 8.75, 9.75, 10.25], "texture": "#12"} } }, { - "from": [11.999, 4.8, 1.75], - "to": [14.999, 7.8, 3.75], - "rotation": {"angle": 45, "axis": "x", "origin": [13.5, 6.3, 2.8]}, + "from": [0, 8, 0], + "to": [3, 14, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [1, 8, 0]}, "faces": { - "north": {"uv": [2.625, 9, 3, 9.375], "texture": "#0"}, - "east": {"uv": [1.75, 5.25, 2, 5.625], "texture": "#0"}, - "south": {"uv": [9, 2.75, 9.375, 3.125], "texture": "#0"}, - "west": {"uv": [3.625, 6.5, 3.875, 6.875], "texture": "#0"}, - "up": {"uv": [3.625, 9.75, 3.25, 9.5], "texture": "#0"}, - "down": {"uv": [9, 9.5, 8.625, 9.75], "texture": "#0"} + "north": {"uv": [3.25, 0.5, 4, 2], "texture": "#12"}, + "east": {"uv": [9.5, 8.75, 9.75, 10.25], "texture": "#12"}, + "west": {"uv": [0, 4.5, 0.25, 6], "texture": "#12"} } }, { - "name": "Octagon", - "from": [5.51472, 7, 14], - "to": [10.48528, 13, 14], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 7.5, 8]}, + "from": [3, 5, 0], + "to": [13, 8, 1], "faces": { - "north": {"uv": [7.125, 1.125, 7.75, 1.875], "texture": "#0"}, - "east": {"uv": [0, 15.25, 0.375, 16], "texture": "#0"}, - "south": {"uv": [4.25, 7.125, 4.875, 7.875], "texture": "#0"}, - "west": {"uv": [0, 15.25, 0.25, 16], "texture": "#0"}, - "up": {"uv": [0.625, 15.625, 0, 16], "texture": "#0"}, - "down": {"uv": [0.625, 15.625, 0, 16], "texture": "#0"} + "north": {"uv": [0.75, 2, 3.25, 2.75], "texture": "#12"}, + "up": {"uv": [7, 10.5, 9.5, 10.75], "texture": "#12"}, + "down": {"uv": [8, 1.75, 10.5, 1.5], "texture": "#12"} } }, { - "name": "Octagon", - "from": [14, 7, 5.51472], - "to": [14, 13, 10.48528], + "from": [0, 5, 0], + "to": [3, 8, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 5, 0]}, "faces": { - "north": {"uv": [0, 15.25, 0.375, 16], "texture": "#0"}, - "east": {"uv": [7.25, 7.875, 7.875, 8.625], "texture": "#0"}, - "south": {"uv": [0, 15.25, 0.375, 16], "texture": "#0"}, - "west": {"uv": [4.25, 8, 4.875, 8.75], "texture": "#0"}, - "up": {"uv": [0, 16, 0.375, 15.375], "texture": "#0"}, - "down": {"uv": [0, 15.375, 0.5, 16], "texture": "#0"} + "north": {"uv": [3.25, 2, 4, 2.75], "texture": "#12"}, + "west": {"uv": [0, 6, 0.25, 6.75], "texture": "#12"} } }, { - "name": "Octagon", - "from": [14, 7, 5.51472], - "to": [14, 13, 10.48528], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 12.5, 8]}, - "faces": { - "north": {"uv": [0.375, 15.25, 0, 16], "texture": "#0"}, - "east": {"uv": [6.5, 7.875, 7.125, 8.625], "texture": "#0"}, - "south": {"uv": [0, 15.25, 0.625, 16], "texture": "#0"}, - "west": {"uv": [7.875, 7, 8.5, 7.75], "texture": "#0"}, - "up": {"uv": [0, 16, 0.5, 15.375], "texture": "#0"}, - "down": {"uv": [0, 15.375, 0.375, 16], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [5.51472, 7, 2], - "to": [10.48528, 13, 2], - "faces": { - "north": {"uv": [7.875, 0.875, 8.5, 1.625], "texture": "#0"}, - "east": {"uv": [0, 15.25, 0.625, 16], "texture": "#0"}, - "south": {"uv": [5.75, 7.875, 6.375, 8.625], "texture": "#0"}, - "west": {"uv": [0, 15.25, 0.5, 16], "texture": "#0"}, - "up": {"uv": [0.625, 15.25, 0, 16], "texture": "#0"}, - "down": {"uv": [0.625, 15.25, 0, 16], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [5.51472, 7, 2], - "to": [10.48528, 13, 2], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 12.5, 8]}, - "faces": { - "north": {"uv": [7.625, 6.125, 8.25, 6.875], "texture": "#0"}, - "east": {"uv": [0, 15.25, 0.5, 16], "texture": "#0"}, - "south": {"uv": [7.75, 0, 8.375, 0.75], "texture": "#0"}, - "west": {"uv": [0, 15.25, 0.625, 16], "texture": "#0"}, - "up": {"uv": [0.625, 15.25, 0, 16], "texture": "#0"}, - "down": {"uv": [0.625, 15.25, 0, 16], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [5.51472, 7, 14], - "to": [10.48528, 13, 14], - "faces": { - "north": {"uv": [5, 7.125, 5.625, 7.875], "texture": "#0"}, - "east": {"uv": [0, 15.25, 0.375, 16], "texture": "#0"}, - "south": {"uv": [7.125, 7, 7.75, 7.75], "texture": "#0"}, - "west": {"uv": [0, 15.25, 0.375, 16], "texture": "#0"}, - "up": {"uv": [0.625, 15.25, 0, 16], "texture": "#0"}, - "down": {"uv": [0.625, 15.375, 0, 16], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [2, 7, 5.51472], - "to": [2, 13, 10.48528], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 7.5, 8]}, - "faces": { - "north": {"uv": [0.5, 15.25, 0, 16], "texture": "#0"}, - "east": {"uv": [1.25, 7.375, 1.875, 8.125], "texture": "#0"}, - "south": {"uv": [0, 15.25, 0.375, 16], "texture": "#0"}, - "west": {"uv": [2, 7.375, 2.625, 8.125], "texture": "#0"}, - "up": {"uv": [0, 16, 0.5, 15.375], "texture": "#0"}, - "down": {"uv": [0, 15.375, 0.5, 16], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [2, 7, 5.51472], - "to": [2, 13, 10.48528], - "faces": { - "north": {"uv": [0, 15.25, 0.375, 16], "texture": "#0"}, - "east": {"uv": [2.75, 7.375, 3.375, 8.125], "texture": "#0"}, - "south": {"uv": [0, 15.25, 0.625, 16], "texture": "#0"}, - "west": {"uv": [7.625, 5.25, 8.25, 6], "texture": "#0"}, - "up": {"uv": [0, 16, 0.375, 15.375], "texture": "#0"}, - "down": {"uv": [0, 15.375, 0.5, 16], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [5.10051, 13, 1], - "to": [10.89949, 15, 15], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 14, 8]}, - "faces": { - "north": {"uv": [5.125, 3.75, 5.875, 4], "texture": "#0"}, - "east": {"uv": [8, 7.875, 9.75, 8.125], "texture": "#0"}, - "south": {"uv": [1.875, 8.625, 2.625, 8.875], "texture": "#0"}, - "west": {"uv": [8.125, 3.625, 9.875, 3.875], "texture": "#0"}, - "up": {"uv": [0.75, 7, 0, 5.25], "texture": "#0"}, - "down": {"uv": [1.625, 5.25, 0.875, 7], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [5.10051, 12.999, 1], - "to": [10.89949, 15, 15], + "from": [13, 5, 0], + "to": [16, 8, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [13, 5, 0]}, "faces": { - "north": {"uv": [2.75, 8.625, 3.5, 8.875], "texture": "#0"}, - "east": {"uv": [8.125, 4, 9.875, 4.25], "texture": "#0"}, - "south": {"uv": [8.625, 6.875, 9.375, 7.125], "texture": "#0"}, - "west": {"uv": [8.125, 4.375, 9.875, 4.625], "texture": "#0"}, - "up": {"uv": [4.75, 7, 4, 5.25], "texture": "#0"}, - "down": {"uv": [5.625, 5.25, 4.875, 7], "texture": "#0"} + "north": {"uv": [0, 2, 0.75, 2.75], "texture": "#12"}, + "east": {"uv": [0, 6, 0.25, 6.75], "texture": "#12"} } }, { - "name": "Octagon", - "from": [1, 13, 5.10051], - "to": [15, 15, 10.89949], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 14, 8]}, + "from": [0, 14, 0], + "to": [3, 16, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 14, 0]}, "faces": { - "north": {"uv": [8.125, 4.75, 9.875, 5], "texture": "#0"}, - "east": {"uv": [8.625, 7.25, 9.375, 7.5], "texture": "#0"}, - "south": {"uv": [0, 8.25, 1.75, 8.5], "texture": "#0"}, - "west": {"uv": [8, 8.625, 8.75, 8.875], "texture": "#0"}, - "up": {"uv": [6.875, 3.625, 5.125, 2.875], "texture": "#0"}, - "down": {"uv": [3.5, 5.75, 1.75, 6.5], "texture": "#0"} + "north": {"uv": [0, 0, 0.75, 0.5], "texture": "#12"}, + "west": {"uv": [0, 4, 0.25, 4.5], "texture": "#12"}, + "up": {"uv": [7.25, 4, 8, 4.25], "texture": "#12"} } }, { - "name": "Octagon", - "from": [1, 12.999, 5.10051], - "to": [15, 15, 10.89949], + "from": [13, 14, 0], + "to": [16, 16, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [13, 14, 0]}, "faces": { - "north": {"uv": [1.875, 8.25, 3.625, 8.5], "texture": "#0"}, - "east": {"uv": [5, 8.75, 5.75, 9], "texture": "#0"}, - "south": {"uv": [8, 8.25, 9.75, 8.5], "texture": "#0"}, - "west": {"uv": [5.875, 8.75, 6.625, 9], "texture": "#0"}, - "up": {"uv": [7.5, 6, 5.75, 5.25], "texture": "#0"}, - "down": {"uv": [7.5, 6.125, 5.75, 6.875], "texture": "#0"} + "north": {"uv": [0, 0, 0.75, 0.5], "texture": "#12"}, + "east": {"uv": [0, 4, 0.25, 4.5], "texture": "#12"}, + "up": {"uv": [4, 4, 4.75, 4.25], "texture": "#12"} } }, { - "name": "Octagon", - "from": [4.68629, 15, 0], - "to": [11.31371, 16, 16], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 16, 8]}, + "from": [3, 1, 1], + "to": [13, 5, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [3, 1, 1]}, "faces": { - "north": {"uv": [8.625, 1.5, 9.5, 1.625], "texture": "#0"}, - "east": {"uv": [8.375, 5.625, 10.375, 5.75], "texture": "#0"}, - "south": {"uv": [8.625, 7.625, 9.5, 7.75], "texture": "#0"}, - "west": {"uv": [8.375, 5.875, 10.375, 6], "texture": "#0"}, - "up": {"uv": [3, 4.70938, 2.125, 2.7875], "texture": "#0"}, - "down": {"uv": [4, 2.75, 3.125, 4.75], "texture": "#0"} + "north": {"uv": [8, 0, 10.5, 1], "texture": "#1"} } }, { - "name": "Octagon", - "from": [4.68629, 15.001, 0], - "to": [11.31371, 16.001, 16], + "from": [3, 8, 1], + "to": [13, 14, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [3, 8, 1]}, "faces": { - "north": {"uv": [9.5, 3.125, 10.375, 3.25], "texture": "#0"}, - "east": {"uv": [8.375, 6.125, 10.375, 6.25], "texture": "#0"}, - "south": {"uv": [6.625, 9.5, 7.5, 9.625], "texture": "#0"}, - "west": {"uv": [8.375, 6.375, 10.375, 6.5], "texture": "#0"}, - "up": {"uv": [4.875, 2, 4, 0], "texture": "#0"}, - "down": {"uv": [5, 2.125, 4.125, 4.125], "texture": "#0"} + "north": {"uv": [7, 8.75, 9.5, 10.25], "texture": "#1"} } }, { - "name": "Octagon", - "from": [0, 15, 4.68629], - "to": [16, 16, 11.31371], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 16, 8]}, + "from": [12, 0, 1], + "to": [16, 9, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [12, 0, 1]}, "faces": { - "north": {"uv": [8.375, 6.625, 10.375, 6.75], "texture": "#0"}, - "east": {"uv": [9.5, 6.875, 10.375, 7], "texture": "#0"}, - "south": {"uv": [8.5, 0, 10.5, 0.125], "texture": "#0"}, - "west": {"uv": [9.5, 7.125, 10.375, 7.25], "texture": "#0"}, - "up": {"uv": [6.9625, 0.875, 5.04062, 0], "texture": "#0"}, - "down": {"uv": [6.125, 4.25, 4.125, 5.125], "texture": "#0"} + "east": {"uv": [4, 5.75, 0.25, 8], "texture": "#12"}, + "south": {"uv": [7, 1.75, 8, 4], "texture": "#12"}, + "down": {"uv": [3, 8, 4, 11.75], "texture": "#12"} } }, { - "name": "Octagon", - "from": [0, 15.002, 4.68629], - "to": [16, 16.002, 11.31371], + "from": [0, 0, 1], + "to": [4, 9, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 0, 1]}, "faces": { - "north": {"uv": [8.5, 0.25, 10.5, 0.375], "texture": "#0"}, - "east": {"uv": [4.775, 11.75, 5.60343, 11.875], "texture": "#0"}, - "south": {"uv": [8.5, 0.5, 10.5, 0.625], "texture": "#0"}, - "west": {"uv": [4.775, 12.125, 5.60343, 12.25], "texture": "#0"}, - "up": {"uv": [2, 12.6625, 0, 11.83408], "texture": "#0"}, - "down": {"uv": [4.375, 11.75, 2.375, 12.625], "texture": "#0"} + "south": {"uv": [4, 1.75, 5, 4], "texture": "#12"}, + "west": {"uv": [0.25, 5.75, 4, 8], "texture": "#12"}, + "down": {"uv": [0, 8, 1, 11.75], "texture": "#12"} } }, { - "from": [6.042, 7.5525, 6.042], - "to": [9.958, 12.4475, 9.958], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 10, 8]}, + "from": [0, 9, 1], + "to": [3, 16, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 9, 1]}, "faces": { - "north": {"uv": [6, 5, 14, 15], "texture": "#1"}, - "east": {"uv": [6, 5, 14, 15], "texture": "#1"}, - "south": {"uv": [6, 5, 14, 15], "texture": "#1"}, - "west": {"uv": [6, 5, 14, 15], "texture": "#1"}, - "up": {"uv": [14, 14, 6, 5], "texture": "#1"}, - "down": {"uv": [14, 14, 6, 6], "texture": "#1"} + "east": {"uv": [16, 3.75, 12.25, 5.75], "texture": "#12"}, + "south": {"uv": [4, 0, 4.75, 1.75], "texture": "#12"}, + "west": {"uv": [0.25, 4, 4, 5.75], "texture": "#12"}, + "up": {"uv": [4, 4.25, 4.75, 8], "texture": "#12"} } }, { - "from": [9.958, 8.042, 6.042], - "to": [10.4475, 11.958, 9.958], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 10, 8]}, + "from": [13, 9, 1], + "to": [16, 16, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [13, 9, 1]}, "faces": { - "north": {"uv": [5, 6, 6, 14], "texture": "#1"}, - "east": {"uv": [6, 6, 14, 14], "texture": "#1"}, - "south": {"uv": [14, 6, 15, 14], "texture": "#1"}, - "west": {"uv": [5, 6, 6, 14], "texture": "#1"}, - "up": {"uv": [5, 6, 6, 7], "texture": "#1"}, - "down": {"uv": [5, 14, 6, 6], "texture": "#1"} + "east": {"uv": [4, 4, 0.25, 5.75], "texture": "#12"}, + "south": {"uv": [7.25, 0, 8, 1.75], "texture": "#12"}, + "west": {"uv": [12.25, 3.75, 16, 5.75], "texture": "#12"}, + "up": {"uv": [7.25, 4.25, 8, 8], "texture": "#12"} } }, { - "from": [5.5525, 8.042, 6.042], - "to": [6.042, 11.958, 9.958], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 10, 8]}, + "from": [4, 0, 1], + "to": [12, 4, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [4, 0, 1]}, "faces": { - "north": {"uv": [14, 6, 15, 14], "texture": "#1"}, - "east": {"uv": [14, 6, 15, 14], "texture": "#1"}, - "south": {"uv": [5, 6, 6, 14], "texture": "#1"}, - "west": {"uv": [6, 6, 14, 14], "texture": "#1"}, - "up": {"uv": [14, 6, 15, 7], "texture": "#1"}, - "down": {"uv": [14, 14, 15, 6], "texture": "#1"} + "south": {"uv": [5, 3, 7, 4], "texture": "#12"}, + "down": {"uv": [1, 8, 3, 11.75], "texture": "#12"} } }, { - "from": [5.063, 9.021, 7.021], - "to": [5.5525, 10.979, 8.979], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 10, 8]}, + "from": [3, 9.05, 13.05], + "to": [13, 16.05, 16.05], + "rotation": {"angle": 0, "axis": "y", "origin": [3, 14.05, 13.05]}, "faces": { - "north": {"uv": [15, 8, 16, 12], "texture": "#1"}, - "east": {"uv": [15, 8, 16, 12], "texture": "#1"}, - "south": {"uv": [4, 8, 5, 12], "texture": "#1"}, - "west": {"uv": [8, 8, 12, 12], "texture": "#1"}, - "up": {"uv": [15, 12, 16, 8], "texture": "#1"}, - "down": {"uv": [15, 12, 16, 8], "texture": "#1"} + "south": {"uv": [4, 8.75, 6.5, 10.5], "texture": "#12"}, + "up": {"uv": [4, 8, 6.5, 8.75], "texture": "#12"} } }, { - "from": [10.4475, 9.021, 7.021], - "to": [10.937, 10.979, 8.979], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 10, 8]}, + "from": [3, 12, 1], + "to": [13, 16, 13], + "rotation": {"angle": 0, "axis": "y", "origin": [3, 14, 1]}, "faces": { - "north": {"uv": [4, 8, 5, 12], "texture": "#1"}, - "east": {"uv": [8, 8, 12, 12], "texture": "#1"}, - "south": {"uv": [15, 8, 16, 12], "texture": "#1"}, - "west": {"uv": [4, 8, 5, 12], "texture": "#1"}, - "up": {"uv": [4, 12, 5, 8], "texture": "#1"}, - "down": {"uv": [4, 8, 5, 12], "texture": "#1"} + "south": {"uv": [8.75, 3.75, 11.25, 4.75], "texture": "#12"}, + "up": {"uv": [4.75, 4.25, 7.25, 7.25], "texture": "#12"} } }, { - "from": [7.021, 12.4475, 7.021], - "to": [8.979, 12.937, 8.979], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 10, 8]}, + "from": [4, 4, 12], + "to": [12, 12, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [5, 9, 14]}, "faces": { - "north": {"uv": [8, 4, 12, 5], "texture": "#1"}, - "east": {"uv": [8, 4, 12, 5], "texture": "#1"}, - "south": {"uv": [8, 4, 12, 5], "texture": "#1"}, - "west": {"uv": [8, 4, 12, 5], "texture": "#1"}, - "up": {"uv": [12, 12, 8, 8], "texture": "#1"}, - "down": {"uv": [8, 4, 12, 5], "texture": "#1"} + "east": {"uv": [8, 1.75, 9, 3.75], "texture": "#12"}, + "south": {"uv": [5, 1, 7, 3], "texture": "#12"}, + "west": {"uv": [8, 1.75, 9, 3.75], "texture": "#12"}, + "up": {"uv": [8, 2.75, 10, 3.75], "texture": "#12"} } }, { - "from": [7.021, 7.063, 7.021], - "to": [8.979, 7.5525, 8.979], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 10, 8]}, + "from": [5, 7.9, 11.875], + "to": [6, 15.9, 15.875], + "rotation": {"angle": 0, "axis": "y", "origin": [5, 7.9, 10.875]}, "faces": { - "north": {"uv": [8, 15, 12, 16], "texture": "#1"}, - "east": {"uv": [8, 15, 12, 16], "texture": "#1"}, - "south": {"uv": [8, 15, 12, 16], "texture": "#1"}, - "west": {"uv": [8, 15, 12, 16], "texture": "#1"}, - "up": {"uv": [8, 15, 12, 16], "texture": "#1"}, - "down": {"uv": [12, 12, 8, 8], "texture": "#1"} + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} } }, { - "from": [6.042, 8.042, 9.958], - "to": [9.958, 11.958, 10.4475], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 10, 8]}, + "from": [7, 7.9, 11.875], + "to": [8, 15.9, 15.875], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 7.9, 10.875]}, "faces": { - "north": {"uv": [6, 6, 14, 14], "texture": "#1"}, - "east": {"uv": [5, 6, 6, 14], "texture": "#1"}, - "south": {"uv": [6, 6, 14, 14], "texture": "#1"}, - "west": {"uv": [13, 6, 14, 14], "texture": "#1"}, - "up": {"uv": [14, 5, 6, 6], "texture": "#1"}, - "down": {"uv": [14, 14, 6, 15], "texture": "#1"} + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} } }, { - "from": [6.042, 8.042, 5.5525], - "to": [9.958, 11.958, 6.042], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 10, 8]}, + "from": [10, 7.9, 11.825], + "to": [11, 15.9, 15.825], + "rotation": {"angle": 0, "axis": "y", "origin": [9, 7.9, 10.825]}, "faces": { - "north": {"uv": [6, 6, 14, 14], "texture": "#1"}, - "east": {"uv": [14, 6, 15, 14], "texture": "#1"}, - "south": {"uv": [6, 6, 14, 14], "texture": "#1"}, - "west": {"uv": [14, 6, 15, 14], "texture": "#1"}, - "up": {"uv": [14, 15, 6, 14], "texture": "#1"}, - "down": {"uv": [6, 5, 14, 6], "texture": "#1"} + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} } }, { - "from": [7.021, 9.021, 5.063], - "to": [8.979, 10.979, 5.5525], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 10, 8]}, + "from": [12, 7.9, 11.9], + "to": [13, 15.9, 15.9], + "rotation": {"angle": 0, "axis": "y", "origin": [11, 7.9, 10.9]}, "faces": { - "north": {"uv": [8, 8, 12, 12], "texture": "#1"}, - "east": {"uv": [15, 8, 16, 12], "texture": "#1"}, - "south": {"uv": [8, 8, 12, 12], "texture": "#1"}, - "west": {"uv": [4, 8, 5, 12], "texture": "#1"}, - "up": {"uv": [12, 15, 8, 16], "texture": "#1"}, - "down": {"uv": [12, 4, 8, 5], "texture": "#1"} + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} } }, { - "from": [7.021, 9.021, 10.4475], - "to": [8.979, 10.979, 10.937], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 10, 8]}, + "from": [3, 7.9, 11.9], + "to": [4, 15.9, 15.9], + "rotation": {"angle": 0, "axis": "y", "origin": [2, 7.9, 10.9]}, "faces": { - "north": {"uv": [8, 8, 12, 12], "texture": "#1"}, - "east": {"uv": [4, 8, 5, 12], "texture": "#1"}, - "south": {"uv": [8, 8, 12, 12], "texture": "#1"}, - "west": {"uv": [15, 8, 16, 12], "texture": "#1"}, - "up": {"uv": [12, 4, 8, 5], "texture": "#1"}, - "down": {"uv": [12, 16, 8, 15], "texture": "#1"} + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} } }, { - "from": [6, 1, 1.999], - "to": [10, 5, 1.999], + "from": [8, 7.9, 11.825], + "to": [9, 15.9, 15.825], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 7.9, 10.825]}, "faces": { - "north": {"uv": [0, 0, 4, 4], "texture": "#1"}, - "east": {"uv": [1, 5, 0, 6], "texture": "#1"}, - "south": {"uv": [0, 4, 1, 5], "texture": "#1"}, - "west": {"uv": [0, 4, 1, 5], "texture": "#1"}, - "up": {"uv": [1, 4, 0, 5], "texture": "#1"}, - "down": {"uv": [1, 4, 0, 5], "texture": "#1"} + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8.5, 6.75, 8.75, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.5, 6.75, 9.75, 7.75], "texture": "#12"} } } ], @@ -1122,8 +325,66 @@ "rotation": [30, -135, 0], "scale": [0.625, 0.625, 0.625] }, + "head": { + "translation": [0, 15, 0] + }, "fixed": { "scale": [0.5, 0.5, 0.5] } - } + }, + "groups": [ + { + "name": "allcube", + "origin": [0, 0, 0], + "color": 0, + "children": [ + { + "name": "baseBlock", + "origin": [0, 0, 0], + "color": 0, + "children": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + { + "name": "back", + "origin": [7, 0.025, -2], + "color": 0, + "children": [20] + }, + 21 + ] + }, + { + "name": "connect", + "origin": [5, 10, 14], + "color": 0, + "children": [22] + }, + { + "name": "copper_past", + "origin": [3, 9, -2], + "color": 0, + "children": [23, 24, 25, 26, 27, 28] + } + ] + } + ] } \ No newline at end of file diff --git a/src/main/resources/assets/useless_mod/models/block/advanced_alloy_furnace_locked.json b/src/main/resources/assets/useless_mod/models/block/advanced_alloy_furnace_locked.json new file mode 100644 index 00000000..758e5c33 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/advanced_alloy_furnace_locked.json @@ -0,0 +1,390 @@ +{ + "format_version": "1.21.6", + "credit": "Made with blockbench,made by xiabi418", + "render_type": "translucent", + "texture_size": [64, 64], + "textures": { + "1": "useless_mod:block/advanced_alloy_furnace_locked", + "12": "useless_mod:block/advanced_alloy_furnace_locked", + "particle": "useless_mod:block/advanced_alloy_furnace" + }, + "elements": [ + { + "from": [3, 0, 0], + "to": [13, 1, 1], + "faces": { + "north": {"uv": [0.75, 3.75, 3.25, 4], "texture": "#12"}, + "up": {"uv": [8, 1.25, 10.5, 1.5], "texture": "#12"}, + "down": {"uv": [1, 11.75, 3, 12], "texture": "#12"} + } + }, + { + "from": [13, 0, 0], + "to": [16, 1, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [3, 0, 0]}, + "faces": { + "north": {"uv": [0, 3.75, 0.75, 4], "texture": "#12"}, + "east": {"uv": [0, 7.75, 0.25, 8], "texture": "#12"}, + "down": {"uv": [0, 11.75, 1, 12], "texture": "#12"} + } + }, + { + "from": [0, 0, 0], + "to": [3, 1, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [-10, 0, 0]}, + "faces": { + "north": {"uv": [3.25, 3.75, 4, 4], "texture": "#12"}, + "west": {"uv": [0, 7.75, 0.25, 8], "texture": "#12"}, + "down": {"uv": [3, 11.75, 4, 12], "texture": "#12"} + } + }, + { + "from": [3, 14, 0], + "to": [13, 16, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [5, 14, 0]}, + "faces": { + "north": {"uv": [0.75, 0, 3.25, 0.5], "texture": "#12"}, + "up": {"uv": [4.75, 4, 7.25, 4.25], "texture": "#12"}, + "down": {"uv": [7, 10.25, 9.5, 10.5], "texture": "#12"} + } + }, + { + "from": [0, 1, 0], + "to": [3, 5, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 1, 0]}, + "faces": { + "north": {"uv": [3.25, 2.75, 4, 3.75], "texture": "#12"}, + "east": {"uv": [10.5, 0, 10.75, 1], "texture": "#12"}, + "west": {"uv": [0, 6.75, 0.25, 7.75], "texture": "#12"} + } + }, + { + "from": [13, 1, 0], + "to": [16, 5, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [14, 1, 0]}, + "faces": { + "north": {"uv": [0, 2.75, 0.75, 3.75], "texture": "#12"}, + "east": {"uv": [0, 6.75, 0.25, 7.75], "texture": "#12"}, + "west": {"uv": [10.5, 0, 10.75, 1], "texture": "#12"} + } + }, + { + "from": [13, 8, 0], + "to": [16, 14, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [14, 8, 0]}, + "faces": { + "north": {"uv": [0, 0.5, 0.75, 2], "texture": "#12"}, + "east": {"uv": [0, 4.5, 0.25, 6], "texture": "#12"}, + "west": {"uv": [9.5, 8.75, 9.75, 10.25], "texture": "#12"} + } + }, + { + "from": [0, 8, 0], + "to": [3, 14, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [1, 8, 0]}, + "faces": { + "north": {"uv": [3.25, 0.5, 4, 2], "texture": "#12"}, + "east": {"uv": [9.5, 8.75, 9.75, 10.25], "texture": "#12"}, + "west": {"uv": [0, 4.5, 0.25, 6], "texture": "#12"} + } + }, + { + "from": [3, 5, 0], + "to": [13, 8, 1], + "faces": { + "north": {"uv": [0.75, 2, 3.25, 2.75], "texture": "#12"}, + "up": {"uv": [7, 10.5, 9.5, 10.75], "texture": "#12"}, + "down": {"uv": [8, 1.75, 10.5, 1.5], "texture": "#12"} + } + }, + { + "from": [0, 5, 0], + "to": [3, 8, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 5, 0]}, + "faces": { + "north": {"uv": [3.25, 2, 4, 2.75], "texture": "#12"}, + "west": {"uv": [0, 6, 0.25, 6.75], "texture": "#12"} + } + }, + { + "from": [13, 5, 0], + "to": [16, 8, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [13, 5, 0]}, + "faces": { + "north": {"uv": [0, 2, 0.75, 2.75], "texture": "#12"}, + "east": {"uv": [0, 6, 0.25, 6.75], "texture": "#12"} + } + }, + { + "from": [0, 14, 0], + "to": [3, 16, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 14, 0]}, + "faces": { + "north": {"uv": [0, 0, 0.75, 0.5], "texture": "#12"}, + "west": {"uv": [0, 4, 0.25, 4.5], "texture": "#12"}, + "up": {"uv": [7.25, 4, 8, 4.25], "texture": "#12"} + } + }, + { + "from": [13, 14, 0], + "to": [16, 16, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [13, 14, 0]}, + "faces": { + "north": {"uv": [0, 0, 0.75, 0.5], "texture": "#12"}, + "east": {"uv": [0, 4, 0.25, 4.5], "texture": "#12"}, + "up": {"uv": [4, 4, 4.75, 4.25], "texture": "#12"} + } + }, + { + "from": [3, 1, 1], + "to": [13, 5, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [3, 1, 1]}, + "faces": { + "north": {"uv": [8, 0, 10.5, 1], "texture": "#1"} + } + }, + { + "from": [3, 8, 1], + "to": [13, 14, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [3, 8, 1]}, + "faces": { + "north": {"uv": [7, 8.75, 9.5, 10.25], "texture": "#1"} + } + }, + { + "from": [12, 0, 1], + "to": [16, 9, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [12, 0, 1]}, + "faces": { + "east": {"uv": [4, 5.75, 0.25, 8], "texture": "#12"}, + "south": {"uv": [7, 1.75, 8, 4], "texture": "#12"}, + "down": {"uv": [3, 8, 4, 11.75], "texture": "#12"} + } + }, + { + "from": [0, 0, 1], + "to": [4, 9, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 0, 1]}, + "faces": { + "south": {"uv": [4, 1.75, 5, 4], "texture": "#12"}, + "west": {"uv": [0.25, 5.75, 4, 8], "texture": "#12"}, + "down": {"uv": [0, 8, 1, 11.75], "texture": "#12"} + } + }, + { + "from": [0, 9, 1], + "to": [3, 16, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 9, 1]}, + "faces": { + "east": {"uv": [16, 3.75, 12.25, 5.75], "texture": "#12"}, + "south": {"uv": [4, 0, 4.75, 1.75], "texture": "#12"}, + "west": {"uv": [0.25, 4, 4, 5.75], "texture": "#12"}, + "up": {"uv": [4, 4.25, 4.75, 8], "texture": "#12"} + } + }, + { + "from": [13, 9, 1], + "to": [16, 16, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [13, 9, 1]}, + "faces": { + "east": {"uv": [4, 4, 0.25, 5.75], "texture": "#12"}, + "south": {"uv": [7.25, 0, 8, 1.75], "texture": "#12"}, + "west": {"uv": [12.25, 3.75, 16, 5.75], "texture": "#12"}, + "up": {"uv": [7.25, 4.25, 8, 8], "texture": "#12"} + } + }, + { + "from": [4, 0, 1], + "to": [12, 4, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [4, 0, 1]}, + "faces": { + "south": {"uv": [5, 3, 7, 4], "texture": "#12"}, + "down": {"uv": [1, 8, 3, 11.75], "texture": "#12"} + } + }, + { + "from": [3, 9.05, 13.05], + "to": [13, 16.05, 16.05], + "rotation": {"angle": 0, "axis": "y", "origin": [3, 14.05, 13.05]}, + "faces": { + "south": {"uv": [4, 8.75, 6.5, 10.5], "texture": "#12"}, + "up": {"uv": [4, 8, 6.5, 8.75], "texture": "#12"} + } + }, + { + "from": [3, 12, 1], + "to": [13, 16, 13], + "rotation": {"angle": 0, "axis": "y", "origin": [3, 14, 1]}, + "faces": { + "south": {"uv": [8.75, 3.75, 11.25, 4.75], "texture": "#12"}, + "up": {"uv": [4.75, 4.25, 7.25, 7.25], "texture": "#12"} + } + }, + { + "from": [4, 4, 12], + "to": [12, 12, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [5, 9, 14]}, + "faces": { + "east": {"uv": [8, 1.75, 9, 3.75], "texture": "#12"}, + "south": {"uv": [5, 1, 7, 3], "texture": "#12"}, + "west": {"uv": [8, 1.75, 9, 3.75], "texture": "#12"}, + "up": {"uv": [8, 2.75, 10, 3.75], "texture": "#12"} + } + }, + { + "from": [5, 7.9, 11.875], + "to": [6, 15.9, 15.875], + "rotation": {"angle": 0, "axis": "y", "origin": [5, 7.9, 10.875]}, + "faces": { + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} + } + }, + { + "from": [7, 7.9, 11.875], + "to": [8, 15.9, 15.875], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 7.9, 10.875]}, + "faces": { + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} + } + }, + { + "from": [10, 7.9, 11.825], + "to": [11, 15.9, 15.825], + "rotation": {"angle": 0, "axis": "y", "origin": [9, 7.9, 10.825]}, + "faces": { + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} + } + }, + { + "from": [12, 7.9, 11.9], + "to": [13, 15.9, 15.9], + "rotation": {"angle": 0, "axis": "y", "origin": [11, 7.9, 10.9]}, + "faces": { + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} + } + }, + { + "from": [3, 7.9, 11.9], + "to": [4, 15.9, 15.9], + "rotation": {"angle": 0, "axis": "y", "origin": [2, 7.9, 10.9]}, + "faces": { + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} + } + }, + { + "from": [8, 7.9, 11.825], + "to": [9, 15.9, 15.825], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 7.9, 10.825]}, + "faces": { + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8.5, 6.75, 8.75, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.5, 6.75, 9.75, 7.75], "texture": "#12"} + } + } + ], + "display": { + "thirdperson_righthand": { + "rotation": [75, 45, 0], + "translation": [0, 2.5, 0], + "scale": [0.375, 0.375, 0.375] + }, + "thirdperson_lefthand": { + "rotation": [75, 45, 0], + "translation": [0, 2.5, 0], + "scale": [0.375, 0.375, 0.375] + }, + "firstperson_righthand": { + "rotation": [0, 45, 0], + "scale": [0.4, 0.4, 0.4] + }, + "firstperson_lefthand": { + "rotation": [0, -135, 0], + "scale": [0.4, 0.4, 0.4] + }, + "ground": { + "translation": [0, 3, 0], + "scale": [0.25, 0.25, 0.25] + }, + "gui": { + "rotation": [30, -135, 0], + "scale": [0.625, 0.625, 0.625] + }, + "head": { + "translation": [0, 15, 0] + }, + "fixed": { + "scale": [0.5, 0.5, 0.5] + } + }, + "groups": [ + { + "name": "allcube", + "origin": [0, 0, 0], + "color": 0, + "children": [ + { + "name": "baseBlock", + "origin": [0, 0, 0], + "color": 0, + "children": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + { + "name": "back", + "origin": [7, 0.025, -2], + "color": 0, + "children": [20] + }, + 21 + ] + }, + { + "name": "connect", + "origin": [5, 10, 14], + "color": 0, + "children": [22] + }, + { + "name": "copper_past", + "origin": [3, 9, -2], + "color": 0, + "children": [23, 24, 25, 26, 27, 28] + } + ] + } + ] +} \ No newline at end of file diff --git a/src/main/resources/assets/useless_mod/models/block/me_pattern_assembly.json b/src/main/resources/assets/useless_mod/models/block/me_pattern_assembly.json new file mode 100644 index 00000000..b28696cf --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/me_pattern_assembly.json @@ -0,0 +1,7 @@ +{ + "parent": "useless_mod:block/multiblock_furnace_overlay", + "textures": { + "base": "useless_mod:block/multiblock_alloy_furnace/furnace_casing", + "overlay": "useless_mod:block/multiblock_alloy_furnace/pattern_assembly_overlay" + } +} diff --git a/src/main/resources/assets/useless_mod/models/block/multiblock_alloy_furnace_core_base.json b/src/main/resources/assets/useless_mod/models/block/multiblock_alloy_furnace_core_base.json new file mode 100644 index 00000000..4e8d8196 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/multiblock_alloy_furnace_core_base.json @@ -0,0 +1,7 @@ +{ + "parent": "useless_mod:block/multiblock_furnace_overlay", + "textures": { + "base": "useless_mod:block/multiblock_alloy_furnace/furnace_casing", + "overlay": "useless_mod:block/multiblock_alloy_furnace/core_base_overlay" + } +} diff --git a/src/main/resources/assets/useless_mod/models/block/multiblock_alloy_furnace_core_idle.json b/src/main/resources/assets/useless_mod/models/block/multiblock_alloy_furnace_core_idle.json new file mode 100644 index 00000000..1dbdf6b6 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/multiblock_alloy_furnace_core_idle.json @@ -0,0 +1,7 @@ +{ + "parent": "useless_mod:block/multiblock_furnace_overlay", + "textures": { + "base": "useless_mod:block/multiblock_alloy_furnace/furnace_casing", + "overlay": "useless_mod:block/multiblock_alloy_furnace/controller_idle_overlay" + } +} diff --git a/src/main/resources/assets/useless_mod/models/block/multiblock_alloy_furnace_core_run.json b/src/main/resources/assets/useless_mod/models/block/multiblock_alloy_furnace_core_run.json new file mode 100644 index 00000000..a9196147 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/multiblock_alloy_furnace_core_run.json @@ -0,0 +1,7 @@ +{ + "parent": "useless_mod:block/multiblock_furnace_overlay", + "textures": { + "base": "useless_mod:block/multiblock_alloy_furnace/furnace_casing", + "overlay": "useless_mod:block/multiblock_alloy_furnace/controller_run_overlay" + } +} diff --git a/src/main/resources/assets/useless_mod/models/block/multiblock_alloy_furnace_core_wait.json b/src/main/resources/assets/useless_mod/models/block/multiblock_alloy_furnace_core_wait.json new file mode 100644 index 00000000..1f4e6d92 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/multiblock_alloy_furnace_core_wait.json @@ -0,0 +1,7 @@ +{ + "parent": "useless_mod:block/multiblock_furnace_overlay", + "textures": { + "base": "useless_mod:block/multiblock_alloy_furnace/furnace_casing", + "overlay": "useless_mod:block/multiblock_alloy_furnace/controller_wait_overlay" + } +} diff --git a/src/main/resources/assets/useless_mod/models/block/multiblock_coil_active.json b/src/main/resources/assets/useless_mod/models/block/multiblock_coil_active.json new file mode 100644 index 00000000..d9bb795d --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/multiblock_coil_active.json @@ -0,0 +1,38 @@ +{ + "parent": "minecraft:block/block", + "render_type": "cutout", + "textures": { + "particle": "#base" + }, + "elements": [ + { + "from": [0, 0, 0], + "to": [16, 16, 16], + "faces": { + "down": { "texture": "#base", "cullface": "down" }, + "up": { "texture": "#base", "cullface": "up" }, + "north": { "texture": "#base", "cullface": "north" }, + "south": { "texture": "#base", "cullface": "south" }, + "west": { "texture": "#base", "cullface": "west" }, + "east": { "texture": "#base", "cullface": "east" } + } + }, + { + "from": [0, 0, 0], + "to": [16, 16, 16], + "shade": false, + "neoforge_data": { + "block_light": 15, + "sky_light": 15 + }, + "faces": { + "down": { "texture": "#active", "cullface": "down" }, + "up": { "texture": "#active", "cullface": "up" }, + "north": { "texture": "#active", "cullface": "north" }, + "south": { "texture": "#active", "cullface": "south" }, + "west": { "texture": "#active", "cullface": "west" }, + "east": { "texture": "#active", "cullface": "east" } + } + } + ] +} diff --git a/src/main/resources/assets/useless_mod/models/block/multiblock_furnace_overlay.json b/src/main/resources/assets/useless_mod/models/block/multiblock_furnace_overlay.json new file mode 100644 index 00000000..58ba523b --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/multiblock_furnace_overlay.json @@ -0,0 +1,29 @@ +{ + "parent": "minecraft:block/block", + "render_type": "cutout", + "textures": { + "particle": "#base" + }, + "elements": [ + { + "from": [0, 0, 0], + "to": [16, 16, 16], + "faces": { + "down": { "texture": "#base", "cullface": "down" }, + "up": { "texture": "#base", "cullface": "up" }, + "north": { "texture": "#base", "cullface": "north" }, + "south": { "texture": "#base", "cullface": "south" }, + "west": { "texture": "#base", "cullface": "west" }, + "east": { "texture": "#base", "cullface": "east" } + } + }, + { + "from": [0, 0, -0.01], + "to": [16, 16, 0], + "shade": false, + "faces": { + "north": { "uv": [0, 0, 16, 16], "texture": "#overlay", "cullface": "north" } + } + } + ] +} diff --git a/src/main/resources/assets/useless_mod/models/block/omniversal_furnace_casing.json b/src/main/resources/assets/useless_mod/models/block/omniversal_furnace_casing.json new file mode 100644 index 00000000..71400789 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/omniversal_furnace_casing.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "useless_mod:block/multiblock_alloy_furnace/furnace_casing" + } +} diff --git a/src/main/resources/assets/useless_mod/models/block/omniversal_mold_hub.json b/src/main/resources/assets/useless_mod/models/block/omniversal_mold_hub.json new file mode 100644 index 00000000..1c8d5f05 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/omniversal_mold_hub.json @@ -0,0 +1,7 @@ +{ + "parent": "useless_mod:block/multiblock_furnace_overlay", + "textures": { + "base": "useless_mod:block/multiblock_alloy_furnace/furnace_casing", + "overlay": "useless_mod:block/multiblock_alloy_furnace/mold_hub_overlay" + } +} diff --git a/src/main/resources/assets/useless_mod/models/block/passive_crafting_hatch.json b/src/main/resources/assets/useless_mod/models/block/passive_crafting_hatch.json new file mode 100644 index 00000000..32e191e8 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/passive_crafting_hatch.json @@ -0,0 +1,7 @@ +{ + "parent": "useless_mod:block/multiblock_furnace_overlay", + "textures": { + "base": "useless_mod:block/multiblock_alloy_furnace/furnace_casing", + "overlay": "useless_mod:block/multiblock_alloy_furnace/passive_crafting_hatch_overlay" + } +} diff --git a/src/main/resources/assets/useless_mod/models/block/supervisor.json b/src/main/resources/assets/useless_mod/models/block/supervisor.json new file mode 100644 index 00000000..68695d7f --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/supervisor.json @@ -0,0 +1,2316 @@ +{ + "format_version": "1.21.11", + "credit": "Made By xiabi with Blockbench", + "loader": "useless_mod:supervisor", + "render_type": "cutout", + "gui_light": "front", + "textures": { + "2": "useless_mod:block/iava_user", + "3": "useless_mod:block/pluto", + "particle": "useless_mod:block/iava_user" + }, + "display": { + "thirdperson_righthand": { + "rotation": [ + 75, + 45, + 0 + ], + "translation": [ + 0, + 2.5, + 0 + ], + "scale": [ + 0.375, + 0.375, + 0.375 + ] + }, + "thirdperson_lefthand": { + "rotation": [ + 75, + 45, + 0 + ], + "translation": [ + 0, + 2.5, + 0 + ], + "scale": [ + 0.375, + 0.375, + 0.375 + ] + }, + "firstperson_righthand": { + "rotation": [ + 0, + 45, + 0 + ], + "translation": [ + 0, + 4.25, + 0 + ], + "scale": [ + 0.4, + 0.4, + 0.4 + ] + }, + "firstperson_lefthand": { + "rotation": [ + 0, + -135, + 0 + ], + "translation": [ + 0, + 3.75, + 0 + ], + "scale": [ + 0.4, + 0.4, + 0.4 + ] + }, + "ground": { + "translation": [ + 0, + 3, + 0 + ], + "scale": [ + 0.25, + 0.25, + 0.25 + ] + }, + "gui": { + "rotation": [ + 30, + -135, + 0 + ], + "scale": [ + 0.625, + 0.625, + 0.625 + ] + }, + "head": { + "translation": [ + 0, + 14.75, + 0 + ] + }, + "fixed": { + "translation": [ + 0, + -1, + -1.75 + ] + }, + "on_shelf": { + "rotation": [ + 180, + 0, + 180 + ] + } + }, + "supervisor_model": { + "format_version": "1.21.11", + "credit": "Made By xiabi with Blockbench", + "texture_size": [ + 64, + 64 + ], + "textures": { + "2": "iava_user", + "3": "pluto", + "particle": "iava_user" + }, + "elements": [ + { + "name": "Head", + "from": [ + 6, + 13.25, + 7 + ], + "to": [ + 10, + 17.25, + 11 + ], + "faces": { + "north": { + "uv": [ + 2, + 2, + 4, + 4 + ], + "texture": "#3" + }, + "east": { + "uv": [ + 0, + 2, + 2, + 4 + ], + "texture": "#3" + }, + "south": { + "uv": [ + 6, + 2, + 8, + 4 + ], + "texture": "#3" + }, + "west": { + "uv": [ + 4, + 2, + 6, + 4 + ], + "texture": "#3" + }, + "up": { + "uv": [ + 4, + 2, + 2, + 0 + ], + "texture": "#3" + }, + "down": { + "uv": [ + 6, + 0, + 4, + 2 + ], + "texture": "#3" + } + } + }, + { + "name": "Hat Layer", + "from": [ + 5.75, + 13, + 6.75 + ], + "to": [ + 10.25, + 17.5, + 11.25 + ], + "faces": { + "north": { + "uv": [ + 10, + 2, + 12, + 4 + ], + "texture": "#3" + }, + "east": { + "uv": [ + 8, + 2, + 10, + 4 + ], + "texture": "#3" + }, + "south": { + "uv": [ + 14, + 2, + 16, + 4 + ], + "texture": "#3" + }, + "west": { + "uv": [ + 12, + 2, + 14, + 4 + ], + "texture": "#3" + }, + "up": { + "uv": [ + 12, + 2, + 10, + 0 + ], + "texture": "#3" + }, + "down": { + "uv": [ + 14, + 0, + 12, + 2 + ], + "texture": "#3" + } + } + }, + { + "name": "Body", + "from": [ + 6, + 7.25, + 8 + ], + "to": [ + 10, + 13.25, + 10 + ], + "rotation": { + "angle": 0, + "axis": "y", + "origin": [ + 10, + -4.75, + 10 + ] + }, + "faces": { + "north": { + "uv": [ + 5, + 5, + 7, + 8 + ], + "texture": "#3" + }, + "east": { + "uv": [ + 4, + 5, + 5, + 8 + ], + "texture": "#3" + }, + "south": { + "uv": [ + 8, + 5, + 10, + 8 + ], + "texture": "#3" + }, + "west": { + "uv": [ + 7, + 5, + 8, + 8 + ], + "texture": "#3" + }, + "up": { + "uv": [ + 7, + 5, + 5, + 4 + ], + "texture": "#3" + }, + "down": { + "uv": [ + 9, + 4, + 7, + 5 + ], + "texture": "#3" + } + } + }, + { + "name": "Body Layer", + "from": [ + 5.875, + 7.125, + 7.875 + ], + "to": [ + 10.125, + 13.375, + 10.125 + ], + "rotation": { + "angle": 0, + "axis": "y", + "origin": [ + 10, + -4.75, + 10 + ] + }, + "faces": { + "north": { + "uv": [ + 5, + 9, + 7, + 12 + ], + "texture": "#3" + }, + "east": { + "uv": [ + 4, + 9, + 5, + 12 + ], + "texture": "#3" + }, + "south": { + "uv": [ + 8, + 9, + 10, + 12 + ], + "texture": "#3" + }, + "west": { + "uv": [ + 7, + 9, + 8, + 12 + ], + "texture": "#3" + }, + "up": { + "uv": [ + 7, + 9, + 5, + 8 + ], + "texture": "#3" + }, + "down": { + "uv": [ + 9, + 8, + 7, + 9 + ], + "texture": "#3" + } + } + }, + { + "name": "Right Arm", + "from": [ + 12.80853, + 23.48743, + 5.75 + ], + "to": [ + 14.30853, + 29.48743, + 7.75 + ], + "rotation": { + "x": -180, + "y": 0, + "z": -20, + "origin": [ + 14.13494, + 20.36406, + 7.875 + ] + }, + "faces": { + "north": { + "uv": [ + 11, + 5, + 11.75, + 8 + ], + "texture": "#3" + }, + "east": { + "uv": [ + 10, + 5, + 11, + 8 + ], + "texture": "#3" + }, + "south": { + "uv": [ + 12.75, + 5, + 13.5, + 8 + ], + "texture": "#3" + }, + "west": { + "uv": [ + 11.75, + 5, + 12.75, + 8 + ], + "texture": "#3" + }, + "up": { + "uv": [ + 11.75, + 5, + 11, + 4 + ], + "texture": "#3" + }, + "down": { + "uv": [ + 12.5, + 4, + 11.75, + 5 + ], + "texture": "#3" + } + } + }, + { + "name": "Right Arm Layer", + "from": [ + 12.59803, + 23.37751, + 5.625 + ], + "to": [ + 14.34803, + 29.62751, + 7.875 + ], + "rotation": { + "x": -180, + "y": 0, + "z": -20, + "origin": [ + 14.13494, + 20.36406, + 7.875 + ] + }, + "faces": { + "north": { + "uv": [ + 11, + 9, + 11.75, + 12 + ], + "texture": "#3" + }, + "east": { + "uv": [ + 10, + 9, + 11, + 12 + ], + "texture": "#3" + }, + "south": { + "uv": [ + 12.75, + 9, + 13.5, + 12 + ], + "texture": "#3" + }, + "west": { + "uv": [ + 11.75, + 9, + 12.75, + 12 + ], + "texture": "#3" + }, + "up": { + "uv": [ + 11.75, + 9, + 11, + 8 + ], + "texture": "#3" + }, + "down": { + "uv": [ + 12.5, + 8, + 11.75, + 9 + ], + "texture": "#3" + } + } + }, + { + "name": "Left Arm", + "from": [ + 4.5, + 10.27584, + 0.54196 + ], + "to": [ + 6, + 16.27584, + 2.54196 + ], + "rotation": { + "angle": 30, + "axis": "x", + "origin": [ + 0, + 0, + 0 + ] + }, + "faces": { + "north": { + "uv": [ + 9, + 13, + 9.75, + 16 + ], + "texture": "#3" + }, + "east": { + "uv": [ + 8, + 13, + 9, + 16 + ], + "texture": "#3" + }, + "south": { + "uv": [ + 10.75, + 13, + 11.5, + 16 + ], + "texture": "#3" + }, + "west": { + "uv": [ + 9.75, + 13, + 10.75, + 16 + ], + "texture": "#3" + }, + "up": { + "uv": [ + 9.75, + 13, + 9, + 12 + ], + "texture": "#3" + }, + "down": { + "uv": [ + 10.5, + 12, + 9.75, + 13 + ], + "texture": "#3" + } + } + }, + { + "name": "Left Arm Layer", + "from": [ + 4.375, + 10.15084, + 0.41696 + ], + "to": [ + 6.125, + 16.40084, + 2.66696 + ], + "rotation": { + "angle": 30, + "axis": "x", + "origin": [ + 0, + 0, + 0 + ] + }, + "faces": { + "north": { + "uv": [ + 13, + 13, + 13.75, + 16 + ], + "texture": "#3" + }, + "east": { + "uv": [ + 12, + 13, + 13, + 16 + ], + "texture": "#3" + }, + "south": { + "uv": [ + 14.75, + 13, + 15.5, + 16 + ], + "texture": "#3" + }, + "west": { + "uv": [ + 13.75, + 13, + 14.75, + 16 + ], + "texture": "#3" + }, + "up": { + "uv": [ + 13.75, + 13, + 13, + 12 + ], + "texture": "#3" + }, + "down": { + "uv": [ + 14.5, + 12, + 13.75, + 13 + ], + "texture": "#3" + } + } + }, + { + "name": "Right Leg", + "from": [ + 10.9052, + -4.89142, + -8.2155 + ], + "to": [ + 12.9052, + 1.10858, + -6.2155 + ], + "rotation": { + "x": 80, + "y": -45, + "z": 0, + "origin": [ + 0, + 0, + 0 + ] + }, + "faces": { + "north": { + "uv": [ + 1, + 5, + 2, + 8 + ], + "texture": "#3" + }, + "east": { + "uv": [ + 0, + 5, + 1, + 8 + ], + "texture": "#3" + }, + "south": { + "uv": [ + 3, + 5, + 4, + 8 + ], + "texture": "#3" + }, + "west": { + "uv": [ + 2, + 5, + 3, + 8 + ], + "texture": "#3" + }, + "up": { + "uv": [ + 2, + 5, + 1, + 4 + ], + "texture": "#3" + }, + "down": { + "uv": [ + 3, + 4, + 2, + 5 + ], + "texture": "#3" + } + } + }, + { + "name": "Right Leg Layer", + "from": [ + 10.7802, + -5.01642, + -8.3405 + ], + "to": [ + 13.0302, + 1.23358, + -6.0905 + ], + "rotation": { + "x": 80, + "y": -45, + "z": 0, + "origin": [ + 0, + 0, + 0 + ] + }, + "faces": { + "north": { + "uv": [ + 1, + 9, + 2, + 12 + ], + "texture": "#3" + }, + "east": { + "uv": [ + 0, + 9, + 1, + 12 + ], + "texture": "#3" + }, + "south": { + "uv": [ + 3, + 9, + 4, + 12 + ], + "texture": "#3" + }, + "west": { + "uv": [ + 2, + 9, + 3, + 12 + ], + "texture": "#3" + }, + "up": { + "uv": [ + 2, + 9, + 1, + 8 + ], + "texture": "#3" + }, + "down": { + "uv": [ + 3, + 8, + 2, + 9 + ], + "texture": "#3" + } + } + }, + { + "name": "Left Leg", + "from": [ + 6.92843, + -0.9644, + 6.78293 + ], + "to": [ + 8.92843, + 5.0356, + 8.78293 + ], + "rotation": { + "x": 80, + "y": 45, + "z": 0, + "origin": [ + 6.1, + 5.25, + 10 + ] + }, + "faces": { + "north": { + "uv": [ + 5, + 13, + 6, + 16 + ], + "texture": "#3" + }, + "east": { + "uv": [ + 4, + 13, + 5, + 16 + ], + "texture": "#3" + }, + "south": { + "uv": [ + 7, + 13, + 8, + 16 + ], + "texture": "#3" + }, + "west": { + "uv": [ + 6, + 13, + 7, + 16 + ], + "texture": "#3" + }, + "up": { + "uv": [ + 6, + 13, + 5, + 12 + ], + "texture": "#3" + }, + "down": { + "uv": [ + 7, + 12, + 6, + 13 + ], + "texture": "#3" + } + } + }, + { + "name": "Left Leg Layer", + "from": [ + 6.80343, + -1.0894, + 6.65793 + ], + "to": [ + 9.05343, + 5.1606, + 8.90793 + ], + "rotation": { + "x": 80, + "y": 45, + "z": 0, + "origin": [ + 6.1, + 5.25, + 10 + ] + }, + "faces": { + "north": { + "uv": [ + 1, + 13, + 2, + 16 + ], + "texture": "#3" + }, + "east": { + "uv": [ + 0, + 13, + 1, + 16 + ], + "texture": "#3" + }, + "south": { + "uv": [ + 3, + 13, + 4, + 16 + ], + "texture": "#3" + }, + "west": { + "uv": [ + 2, + 13, + 3, + 16 + ], + "texture": "#3" + }, + "up": { + "uv": [ + 2, + 13, + 1, + 12 + ], + "texture": "#3" + }, + "down": { + "uv": [ + 3, + 12, + 2, + 13 + ], + "texture": "#3" + } + } + }, + { + "name": "Head", + "from": [ + 6, + 4, + 1 + ], + "to": [ + 10, + 8, + 5 + ], + "faces": { + "north": { + "uv": [ + 2, + 2, + 4, + 4 + ], + "texture": "#2" + }, + "east": { + "uv": [ + 0, + 2, + 2, + 4 + ], + "texture": "#2" + }, + "south": { + "uv": [ + 6, + 2, + 8, + 4 + ], + "texture": "#2" + }, + "west": { + "uv": [ + 4, + 2, + 6, + 4 + ], + "texture": "#2" + }, + "up": { + "uv": [ + 4, + 2, + 2, + 0 + ], + "texture": "#2" + }, + "down": { + "uv": [ + 6, + 0, + 4, + 2 + ], + "texture": "#2" + } + } + }, + { + "name": "Hat Layer", + "from": [ + 5.75, + 3.75, + 0.75 + ], + "to": [ + 10.25, + 8.25, + 5.25 + ], + "faces": { + "north": { + "uv": [ + 10, + 2, + 12, + 4 + ], + "texture": "#2" + }, + "east": { + "uv": [ + 8, + 2, + 10, + 4 + ], + "texture": "#2" + }, + "south": { + "uv": [ + 14, + 2, + 16, + 4 + ], + "texture": "#2" + }, + "west": { + "uv": [ + 12, + 2, + 14, + 4 + ], + "texture": "#2" + }, + "up": { + "uv": [ + 12, + 2, + 10, + 0 + ], + "texture": "#2" + }, + "down": { + "uv": [ + 14, + 0, + 12, + 2 + ], + "texture": "#2" + } + } + }, + { + "name": "Body", + "from": [ + 6, + 0, + 3 + ], + "to": [ + 10, + 6, + 5 + ], + "rotation": { + "x": -90, + "y": 0, + "z": 0, + "origin": [ + 10, + 6, + 5 + ] + }, + "faces": { + "north": { + "uv": [ + 5, + 5, + 7, + 8 + ], + "texture": "#2" + }, + "east": { + "uv": [ + 4, + 5, + 5, + 8 + ], + "texture": "#2" + }, + "south": { + "uv": [ + 8, + 5, + 10, + 8 + ], + "texture": "#2" + }, + "west": { + "uv": [ + 7, + 5, + 8, + 8 + ], + "texture": "#2" + }, + "up": { + "uv": [ + 7, + 5, + 5, + 4 + ], + "texture": "#2" + }, + "down": { + "uv": [ + 9, + 4, + 7, + 5 + ], + "texture": "#2" + } + } + }, + { + "name": "Body Layer", + "from": [ + 5.875, + -0.125, + 2.875 + ], + "to": [ + 10.125, + 6.125, + 5.125 + ], + "rotation": { + "x": -90, + "y": 0, + "z": 0, + "origin": [ + 10, + 6, + 5 + ] + }, + "faces": { + "north": { + "uv": [ + 5, + 9, + 7, + 12 + ], + "texture": "#2" + }, + "east": { + "uv": [ + 4, + 9, + 5, + 12 + ], + "texture": "#2" + }, + "south": { + "uv": [ + 8, + 9, + 10, + 12 + ], + "texture": "#2" + }, + "west": { + "uv": [ + 7, + 9, + 8, + 12 + ], + "texture": "#2" + }, + "up": { + "uv": [ + 7, + 9, + 5, + 8 + ], + "texture": "#2" + }, + "down": { + "uv": [ + 9, + 8, + 7, + 9 + ], + "texture": "#2" + } + } + }, + { + "name": "Right Arm", + "from": [ + 10, + 0, + 5 + ], + "to": [ + 11.5, + 6, + 7 + ], + "faces": { + "north": { + "uv": [ + 11, + 5, + 11.75, + 8 + ], + "texture": "#2" + }, + "east": { + "uv": [ + 10, + 5, + 11, + 8 + ], + "texture": "#2" + }, + "south": { + "uv": [ + 12.75, + 5, + 13.5, + 8 + ], + "texture": "#2" + }, + "west": { + "uv": [ + 11.75, + 5, + 12.75, + 8 + ], + "texture": "#2" + }, + "up": { + "uv": [ + 11.75, + 5, + 11, + 4 + ], + "texture": "#2" + }, + "down": { + "uv": [ + 12.5, + 4, + 11.75, + 5 + ], + "texture": "#2" + } + } + }, + { + "name": "Right Arm Layer", + "from": [ + 9.875, + -0.125, + 4.875 + ], + "to": [ + 11.625, + 6.125, + 7.125 + ], + "faces": { + "north": { + "uv": [ + 11, + 9, + 11.75, + 12 + ], + "texture": "#2" + }, + "east": { + "uv": [ + 10, + 9, + 11, + 12 + ], + "texture": "#2" + }, + "south": { + "uv": [ + 12.75, + 9, + 13.5, + 12 + ], + "texture": "#2" + }, + "west": { + "uv": [ + 11.75, + 9, + 12.75, + 12 + ], + "texture": "#2" + }, + "up": { + "uv": [ + 11.75, + 9, + 11, + 8 + ], + "texture": "#2" + }, + "down": { + "uv": [ + 12.5, + 8, + 11.75, + 9 + ], + "texture": "#2" + } + } + }, + { + "name": "Left Arm", + "from": [ + 4, + 0, + 5 + ], + "to": [ + 5.5, + 6, + 7 + ], + "rotation": { + "angle": 0, + "axis": "y", + "origin": [ + 13.5, + 18, + 0 + ] + }, + "faces": { + "north": { + "uv": [ + 9, + 13, + 9.75, + 16 + ], + "texture": "#2" + }, + "east": { + "uv": [ + 8, + 13, + 9, + 16 + ], + "texture": "#2" + }, + "south": { + "uv": [ + 10.75, + 13, + 11.5, + 16 + ], + "texture": "#2" + }, + "west": { + "uv": [ + 9.75, + 13, + 10.75, + 16 + ], + "texture": "#2" + }, + "up": { + "uv": [ + 9.75, + 13, + 9, + 12 + ], + "texture": "#2" + }, + "down": { + "uv": [ + 10.5, + 12, + 9.75, + 13 + ], + "texture": "#2" + } + } + }, + { + "name": "Left Arm Layer", + "from": [ + 3.875, + -0.125, + 4.875 + ], + "to": [ + 5.625, + 6.125, + 7.125 + ], + "rotation": { + "angle": 0, + "axis": "y", + "origin": [ + 13.5, + 18, + 0 + ] + }, + "faces": { + "north": { + "uv": [ + 13, + 13, + 13.75, + 16 + ], + "texture": "#2" + }, + "east": { + "uv": [ + 12, + 13, + 13, + 16 + ], + "texture": "#2" + }, + "south": { + "uv": [ + 14.75, + 13, + 15.5, + 16 + ], + "texture": "#2" + }, + "west": { + "uv": [ + 13.75, + 13, + 14.75, + 16 + ], + "texture": "#2" + }, + "up": { + "uv": [ + 13.75, + 13, + 13, + 12 + ], + "texture": "#2" + }, + "down": { + "uv": [ + 14.5, + 12, + 13.75, + 13 + ], + "texture": "#2" + } + } + }, + { + "name": "Right Leg", + "from": [ + 9.9, + 0, + 10 + ], + "to": [ + 11.9, + 6, + 12 + ], + "faces": { + "north": { + "uv": [ + 1, + 5, + 2, + 8 + ], + "texture": "#2" + }, + "east": { + "uv": [ + 0, + 5, + 1, + 8 + ], + "texture": "#2" + }, + "south": { + "uv": [ + 3, + 5, + 4, + 8 + ], + "texture": "#2" + }, + "west": { + "uv": [ + 2, + 5, + 3, + 8 + ], + "texture": "#2" + }, + "up": { + "uv": [ + 2, + 5, + 1, + 4 + ], + "texture": "#2" + }, + "down": { + "uv": [ + 3, + 4, + 2, + 5 + ], + "texture": "#2" + } + } + }, + { + "name": "Right Leg Layer", + "from": [ + 9.775, + -0.125, + 9.875 + ], + "to": [ + 12.025, + 6.125, + 12.125 + ], + "faces": { + "north": { + "uv": [ + 1, + 9, + 2, + 12 + ], + "texture": "#2" + }, + "east": { + "uv": [ + 0, + 9, + 1, + 12 + ], + "texture": "#2" + }, + "south": { + "uv": [ + 3, + 9, + 4, + 12 + ], + "texture": "#2" + }, + "west": { + "uv": [ + 2, + 9, + 3, + 12 + ], + "texture": "#2" + }, + "up": { + "uv": [ + 2, + 9, + 1, + 8 + ], + "texture": "#2" + }, + "down": { + "uv": [ + 3, + 8, + 2, + 9 + ], + "texture": "#2" + } + } + }, + { + "name": "Left Leg", + "from": [ + 4.1, + 0, + 10 + ], + "to": [ + 6.1, + 6, + 12 + ], + "faces": { + "north": { + "uv": [ + 5, + 13, + 6, + 16 + ], + "texture": "#2" + }, + "east": { + "uv": [ + 4, + 13, + 5, + 16 + ], + "texture": "#2" + }, + "south": { + "uv": [ + 7, + 13, + 8, + 16 + ], + "texture": "#2" + }, + "west": { + "uv": [ + 6, + 13, + 7, + 16 + ], + "texture": "#2" + }, + "up": { + "uv": [ + 6, + 13, + 5, + 12 + ], + "texture": "#2" + }, + "down": { + "uv": [ + 7, + 12, + 6, + 13 + ], + "texture": "#2" + } + } + }, + { + "name": "Left Leg Layer", + "from": [ + 3.975, + -0.125, + 9.875 + ], + "to": [ + 6.225, + 6.125, + 12.125 + ], + "faces": { + "north": { + "uv": [ + 1, + 13, + 2, + 16 + ], + "texture": "#2" + }, + "east": { + "uv": [ + 0, + 13, + 1, + 16 + ], + "texture": "#2" + }, + "south": { + "uv": [ + 3, + 13, + 4, + 16 + ], + "texture": "#2" + }, + "west": { + "uv": [ + 2, + 13, + 3, + 16 + ], + "texture": "#2" + }, + "up": { + "uv": [ + 2, + 13, + 1, + 12 + ], + "texture": "#2" + }, + "down": { + "uv": [ + 3, + 12, + 2, + 13 + ], + "texture": "#2" + } + } + } + ], + "gui_light": "front", + "display": { + "thirdperson_righthand": { + "rotation": [ + 75, + 45, + 0 + ], + "translation": [ + 0, + 2.5, + 0 + ], + "scale": [ + 0.375, + 0.375, + 0.375 + ] + }, + "thirdperson_lefthand": { + "rotation": [ + 75, + 45, + 0 + ], + "translation": [ + 0, + 2.5, + 0 + ], + "scale": [ + 0.375, + 0.375, + 0.375 + ] + }, + "firstperson_righthand": { + "rotation": [ + 0, + 45, + 0 + ], + "translation": [ + 0, + 4.25, + 0 + ], + "scale": [ + 0.4, + 0.4, + 0.4 + ] + }, + "firstperson_lefthand": { + "rotation": [ + 0, + -135, + 0 + ], + "translation": [ + 0, + 3.75, + 0 + ], + "scale": [ + 0.4, + 0.4, + 0.4 + ] + }, + "ground": { + "translation": [ + 0, + 3, + 0 + ], + "scale": [ + 0.25, + 0.25, + 0.25 + ] + }, + "gui": { + "rotation": [ + 30, + -135, + 0 + ], + "scale": [ + 0.625, + 0.625, + 0.625 + ] + }, + "head": { + "translation": [ + 0, + 14.75, + 0 + ] + }, + "fixed": { + "translation": [ + 0, + -1, + -1.75 + ] + }, + "on_shelf": { + "rotation": [ + 180, + 0, + 180 + ] + } + }, + "groups": [ + { + "name": "group", + "origin": [ + 0, + 12, + 0 + ], + "scope": 0, + "color": 0, + "children": [ + { + "name": "Waist", + "origin": [ + 0, + 12, + 0 + ], + "scope": 0, + "color": 0, + "children": [ + { + "name": "Head", + "origin": [ + 0, + 24, + 0 + ], + "rotation": [ + 6, + -5, + 0 + ], + "scope": 0, + "color": 0, + "children": [ + 0, + 1 + ] + }, + { + "name": "Body", + "origin": [ + 0, + 24, + 0 + ], + "scope": 0, + "color": 0, + "children": [ + 2, + 3 + ] + }, + { + "name": "Right Arm", + "origin": [ + 5, + 22, + 0 + ], + "rotation": [ + 10, + 0, + 0 + ], + "scope": 0, + "color": 0, + "children": [ + 4, + 5 + ] + }, + { + "name": "Left Arm", + "origin": [ + -5, + 22, + 0 + ], + "rotation": [ + -12, + 0, + 0 + ], + "scope": 0, + "color": 0, + "children": [ + 6, + 7 + ] + } + ] + }, + { + "name": "Right Leg", + "origin": [ + 1.9, + 12, + 0 + ], + "rotation": [ + -11, + 0, + 2 + ], + "scope": 0, + "color": 0, + "children": [ + 8, + 9 + ] + }, + { + "name": "Left Leg", + "origin": [ + -1.9, + 12, + 0 + ], + "rotation": [ + 10, + 0, + -2 + ], + "scope": 0, + "color": 0, + "children": [ + 10, + 11 + ] + } + ] + }, + { + "name": "group", + "origin": [ + 0, + 12, + 0 + ], + "scope": 0, + "color": 0, + "children": [ + { + "name": "Waist", + "origin": [ + 0, + 12, + 0 + ], + "scope": 0, + "color": 0, + "children": [ + { + "name": "Head", + "origin": [ + 0, + 24, + 0 + ], + "rotation": [ + 6, + -5, + 0 + ], + "scope": 0, + "color": 0, + "children": [ + 12, + 13 + ] + }, + { + "name": "Body", + "origin": [ + 0, + 24, + 0 + ], + "scope": 0, + "color": 0, + "children": [ + 14, + 15 + ] + }, + { + "name": "Right Arm", + "origin": [ + 5, + 22, + 0 + ], + "rotation": [ + 10, + 0, + 0 + ], + "scope": 0, + "color": 0, + "children": [ + 16, + 17 + ] + }, + { + "name": "Left Arm", + "origin": [ + -5, + 22, + 0 + ], + "rotation": [ + -12, + 0, + 0 + ], + "scope": 0, + "color": 0, + "children": [ + 18, + 19 + ] + } + ] + }, + { + "name": "Right Leg", + "origin": [ + 1.9, + 12, + 0 + ], + "rotation": [ + -11, + 0, + 2 + ], + "scope": 0, + "color": 0, + "children": [ + 20, + 21 + ] + }, + { + "name": "Left Leg", + "origin": [ + -1.9, + 12, + 0 + ], + "rotation": [ + 10, + 0, + -2 + ], + "scope": 0, + "color": 0, + "children": [ + 22, + 23 + ] + } + ] + } + ] + } +} diff --git a/src/main/resources/assets/useless_mod/models/block/temperature_regulator.json b/src/main/resources/assets/useless_mod/models/block/temperature_regulator.json new file mode 100644 index 00000000..bfcc81c9 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/temperature_regulator.json @@ -0,0 +1,35 @@ +{ + "parent": "block/block", + "render_type": "minecraft:cutout", + "textures": { + "particle": "useless_mod:block/temperature_regulator_frame", + "frame": "useless_mod:block/temperature_regulator_frame", + "panel": "useless_mod:block/temperature_regulator_panel" + }, + "elements": [ + { + "from": [0, 0, 0], + "to": [16, 16, 16], + "faces": { + "down": { "texture": "#frame", "cullface": "down" }, + "up": { "texture": "#frame", "cullface": "up" }, + "north": { "texture": "#frame", "cullface": "north" }, + "south": { "texture": "#frame", "cullface": "south" }, + "west": { "texture": "#frame", "cullface": "west" }, + "east": { "texture": "#frame", "cullface": "east" } + } + }, + { + "from": [0, 0, 0], + "to": [16, 16, 16], + "faces": { + "down": { "texture": "#panel", "tintindex": 0, "cullface": "down" }, + "up": { "texture": "#panel", "tintindex": 0, "cullface": "up" }, + "north": { "texture": "#panel", "tintindex": 0, "cullface": "north" }, + "south": { "texture": "#panel", "tintindex": 0, "cullface": "south" }, + "west": { "texture": "#panel", "tintindex": 0, "cullface": "west" }, + "east": { "texture": "#panel", "tintindex": 0, "cullface": "east" } + } + } + ] +} diff --git a/src/main/resources/assets/useless_mod/models/block/useful_coil.json b/src/main/resources/assets/useless_mod/models/block/useful_coil.json new file mode 100644 index 00000000..451d8372 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/useful_coil.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "useless_mod:block/multiblock_alloy_furnace/coils/useful_coil" + } +} diff --git a/src/main/resources/assets/useless_mod/models/block/useful_coil_active.json b/src/main/resources/assets/useless_mod/models/block/useful_coil_active.json new file mode 100644 index 00000000..164e36e3 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/useful_coil_active.json @@ -0,0 +1,7 @@ +{ + "parent": "useless_mod:block/multiblock_coil_active", + "textures": { + "base": "useless_mod:block/multiblock_alloy_furnace/coils/useful_coil", + "active": "useless_mod:block/multiblock_alloy_furnace/coils/useful_coil_active" + } +} diff --git a/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_1.json b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_1.json new file mode 100644 index 00000000..46d769a9 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_1.json @@ -0,0 +1,4 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { "all": "useless_mod:block/multiblock_alloy_furnace/coils/useless_coil_tier_1" } +} diff --git a/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_1_active.json b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_1_active.json new file mode 100644 index 00000000..c68343bc --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_1_active.json @@ -0,0 +1,7 @@ +{ + "parent": "useless_mod:block/multiblock_coil_active", + "textures": { + "base": "useless_mod:block/multiblock_alloy_furnace/coils/useless_coil_tier_1", + "active": "useless_mod:block/multiblock_alloy_furnace/coils/useless_coil_active" + } +} diff --git a/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_2.json b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_2.json new file mode 100644 index 00000000..f0591b50 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_2.json @@ -0,0 +1,4 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { "all": "useless_mod:block/multiblock_alloy_furnace/coils/useless_coil_tier_2" } +} diff --git a/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_2_active.json b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_2_active.json new file mode 100644 index 00000000..b147a68d --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_2_active.json @@ -0,0 +1,7 @@ +{ + "parent": "useless_mod:block/multiblock_coil_active", + "textures": { + "base": "useless_mod:block/multiblock_alloy_furnace/coils/useless_coil_tier_2", + "active": "useless_mod:block/multiblock_alloy_furnace/coils/useless_coil_active" + } +} diff --git a/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_3.json b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_3.json new file mode 100644 index 00000000..f940970d --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_3.json @@ -0,0 +1,4 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { "all": "useless_mod:block/multiblock_alloy_furnace/coils/useless_coil_tier_3" } +} diff --git a/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_3_active.json b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_3_active.json new file mode 100644 index 00000000..3dac6792 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_3_active.json @@ -0,0 +1,7 @@ +{ + "parent": "useless_mod:block/multiblock_coil_active", + "textures": { + "base": "useless_mod:block/multiblock_alloy_furnace/coils/useless_coil_tier_3", + "active": "useless_mod:block/multiblock_alloy_furnace/coils/useless_coil_active" + } +} diff --git a/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_4.json b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_4.json new file mode 100644 index 00000000..686c1c3b --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_4.json @@ -0,0 +1,4 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { "all": "useless_mod:block/multiblock_alloy_furnace/coils/useless_coil_tier_4" } +} diff --git a/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_4_active.json b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_4_active.json new file mode 100644 index 00000000..b037ca39 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_4_active.json @@ -0,0 +1,7 @@ +{ + "parent": "useless_mod:block/multiblock_coil_active", + "textures": { + "base": "useless_mod:block/multiblock_alloy_furnace/coils/useless_coil_tier_4", + "active": "useless_mod:block/multiblock_alloy_furnace/coils/useless_coil_active" + } +} diff --git a/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_5.json b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_5.json new file mode 100644 index 00000000..0c230751 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_5.json @@ -0,0 +1,4 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { "all": "useless_mod:block/multiblock_alloy_furnace/coils/useless_coil_tier_5" } +} diff --git a/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_5_active.json b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_5_active.json new file mode 100644 index 00000000..a7187bf8 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_5_active.json @@ -0,0 +1,7 @@ +{ + "parent": "useless_mod:block/multiblock_coil_active", + "textures": { + "base": "useless_mod:block/multiblock_alloy_furnace/coils/useless_coil_tier_5", + "active": "useless_mod:block/multiblock_alloy_furnace/coils/useless_coil_active" + } +} diff --git a/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_6.json b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_6.json new file mode 100644 index 00000000..0173d9fc --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_6.json @@ -0,0 +1,4 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { "all": "useless_mod:block/multiblock_alloy_furnace/coils/useless_coil_tier_6" } +} diff --git a/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_6_active.json b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_6_active.json new file mode 100644 index 00000000..7f335fda --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_6_active.json @@ -0,0 +1,7 @@ +{ + "parent": "useless_mod:block/multiblock_coil_active", + "textures": { + "base": "useless_mod:block/multiblock_alloy_furnace/coils/useless_coil_tier_6", + "active": "useless_mod:block/multiblock_alloy_furnace/coils/useless_coil_active" + } +} diff --git a/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_7.json b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_7.json new file mode 100644 index 00000000..c9ad0bac --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_7.json @@ -0,0 +1,4 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { "all": "useless_mod:block/multiblock_alloy_furnace/coils/useless_coil_tier_7" } +} diff --git a/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_7_active.json b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_7_active.json new file mode 100644 index 00000000..21399928 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_7_active.json @@ -0,0 +1,7 @@ +{ + "parent": "useless_mod:block/multiblock_coil_active", + "textures": { + "base": "useless_mod:block/multiblock_alloy_furnace/coils/useless_coil_tier_7", + "active": "useless_mod:block/multiblock_alloy_furnace/coils/useless_coil_active" + } +} diff --git a/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_8.json b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_8.json new file mode 100644 index 00000000..06cb8f55 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_8.json @@ -0,0 +1,4 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { "all": "useless_mod:block/multiblock_alloy_furnace/coils/useless_coil_tier_8" } +} diff --git a/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_8_active.json b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_8_active.json new file mode 100644 index 00000000..85d6b95a --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_8_active.json @@ -0,0 +1,7 @@ +{ + "parent": "useless_mod:block/multiblock_coil_active", + "textures": { + "base": "useless_mod:block/multiblock_alloy_furnace/coils/useless_coil_tier_8", + "active": "useless_mod:block/multiblock_alloy_furnace/coils/useless_coil_active" + } +} diff --git a/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_9.json b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_9.json new file mode 100644 index 00000000..316a69e2 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_9.json @@ -0,0 +1,4 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { "all": "useless_mod:block/multiblock_alloy_furnace/coils/useless_coil_tier_9" } +} diff --git a/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_9_active.json b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_9_active.json new file mode 100644 index 00000000..2b790046 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/useless_coil_tier_9_active.json @@ -0,0 +1,7 @@ +{ + "parent": "useless_mod:block/multiblock_coil_active", + "textures": { + "base": "useless_mod:block/multiblock_alloy_furnace/coils/useless_coil_tier_9", + "active": "useless_mod:block/multiblock_alloy_furnace/coils/useless_coil_active" + } +} diff --git a/src/main/resources/assets/useless_mod/models/block/useless_compact_c9.json b/src/main/resources/assets/useless_mod/models/block/useless_compact_c9.json new file mode 100644 index 00000000..0995daaf --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/useless_compact_c9.json @@ -0,0 +1,52 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "particle": "useless_mod:block/useless_compact_c9_front", + "front": "useless_mod:block/useless_compact_c9_front", + "back": "useless_mod:block/useless_compact_c9_back", + "left": "useless_mod:block/useless_compact_c9_left", + "right": "useless_mod:block/useless_compact_c9_right", + "top": "useless_mod:block/useless_compact_c9_top" + }, + "elements": [ + { + "from": [ + 0, + 0, + 0 + ], + "to": [ + 16, + 16, + 16 + ], + "faces": { + "north": { + "texture": "#front", + "cullface": "north" + }, + "south": { + "texture": "#back", + "cullface": "south" + }, + "east": { + "texture": "#left", + "cullface": "east" + }, + "west": { + "texture": "#right", + "cullface": "west" + }, + "up": { + "texture": "#top", + "cullface": "up", + "rotation": 180 + }, + "down": { + "texture": "#top", + "cullface": "down" + } + } + } + ] +} diff --git a/src/main/resources/assets/useless_mod/models/block/useless_compact_f9.json b/src/main/resources/assets/useless_mod/models/block/useless_compact_f9.json new file mode 100644 index 00000000..e6bfec1a --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/useless_compact_f9.json @@ -0,0 +1,52 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "particle": "useless_mod:block/useless_compact_f9_front", + "front": "useless_mod:block/useless_compact_f9_front", + "back": "useless_mod:block/useless_compact_f9_back", + "left": "useless_mod:block/useless_compact_f9_left", + "right": "useless_mod:block/useless_compact_f9_right", + "top": "useless_mod:block/useless_compact_f9_top" + }, + "elements": [ + { + "from": [ + 0, + 0, + 0 + ], + "to": [ + 16, + 16, + 16 + ], + "faces": { + "north": { + "texture": "#front", + "cullface": "north" + }, + "south": { + "texture": "#back", + "cullface": "south" + }, + "east": { + "texture": "#left", + "cullface": "east" + }, + "west": { + "texture": "#right", + "cullface": "west" + }, + "up": { + "texture": "#top", + "cullface": "up", + "rotation": 180 + }, + "down": { + "texture": "#top", + "cullface": "down" + } + } + } + ] +} diff --git a/src/main/resources/assets/useless_mod/models/block/useless_compact_l9.json b/src/main/resources/assets/useless_mod/models/block/useless_compact_l9.json new file mode 100644 index 00000000..2f565b6a --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/block/useless_compact_l9.json @@ -0,0 +1,52 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "particle": "useless_mod:block/useless_compact_l9_front", + "front": "useless_mod:block/useless_compact_l9_front", + "back": "useless_mod:block/useless_compact_l9_back", + "left": "useless_mod:block/useless_compact_l9_left", + "right": "useless_mod:block/useless_compact_l9_right", + "top": "useless_mod:block/useless_compact_l9_top" + }, + "elements": [ + { + "from": [ + 0, + 0, + 0 + ], + "to": [ + 16, + 16, + 16 + ], + "faces": { + "north": { + "texture": "#front", + "cullface": "north" + }, + "south": { + "texture": "#back", + "cullface": "south" + }, + "east": { + "texture": "#left", + "cullface": "east" + }, + "west": { + "texture": "#right", + "cullface": "west" + }, + "up": { + "texture": "#top", + "cullface": "up", + "rotation": 180 + }, + "down": { + "texture": "#top", + "cullface": "down" + } + } + } + ] +} diff --git a/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_1.json b/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_1.json index 852fbbd1..7433d79c 100644 --- a/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_1.json +++ b/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_1.json @@ -1,6 +1,7 @@ { "parent": "minecraft:block/cube_all", + "render_type": "translucent", "textures": { "all": "useless_mod:block/useless_glass_tier_1" } -} \ No newline at end of file +} diff --git a/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_2.json b/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_2.json index 3bca8f1c..f2630b56 100644 --- a/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_2.json +++ b/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_2.json @@ -1,6 +1,7 @@ { "parent": "minecraft:block/cube_all", + "render_type": "translucent", "textures": { "all": "useless_mod:block/useless_glass_tier_2" } -} \ No newline at end of file +} diff --git a/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_3.json b/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_3.json index 5561d572..fc2ac9c6 100644 --- a/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_3.json +++ b/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_3.json @@ -1,6 +1,7 @@ { "parent": "minecraft:block/cube_all", + "render_type": "translucent", "textures": { "all": "useless_mod:block/useless_glass_tier_3" } -} \ No newline at end of file +} diff --git a/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_4.json b/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_4.json index 6a5570e9..ff177110 100644 --- a/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_4.json +++ b/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_4.json @@ -1,6 +1,7 @@ { "parent": "minecraft:block/cube_all", + "render_type": "translucent", "textures": { "all": "useless_mod:block/useless_glass_tier_4" } -} \ No newline at end of file +} diff --git a/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_5.json b/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_5.json index a68c1037..b1131cfc 100644 --- a/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_5.json +++ b/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_5.json @@ -1,6 +1,7 @@ { "parent": "minecraft:block/cube_all", + "render_type": "translucent", "textures": { "all": "useless_mod:block/useless_glass_tier_5" } -} \ No newline at end of file +} diff --git a/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_6.json b/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_6.json index e665d5be..18db25c1 100644 --- a/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_6.json +++ b/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_6.json @@ -1,6 +1,7 @@ { "parent": "minecraft:block/cube_all", + "render_type": "translucent", "textures": { "all": "useless_mod:block/useless_glass_tier_6" } -} \ No newline at end of file +} diff --git a/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_7.json b/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_7.json index e55033c3..446ec8c7 100644 --- a/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_7.json +++ b/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_7.json @@ -1,6 +1,7 @@ { "parent": "minecraft:block/cube_all", + "render_type": "translucent", "textures": { "all": "useless_mod:block/useless_glass_tier_7" } -} \ No newline at end of file +} diff --git a/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_8.json b/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_8.json index 0e242222..644a468a 100644 --- a/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_8.json +++ b/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_8.json @@ -1,6 +1,7 @@ { "parent": "minecraft:block/cube_all", + "render_type": "translucent", "textures": { "all": "useless_mod:block/useless_glass_tier_8" } -} \ No newline at end of file +} diff --git a/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_9.json b/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_9.json index 9cf4a098..4e999752 100644 --- a/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_9.json +++ b/src/main/resources/assets/useless_mod/models/block/useless_glass_tier_9.json @@ -1,6 +1,7 @@ { "parent": "minecraft:block/cube_all", + "render_type": "translucent", "textures": { "all": "useless_mod:block/useless_glass_tier_9" } -} \ No newline at end of file +} diff --git a/src/main/resources/assets/useless_mod/models/item/advanced_alloy_furnace_block.json b/src/main/resources/assets/useless_mod/models/item/advanced_alloy_furnace_block.json index c999bea1..bf7413e3 100644 --- a/src/main/resources/assets/useless_mod/models/item/advanced_alloy_furnace_block.json +++ b/src/main/resources/assets/useless_mod/models/item/advanced_alloy_furnace_block.json @@ -1,1097 +1,165 @@ { - "format_version": "1.9.0", - "credit": "Made with _leng", - "render_type": "translucent", + "format_version": "1.21.6", + "credit": "Made with blockbench,made by xiabi418", + "texture_size": [64, 64], "textures": { - "0": "useless_mod:block/advanced_alloy_furnace", - "1": "useless_mod:block/advanced_alloy_furnace_flame" + "12": "useless_mod:block/advanced_alloy_furnace", + "particle": "#12" }, "elements": [ - { - "from": [12, 1, 0], - "to": [13, 6, 2], - "faces": { - "north": {"uv": [8.375, 9.75, 8.5, 10.375], "texture": "#0"}, - "east": {"uv": [3.625, 5.75, 3.875, 6.375], "texture": "#0"}, - "south": {"uv": [9.125, 9.75, 9.25, 10.375], "texture": "#0"}, - "west": {"uv": [3.75, 8.25, 4, 8.875], "texture": "#0"}, - "up": {"uv": [6.125, 4, 6, 3.75], "texture": "#0"}, - "down": {"uv": [8, 10.75, 7.875, 11], "texture": "#0"} - } - }, - { - "from": [1.001, 4.95312, 13.63033], - "to": [4.001, 7.95312, 15.63033], - "rotation": {"angle": -45, "axis": "x", "origin": [8, 4.65, 14.1]}, - "faces": { - "north": {"uv": [4.875, 9.125, 5.25, 9.5], "texture": "#0"}, - "east": {"uv": [4.125, 9.625, 4.375, 10], "texture": "#0"}, - "south": {"uv": [5.375, 9.125, 5.75, 9.5], "texture": "#0"}, - "west": {"uv": [4.5, 9.625, 4.75, 10], "texture": "#0"}, - "up": {"uv": [5.25, 9.875, 4.875, 9.625], "texture": "#0"}, - "down": {"uv": [5.75, 9.625, 5.375, 9.875], "texture": "#0"} - } - }, - { - "from": [3, 1, 14], - "to": [4, 6, 16], - "faces": { - "north": {"uv": [1.25, 10, 1.375, 10.625], "texture": "#0"}, - "east": {"uv": [1.875, 9, 2.125, 9.625], "texture": "#0"}, - "south": {"uv": [1.5, 10, 1.625, 10.625], "texture": "#0"}, - "west": {"uv": [2.25, 9, 2.5, 9.625], "texture": "#0"}, - "up": {"uv": [11.125, 0.625, 11, 0.375], "texture": "#0"}, - "down": {"uv": [11.125, 0.75, 11, 1], "texture": "#0"} - } - }, - { - "from": [1, 1, 13], - "to": [15, 6, 14], - "faces": { - "north": {"uv": [6.25, 3.75, 8, 4.375], "texture": "#0"}, - "east": {"uv": [1.75, 10, 1.875, 10.625], "texture": "#0"}, - "south": {"uv": [6.25, 4.5, 8, 5.125], "texture": "#0"}, - "west": {"uv": [2, 10, 2.125, 10.625], "texture": "#0"}, - "up": {"uv": [1.75, 8.75, 0, 8.625], "texture": "#0"}, - "down": {"uv": [10.375, 0.75, 8.625, 0.875], "texture": "#0"} - } - }, - { - "from": [1, 1, 2], - "to": [15, 6, 3], - "faces": { - "north": {"uv": [1.75, 6.625, 3.5, 7.25], "texture": "#0"}, - "east": {"uv": [5.625, 10, 5.75, 10.625], "texture": "#0"}, - "south": {"uv": [7, 2, 8.75, 2.625], "texture": "#0"}, - "west": {"uv": [6.625, 10, 6.75, 10.625], "texture": "#0"}, - "up": {"uv": [10.375, 1.125, 8.625, 1], "texture": "#0"}, - "down": {"uv": [10.375, 1.25, 8.625, 1.375], "texture": "#0"} - } - }, - { - "from": [1, 1, 14], - "to": [2, 6, 16], - "faces": { - "north": {"uv": [0.75, 10, 0.875, 10.625], "texture": "#0"}, - "east": {"uv": [4.5, 8.875, 4.75, 9.5], "texture": "#0"}, - "south": {"uv": [1, 10, 1.125, 10.625], "texture": "#0"}, - "west": {"uv": [8.875, 8.625, 9.125, 9.25], "texture": "#0"}, - "up": {"uv": [10.25, 11.125, 10.125, 10.875], "texture": "#0"}, - "down": {"uv": [11, 10.625, 10.875, 10.875], "texture": "#0"} - } - }, - { - "from": [11.999, 4.95312, 13.63033], - "to": [14.999, 7.95312, 15.63033], - "rotation": {"angle": -45, "axis": "x", "origin": [8, 4.65, 14.1]}, - "faces": { - "north": {"uv": [7.875, 9, 8.25, 9.375], "texture": "#0"}, - "east": {"uv": [0, 9.625, 0.25, 10], "texture": "#0"}, - "south": {"uv": [8.375, 9, 8.75, 9.375], "texture": "#0"}, - "west": {"uv": [0.375, 9.625, 0.625, 10], "texture": "#0"}, - "up": {"uv": [1.125, 9.875, 0.75, 9.625], "texture": "#0"}, - "down": {"uv": [1.625, 9.625, 1.25, 9.875], "texture": "#0"} - } - }, - { - "from": [14, 1, 14], - "to": [15, 6, 16], - "faces": { - "north": {"uv": [9.625, 9.875, 9.75, 10.5], "texture": "#0"}, - "east": {"uv": [8.875, 2, 9.125, 2.625], "texture": "#0"}, - "south": {"uv": [9.875, 9.875, 10, 10.5], "texture": "#0"}, - "west": {"uv": [4.125, 8.875, 4.375, 9.5], "texture": "#0"}, - "up": {"uv": [11, 7.875, 10.875, 7.625], "texture": "#0"}, - "down": {"uv": [11, 9.25, 10.875, 9.5], "texture": "#0"} - } - }, - { - "from": [12, 1, 14], - "to": [13, 6, 16], - "faces": { - "north": {"uv": [8.875, 9.875, 9, 10.5], "texture": "#0"}, - "east": {"uv": [1.125, 8.875, 1.375, 9.5], "texture": "#0"}, - "south": {"uv": [9.375, 9.875, 9.5, 10.5], "texture": "#0"}, - "west": {"uv": [1.5, 8.875, 1.75, 9.5], "texture": "#0"}, - "up": {"uv": [4.375, 11.125, 4.25, 10.875], "texture": "#0"}, - "down": {"uv": [4.625, 10.875, 4.5, 11.125], "texture": "#0"} - } - }, - { - "from": [10, 1, 14], - "to": [11, 6, 15], - "faces": { - "north": {"uv": [2.25, 10, 2.375, 10.625], "texture": "#0"}, - "east": {"uv": [3.625, 10, 3.75, 10.625], "texture": "#0"}, - "south": {"uv": [10, 3.625, 10.125, 4.25], "texture": "#0"}, - "west": {"uv": [3.875, 10, 4, 10.625], "texture": "#0"}, - "up": {"uv": [11.125, 5.625, 11, 5.5], "texture": "#0"}, - "down": {"uv": [11.125, 6.625, 11, 6.75], "texture": "#0"} - } - }, - { - "from": [5, 1, 14], - "to": [6, 6, 15], - "faces": { - "north": {"uv": [10, 4.375, 10.125, 5], "texture": "#0"}, - "east": {"uv": [4.875, 10, 5, 10.625], "texture": "#0"}, - "south": {"uv": [5.125, 10, 5.25, 10.625], "texture": "#0"}, - "west": {"uv": [5.375, 10, 5.5, 10.625], "texture": "#0"}, - "up": {"uv": [7, 11.125, 6.875, 11], "texture": "#0"}, - "down": {"uv": [7.25, 11, 7.125, 11.125], "texture": "#0"} - } - }, - { - "from": [10, 1, 1], - "to": [11, 6, 2], - "faces": { - "north": {"uv": [7.875, 10, 8, 10.625], "texture": "#0"}, - "east": {"uv": [8.125, 10, 8.25, 10.625], "texture": "#0"}, - "south": {"uv": [10, 8.625, 10.125, 9.25], "texture": "#0"}, - "west": {"uv": [0, 10.125, 0.125, 10.75], "texture": "#0"}, - "up": {"uv": [11, 11.125, 10.875, 11], "texture": "#0"}, - "down": {"uv": [11.25, 0, 11.125, 0.125], "texture": "#0"} - } - }, - { - "from": [6, 5, 1], - "to": [10, 6, 2], - "faces": { - "north": {"uv": [10, 1.75, 10.5, 1.875], "texture": "#0"}, - "east": {"uv": [11, 10.125, 11.125, 10.25], "texture": "#0"}, - "south": {"uv": [10.125, 2.75, 10.625, 2.875], "texture": "#0"}, - "west": {"uv": [11, 10.375, 11.125, 10.5], "texture": "#0"}, - "up": {"uv": [10.875, 10.25, 10.375, 10.125], "texture": "#0"}, - "down": {"uv": [10.875, 10.375, 10.375, 10.5], "texture": "#0"} - } - }, - { - "from": [6, 5, 14], - "to": [10, 6, 15], - "faces": { - "north": {"uv": [10.125, 2.75, 10.625, 2.875], "texture": "#0"}, - "east": {"uv": [11, 10.375, 11.125, 10.5], "texture": "#0"}, - "south": {"uv": [10, 1.75, 10.5, 1.875], "texture": "#0"}, - "west": {"uv": [11, 10.125, 11.125, 10.25], "texture": "#0"}, - "up": {"uv": [10.875, 10.25, 10.375, 10.125], "rotation": 180, "texture": "#0"}, - "down": {"uv": [10.875, 10.375, 10.375, 10.5], "rotation": 180, "texture": "#0"} - } - }, - { - "from": [9, 6, 1], - "to": [10, 7, 2], - "faces": { - "north": {"uv": [11.125, 1.375, 11.25, 1.5], "texture": "#0"}, - "east": {"uv": [11.125, 3.625, 11.25, 3.75], "texture": "#0"}, - "south": {"uv": [11.125, 7.625, 11.25, 7.75], "texture": "#0"}, - "west": {"uv": [7.875, 11.125, 8, 11.25], "texture": "#0"}, - "up": {"uv": [8.25, 11.25, 8.125, 11.125], "texture": "#0"}, - "down": {"uv": [8.5, 11.125, 8.375, 11.25], "texture": "#0"} - } - }, - { - "from": [5, 6, 2], - "to": [11, 7, 3], - "faces": { - "north": {"uv": [7.5, 9.75, 8.25, 9.875], "texture": "#0"}, - "east": {"uv": [5.75, 11.375, 5.875, 11.5], "texture": "#0"}, - "south": {"uv": [9.875, 2, 10.625, 2.125], "texture": "#0"}, - "west": {"uv": [11.375, 5.875, 11.5, 6], "texture": "#0"}, - "up": {"uv": [10.625, 2.375, 9.875, 2.25], "texture": "#0"}, - "down": {"uv": [10.625, 2.5, 9.875, 2.625], "texture": "#0"} - } - }, - { - "from": [4.6, 7, 2.625], - "to": [5.6, 7, 3.625], - "rotation": {"angle": -45, "axis": "y", "origin": [5.1, 7, 3.125]}, - "faces": { - "north": {"uv": [0, 15.625, 0.375, 16], "texture": "#0"}, - "east": {"uv": [0, 15.625, 0.375, 16], "texture": "#0"}, - "south": {"uv": [0, 15.625, 0.375, 16], "texture": "#0"}, - "west": {"uv": [0, 15.625, 0.375, 16], "texture": "#0"}, - "up": {"uv": [6.125, 11.5, 6, 11.375], "texture": "#0"}, - "down": {"uv": [6.375, 11.375, 6.25, 11.5], "texture": "#0"} - } - }, - { - "from": [10.275, 7, 12.5], - "to": [11.275, 7, 13.5], - "rotation": {"angle": -45, "axis": "y", "origin": [10.775, 7, 13]}, - "faces": { - "north": {"uv": [0, 15.5, 0.375, 16], "texture": "#0"}, - "east": {"uv": [0, 15.625, 0.375, 16], "texture": "#0"}, - "south": {"uv": [0, 15.375, 0.625, 16], "texture": "#0"}, - "west": {"uv": [0, 15.625, 0.375, 16], "texture": "#0"}, - "up": {"uv": [6.625, 11.5, 6.5, 11.375], "texture": "#0"}, - "down": {"uv": [11.5, 6.75, 11.375, 6.875], "texture": "#0"} - } - }, - { - "from": [4.725, 7, 12.5], - "to": [5.725, 7, 13.5], - "rotation": {"angle": 45, "axis": "y", "origin": [5.225, 7, 13]}, - "faces": { - "north": {"uv": [0, 15.625, 0.375, 16], "texture": "#0"}, - "east": {"uv": [0, 15.5, 0.5, 16], "texture": "#0"}, - "south": {"uv": [0, 15.625, 0.375, 16], "texture": "#0"}, - "west": {"uv": [0, 15.625, 0.375, 16], "texture": "#0"}, - "up": {"uv": [11.5, 7.125, 11.375, 7], "texture": "#0"}, - "down": {"uv": [11.5, 7.25, 11.375, 7.375], "texture": "#0"} - } - }, - { - "from": [10.275, 7, 2.5], - "to": [11.275, 7, 3.5], - "rotation": {"angle": 45, "axis": "y", "origin": [10.775, 7, 3]}, - "faces": { - "north": {"uv": [0, 16, 0.25, 15.75], "texture": "#0"}, - "east": {"uv": [0, 16, 0.25, 15.75], "texture": "#0"}, - "south": {"uv": [0, 15.625, 0.375, 16], "texture": "#0"}, - "west": {"uv": [0, 16, 0.375, 15.625], "texture": "#0"}, - "up": {"uv": [11.5, 7.625, 11.375, 7.5], "texture": "#0"}, - "down": {"uv": [8, 11.375, 7.875, 11.5], "texture": "#0"} - } - }, - { - "from": [6, 6, 1], - "to": [7, 7, 2], - "faces": { - "north": {"uv": [9.125, 11.125, 9.25, 11.25], "texture": "#0"}, - "east": {"uv": [11.125, 9.25, 11.25, 9.375], "texture": "#0"}, - "south": {"uv": [11.125, 10.625, 11.25, 10.75], "texture": "#0"}, - "west": {"uv": [11.125, 10.875, 11.25, 11], "texture": "#0"}, - "up": {"uv": [11.25, 11.25, 11.125, 11.125], "texture": "#0"}, - "down": {"uv": [0.125, 11.25, 0, 11.375], "texture": "#0"} - } - }, - { - "from": [6, 6, 14.001], - "to": [10, 10, 16.001], - "faces": { - "north": {"uv": [5, 8, 5.5, 8.5], "texture": "#0"}, - "east": {"uv": [10.5, 0.75, 10.75, 1.25], "texture": "#0"}, - "south": {"uv": [8.375, 2.75, 8.875, 3.25], "texture": "#0"}, - "west": {"uv": [10.5, 3, 10.75, 3.5], "texture": "#0"}, - "up": {"uv": [11, 1.625, 10.5, 1.375], "texture": "#0"}, - "down": {"uv": [11, 3.5, 10.5, 3.75], "texture": "#0"} - } - }, - { - "from": [5, 6, 13], - "to": [11, 7, 14], - "faces": { - "north": {"uv": [9.625, 1.5, 10.375, 1.625], "texture": "#0"}, - "east": {"uv": [5.5, 11.375, 5.625, 11.5], "texture": "#0"}, - "south": {"uv": [9.625, 7.625, 10.375, 7.75], "texture": "#0"}, - "west": {"uv": [11.375, 5.625, 11.5, 5.75], "texture": "#0"}, - "up": {"uv": [2.5, 9.875, 1.75, 9.75], "texture": "#0"}, - "down": {"uv": [7.375, 9.75, 6.625, 9.875], "texture": "#0"} - } - }, - { - "from": [2, 5, 15], - "to": [3, 6, 16], - "faces": { - "north": {"uv": [8.875, 11.25, 9, 11.375], "texture": "#0"}, - "east": {"uv": [9.375, 11.25, 9.5, 11.375], "texture": "#0"}, - "south": {"uv": [11.25, 9.5, 11.375, 9.625], "texture": "#0"}, - "west": {"uv": [9.625, 11.25, 9.75, 11.375], "texture": "#0"}, - "up": {"uv": [11.375, 9.875, 11.25, 9.75], "texture": "#0"}, - "down": {"uv": [10, 11.25, 9.875, 11.375], "texture": "#0"} - } - }, - { - "from": [13, 5, 15], - "to": [14, 6, 16], - "faces": { - "north": {"uv": [11.25, 10, 11.375, 10.125], "texture": "#0"}, - "east": {"uv": [10.125, 11.25, 10.25, 11.375], "texture": "#0"}, - "south": {"uv": [11.25, 10.25, 11.375, 10.375], "texture": "#0"}, - "west": {"uv": [10.375, 11.25, 10.5, 11.375], "texture": "#0"}, - "up": {"uv": [10.75, 11.375, 10.625, 11.25], "texture": "#0"}, - "down": {"uv": [11, 11.25, 10.875, 11.375], "texture": "#0"} - } - }, - { - "from": [2, 5, 0], - "to": [3, 6, 1], - "faces": { - "north": {"uv": [1.5, 11.375, 1.625, 11.5], "texture": "#0"}, - "east": {"uv": [1.75, 11.375, 1.875, 11.5], "texture": "#0"}, - "south": {"uv": [2, 11.375, 2.125, 11.5], "texture": "#0"}, - "west": {"uv": [2.25, 11.375, 2.375, 11.5], "texture": "#0"}, - "up": {"uv": [11.5, 3.25, 11.375, 3.125], "texture": "#0"}, - "down": {"uv": [11.5, 3.375, 11.375, 3.5], "texture": "#0"} - } - }, - { - "from": [13, 5, 0], - "to": [14, 6, 1], - "faces": { - "north": {"uv": [11.375, 0, 11.5, 0.125], "texture": "#0"}, - "east": {"uv": [0.75, 11.375, 0.875, 11.5], "texture": "#0"}, - "south": {"uv": [1, 11.375, 1.125, 11.5], "texture": "#0"}, - "west": {"uv": [11.375, 1, 11.5, 1.125], "texture": "#0"}, - "up": {"uv": [1.375, 11.5, 1.25, 11.375], "texture": "#0"}, - "down": {"uv": [11.5, 1.25, 11.375, 1.375], "texture": "#0"} - } - }, - { - "from": [5, 1, 1], - "to": [6, 6, 2], - "faces": { - "north": {"uv": [6.875, 10, 7, 10.625], "texture": "#0"}, - "east": {"uv": [7.125, 10, 7.25, 10.625], "texture": "#0"}, - "south": {"uv": [7.375, 10, 7.5, 10.625], "texture": "#0"}, - "west": {"uv": [7.625, 10, 7.75, 10.625], "texture": "#0"}, - "up": {"uv": [7.5, 11.125, 7.375, 11], "texture": "#0"}, - "down": {"uv": [7.75, 11, 7.625, 11.125], "texture": "#0"} - } - }, - { - "from": [1.001, 4.8, 1.8], - "to": [4.001, 7.8, 3.8], - "rotation": {"angle": 45, "axis": "x", "origin": [2.5, 6.3, 2.8]}, - "faces": { - "north": {"uv": [3.125, 9, 3.5, 9.375], "texture": "#0"}, - "east": {"uv": [3.75, 9.5, 4, 9.875], "texture": "#0"}, - "south": {"uv": [3.625, 9, 4, 9.375], "texture": "#0"}, - "west": {"uv": [9.5, 8.625, 9.75, 9], "texture": "#0"}, - "up": {"uv": [9.875, 9.375, 9.5, 9.125], "texture": "#0"}, - "down": {"uv": [9.875, 9.5, 9.5, 9.75], "texture": "#0"} - } - }, - { - "from": [3, 1, 0], - "to": [4, 6, 2], - "faces": { - "north": {"uv": [9.875, 7.875, 10, 8.5], "texture": "#0"}, - "east": {"uv": [0.375, 8.875, 0.625, 9.5], "texture": "#0"}, - "south": {"uv": [8.625, 9.875, 8.75, 10.5], "texture": "#0"}, - "west": {"uv": [0.75, 8.875, 1, 9.5], "texture": "#0"}, - "up": {"uv": [0.625, 11.125, 0.5, 10.875], "texture": "#0"}, - "down": {"uv": [4.125, 10.875, 4, 11.125], "texture": "#0"} - } - }, - { - "from": [2, 1, 1], - "to": [3, 6, 2], - "faces": { - "north": {"uv": [0.25, 10.125, 0.375, 10.75], "texture": "#0"}, - "east": {"uv": [0.5, 10.125, 0.625, 10.75], "texture": "#0"}, - "south": {"uv": [4.125, 10.125, 4.25, 10.75], "texture": "#0"}, - "west": {"uv": [4.375, 10.125, 4.5, 10.75], "texture": "#0"}, - "up": {"uv": [11.375, 6.625, 11.25, 6.5], "texture": "#0"}, - "down": {"uv": [7, 11.25, 6.875, 11.375], "texture": "#0"} - } - }, - { - "from": [13, 1, 1], - "to": [14, 6, 2], - "faces": { - "north": {"uv": [4.625, 10.125, 4.75, 10.75], "texture": "#0"}, - "east": {"uv": [10.125, 7.875, 10.25, 8.5], "texture": "#0"}, - "south": {"uv": [10.125, 9.375, 10.25, 10], "texture": "#0"}, - "west": {"uv": [10.125, 10.125, 10.25, 10.75], "texture": "#0"}, - "up": {"uv": [7.25, 11.375, 7.125, 11.25], "texture": "#0"}, - "down": {"uv": [7.5, 11.25, 7.375, 11.375], "texture": "#0"} - } - }, - { - "from": [2, 1, 14], - "to": [3, 6, 15], - "faces": { - "north": {"uv": [10.25, 3.625, 10.375, 4.25], "texture": "#0"}, - "east": {"uv": [10.25, 4.375, 10.375, 5], "texture": "#0"}, - "south": {"uv": [5.875, 10.25, 6, 10.875], "texture": "#0"}, - "west": {"uv": [6.125, 10.25, 6.25, 10.875], "texture": "#0"}, - "up": {"uv": [7.75, 11.375, 7.625, 11.25], "texture": "#0"}, - "down": {"uv": [11.375, 7.875, 11.25, 8], "texture": "#0"} - } - }, - { - "from": [13, 1, 14], - "to": [14, 6, 15], - "faces": { - "north": {"uv": [6.375, 10.25, 6.5, 10.875], "texture": "#0"}, - "east": {"uv": [10.25, 8.625, 10.375, 9.25], "texture": "#0"}, - "south": {"uv": [10.375, 7.875, 10.5, 8.5], "texture": "#0"}, - "west": {"uv": [10.375, 9.375, 10.5, 10], "texture": "#0"}, - "up": {"uv": [11.375, 8.25, 11.25, 8.125], "texture": "#0"}, - "down": {"uv": [8.75, 11.25, 8.625, 11.375], "texture": "#0"} - } - }, - { - "from": [1, 1, 0], - "to": [2, 6, 2], - "faces": { - "north": {"uv": [3.125, 9.875, 3.25, 10.5], "texture": "#0"}, - "east": {"uv": [7.5, 8.75, 7.75, 9.375], "texture": "#0"}, - "south": {"uv": [3.375, 9.875, 3.5, 10.5], "texture": "#0"}, - "west": {"uv": [0, 8.875, 0.25, 9.5], "texture": "#0"}, - "up": {"uv": [11, 0.25, 10.875, 0], "texture": "#0"}, - "down": {"uv": [0.375, 10.875, 0.25, 11.125], "texture": "#0"} - } - }, - { - "from": [0, 1, 1], - "to": [1, 5, 2], - "faces": { - "north": {"uv": [10.5, 3.875, 10.625, 4.375], "texture": "#0"}, - "east": {"uv": [10.5, 4.5, 10.625, 5], "texture": "#0"}, - "south": {"uv": [10.5, 5.125, 10.625, 5.625], "texture": "#0"}, - "west": {"uv": [10.5, 5.75, 10.625, 6.25], "texture": "#0"}, - "up": {"uv": [0.375, 11.375, 0.25, 11.25], "texture": "#0"}, - "down": {"uv": [11.375, 0.25, 11.25, 0.375], "texture": "#0"} - } - }, - { - "from": [15, 4, 12], - "to": [16, 5, 14], - "faces": { - "north": {"uv": [11.25, 5.375, 11.375, 5.5], "texture": "#0"}, - "east": {"uv": [11, 4.625, 11.25, 4.75], "texture": "#0"}, - "south": {"uv": [11.25, 6.25, 11.375, 6.375], "texture": "#0"}, - "west": {"uv": [11, 4.875, 11.25, 5], "texture": "#0"}, - "up": {"uv": [11.125, 5.375, 11, 5.125], "texture": "#0"}, - "down": {"uv": [6, 11, 5.875, 11.25], "texture": "#0"} - } - }, - { - "from": [15, 1, 14], - "to": [16, 5, 15], - "faces": { - "north": {"uv": [10.75, 1.625, 10.875, 2.125], "texture": "#0"}, - "east": {"uv": [1.75, 10.75, 1.875, 11.25], "texture": "#0"}, - "south": {"uv": [2, 10.75, 2.125, 11.25], "texture": "#0"}, - "west": {"uv": [2.25, 10.75, 2.375, 11.25], "texture": "#0"}, - "up": {"uv": [4.625, 11.375, 4.5, 11.25], "texture": "#0"}, - "down": {"uv": [11.375, 5.125, 11.25, 5.25], "texture": "#0"} - } - }, - { - "from": [15, 1, 11], - "to": [16, 5, 12], - "faces": { - "north": {"uv": [0.75, 10.75, 0.875, 11.25], "texture": "#0"}, - "east": {"uv": [1, 10.75, 1.125, 11.25], "texture": "#0"}, - "south": {"uv": [1.25, 10.75, 1.375, 11.25], "texture": "#0"}, - "west": {"uv": [1.5, 10.75, 1.625, 11.25], "texture": "#0"}, - "up": {"uv": [4.375, 11.375, 4.25, 11.25], "texture": "#0"}, - "down": {"uv": [11.375, 4.375, 11.25, 4.5], "texture": "#0"} - } - }, - { - "from": [0, 1, 6], - "to": [1, 10, 10], - "faces": { - "north": {"uv": [5.875, 9.125, 6, 10.25], "texture": "#0"}, - "east": {"uv": [0, 7.125, 0.5, 8.25], "texture": "#0"}, - "south": {"uv": [6.125, 9.125, 6.25, 10.25], "texture": "#0"}, - "west": {"uv": [7.125, 0, 7.625, 1.125], "texture": "#0"}, - "up": {"uv": [10.875, 2.75, 10.75, 2.25], "texture": "#0"}, - "down": {"uv": [10.875, 2.875, 10.75, 3.375], "texture": "#0"} - } - }, - { - "from": [15, 1, 6], - "to": [16, 10, 10], - "faces": { - "north": {"uv": [6.375, 9.125, 6.5, 10.25], "texture": "#0"}, - "east": {"uv": [0.625, 7.125, 1.125, 8.25], "texture": "#0"}, - "south": {"uv": [9.25, 8.625, 9.375, 9.75], "texture": "#0"}, - "west": {"uv": [3.625, 7.125, 4.125, 8.25], "texture": "#0"}, - "up": {"uv": [3.625, 11.25, 3.5, 10.75], "texture": "#0"}, - "down": {"uv": [3.875, 10.75, 3.75, 11.25], "texture": "#0"} - } - }, - { - "from": [14.001, 7, 6], - "to": [15.001, 10, 10], - "faces": { - "north": {"uv": [6.125, 11, 6.25, 11.25], "texture": "#0"}, - "east": {"uv": [9.25, 2, 9.75, 2.25], "texture": "#0"}, - "south": {"uv": [11, 6.25, 11.125, 6.5], "texture": "#0"}, - "west": {"uv": [9.25, 2.375, 9.75, 2.625], "texture": "#0"}, - "up": {"uv": [10.875, 4.375, 10.75, 3.875], "texture": "#0"}, - "down": {"uv": [10.875, 4.5, 10.75, 5], "texture": "#0"} - } - }, - { - "from": [15, 14, 6], - "to": [16, 15, 10], - "faces": { - "north": {"uv": [3.5, 11.375, 3.625, 11.5], "texture": "#0"}, - "east": {"uv": [10.625, 8.25, 11.125, 8.375], "texture": "#0"}, - "south": {"uv": [11.375, 3.625, 11.5, 3.75], "texture": "#0"}, - "west": {"uv": [10.625, 9.875, 11.125, 10], "texture": "#0"}, - "up": {"uv": [10.875, 5.625, 10.75, 5.125], "texture": "#0"}, - "down": {"uv": [5.5, 10.75, 5.375, 11.25], "texture": "#0"} - } - }, - { - "from": [0, 14, 6], - "to": [1, 15, 10], - "faces": { - "north": {"uv": [3.75, 11.375, 3.875, 11.5], "texture": "#0"}, - "east": {"uv": [10.75, 5.75, 11.25, 5.875], "texture": "#0"}, - "south": {"uv": [11.375, 4.625, 11.5, 4.75], "texture": "#0"}, - "west": {"uv": [10.75, 6, 11.25, 6.125], "texture": "#0"}, - "up": {"uv": [5.75, 11.25, 5.625, 10.75], "texture": "#0"}, - "down": {"uv": [10.875, 6.25, 10.75, 6.75], "texture": "#0"} - } - }, - { - "from": [6, 14, 0], - "to": [10, 15, 1], - "faces": { - "north": {"uv": [6.625, 10.75, 7.125, 10.875], "texture": "#0"}, - "east": {"uv": [4.75, 11.375, 4.875, 11.5], "texture": "#0"}, - "south": {"uv": [10.75, 6.875, 11.25, 7], "texture": "#0"}, - "west": {"uv": [11.375, 4.875, 11.5, 5], "texture": "#0"}, - "up": {"uv": [11.25, 7.25, 10.75, 7.125], "texture": "#0"}, - "down": {"uv": [7.75, 10.75, 7.25, 10.875], "texture": "#0"} - } - }, - { - "from": [6, 14, 15], - "to": [10, 15, 16], - "faces": { - "north": {"uv": [10.75, 7.375, 11.25, 7.5], "texture": "#0"}, - "east": {"uv": [5, 11.375, 5.125, 11.5], "texture": "#0"}, - "south": {"uv": [10.75, 8.5, 11.25, 8.625], "texture": "#0"}, - "west": {"uv": [5.25, 11.375, 5.375, 11.5], "texture": "#0"}, - "up": {"uv": [11.25, 8.875, 10.75, 8.75], "texture": "#0"}, - "down": {"uv": [11.25, 9, 10.75, 9.125], "texture": "#0"} - } - }, - { - "from": [1, 7, 6], - "to": [2, 10, 10], - "faces": { - "north": {"uv": [6.375, 11, 6.5, 11.25], "texture": "#0"}, - "east": {"uv": [2.625, 9.5, 3.125, 9.75], "texture": "#0"}, - "south": {"uv": [6.625, 11, 6.75, 11.25], "texture": "#0"}, - "west": {"uv": [9.5, 2.75, 10, 3], "texture": "#0"}, - "up": {"uv": [5, 11.25, 4.875, 10.75], "texture": "#0"}, - "down": {"uv": [5.25, 10.75, 5.125, 11.25], "texture": "#0"} - } - }, - { - "from": [1, 1.0001, 3], - "to": [15, 7.0001, 13], - "faces": { - "north": {"uv": [2.125, 4.875, 3.875, 5.625], "texture": "#0"}, - "east": {"uv": [7, 2.75, 8.25, 3.5], "texture": "#0"}, - "south": {"uv": [5.125, 2, 6.875, 2.75], "texture": "#0"}, - "west": {"uv": [5.75, 7, 7, 7.75], "texture": "#0"}, - "up": {"uv": [3.875, 1.25, 2.125, 0], "texture": "#0"}, - "down": {"uv": [3.875, 1.375, 2.125, 2.625], "texture": "#0"} - } - }, - { - "from": [15, 1, 4], - "to": [16, 5, 5], - "faces": { - "north": {"uv": [9.875, 10.625, 10, 11.125], "texture": "#0"}, - "east": {"uv": [10.375, 10.625, 10.5, 11.125], "texture": "#0"}, - "south": {"uv": [10.625, 10.625, 10.75, 11.125], "texture": "#0"}, - "west": {"uv": [10.75, 0.625, 10.875, 1.125], "texture": "#0"}, - "up": {"uv": [4.125, 11.375, 4, 11.25], "texture": "#0"}, - "down": {"uv": [11.375, 4.125, 11.25, 4.25], "texture": "#0"} - } - }, - { - "from": [15, 4, 2], - "to": [16, 5, 4], - "faces": { - "north": {"uv": [3.25, 11.25, 3.375, 11.375], "texture": "#0"}, - "east": {"uv": [11, 3.125, 11.25, 3.25], "texture": "#0"}, - "south": {"uv": [11.25, 3.875, 11.375, 4], "texture": "#0"}, - "west": {"uv": [11, 3.375, 11.25, 3.5], "texture": "#0"}, - "up": {"uv": [11.125, 4.125, 11, 3.875], "texture": "#0"}, - "down": {"uv": [11.125, 4.25, 11, 4.5], "texture": "#0"} - } - }, - { - "from": [15, 1, 1], - "to": [16, 5, 2], - "faces": { - "north": {"uv": [8.875, 10.625, 9, 11.125], "texture": "#0"}, - "east": {"uv": [10.625, 9.25, 10.75, 9.75], "texture": "#0"}, - "south": {"uv": [9.375, 10.625, 9.5, 11.125], "texture": "#0"}, - "west": {"uv": [9.625, 10.625, 9.75, 11.125], "texture": "#0"}, - "up": {"uv": [11.375, 3, 11.25, 2.875], "texture": "#0"}, - "down": {"uv": [3.125, 11.25, 3, 11.375], "texture": "#0"} - } - }, - { - "from": [0, 4, 2], - "to": [1, 5, 4], - "faces": { - "north": {"uv": [11.25, 2.375, 11.375, 2.5], "texture": "#0"}, - "east": {"uv": [1.25, 7.125, 1.5, 7.25], "texture": "#0"}, - "south": {"uv": [2.5, 11.25, 2.625, 11.375], "texture": "#0"}, - "west": {"uv": [10.875, 8, 11.125, 8.125], "texture": "#0"}, - "up": {"uv": [11.125, 1.875, 11, 1.625], "texture": "#0"}, - "down": {"uv": [11.125, 2, 11, 2.25], "texture": "#0"} - } - }, - { - "from": [0, 4, 12], - "to": [1, 5, 14], - "faces": { - "north": {"uv": [11.25, 2.625, 11.375, 2.75], "texture": "#0"}, - "east": {"uv": [10.875, 9.625, 11.125, 9.75], "texture": "#0"}, - "south": {"uv": [2.75, 11.25, 2.875, 11.375], "texture": "#0"}, - "west": {"uv": [11, 1.125, 11.25, 1.25], "texture": "#0"}, - "up": {"uv": [11.125, 2.625, 11, 2.375], "texture": "#0"}, - "down": {"uv": [11.125, 2.75, 11, 3], "texture": "#0"} - } - }, { "from": [0, 0, 0], - "to": [16, 1, 16], - "faces": { - "north": {"uv": [7.875, 1.75, 9.875, 1.875], "texture": "#0"}, - "east": {"uv": [8.375, 3.375, 10.375, 3.5], "texture": "#0"}, - "south": {"uv": [8.375, 5.125, 10.375, 5.25], "texture": "#0"}, - "west": {"uv": [8.375, 5.375, 10.375, 5.5], "texture": "#0"}, - "up": {"uv": [2, 2, 0, 0], "texture": "#0"}, - "down": {"uv": [2, 2.125, 0, 4.125], "texture": "#0"} - } - }, - { - "from": [0, 1, 14], - "to": [1, 5, 15], + "to": [16, 16, 16], "faces": { - "north": {"uv": [3, 10.625, 3.125, 11.125], "texture": "#0"}, - "east": {"uv": [3.25, 10.625, 3.375, 11.125], "texture": "#0"}, - "south": {"uv": [10.625, 7.625, 10.75, 8.125], "texture": "#0"}, - "west": {"uv": [8.625, 10.625, 8.75, 11.125], "texture": "#0"}, - "up": {"uv": [11.375, 2, 11.25, 1.875], "texture": "#0"}, - "down": {"uv": [11.375, 2.125, 11.25, 2.25], "texture": "#0"} + "north": {"uv": [0, 0, 4, 4], "texture": "#12"}, + "east": {"uv": [4, 4, 0, 8], "texture": "#12"}, + "south": {"uv": [4, 0, 8, 4], "texture": "#12"}, + "west": {"uv": [0, 4, 4, 8], "texture": "#12"}, + "up": {"uv": [8, 8, 4, 4], "texture": "#12"}, + "down": {"uv": [4, 8, 0, 12], "texture": "#12"} } }, { - "from": [0, 1, 11], - "to": [1, 5, 12], + "from": [3, 9, 13], + "to": [13, 16, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [3, 14, 13]}, "faces": { - "north": {"uv": [9.125, 10.5, 9.25, 11], "texture": "#0"}, - "east": {"uv": [10.625, 0, 10.75, 0.5], "texture": "#0"}, - "south": {"uv": [2.5, 10.625, 2.625, 11.125], "texture": "#0"}, - "west": {"uv": [2.75, 10.625, 2.875, 11.125], "texture": "#0"}, - "up": {"uv": [11.375, 0.875, 11.25, 0.75], "texture": "#0"}, - "down": {"uv": [11.375, 1.625, 11.25, 1.75], "texture": "#0"} + "north": {"uv": [13.5, 0, 16, 1.75], "texture": "#12"}, + "east": {"uv": [13.5, 0, 14.25, 1.75], "texture": "#12"}, + "south": {"uv": [4, 8.75, 6.5, 10.5], "texture": "#12"}, + "west": {"uv": [15.25, 0, 16, 1.75], "texture": "#12"}, + "up": {"uv": [4, 8, 6.5, 8.75], "texture": "#12"}, + "down": {"uv": [13.5, 0, 16, 0.75], "texture": "#12"} } }, { - "from": [0, 1, 4], - "to": [1, 5, 5], + "from": [4, 8, 12], + "to": [12, 12, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [5, 10, 14]}, "faces": { - "north": {"uv": [10.5, 6.375, 10.625, 6.875], "texture": "#0"}, - "east": {"uv": [10.5, 7, 10.625, 7.5], "texture": "#0"}, - "south": {"uv": [8.375, 10.5, 8.5, 11], "texture": "#0"}, - "west": {"uv": [10.5, 8.625, 10.625, 9.125], "texture": "#0"}, - "up": {"uv": [0.625, 11.375, 0.5, 11.25], "texture": "#0"}, - "down": {"uv": [11.375, 0.5, 11.25, 0.625], "texture": "#0"} + "north": {"uv": [10.75, 0, 12.75, 1], "texture": "#12"}, + "east": {"uv": [9, 1.75, 8, 2.75], "texture": "#12"}, + "south": {"uv": [10.75, 0, 12.75, 1], "texture": "#12"}, + "west": {"uv": [8, 1.75, 9, 2.75], "texture": "#12"}, + "up": {"uv": [8, 2.75, 10, 3.75], "texture": "#12"}, + "down": {"uv": [8, 2.75, 10, 3.75], "texture": "#12"} } }, { - "from": [14, 1, 0], - "to": [15, 6, 2], + "from": [2, 8, 16], + "to": [14, 16, 12], + "rotation": {"angle": 0, "axis": "y", "origin": [5, 10, 10]}, "faces": { - "north": {"uv": [2.625, 9.875, 2.75, 10.5], "texture": "#0"}, - "east": {"uv": [6.75, 8.75, 7, 9.375], "texture": "#0"}, - "south": {"uv": [2.875, 9.875, 3, 10.5], "texture": "#0"}, - "west": {"uv": [7.125, 8.75, 7.375, 9.375], "texture": "#0"}, - "up": {"uv": [8.25, 11, 8.125, 10.75], "texture": "#0"}, - "down": {"uv": [0.125, 10.875, 0, 11.125], "texture": "#0"} + "north": {"uv": [12, 3.75, 16, 5.75], "texture": "#12"}, + "east": {"uv": [15, 3.75, 16, 5.75], "texture": "#12"}, + "south": {"uv": [8.5, 3.75, 11.5, 5.75], "texture": "#12"}, + "west": {"uv": [15, 3.75, 16, 5.75], "texture": "#12"}, + "up": {"uv": [12, 3.75, 16, 4.75], "texture": "#12"}, + "down": {"uv": [8.5, 5.75, 11.5, 6.75], "texture": "#12"} } }, { - "from": [11.999, 4.8, 1.8], - "to": [14.999, 7.8, 3.8], - "rotation": {"angle": 45, "axis": "x", "origin": [13.5, 6.3, 2.8]}, + "from": [3, 8, 1], + "to": [13, 14, 0], + "rotation": {"angle": 0, "axis": "y", "origin": [3, 9, -2]}, "faces": { - "north": {"uv": [2.625, 9, 3, 9.375], "texture": "#0"}, - "east": {"uv": [1.75, 5.25, 2, 5.625], "texture": "#0"}, - "south": {"uv": [9, 2.75, 9.375, 3.125], "texture": "#0"}, - "west": {"uv": [3.625, 6.5, 3.875, 6.875], "texture": "#0"}, - "up": {"uv": [3.625, 9.75, 3.25, 9.5], "texture": "#0"}, - "down": {"uv": [9, 9.5, 8.625, 9.75], "texture": "#0"} + "north": {"uv": [7, 8.75, 9.5, 10.25], "texture": "#12"}, + "east": {"uv": [9.75, 8.75, 9.5, 10.25], "texture": "#12"}, + "south": {"uv": [7, 8.75, 9.5, 10.25], "texture": "#12"}, + "west": {"uv": [9.5, 8.75, 9.75, 10.25], "texture": "#12"}, + "up": {"uv": [7, 10.25, 9.5, 10.5], "texture": "#12"}, + "down": {"uv": [7, 10.5, 9.5, 10.75], "texture": "#12"} } }, { - "name": "Octagon", - "from": [5.51472, 7, 14], - "to": [10.48528, 13, 14], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 7.5, 8]}, + "from": [3, 0.025, 1], + "to": [13, 5.025, 0], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 0.025, -2]}, "faces": { - "north": {"uv": [7.125, 1.125, 7.75, 1.875], "texture": "#0"}, - "east": {"uv": [0, 15.25, 0.375, 16], "texture": "#0"}, - "south": {"uv": [4.25, 7.125, 4.875, 7.875], "texture": "#0"}, - "west": {"uv": [0, 15.25, 0.25, 16], "texture": "#0"}, - "up": {"uv": [0.625, 15.625, 0, 16], "texture": "#0"}, - "down": {"uv": [0.625, 15.625, 0, 16], "texture": "#0"} + "north": {"uv": [8, 0, 10.5, 1.25], "texture": "#12"}, + "east": {"uv": [10.5, 0, 10.75, 1.25], "texture": "#12"}, + "south": {"uv": [8, 0, 10.5, 1.25], "texture": "#12"}, + "west": {"uv": [10.5, 0, 10.75, 1.25], "texture": "#12"}, + "up": {"uv": [8, 1.25, 10.5, 1.5], "texture": "#12"}, + "down": {"uv": [8, 1.5, 10.5, 1.75], "texture": "#12"} } }, { - "name": "Octagon", - "from": [14, 7, 5.51472], - "to": [14, 13, 10.48528], + "from": [5, 7.9, 11.875], + "to": [6, 15.9, 15.875], + "rotation": {"angle": 0, "axis": "y", "origin": [5, 7.9, 10.875]}, "faces": { - "north": {"uv": [0, 15.25, 0.375, 16], "texture": "#0"}, - "east": {"uv": [7.25, 7.875, 7.875, 8.625], "texture": "#0"}, - "south": {"uv": [0, 15.25, 0.375, 16], "texture": "#0"}, - "west": {"uv": [4.25, 8, 4.875, 8.75], "texture": "#0"}, - "up": {"uv": [0, 16, 0.375, 15.375], "texture": "#0"}, - "down": {"uv": [0, 15.375, 0.5, 16], "texture": "#0"} + "north": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"}, + "down": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} } }, { - "name": "Octagon", - "from": [14, 7, 5.51472], - "to": [14, 13, 10.48528], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 12.5, 8]}, + "from": [7, 7.9, 11.875], + "to": [8, 15.9, 15.875], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 7.9, 10.875]}, "faces": { - "north": {"uv": [0.375, 15.25, 0, 16], "texture": "#0"}, - "east": {"uv": [6.5, 7.875, 7.125, 8.625], "texture": "#0"}, - "south": {"uv": [0, 15.25, 0.625, 16], "texture": "#0"}, - "west": {"uv": [7.875, 7, 8.5, 7.75], "texture": "#0"}, - "up": {"uv": [0, 16, 0.5, 15.375], "texture": "#0"}, - "down": {"uv": [0, 15.375, 0.375, 16], "texture": "#0"} + "north": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"}, + "down": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} } }, { - "name": "Octagon", - "from": [5.51472, 7, 2], - "to": [10.48528, 13, 2], + "from": [10, 7.9, 11.825], + "to": [11, 15.9, 15.825], + "rotation": {"angle": 0, "axis": "y", "origin": [9, 7.9, 10.825]}, "faces": { - "north": {"uv": [7.875, 0.875, 8.5, 1.625], "texture": "#0"}, - "east": {"uv": [0, 15.25, 0.625, 16], "texture": "#0"}, - "south": {"uv": [5.75, 7.875, 6.375, 8.625], "texture": "#0"}, - "west": {"uv": [0, 15.25, 0.5, 16], "texture": "#0"}, - "up": {"uv": [0.625, 15.25, 0, 16], "texture": "#0"}, - "down": {"uv": [0.625, 15.25, 0, 16], "texture": "#0"} + "north": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"}, + "down": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} } }, { - "name": "Octagon", - "from": [5.51472, 7, 2], - "to": [10.48528, 13, 2], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 12.5, 8]}, + "from": [12, 7.9, 11.9], + "to": [13, 15.9, 15.9], + "rotation": {"angle": 0, "axis": "y", "origin": [11, 7.9, 10.9]}, "faces": { - "north": {"uv": [7.625, 6.125, 8.25, 6.875], "texture": "#0"}, - "east": {"uv": [0, 15.25, 0.5, 16], "texture": "#0"}, - "south": {"uv": [7.75, 0, 8.375, 0.75], "texture": "#0"}, - "west": {"uv": [0, 15.25, 0.625, 16], "texture": "#0"}, - "up": {"uv": [0.625, 15.25, 0, 16], "texture": "#0"}, - "down": {"uv": [0.625, 15.25, 0, 16], "texture": "#0"} + "north": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"}, + "down": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} } }, { - "name": "Octagon", - "from": [5.51472, 7, 14], - "to": [10.48528, 13, 14], + "from": [3, 7.9, 11.9], + "to": [4, 15.9, 15.9], + "rotation": {"angle": 0, "axis": "y", "origin": [2, 7.9, 10.9]}, "faces": { - "north": {"uv": [5, 7.125, 5.625, 7.875], "texture": "#0"}, - "east": {"uv": [0, 15.25, 0.375, 16], "texture": "#0"}, - "south": {"uv": [7.125, 7, 7.75, 7.75], "texture": "#0"}, - "west": {"uv": [0, 15.25, 0.375, 16], "texture": "#0"}, - "up": {"uv": [0.625, 15.25, 0, 16], "texture": "#0"}, - "down": {"uv": [0.625, 15.375, 0, 16], "texture": "#0"} + "north": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"}, + "down": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} } }, { - "name": "Octagon", - "from": [2, 7, 5.51472], - "to": [2, 13, 10.48528], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 7.5, 8]}, + "from": [8, 7.9, 11.825], + "to": [9, 15.9, 15.825], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 7.9, 10.825]}, "faces": { - "north": {"uv": [0.5, 15.25, 0, 16], "texture": "#0"}, - "east": {"uv": [1.25, 7.375, 1.875, 8.125], "texture": "#0"}, - "south": {"uv": [0, 15.25, 0.375, 16], "texture": "#0"}, - "west": {"uv": [2, 7.375, 2.625, 8.125], "texture": "#0"}, - "up": {"uv": [0, 16, 0.5, 15.375], "texture": "#0"}, - "down": {"uv": [0, 15.375, 0.5, 16], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [2, 7, 5.51472], - "to": [2, 13, 10.48528], - "faces": { - "north": {"uv": [0, 15.25, 0.375, 16], "texture": "#0"}, - "east": {"uv": [2.75, 7.375, 3.375, 8.125], "texture": "#0"}, - "south": {"uv": [0, 15.25, 0.625, 16], "texture": "#0"}, - "west": {"uv": [7.625, 5.25, 8.25, 6], "texture": "#0"}, - "up": {"uv": [0, 16, 0.375, 15.375], "texture": "#0"}, - "down": {"uv": [0, 15.375, 0.5, 16], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [5.10051, 13, 1], - "to": [10.89949, 15, 15], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 14, 8]}, - "faces": { - "north": {"uv": [5.125, 3.75, 5.875, 4], "texture": "#0"}, - "east": {"uv": [8, 7.875, 9.75, 8.125], "texture": "#0"}, - "south": {"uv": [1.875, 8.625, 2.625, 8.875], "texture": "#0"}, - "west": {"uv": [8.125, 3.625, 9.875, 3.875], "texture": "#0"}, - "up": {"uv": [0.75, 7, 0, 5.25], "texture": "#0"}, - "down": {"uv": [1.625, 5.25, 0.875, 7], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [5.10051, 12.999, 1], - "to": [10.89949, 15, 15], - "faces": { - "north": {"uv": [2.75, 8.625, 3.5, 8.875], "texture": "#0"}, - "east": {"uv": [8.125, 4, 9.875, 4.25], "texture": "#0"}, - "south": {"uv": [8.625, 6.875, 9.375, 7.125], "texture": "#0"}, - "west": {"uv": [8.125, 4.375, 9.875, 4.625], "texture": "#0"}, - "up": {"uv": [4.75, 7, 4, 5.25], "texture": "#0"}, - "down": {"uv": [5.625, 5.25, 4.875, 7], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [1, 13, 5.10051], - "to": [15, 15, 10.89949], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 14, 8]}, - "faces": { - "north": {"uv": [8.125, 4.75, 9.875, 5], "texture": "#0"}, - "east": {"uv": [8.625, 7.25, 9.375, 7.5], "texture": "#0"}, - "south": {"uv": [0, 8.25, 1.75, 8.5], "texture": "#0"}, - "west": {"uv": [8, 8.625, 8.75, 8.875], "texture": "#0"}, - "up": {"uv": [6.875, 3.625, 5.125, 2.875], "texture": "#0"}, - "down": {"uv": [3.5, 5.75, 1.75, 6.5], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [1, 12.999, 5.10051], - "to": [15, 15, 10.89949], - "faces": { - "north": {"uv": [1.875, 8.25, 3.625, 8.5], "texture": "#0"}, - "east": {"uv": [5, 8.75, 5.75, 9], "texture": "#0"}, - "south": {"uv": [8, 8.25, 9.75, 8.5], "texture": "#0"}, - "west": {"uv": [5.875, 8.75, 6.625, 9], "texture": "#0"}, - "up": {"uv": [7.5, 6, 5.75, 5.25], "texture": "#0"}, - "down": {"uv": [7.5, 6.125, 5.75, 6.875], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [4.68629, 15, 0], - "to": [11.31371, 16, 16], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 16, 8]}, - "faces": { - "north": {"uv": [8.625, 1.5, 9.5, 1.625], "texture": "#0"}, - "east": {"uv": [8.375, 5.625, 10.375, 5.75], "texture": "#0"}, - "south": {"uv": [8.625, 7.625, 9.5, 7.75], "texture": "#0"}, - "west": {"uv": [8.375, 5.875, 10.375, 6], "texture": "#0"}, - "up": {"uv": [3, 4.70938, 2.125, 2.7875], "texture": "#0"}, - "down": {"uv": [4, 2.75, 3.125, 4.75], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [4.68629, 15.001, 0], - "to": [11.31371, 16.001, 16], - "faces": { - "north": {"uv": [9.5, 3.125, 10.375, 3.25], "texture": "#0"}, - "east": {"uv": [8.375, 6.125, 10.375, 6.25], "texture": "#0"}, - "south": {"uv": [6.625, 9.5, 7.5, 9.625], "texture": "#0"}, - "west": {"uv": [8.375, 6.375, 10.375, 6.5], "texture": "#0"}, - "up": {"uv": [4.875, 2, 4, 0], "texture": "#0"}, - "down": {"uv": [5, 2.125, 4.125, 4.125], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [0, 15, 4.68629], - "to": [16, 16, 11.31371], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 16, 8]}, - "faces": { - "north": {"uv": [8.375, 6.625, 10.375, 6.75], "texture": "#0"}, - "east": {"uv": [9.5, 6.875, 10.375, 7], "texture": "#0"}, - "south": {"uv": [8.5, 0, 10.5, 0.125], "texture": "#0"}, - "west": {"uv": [9.5, 7.125, 10.375, 7.25], "texture": "#0"}, - "up": {"uv": [6.9625, 0.875, 5.04062, 0], "texture": "#0"}, - "down": {"uv": [6.125, 4.25, 4.125, 5.125], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [0, 15.002, 4.68629], - "to": [16, 16.002, 11.31371], - "faces": { - "north": {"uv": [8.5, 0.25, 10.5, 0.375], "texture": "#0"}, - "east": {"uv": [4.775, 11.75, 5.60343, 11.875], "texture": "#0"}, - "south": {"uv": [8.5, 0.5, 10.5, 0.625], "texture": "#0"}, - "west": {"uv": [4.775, 12.125, 5.60343, 12.25], "texture": "#0"}, - "up": {"uv": [2, 12.6625, 0, 11.83408], "texture": "#0"}, - "down": {"uv": [4.375, 11.75, 2.375, 12.625], "texture": "#0"} - } - }, - { - "from": [6.042, 7.5525, 6.042], - "to": [9.958, 12.4475, 9.958], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 10, 8]}, - "faces": { - "north": {"uv": [6, 5, 14, 15], "texture": "#1"}, - "east": {"uv": [6, 5, 14, 15], "texture": "#1"}, - "south": {"uv": [6, 5, 14, 15], "texture": "#1"}, - "west": {"uv": [6, 5, 14, 15], "texture": "#1"}, - "up": {"uv": [14, 14, 6, 5], "texture": "#1"}, - "down": {"uv": [14, 14, 6, 6], "texture": "#1"} - } - }, - { - "from": [9.958, 8.042, 6.042], - "to": [10.4475, 11.958, 9.958], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 10, 8]}, - "faces": { - "north": {"uv": [5, 6, 6, 14], "texture": "#1"}, - "east": {"uv": [6, 6, 14, 14], "texture": "#1"}, - "south": {"uv": [14, 6, 15, 14], "texture": "#1"}, - "west": {"uv": [5, 6, 6, 14], "texture": "#1"}, - "up": {"uv": [5, 6, 6, 7], "texture": "#1"}, - "down": {"uv": [5, 14, 6, 6], "texture": "#1"} - } - }, - { - "from": [5.5525, 8.042, 6.042], - "to": [6.042, 11.958, 9.958], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 10, 8]}, - "faces": { - "north": {"uv": [14, 6, 15, 14], "texture": "#1"}, - "east": {"uv": [14, 6, 15, 14], "texture": "#1"}, - "south": {"uv": [5, 6, 6, 14], "texture": "#1"}, - "west": {"uv": [6, 6, 14, 14], "texture": "#1"}, - "up": {"uv": [14, 6, 15, 7], "texture": "#1"}, - "down": {"uv": [14, 14, 15, 6], "texture": "#1"} - } - }, - { - "from": [5.063, 9.021, 7.021], - "to": [5.5525, 10.979, 8.979], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 10, 8]}, - "faces": { - "north": {"uv": [15, 8, 16, 12], "texture": "#1"}, - "east": {"uv": [15, 8, 16, 12], "texture": "#1"}, - "south": {"uv": [4, 8, 5, 12], "texture": "#1"}, - "west": {"uv": [8, 8, 12, 12], "texture": "#1"}, - "up": {"uv": [15, 12, 16, 8], "texture": "#1"}, - "down": {"uv": [15, 12, 16, 8], "texture": "#1"} - } - }, - { - "from": [10.4475, 9.021, 7.021], - "to": [10.937, 10.979, 8.979], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 10, 8]}, - "faces": { - "north": {"uv": [4, 8, 5, 12], "texture": "#1"}, - "east": {"uv": [8, 8, 12, 12], "texture": "#1"}, - "south": {"uv": [15, 8, 16, 12], "texture": "#1"}, - "west": {"uv": [4, 8, 5, 12], "texture": "#1"}, - "up": {"uv": [4, 12, 5, 8], "texture": "#1"}, - "down": {"uv": [4, 8, 5, 12], "texture": "#1"} - } - }, - { - "from": [7.021, 12.4475, 7.021], - "to": [8.979, 12.937, 8.979], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 10, 8]}, - "faces": { - "north": {"uv": [8, 4, 12, 5], "texture": "#1"}, - "east": {"uv": [8, 4, 12, 5], "texture": "#1"}, - "south": {"uv": [8, 4, 12, 5], "texture": "#1"}, - "west": {"uv": [8, 4, 12, 5], "texture": "#1"}, - "up": {"uv": [12, 12, 8, 8], "texture": "#1"}, - "down": {"uv": [8, 4, 12, 5], "texture": "#1"} - } - }, - { - "from": [7.021, 7.063, 7.021], - "to": [8.979, 7.5525, 8.979], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 10, 8]}, - "faces": { - "north": {"uv": [8, 15, 12, 16], "texture": "#1"}, - "east": {"uv": [8, 15, 12, 16], "texture": "#1"}, - "south": {"uv": [8, 15, 12, 16], "texture": "#1"}, - "west": {"uv": [8, 15, 12, 16], "texture": "#1"}, - "up": {"uv": [8, 15, 12, 16], "texture": "#1"}, - "down": {"uv": [12, 12, 8, 8], "texture": "#1"} - } - }, - { - "from": [6.042, 8.042, 9.958], - "to": [9.958, 11.958, 10.4475], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 10, 8]}, - "faces": { - "north": {"uv": [6, 6, 14, 14], "texture": "#1"}, - "east": {"uv": [5, 6, 6, 14], "texture": "#1"}, - "south": {"uv": [6, 6, 14, 14], "texture": "#1"}, - "west": {"uv": [13, 6, 14, 14], "texture": "#1"}, - "up": {"uv": [14, 5, 6, 6], "texture": "#1"}, - "down": {"uv": [14, 14, 6, 15], "texture": "#1"} - } - }, - { - "from": [6.042, 8.042, 5.5525], - "to": [9.958, 11.958, 6.042], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 10, 8]}, - "faces": { - "north": {"uv": [6, 6, 14, 14], "texture": "#1"}, - "east": {"uv": [14, 6, 15, 14], "texture": "#1"}, - "south": {"uv": [6, 6, 14, 14], "texture": "#1"}, - "west": {"uv": [14, 6, 15, 14], "texture": "#1"}, - "up": {"uv": [14, 15, 6, 14], "texture": "#1"}, - "down": {"uv": [6, 5, 14, 6], "texture": "#1"} - } - }, - { - "from": [7.021, 9.021, 5.063], - "to": [8.979, 10.979, 5.5525], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 10, 8]}, - "faces": { - "north": {"uv": [8, 8, 12, 12], "texture": "#1"}, - "east": {"uv": [15, 8, 16, 12], "texture": "#1"}, - "south": {"uv": [8, 8, 12, 12], "texture": "#1"}, - "west": {"uv": [4, 8, 5, 12], "texture": "#1"}, - "up": {"uv": [12, 15, 8, 16], "texture": "#1"}, - "down": {"uv": [12, 4, 8, 5], "texture": "#1"} - } - }, - { - "from": [7.021, 9.021, 10.4475], - "to": [8.979, 10.979, 10.937], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 10, 8]}, - "faces": { - "north": {"uv": [8, 8, 12, 12], "texture": "#1"}, - "east": {"uv": [4, 8, 5, 12], "texture": "#1"}, - "south": {"uv": [8, 8, 12, 12], "texture": "#1"}, - "west": {"uv": [15, 8, 16, 12], "texture": "#1"}, - "up": {"uv": [12, 4, 8, 5], "texture": "#1"}, - "down": {"uv": [12, 16, 8, 15], "texture": "#1"} - } - }, - { - "from": [6, 1, 1.999], - "to": [10, 5, 1.999], - "faces": { - "north": {"uv": [0, 0, 4, 4], "texture": "#1"}, - "east": {"uv": [1, 5, 0, 6], "texture": "#1"}, - "south": {"uv": [0, 4, 1, 5], "texture": "#1"}, - "west": {"uv": [0, 4, 1, 5], "texture": "#1"}, - "up": {"uv": [1, 4, 0, 5], "texture": "#1"}, - "down": {"uv": [1, 4, 0, 5], "texture": "#1"} + "north": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8.5, 6.75, 8.75, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.5, 6.75, 9.75, 7.75], "texture": "#12"}, + "down": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} } } ], @@ -1123,10 +191,43 @@ "scale": [0.625, 0.625, 0.625] }, "head": { - "translation": [0, 14.75, 0] + "translation": [0, 15, 0] }, "fixed": { "scale": [0.5, 0.5, 0.5] } - } + }, + "groups": [ + { + "name": "allcube", + "origin": [0, 0, 0], + "color": 0, + "children": [ + { + "name": "baseBlock", + "origin": [0, 0, 0], + "color": 0, + "children": [0, 1] + }, + { + "name": "connect", + "origin": [5, 10, 14], + "color": 0, + "children": [2] + }, + { + "name": "back", + "origin": [7, 0.025, -2], + "color": 0, + "children": [3, 4, 5] + }, + { + "name": "copper_past", + "origin": [3, 9, -2], + "color": 0, + "children": [6, 7, 8, 9, 10, 11] + } + ] + } + ] } \ No newline at end of file diff --git a/src/main/resources/assets/useless_mod/models/item/ae2_gift_package.json b/src/main/resources/assets/useless_mod/models/item/ae2_gift_package.json new file mode 100644 index 00000000..cf3023fe --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/ae2_gift_package.json @@ -0,0 +1,6 @@ +{ + "parent": "item/generated", + "textures": { + "layer0": "useless_mod:item/ae2_gift_package" + } +} \ No newline at end of file diff --git a/src/main/resources/assets/useless_mod/models/item/aqua_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/aqua_glow_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/aqua_glow_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/aqua_plastic.json b/src/main/resources/assets/useless_mod/models/item/aqua_plastic.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/aqua_plastic.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/aqua_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/aqua_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/aqua_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/black_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/black_glow_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/black_glow_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/black_plastic.json b/src/main/resources/assets/useless_mod/models/item/black_plastic.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/black_plastic.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/black_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/black_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/black_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/blue_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/blue_glow_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/blue_glow_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/blue_plastic.json b/src/main/resources/assets/useless_mod/models/item/blue_plastic.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/blue_plastic.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/blue_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/blue_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/blue_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/brown_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/brown_glow_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/brown_glow_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/brown_plastic.json b/src/main/resources/assets/useless_mod/models/item/brown_plastic.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/brown_plastic.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/brown_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/brown_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/brown_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/cyan_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/cyan_glow_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/cyan_glow_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/cyan_plastic.json b/src/main/resources/assets/useless_mod/models/item/cyan_plastic.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/cyan_plastic.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/cyan_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/cyan_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/cyan_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/dark_red_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/dark_red_glow_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/dark_red_glow_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/dark_red_plastic.json b/src/main/resources/assets/useless_mod/models/item/dark_red_plastic.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/dark_red_plastic.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/dark_red_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/dark_red_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/dark_red_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/endless_beaf_crowbar.json b/src/main/resources/assets/useless_mod/models/item/endless_beaf_crowbar.json index bd722fa3..fbb57204 100644 --- a/src/main/resources/assets/useless_mod/models/item/endless_beaf_crowbar.json +++ b/src/main/resources/assets/useless_mod/models/item/endless_beaf_crowbar.json @@ -6,7 +6,7 @@ "overrides": [ { "predicate": { - "useless_mod:silk_touch_mode": 1.0 + "custom_model_data": 1 }, "model": "useless_mod:item/endless_beaf_item_silk_touch" } diff --git a/src/main/resources/assets/useless_mod/models/item/endless_beaf_hammer.json b/src/main/resources/assets/useless_mod/models/item/endless_beaf_hammer.json index bd722fa3..fbb57204 100644 --- a/src/main/resources/assets/useless_mod/models/item/endless_beaf_hammer.json +++ b/src/main/resources/assets/useless_mod/models/item/endless_beaf_hammer.json @@ -6,7 +6,7 @@ "overrides": [ { "predicate": { - "useless_mod:silk_touch_mode": 1.0 + "custom_model_data": 1 }, "model": "useless_mod:item/endless_beaf_item_silk_touch" } diff --git a/src/main/resources/assets/useless_mod/models/item/endless_beaf_item.json b/src/main/resources/assets/useless_mod/models/item/endless_beaf_item.json index bd722fa3..fbb57204 100644 --- a/src/main/resources/assets/useless_mod/models/item/endless_beaf_item.json +++ b/src/main/resources/assets/useless_mod/models/item/endless_beaf_item.json @@ -6,7 +6,7 @@ "overrides": [ { "predicate": { - "useless_mod:silk_touch_mode": 1.0 + "custom_model_data": 1 }, "model": "useless_mod:item/endless_beaf_item_silk_touch" } diff --git a/src/main/resources/assets/useless_mod/models/item/endless_beaf_item_no_wrench.json b/src/main/resources/assets/useless_mod/models/item/endless_beaf_item_no_wrench.json new file mode 100644 index 00000000..fffc24c5 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/endless_beaf_item_no_wrench.json @@ -0,0 +1,3 @@ +{ + "parent": "useless_mod:item/endless_beaf_item" +} diff --git a/src/main/resources/assets/useless_mod/models/item/endless_beaf_mallet.json b/src/main/resources/assets/useless_mod/models/item/endless_beaf_mallet.json index bd722fa3..fbb57204 100644 --- a/src/main/resources/assets/useless_mod/models/item/endless_beaf_mallet.json +++ b/src/main/resources/assets/useless_mod/models/item/endless_beaf_mallet.json @@ -6,7 +6,7 @@ "overrides": [ { "predicate": { - "useless_mod:silk_touch_mode": 1.0 + "custom_model_data": 1 }, "model": "useless_mod:item/endless_beaf_item_silk_touch" } diff --git a/src/main/resources/assets/useless_mod/models/item/endless_beaf_screwdriver.json b/src/main/resources/assets/useless_mod/models/item/endless_beaf_screwdriver.json index bd722fa3..fbb57204 100644 --- a/src/main/resources/assets/useless_mod/models/item/endless_beaf_screwdriver.json +++ b/src/main/resources/assets/useless_mod/models/item/endless_beaf_screwdriver.json @@ -6,7 +6,7 @@ "overrides": [ { "predicate": { - "useless_mod:silk_touch_mode": 1.0 + "custom_model_data": 1 }, "model": "useless_mod:item/endless_beaf_item_silk_touch" } diff --git a/src/main/resources/assets/useless_mod/models/item/endless_beaf_wrench.json b/src/main/resources/assets/useless_mod/models/item/endless_beaf_wrench.json index bd722fa3..fbb57204 100644 --- a/src/main/resources/assets/useless_mod/models/item/endless_beaf_wrench.json +++ b/src/main/resources/assets/useless_mod/models/item/endless_beaf_wrench.json @@ -6,7 +6,7 @@ "overrides": [ { "predicate": { - "useless_mod:silk_touch_mode": 1.0 + "custom_model_data": 1 }, "model": "useless_mod:item/endless_beaf_item_silk_touch" } diff --git a/src/main/resources/assets/useless_mod/models/item/gray_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/gray_glow_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/gray_glow_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/gray_plastic.json b/src/main/resources/assets/useless_mod/models/item/gray_plastic.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/gray_plastic.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/gray_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/gray_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/gray_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/green_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/green_glow_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/green_glow_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/green_plastic.json b/src/main/resources/assets/useless_mod/models/item/green_plastic.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/green_plastic.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/green_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/green_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/green_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/light_blue_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/light_blue_glow_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/light_blue_glow_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/light_blue_plastic.json b/src/main/resources/assets/useless_mod/models/item/light_blue_plastic.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/light_blue_plastic.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/light_blue_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/light_blue_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/light_blue_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/light_gray_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/light_gray_glow_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/light_gray_glow_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/light_gray_plastic.json b/src/main/resources/assets/useless_mod/models/item/light_gray_plastic.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/light_gray_plastic.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/light_gray_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/light_gray_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/light_gray_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/lime_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/lime_glow_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/lime_glow_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/lime_plastic.json b/src/main/resources/assets/useless_mod/models/item/lime_plastic.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/lime_plastic.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/lime_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/lime_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/lime_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/magenta_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/magenta_glow_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/magenta_glow_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/magenta_plastic.json b/src/main/resources/assets/useless_mod/models/item/magenta_plastic.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/magenta_plastic.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/magenta_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/magenta_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/magenta_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/me_pattern_assembly.json b/src/main/resources/assets/useless_mod/models/item/me_pattern_assembly.json new file mode 100644 index 00000000..cca07e30 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/me_pattern_assembly.json @@ -0,0 +1,3 @@ +{ + "parent": "useless_mod:block/me_pattern_assembly" +} diff --git a/src/main/resources/assets/useless_mod/models/item/multiblock_alloy_furnace_core.json b/src/main/resources/assets/useless_mod/models/item/multiblock_alloy_furnace_core.json new file mode 100644 index 00000000..5f822427 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/multiblock_alloy_furnace_core.json @@ -0,0 +1,3 @@ +{ + "parent": "useless_mod:block/multiblock_alloy_furnace_core_base" +} diff --git a/src/main/resources/assets/useless_mod/models/item/omniversal_furnace_casing.json b/src/main/resources/assets/useless_mod/models/item/omniversal_furnace_casing.json new file mode 100644 index 00000000..4bf0264b --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/omniversal_furnace_casing.json @@ -0,0 +1,3 @@ +{ + "parent": "useless_mod:block/omniversal_furnace_casing" +} diff --git a/src/main/resources/assets/useless_mod/models/item/omniversal_mold_hub.json b/src/main/resources/assets/useless_mod/models/item/omniversal_mold_hub.json new file mode 100644 index 00000000..bf7a292f --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/omniversal_mold_hub.json @@ -0,0 +1,3 @@ +{ + "parent": "useless_mod:block/omniversal_mold_hub" +} diff --git a/src/main/resources/assets/useless_mod/models/item/omniversal_pattern.json b/src/main/resources/assets/useless_mod/models/item/omniversal_pattern.json new file mode 100644 index 00000000..fbfcde54 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/omniversal_pattern.json @@ -0,0 +1,6 @@ +{ + "parent": "ae2:item/processing_pattern", + "textures": { + "layer0": "useless_mod:item/omniversal_pattern" + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/omniversal_pattern_converter.json b/src/main/resources/assets/useless_mod/models/item/omniversal_pattern_converter.json new file mode 100644 index 00000000..d1578c38 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/omniversal_pattern_converter.json @@ -0,0 +1,6 @@ +{ + "parent": "ae2:item/processing_pattern", + "textures": { + "layer0": "useless_mod:item/omniversal_pattern_converter" + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/orange_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/orange_glow_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/orange_glow_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/orange_plastic.json b/src/main/resources/assets/useless_mod/models/item/orange_plastic.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/orange_plastic.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/orange_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/orange_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/orange_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/passive_crafting_hatch.json b/src/main/resources/assets/useless_mod/models/item/passive_crafting_hatch.json new file mode 100644 index 00000000..08d9ee25 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/passive_crafting_hatch.json @@ -0,0 +1,3 @@ +{ + "parent": "useless_mod:block/passive_crafting_hatch" +} diff --git a/src/main/resources/assets/useless_mod/models/item/pink_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/pink_glow_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/pink_glow_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/pink_plastic.json b/src/main/resources/assets/useless_mod/models/item/pink_plastic.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/pink_plastic.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/pink_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/pink_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/pink_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/purple_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/purple_glow_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/purple_glow_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/purple_plastic.json b/src/main/resources/assets/useless_mod/models/item/purple_plastic.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/purple_plastic.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/purple_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/purple_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/purple_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/red_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/red_glow_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/red_glow_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/red_plastic.json b/src/main/resources/assets/useless_mod/models/item/red_plastic.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/red_plastic.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/red_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/red_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/red_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/ritual_blueprint.json b/src/main/resources/assets/useless_mod/models/item/ritual_blueprint.json new file mode 100644 index 00000000..1fa971f1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/ritual_blueprint.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "minecraft:item/paper" + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/supervisor.json b/src/main/resources/assets/useless_mod/models/item/supervisor.json new file mode 100644 index 00000000..6491acf7 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/supervisor.json @@ -0,0 +1,3 @@ +{ + "parent": "useless_mod:block/supervisor" +} diff --git a/src/main/resources/assets/useless_mod/models/item/temperature_regulator.json b/src/main/resources/assets/useless_mod/models/item/temperature_regulator.json new file mode 100644 index 00000000..9f1a1afc --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/temperature_regulator.json @@ -0,0 +1,10 @@ +{ + "parent": "useless_mod:block/temperature_regulator", + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/useful_coil.json b/src/main/resources/assets/useless_mod/models/item/useful_coil.json new file mode 100644 index 00000000..5687f133 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/useful_coil.json @@ -0,0 +1,3 @@ +{ + "parent": "useless_mod:block/useful_coil" +} diff --git a/src/main/resources/assets/useless_mod/models/item/useless_coil_tier_1.json b/src/main/resources/assets/useless_mod/models/item/useless_coil_tier_1.json new file mode 100644 index 00000000..5c856630 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/useless_coil_tier_1.json @@ -0,0 +1,3 @@ +{ + "parent": "useless_mod:block/useless_coil_tier_1" +} diff --git a/src/main/resources/assets/useless_mod/models/item/useless_coil_tier_2.json b/src/main/resources/assets/useless_mod/models/item/useless_coil_tier_2.json new file mode 100644 index 00000000..c8524075 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/useless_coil_tier_2.json @@ -0,0 +1,3 @@ +{ + "parent": "useless_mod:block/useless_coil_tier_2" +} diff --git a/src/main/resources/assets/useless_mod/models/item/useless_coil_tier_3.json b/src/main/resources/assets/useless_mod/models/item/useless_coil_tier_3.json new file mode 100644 index 00000000..5699c4b1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/useless_coil_tier_3.json @@ -0,0 +1,3 @@ +{ + "parent": "useless_mod:block/useless_coil_tier_3" +} diff --git a/src/main/resources/assets/useless_mod/models/item/useless_coil_tier_4.json b/src/main/resources/assets/useless_mod/models/item/useless_coil_tier_4.json new file mode 100644 index 00000000..17fa97d8 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/useless_coil_tier_4.json @@ -0,0 +1,3 @@ +{ + "parent": "useless_mod:block/useless_coil_tier_4" +} diff --git a/src/main/resources/assets/useless_mod/models/item/useless_coil_tier_5.json b/src/main/resources/assets/useless_mod/models/item/useless_coil_tier_5.json new file mode 100644 index 00000000..c783cdf1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/useless_coil_tier_5.json @@ -0,0 +1,3 @@ +{ + "parent": "useless_mod:block/useless_coil_tier_5" +} diff --git a/src/main/resources/assets/useless_mod/models/item/useless_coil_tier_6.json b/src/main/resources/assets/useless_mod/models/item/useless_coil_tier_6.json new file mode 100644 index 00000000..fa690448 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/useless_coil_tier_6.json @@ -0,0 +1,3 @@ +{ + "parent": "useless_mod:block/useless_coil_tier_6" +} diff --git a/src/main/resources/assets/useless_mod/models/item/useless_coil_tier_7.json b/src/main/resources/assets/useless_mod/models/item/useless_coil_tier_7.json new file mode 100644 index 00000000..bd25278b --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/useless_coil_tier_7.json @@ -0,0 +1,3 @@ +{ + "parent": "useless_mod:block/useless_coil_tier_7" +} diff --git a/src/main/resources/assets/useless_mod/models/item/useless_coil_tier_8.json b/src/main/resources/assets/useless_mod/models/item/useless_coil_tier_8.json new file mode 100644 index 00000000..10b7a8ee --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/useless_coil_tier_8.json @@ -0,0 +1,3 @@ +{ + "parent": "useless_mod:block/useless_coil_tier_8" +} diff --git a/src/main/resources/assets/useless_mod/models/item/useless_coil_tier_9.json b/src/main/resources/assets/useless_mod/models/item/useless_coil_tier_9.json new file mode 100644 index 00000000..9b6af4be --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/useless_coil_tier_9.json @@ -0,0 +1,3 @@ +{ + "parent": "useless_mod:block/useless_coil_tier_9" +} diff --git a/src/main/resources/assets/useless_mod/models/item/useless_compact_c9.json b/src/main/resources/assets/useless_mod/models/item/useless_compact_c9.json new file mode 100644 index 00000000..7eb07674 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/useless_compact_c9.json @@ -0,0 +1,3 @@ +{ + "parent": "useless_mod:block/useless_compact_c9" +} diff --git a/src/main/resources/assets/useless_mod/models/item/useless_compact_f9.json b/src/main/resources/assets/useless_mod/models/item/useless_compact_f9.json new file mode 100644 index 00000000..3a0e0ccc --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/useless_compact_f9.json @@ -0,0 +1,3 @@ +{ + "parent": "useless_mod:block/useless_compact_f9" +} diff --git a/src/main/resources/assets/useless_mod/models/item/useless_compact_l9.json b/src/main/resources/assets/useless_mod/models/item/useless_compact_l9.json new file mode 100644 index 00000000..b6d8675e --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/useless_compact_l9.json @@ -0,0 +1,3 @@ +{ + "parent": "useless_mod:block/useless_compact_l9" +} diff --git a/src/main/resources/assets/useless_mod/models/item/white_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/white_glow_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/white_glow_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/white_plastic.json b/src/main/resources/assets/useless_mod/models/item/white_plastic.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/white_plastic.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/white_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/white_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/white_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/yellow_glow_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/yellow_glow_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/yellow_glow_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/yellow_plastic.json b/src/main/resources/assets/useless_mod/models/item/yellow_plastic.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/yellow_plastic.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/models/item/yellow_plastic_ctm.json b/src/main/resources/assets/useless_mod/models/item/yellow_plastic_ctm.json new file mode 100644 index 00000000..028571d1 --- /dev/null +++ b/src/main/resources/assets/useless_mod/models/item/yellow_plastic_ctm.json @@ -0,0 +1,13 @@ +{ + "parent": "useless_mod:block/plastic/glow", + "textures": { + "all": "useless_mod:block/plastic_glow_block" + }, + "display": { + "gui": { + "rotation": [30, 225, 0], + "translation": [0, 0, 0], + "scale": [0.625, 0.625, 0.625] + } + } +} diff --git a/src/main/resources/assets/useless_mod/sounds.json b/src/main/resources/assets/useless_mod/sounds.json new file mode 100644 index 00000000..78028d7d --- /dev/null +++ b/src/main/resources/assets/useless_mod/sounds.json @@ -0,0 +1,19 @@ +{ + "howl": { + "sounds": [ + "useless_mod:howl" + ] + }, + "kill_aura_on": { + "subtitle": "subtitles.useless_mod.kill_aura_on", + "sounds": [ + "useless_mod:kill_aura_on" + ] + }, + "kill_aura_off": { + "subtitle": "subtitles.useless_mod.kill_aura_off", + "sounds": [ + "useless_mod:kill_aura_off" + ] + } +} diff --git a/src/main/resources/assets/useless_mod/sounds/howl.ogg b/src/main/resources/assets/useless_mod/sounds/howl.ogg new file mode 100644 index 00000000..97476687 Binary files /dev/null and b/src/main/resources/assets/useless_mod/sounds/howl.ogg differ diff --git a/src/main/resources/assets/useless_mod/sounds/kill_aura_off.ogg b/src/main/resources/assets/useless_mod/sounds/kill_aura_off.ogg new file mode 100644 index 00000000..47dc60a4 Binary files /dev/null and b/src/main/resources/assets/useless_mod/sounds/kill_aura_off.ogg differ diff --git a/src/main/resources/assets/useless_mod/sounds/kill_aura_on.ogg b/src/main/resources/assets/useless_mod/sounds/kill_aura_on.ogg new file mode 100644 index 00000000..d109ea70 Binary files /dev/null and b/src/main/resources/assets/useless_mod/sounds/kill_aura_on.ogg differ diff --git a/src/main/resources/assets/useless_mod/textures/block/advanced_alloy_furnace.png b/src/main/resources/assets/useless_mod/textures/block/advanced_alloy_furnace.png index d1b50bf8..33a8bfda 100644 Binary files a/src/main/resources/assets/useless_mod/textures/block/advanced_alloy_furnace.png and b/src/main/resources/assets/useless_mod/textures/block/advanced_alloy_furnace.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/advanced_alloy_furnace_active.png b/src/main/resources/assets/useless_mod/textures/block/advanced_alloy_furnace_active.png new file mode 100644 index 00000000..2368dc57 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/advanced_alloy_furnace_active.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/advanced_alloy_furnace_granule.png b/src/main/resources/assets/useless_mod/textures/block/advanced_alloy_furnace_granule.png new file mode 100644 index 00000000..84f01325 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/advanced_alloy_furnace_granule.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/advanced_alloy_furnace_locked.png b/src/main/resources/assets/useless_mod/textures/block/advanced_alloy_furnace_locked.png new file mode 100644 index 00000000..851e3dda Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/advanced_alloy_furnace_locked.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/iava_user.png b/src/main/resources/assets/useless_mod/textures/block/iava_user.png new file mode 100644 index 00000000..6e6d7894 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/iava_user.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil.png b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil.png new file mode 100644 index 00000000..2edfd2b9 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil.png.mcmeta b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil.png.mcmeta new file mode 100644 index 00000000..00f1d1eb --- /dev/null +++ b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil.png.mcmeta @@ -0,0 +1,7 @@ +{ + "animation": { + "frametime": 40, + "interpolate": true, + "frames": [0, 1] + } +} diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil_active.png b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil_active.png new file mode 100644 index 00000000..fea7537f Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil_active.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil_active_ctm.png b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil_active_ctm.png new file mode 100644 index 00000000..a5cd430b Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil_active_ctm.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil_ctm.png b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil_ctm.png new file mode 100644 index 00000000..b8ae45d4 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil_ctm.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil_ctm.png.mcmeta b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil_ctm.png.mcmeta new file mode 100644 index 00000000..00f1d1eb --- /dev/null +++ b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil_ctm.png.mcmeta @@ -0,0 +1,7 @@ +{ + "animation": { + "frametime": 40, + "interpolate": true, + "frames": [0, 1] + } +} diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_active.png b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_active.png new file mode 100644 index 00000000..fb7672f9 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_active.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_active_ctm.png b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_active_ctm.png new file mode 100644 index 00000000..a0fbd239 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_active_ctm.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_1.png b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_1.png new file mode 100644 index 00000000..600f3d02 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_1.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_1_ctm.png b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_1_ctm.png new file mode 100644 index 00000000..96eb3725 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_1_ctm.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_2.png b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_2.png new file mode 100644 index 00000000..069bec1b Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_2.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_2_ctm.png b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_2_ctm.png new file mode 100644 index 00000000..26120524 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_2_ctm.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_3.png b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_3.png new file mode 100644 index 00000000..f952a8d2 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_3.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_3_ctm.png b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_3_ctm.png new file mode 100644 index 00000000..3d638cf4 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_3_ctm.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_4.png b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_4.png new file mode 100644 index 00000000..5de73b87 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_4.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_4_ctm.png b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_4_ctm.png new file mode 100644 index 00000000..250a4899 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_4_ctm.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_5.png b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_5.png new file mode 100644 index 00000000..026ac236 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_5.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_5_ctm.png b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_5_ctm.png new file mode 100644 index 00000000..17b2c892 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_5_ctm.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_6.png b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_6.png new file mode 100644 index 00000000..3d3922fd Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_6.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_6_ctm.png b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_6_ctm.png new file mode 100644 index 00000000..054f1642 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_6_ctm.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_7.png b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_7.png new file mode 100644 index 00000000..f01d4bc4 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_7.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_7_ctm.png b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_7_ctm.png new file mode 100644 index 00000000..125aca7a Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_7_ctm.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_8.png b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_8.png new file mode 100644 index 00000000..ba29ba8b Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_8.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_8_ctm.png b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_8_ctm.png new file mode 100644 index 00000000..abe2fc8f Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_8_ctm.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_9.png b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_9.png new file mode 100644 index 00000000..428b4733 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_9.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_9_ctm.png b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_9_ctm.png new file mode 100644 index 00000000..b1a42653 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_9_ctm.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/controller_idle_overlay.png b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/controller_idle_overlay.png new file mode 100644 index 00000000..3e051438 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/controller_idle_overlay.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/controller_run_overlay.png b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/controller_run_overlay.png new file mode 100644 index 00000000..9129d8c7 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/controller_run_overlay.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/controller_wait_overlay.png b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/controller_wait_overlay.png new file mode 100644 index 00000000..5977f7fa Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/controller_wait_overlay.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/core_base_overlay.png b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/core_base_overlay.png new file mode 100644 index 00000000..5c304117 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/core_base_overlay.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/furnace_casing.png b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/furnace_casing.png new file mode 100644 index 00000000..ef4e00a1 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/furnace_casing.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/furnace_casing_ctm.png b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/furnace_casing_ctm.png new file mode 100644 index 00000000..6e2af0a5 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/furnace_casing_ctm.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/mold_hub_overlay.png b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/mold_hub_overlay.png new file mode 100644 index 00000000..9d9049d4 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/mold_hub_overlay.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/passive_crafting_hatch_overlay.png b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/passive_crafting_hatch_overlay.png new file mode 100644 index 00000000..3bbace34 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/passive_crafting_hatch_overlay.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/pattern_assembly_overlay.png b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/pattern_assembly_overlay.png new file mode 100644 index 00000000..28860aa3 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/multiblock_alloy_furnace/pattern_assembly_overlay.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/ore_generator_block.png b/src/main/resources/assets/useless_mod/textures/block/ore_generator_block.png index 92e9b681..76233faa 100644 Binary files a/src/main/resources/assets/useless_mod/textures/block/ore_generator_block.png and b/src/main/resources/assets/useless_mod/textures/block/ore_generator_block.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/plastic_glow_block.png b/src/main/resources/assets/useless_mod/textures/block/plastic_glow_block.png index da7ca000..63e02761 100644 Binary files a/src/main/resources/assets/useless_mod/textures/block/plastic_glow_block.png and b/src/main/resources/assets/useless_mod/textures/block/plastic_glow_block.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/plastic_glow_block_ctm.png b/src/main/resources/assets/useless_mod/textures/block/plastic_glow_block_ctm.png new file mode 100644 index 00000000..9fbe4594 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/plastic_glow_block_ctm.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/pluto.png b/src/main/resources/assets/useless_mod/textures/block/pluto.png new file mode 100644 index 00000000..a619ee3f Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/pluto.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/teleport_block.png b/src/main/resources/assets/useless_mod/textures/block/teleport_block.png index 53689f76..fcf96fe7 100644 Binary files a/src/main/resources/assets/useless_mod/textures/block/teleport_block.png and b/src/main/resources/assets/useless_mod/textures/block/teleport_block.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/teleport_block_2.png b/src/main/resources/assets/useless_mod/textures/block/teleport_block_2.png index 53689f76..2c33e70a 100644 Binary files a/src/main/resources/assets/useless_mod/textures/block/teleport_block_2.png and b/src/main/resources/assets/useless_mod/textures/block/teleport_block_2.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/teleport_block_3.png b/src/main/resources/assets/useless_mod/textures/block/teleport_block_3.png index 53689f76..f4eaed4e 100644 Binary files a/src/main/resources/assets/useless_mod/textures/block/teleport_block_3.png and b/src/main/resources/assets/useless_mod/textures/block/teleport_block_3.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/temperature_regulator_frame.png b/src/main/resources/assets/useless_mod/textures/block/temperature_regulator_frame.png new file mode 100644 index 00000000..be4b71a1 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/temperature_regulator_frame.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/temperature_regulator_panel.dark-original.bak.png b/src/main/resources/assets/useless_mod/textures/block/temperature_regulator_panel.dark-original.bak.png new file mode 100644 index 00000000..59eab450 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/temperature_regulator_panel.dark-original.bak.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/temperature_regulator_panel.png b/src/main/resources/assets/useless_mod/textures/block/temperature_regulator_panel.png new file mode 100644 index 00000000..6a39ac7e Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/temperature_regulator_panel.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/useless_compact_c9_back.png b/src/main/resources/assets/useless_mod/textures/block/useless_compact_c9_back.png new file mode 100644 index 00000000..fda2dc03 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/useless_compact_c9_back.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/useless_compact_c9_front.png b/src/main/resources/assets/useless_mod/textures/block/useless_compact_c9_front.png new file mode 100644 index 00000000..032042b6 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/useless_compact_c9_front.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/useless_compact_c9_left.png b/src/main/resources/assets/useless_mod/textures/block/useless_compact_c9_left.png new file mode 100644 index 00000000..c0238e7d Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/useless_compact_c9_left.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/useless_compact_c9_right.png b/src/main/resources/assets/useless_mod/textures/block/useless_compact_c9_right.png new file mode 100644 index 00000000..4cf9f596 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/useless_compact_c9_right.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/useless_compact_c9_top.png b/src/main/resources/assets/useless_mod/textures/block/useless_compact_c9_top.png new file mode 100644 index 00000000..93a2a6f6 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/useless_compact_c9_top.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/useless_compact_f9_back.png b/src/main/resources/assets/useless_mod/textures/block/useless_compact_f9_back.png new file mode 100644 index 00000000..e7ab1da3 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/useless_compact_f9_back.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/useless_compact_f9_front.png b/src/main/resources/assets/useless_mod/textures/block/useless_compact_f9_front.png new file mode 100644 index 00000000..081ba0c8 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/useless_compact_f9_front.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/useless_compact_f9_left.png b/src/main/resources/assets/useless_mod/textures/block/useless_compact_f9_left.png new file mode 100644 index 00000000..4d056f83 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/useless_compact_f9_left.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/useless_compact_f9_right.png b/src/main/resources/assets/useless_mod/textures/block/useless_compact_f9_right.png new file mode 100644 index 00000000..0269e16d Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/useless_compact_f9_right.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/useless_compact_f9_top.png b/src/main/resources/assets/useless_mod/textures/block/useless_compact_f9_top.png new file mode 100644 index 00000000..813ecd7e Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/useless_compact_f9_top.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/useless_compact_l9_back.png b/src/main/resources/assets/useless_mod/textures/block/useless_compact_l9_back.png new file mode 100644 index 00000000..dd3ef38e Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/useless_compact_l9_back.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/useless_compact_l9_front.png b/src/main/resources/assets/useless_mod/textures/block/useless_compact_l9_front.png new file mode 100644 index 00000000..b5262949 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/useless_compact_l9_front.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/useless_compact_l9_left.png b/src/main/resources/assets/useless_mod/textures/block/useless_compact_l9_left.png new file mode 100644 index 00000000..c0238e7d Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/useless_compact_l9_left.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/useless_compact_l9_right.png b/src/main/resources/assets/useless_mod/textures/block/useless_compact_l9_right.png new file mode 100644 index 00000000..502c565e Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/useless_compact_l9_right.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/useless_compact_l9_top.png b/src/main/resources/assets/useless_mod/textures/block/useless_compact_l9_top.png new file mode 100644 index 00000000..f6757239 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/block/useless_compact_l9_top.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_1.png b/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_1.png index 62bb397c..c41b2c39 100644 Binary files a/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_1.png and b/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_1.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_2.png b/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_2.png index 8c45f511..389b27d9 100644 Binary files a/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_2.png and b/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_2.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_3.png b/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_3.png index 066fa111..d47eb584 100644 Binary files a/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_3.png and b/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_3.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_4.png b/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_4.png index 6354de3b..89897c9e 100644 Binary files a/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_4.png and b/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_4.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_5.png b/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_5.png index a21728dd..0ac174c3 100644 Binary files a/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_5.png and b/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_5.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_6.png b/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_6.png index 6de9bb66..d982b59d 100644 Binary files a/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_6.png and b/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_6.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_7.png b/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_7.png index 0757665d..7de372ee 100644 Binary files a/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_7.png and b/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_7.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_8.png b/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_8.png index 721e7b5e..aa9efa11 100644 Binary files a/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_8.png and b/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_8.png differ diff --git a/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_9.png b/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_9.png index 8cd9a422..52fc5e63 100644 Binary files a/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_9.png and b/src/main/resources/assets/useless_mod/textures/block/useless_glass_tier_9.png differ diff --git a/src/main/resources/assets/useless_mod/textures/gui/advanced_alloy_furnace_gui.png b/src/main/resources/assets/useless_mod/textures/gui/advanced_alloy_furnace_gui.png index 8be7fb75..4dee6dc0 100644 Binary files a/src/main/resources/assets/useless_mod/textures/gui/advanced_alloy_furnace_gui.png and b/src/main/resources/assets/useless_mod/textures/gui/advanced_alloy_furnace_gui.png differ diff --git a/src/main/resources/assets/useless_mod/textures/gui/advanced_alloy_furnace_pattern_solt.png b/src/main/resources/assets/useless_mod/textures/gui/advanced_alloy_furnace_pattern_solt.png new file mode 100644 index 00000000..b2b09413 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/gui/advanced_alloy_furnace_pattern_solt.png differ diff --git a/src/main/resources/assets/useless_mod/textures/gui/advanced_alloy_furnace_tips.png b/src/main/resources/assets/useless_mod/textures/gui/advanced_alloy_furnace_tips.png index e067c309..bc23b7db 100644 Binary files a/src/main/resources/assets/useless_mod/textures/gui/advanced_alloy_furnace_tips.png and b/src/main/resources/assets/useless_mod/textures/gui/advanced_alloy_furnace_tips.png differ diff --git a/src/main/resources/assets/useless_mod/textures/gui/jei_advanced_alloy_furnace_gui.png b/src/main/resources/assets/useless_mod/textures/gui/jei_advanced_alloy_furnace_gui.png index 588bd150..f21a5dad 100644 Binary files a/src/main/resources/assets/useless_mod/textures/gui/jei_advanced_alloy_furnace_gui.png and b/src/main/resources/assets/useless_mod/textures/gui/jei_advanced_alloy_furnace_gui.png differ diff --git a/src/main/resources/assets/useless_mod/textures/gui/jei_arrow_advanced_alloy_furnace_gui.png b/src/main/resources/assets/useless_mod/textures/gui/jei_arrow_advanced_alloy_furnace_gui.png index 59668b5f..c5c16997 100644 Binary files a/src/main/resources/assets/useless_mod/textures/gui/jei_arrow_advanced_alloy_furnace_gui.png and b/src/main/resources/assets/useless_mod/textures/gui/jei_arrow_advanced_alloy_furnace_gui.png differ diff --git a/src/main/resources/assets/useless_mod/textures/gui/jei_catalyst_info_gui.png b/src/main/resources/assets/useless_mod/textures/gui/jei_catalyst_info_gui.png index abeefa70..3714ae14 100644 Binary files a/src/main/resources/assets/useless_mod/textures/gui/jei_catalyst_info_gui.png and b/src/main/resources/assets/useless_mod/textures/gui/jei_catalyst_info_gui.png differ diff --git a/src/main/resources/assets/useless_mod/textures/gui/locate_picture.png b/src/main/resources/assets/useless_mod/textures/gui/locate_picture.png new file mode 100644 index 00000000..693805d1 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/gui/locate_picture.png differ diff --git a/src/main/resources/assets/useless_mod/textures/item/ae2_gift_package.png b/src/main/resources/assets/useless_mod/textures/item/ae2_gift_package.png new file mode 100644 index 00000000..1b440ce6 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/item/ae2_gift_package.png differ diff --git a/src/main/resources/assets/useless_mod/textures/item/endless_beaf_item_silk_touch.png b/src/main/resources/assets/useless_mod/textures/item/endless_beaf_item_silk_touch.png index dbc3ac6c..0f8c060e 100644 Binary files a/src/main/resources/assets/useless_mod/textures/item/endless_beaf_item_silk_touch.png and b/src/main/resources/assets/useless_mod/textures/item/endless_beaf_item_silk_touch.png differ diff --git a/src/main/resources/assets/useless_mod/textures/item/ingot/possible_useful_ingot.png b/src/main/resources/assets/useless_mod/textures/item/ingot/possible_useful_ingot.png index 8127ceec..1d26159c 100644 Binary files a/src/main/resources/assets/useless_mod/textures/item/ingot/possible_useful_ingot.png and b/src/main/resources/assets/useless_mod/textures/item/ingot/possible_useful_ingot.png differ diff --git a/src/main/resources/assets/useless_mod/textures/item/ingot/useful_ingot.png b/src/main/resources/assets/useless_mod/textures/item/ingot/useful_ingot.png index 3dcce007..c5b6bfc6 100644 Binary files a/src/main/resources/assets/useless_mod/textures/item/ingot/useful_ingot.png and b/src/main/resources/assets/useless_mod/textures/item/ingot/useful_ingot.png differ diff --git a/src/main/resources/assets/useless_mod/textures/item/ingot/useful_ingot.png.mcmeta b/src/main/resources/assets/useless_mod/textures/item/ingot/useful_ingot.png.mcmeta index addfdd55..88a82d79 100644 --- a/src/main/resources/assets/useless_mod/textures/item/ingot/useful_ingot.png.mcmeta +++ b/src/main/resources/assets/useless_mod/textures/item/ingot/useful_ingot.png.mcmeta @@ -1,5 +1,10 @@ { "animation": { - "frametime": 4 + "frametime": 40, + "interpolate": true, + "frames": [ + {"index": 0, "time": 40}, + {"index": 1, "time": 10} + ] } } \ No newline at end of file diff --git a/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_1.png b/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_1.png index bc4985f4..40171e9c 100644 Binary files a/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_1.png and b/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_1.png differ diff --git a/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_2.png b/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_2.png index ae023862..1f8f401b 100644 Binary files a/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_2.png and b/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_2.png differ diff --git a/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_3.png b/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_3.png index 16b7e0a9..9367682c 100644 Binary files a/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_3.png and b/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_3.png differ diff --git a/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_4.png b/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_4.png index dcc822e7..46b25991 100644 Binary files a/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_4.png and b/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_4.png differ diff --git a/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_5.png b/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_5.png index 86bcdb9a..8a7d5c68 100644 Binary files a/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_5.png and b/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_5.png differ diff --git a/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_6.png b/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_6.png index fcd86ee7..0af79d8a 100644 Binary files a/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_6.png and b/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_6.png differ diff --git a/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_7.png b/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_7.png index 63865ef1..26e743d3 100644 Binary files a/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_7.png and b/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_7.png differ diff --git a/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_8.png b/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_8.png index 38c718eb..c1c4fe06 100644 Binary files a/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_8.png and b/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_8.png differ diff --git a/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_9.png b/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_9.png index 93df0cef..95d3b718 100644 Binary files a/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_9.png and b/src/main/resources/assets/useless_mod/textures/item/ingot/useless_ingot_tier_9.png differ diff --git a/src/main/resources/assets/useless_mod/textures/item/omniversal_pattern.png b/src/main/resources/assets/useless_mod/textures/item/omniversal_pattern.png new file mode 100644 index 00000000..75a08ebe Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/item/omniversal_pattern.png differ diff --git a/src/main/resources/assets/useless_mod/textures/item/omniversal_pattern_converter.png b/src/main/resources/assets/useless_mod/textures/item/omniversal_pattern_converter.png new file mode 100644 index 00000000..841f4aa4 Binary files /dev/null and b/src/main/resources/assets/useless_mod/textures/item/omniversal_pattern_converter.png differ diff --git a/src/main/resources/data/apotheosis/tags/items/pickaxe.json b/src/main/resources/data/apotheosis/tags/items/pickaxe.json deleted file mode 100644 index 5b66bd09..00000000 --- a/src/main/resources/data/apotheosis/tags/items/pickaxe.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "replace": false, - "values": [ - "useless_mod:endless_beaf_item" - ] -} \ No newline at end of file diff --git a/src/main/resources/data/blue_skies/tags/items/tools/pickaxes.json b/src/main/resources/data/blue_skies/tags/items/tools/pickaxes.json deleted file mode 100644 index 69f35346..00000000 --- a/src/main/resources/data/blue_skies/tags/items/tools/pickaxes.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "replace": false, - "values": [ - "useless_mod:endless_beaf_item", - "useless_mod:endless_beaf_wrench", - "useless_mod:endless_beaf_screwdriver", - "useless_mod:endless_beaf_mallet", - "useless_mod:endless_beaf_crowbar", - "useless_mod:endless_beaf_hammer" - ] -} \ No newline at end of file diff --git a/src/main/resources/data/forge/tags/items/shears.json b/src/main/resources/data/forge/tags/items/shears.json deleted file mode 100644 index 69f35346..00000000 --- a/src/main/resources/data/forge/tags/items/shears.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "replace": false, - "values": [ - "useless_mod:endless_beaf_item", - "useless_mod:endless_beaf_wrench", - "useless_mod:endless_beaf_screwdriver", - "useless_mod:endless_beaf_mallet", - "useless_mod:endless_beaf_crowbar", - "useless_mod:endless_beaf_hammer" - ] -} \ No newline at end of file diff --git a/src/main/resources/data/forge/tags/items/tools/axes.json b/src/main/resources/data/forge/tags/items/tools/axes.json deleted file mode 100644 index a22514f6..00000000 --- a/src/main/resources/data/forge/tags/items/tools/axes.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "replace": false, - "values": [ - ] -} \ No newline at end of file diff --git a/src/main/resources/data/forge/tags/items/tools/crowbars.json b/src/main/resources/data/forge/tags/items/tools/crowbars.json deleted file mode 100644 index 3465125d..00000000 --- a/src/main/resources/data/forge/tags/items/tools/crowbars.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "replace": false, - "values": [ - "useless_mod:endless_beaf_item", - "useless_mod:endless_beaf_crowbar" - ] -} \ No newline at end of file diff --git a/src/main/resources/data/forge/tags/items/tools/files.json b/src/main/resources/data/forge/tags/items/tools/files.json deleted file mode 100644 index 69f35346..00000000 --- a/src/main/resources/data/forge/tags/items/tools/files.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "replace": false, - "values": [ - "useless_mod:endless_beaf_item", - "useless_mod:endless_beaf_wrench", - "useless_mod:endless_beaf_screwdriver", - "useless_mod:endless_beaf_mallet", - "useless_mod:endless_beaf_crowbar", - "useless_mod:endless_beaf_hammer" - ] -} \ No newline at end of file diff --git a/src/main/resources/data/forge/tags/items/tools/hammers.json b/src/main/resources/data/forge/tags/items/tools/hammers.json deleted file mode 100644 index 806d934d..00000000 --- a/src/main/resources/data/forge/tags/items/tools/hammers.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "replace": false, - "values": [ - "useless_mod:endless_beaf_item", - "useless_mod:endless_beaf_hammer" - ] -} \ No newline at end of file diff --git a/src/main/resources/data/forge/tags/items/tools/hoes.json b/src/main/resources/data/forge/tags/items/tools/hoes.json deleted file mode 100644 index 69f35346..00000000 --- a/src/main/resources/data/forge/tags/items/tools/hoes.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "replace": false, - "values": [ - "useless_mod:endless_beaf_item", - "useless_mod:endless_beaf_wrench", - "useless_mod:endless_beaf_screwdriver", - "useless_mod:endless_beaf_mallet", - "useless_mod:endless_beaf_crowbar", - "useless_mod:endless_beaf_hammer" - ] -} \ No newline at end of file diff --git a/src/main/resources/data/forge/tags/items/tools/knives.json b/src/main/resources/data/forge/tags/items/tools/knives.json deleted file mode 100644 index 69f35346..00000000 --- a/src/main/resources/data/forge/tags/items/tools/knives.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "replace": false, - "values": [ - "useless_mod:endless_beaf_item", - "useless_mod:endless_beaf_wrench", - "useless_mod:endless_beaf_screwdriver", - "useless_mod:endless_beaf_mallet", - "useless_mod:endless_beaf_crowbar", - "useless_mod:endless_beaf_hammer" - ] -} \ No newline at end of file diff --git a/src/main/resources/data/forge/tags/items/tools/mallets.json b/src/main/resources/data/forge/tags/items/tools/mallets.json deleted file mode 100644 index 275b5062..00000000 --- a/src/main/resources/data/forge/tags/items/tools/mallets.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "replace": false, - "values": [ - "useless_mod:endless_beaf_item", - "useless_mod:endless_beaf_mallet" - ] -} \ No newline at end of file diff --git a/src/main/resources/data/forge/tags/items/tools/mortars.json b/src/main/resources/data/forge/tags/items/tools/mortars.json deleted file mode 100644 index 69f35346..00000000 --- a/src/main/resources/data/forge/tags/items/tools/mortars.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "replace": false, - "values": [ - "useless_mod:endless_beaf_item", - "useless_mod:endless_beaf_wrench", - "useless_mod:endless_beaf_screwdriver", - "useless_mod:endless_beaf_mallet", - "useless_mod:endless_beaf_crowbar", - "useless_mod:endless_beaf_hammer" - ] -} \ No newline at end of file diff --git a/src/main/resources/data/forge/tags/items/tools/pickaxes.json b/src/main/resources/data/forge/tags/items/tools/pickaxes.json deleted file mode 100644 index 69f35346..00000000 --- a/src/main/resources/data/forge/tags/items/tools/pickaxes.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "replace": false, - "values": [ - "useless_mod:endless_beaf_item", - "useless_mod:endless_beaf_wrench", - "useless_mod:endless_beaf_screwdriver", - "useless_mod:endless_beaf_mallet", - "useless_mod:endless_beaf_crowbar", - "useless_mod:endless_beaf_hammer" - ] -} \ No newline at end of file diff --git a/src/main/resources/data/forge/tags/items/tools/saws.json b/src/main/resources/data/forge/tags/items/tools/saws.json deleted file mode 100644 index 69f35346..00000000 --- a/src/main/resources/data/forge/tags/items/tools/saws.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "replace": false, - "values": [ - "useless_mod:endless_beaf_item", - "useless_mod:endless_beaf_wrench", - "useless_mod:endless_beaf_screwdriver", - "useless_mod:endless_beaf_mallet", - "useless_mod:endless_beaf_crowbar", - "useless_mod:endless_beaf_hammer" - ] -} \ No newline at end of file diff --git a/src/main/resources/data/forge/tags/items/tools/screwdrivers.json b/src/main/resources/data/forge/tags/items/tools/screwdrivers.json deleted file mode 100644 index b418e242..00000000 --- a/src/main/resources/data/forge/tags/items/tools/screwdrivers.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "replace": false, - "values": [ - "useless_mod:endless_beaf_item", - "useless_mod:endless_beaf_screwdriver" - ] -} \ No newline at end of file diff --git a/src/main/resources/data/forge/tags/items/tools/swords.json b/src/main/resources/data/forge/tags/items/tools/swords.json deleted file mode 100644 index a22514f6..00000000 --- a/src/main/resources/data/forge/tags/items/tools/swords.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "replace": false, - "values": [ - ] -} \ No newline at end of file diff --git a/src/main/resources/data/forge/tags/items/tools/wire_cutters.json b/src/main/resources/data/forge/tags/items/tools/wire_cutters.json deleted file mode 100644 index fef4e3ab..00000000 --- a/src/main/resources/data/forge/tags/items/tools/wire_cutters.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "replace": false, - "values": [ - "useless_mod:endless_beaf_item", - "useless_mod:endless_beaf_wrench", - "useless_mod:endless_beaf_crowbar", - "useless_mod:endless_beaf_hammer" - ] -} \ No newline at end of file diff --git a/src/main/resources/data/forge/tags/items/tools/wrench.json b/src/main/resources/data/forge/tags/items/tools/wrench.json deleted file mode 100644 index 79ac7829..00000000 --- a/src/main/resources/data/forge/tags/items/tools/wrench.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "replace": false, - "values": [ - "useless_mod:endless_beaf_wrench" - ] -} \ No newline at end of file diff --git a/src/main/resources/data/forge/tags/items/tools/wrenches.json b/src/main/resources/data/forge/tags/items/tools/wrenches.json deleted file mode 100644 index edd230b5..00000000 --- a/src/main/resources/data/forge/tags/items/tools/wrenches.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "replace": false, - "values": [ - "useless_mod:endless_beaf_item", - "useless_mod:endless_beaf_wrench" - ] -} \ No newline at end of file diff --git a/src/main/resources/data/ftbultimine/tags/items/excluded_tools.json b/src/main/resources/data/ftbultimine/tags/items/excluded_tools.json new file mode 100644 index 00000000..112c62b0 --- /dev/null +++ b/src/main/resources/data/ftbultimine/tags/items/excluded_tools.json @@ -0,0 +1,12 @@ +{ + "replace": false, + "values": [ + "useless_mod:endless_beaf_item", + "useless_mod:endless_beaf_item_no_wrench", + "useless_mod:endless_beaf_wrench", + "useless_mod:endless_beaf_screwdriver", + "useless_mod:endless_beaf_mallet", + "useless_mod:endless_beaf_crowbar", + "useless_mod:endless_beaf_hammer" + ] +} diff --git a/src/main/resources/data/gtceu/tags/items/tools/crafting_crowbars.json b/src/main/resources/data/gtceu/tags/items/tools/crafting_crowbars.json deleted file mode 100644 index 5b66bd09..00000000 --- a/src/main/resources/data/gtceu/tags/items/tools/crafting_crowbars.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "replace": false, - "values": [ - "useless_mod:endless_beaf_item" - ] -} \ No newline at end of file diff --git a/src/main/resources/data/gtceu/tags/items/tools/crafting_files.json b/src/main/resources/data/gtceu/tags/items/tools/crafting_files.json deleted file mode 100644 index 5b66bd09..00000000 --- a/src/main/resources/data/gtceu/tags/items/tools/crafting_files.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "replace": false, - "values": [ - "useless_mod:endless_beaf_item" - ] -} \ No newline at end of file diff --git a/src/main/resources/data/gtceu/tags/items/tools/crafting_hammers.json b/src/main/resources/data/gtceu/tags/items/tools/crafting_hammers.json deleted file mode 100644 index 5b66bd09..00000000 --- a/src/main/resources/data/gtceu/tags/items/tools/crafting_hammers.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "replace": false, - "values": [ - "useless_mod:endless_beaf_item" - ] -} \ No newline at end of file diff --git a/src/main/resources/data/gtceu/tags/items/tools/crafting_knives.json b/src/main/resources/data/gtceu/tags/items/tools/crafting_knives.json deleted file mode 100644 index 5b66bd09..00000000 --- a/src/main/resources/data/gtceu/tags/items/tools/crafting_knives.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "replace": false, - "values": [ - "useless_mod:endless_beaf_item" - ] -} \ No newline at end of file diff --git a/src/main/resources/data/gtceu/tags/items/tools/crafting_mallets.json b/src/main/resources/data/gtceu/tags/items/tools/crafting_mallets.json deleted file mode 100644 index 5b66bd09..00000000 --- a/src/main/resources/data/gtceu/tags/items/tools/crafting_mallets.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "replace": false, - "values": [ - "useless_mod:endless_beaf_item" - ] -} \ No newline at end of file diff --git a/src/main/resources/data/gtceu/tags/items/tools/crafting_mortars.json b/src/main/resources/data/gtceu/tags/items/tools/crafting_mortars.json deleted file mode 100644 index 5b66bd09..00000000 --- a/src/main/resources/data/gtceu/tags/items/tools/crafting_mortars.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "replace": false, - "values": [ - "useless_mod:endless_beaf_item" - ] -} \ No newline at end of file diff --git a/src/main/resources/data/gtceu/tags/items/tools/crafting_saws.json b/src/main/resources/data/gtceu/tags/items/tools/crafting_saws.json deleted file mode 100644 index 5b66bd09..00000000 --- a/src/main/resources/data/gtceu/tags/items/tools/crafting_saws.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "replace": false, - "values": [ - "useless_mod:endless_beaf_item" - ] -} \ No newline at end of file diff --git a/src/main/resources/data/gtceu/tags/items/tools/crafting_screwdrivers.json b/src/main/resources/data/gtceu/tags/items/tools/crafting_screwdrivers.json deleted file mode 100644 index 5b66bd09..00000000 --- a/src/main/resources/data/gtceu/tags/items/tools/crafting_screwdrivers.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "replace": false, - "values": [ - "useless_mod:endless_beaf_item" - ] -} \ No newline at end of file diff --git a/src/main/resources/data/gtceu/tags/items/tools/crafting_wire_cutters.json b/src/main/resources/data/gtceu/tags/items/tools/crafting_wire_cutters.json deleted file mode 100644 index 5b66bd09..00000000 --- a/src/main/resources/data/gtceu/tags/items/tools/crafting_wire_cutters.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "replace": false, - "values": [ - "useless_mod:endless_beaf_item" - ] -} \ No newline at end of file diff --git a/src/main/resources/data/gtceu/tags/items/tools/crafting_wrenches.json b/src/main/resources/data/gtceu/tags/items/tools/crafting_wrenches.json deleted file mode 100644 index 5b66bd09..00000000 --- a/src/main/resources/data/gtceu/tags/items/tools/crafting_wrenches.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "replace": false, - "values": [ - "useless_mod:endless_beaf_item" - ] -} \ No newline at end of file diff --git a/src/main/resources/data/malum/tags/items/soul_shatter_capable_weapon.json b/src/main/resources/data/malum/tags/items/soul_shatter_capable_weapon.json new file mode 100644 index 00000000..1707fe08 --- /dev/null +++ b/src/main/resources/data/malum/tags/items/soul_shatter_capable_weapon.json @@ -0,0 +1,11 @@ +{ + "replace": false, + "values": [ + "useless_mod:endless_beaf_item", + "useless_mod:endless_beaf_wrench", + "useless_mod:endless_beaf_screwdriver", + "useless_mod:endless_beaf_mallet", + "useless_mod:endless_beaf_crowbar", + "useless_mod:endless_beaf_hammer" + ] +} diff --git a/src/main/resources/data/minecraft/tags/blocks/mineable/pickaxe.json b/src/main/resources/data/minecraft/tags/blocks/mineable/pickaxe.json deleted file mode 100644 index b74835ec..00000000 --- a/src/main/resources/data/minecraft/tags/blocks/mineable/pickaxe.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "replace": false, - "values": [ - "useless_mod:ore_generator_block", - "useless_mod:teleport_block", - "useless_mod:teleport_block_2", - "useless_mod:black_glow_plastic", - "useless_mod:red_glow_plastic", - "useless_mod:green_glow_plastic", - "useless_mod:brown_glow_plastic", - "useless_mod:blue_glow_plastic", - "useless_mod:purple_glow_plastic", - "useless_mod:cyan_glow_plastic", - "useless_mod:light_gray_glow_plastic", - "useless_mod:gray_glow_plastic", - "useless_mod:pink_glow_plastic", - "useless_mod:lime_glow_plastic", - "useless_mod:yellow_glow_plastic", - "useless_mod:light_blue_glow_plastic", - "useless_mod:magenta_glow_plastic", - "useless_mod:orange_glow_plastic", - "useless_mod:white_glow_plastic", - "useless_mod:dark_red_glow_plastic", - "useless_mod:aqua_glow_plastic", - "useless_mod:advanced_alloy_furnace_block" - ] -} \ No newline at end of file diff --git a/src/main/resources/data/minecraft/tags/items/axes.json b/src/main/resources/data/minecraft/tags/items/axes.json deleted file mode 100644 index a22514f6..00000000 --- a/src/main/resources/data/minecraft/tags/items/axes.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "replace": false, - "values": [ - ] -} \ No newline at end of file diff --git a/src/main/resources/data/minecraft/tags/items/hoes.json b/src/main/resources/data/minecraft/tags/items/hoes.json deleted file mode 100644 index 69f35346..00000000 --- a/src/main/resources/data/minecraft/tags/items/hoes.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "replace": false, - "values": [ - "useless_mod:endless_beaf_item", - "useless_mod:endless_beaf_wrench", - "useless_mod:endless_beaf_screwdriver", - "useless_mod:endless_beaf_mallet", - "useless_mod:endless_beaf_crowbar", - "useless_mod:endless_beaf_hammer" - ] -} \ No newline at end of file diff --git a/src/main/resources/data/minecraft/tags/items/pickaxes.json b/src/main/resources/data/minecraft/tags/items/pickaxes.json deleted file mode 100644 index 69f35346..00000000 --- a/src/main/resources/data/minecraft/tags/items/pickaxes.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "replace": false, - "values": [ - "useless_mod:endless_beaf_item", - "useless_mod:endless_beaf_wrench", - "useless_mod:endless_beaf_screwdriver", - "useless_mod:endless_beaf_mallet", - "useless_mod:endless_beaf_crowbar", - "useless_mod:endless_beaf_hammer" - ] -} \ No newline at end of file diff --git a/src/main/resources/data/minecraft/tags/items/shears.json b/src/main/resources/data/minecraft/tags/items/shears.json deleted file mode 100644 index 69f35346..00000000 --- a/src/main/resources/data/minecraft/tags/items/shears.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "replace": false, - "values": [ - "useless_mod:endless_beaf_item", - "useless_mod:endless_beaf_wrench", - "useless_mod:endless_beaf_screwdriver", - "useless_mod:endless_beaf_mallet", - "useless_mod:endless_beaf_crowbar", - "useless_mod:endless_beaf_hammer" - ] -} \ No newline at end of file diff --git a/src/main/resources/data/minecraft/tags/items/swords.json b/src/main/resources/data/minecraft/tags/items/swords.json deleted file mode 100644 index a22514f6..00000000 --- a/src/main/resources/data/minecraft/tags/items/swords.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "replace": false, - "values": [ - ] -} \ No newline at end of file diff --git a/src/main/resources/data/thermal/tags/blocks/machines.json b/src/main/resources/data/thermal/tags/blocks/machines.json deleted file mode 100644 index 708fb1b5..00000000 --- a/src/main/resources/data/thermal/tags/blocks/machines.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "values": [ - "advanced_alloy_furnace_block" - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/advancements/grass_wand_drop.json b/src/main/resources/data/useless_mod/advancements/grass_wand_drop.json new file mode 100644 index 00000000..830a0de1 --- /dev/null +++ b/src/main/resources/data/useless_mod/advancements/grass_wand_drop.json @@ -0,0 +1,28 @@ +{ + "display": { + "icon": { + "item": "useless_mod:endless_beaf_item" + }, + "title": { + "translate": "advancements.useless_mod.grass_wand_drop.title" + }, + "description": { + "translate": "advancements.useless_mod.grass_wand_drop.description" + }, + "frame": "challenge", + "show_toast": true, + "announce_to_chat": true, + "hidden": true + }, + "criteria": { + "main": { + "trigger": "minecraft:impossible" + } + }, + "requirements": [ + [ + "main" + ] + ], + "sends_telemetry_event": false +} diff --git a/src/main/resources/data/useless_mod/damage_type/beef_tool.json b/src/main/resources/data/useless_mod/damage_type/beef_tool.json new file mode 100644 index 00000000..ce87978d --- /dev/null +++ b/src/main/resources/data/useless_mod/damage_type/beef_tool.json @@ -0,0 +1,6 @@ +{ + "message_id": "useless_mod.beef_tool", + "scaling": "never", + "exhaustion": 0.0, + "effects": "hurt" +} diff --git a/src/main/resources/data/useless_mod/dimension/uselessdim.json b/src/main/resources/data/useless_mod/dimension/uselessdim.json index abbee5a2..a751464e 100644 --- a/src/main/resources/data/useless_mod/dimension/uselessdim.json +++ b/src/main/resources/data/useless_mod/dimension/uselessdim.json @@ -6,6 +6,5 @@ "type": "minecraft:fixed", "biome": "useless_mod:useless_biome" } - } } \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/dimension/uselessdim2.json b/src/main/resources/data/useless_mod/dimension/uselessdim2.json index a8713ac3..3eae6138 100644 --- a/src/main/resources/data/useless_mod/dimension/uselessdim2.json +++ b/src/main/resources/data/useless_mod/dimension/uselessdim2.json @@ -6,6 +6,5 @@ "type": "minecraft:fixed", "biome": "useless_mod:useless_biome" } - } } \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/dimension/uselessdim3.json b/src/main/resources/data/useless_mod/dimension/uselessdim3.json index 07286bb2..09f07538 100644 --- a/src/main/resources/data/useless_mod/dimension/uselessdim3.json +++ b/src/main/resources/data/useless_mod/dimension/uselessdim3.json @@ -1,11 +1,10 @@ { "type": "useless_mod:uselessdim3", "generator": { - "type": "useless_mod:uselessdim3_gen", + "type": "useless_mod:uselessdim_gen_3", "biome_source": { "type": "minecraft:fixed", "biome": "useless_mod:useless_biome" } - } } \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/dimension_effect/useless_effects.json b/src/main/resources/data/useless_mod/dimension_effect/useless_effects.json deleted file mode 100644 index 3e9190d2..00000000 --- a/src/main/resources/data/useless_mod/dimension_effect/useless_effects.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "sky_color": 16777215, - "fog_color": 16777215, - "water_color": 8878075, - "water_fog_color": 8878075, - "foliage_color": 8878075, - "grass_color": 8878075, - "cloud_height": 192.0, - "has_sky_light": true, - "has_ground": true, - "ambient_light": 0.0, - "forced": false -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/dimension_type/uselessdim.json b/src/main/resources/data/useless_mod/dimension_type/uselessdim.json index 65c82491..9ae4b7d1 100644 --- a/src/main/resources/data/useless_mod/dimension_type/uselessdim.json +++ b/src/main/resources/data/useless_mod/dimension_type/uselessdim.json @@ -10,18 +10,12 @@ "has_weather": false, "coordinate_scale": 1.0, "ambient_light": 0.0, - "fixed_time": 6000, + "fixed_time": 5000, "logical_height": 384, "effects": "minecraft:overworld", "infiniburn": "#minecraft:infiniburn_overworld", "min_y": -64, "height": 384, - "monster_spawn_light_level": { - "type": "minecraft:uniform", - "value": { - "min_inclusive": 0, - "max_inclusive": 7 - } - }, + "monster_spawn_light_level": 7, "monster_spawn_block_light_limit": 0 -} \ No newline at end of file +} diff --git a/src/main/resources/data/useless_mod/dimension_type/uselessdim2.json b/src/main/resources/data/useless_mod/dimension_type/uselessdim2.json index 65c82491..9ae4b7d1 100644 --- a/src/main/resources/data/useless_mod/dimension_type/uselessdim2.json +++ b/src/main/resources/data/useless_mod/dimension_type/uselessdim2.json @@ -10,18 +10,12 @@ "has_weather": false, "coordinate_scale": 1.0, "ambient_light": 0.0, - "fixed_time": 6000, + "fixed_time": 5000, "logical_height": 384, "effects": "minecraft:overworld", "infiniburn": "#minecraft:infiniburn_overworld", "min_y": -64, "height": 384, - "monster_spawn_light_level": { - "type": "minecraft:uniform", - "value": { - "min_inclusive": 0, - "max_inclusive": 7 - } - }, + "monster_spawn_light_level": 7, "monster_spawn_block_light_limit": 0 -} \ No newline at end of file +} diff --git a/src/main/resources/data/useless_mod/dimension_type/uselessdim3.json b/src/main/resources/data/useless_mod/dimension_type/uselessdim3.json index 65c82491..9ae4b7d1 100644 --- a/src/main/resources/data/useless_mod/dimension_type/uselessdim3.json +++ b/src/main/resources/data/useless_mod/dimension_type/uselessdim3.json @@ -10,18 +10,12 @@ "has_weather": false, "coordinate_scale": 1.0, "ambient_light": 0.0, - "fixed_time": 6000, + "fixed_time": 5000, "logical_height": 384, "effects": "minecraft:overworld", "infiniburn": "#minecraft:infiniburn_overworld", "min_y": -64, "height": 384, - "monster_spawn_light_level": { - "type": "minecraft:uniform", - "value": { - "min_inclusive": 0, - "max_inclusive": 7 - } - }, + "monster_spawn_light_level": 7, "monster_spawn_block_light_limit": 0 -} \ No newline at end of file +} diff --git a/src/main/resources/data/useless_mod/loot_tables/blocks/advanced_alloy_furnace_block.json b/src/main/resources/data/useless_mod/loot_tables/blocks/advanced_alloy_furnace_block.json deleted file mode 100644 index 70fe1ef9..00000000 --- a/src/main/resources/data/useless_mod/loot_tables/blocks/advanced_alloy_furnace_block.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "useless_mod:advanced_alloy_furnace_block" - } - ], - "functions": [ - { - "function": "minecraft:copy_nbt", - "source": "block_entity", - "ops": [ - { - "source": "Inventory", - "target": "BlockEntityTag.Inventory", - "op": "replace" - }, - { - "source": "Energy", - "target": "BlockEntityTag.Energy", - "op": "replace" - }, - { - "source": "InputFluid", - "target": "BlockEntityTag.InputFluid", - "op": "replace" - }, - { - "source": "OutputFluid", - "target": "BlockEntityTag.OutputFluid", - "op": "replace" - }, - { - "source": "Process", - "target": "BlockEntityTag.Process", - "op": "replace" - }, - { - "source": "ProcessMax", - "target": "BlockEntityTag.ProcessMax", - "op": "replace" - }, - { - "source": "ProcessTick", - "target": "BlockEntityTag.ProcessTick", - "op": "replace" - }, - { - "source": "IsActive", - "target": "BlockEntityTag.IsActive", - "op": "replace" - }, - { - "source": "WasActive", - "target": "BlockEntityTag.WasActive", - "op": "replace" - }, - { - "source": "AutoOutputCounter", - "target": "BlockEntityTag.AutoOutputCounter", - "op": "replace" - }, - { - "source": "CurrentParallel", - "target": "BlockEntityTag.CurrentParallel", - "op": "replace" - }, - { - "source": "MaxParallel", - "target": "BlockEntityTag.MaxParallel", - "op": "replace" - } - ] - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/loot_tables/blocks/aqua_glow_plastic.json b/src/main/resources/data/useless_mod/loot_tables/blocks/aqua_glow_plastic.json deleted file mode 100644 index a7a1ddf1..00000000 --- a/src/main/resources/data/useless_mod/loot_tables/blocks/aqua_glow_plastic.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "useless_mod:aqua_glow_plastic" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/loot_tables/blocks/black_glow_plastic.json b/src/main/resources/data/useless_mod/loot_tables/blocks/black_glow_plastic.json deleted file mode 100644 index 4776b0e2..00000000 --- a/src/main/resources/data/useless_mod/loot_tables/blocks/black_glow_plastic.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "useless_mod:black_glow_plastic" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/loot_tables/blocks/blue_glow_plastic.json b/src/main/resources/data/useless_mod/loot_tables/blocks/blue_glow_plastic.json deleted file mode 100644 index d54aba42..00000000 --- a/src/main/resources/data/useless_mod/loot_tables/blocks/blue_glow_plastic.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "useless_mod:blue_glow_plastic" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/loot_tables/blocks/brown_glow_plastic.json b/src/main/resources/data/useless_mod/loot_tables/blocks/brown_glow_plastic.json deleted file mode 100644 index 86c6c858..00000000 --- a/src/main/resources/data/useless_mod/loot_tables/blocks/brown_glow_plastic.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "useless_mod:brown_glow_plastic" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/loot_tables/blocks/creator_doll_block.json b/src/main/resources/data/useless_mod/loot_tables/blocks/creator_doll_block.json deleted file mode 100644 index 4ea70ede..00000000 --- a/src/main/resources/data/useless_mod/loot_tables/blocks/creator_doll_block.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "useless_mod:creator_doll_block" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} diff --git a/src/main/resources/data/useless_mod/loot_tables/blocks/cyan_glow_plastic.json b/src/main/resources/data/useless_mod/loot_tables/blocks/cyan_glow_plastic.json deleted file mode 100644 index 51284116..00000000 --- a/src/main/resources/data/useless_mod/loot_tables/blocks/cyan_glow_plastic.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "useless_mod:cyan_glow_plastic" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/loot_tables/blocks/dark_red_glow_plastic.json b/src/main/resources/data/useless_mod/loot_tables/blocks/dark_red_glow_plastic.json deleted file mode 100644 index 59646476..00000000 --- a/src/main/resources/data/useless_mod/loot_tables/blocks/dark_red_glow_plastic.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "useless_mod:dark_red_glow_plastic" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/loot_tables/blocks/gray_glow_plastic.json b/src/main/resources/data/useless_mod/loot_tables/blocks/gray_glow_plastic.json deleted file mode 100644 index a94572a5..00000000 --- a/src/main/resources/data/useless_mod/loot_tables/blocks/gray_glow_plastic.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "useless_mod:gray_glow_plastic" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/loot_tables/blocks/green_glow_plastic.json b/src/main/resources/data/useless_mod/loot_tables/blocks/green_glow_plastic.json deleted file mode 100644 index 7de59354..00000000 --- a/src/main/resources/data/useless_mod/loot_tables/blocks/green_glow_plastic.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "useless_mod:green_glow_plastic" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/loot_tables/blocks/light_blue_glow_plastic.json b/src/main/resources/data/useless_mod/loot_tables/blocks/light_blue_glow_plastic.json deleted file mode 100644 index 89f76bb0..00000000 --- a/src/main/resources/data/useless_mod/loot_tables/blocks/light_blue_glow_plastic.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "useless_mod:light_blue_glow_plastic" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/loot_tables/blocks/light_gray_glow_plastic.json b/src/main/resources/data/useless_mod/loot_tables/blocks/light_gray_glow_plastic.json deleted file mode 100644 index 1a5b2ebe..00000000 --- a/src/main/resources/data/useless_mod/loot_tables/blocks/light_gray_glow_plastic.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "useless_mod:light_gray_glow_plastic" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/loot_tables/blocks/lime_glow_plastic.json b/src/main/resources/data/useless_mod/loot_tables/blocks/lime_glow_plastic.json deleted file mode 100644 index b52bfe77..00000000 --- a/src/main/resources/data/useless_mod/loot_tables/blocks/lime_glow_plastic.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "useless_mod:lime_glow_plastic" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/loot_tables/blocks/magenta_glow_plastic.json b/src/main/resources/data/useless_mod/loot_tables/blocks/magenta_glow_plastic.json deleted file mode 100644 index c986420e..00000000 --- a/src/main/resources/data/useless_mod/loot_tables/blocks/magenta_glow_plastic.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "useless_mod:magenta_glow_plastic" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/loot_tables/blocks/orange_glow_plastic.json b/src/main/resources/data/useless_mod/loot_tables/blocks/orange_glow_plastic.json deleted file mode 100644 index bd533686..00000000 --- a/src/main/resources/data/useless_mod/loot_tables/blocks/orange_glow_plastic.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "useless_mod:orange_glow_plastic" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/loot_tables/blocks/ore_generator_block.json b/src/main/resources/data/useless_mod/loot_tables/blocks/ore_generator_block.json deleted file mode 100644 index 379b4acd..00000000 --- a/src/main/resources/data/useless_mod/loot_tables/blocks/ore_generator_block.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "useless_mod:ore_generator_block" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/loot_tables/blocks/pink_glow_plastic.json b/src/main/resources/data/useless_mod/loot_tables/blocks/pink_glow_plastic.json deleted file mode 100644 index a9bf422d..00000000 --- a/src/main/resources/data/useless_mod/loot_tables/blocks/pink_glow_plastic.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "useless_mod:pink_glow_plastic" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/loot_tables/blocks/purple_glow_plastic.json b/src/main/resources/data/useless_mod/loot_tables/blocks/purple_glow_plastic.json deleted file mode 100644 index 680aa7b0..00000000 --- a/src/main/resources/data/useless_mod/loot_tables/blocks/purple_glow_plastic.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "useless_mod:purple_glow_plastic" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/loot_tables/blocks/red_glow_plastic.json b/src/main/resources/data/useless_mod/loot_tables/blocks/red_glow_plastic.json deleted file mode 100644 index 8b7398fe..00000000 --- a/src/main/resources/data/useless_mod/loot_tables/blocks/red_glow_plastic.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "useless_mod:red_glow_plastic" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/loot_tables/blocks/teleport_block.json b/src/main/resources/data/useless_mod/loot_tables/blocks/teleport_block.json deleted file mode 100644 index 897a53ce..00000000 --- a/src/main/resources/data/useless_mod/loot_tables/blocks/teleport_block.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "useless_mod:teleport_block" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/loot_tables/blocks/teleport_block_2.json b/src/main/resources/data/useless_mod/loot_tables/blocks/teleport_block_2.json deleted file mode 100644 index e9a1d958..00000000 --- a/src/main/resources/data/useless_mod/loot_tables/blocks/teleport_block_2.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "useless_mod:teleport_block_2" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/loot_tables/blocks/teleport_block_3.json b/src/main/resources/data/useless_mod/loot_tables/blocks/teleport_block_3.json deleted file mode 100644 index cf4dfb0c..00000000 --- a/src/main/resources/data/useless_mod/loot_tables/blocks/teleport_block_3.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "useless_mod:teleport_block_3" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/loot_tables/blocks/white_glow_plastic.json b/src/main/resources/data/useless_mod/loot_tables/blocks/white_glow_plastic.json deleted file mode 100644 index 4eb66912..00000000 --- a/src/main/resources/data/useless_mod/loot_tables/blocks/white_glow_plastic.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "useless_mod:white_glow_plastic" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/loot_tables/blocks/yellow_glow_plastic.json b/src/main/resources/data/useless_mod/loot_tables/blocks/yellow_glow_plastic.json deleted file mode 100644 index 2ed36079..00000000 --- a/src/main/resources/data/useless_mod/loot_tables/blocks/yellow_glow_plastic.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "useless_mod:yellow_glow_plastic" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/advanced_alloy/compact_c9.json b/src/main/resources/data/useless_mod/recipes/advanced_alloy/compact_c9.json new file mode 100644 index 00000000..81fabec7 --- /dev/null +++ b/src/main/resources/data/useless_mod/recipes/advanced_alloy/compact_c9.json @@ -0,0 +1,73 @@ +{ + "type": "useless_mod:advanced_alloy_furnace", + "conditions": [ + { + "type": "forge:mod_loaded", + "modid": "neoecoae" + } + ], + "energy": 4000000, + "id": "useless_mod:advanced_alloy/compact_c9", + "ingredients": [ + { + "count": 168, + "ingredient": { + "item": "neoecoae:computation_casing" + } + }, + { + "count": 8, + "ingredient": { + "item": "neoecoae:computation_system_l9" + } + }, + { + "count": 8, + "ingredient": { + "item": "neoecoae:computation_interface" + } + }, + { + "count": 88, + "ingredient": { + "item": "neoecoae:computation_transmitter" + } + }, + { + "count": 176, + "ingredient": { + "item": "neoecoae:computation_drive" + } + }, + { + "count": 176, + "ingredient": { + "item": "neoecoae:computation_parallel_core_l9" + } + }, + { + "count": 88, + "ingredient": { + "item": "neoecoae:computation_threading_core_l9" + } + }, + { + "count": 8, + "ingredient": { + "item": "neoecoae:computation_cooling_controller_l9" + } + }, + { + "count": 176, + "ingredient": { + "item": "neoecoae:eco_computation_cell_l9" + } + } + ], + "outputs": [ + { + "id": "useless_mod:useless_compact_c9" + } + ], + "process_time": 200 +} diff --git a/src/main/resources/data/useless_mod/recipes/advanced_alloy/compact_f9.json b/src/main/resources/data/useless_mod/recipes/advanced_alloy/compact_f9.json new file mode 100644 index 00000000..5cbc18fb --- /dev/null +++ b/src/main/resources/data/useless_mod/recipes/advanced_alloy/compact_f9.json @@ -0,0 +1,73 @@ +{ + "type": "useless_mod:advanced_alloy_furnace", + "conditions": [ + { + "type": "forge:mod_loaded", + "modid": "neoecoae" + } + ], + "energy": 4000000, + "id": "useless_mod:advanced_alloy/compact_f9", + "ingredients": [ + { + "count": 160, + "ingredient": { + "item": "neoecoae:crafting_casing" + } + }, + { + "count": 8, + "ingredient": { + "item": "neoecoae:crafting_system_l9" + } + }, + { + "count": 8, + "ingredient": { + "item": "neoecoae:output_hatch" + } + }, + { + "count": 8, + "ingredient": { + "item": "neoecoae:crafting_interface" + } + }, + { + "count": 8, + "ingredient": { + "item": "neoecoae:input_hatch" + } + }, + { + "count": 88, + "ingredient": { + "item": "neoecoae:crafting_worker" + } + }, + { + "count": 176, + "ingredient": { + "item": "neoecoae:crafting_parallel_core_l9" + } + }, + { + "count": 176, + "ingredient": { + "item": "neoecoae:crafting_pattern_bus" + } + }, + { + "count": 88, + "ingredient": { + "item": "neoecoae:crafting_vent" + } + } + ], + "outputs": [ + { + "id": "useless_mod:useless_compact_f9" + } + ], + "process_time": 200 +} diff --git a/src/main/resources/data/useless_mod/recipes/advanced_alloy/compact_l9.json b/src/main/resources/data/useless_mod/recipes/advanced_alloy/compact_l9.json new file mode 100644 index 00000000..64efef9d --- /dev/null +++ b/src/main/resources/data/useless_mod/recipes/advanced_alloy/compact_l9.json @@ -0,0 +1,67 @@ +{ + "type": "useless_mod:advanced_alloy_furnace", + "conditions": [ + { + "type": "forge:mod_loaded", + "modid": "neoecoae" + } + ], + "energy": 4000000, + "id": "useless_mod:advanced_alloy/compact_l9", + "ingredients": [ + { + "count": 22, + "ingredient": { + "item": "neoecoae:storage_casing" + } + }, + { + "count": 1, + "ingredient": { + "item": "neoecoae:storage_system_l9" + } + }, + { + "count": 1, + "ingredient": { + "item": "neoecoae:storage_interface" + } + }, + { + "count": 33, + "ingredient": { + "item": "neoecoae:eco_drive" + } + }, + { + "count": 22, + "ingredient": { + "item": "neoecoae:energy_cell_l9" + } + }, + { + "count": 11, + "ingredient": { + "item": "neoecoae:storage_vent" + } + }, + { + "count": 64, + "ingredient": { + "item": "neoecoae:eco_infinite_cell_component" + } + }, + { + "count": 12, + "ingredient": { + "item": "neoecoae:eco_item_storage_cell_256m" + } + } + ], + "outputs": [ + { + "id": "useless_mod:useless_compact_l9" + } + ], + "process_time": 200 +} diff --git a/src/main/resources/data/useless_mod/recipes/advanced_alloy_furnace.json b/src/main/resources/data/useless_mod/recipes/advanced_alloy_furnace.json deleted file mode 100644 index 8e4ca5c7..00000000 --- a/src/main/resources/data/useless_mod/recipes/advanced_alloy_furnace.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "type": "minecraft:crafting_shaped", - "pattern": [ - "ABC", - "DEF", - "GHI" - ], - "key": { - "A": { - "item": "minecraft:emerald_block" - }, - "B": { - "item": "minecraft:diamond_block" - }, - "C": { - "item": "minecraft:lapis_block" - }, - "D": { - "item": "minecraft:iron_block" - }, - "E": { - "item": "minecraft:furnace" - }, - "F": { - "item": "minecraft:gold_block" - }, - "G": { - "item": "minecraft:water_bucket" - }, - "H": { - "item": "minecraft:netherite_block" - }, - "I": { - "item": "minecraft:lava_bucket" - } - }, - "result": { - "item": "useless_mod:advanced_alloy_furnace_block", - "count": 1 - } -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/augment_parallel_1.json b/src/main/resources/data/useless_mod/recipes/augment_parallel_1.json deleted file mode 100644 index 768a4b1c..00000000 --- a/src/main/resources/data/useless_mod/recipes/augment_parallel_1.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "type": "minecraft:crafting_shaped", - "pattern": [ - "AAA", - "BBB", - "CCC" - ], - "key": { - "A": { - "item": "useless_mod:useless_integral_component_tier_1" - }, - "B": { - "item": "useless_mod:useless_integral_component_tier_2" - }, - "C": { - "item": "useless_mod:useless_integral_component_tier_3" - } - }, - "result": { - "item": "useless_mod:augment_parallel_1", - "count": 1 - } -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/augment_parallel_2.json b/src/main/resources/data/useless_mod/recipes/augment_parallel_2.json deleted file mode 100644 index bf7d8edc..00000000 --- a/src/main/resources/data/useless_mod/recipes/augment_parallel_2.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "type": "minecraft:crafting_shaped", - "pattern": [ - "AAA", - "BBB", - "CCC" - ], - "key": { - "A": { - "item": "useless_mod:augment_parallel_1" - }, - "B": { - "item": "useless_mod:useless_integral_component_tier_4" - }, - "C": { - "item": "useless_mod:useless_integral_component_tier_5" - } - }, - "result": { - "item": "useless_mod:augment_parallel_2", - "count": 1 - } -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/augment_parallel_3.json b/src/main/resources/data/useless_mod/recipes/augment_parallel_3.json deleted file mode 100644 index dda3a227..00000000 --- a/src/main/resources/data/useless_mod/recipes/augment_parallel_3.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "type": "minecraft:crafting_shaped", - "pattern": [ - "AAA", - "BBB", - "CCC" - ], - "key": { - "A": { - "item": "useless_mod:augment_parallel_2" - }, - "B": { - "item": "useless_mod:useless_integral_component_tier_6" - }, - "C": { - "item": "useless_mod:useless_integral_component_tier_7" - } - }, - "result": { - "item": "useless_mod:augment_parallel_3", - "count": 1 - } -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/beef_unbreakable.json b/src/main/resources/data/useless_mod/recipes/beef_unbreakable.json new file mode 100644 index 00000000..4836057f --- /dev/null +++ b/src/main/resources/data/useless_mod/recipes/beef_unbreakable.json @@ -0,0 +1,4 @@ +{ + "type": "useless_mod:beef_unbreakable", + "category": "misc" +} diff --git a/src/main/resources/data/useless_mod/recipes/endless_beaf_item.json b/src/main/resources/data/useless_mod/recipes/endless_beaf_item.json deleted file mode 100644 index 58994a15..00000000 --- a/src/main/resources/data/useless_mod/recipes/endless_beaf_item.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "type": "minecraft:crafting_shaped", - "pattern": [ - "ABC", - "DEF", - "IGH" - ], - "key": { - "A": { - "item": "useless_mod:useless_ingot_tier_5" - }, - "B": { - "item": "useless_mod:useless_ingot_tier_4" - }, - "C": { - "item": "useless_mod:useless_ingot_tier_3" - }, - "D": { - "item": "minecraft:diamond_pickaxe" - }, - "E": { - "item": "useless_mod:useless_ingot_tier_2" - }, - "F": { - "item": "minecraft:netherite_pickaxe" - }, - "G": { - "item": "useless_mod:useless_ingot_tier_1" - }, - "H": { - "item": "minecraft:golden_carrot" - }, - "I": { - "item": "minecraft:ghast_tear" - } - }, - "result": { - "item": "useless_mod:endless_beaf_item", - "count": 1 - } -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/alloy_smelter.json b/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/alloy_smelter.json new file mode 100644 index 00000000..3f884fd2 --- /dev/null +++ b/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/alloy_smelter.json @@ -0,0 +1,30 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building_blocks", + "conditions": [ + { + "type": "forge:mod_loaded", + "modid": "immersiveengineering" + } + ], + "pattern": [ + "IGI", + "GCG", + "IGI" + ], + "key": { + "I": { + "item": "useless_mod:useless_ingot_tier_1" + }, + "G": { + "item": "useless_mod:useless_glass_tier_1" + }, + "C": { + "item": "immersiveengineering:alloybrick" + } + }, + "result": { + "count": 1, + "item": "immersiveengineering:alloy_smelter" + } +} diff --git a/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/arc_furnace.json b/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/arc_furnace.json new file mode 100644 index 00000000..74d81155 --- /dev/null +++ b/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/arc_furnace.json @@ -0,0 +1,30 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building_blocks", + "conditions": [ + { + "type": "forge:mod_loaded", + "modid": "immersiveengineering" + } + ], + "pattern": [ + "IGI", + "GCG", + "IGI" + ], + "key": { + "I": { + "item": "useless_mod:useless_ingot_tier_1" + }, + "G": { + "item": "useless_mod:useless_glass_tier_1" + }, + "C": { + "item": "immersiveengineering:heavy_engineering" + } + }, + "result": { + "count": 1, + "item": "immersiveengineering:arc_furnace" + } +} diff --git a/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/blast_furnace.json b/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/blast_furnace.json new file mode 100644 index 00000000..8956d69f --- /dev/null +++ b/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/blast_furnace.json @@ -0,0 +1,30 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building_blocks", + "conditions": [ + { + "type": "forge:mod_loaded", + "modid": "immersiveengineering" + } + ], + "pattern": [ + "IGI", + "GCG", + "IGI" + ], + "key": { + "I": { + "item": "useless_mod:useless_ingot_tier_1" + }, + "G": { + "item": "useless_mod:useless_glass_tier_1" + }, + "C": { + "item": "immersiveengineering:blastbrick" + } + }, + "result": { + "count": 1, + "item": "immersiveengineering:blast_furnace" + } +} diff --git a/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/bottling_machine.json b/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/bottling_machine.json new file mode 100644 index 00000000..b4f31f4f --- /dev/null +++ b/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/bottling_machine.json @@ -0,0 +1,30 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building_blocks", + "conditions": [ + { + "type": "forge:mod_loaded", + "modid": "immersiveengineering" + } + ], + "pattern": [ + "IGI", + "GCG", + "IGI" + ], + "key": { + "I": { + "item": "useless_mod:useless_ingot_tier_1" + }, + "G": { + "item": "useless_mod:useless_glass_tier_1" + }, + "C": { + "item": "immersiveengineering:conveyor_basic" + } + }, + "result": { + "count": 1, + "item": "immersiveengineering:bottling_machine" + } +} diff --git a/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/coke_oven.json b/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/coke_oven.json new file mode 100644 index 00000000..02919935 --- /dev/null +++ b/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/coke_oven.json @@ -0,0 +1,30 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building_blocks", + "conditions": [ + { + "type": "forge:mod_loaded", + "modid": "immersiveengineering" + } + ], + "pattern": [ + "IGI", + "GCG", + "IGI" + ], + "key": { + "I": { + "item": "useless_mod:useless_ingot_tier_1" + }, + "G": { + "item": "useless_mod:useless_glass_tier_1" + }, + "C": { + "item": "immersiveengineering:cokebrick" + } + }, + "result": { + "count": 1, + "item": "immersiveengineering:coke_oven" + } +} diff --git a/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/crusher.json b/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/crusher.json new file mode 100644 index 00000000..00f9cbfb --- /dev/null +++ b/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/crusher.json @@ -0,0 +1,30 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building_blocks", + "conditions": [ + { + "type": "forge:mod_loaded", + "modid": "immersiveengineering" + } + ], + "pattern": [ + "IGI", + "GCG", + "IGI" + ], + "key": { + "I": { + "item": "useless_mod:useless_ingot_tier_1" + }, + "G": { + "item": "useless_mod:useless_glass_tier_1" + }, + "C": { + "item": "immersiveengineering:steel_scaffolding_standard" + } + }, + "result": { + "count": 1, + "item": "immersiveengineering:crusher" + } +} diff --git a/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/fermenter.json b/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/fermenter.json new file mode 100644 index 00000000..3bd5d30c --- /dev/null +++ b/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/fermenter.json @@ -0,0 +1,30 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building_blocks", + "conditions": [ + { + "type": "forge:mod_loaded", + "modid": "immersiveengineering" + } + ], + "pattern": [ + "IGI", + "GCG", + "IGI" + ], + "key": { + "I": { + "item": "useless_mod:useless_ingot_tier_1" + }, + "G": { + "item": "useless_mod:useless_glass_tier_1" + }, + "C": { + "item": "immersiveengineering:light_engineering" + } + }, + "result": { + "count": 1, + "item": "immersiveengineering:fermenter" + } +} diff --git a/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/metal_press.json b/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/metal_press.json new file mode 100644 index 00000000..2ad2bf4a --- /dev/null +++ b/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/metal_press.json @@ -0,0 +1,30 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building_blocks", + "conditions": [ + { + "type": "forge:mod_loaded", + "modid": "immersiveengineering" + } + ], + "pattern": [ + "IGI", + "GCG", + "IGI" + ], + "key": { + "I": { + "item": "useless_mod:useless_ingot_tier_1" + }, + "G": { + "item": "useless_mod:useless_glass_tier_1" + }, + "C": { + "item": "immersiveengineering:component_steel" + } + }, + "result": { + "count": 1, + "item": "immersiveengineering:metal_press" + } +} diff --git a/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/mixer.json b/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/mixer.json new file mode 100644 index 00000000..16337495 --- /dev/null +++ b/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/mixer.json @@ -0,0 +1,30 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building_blocks", + "conditions": [ + { + "type": "forge:mod_loaded", + "modid": "immersiveengineering" + } + ], + "pattern": [ + "IGI", + "GCG", + "IGI" + ], + "key": { + "I": { + "item": "useless_mod:useless_ingot_tier_1" + }, + "G": { + "item": "useless_mod:useless_glass_tier_1" + }, + "C": { + "item": "immersiveengineering:component_iron" + } + }, + "result": { + "count": 1, + "item": "immersiveengineering:mixer" + } +} diff --git a/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/refinery.json b/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/refinery.json new file mode 100644 index 00000000..16d67617 --- /dev/null +++ b/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/refinery.json @@ -0,0 +1,30 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building_blocks", + "conditions": [ + { + "type": "forge:mod_loaded", + "modid": "immersiveengineering" + } + ], + "pattern": [ + "IGI", + "GCG", + "IGI" + ], + "key": { + "I": { + "item": "useless_mod:useless_ingot_tier_1" + }, + "G": { + "item": "useless_mod:useless_glass_tier_1" + }, + "C": { + "item": "immersiveengineering:sheetmetal_steel" + } + }, + "result": { + "count": 1, + "item": "immersiveengineering:refinery" + } +} diff --git a/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/sawmill.json b/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/sawmill.json new file mode 100644 index 00000000..4f11875e --- /dev/null +++ b/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/sawmill.json @@ -0,0 +1,30 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building_blocks", + "conditions": [ + { + "type": "forge:mod_loaded", + "modid": "immersiveengineering" + } + ], + "pattern": [ + "IGI", + "GCG", + "IGI" + ], + "key": { + "I": { + "item": "useless_mod:useless_ingot_tier_1" + }, + "G": { + "item": "useless_mod:useless_glass_tier_1" + }, + "C": { + "item": "immersiveengineering:sawblade" + } + }, + "result": { + "count": 1, + "item": "immersiveengineering:sawmill" + } +} diff --git a/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/squeezer.json b/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/squeezer.json new file mode 100644 index 00000000..6e47c92a --- /dev/null +++ b/src/main/resources/data/useless_mod/recipes/immersiveengineering/mold/squeezer.json @@ -0,0 +1,30 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building_blocks", + "conditions": [ + { + "type": "forge:mod_loaded", + "modid": "immersiveengineering" + } + ], + "pattern": [ + "IGI", + "GCG", + "IGI" + ], + "key": { + "I": { + "item": "useless_mod:useless_ingot_tier_1" + }, + "G": { + "item": "useless_mod:useless_glass_tier_1" + }, + "C": { + "item": "immersiveengineering:hemp_fiber" + } + }, + "result": { + "count": 1, + "item": "immersiveengineering:squeezer" + } +} diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/advacedae/quantum_alloy.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/advacedae/quantum_alloy.json deleted file mode 100644 index 8ac8d45b..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/advacedae/quantum_alloy.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "advanced_ae" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:ingots/copper" - }, - "count": 256 - }, - { - "ingredient": { - "item": "advanced_ae:shattered_singularity" - }, - "count": 256 - }, - { - "ingredient": { - "item": "ae2:singularity" - }, - "count": 256 - } - ], - "mold": { - "item": "ae2:inscriber" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "input_fluid": { - "fluid": "advanced_ae:quantum_infusion_source", - "amount": 64000 - }, - "results": [ - { - "item": "advanced_ae:quantum_alloy", - "count": 64 - } - ], - "energy": 10000, - "process_time": 40 - } - } - ] -} diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/advacedae/quantum_alloy_plate.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/advacedae/quantum_alloy_plate.json deleted file mode 100644 index 02eaafd2..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/advacedae/quantum_alloy_plate.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "advanced_ae" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "advanced_ae:quantum_alloy" - }, - "count": 512 - }, - { - "ingredient": { - "tag": "forge:ingots/netherite" - }, - "count": 128 - }, - { - "ingredient": { - "tag": "forge:nether_stars" - }, - "count": 64 - } - ], - "mold": { - "item": "ae2:inscriber" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "input_fluid": { - "fluid": "advanced_ae:quantum_infusion_source", - "amount": 64000 - }, - "results": [ - { - "item": "advanced_ae:quantum_alloy_plate", - "count": 64 - } - ], - "energy": 10000, - "process_time": 40 - } - } - ] -} diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/advacedae/quantum_infused_dust.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/advacedae/quantum_infused_dust.json deleted file mode 100644 index 6213167a..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/advacedae/quantum_infused_dust.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "advanced_ae" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": [ - { - "item": "advanced_ae:shattered_singularity" - } - ], - "count": 64 - } - ], - "mold": { - "item": "ae2:certus_quartz_cutting_knife" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "advanced_ae:quantum_infused_dust", - "count": 64 - } - ], - "energy": 10000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/advacedae/quantum_infusion_source.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/advacedae/quantum_infusion_source.json deleted file mode 100644 index 3824e561..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/advacedae/quantum_infusion_source.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "advanced_ae" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "advanced_ae:quantum_infused_dust" - }, - "count": 64 - } - ], - "mold": { - "item": "ae2:inscriber" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "input_fluid": { - "fluid": "minecraft:water", - "amount": 64000 - }, - "output_fluid": { - "fluid": "advanced_ae:quantum_infusion_source", - "amount": 64000 - }, - "energy": 10000, - "process_time": 40 - } - } - ] -} diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/advacedae/quantum_processor.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/advacedae/quantum_processor.json deleted file mode 100644 index 7af8ded3..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/advacedae/quantum_processor.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "advanced_ae" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "advanced_ae:quantum_alloy" - }, - "count": 64 - }, - { - "ingredient": { - "tag": "forge:dusts/redstone" - }, - "count": 64 - }, - { - "ingredient": { - "tag": "forge:silicon" - }, - "count": 64 - } - ], - "mold": { - "item": "ae2:inscriber" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "advanced_ae:quantum_processor", - "count": 64 - } - ], - "energy": 10000, - "process_time": 40 - } - } - ] -} diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/advacedae/shattered_singularity.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/advacedae/shattered_singularity.json deleted file mode 100644 index 6b9c5bce..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/advacedae/shattered_singularity.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "advanced_ae" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "ae2:singularity" - }, - "count": 64 - }, - { - "ingredient": { - "tag": "forge:dusts/ender_pearl" - }, - "count": 128 - }, - { - "ingredient": { - "item": "ae2:sky_dust" - }, - "count": 64 - } - ], - "mold": { - "item": "ae2:inscriber" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "input_fluid": { - "fluid": "minecraft:lava", - "amount": 64000 - }, - "results": [ - { - "item": "advanced_ae:shattered_singularity", - "count": 64 - } - ], - "energy": 10000, - "process_time": 40 - } - } - ] -} diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/appflux/energy_processor.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/appflux/energy_processor.json deleted file mode 100644 index d0aae0a6..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/appflux/energy_processor.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "appflux" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "appflux:redstone_crystal" - }, - "count": 64 - }, - { - "ingredient": { - "tag": "forge:dusts/redstone" - }, - "count": 64 - }, - { - "ingredient": { - "tag": "forge:silicon" - }, - "count": 64 - } - ], - "mold": { - "item": "ae2:inscriber" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "appflux:energy_processor", - "count": 64 - } - ], - "energy": 10000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/appflux/redstone_crystal.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/appflux/redstone_crystal.json deleted file mode 100644 index e62ebd65..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/appflux/redstone_crystal.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "appflux" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:storage_blocks/redstone" - }, - "count": 16 - }, - { - "ingredient": { - "tag": "forge:gems/fluix" - }, - "count": 16 - }, - { - "ingredient": { - "tag": "forge:dusts/glowstone" - }, - "count": 16 - } - ], - "mold": { - "item": "ae2:inscriber" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "appflux:redstone_crystal", - "count": 64 - } - ], - "energy": 10000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/common/calculation_processor.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/common/calculation_processor.json deleted file mode 100644 index 39c5dfbc..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/common/calculation_processor.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "ae2" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "ae2:all_certus_quartz" - }, - "count": 64 - }, - { - "ingredient": { - "tag": "forge:dusts/redstone" - }, - "count": 64 - }, - { - "ingredient": { - "tag": "forge:silicon" - }, - "count": 64 - } - ], - "mold": { - "item": "ae2:inscriber" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "ae2:calculation_processor", - "count": 64 - } - ], - "energy": 10000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/common/certus_quartz_dust.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/common/certus_quartz_dust.json deleted file mode 100644 index 01d05c0e..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/common/certus_quartz_dust.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "ae2" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": [ - { - "tag": "forge:gems/certus_quartz" - }, - { - "tag": "ae2:all_certus_quartz" - } - ], - "count": 64 - } - ], - "mold": { - "item": "ae2:certus_quartz_cutting_knife" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "ae2:certus_quartz_dust", - "count": 64 - } - ], - "energy": 10000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/common/charged_certus_quartz_crystal.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/common/charged_certus_quartz_crystal.json deleted file mode 100644 index 1984b799..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/common/charged_certus_quartz_crystal.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "ae2" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": [ - { - "item": "ae2:certus_quartz_crystal" - } - ], - "count": 64 - } - ], - "mold": { - "item": "ae2:charger" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "ae2:charged_certus_quartz_crystal", - "count": 64 - } - ], - "energy": 10000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/common/ender_dust.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/common/ender_dust.json deleted file mode 100644 index c68d20b5..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/common/ender_dust.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "ae2" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": [ - { - "tag": "forge:gems/ender_pearl" - }, - { - "tag": "forge:ender_pearls" - } - ], - "count": 64 - } - ], - "mold": { - "item": "ae2:certus_quartz_cutting_knife" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "ae2:ender_dust", - "count": 64 - } - ], - "energy": 10000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/common/engineering_processor.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/common/engineering_processor.json deleted file mode 100644 index de431101..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/common/engineering_processor.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "ae2" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:gems/diamond" - }, - "count": 64 - }, - { - "ingredient": { - "tag": "forge:dusts/redstone" - }, - "count": 64 - }, - { - "ingredient": { - "tag": "forge:silicon" - }, - "count": 64 - } - ], - "mold": { - "item": "ae2:inscriber" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "ae2:engineering_processor", - "count": 64 - } - ], - "energy": 10000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/common/fluix_crystal.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/common/fluix_crystal.json deleted file mode 100644 index d78e1535..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/common/fluix_crystal.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "ae2" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "ae2:charged_certus_quartz_crystal" - }, - "count": 16 - }, - { - "ingredient": { - "tag": "forge:gems/quartz" - }, - "count": 16 - }, - { - "ingredient": { - "tag": "forge:dusts/redstone" - }, - "count": 16 - } - ], - "mold": { - "item": "ae2:inscriber" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "input_fluid": { - "fluid": "minecraft:water", - "amount": 16000 - }, - "results": [ - { - "item": "ae2:fluix_crystal", - "count": 64 - } - ], - "energy": 10000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/common/fluix_dust.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/common/fluix_dust.json deleted file mode 100644 index 825df0ca..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/common/fluix_dust.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "ae2" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:gems/fluix" - }, - "count": 64 - } - ], - "mold": { - "item": "ae2:certus_quartz_cutting_knife" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "ae2:fluix_dust", - "count": 64 - } - ], - "energy": 10000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/common/logic_processor.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/common/logic_processor.json deleted file mode 100644 index eeff2f72..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/common/logic_processor.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "ae2" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:ingots/gold" - }, - "count": 64 - }, - { - "ingredient": { - "tag": "forge:dusts/redstone" - }, - "count": 64 - }, - { - "ingredient": { - "tag": "forge:silicon" - }, - "count": 64 - } - ], - "mold": { - "item": "ae2:inscriber" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "ae2:logic_processor", - "count": 64 - } - ], - "energy": 10000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/common/sky_dust.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/common/sky_dust.json deleted file mode 100644 index 2062ab68..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/common/sky_dust.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "ae2" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": [ - { - "item": "ae2:sky_stone_block" - } - ], - "count": 64 - } - ], - "mold": { - "item": "ae2:certus_quartz_cutting_knife" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "ae2:sky_dust", - "count": 64 - } - ], - "energy": 10000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/megacells/accumulation_processor.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/megacells/accumulation_processor.json deleted file mode 100644 index ba46547b..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/megacells/accumulation_processor.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "megacells" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "megacells:sky_steel_ingot" - }, - "count": 64 - }, - { - "ingredient": { - "tag": "forge:dusts/redstone" - }, - "count": 64 - }, - { - "ingredient": { - "tag": "forge:silicon" - }, - "count": 64 - } - ], - "mold": { - "item": "ae2:inscriber" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "megacells:accumulation_processor", - "count": 64 - } - ], - "energy": 10000, - "process_time": 20 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/megacells/sky_steel_ingot.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/megacells/sky_steel_ingot.json deleted file mode 100644 index b44bb6ed..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/megacells/sky_steel_ingot.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "megacells" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "ae2:charged_certus_quartz_crystal" - }, - "count": 16 - }, - { - "ingredient": { - "tag": "forge:ingots/iron" - }, - "count": 16 - }, - { - "ingredient": { - "item": "ae2:sky_stone_block" - }, - "count": 64 - } - ], - "mold": { - "item": "ae2:inscriber" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "input_fluid": { - "fluid": "minecraft:lava", - "amount": 500 - }, - "results": [ - { - "item": "megacells:sky_steel_ingot", - "count": 64 - } - ], - "energy": 10000, - "process_time": 40 - } - } - ] -} diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/omnicells/complex_link_processor.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/omnicells/complex_link_processor.json deleted file mode 100644 index 660ce975..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/omnicells/complex_link_processor.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "ae2omnicells" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "minecraft:netherite_scrap" - }, - "count": 64 - }, - { - "ingredient": { - "tag": "forge:dusts/redstone" - }, - "count": 64 - }, - { - "ingredient": { - "tag": "forge:silicon" - }, - "count": 64 - } - ], - "mold": { - "item": "ae2:inscriber" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "ae2omnicells:complex_link_processor", - "count": 64 - } - ], - "energy": 10000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/omnicells/ender_ingot.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/omnicells/ender_ingot.json deleted file mode 100644 index 39c7ee17..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/omnicells/ender_ingot.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "ae2omnicells" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:dusts/ender_pearl" - }, - "count": 64 - }, - { - "ingredient": [ - { - "tag": "ae2:all_quartz_dust" - }, - { - "tag": "forge:dusts/certus_quartz" - } - ], - "count": 64 - }, - { - "ingredient": { - "tag": "forge:ingots/iron" - }, - "count": 64 - } - ], - "mold": { - "item": "ae2:inscriber" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "ae2omnicells:ender_ingot", - "count": 64 - } - ], - "energy": 10000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/omnicells/multidimensional_expansion_processor.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/omnicells/multidimensional_expansion_processor.json deleted file mode 100644 index eb241713..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/omnicells/multidimensional_expansion_processor.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "ae2omnicells" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "ae2:singularity" - }, - "count": 64 - }, - { - "ingredient": { - "tag": "forge:dusts/redstone" - }, - "count": 64 - }, - { - "ingredient": { - "tag": "forge:silicon" - }, - "count": 64 - } - ], - "mold": { - "item": "ae2:inscriber" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "ae2omnicells:multidimensional_expansion_processor", - "count": 64 - } - ], - "energy": 10000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/omnicells/omni_link_processor.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/omnicells/omni_link_processor.json deleted file mode 100644 index 29230934..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/ae2/omnicells/omni_link_processor.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "ae2omnicells" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "ae2omnicells:ender_ingot" - }, - "count": 64 - }, - { - "ingredient": { - "tag": "forge:dusts/redstone" - }, - "count": 64 - }, - { - "ingredient": { - "tag": "forge:silicon" - }, - "count": 64 - } - ], - "mold": { - "item": "ae2:inscriber" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "ae2omnicells:omni_link_processor", - "count": 64 - } - ], - "energy": 10000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/fluid_conversion_test.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/fluid_conversion_test.json deleted file mode 100644 index 0eb0225a..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/fluid_conversion_test.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "item": "minecraft:cobblestone", - "count": 64 - } - ], - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "input_fluid": { - "fluid": "minecraft:water", - "amount": 64000 - }, - "results": [ - { - "item": "minecraft:obsidian", - "count": 64 - } - ], - "output_fluid": { - "fluid": "minecraft:lava", - "amount": 64000 - }, - "energy": 2000, - "process_time": 40 -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/bronze_gear.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/bronze_gear.json deleted file mode 100644 index 102aff04..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/bronze_gear.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:ingots/bronze" - }, - "count": 256 - } - ], - "mold": { - "item": "useless_mod:metal_mold_gear" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:bronze_gear", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/constantan_gear.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/constantan_gear.json deleted file mode 100644 index 4146590d..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/constantan_gear.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:ingots/constantan" - }, - "count": 256 - } - ], - "mold": { - "item": "useless_mod:metal_mold_gear" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:constantan_gear", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/copper_gear.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/copper_gear.json deleted file mode 100644 index e7208ada..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/copper_gear.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:ingots/copper" - }, - "count": 256 - } - ], - "mold": { - "item": "useless_mod:metal_mold_gear" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:copper_gear", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/diamond_gear.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/diamond_gear.json deleted file mode 100644 index 253cdfac..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/diamond_gear.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:gems/diamond" - }, - "count": 256 - } - ], - "mold": { - "item": "useless_mod:metal_mold_gear" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:diamond_gear", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/electrum_gear.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/electrum_gear.json deleted file mode 100644 index 5393010e..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/electrum_gear.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:ingots/electrum" - }, - "count": 256 - } - ], - "mold": { - "item": "useless_mod:metal_mold_gear" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:electrum_gear", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/emerald_gear.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/emerald_gear.json deleted file mode 100644 index 9222a5fb..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/emerald_gear.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:gems/emerald" - }, - "count": 256 - } - ], - "mold": { - "item": "useless_mod:metal_mold_gear" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:emerald_gear", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/enderium_gear.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/enderium_gear.json deleted file mode 100644 index 8426a938..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/enderium_gear.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:ingots/enderium" - }, - "count": 256 - } - ], - "mold": { - "item": "useless_mod:metal_mold_gear" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:enderium_gear", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/gold_gear.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/gold_gear.json deleted file mode 100644 index 49ef54e8..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/gold_gear.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:ingots/gold" - }, - "count": 256 - } - ], - "mold": { - "item": "useless_mod:metal_mold_gear" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:gold_gear", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/invar_gear.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/invar_gear.json deleted file mode 100644 index 6280a6c8..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/invar_gear.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:ingots/invar" - }, - "count": 256 - } - ], - "mold": { - "item": "useless_mod:metal_mold_gear" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:invar_gear", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/iron_gear.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/iron_gear.json deleted file mode 100644 index 7635e250..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/iron_gear.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:ingots/iron" - }, - "count": 256 - } - ], - "mold": { - "item": "useless_mod:metal_mold_gear" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:iron_gear", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/lapis_gear.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/lapis_gear.json deleted file mode 100644 index c51bf89e..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/lapis_gear.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:gems/lapis" - }, - "count": 256 - } - ], - "mold": { - "item": "useless_mod:metal_mold_gear" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:lapis_gear", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/lead_gear.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/lead_gear.json deleted file mode 100644 index 194ac9ce..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/lead_gear.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:ingots/lead" - }, - "count": 256 - } - ], - "mold": { - "item": "useless_mod:metal_mold_gear" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:lead_gear", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/lumium_gear.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/lumium_gear.json deleted file mode 100644 index 02cd7b61..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/lumium_gear.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:ingots/lumium" - }, - "count": 256 - } - ], - "mold": { - "item": "useless_mod:metal_mold_gear" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:lumium_gear", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/netherite_gear.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/netherite_gear.json deleted file mode 100644 index 1495ff01..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/netherite_gear.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:ingots/netherite" - }, - "count": 256 - } - ], - "mold": { - "item": "useless_mod:metal_mold_gear" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:netherite_gear", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/nickel_gear.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/nickel_gear.json deleted file mode 100644 index e3fe61d3..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/nickel_gear.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:ingots/nickel" - }, - "count": 256 - } - ], - "mold": { - "item": "useless_mod:metal_mold_gear" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:nickel_gear", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/quartz_gear.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/quartz_gear.json deleted file mode 100644 index 8fa98d14..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/quartz_gear.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:gems/quartz" - }, - "count": 256 - } - ], - "mold": { - "item": "useless_mod:metal_mold_gear" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:quartz_gear", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/ruby_gear.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/ruby_gear.json deleted file mode 100644 index b8811097..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/ruby_gear.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:gems/ruby" - }, - "count": 256 - } - ], - "mold": { - "item": "useless_mod:metal_mold_gear" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:ruby_gear", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/sapphire_gear.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/sapphire_gear.json deleted file mode 100644 index 405ea052..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/sapphire_gear.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:gems/sapphire" - }, - "count": 256 - } - ], - "mold": { - "item": "useless_mod:metal_mold_gear" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:sapphire_gear", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/signalum_gear.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/signalum_gear.json deleted file mode 100644 index c4854b5c..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/signalum_gear.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:ingots/signalum" - }, - "count": 256 - } - ], - "mold": { - "item": "useless_mod:metal_mold_gear" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:signalum_gear", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/silver_gear.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/silver_gear.json deleted file mode 100644 index d8d95dd6..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/silver_gear.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:ingots/silver" - }, - "count": 256 - } - ], - "mold": { - "item": "useless_mod:metal_mold_gear" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:silver_gear", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/tin_gear.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/tin_gear.json deleted file mode 100644 index 5b619e66..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/gear/tin_gear.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:ingots/tin" - }, - "count": 256 - } - ], - "mold": { - "item": "useless_mod:metal_mold_gear" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:tin_gear", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/bronze_plate.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/bronze_plate.json deleted file mode 100644 index fbc47a9b..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/bronze_plate.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:ingots/bronze" - }, - "count": 64 - } - ], - "mold": { - "item": "useless_mod:metal_mold_plate" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:bronze_plate", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/constantan_plate.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/constantan_plate.json deleted file mode 100644 index 118031f6..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/constantan_plate.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:ingots/constantan" - }, - "count": 64 - } - ], - "mold": { - "item": "useless_mod:metal_mold_plate" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:constantan_plate", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/copper_plate.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/copper_plate.json deleted file mode 100644 index b7613f59..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/copper_plate.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:ingots/copper" - }, - "count": 64 - } - ], - "mold": { - "item": "useless_mod:metal_mold_plate" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:copper_plate", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/electrum_plate.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/electrum_plate.json deleted file mode 100644 index 8e42ef9d..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/electrum_plate.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:ingots/electrum" - }, - "count": 64 - } - ], - "mold": { - "item": "useless_mod:metal_mold_plate" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:electrum_plate", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/enderium_plate.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/enderium_plate.json deleted file mode 100644 index 7d9e9565..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/enderium_plate.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:ingots/enderium" - }, - "count": 64 - } - ], - "mold": { - "item": "useless_mod:metal_mold_plate" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:enderium_plate", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/gold_plate.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/gold_plate.json deleted file mode 100644 index 1b807ade..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/gold_plate.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:ingots/gold" - }, - "count": 64 - } - ], - "mold": { - "item": "useless_mod:metal_mold_plate" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:gold_plate", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/invar_plate.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/invar_plate.json deleted file mode 100644 index 86dff833..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/invar_plate.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:ingots/invar" - }, - "count": 64 - } - ], - "mold": { - "item": "useless_mod:metal_mold_plate" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:invar_plate", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/iron_plate.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/iron_plate.json deleted file mode 100644 index 68df08e6..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/iron_plate.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:ingots/iron" - }, - "count": 64 - } - ], - "mold": { - "item": "useless_mod:metal_mold_plate" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:iron_plate", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/lead_plate.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/lead_plate.json deleted file mode 100644 index cbde715f..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/lead_plate.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:ingots/lead" - }, - "count": 64 - } - ], - "mold": { - "item": "useless_mod:metal_mold_plate" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:lead_plate", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/lumium_plate.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/lumium_plate.json deleted file mode 100644 index 24f29579..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/lumium_plate.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:ingots/lumium" - }, - "count": 64 - } - ], - "mold": { - "item": "useless_mod:metal_mold_plate" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:lumium_plate", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/netherite_plate.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/netherite_plate.json deleted file mode 100644 index 5a978eab..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/netherite_plate.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:ingots/netherite" - }, - "count": 64 - } - ], - "mold": { - "item": "useless_mod:metal_mold_plate" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:netherite_plate", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/nickel_plate.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/nickel_plate.json deleted file mode 100644 index e6a89be5..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/nickel_plate.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:ingots/nickel" - }, - "count": 64 - } - ], - "mold": { - "item": "useless_mod:metal_mold_plate" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:nickel_plate", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/signalum_plate.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/signalum_plate.json deleted file mode 100644 index 182e65d0..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/signalum_plate.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:ingots/signalum" - }, - "count": 64 - } - ], - "mold": { - "item": "useless_mod:metal_mold_plate" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:signalum_plate", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/silver_plate.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/silver_plate.json deleted file mode 100644 index 78cb6cf3..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/silver_plate.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:ingots/silver" - }, - "count": 64 - } - ], - "mold": { - "item": "useless_mod:metal_mold_plate" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:silver_plate", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/tin_plate.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/tin_plate.json deleted file mode 100644 index a91b79c0..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/thermal/plate/tin_plate.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "forge:conditional", - "recipes": [ - { - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "thermal_foundation" - } - ], - "recipe": { - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "tag": "forge:ingots/tin" - }, - "count": 64 - } - ], - "mold": { - "item": "useless_mod:metal_mold_plate" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "thermal:tin_plate", - "count": 64 - } - ], - "energy": 64000, - "process_time": 40 - } - } - ] -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/gear/useless_gear_tier_1.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/gear/useless_gear_tier_1.json deleted file mode 100644 index 640da1ba..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/gear/useless_gear_tier_1.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "useless_mod:useless_ingot_tier_1" - }, - "count": 4 - } - ], - "mold": { - "item": "useless_mod:metal_mold_gear" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "useless_mod:useless_gear_tier_1", - "count": 1 - } - ], - "energy": 1000, - "process_time": 40 -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/gear/useless_gear_tier_2.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/gear/useless_gear_tier_2.json deleted file mode 100644 index ce969279..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/gear/useless_gear_tier_2.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "useless_mod:useless_ingot_tier_2" - }, - "count": 4 - } - ], - "mold": { - "item": "useless_mod:metal_mold_gear" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "useless_mod:useless_gear_tier_2", - "count": 1 - } - ], - "energy": 1000, - "process_time": 40 -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/gear/useless_gear_tier_3.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/gear/useless_gear_tier_3.json deleted file mode 100644 index a4535991..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/gear/useless_gear_tier_3.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "useless_mod:useless_ingot_tier_3" - }, - "count": 4 - } - ], - "mold": { - "item": "useless_mod:metal_mold_gear" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "useless_mod:useless_gear_tier_3", - "count": 1 - } - ], - "energy": 1000, - "process_time": 40 -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/gear/useless_gear_tier_4.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/gear/useless_gear_tier_4.json deleted file mode 100644 index 31e9ead1..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/gear/useless_gear_tier_4.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "useless_mod:useless_ingot_tier_4" - }, - "count": 4 - } - ], - "mold": { - "item": "useless_mod:metal_mold_gear" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "useless_mod:useless_gear_tier_4", - "count": 1 - } - ], - "energy": 1000, - "process_time": 40 -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/gear/useless_gear_tier_5.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/gear/useless_gear_tier_5.json deleted file mode 100644 index dbcc7dac..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/gear/useless_gear_tier_5.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "useless_mod:useless_ingot_tier_5" - }, - "count": 4 - } - ], - "mold": { - "item": "useless_mod:metal_mold_gear" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "useless_mod:useless_gear_tier_5", - "count": 1 - } - ], - "energy": 1000, - "process_time": 40 -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/gear/useless_gear_tier_6.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/gear/useless_gear_tier_6.json deleted file mode 100644 index 9bd6641e..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/gear/useless_gear_tier_6.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "useless_mod:useless_ingot_tier_6" - }, - "count": 4 - } - ], - "mold": { - "item": "useless_mod:metal_mold_gear" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "useless_mod:useless_gear_tier_6", - "count": 1 - } - ], - "energy": 1000, - "process_time": 40 -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/gear/useless_gear_tier_7.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/gear/useless_gear_tier_7.json deleted file mode 100644 index 539813ab..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/gear/useless_gear_tier_7.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "useless_mod:useless_ingot_tier_7" - }, - "count": 4 - } - ], - "mold": { - "item": "useless_mod:metal_mold_gear" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "useless_mod:useless_gear_tier_7", - "count": 1 - } - ], - "energy": 1000, - "process_time": 40 -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/gear/useless_gear_tier_8.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/gear/useless_gear_tier_8.json deleted file mode 100644 index 959dc238..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/gear/useless_gear_tier_8.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "useless_mod:useless_ingot_tier_8" - }, - "count": 4 - } - ], - "mold": { - "item": "useless_mod:metal_mold_gear" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "useless_mod:useless_gear_tier_8", - "count": 1 - } - ], - "energy": 1000, - "process_time": 40 -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/gear/useless_gear_tier_9.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/gear/useless_gear_tier_9.json deleted file mode 100644 index 6f2478c2..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/gear/useless_gear_tier_9.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "useless_mod:useless_ingot_tier_9" - }, - "count": 4 - } - ], - "mold": { - "item": "useless_mod:metal_mold_gear" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "useless_mod:useless_gear_tier_9", - "count": 1 - } - ], - "energy": 1000, - "process_time": 40 -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/glass/useless_glass_tier_1.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/glass/useless_glass_tier_1.json deleted file mode 100644 index 9bf5ce61..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/glass/useless_glass_tier_1.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "minecraft:glass" - }, - "count": 2 - }, - { - "ingredient": { - "item": "useless_mod:useless_ingot_tier_1" - }, - "count": 1 - } - ], - "mold": { - "item": "useless_mod:metal_mold_block" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "useless_mod:useless_glass_tier_1", - "count": 1 - } - ], - "energy": 1000, - "process_time": 40 -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/glass/useless_glass_tier_2.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/glass/useless_glass_tier_2.json deleted file mode 100644 index 7192ed4a..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/glass/useless_glass_tier_2.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "useless_mod:useless_glass_tier_1" - }, - "count": 2 - }, - { - "ingredient": { - "item": "useless_mod:useless_ingot_tier_2" - }, - "count": 1 - } - ], - "mold": { - "item": "useless_mod:metal_mold_block" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "useless_mod:useless_glass_tier_2", - "count": 1 - } - ], - "energy": 1000, - "process_time": 40 -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/glass/useless_glass_tier_3.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/glass/useless_glass_tier_3.json deleted file mode 100644 index 4d15313d..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/glass/useless_glass_tier_3.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "useless_mod:useless_glass_tier_2" - }, - "count": 2 - }, - { - "ingredient": { - "item": "useless_mod:useless_ingot_tier_3" - }, - "count": 1 - } - ], - "mold": { - "item": "useless_mod:metal_mold_block" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "useless_mod:useless_glass_tier_3", - "count": 1 - } - ], - "energy": 1000, - "process_time": 40 -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/glass/useless_glass_tier_4.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/glass/useless_glass_tier_4.json deleted file mode 100644 index 20bc99de..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/glass/useless_glass_tier_4.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "useless_mod:useless_glass_tier_3" - }, - "count": 2 - }, - { - "ingredient": { - "item": "useless_mod:useless_ingot_tier_4" - }, - "count": 1 - } - ], - "mold": { - "item": "useless_mod:metal_mold_block" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "useless_mod:useless_glass_tier_4", - "count": 1 - } - ], - "energy": 1000, - "process_time": 40 -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/glass/useless_glass_tier_5.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/glass/useless_glass_tier_5.json deleted file mode 100644 index e7c48d21..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/glass/useless_glass_tier_5.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "useless_mod:useless_glass_tier_4" - }, - "count": 2 - }, - { - "ingredient": { - "item": "useless_mod:useless_ingot_tier_5" - }, - "count": 1 - } - ], - "mold": { - "item": "useless_mod:metal_mold_block" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "useless_mod:useless_glass_tier_5", - "count": 1 - } - ], - "energy": 1000, - "process_time": 40 -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/glass/useless_glass_tier_6.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/glass/useless_glass_tier_6.json deleted file mode 100644 index 1d5d6f71..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/glass/useless_glass_tier_6.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "useless_mod:useless_glass_tier_5" - }, - "count": 2 - }, - { - "ingredient": { - "item": "useless_mod:useless_ingot_tier_6" - }, - "count": 1 - } - ], - "mold": { - "item": "useless_mod:metal_mold_block" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "useless_mod:useless_glass_tier_6", - "count": 1 - } - ], - "energy": 1000, - "process_time": 40 -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/glass/useless_glass_tier_7.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/glass/useless_glass_tier_7.json deleted file mode 100644 index ebc0a7a0..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/glass/useless_glass_tier_7.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "useless_mod:useless_glass_tier_6" - }, - "count": 2 - }, - { - "ingredient": { - "item": "useless_mod:useless_ingot_tier_7" - }, - "count": 1 - } - ], - "mold": { - "item": "useless_mod:metal_mold_block" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "useless_mod:useless_glass_tier_7", - "count": 1 - } - ], - "energy": 1000, - "process_time": 40 -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/glass/useless_glass_tier_8.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/glass/useless_glass_tier_8.json deleted file mode 100644 index e04636ec..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/glass/useless_glass_tier_8.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "useless_mod:useless_glass_tier_7" - }, - "count": 2 - }, - { - "ingredient": { - "item": "useless_mod:useless_ingot_tier_8" - }, - "count": 1 - } - ], - "mold": { - "item": "useless_mod:metal_mold_block" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "useless_mod:useless_glass_tier_8", - "count": 1 - } - ], - "energy": 1000, - "process_time": 40 -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/glass/useless_glass_tier_9.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/glass/useless_glass_tier_9.json deleted file mode 100644 index 2a61be35..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/glass/useless_glass_tier_9.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "useless_mod:useless_glass_tier_8" - }, - "count": 2 - }, - { - "ingredient": { - "item": "useless_mod:useless_ingot_tier_9" - }, - "count": 1 - } - ], - "mold": { - "item": "useless_mod:metal_mold_block" - }, - "catalyst": { - "tag": "useless_mod:useless_ingots", - "count": 1 - }, - "results": [ - { - "item": "useless_mod:useless_glass_tier_9", - "count": 1 - } - ], - "energy": 1000, - "process_time": 40 -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/ingot/possible_useful_ingot.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/ingot/possible_useful_ingot.json deleted file mode 100644 index cac49a5a..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/ingot/possible_useful_ingot.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "type": "minecraft:crafting_shaped", - "pattern": [ - "123", - "456", - "789" - ], - "key": { - "1": { - "item": "useless_mod:useless_ingot_tier_1" - }, - "2": { - "item": "useless_mod:useless_ingot_tier_2" - }, - "3": { - "item": "useless_mod:useless_ingot_tier_3" - }, - "4": { - "item": "useless_mod:useless_ingot_tier_4" - }, - "5": { - "item": "useless_mod:useless_ingot_tier_5" - }, - "6": { - "item": "useless_mod:useless_ingot_tier_6" - }, - "7": { - "item": "useless_mod:useless_ingot_tier_7" - }, - "8": { - "item": "useless_mod:useless_ingot_tier_8" - }, - "9": { - "item": "useless_mod:useless_ingot_tier_9" - } - }, - "result": { - "item": "useless_mod:possible_useful_ingot", - "count": 1 - }, - "group": "useless_ingots" -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/ingot/useful_ingot.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/ingot/useful_ingot.json deleted file mode 100644 index 3ece3be5..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/ingot/useful_ingot.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "useless_mod:possible_useful_ingot" - }, - "count": 64 - }, - { - "ingredient": { - "item": "minecraft:iron_block" - }, - "count": 1024 - }, - { - "ingredient": { - "item": "minecraft:gold_block" - } - ,"count": 1024 - }, - { - "ingredient": { - "item": "minecraft:diamond_block" - }, - "count": 1024 - }, - { - "ingredient": { - "item": "minecraft:emerald_block" - }, - "count": 1024 - }, - { - "ingredient": { - "item": "minecraft:netherite_block" - }, - "count": 1024 - } - ], - "input_fluid": - { - "fluid": "minecraft:lava", - "amount": 10000000 - }, - "results": [ - { - "item": "useless_mod:useful_ingot", - "count": 1 - } - ], - "energy": 10000000, - "process_time": 600 -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/ingot/useless_ingot_tier_1.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/ingot/useless_ingot_tier_1.json deleted file mode 100644 index c8f7a9f4..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/ingot/useless_ingot_tier_1.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "minecraft:mutton" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:beef" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:chicken" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:iron_ingot" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:gold_ingot" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:diamond" - }, - "count": 2 - } - ], - "results": [ - { - "item": "useless_mod:useless_ingot_tier_1", - "count": 1 - } - ], - "energy": 10000, - "process_time": 1 -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/ingot/useless_ingot_tier_2.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/ingot/useless_ingot_tier_2.json deleted file mode 100644 index 50c7f0fa..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/ingot/useless_ingot_tier_2.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "minecraft:rotten_flesh" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:gunpowder" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:bone" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:gold_ingot" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:diamond" - }, - "count": 2 - } - ], - "catalyst": { - "item": "useless_mod:useless_ingot_tier_1", - "count": 1 - }, - "results": [ - { - "item": "useless_mod:useless_ingot_tier_2", - "count": 1 - } - ], - "energy": 50000, - "process_time": 1 -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/ingot/useless_ingot_tier_3.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/ingot/useless_ingot_tier_3.json deleted file mode 100644 index 76c1a9d5..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/ingot/useless_ingot_tier_3.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "minecraft:nether_wart" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:blaze_rod" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:nether_brick" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:gold_ingot" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:diamond" - }, - "count": 2 - } - ], - "catalyst": { - "item": "useless_mod:useless_ingot_tier_2", - "count": 1 - }, - "results": [ - { - "item": "useless_mod:useless_ingot_tier_3", - "count": 1 - } - ], - "energy": 100000, - "process_time": 1 -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/ingot/useless_ingot_tier_4.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/ingot/useless_ingot_tier_4.json deleted file mode 100644 index f1729336..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/ingot/useless_ingot_tier_4.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "minecraft:ender_pearl" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:chorus_fruit" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:shulker_shell" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:gold_block" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:diamond_block" - }, - "count": 2 - } - ], - "catalyst": { - "item": "useless_mod:useless_ingot_tier_3", - "count": 1 - }, - "results": [ - { - "item": "useless_mod:useless_ingot_tier_4", - "count": 1 - } - ], - "energy": 200000, - "process_time": 1 -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/ingot/useless_ingot_tier_5.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/ingot/useless_ingot_tier_5.json deleted file mode 100644 index 3ecf7465..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/ingot/useless_ingot_tier_5.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "minecraft:wither_skeleton_skull" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:phantom_membrane" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:sea_lantern" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:gold_block" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:diamond_block" - }, - "count": 2 - } - ], - "catalyst": { - "item": "useless_mod:useless_ingot_tier_4", - "count": 1 - }, - "results": [ - { - "item": "useless_mod:useless_ingot_tier_5", - "count": 1 - } - ], - "energy": 500000, - "process_time": 1 -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/ingot/useless_ingot_tier_6.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/ingot/useless_ingot_tier_6.json deleted file mode 100644 index 7b569180..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/ingot/useless_ingot_tier_6.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "minecraft:dragon_breath" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:totem_of_undying" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:enchanted_golden_apple" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:gold_block" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:diamond_block" - }, - "count": 2 - } - ], - "catalyst": { - "item": "useless_mod:useless_ingot_tier_5", - "count": 1 - }, - "results": [ - { - "item": "useless_mod:useless_ingot_tier_6", - "count": 1 - } - ], - "energy": 2000000, - "process_time": 1 -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/ingot/useless_ingot_tier_7.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/ingot/useless_ingot_tier_7.json deleted file mode 100644 index be6d574e..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/ingot/useless_ingot_tier_7.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "minecraft:nether_star" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:wither_rose" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:ghast_tear" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:diamond_block" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:netherite_ingot" - }, - "count": 2 - } - ], - "catalyst": { - "item": "useless_mod:useless_ingot_tier_6", - "count": 1 - }, - "results": [ - { - "item": "useless_mod:useless_ingot_tier_7", - "count": 1 - } - ], - "energy": 4000000, - "process_time": 1 -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/ingot/useless_ingot_tier_8.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/ingot/useless_ingot_tier_8.json deleted file mode 100644 index 513f1201..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/ingot/useless_ingot_tier_8.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "minecraft:dragon_head" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:echo_shard" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:conduit" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:diamond_block" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:netherite_ingot" - }, - "count": 2 - } - ], - "catalyst": { - "item": "useless_mod:useless_ingot_tier_7", - "count": 1 - }, - "results": [ - { - "item": "useless_mod:useless_ingot_tier_8", - "count": 1 - } - ], - "energy": 8000000, - "process_time": 1 -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/ingot/useless_ingot_tier_9.json b/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/ingot/useless_ingot_tier_9.json deleted file mode 100644 index 222eb526..00000000 --- a/src/main/resources/data/useless_mod/recipes/machine/smelter/useless/ingot/useless_ingot_tier_9.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "type": "useless_mod:advanced_alloy_furnace", - "ingredients": [ - { - "ingredient": { - "item": "minecraft:turtle_helmet" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:goat_horn" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:rabbit_foot" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:diamond_block" - }, - "count": 2 - }, - { - "ingredient": { - "item": "minecraft:netherite_block" - }, - "count": 2 - } - ], - "catalyst": { - "item": "useless_mod:useless_ingot_tier_8", - "count": 1 - }, - "results": [ - { - "item": "useless_mod:useless_ingot_tier_9", - "count": 1 - } - ], - "energy": 16000000, - "process_time": 1 -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/mold/metal_mold_block.json b/src/main/resources/data/useless_mod/recipes/mold/metal_mold_block.json deleted file mode 100644 index e184dc0e..00000000 --- a/src/main/resources/data/useless_mod/recipes/mold/metal_mold_block.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "type": "minecraft:crafting_shaped", - "pattern": [ - "XXX", - "X X", - "XXX" - ], - "key": { - "X": { - "item": "useless_mod:useless_ingot_tier_1" - } - }, - "result": { - "item": "useless_mod:metal_mold_block", - "count": 1 - }, - "group": "metal_molds" -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/mold/metal_mold_gear.json b/src/main/resources/data/useless_mod/recipes/mold/metal_mold_gear.json deleted file mode 100644 index 6345c1bc..00000000 --- a/src/main/resources/data/useless_mod/recipes/mold/metal_mold_gear.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "type": "minecraft:crafting_shaped", - "pattern": [ - " X ", - "X X", - " X " - ], - "key": { - "X": { - "item": "useless_mod:useless_ingot_tier_1" - } - }, - "result": { - "item": "useless_mod:metal_mold_gear", - "count": 1 - }, - "group": "metal_molds" -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/mold/metal_mold_plate.json b/src/main/resources/data/useless_mod/recipes/mold/metal_mold_plate.json deleted file mode 100644 index 0180a541..00000000 --- a/src/main/resources/data/useless_mod/recipes/mold/metal_mold_plate.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "type": "minecraft:crafting_shaped", - "pattern": [ - "XXX" - ], - "key": { - "X": { - "item": "useless_mod:useless_ingot_tier_1" - } - }, - "result": { - "item": "useless_mod:metal_mold_plate", - "count": 1 - }, - "group": "metal_molds" -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/mold/metal_mold_rod.json b/src/main/resources/data/useless_mod/recipes/mold/metal_mold_rod.json deleted file mode 100644 index bf7ce4de..00000000 --- a/src/main/resources/data/useless_mod/recipes/mold/metal_mold_rod.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "type": "minecraft:crafting_shaped", - "pattern": [ - "X", - "X", - "X" - ], - "key": { - "X": { - "item": "useless_mod:useless_ingot_tier_1" - } - }, - "result": { - "item": "useless_mod:metal_mold_rod", - "count": 1 - }, - "group": "metal_molds" -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/mold/metal_mold_wire.json b/src/main/resources/data/useless_mod/recipes/mold/metal_mold_wire.json deleted file mode 100644 index 7698e006..00000000 --- a/src/main/resources/data/useless_mod/recipes/mold/metal_mold_wire.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "type": "minecraft:crafting_shaped", - "pattern": [ - " X ", - "XXX", - " X " - ], - "key": { - "X": { - "item": "useless_mod:useless_ingot_tier_1" - } - }, - "result": { - "item": "useless_mod:metal_mold_wire", - "count": 1 - }, - "group": "metal_molds" -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/ore_generator_block.json b/src/main/resources/data/useless_mod/recipes/ore_generator_block.json deleted file mode 100644 index 546802cd..00000000 --- a/src/main/resources/data/useless_mod/recipes/ore_generator_block.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "type": "minecraft:crafting_shaped", - "pattern": [ - "AAA", - "AAA", - "AAA" - ], - "key": { - "A": { - "item": "useless_mod:useful_ingot" - } - }, - "result": { - "item": "useless_mod:ore_generator_block", - "count": 1 - } -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/ritual_blueprint.json b/src/main/resources/data/useless_mod/recipes/ritual_blueprint.json new file mode 100644 index 00000000..92ae6238 --- /dev/null +++ b/src/main/resources/data/useless_mod/recipes/ritual_blueprint.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:crafting_shapeless", + "conditions": [ + { + "type": "forge:mod_loaded", + "modid": "occultism" + } + ], + "ingredients": [ + { + "item": "minecraft:paper" + }, + { + "item": "occultism:otherworld_essence" + } + ], + "result": { + "count": 1, + "item": "useless_mod:ritual_blueprint" + } +} diff --git a/src/main/resources/data/useless_mod/recipes/teleport_block.json b/src/main/resources/data/useless_mod/recipes/teleport_block.json deleted file mode 100644 index a865b7f8..00000000 --- a/src/main/resources/data/useless_mod/recipes/teleport_block.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "type": "minecraft:crafting_shaped", - "pattern": [ - "AAA", - "ABA", - "AAA" - ], - "key": { - "A": { - "tag": "minecraft:planks" - }, - "B": { - "item": "minecraft:dirt" - } - }, - "result": { - "item": "useless_mod:teleport_block", - "count": 1 - } -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/teleport_block_2.json b/src/main/resources/data/useless_mod/recipes/teleport_block_2.json deleted file mode 100644 index 2746f958..00000000 --- a/src/main/resources/data/useless_mod/recipes/teleport_block_2.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "type": "minecraft:crafting_shaped", - "pattern": [ - "BBB", - "BAB", - "BBB" - ], - "key": { - "A": { - "tag": "minecraft:planks" - }, - "B": { - "item": "minecraft:dirt" - } - }, - "result": { - "item": "useless_mod:teleport_block_2", - "count": 1 - } -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/teleport_block_3.json b/src/main/resources/data/useless_mod/recipes/teleport_block_3.json deleted file mode 100644 index 713bcf7a..00000000 --- a/src/main/resources/data/useless_mod/recipes/teleport_block_3.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "type": "minecraft:crafting_shaped", - "pattern": [ - "BBB", - "AAA", - "BBB" - ], - "key": { - "B": { - "tag": "minecraft:planks" - }, - "A": { - "item": "minecraft:dirt" - } - }, - "result": { - "item": "useless_mod:teleport_block_3", - "count": 1 - } -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/useless_integral_component_tier_1.json b/src/main/resources/data/useless_mod/recipes/useless_integral_component_tier_1.json deleted file mode 100644 index 4911a1d2..00000000 --- a/src/main/resources/data/useless_mod/recipes/useless_integral_component_tier_1.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "type": "minecraft:crafting_shaped", - "pattern": [ - "#O#", - "GCG", - "#O#" - ], - "key": { - "#": { - "item": "useless_mod:useless_ingot_tier_1" - }, - "O": { - "item": "useless_mod:useless_glass_tier_1" - }, - "G": { - "item": "useless_mod:useless_gear_tier_1" - }, - "C": { - "item": "thermal:upgrade_augment_3" - } - }, - "result": { - "item": "useless_mod:useless_integral_component_tier_1", - "count": 1 - } -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/useless_integral_component_tier_2.json b/src/main/resources/data/useless_mod/recipes/useless_integral_component_tier_2.json deleted file mode 100644 index fe8c1b17..00000000 --- a/src/main/resources/data/useless_mod/recipes/useless_integral_component_tier_2.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "type": "minecraft:crafting_shaped", - "pattern": [ - "#O#", - "GCG", - "#O#" - ], - "key": { - "#": { - "item": "useless_mod:useless_ingot_tier_2" - }, - "O": { - "item": "useless_mod:useless_glass_tier_2" - }, - "G": { - "item": "useless_mod:useless_gear_tier_2" - }, - "C": { - "item": "useless_mod:useless_integral_component_tier_1" - } - }, - "result": { - "item": "useless_mod:useless_integral_component_tier_2", - "count": 1 - } -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/useless_integral_component_tier_3.json b/src/main/resources/data/useless_mod/recipes/useless_integral_component_tier_3.json deleted file mode 100644 index de7a790b..00000000 --- a/src/main/resources/data/useless_mod/recipes/useless_integral_component_tier_3.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "type": "minecraft:crafting_shaped", - "pattern": [ - "#O#", - "GCG", - "#O#" - ], - "key": { - "#": { - "item": "useless_mod:useless_ingot_tier_3" - }, - "O": { - "item": "useless_mod:useless_glass_tier_3" - }, - "G": { - "item": "useless_mod:useless_gear_tier_3" - }, - "C": { - "item": "useless_mod:useless_integral_component_tier_2" - } - }, - "result": { - "item": "useless_mod:useless_integral_component_tier_3", - "count": 1 - } -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/useless_integral_component_tier_4.json b/src/main/resources/data/useless_mod/recipes/useless_integral_component_tier_4.json deleted file mode 100644 index a685a4c8..00000000 --- a/src/main/resources/data/useless_mod/recipes/useless_integral_component_tier_4.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "type": "minecraft:crafting_shaped", - "pattern": [ - "#O#", - "GCG", - "#O#" - ], - "key": { - "#": { - "item": "useless_mod:useless_ingot_tier_4" - }, - "O": { - "item": "useless_mod:useless_glass_tier_4" - }, - "G": { - "item": "useless_mod:useless_gear_tier_4" - }, - "C": { - "item": "useless_mod:useless_integral_component_tier_3" - } - }, - "result": { - "item": "useless_mod:useless_integral_component_tier_4", - "count": 1 - } -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/useless_integral_component_tier_5.json b/src/main/resources/data/useless_mod/recipes/useless_integral_component_tier_5.json deleted file mode 100644 index 25312299..00000000 --- a/src/main/resources/data/useless_mod/recipes/useless_integral_component_tier_5.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "type": "minecraft:crafting_shaped", - "pattern": [ - "#O#", - "GCG", - "#O#" - ], - "key": { - "#": { - "item": "useless_mod:useless_ingot_tier_5" - }, - "O": { - "item": "useless_mod:useless_glass_tier_5" - }, - "G": { - "item": "useless_mod:useless_gear_tier_5" - }, - "C": { - "item": "useless_mod:useless_integral_component_tier_4" - } - }, - "result": { - "item": "useless_mod:useless_integral_component_tier_5", - "count": 1 - } -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/useless_integral_component_tier_6.json b/src/main/resources/data/useless_mod/recipes/useless_integral_component_tier_6.json deleted file mode 100644 index a43063dd..00000000 --- a/src/main/resources/data/useless_mod/recipes/useless_integral_component_tier_6.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "type": "minecraft:crafting_shaped", - "pattern": [ - "#O#", - "GCG", - "#O#" - ], - "key": { - "#": { - "item": "useless_mod:useless_ingot_tier_6" - }, - "O": { - "item": "useless_mod:useless_glass_tier_6" - }, - "G": { - "item": "useless_mod:useless_gear_tier_6" - }, - "C": { - "item": "useless_mod:useless_integral_component_tier_5" - } - }, - "result": { - "item": "useless_mod:useless_integral_component_tier_6", - "count": 1 - } -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/useless_integral_component_tier_7.json b/src/main/resources/data/useless_mod/recipes/useless_integral_component_tier_7.json deleted file mode 100644 index ecf181fb..00000000 --- a/src/main/resources/data/useless_mod/recipes/useless_integral_component_tier_7.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "type": "minecraft:crafting_shaped", - "pattern": [ - "#O#", - "GCG", - "#O#" - ], - "key": { - "#": { - "item": "useless_mod:useless_ingot_tier_7" - }, - "O": { - "item": "useless_mod:useless_glass_tier_7" - }, - "G": { - "item": "useless_mod:useless_gear_tier_7" - }, - "C": { - "item": "useless_mod:useless_integral_component_tier_6" - } - }, - "result": { - "item": "useless_mod:useless_integral_component_tier_7", - "count": 1 - } -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/useless_integral_component_tier_8.json b/src/main/resources/data/useless_mod/recipes/useless_integral_component_tier_8.json deleted file mode 100644 index 6d36c44f..00000000 --- a/src/main/resources/data/useless_mod/recipes/useless_integral_component_tier_8.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "type": "minecraft:crafting_shaped", - "pattern": [ - "#O#", - "GCG", - "#O#" - ], - "key": { - "#": { - "item": "useless_mod:useless_ingot_tier_8" - }, - "O": { - "item": "useless_mod:useless_glass_tier_8" - }, - "G": { - "item": "useless_mod:useless_gear_tier_8" - }, - "C": { - "item": "useless_mod:useless_integral_component_tier_7" - } - }, - "result": { - "item": "useless_mod:useless_integral_component_tier_8", - "count": 1 - } -} \ No newline at end of file diff --git a/src/main/resources/data/useless_mod/recipes/useless_integral_component_tier_9.json b/src/main/resources/data/useless_mod/recipes/useless_integral_component_tier_9.json deleted file mode 100644 index 1519c236..00000000 --- a/src/main/resources/data/useless_mod/recipes/useless_integral_component_tier_9.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "type": "minecraft:crafting_shaped", - "pattern": [ - "#O#", - "GCG", - "#O#" - ], - "key": { - "#": { - "item": "useless_mod:useless_ingot_tier_9" - }, - "O": { - "item": "useless_mod:useless_glass_tier_9" - }, - "G": { - "item": "useless_mod:useless_gear_tier_9" - }, - "C": { - "item": "useless_mod:useless_integral_component_tier_8" - } - }, - "result": { - "item": "useless_mod:useless_integral_component_tier_9", - "count": 1 - } -} \ No newline at end of file diff --git a/src/main/resources/god/assets/useless_mod/models/item/endless_beaf_crowbar.json b/src/main/resources/god/assets/useless_mod/models/item/endless_beaf_crowbar.json deleted file mode 100644 index 4e97a9ce..00000000 --- a/src/main/resources/god/assets/useless_mod/models/item/endless_beaf_crowbar.json +++ /dev/null @@ -1,880 +0,0 @@ -{ - "render_type":"cutout", - "format_version": "1.9.0", - "credit": "Made with _leng", - "texture_size": [32, 32], - "textures": { - "0": "useless_mod:item/endless_beaf_item", - "1": "useless_mod:item/endless_beaf_item_wing", - "2": "useless_mod:item/lable_wireless_transceiver_lighting", - "3": "useless_mod:item/endless_special_efficacy" - }, - "elements": [ - { - "name": "Octagon", - "from": [7.79289, -12.5001, 7.5], - "to": [8.20711, 18.0001, 8.5], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 8, 8]}, - "faces": { - "north": {"uv": [4, 0, 4.125, 7.625], "texture": "#0"}, - "east": {"uv": [0, 0, 0.25, 7.625], "texture": "#0"}, - "south": {"uv": [4.5, 0, 4.625, 7.625], "texture": "#0"}, - "west": {"uv": [0.5, 0, 0.75, 7.625], "texture": "#0"}, - "up": {"uv": [10.875, 8.25, 10.75, 8], "texture": "#0"}, - "down": {"uv": [10.875, 8.5, 10.75, 8.75], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [7.79289, -12.5003, 7.5], - "to": [8.20711, 18.0003, 8.5], - "faces": { - "north": {"uv": [5, 0, 5.125, 7.625], "texture": "#0"}, - "east": {"uv": [1, 0, 1.25, 7.625], "texture": "#0"}, - "south": {"uv": [5.5, 0, 5.625, 7.625], "texture": "#0"}, - "west": {"uv": [1.5, 0, 1.75, 7.625], "texture": "#0"}, - "up": {"uv": [10.875, 9.25, 10.75, 9], "texture": "#0"}, - "down": {"uv": [10.875, 9.5, 10.75, 9.75], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [7.5, -12.5, 7.79289], - "to": [8.5, 18, 8.20711], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 8, 8]}, - "faces": { - "north": {"uv": [2, 0, 2.25, 7.625], "texture": "#0"}, - "east": {"uv": [6, 0, 6.125, 7.625], "texture": "#0"}, - "south": {"uv": [2.5, 0, 2.75, 7.625], "texture": "#0"}, - "west": {"uv": [6.5, 0, 6.625, 7.625], "texture": "#0"}, - "up": {"uv": [11, 10.125, 10.75, 10], "texture": "#0"}, - "down": {"uv": [11, 10.5, 10.75, 10.625], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [7.5, -12.5002, 7.79289], - "to": [8.5, 18.0002, 8.20711], - "faces": { - "north": {"uv": [3, 0, 3.25, 7.625], "texture": "#0"}, - "east": {"uv": [7, 0, 7.125, 7.625], "texture": "#0"}, - "south": {"uv": [3.5, 0, 3.75, 7.625], "texture": "#0"}, - "west": {"uv": [7.5, 0, 7.625, 7.625], "texture": "#0"}, - "up": {"uv": [11.25, 0.125, 11, 0], "texture": "#0"}, - "down": {"uv": [11.25, 0.5, 11, 0.625], "texture": "#0"} - } - }, - { - "name": "duzuo", - "from": [6.869, -14.12, 8], - "to": [9.131, -10.118, 8], - "faces": { - "north": {"uv": [0, 9, 0.625, 10], "texture": "#0"}, - "east": {"uv": [10.25, 6, 10.375, 7], "texture": "#0"}, - "south": {"uv": [0.625, 9, 1.25, 10], "texture": "#0"}, - "west": {"uv": [10.25, 7.25, 10.375, 8.25], "texture": "#0"}, - "up": {"uv": [8, 10.625, 7.5, 10.5], "texture": "#0"}, - "down": {"uv": [8.75, 10.5, 8.25, 10.625], "texture": "#0"} - } - }, - { - "from": [7.4, -11.3, 7.4], - "to": [8.6, -10.1, 8.6], - "rotation": {"angle": -45, "axis": "z", "origin": [8, -10.7, 8]}, - "faces": { - "north": {"uv": [0, 10.75, 0.25, 11], "texture": "#0"}, - "east": {"uv": [0.5, 10.75, 0.75, 11], "texture": "#0"}, - "south": {"uv": [10.75, 1.5, 11, 1.75], "texture": "#0"}, - "west": {"uv": [10.75, 2, 11, 2.25], "texture": "#0"}, - "up": {"uv": [11, 4.75, 10.75, 4.5], "texture": "#0"}, - "down": {"uv": [11, 5, 10.75, 5.25], "texture": "#0"} - } - }, - { - "from": [7.8, -10.9, 7.3], - "to": [8.2, -10.5, 8.7], - "rotation": {"angle": -45, "axis": "z", "origin": [8, -10.7, 8]}, - "faces": { - "north": {"uv": [0.25, 0.75, 0.375, 0.875], "texture": "#0"}, - "east": {"uv": [0.25, 0, 0.5, 0.125], "texture": "#0"}, - "south": {"uv": [0.75, 0.25, 0.875, 0.375], "texture": "#0"}, - "west": {"uv": [0.25, 0.25, 0.5, 0.375], "texture": "#0"}, - "up": {"uv": [0.375, 0.75, 0.25, 0.5], "texture": "#0"}, - "down": {"uv": [0.875, 0, 0.75, 0.25], "texture": "#0"} - } - }, - { - "from": [7.8, 15.12, 7.3], - "to": [8.2, 15.52, 8.7], - "rotation": {"angle": -45, "axis": "z", "origin": [8, 15.32, 8]}, - "faces": { - "north": {"uv": [0.25, 0.75, 0.375, 0.875], "texture": "#0"}, - "east": {"uv": [0.25, 0, 0.5, 0.125], "texture": "#0"}, - "south": {"uv": [0.75, 0.25, 0.875, 0.375], "texture": "#0"}, - "west": {"uv": [0.25, 0.25, 0.5, 0.375], "texture": "#0"}, - "up": {"uv": [0.375, 0.75, 0.25, 0.5], "texture": "#0"}, - "down": {"uv": [0.875, 0, 0.75, 0.25], "texture": "#0"} - } - }, - { - "from": [7.39, -10.31, 7.39], - "to": [8.61, -9.84, 8.61], - "faces": { - "north": {"uv": [11, 1, 11.25, 1.125], "texture": "#0"}, - "east": {"uv": [1.5, 11, 1.75, 11.125], "texture": "#0"}, - "south": {"uv": [2, 11, 2.25, 11.125], "texture": "#0"}, - "west": {"uv": [2.5, 11, 2.75, 11.125], "texture": "#0"}, - "up": {"uv": [5.75, 11, 5.5, 10.75], "texture": "#0"}, - "down": {"uv": [6.25, 10.75, 6, 11], "texture": "#0"} - } - }, - { - "from": [7.39, 15.09, 7.39], - "to": [8.61, 15.56, 8.61], - "faces": { - "north": {"uv": [11, 2.5, 11.25, 2.625], "texture": "#0"}, - "east": {"uv": [3, 11, 3.25, 11.125], "texture": "#0"}, - "south": {"uv": [11, 3, 11.25, 3.125], "texture": "#0"}, - "west": {"uv": [3.5, 11, 3.75, 11.125], "texture": "#0"}, - "up": {"uv": [11, 6.25, 10.75, 6], "texture": "#0"}, - "down": {"uv": [6.75, 10.75, 6.5, 11], "texture": "#0"} - } - }, - { - "from": [7.39, 15.99, 7.39], - "to": [8.61, 17.71, 8.61], - "faces": { - "north": {"uv": [10.25, 8.5, 10.5, 8.875], "texture": "#0"}, - "east": {"uv": [10.25, 9.25, 10.5, 9.625], "texture": "#0"}, - "south": {"uv": [10.5, 0, 10.75, 0.375], "texture": "#0"}, - "west": {"uv": [10.5, 0.75, 10.75, 1.125], "texture": "#0"}, - "up": {"uv": [11, 6.75, 10.75, 6.5], "texture": "#0"}, - "down": {"uv": [7.25, 10.75, 7, 11], "texture": "#0"} - } - }, - { - "from": [6.65, 17.9, 6.68], - "to": [9.15, 20.4, 9.315], - "rotation": {"angle": -45, "axis": "z", "origin": [7.6, 18.9, 8.4]}, - "faces": { - "north": {"uv": [9.25, 1.5, 9.875, 2.125], "texture": "#0"}, - "east": {"uv": [9.25, 2.5, 9.875, 3.125], "texture": "#0"}, - "south": {"uv": [9.25, 6, 9.875, 6.625], "texture": "#0"}, - "west": {"uv": [9.25, 7, 9.875, 7.625], "texture": "#0"}, - "up": {"uv": [9.875, 8.625, 9.25, 8], "texture": "#0"}, - "down": {"uv": [9.875, 9, 9.25, 9.625], "texture": "#0"} - } - }, - { - "from": [7.8, 18.05, 6.48], - "to": [8.6, 18.85, 9.515], - "rotation": {"angle": -45, "axis": "z", "origin": [8.6, 18.9, 8.4]}, - "faces": { - "north": {"uv": [10.75, 7, 11, 7.25], "texture": "#0"}, - "east": {"uv": [10, 5.5, 10.75, 5.75], "texture": "#0"}, - "south": {"uv": [10.75, 7.5, 11, 7.75], "texture": "#0"}, - "west": {"uv": [0, 10.25, 0.75, 10.5], "texture": "#0"}, - "up": {"uv": [1.25, 11, 1, 10.25], "texture": "#0"}, - "down": {"uv": [10.5, 1.5, 10.25, 2.25], "texture": "#0"} - } - }, - { - "from": [8, 16.8, 7.6], - "to": [8, 17.8, 9.6], - "rotation": {"angle": -45, "axis": "x", "origin": [7.9, 17.8, 8.6]}, - "faces": { - "north": {"uv": [4, 11, 3.875, 11.25], "texture": "#0"}, - "east": {"uv": [10.25, 2.5, 10.75, 2.75], "texture": "#0"}, - "south": {"uv": [5.125, 11.25, 5.25, 11.5], "texture": "#0"}, - "west": {"uv": [10.25, 3, 10.75, 3.25], "texture": "#0"}, - "up": {"uv": [5.375, 11.75, 5.5, 11.25], "texture": "#0"}, - "down": {"uv": [5.625, 11.25, 5.75, 11.75], "texture": "#0"} - } - }, - { - "from": [8, 16.8, 6.4], - "to": [8, 17.8, 8.4], - "rotation": {"angle": 45, "axis": "x", "origin": [7.9, 17.8, 7.4]}, - "faces": { - "north": {"uv": [7.625, 10.875, 8, 11.375], "texture": "#0"}, - "east": {"uv": [7.5, 9, 8, 9.25], "texture": "#0"}, - "south": {"uv": [7.9, 7.5, 8.125, 7.75], "texture": "#0"}, - "west": {"uv": [9.25, 3.5, 9.75, 3.75], "texture": "#0"}, - "up": {"uv": [8.875, 11, 9.125, 10.5], "texture": "#0"}, - "down": {"uv": [10.875, 3.5, 11.125, 4], "texture": "#0"} - } - }, - { - "from": [8.10331, 19.03251, 6.9], - "to": [10.78331, 20.70751, 9.1], - "rotation": {"angle": 22.5, "axis": "z", "origin": [8.77331, 19.70251, 7]}, - "faces": { - "north": {"uv": [0, 8, 1, 8.625], "texture": "#0"}, - "east": {"uv": [7.5, 9.5, 8.125, 10.125], "texture": "#0"}, - "south": {"uv": [8, 0, 9, 0.625], "texture": "#0"}, - "west": {"uv": [1.5, 10, 2.125, 10.625], "texture": "#0"}, - "up": {"uv": [9, 1.625, 8, 1], "texture": "#0"}, - "down": {"uv": [2.25, 8, 1.25, 8.625], "texture": "#0"} - } - }, - { - "from": [5.21669, 19.03251, 6.9], - "to": [7.89669, 20.70751, 9.1], - "rotation": {"angle": -22.5, "axis": "z", "origin": [7.22669, 19.70251, 7]}, - "faces": { - "north": {"uv": [8, 2, 9, 2.625], "texture": "#0"}, - "east": {"uv": [2.5, 10, 3.125, 10.625], "texture": "#0"}, - "south": {"uv": [2.5, 8, 3.5, 8.625], "texture": "#0"}, - "west": {"uv": [3.5, 10, 4.125, 10.625], "texture": "#0"}, - "up": {"uv": [9, 3.625, 8, 3], "texture": "#0"}, - "down": {"uv": [4.75, 8, 3.75, 8.625], "texture": "#0"} - } - }, - { - "from": [6.48891, 21.54663, 6.99], - "to": [8.98891, 24.04663, 8.99], - "rotation": {"angle": -45, "axis": "z", "origin": [7.98891, 22.54663, 7.99]}, - "faces": { - "north": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "east": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "south": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "west": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "up": {"uv": [16, 16, 0, 0], "texture": "#2"}, - "down": {"uv": [16, 0, 0, 16], "texture": "#2"} - } - }, - { - "from": [6.48891, 19.04663, 6.99], - "to": [8.98891, 21.54663, 8.99], - "rotation": {"angle": -45, "axis": "z", "origin": [7.98891, 20.04663, 7.991]}, - "faces": { - "north": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "east": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "south": {"uv": [0, 0, 15.9, 16], "texture": "#2"}, - "west": {"uv": [0, 0, 15.9, 15.8], "texture": "#2"}, - "up": {"uv": [15.9, 15.8, 0, 0], "texture": "#2"}, - "down": {"uv": [16, 0, 0, 15.8], "texture": "#2"} - } - }, - { - "from": [6.22114, 20.40018, 6.989], - "to": [9.75668, 22.90018, 8.991], - "faces": { - "north": {"uv": [0, 0, 15.975, 16.025], "texture": "#2"}, - "east": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "south": {"uv": [0, 0, 15.975, 16], "texture": "#2"}, - "west": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "up": {"uv": [15.975, 16, 0, 0], "texture": "#2"}, - "down": {"uv": [16, 0, 0, 16], "texture": "#2"} - } - }, - { - "from": [7.17754, 24.8802, 7.99], - "to": [8.80028, 27.1202, 7.99], - "faces": { - "north": {"uv": [8.25, 7, 8.875, 7.875], "texture": "#0"}, - "east": {"uv": [5, 11, 5.225, 12.1375], "texture": "#0"}, - "south": {"uv": [8.25, 8.25, 8.875, 9.125], "texture": "#0"}, - "west": {"uv": [5.25, 11, 5.475, 12.1375], "texture": "#0"}, - "up": {"uv": [11.85, 4.25, 11, 4.5125], "texture": "#0"}, - "down": {"uv": [11.85, 5.5, 11, 5.7625], "texture": "#0"} - } - }, - { - "name": "cube_outline", - "from": [9.08891, 24.14663, 9.09], - "to": [6.38891, 21.44663, 6.89], - "rotation": {"angle": -45, "axis": "z", "origin": [7.98891, 22.54663, 7.99]}, - "faces": { - "north": {"uv": [0.75, 11.125, 0.875, 11.25], "rotation": 180, "texture": "#0"}, - "east": {"uv": [0.875, 11.125, 1, 11.25], "rotation": 180, "texture": "#0"}, - "south": {"uv": [1, 11.125, 1.125, 11.25], "rotation": 180, "texture": "#0"}, - "west": {"uv": [1.125, 11.125, 1.25, 11.25], "rotation": 180, "texture": "#0"}, - "up": {"uv": [1.375, 11.125, 1.25, 11.25], "rotation": 180, "texture": "#0"}, - "down": {"uv": [1.5, 11.25, 1.375, 11.125], "rotation": 180, "texture": "#0"} - } - }, - { - "name": "cube_outline", - "from": [9.85668, 23.00018, 9.091], - "to": [6.12114, 20.30018, 6.889], - "faces": { - "north": {"uv": [0, 11.125, 0.125, 11.25], "rotation": 180, "texture": "#0"}, - "east": {"uv": [0.125, 11.125, 0.25, 11.25], "rotation": 180, "texture": "#0"}, - "south": {"uv": [0.25, 11.125, 0.375, 11.25], "rotation": 180, "texture": "#0"}, - "west": {"uv": [0.375, 11.125, 0.5, 11.25], "rotation": 180, "texture": "#0"}, - "up": {"uv": [0.625, 11.125, 0.5, 11.25], "rotation": 180, "texture": "#0"}, - "down": {"uv": [0.75, 11.25, 0.625, 11.125], "rotation": 180, "texture": "#0"} - } - }, - { - "name": "cube_outline", - "from": [9.08891, 21.64663, 9.09], - "to": [6.38891, 18.94663, 6.89], - "rotation": {"angle": -45, "axis": "z", "origin": [7.98891, 20.04663, 7.991]}, - "faces": { - "north": {"uv": [1.5, 11.125, 1.625, 11.25], "rotation": 180, "texture": "#0"}, - "east": {"uv": [1.625, 11.125, 1.75, 11.25], "rotation": 180, "texture": "#0"}, - "south": {"uv": [1.75, 11.125, 1.875, 11.25], "rotation": 180, "texture": "#0"}, - "west": {"uv": [1.875, 11.125, 2, 11.25], "rotation": 180, "texture": "#0"}, - "up": {"uv": [2.125, 11.125, 2, 11.25], "rotation": 180, "texture": "#0"}, - "down": {"uv": [2.25, 11.25, 2.125, 11.125], "rotation": 180, "texture": "#0"} - } - }, - { - "from": [7.34391, 24.0552, 7.4525], - "to": [8.63391, 25.3452, 8.5275], - "rotation": {"angle": -45, "axis": "z", "origin": [7.98891, 24.70019, 8.00075]}, - "faces": { - "north": {"uv": [5.75, 8, 5, 8.75], "texture": "#0"}, - "east": {"uv": [4.5, 9.5, 3.875, 8.75], "texture": "#0"}, - "south": {"uv": [8.75, 4, 8, 4.75], "texture": "#0"}, - "west": {"uv": [3.75, 8.75, 3.125, 9.5], "texture": "#0"}, - "up": {"uv": [4.875, 10.75, 4.125, 11.375], "texture": "#0"}, - "down": {"uv": [9.75, 4, 9, 4.625], "texture": "#0"} - } - }, - { - "from": [6.95149, 23.04957, 7.249], - "to": [8.72989, 24.82797, 8.731], - "rotation": {"angle": -45, "axis": "z", "origin": [7.9889, 23.79059, 7.87144]}, - "faces": { - "north": {"uv": [8, 5, 8.75, 5.75], "texture": "#0"}, - "east": {"uv": [4.5, 9, 5.125, 9.75], "texture": "#0"}, - "south": {"uv": [6, 8, 6.75, 8.75], "texture": "#0"}, - "west": {"uv": [9, 5, 9.625, 5.75], "texture": "#0"}, - "up": {"uv": [6.25, 9.625, 5.5, 9], "texture": "#0"}, - "down": {"uv": [7.25, 9, 6.5, 9.625], "texture": "#0"} - } - }, - { - "from": [5.266, 20.3249, 7.76026], - "to": [7.0084, 20.8001, 8.23546], - "rotation": {"angle": -45, "axis": "z", "origin": [7.0084, 20.4833, 7.83946]}, - "faces": { - "north": {"uv": [5.875, 5.75, 4.5, 6.125], "texture": "#1"}, - "east": {"uv": [4.875, 6.5, 4.5, 6.875], "texture": "#1"}, - "south": {"uv": [7.125, 5, 5.75, 5.375], "texture": "#1"}, - "west": {"uv": [6.875, 2, 6.5, 2.375], "texture": "#1"}, - "up": {"uv": [6, 0.375, 7.375, 0], "texture": "#1"}, - "down": {"uv": [6, 0.75, 7.375, 1.125], "texture": "#1"} - } - }, - { - "from": [4.16353, 23.23946, 7.83914], - "to": [5.90593, 23.55626, 8.15594], - "rotation": {"angle": -45, "axis": "z", "origin": [5.90593, 23.23946, 7.83946]}, - "faces": { - "north": {"uv": [7.375, 3.25, 6, 3.5], "texture": "#1"}, - "east": {"uv": [6, 6.5, 5.75, 6.75], "texture": "#1"}, - "south": {"uv": [7.625, 5.75, 6.25, 6], "texture": "#1"}, - "west": {"uv": [5.5, 6.5, 5.25, 6.75], "texture": "#1"}, - "up": {"uv": [6.25, 6.5, 7.625, 6.25], "texture": "#1"}, - "down": {"uv": [6.5, 1.5, 7.875, 1.75], "texture": "#1"} - } - }, - { - "from": [5.77633, 21.71536, 7.83788], - "to": [6.09313, 23.45776, 8.15468], - "faces": { - "north": {"uv": [6.25, 1.5, 6, 2.875], "texture": "#1"}, - "east": {"uv": [1.25, 6.25, 1, 7.625], "texture": "#1"}, - "south": {"uv": [0.75, 6.25, 0.5, 7.625], "texture": "#1"}, - "west": {"uv": [0.25, 6.25, 0, 7.625], "texture": "#1"}, - "up": {"uv": [1.5, 5.5, 1.75, 5.25], "texture": "#1"}, - "down": {"uv": [6.5, 2.75, 6.75, 3], "texture": "#1"} - } - }, - { - "from": [1.43272, 22.09898, 7.90282], - "to": [4.44232, 24.15818, 7.90282], - "rotation": {"angle": -45, "axis": "z", "origin": [4.44232, 21.78218, 7.90282]}, - "faces": { - "north": {"uv": [2.375, 0, 0, 1.625], "texture": "#1"}, - "east": {"uv": [1.75, 6.75, 1.75, 8.375], "texture": "#1"}, - "south": {"uv": [2.375, 2, 0, 3.625], "texture": "#1"}, - "west": {"uv": [1.5, 6.75, 1.5, 8.375], "texture": "#1"}, - "up": {"uv": [6, 3.75, 8.375, 3.75], "texture": "#1"}, - "down": {"uv": [6.25, 6.75, 8.625, 6.75], "texture": "#1"} - } - }, - { - "from": [3.61751, 21.4698, 7.90314], - "to": [6.08855, 23.11716, 7.90314], - "faces": { - "north": {"uv": [4.75, 0, 2.75, 1.25], "texture": "#1"}, - "east": {"uv": [2.25, 6.75, 2.25, 8], "texture": "#1"}, - "south": {"uv": [4.75, 1.5, 2.75, 2.75], "texture": "#1"}, - "west": {"uv": [2, 6.75, 2, 8], "texture": "#1"}, - "up": {"uv": [7, 2.75, 9, 2.75], "texture": "#1"}, - "down": {"uv": [7, 3, 9, 3], "texture": "#1"} - } - }, - { - "from": [3.64919, 20.55108, 7.90346], - "to": [6.08855, 21.84996, 7.90346], - "rotation": {"angle": 22.5, "axis": "z", "origin": [4.58375, 21.58068, 7.90282]}, - "faces": { - "north": {"uv": [4.625, 3, 2.75, 4], "texture": "#1"}, - "east": {"uv": [2.75, 6.75, 2.75, 7.75], "texture": "#1"}, - "south": {"uv": [1.875, 4, 0, 5], "texture": "#1"}, - "west": {"uv": [2.5, 6.75, 2.5, 7.75], "texture": "#1"}, - "up": {"uv": [5.25, 7, 7.125, 7], "texture": "#1"}, - "down": {"uv": [7.25, 2, 9.125, 2], "texture": "#1"} - } - }, - { - "from": [4.78967, 19.22052, 7.90314], - "to": [6.65879, 20.86788, 7.90314], - "rotation": {"angle": 45, "axis": "z", "origin": [5.15399, 20.25012, 7.90282]}, - "faces": { - "north": {"uv": [3.75, 4.25, 2.25, 5.5], "texture": "#1"}, - "east": {"uv": [3.25, 6.75, 3.25, 8], "texture": "#1"}, - "south": {"uv": [5.5, 4.25, 4, 5.5], "texture": "#1"}, - "west": {"uv": [3, 6.75, 3, 8], "texture": "#1"}, - "up": {"uv": [7.25, 2.25, 8.75, 2.25], "texture": "#1"}, - "down": {"uv": [7.25, 2.5, 8.75, 2.5], "texture": "#1"} - } - }, - { - "from": [2.69992, 23.3345, 8.02954], - "to": [4.12552, 24.06314, 8.02954], - "rotation": {"angle": -45, "axis": "z", "origin": [4.12552, 21.71882, 8.02954]}, - "faces": { - "north": {"uv": [1.125, 5.25, 0, 5.875], "texture": "#1"}, - "east": {"uv": [3.75, 6.75, 3.75, 7.375], "texture": "#1"}, - "south": {"uv": [2.625, 5.75, 1.5, 6.375], "texture": "#1"}, - "west": {"uv": [3.5, 6.75, 3.5, 7.375], "texture": "#1"}, - "up": {"uv": [4, 7.25, 5.125, 7.25], "texture": "#1"}, - "down": {"uv": [7.25, 4, 8.375, 4], "texture": "#1"} - } - }, - { - "from": [3.6028, 20.5625, 7.87114], - "to": [5.0284, 21.29114, 7.87114], - "rotation": {"angle": -45, "axis": "z", "origin": [5.01256, 18.93098, 7.87114]}, - "faces": { - "north": {"uv": [4.125, 5.75, 3, 6.375], "texture": "#1"}, - "east": {"uv": [6.25, 7.25, 6.25, 7.875], "texture": "#1"}, - "south": {"uv": [6.875, 4, 5.75, 4.625], "texture": "#1"}, - "west": {"uv": [6, 7.25, 6, 7.875], "texture": "#1"}, - "up": {"uv": [7.25, 4.75, 8.375, 4.75], "texture": "#1"}, - "down": {"uv": [6.5, 7.25, 7.625, 7.25], "texture": "#1"} - } - }, - { - "from": [5.06908, 21.24691, 7.87146], - "to": [5.79772, 23.36947, 7.87146], - "faces": { - "north": {"uv": [5.625, 0, 5, 1.625], "texture": "#1"}, - "east": {"uv": [5.75, 7.25, 5.75, 8.875], "texture": "#1"}, - "south": {"uv": [5.625, 2, 5, 3.625], "texture": "#1"}, - "west": {"uv": [5.5, 7.25, 5.5, 8.875], "texture": "#1"}, - "up": {"uv": [7.25, 4.25, 7.875, 4.25], "texture": "#1"}, - "down": {"uv": [7.25, 4.5, 7.875, 4.5], "texture": "#1"} - } - }, - { - "from": [2.69992, 23.3345, 7.87114], - "to": [4.12552, 24.06314, 7.87114], - "rotation": {"angle": -45, "axis": "z", "origin": [4.12552, 21.71882, 7.87114]}, - "faces": { - "north": {"uv": [1.125, 5.25, 0, 5.875], "texture": "#1"}, - "east": {"uv": [3.75, 6.75, 3.75, 7.375], "texture": "#1"}, - "south": {"uv": [2.625, 5.75, 1.5, 6.375], "texture": "#1"}, - "west": {"uv": [3.5, 6.75, 3.5, 7.375], "texture": "#1"}, - "up": {"uv": [4, 7.25, 5.125, 7.25], "texture": "#1"}, - "down": {"uv": [7.25, 4, 8.375, 4], "texture": "#1"} - } - }, - { - "from": [3.6028, 20.5625, 8.02954], - "to": [5.0284, 21.29114, 8.02954], - "rotation": {"angle": -45, "axis": "z", "origin": [5.01256, 18.93098, 8.02954]}, - "faces": { - "north": {"uv": [4.125, 5.75, 3, 6.375], "texture": "#1"}, - "east": {"uv": [6.25, 7.25, 6.25, 7.875], "texture": "#1"}, - "south": {"uv": [6.875, 4, 5.75, 4.625], "texture": "#1"}, - "west": {"uv": [6, 7.25, 6, 7.875], "texture": "#1"}, - "up": {"uv": [7.25, 4.75, 8.375, 4.75], "texture": "#1"}, - "down": {"uv": [6.5, 7.25, 7.625, 7.25], "texture": "#1"} - } - }, - { - "from": [5.06908, 21.24691, 8.02986], - "to": [5.79772, 23.36947, 8.02986], - "faces": { - "north": {"uv": [5.625, 0, 5, 1.625], "texture": "#1"}, - "east": {"uv": [5.75, 7.25, 5.75, 8.875], "texture": "#1"}, - "south": {"uv": [5.625, 2, 5, 3.625], "texture": "#1"}, - "west": {"uv": [5.5, 7.25, 5.5, 8.875], "texture": "#1"}, - "up": {"uv": [7.25, 4.25, 7.875, 4.25], "texture": "#1"}, - "down": {"uv": [7.25, 4.5, 7.875, 4.5], "texture": "#1"} - } - }, - { - "from": [8.9916, 20.3249, 7.76026], - "to": [10.734, 20.8001, 8.23546], - "rotation": {"angle": 45, "axis": "z", "origin": [8.9916, 20.4833, 7.83946]}, - "faces": { - "north": {"uv": [4.5, 5.75, 5.875, 6.125], "texture": "#1"}, - "east": {"uv": [6.5, 2, 6.875, 2.375], "texture": "#1"}, - "south": {"uv": [5.75, 5, 7.125, 5.375], "texture": "#1"}, - "west": {"uv": [4.5, 6.5, 4.875, 6.875], "texture": "#1"}, - "up": {"uv": [7.375, 0.375, 6, 0], "texture": "#1"}, - "down": {"uv": [7.375, 0.75, 6, 1.125], "texture": "#1"} - } - }, - { - "from": [10.09407, 23.23946, 7.83914], - "to": [11.83647, 23.55626, 8.15594], - "rotation": {"angle": 45, "axis": "z", "origin": [10.09407, 23.23946, 7.83946]}, - "faces": { - "north": {"uv": [6, 3.25, 7.375, 3.5], "texture": "#1"}, - "east": {"uv": [5.25, 6.5, 5.5, 6.75], "texture": "#1"}, - "south": {"uv": [6.25, 5.75, 7.625, 6], "texture": "#1"}, - "west": {"uv": [5.75, 6.5, 6, 6.75], "texture": "#1"}, - "up": {"uv": [7.625, 6.5, 6.25, 6.25], "texture": "#1"}, - "down": {"uv": [7.875, 1.5, 6.5, 1.75], "texture": "#1"} - } - }, - { - "from": [9.90687, 21.71536, 7.83788], - "to": [10.22367, 23.45776, 8.15468], - "faces": { - "north": {"uv": [6, 1.5, 6.25, 2.875], "texture": "#1"}, - "east": {"uv": [0, 6.25, 0.25, 7.625], "texture": "#1"}, - "south": {"uv": [0.5, 6.25, 0.75, 7.625], "texture": "#1"}, - "west": {"uv": [1, 6.25, 1.25, 7.625], "texture": "#1"}, - "up": {"uv": [1.75, 5.5, 1.5, 5.25], "texture": "#1"}, - "down": {"uv": [6.75, 2.75, 6.5, 3], "texture": "#1"} - } - }, - { - "from": [11.55768, 22.09898, 7.90282], - "to": [14.56728, 24.15818, 7.90282], - "rotation": {"angle": 45, "axis": "z", "origin": [11.55768, 21.78218, 7.90282]}, - "faces": { - "north": {"uv": [0, 0, 2.375, 1.625], "texture": "#1"}, - "east": {"uv": [1.5, 6.75, 1.5, 8.375], "texture": "#1"}, - "south": {"uv": [0, 2, 2.375, 3.625], "texture": "#1"}, - "west": {"uv": [1.75, 6.75, 1.75, 8.375], "texture": "#1"}, - "up": {"uv": [8.375, 3.75, 6, 3.75], "texture": "#1"}, - "down": {"uv": [8.625, 6.75, 6.25, 6.75], "texture": "#1"} - } - }, - { - "from": [9.91145, 21.4698, 7.90314], - "to": [12.38249, 23.11716, 7.90314], - "faces": { - "north": {"uv": [2.75, 0, 4.75, 1.25], "texture": "#1"}, - "east": {"uv": [2, 6.75, 2, 8], "texture": "#1"}, - "south": {"uv": [2.75, 1.5, 4.75, 2.75], "texture": "#1"}, - "west": {"uv": [2.25, 6.75, 2.25, 8], "texture": "#1"}, - "up": {"uv": [9, 2.75, 7, 2.75], "texture": "#1"}, - "down": {"uv": [9, 3, 7, 3], "texture": "#1"} - } - }, - { - "from": [9.91145, 20.55108, 7.90346], - "to": [12.35081, 21.84996, 7.90346], - "rotation": {"angle": -22.5, "axis": "z", "origin": [11.41625, 21.58068, 7.90282]}, - "faces": { - "north": {"uv": [2.75, 3, 4.625, 4], "texture": "#1"}, - "east": {"uv": [2.5, 6.75, 2.5, 7.75], "texture": "#1"}, - "south": {"uv": [0, 4, 1.875, 5], "texture": "#1"}, - "west": {"uv": [2.75, 6.75, 2.75, 7.75], "texture": "#1"}, - "up": {"uv": [7.125, 7, 5.25, 7], "texture": "#1"}, - "down": {"uv": [9.125, 2, 7.25, 2], "texture": "#1"} - } - }, - { - "from": [9.34121, 19.22052, 7.90314], - "to": [11.21033, 20.86788, 7.90314], - "rotation": {"angle": -45, "axis": "z", "origin": [10.84601, 20.25012, 7.90282]}, - "faces": { - "north": {"uv": [2.25, 4.25, 3.75, 5.5], "texture": "#1"}, - "east": {"uv": [3, 6.75, 3, 8], "texture": "#1"}, - "south": {"uv": [4, 4.25, 5.5, 5.5], "texture": "#1"}, - "west": {"uv": [3.25, 6.75, 3.25, 8], "texture": "#1"}, - "up": {"uv": [8.75, 2.25, 7.25, 2.25], "texture": "#1"}, - "down": {"uv": [8.75, 2.5, 7.25, 2.5], "texture": "#1"} - } - }, - { - "from": [11.87448, 23.3345, 8.02954], - "to": [13.30008, 24.06314, 8.02954], - "rotation": {"angle": 45, "axis": "z", "origin": [11.87448, 21.71882, 8.02954]}, - "faces": { - "north": {"uv": [0, 5.25, 1.125, 5.875], "texture": "#1"}, - "east": {"uv": [3.5, 6.75, 3.5, 7.375], "texture": "#1"}, - "south": {"uv": [1.5, 5.75, 2.625, 6.375], "texture": "#1"}, - "west": {"uv": [3.75, 6.75, 3.75, 7.375], "texture": "#1"}, - "up": {"uv": [5.125, 7.25, 4, 7.25], "texture": "#1"}, - "down": {"uv": [8.375, 4, 7.25, 4], "texture": "#1"} - } - }, - { - "from": [10.9716, 20.5625, 7.87114], - "to": [12.3972, 21.29114, 7.87114], - "rotation": {"angle": 45, "axis": "z", "origin": [10.98744, 18.93098, 7.87114]}, - "faces": { - "north": {"uv": [3, 5.75, 4.125, 6.375], "texture": "#1"}, - "east": {"uv": [6, 7.25, 6, 7.875], "texture": "#1"}, - "south": {"uv": [5.75, 4, 6.875, 4.625], "texture": "#1"}, - "west": {"uv": [6.25, 7.25, 6.25, 7.875], "texture": "#1"}, - "up": {"uv": [8.375, 4.75, 7.25, 4.75], "texture": "#1"}, - "down": {"uv": [7.625, 7.25, 6.5, 7.25], "texture": "#1"} - } - }, - { - "from": [10.20228, 21.24691, 7.87146], - "to": [10.93092, 23.36947, 7.87146], - "faces": { - "north": {"uv": [5, 0, 5.625, 1.625], "texture": "#1"}, - "east": {"uv": [5.5, 7.25, 5.5, 8.875], "texture": "#1"}, - "south": {"uv": [5, 2, 5.625, 3.625], "texture": "#1"}, - "west": {"uv": [5.75, 7.25, 5.75, 8.875], "texture": "#1"}, - "up": {"uv": [7.875, 4.25, 7.25, 4.25], "texture": "#1"}, - "down": {"uv": [7.875, 4.5, 7.25, 4.5], "texture": "#1"} - } - }, - { - "from": [11.87448, 23.3345, 7.87114], - "to": [13.30008, 24.06314, 7.87114], - "rotation": {"angle": 45, "axis": "z", "origin": [11.87448, 21.71882, 7.87114]}, - "faces": { - "north": {"uv": [0, 5.25, 1.125, 5.875], "texture": "#1"}, - "east": {"uv": [3.5, 6.75, 3.5, 7.375], "texture": "#1"}, - "south": {"uv": [1.5, 5.75, 2.625, 6.375], "texture": "#1"}, - "west": {"uv": [3.75, 6.75, 3.75, 7.375], "texture": "#1"}, - "up": {"uv": [5.125, 7.25, 4, 7.25], "texture": "#1"}, - "down": {"uv": [8.375, 4, 7.25, 4], "texture": "#1"} - } - }, - { - "from": [10.9716, 20.5625, 8.02954], - "to": [12.3972, 21.29114, 8.02954], - "rotation": {"angle": 45, "axis": "z", "origin": [10.98744, 18.93098, 8.02954]}, - "faces": { - "north": {"uv": [3, 5.75, 4.125, 6.375], "texture": "#1"}, - "east": {"uv": [6.25625, 7.25, 6, 8.29375], "texture": "#1"}, - "south": {"uv": [5.75, 4, 6.875, 4.625], "texture": "#1"}, - "west": {"uv": [6.50625, 7.25, 6.25, 8.29375], "texture": "#1"}, - "up": {"uv": [8.375, 4.75, 7.50625, 5.16875], "texture": "#1"}, - "down": {"uv": [7.625, 7.25, 6.75625, 7.66875], "texture": "#1"} - } - }, - { - "from": [10.20228, 21.24691, 8.02986], - "to": [10.93092, 23.36947, 8.02986], - "faces": { - "north": {"uv": [5, 0, 5.625, 1.625], "texture": "#1"}, - "east": {"uv": [5.5, 7.25, 5.5, 8.875], "texture": "#1"}, - "south": {"uv": [5, 2, 5.625, 3.625], "texture": "#1"}, - "west": {"uv": [5.75, 7.25, 5.75, 8.875], "texture": "#1"}, - "up": {"uv": [7.875, 4.25, 7.25, 4.25], "texture": "#1"}, - "down": {"uv": [7.875, 4.5, 7.25, 4.5], "texture": "#1"} - } - }, - { - "from": [9.00728, 15.90762, 7.80282], - "to": [11.06648, 18.91722, 7.80282], - "rotation": {"angle": 45, "axis": "z", "origin": [8.69048, 18.91722, 7.80282]}, - "faces": { - "north": {"uv": [0, 0, 2.375, 1.625], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.375, 3.75, 6, 3.75], "rotation": 90, "texture": "#1"}, - "south": {"uv": [0, 2, 2.375, 3.625], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.625, 6.75, 6.25, 6.75], "rotation": 90, "texture": "#1"}, - "up": {"uv": [1.75, 6.75, 1.75, 8.375], "rotation": 90, "texture": "#1"}, - "down": {"uv": [1.5, 6.75, 1.5, 8.375], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [8.37811, 18.09241, 7.80314], - "to": [10.02547, 20.56345, 7.80314], - "faces": { - "north": {"uv": [2.75, 0, 4.75, 1.25], "rotation": 270, "texture": "#1"}, - "east": {"uv": [9, 2.75, 7, 2.75], "rotation": 90, "texture": "#1"}, - "south": {"uv": [2.75, 1.5, 4.75, 2.75], "rotation": 90, "texture": "#1"}, - "west": {"uv": [9, 3, 7, 3], "rotation": 90, "texture": "#1"}, - "up": {"uv": [2.25, 6.75, 2.25, 8], "rotation": 90, "texture": "#1"}, - "down": {"uv": [2, 6.75, 2, 8], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [10.2428, 17.17482, 7.92954], - "to": [10.97144, 18.60042, 7.92954], - "rotation": {"angle": 45, "axis": "z", "origin": [8.62712, 18.60042, 7.92954]}, - "faces": { - "north": {"uv": [0, 5.25, 1.125, 5.875], "rotation": 270, "texture": "#1"}, - "east": {"uv": [5.625, 7.25, 4, 7.6875], "rotation": 90, "texture": "#1"}, - "south": {"uv": [1.5, 5.75, 2.625, 6.375], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.875, 4, 7.25, 4.4375], "rotation": 90, "texture": "#1"}, - "up": {"uv": [3.75, 6.75, 4.25, 7.8125], "rotation": 90, "texture": "#1"}, - "down": {"uv": [3.5, 6.75, 4, 7.8125], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [8.15522, 19.54399, 7.77146], - "to": [10.27778, 20.27263, 7.77146], - "faces": { - "north": {"uv": [5, 0, 5.625, 1.625], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.01875, 4.25, 7.25, 4.54375], "rotation": 90, "texture": "#1"}, - "south": {"uv": [5, 2, 5.625, 3.625], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.01875, 4.5, 7.25, 4.79375], "rotation": 90, "texture": "#1"}, - "up": {"uv": [5.75, 7.25, 5.89375, 9.16875], "rotation": 90, "texture": "#1"}, - "down": {"uv": [5.5, 7.25, 5.64375, 9.16875], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [10.2428, 17.17482, 7.77114], - "to": [10.97144, 18.60042, 7.77114], - "rotation": {"angle": 45, "axis": "z", "origin": [8.62712, 18.60042, 7.77114]}, - "faces": { - "north": {"uv": [0, 5.25, 1.125, 5.875], "rotation": 270, "texture": "#1"}, - "east": {"uv": [5.3652, 10.89375, 3.33228, 10.38637], "rotation": 90, "texture": "#1"}, - "south": {"uv": [1.5, 5.75, 2.625, 6.375], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.74375, 7.02613, 6.71083, 6.51875], "rotation": 90, "texture": "#1"}, - "up": {"uv": [3.07239, 9.95498, 3.93581, 10.87888], "rotation": 90, "texture": "#1"}, - "down": {"uv": [2.8125, 9.95498, 3.67592, 10.87888], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [8.15522, 19.54399, 7.92986], - "to": [10.27778, 20.27263, 7.92986], - "faces": { - "north": {"uv": [5, 0, 5.625, 1.625], "rotation": 270, "texture": "#1"}, - "east": {"uv": [4.0625, 6.6875, 2.75, 7.44375], "rotation": 90, "texture": "#1"}, - "south": {"uv": [5, 2, 5.625, 3.625], "rotation": 90, "texture": "#1"}, - "west": {"uv": [4.0625, 6.9375, 2.75, 7.69375], "rotation": 90, "texture": "#1"}, - "up": {"uv": [1.25, 9.6875, 1.9375, 12.06875], "rotation": 90, "texture": "#1"}, - "down": {"uv": [1, 9.6875, 1.6875, 12.06875], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [4.93352, 15.90762, 7.80282], - "to": [6.99272, 18.91722, 7.80282], - "rotation": {"angle": -45, "axis": "z", "origin": [7.30952, 18.91722, 7.80282]}, - "faces": { - "north": {"uv": [0, 1.625, 2.375, 0], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.625, 6.75, 6.25, 6.99375], "rotation": 90, "texture": "#1"}, - "south": {"uv": [0, 3.625, 2.375, 2], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.375, 3.75, 6, 3.99375], "rotation": 90, "texture": "#1"}, - "up": {"uv": [1.75, 8.61875, 2.5875, 6.75], "rotation": 90, "texture": "#1"}, - "down": {"uv": [1.5, 8.61875, 2.3375, 6.75], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [5.97453, 18.09241, 7.80314], - "to": [7.62189, 20.56345, 7.80314], - "faces": { - "north": {"uv": [2.75, 1.25, 4.75, 0], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.74375, 7.3, 5.825, 7.90625], "rotation": 90, "texture": "#1"}, - "south": {"uv": [2.75, 2.75, 4.75, 1.5], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.74375, 7.05, 5.825, 7.65625], "rotation": 90, "texture": "#1"}, - "up": {"uv": [1.075, 12.90625, 1.99375, 11.05], "rotation": 90, "texture": "#1"}, - "down": {"uv": [0.825, 12.90625, 1.74375, 11.05], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [5.02856, 17.17482, 7.92954], - "to": [5.7572, 18.60042, 7.92954], - "rotation": {"angle": -45, "axis": "z", "origin": [7.37288, 18.60042, 7.92954]}, - "faces": { - "north": {"uv": [0, 5.875, 1.125, 5.25], "rotation": 270, "texture": "#1"}, - "east": {"uv": [5.46875, 7.25, 3.9375, 7.65625], "rotation": 90, "texture": "#1"}, - "south": {"uv": [1.5, 6.375, 2.625, 5.75], "rotation": 90, "texture": "#1"}, - "west": {"uv": [2.21875, 10.5, 0.6875, 10.90625], "rotation": 90, "texture": "#1"}, - "up": {"uv": [0.4375, 11.03125, 0.84375, 10], "rotation": 90, "texture": "#1"}, - "down": {"uv": [0.1875, 11.03125, 0.59375, 10], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [5.72222, 19.54399, 7.77146], - "to": [7.84478, 20.27263, 7.77146], - "faces": { - "north": {"uv": [5, 1.625, 5.625, 0], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.275, 4.7875, 7.34375, 4.4375], "rotation": 90, "texture": "#1"}, - "south": {"uv": [5, 3.625, 5.625, 2], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.275, 4.5375, 7.34375, 4.1875], "rotation": 90, "texture": "#1"}, - "up": {"uv": [5.75, 8.875, 6.05625, 7.25], "rotation": 90, "texture": "#1"}, - "down": {"uv": [5.5, 8.875, 5.80625, 7.25], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [5.02856, 17.17482, 7.77114], - "to": [5.7572, 18.60042, 7.77114], - "rotation": {"angle": -45, "axis": "z", "origin": [7.37288, 18.60042, 7.77114]}, - "faces": { - "north": {"uv": [0, 5.875, 1.125, 5.25], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.375, 4, 7.25, 4], "rotation": 90, "texture": "#1"}, - "south": {"uv": [1.5, 6.375, 2.625, 5.75], "rotation": 90, "texture": "#1"}, - "west": {"uv": [5.125, 7.25, 4, 7.25], "rotation": 90, "texture": "#1"}, - "up": {"uv": [3.75, 7.375, 3.75, 6.75], "rotation": 90, "texture": "#1"}, - "down": {"uv": [3.5, 7.375, 3.5, 6.75], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [5.72222, 19.54399, 7.92986], - "to": [7.84478, 20.27263, 7.92986], - "faces": { - "north": {"uv": [5, 1.625, 5.625, 0], "rotation": 270, "texture": "#1"}, - "east": {"uv": [7.875, 4.5, 7.25, 4.5], "rotation": 90, "texture": "#1"}, - "south": {"uv": [5, 3.625, 5.625, 2], "rotation": 90, "texture": "#1"}, - "west": {"uv": [7.875, 4.25, 7.25, 4.25], "rotation": 90, "texture": "#1"}, - "up": {"uv": [5.75, 8.875, 5.75, 7.25], "rotation": 90, "texture": "#1"}, - "down": {"uv": [5.5, 8.875, 5.5, 7.25], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [0.4, 15.2, 8], - "to": [15.4, 30.2, 8], - "faces": { - "north": {"uv": [0, 0, 15, 15], "texture": "#3"}, - "east": {"uv": [0, 0, 0, 7.5], "texture": "#3"}, - "south": {"uv": [15, 0, 0, 15], "texture": "#3"}, - "west": {"uv": [0, 0, 0, 7.5], "texture": "#3"}, - "up": {"uv": [7.5, 0, 0, 0], "texture": "#3"}, - "down": {"uv": [7.5, 0, 0, 0], "texture": "#3"} - } - } - ], - "display": { - "thirdperson_righthand": { - "rotation": [0.5, 90, 0], - "translation": [0, 4, 0.75], - "scale": [0.85, 0.85, 0.85] - }, - "thirdperson_lefthand": { - "rotation": [0.25, -90, 0], - "translation": [0, 4, 0.75], - "scale": [0.85, 0.85, 0.85] - }, - "firstperson_righthand": { - "rotation": [-15.71, -76.01, -1.19], - "translation": [3.13, -0.8, 1.13], - "scale": [0.68, 0.68, 0.68] - }, - "firstperson_lefthand": { - "rotation": [-180, 79.75, 167], - "translation": [3.13, -0.05, 1.38], - "scale": [0.68, 0.68, 0.68] - }, - "ground": { - "translation": [0, 15.5, 0] - }, - "gui": { - "rotation": [0, 0, -45], - "scale": [0.5, 0.5, 0.5] - }, - "head": { - "rotation": [0, 0, -31.25], - "translation": [5.25, -12.25, 3.75] - }, - "fixed": { - "rotation": [0, 0, 45], - "scale": [0.65, 0.65, 0.65] - } - } -} \ No newline at end of file diff --git a/src/main/resources/god/assets/useless_mod/models/item/endless_beaf_hammer.json b/src/main/resources/god/assets/useless_mod/models/item/endless_beaf_hammer.json deleted file mode 100644 index 4e97a9ce..00000000 --- a/src/main/resources/god/assets/useless_mod/models/item/endless_beaf_hammer.json +++ /dev/null @@ -1,880 +0,0 @@ -{ - "render_type":"cutout", - "format_version": "1.9.0", - "credit": "Made with _leng", - "texture_size": [32, 32], - "textures": { - "0": "useless_mod:item/endless_beaf_item", - "1": "useless_mod:item/endless_beaf_item_wing", - "2": "useless_mod:item/lable_wireless_transceiver_lighting", - "3": "useless_mod:item/endless_special_efficacy" - }, - "elements": [ - { - "name": "Octagon", - "from": [7.79289, -12.5001, 7.5], - "to": [8.20711, 18.0001, 8.5], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 8, 8]}, - "faces": { - "north": {"uv": [4, 0, 4.125, 7.625], "texture": "#0"}, - "east": {"uv": [0, 0, 0.25, 7.625], "texture": "#0"}, - "south": {"uv": [4.5, 0, 4.625, 7.625], "texture": "#0"}, - "west": {"uv": [0.5, 0, 0.75, 7.625], "texture": "#0"}, - "up": {"uv": [10.875, 8.25, 10.75, 8], "texture": "#0"}, - "down": {"uv": [10.875, 8.5, 10.75, 8.75], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [7.79289, -12.5003, 7.5], - "to": [8.20711, 18.0003, 8.5], - "faces": { - "north": {"uv": [5, 0, 5.125, 7.625], "texture": "#0"}, - "east": {"uv": [1, 0, 1.25, 7.625], "texture": "#0"}, - "south": {"uv": [5.5, 0, 5.625, 7.625], "texture": "#0"}, - "west": {"uv": [1.5, 0, 1.75, 7.625], "texture": "#0"}, - "up": {"uv": [10.875, 9.25, 10.75, 9], "texture": "#0"}, - "down": {"uv": [10.875, 9.5, 10.75, 9.75], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [7.5, -12.5, 7.79289], - "to": [8.5, 18, 8.20711], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 8, 8]}, - "faces": { - "north": {"uv": [2, 0, 2.25, 7.625], "texture": "#0"}, - "east": {"uv": [6, 0, 6.125, 7.625], "texture": "#0"}, - "south": {"uv": [2.5, 0, 2.75, 7.625], "texture": "#0"}, - "west": {"uv": [6.5, 0, 6.625, 7.625], "texture": "#0"}, - "up": {"uv": [11, 10.125, 10.75, 10], "texture": "#0"}, - "down": {"uv": [11, 10.5, 10.75, 10.625], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [7.5, -12.5002, 7.79289], - "to": [8.5, 18.0002, 8.20711], - "faces": { - "north": {"uv": [3, 0, 3.25, 7.625], "texture": "#0"}, - "east": {"uv": [7, 0, 7.125, 7.625], "texture": "#0"}, - "south": {"uv": [3.5, 0, 3.75, 7.625], "texture": "#0"}, - "west": {"uv": [7.5, 0, 7.625, 7.625], "texture": "#0"}, - "up": {"uv": [11.25, 0.125, 11, 0], "texture": "#0"}, - "down": {"uv": [11.25, 0.5, 11, 0.625], "texture": "#0"} - } - }, - { - "name": "duzuo", - "from": [6.869, -14.12, 8], - "to": [9.131, -10.118, 8], - "faces": { - "north": {"uv": [0, 9, 0.625, 10], "texture": "#0"}, - "east": {"uv": [10.25, 6, 10.375, 7], "texture": "#0"}, - "south": {"uv": [0.625, 9, 1.25, 10], "texture": "#0"}, - "west": {"uv": [10.25, 7.25, 10.375, 8.25], "texture": "#0"}, - "up": {"uv": [8, 10.625, 7.5, 10.5], "texture": "#0"}, - "down": {"uv": [8.75, 10.5, 8.25, 10.625], "texture": "#0"} - } - }, - { - "from": [7.4, -11.3, 7.4], - "to": [8.6, -10.1, 8.6], - "rotation": {"angle": -45, "axis": "z", "origin": [8, -10.7, 8]}, - "faces": { - "north": {"uv": [0, 10.75, 0.25, 11], "texture": "#0"}, - "east": {"uv": [0.5, 10.75, 0.75, 11], "texture": "#0"}, - "south": {"uv": [10.75, 1.5, 11, 1.75], "texture": "#0"}, - "west": {"uv": [10.75, 2, 11, 2.25], "texture": "#0"}, - "up": {"uv": [11, 4.75, 10.75, 4.5], "texture": "#0"}, - "down": {"uv": [11, 5, 10.75, 5.25], "texture": "#0"} - } - }, - { - "from": [7.8, -10.9, 7.3], - "to": [8.2, -10.5, 8.7], - "rotation": {"angle": -45, "axis": "z", "origin": [8, -10.7, 8]}, - "faces": { - "north": {"uv": [0.25, 0.75, 0.375, 0.875], "texture": "#0"}, - "east": {"uv": [0.25, 0, 0.5, 0.125], "texture": "#0"}, - "south": {"uv": [0.75, 0.25, 0.875, 0.375], "texture": "#0"}, - "west": {"uv": [0.25, 0.25, 0.5, 0.375], "texture": "#0"}, - "up": {"uv": [0.375, 0.75, 0.25, 0.5], "texture": "#0"}, - "down": {"uv": [0.875, 0, 0.75, 0.25], "texture": "#0"} - } - }, - { - "from": [7.8, 15.12, 7.3], - "to": [8.2, 15.52, 8.7], - "rotation": {"angle": -45, "axis": "z", "origin": [8, 15.32, 8]}, - "faces": { - "north": {"uv": [0.25, 0.75, 0.375, 0.875], "texture": "#0"}, - "east": {"uv": [0.25, 0, 0.5, 0.125], "texture": "#0"}, - "south": {"uv": [0.75, 0.25, 0.875, 0.375], "texture": "#0"}, - "west": {"uv": [0.25, 0.25, 0.5, 0.375], "texture": "#0"}, - "up": {"uv": [0.375, 0.75, 0.25, 0.5], "texture": "#0"}, - "down": {"uv": [0.875, 0, 0.75, 0.25], "texture": "#0"} - } - }, - { - "from": [7.39, -10.31, 7.39], - "to": [8.61, -9.84, 8.61], - "faces": { - "north": {"uv": [11, 1, 11.25, 1.125], "texture": "#0"}, - "east": {"uv": [1.5, 11, 1.75, 11.125], "texture": "#0"}, - "south": {"uv": [2, 11, 2.25, 11.125], "texture": "#0"}, - "west": {"uv": [2.5, 11, 2.75, 11.125], "texture": "#0"}, - "up": {"uv": [5.75, 11, 5.5, 10.75], "texture": "#0"}, - "down": {"uv": [6.25, 10.75, 6, 11], "texture": "#0"} - } - }, - { - "from": [7.39, 15.09, 7.39], - "to": [8.61, 15.56, 8.61], - "faces": { - "north": {"uv": [11, 2.5, 11.25, 2.625], "texture": "#0"}, - "east": {"uv": [3, 11, 3.25, 11.125], "texture": "#0"}, - "south": {"uv": [11, 3, 11.25, 3.125], "texture": "#0"}, - "west": {"uv": [3.5, 11, 3.75, 11.125], "texture": "#0"}, - "up": {"uv": [11, 6.25, 10.75, 6], "texture": "#0"}, - "down": {"uv": [6.75, 10.75, 6.5, 11], "texture": "#0"} - } - }, - { - "from": [7.39, 15.99, 7.39], - "to": [8.61, 17.71, 8.61], - "faces": { - "north": {"uv": [10.25, 8.5, 10.5, 8.875], "texture": "#0"}, - "east": {"uv": [10.25, 9.25, 10.5, 9.625], "texture": "#0"}, - "south": {"uv": [10.5, 0, 10.75, 0.375], "texture": "#0"}, - "west": {"uv": [10.5, 0.75, 10.75, 1.125], "texture": "#0"}, - "up": {"uv": [11, 6.75, 10.75, 6.5], "texture": "#0"}, - "down": {"uv": [7.25, 10.75, 7, 11], "texture": "#0"} - } - }, - { - "from": [6.65, 17.9, 6.68], - "to": [9.15, 20.4, 9.315], - "rotation": {"angle": -45, "axis": "z", "origin": [7.6, 18.9, 8.4]}, - "faces": { - "north": {"uv": [9.25, 1.5, 9.875, 2.125], "texture": "#0"}, - "east": {"uv": [9.25, 2.5, 9.875, 3.125], "texture": "#0"}, - "south": {"uv": [9.25, 6, 9.875, 6.625], "texture": "#0"}, - "west": {"uv": [9.25, 7, 9.875, 7.625], "texture": "#0"}, - "up": {"uv": [9.875, 8.625, 9.25, 8], "texture": "#0"}, - "down": {"uv": [9.875, 9, 9.25, 9.625], "texture": "#0"} - } - }, - { - "from": [7.8, 18.05, 6.48], - "to": [8.6, 18.85, 9.515], - "rotation": {"angle": -45, "axis": "z", "origin": [8.6, 18.9, 8.4]}, - "faces": { - "north": {"uv": [10.75, 7, 11, 7.25], "texture": "#0"}, - "east": {"uv": [10, 5.5, 10.75, 5.75], "texture": "#0"}, - "south": {"uv": [10.75, 7.5, 11, 7.75], "texture": "#0"}, - "west": {"uv": [0, 10.25, 0.75, 10.5], "texture": "#0"}, - "up": {"uv": [1.25, 11, 1, 10.25], "texture": "#0"}, - "down": {"uv": [10.5, 1.5, 10.25, 2.25], "texture": "#0"} - } - }, - { - "from": [8, 16.8, 7.6], - "to": [8, 17.8, 9.6], - "rotation": {"angle": -45, "axis": "x", "origin": [7.9, 17.8, 8.6]}, - "faces": { - "north": {"uv": [4, 11, 3.875, 11.25], "texture": "#0"}, - "east": {"uv": [10.25, 2.5, 10.75, 2.75], "texture": "#0"}, - "south": {"uv": [5.125, 11.25, 5.25, 11.5], "texture": "#0"}, - "west": {"uv": [10.25, 3, 10.75, 3.25], "texture": "#0"}, - "up": {"uv": [5.375, 11.75, 5.5, 11.25], "texture": "#0"}, - "down": {"uv": [5.625, 11.25, 5.75, 11.75], "texture": "#0"} - } - }, - { - "from": [8, 16.8, 6.4], - "to": [8, 17.8, 8.4], - "rotation": {"angle": 45, "axis": "x", "origin": [7.9, 17.8, 7.4]}, - "faces": { - "north": {"uv": [7.625, 10.875, 8, 11.375], "texture": "#0"}, - "east": {"uv": [7.5, 9, 8, 9.25], "texture": "#0"}, - "south": {"uv": [7.9, 7.5, 8.125, 7.75], "texture": "#0"}, - "west": {"uv": [9.25, 3.5, 9.75, 3.75], "texture": "#0"}, - "up": {"uv": [8.875, 11, 9.125, 10.5], "texture": "#0"}, - "down": {"uv": [10.875, 3.5, 11.125, 4], "texture": "#0"} - } - }, - { - "from": [8.10331, 19.03251, 6.9], - "to": [10.78331, 20.70751, 9.1], - "rotation": {"angle": 22.5, "axis": "z", "origin": [8.77331, 19.70251, 7]}, - "faces": { - "north": {"uv": [0, 8, 1, 8.625], "texture": "#0"}, - "east": {"uv": [7.5, 9.5, 8.125, 10.125], "texture": "#0"}, - "south": {"uv": [8, 0, 9, 0.625], "texture": "#0"}, - "west": {"uv": [1.5, 10, 2.125, 10.625], "texture": "#0"}, - "up": {"uv": [9, 1.625, 8, 1], "texture": "#0"}, - "down": {"uv": [2.25, 8, 1.25, 8.625], "texture": "#0"} - } - }, - { - "from": [5.21669, 19.03251, 6.9], - "to": [7.89669, 20.70751, 9.1], - "rotation": {"angle": -22.5, "axis": "z", "origin": [7.22669, 19.70251, 7]}, - "faces": { - "north": {"uv": [8, 2, 9, 2.625], "texture": "#0"}, - "east": {"uv": [2.5, 10, 3.125, 10.625], "texture": "#0"}, - "south": {"uv": [2.5, 8, 3.5, 8.625], "texture": "#0"}, - "west": {"uv": [3.5, 10, 4.125, 10.625], "texture": "#0"}, - "up": {"uv": [9, 3.625, 8, 3], "texture": "#0"}, - "down": {"uv": [4.75, 8, 3.75, 8.625], "texture": "#0"} - } - }, - { - "from": [6.48891, 21.54663, 6.99], - "to": [8.98891, 24.04663, 8.99], - "rotation": {"angle": -45, "axis": "z", "origin": [7.98891, 22.54663, 7.99]}, - "faces": { - "north": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "east": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "south": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "west": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "up": {"uv": [16, 16, 0, 0], "texture": "#2"}, - "down": {"uv": [16, 0, 0, 16], "texture": "#2"} - } - }, - { - "from": [6.48891, 19.04663, 6.99], - "to": [8.98891, 21.54663, 8.99], - "rotation": {"angle": -45, "axis": "z", "origin": [7.98891, 20.04663, 7.991]}, - "faces": { - "north": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "east": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "south": {"uv": [0, 0, 15.9, 16], "texture": "#2"}, - "west": {"uv": [0, 0, 15.9, 15.8], "texture": "#2"}, - "up": {"uv": [15.9, 15.8, 0, 0], "texture": "#2"}, - "down": {"uv": [16, 0, 0, 15.8], "texture": "#2"} - } - }, - { - "from": [6.22114, 20.40018, 6.989], - "to": [9.75668, 22.90018, 8.991], - "faces": { - "north": {"uv": [0, 0, 15.975, 16.025], "texture": "#2"}, - "east": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "south": {"uv": [0, 0, 15.975, 16], "texture": "#2"}, - "west": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "up": {"uv": [15.975, 16, 0, 0], "texture": "#2"}, - "down": {"uv": [16, 0, 0, 16], "texture": "#2"} - } - }, - { - "from": [7.17754, 24.8802, 7.99], - "to": [8.80028, 27.1202, 7.99], - "faces": { - "north": {"uv": [8.25, 7, 8.875, 7.875], "texture": "#0"}, - "east": {"uv": [5, 11, 5.225, 12.1375], "texture": "#0"}, - "south": {"uv": [8.25, 8.25, 8.875, 9.125], "texture": "#0"}, - "west": {"uv": [5.25, 11, 5.475, 12.1375], "texture": "#0"}, - "up": {"uv": [11.85, 4.25, 11, 4.5125], "texture": "#0"}, - "down": {"uv": [11.85, 5.5, 11, 5.7625], "texture": "#0"} - } - }, - { - "name": "cube_outline", - "from": [9.08891, 24.14663, 9.09], - "to": [6.38891, 21.44663, 6.89], - "rotation": {"angle": -45, "axis": "z", "origin": [7.98891, 22.54663, 7.99]}, - "faces": { - "north": {"uv": [0.75, 11.125, 0.875, 11.25], "rotation": 180, "texture": "#0"}, - "east": {"uv": [0.875, 11.125, 1, 11.25], "rotation": 180, "texture": "#0"}, - "south": {"uv": [1, 11.125, 1.125, 11.25], "rotation": 180, "texture": "#0"}, - "west": {"uv": [1.125, 11.125, 1.25, 11.25], "rotation": 180, "texture": "#0"}, - "up": {"uv": [1.375, 11.125, 1.25, 11.25], "rotation": 180, "texture": "#0"}, - "down": {"uv": [1.5, 11.25, 1.375, 11.125], "rotation": 180, "texture": "#0"} - } - }, - { - "name": "cube_outline", - "from": [9.85668, 23.00018, 9.091], - "to": [6.12114, 20.30018, 6.889], - "faces": { - "north": {"uv": [0, 11.125, 0.125, 11.25], "rotation": 180, "texture": "#0"}, - "east": {"uv": [0.125, 11.125, 0.25, 11.25], "rotation": 180, "texture": "#0"}, - "south": {"uv": [0.25, 11.125, 0.375, 11.25], "rotation": 180, "texture": "#0"}, - "west": {"uv": [0.375, 11.125, 0.5, 11.25], "rotation": 180, "texture": "#0"}, - "up": {"uv": [0.625, 11.125, 0.5, 11.25], "rotation": 180, "texture": "#0"}, - "down": {"uv": [0.75, 11.25, 0.625, 11.125], "rotation": 180, "texture": "#0"} - } - }, - { - "name": "cube_outline", - "from": [9.08891, 21.64663, 9.09], - "to": [6.38891, 18.94663, 6.89], - "rotation": {"angle": -45, "axis": "z", "origin": [7.98891, 20.04663, 7.991]}, - "faces": { - "north": {"uv": [1.5, 11.125, 1.625, 11.25], "rotation": 180, "texture": "#0"}, - "east": {"uv": [1.625, 11.125, 1.75, 11.25], "rotation": 180, "texture": "#0"}, - "south": {"uv": [1.75, 11.125, 1.875, 11.25], "rotation": 180, "texture": "#0"}, - "west": {"uv": [1.875, 11.125, 2, 11.25], "rotation": 180, "texture": "#0"}, - "up": {"uv": [2.125, 11.125, 2, 11.25], "rotation": 180, "texture": "#0"}, - "down": {"uv": [2.25, 11.25, 2.125, 11.125], "rotation": 180, "texture": "#0"} - } - }, - { - "from": [7.34391, 24.0552, 7.4525], - "to": [8.63391, 25.3452, 8.5275], - "rotation": {"angle": -45, "axis": "z", "origin": [7.98891, 24.70019, 8.00075]}, - "faces": { - "north": {"uv": [5.75, 8, 5, 8.75], "texture": "#0"}, - "east": {"uv": [4.5, 9.5, 3.875, 8.75], "texture": "#0"}, - "south": {"uv": [8.75, 4, 8, 4.75], "texture": "#0"}, - "west": {"uv": [3.75, 8.75, 3.125, 9.5], "texture": "#0"}, - "up": {"uv": [4.875, 10.75, 4.125, 11.375], "texture": "#0"}, - "down": {"uv": [9.75, 4, 9, 4.625], "texture": "#0"} - } - }, - { - "from": [6.95149, 23.04957, 7.249], - "to": [8.72989, 24.82797, 8.731], - "rotation": {"angle": -45, "axis": "z", "origin": [7.9889, 23.79059, 7.87144]}, - "faces": { - "north": {"uv": [8, 5, 8.75, 5.75], "texture": "#0"}, - "east": {"uv": [4.5, 9, 5.125, 9.75], "texture": "#0"}, - "south": {"uv": [6, 8, 6.75, 8.75], "texture": "#0"}, - "west": {"uv": [9, 5, 9.625, 5.75], "texture": "#0"}, - "up": {"uv": [6.25, 9.625, 5.5, 9], "texture": "#0"}, - "down": {"uv": [7.25, 9, 6.5, 9.625], "texture": "#0"} - } - }, - { - "from": [5.266, 20.3249, 7.76026], - "to": [7.0084, 20.8001, 8.23546], - "rotation": {"angle": -45, "axis": "z", "origin": [7.0084, 20.4833, 7.83946]}, - "faces": { - "north": {"uv": [5.875, 5.75, 4.5, 6.125], "texture": "#1"}, - "east": {"uv": [4.875, 6.5, 4.5, 6.875], "texture": "#1"}, - "south": {"uv": [7.125, 5, 5.75, 5.375], "texture": "#1"}, - "west": {"uv": [6.875, 2, 6.5, 2.375], "texture": "#1"}, - "up": {"uv": [6, 0.375, 7.375, 0], "texture": "#1"}, - "down": {"uv": [6, 0.75, 7.375, 1.125], "texture": "#1"} - } - }, - { - "from": [4.16353, 23.23946, 7.83914], - "to": [5.90593, 23.55626, 8.15594], - "rotation": {"angle": -45, "axis": "z", "origin": [5.90593, 23.23946, 7.83946]}, - "faces": { - "north": {"uv": [7.375, 3.25, 6, 3.5], "texture": "#1"}, - "east": {"uv": [6, 6.5, 5.75, 6.75], "texture": "#1"}, - "south": {"uv": [7.625, 5.75, 6.25, 6], "texture": "#1"}, - "west": {"uv": [5.5, 6.5, 5.25, 6.75], "texture": "#1"}, - "up": {"uv": [6.25, 6.5, 7.625, 6.25], "texture": "#1"}, - "down": {"uv": [6.5, 1.5, 7.875, 1.75], "texture": "#1"} - } - }, - { - "from": [5.77633, 21.71536, 7.83788], - "to": [6.09313, 23.45776, 8.15468], - "faces": { - "north": {"uv": [6.25, 1.5, 6, 2.875], "texture": "#1"}, - "east": {"uv": [1.25, 6.25, 1, 7.625], "texture": "#1"}, - "south": {"uv": [0.75, 6.25, 0.5, 7.625], "texture": "#1"}, - "west": {"uv": [0.25, 6.25, 0, 7.625], "texture": "#1"}, - "up": {"uv": [1.5, 5.5, 1.75, 5.25], "texture": "#1"}, - "down": {"uv": [6.5, 2.75, 6.75, 3], "texture": "#1"} - } - }, - { - "from": [1.43272, 22.09898, 7.90282], - "to": [4.44232, 24.15818, 7.90282], - "rotation": {"angle": -45, "axis": "z", "origin": [4.44232, 21.78218, 7.90282]}, - "faces": { - "north": {"uv": [2.375, 0, 0, 1.625], "texture": "#1"}, - "east": {"uv": [1.75, 6.75, 1.75, 8.375], "texture": "#1"}, - "south": {"uv": [2.375, 2, 0, 3.625], "texture": "#1"}, - "west": {"uv": [1.5, 6.75, 1.5, 8.375], "texture": "#1"}, - "up": {"uv": [6, 3.75, 8.375, 3.75], "texture": "#1"}, - "down": {"uv": [6.25, 6.75, 8.625, 6.75], "texture": "#1"} - } - }, - { - "from": [3.61751, 21.4698, 7.90314], - "to": [6.08855, 23.11716, 7.90314], - "faces": { - "north": {"uv": [4.75, 0, 2.75, 1.25], "texture": "#1"}, - "east": {"uv": [2.25, 6.75, 2.25, 8], "texture": "#1"}, - "south": {"uv": [4.75, 1.5, 2.75, 2.75], "texture": "#1"}, - "west": {"uv": [2, 6.75, 2, 8], "texture": "#1"}, - "up": {"uv": [7, 2.75, 9, 2.75], "texture": "#1"}, - "down": {"uv": [7, 3, 9, 3], "texture": "#1"} - } - }, - { - "from": [3.64919, 20.55108, 7.90346], - "to": [6.08855, 21.84996, 7.90346], - "rotation": {"angle": 22.5, "axis": "z", "origin": [4.58375, 21.58068, 7.90282]}, - "faces": { - "north": {"uv": [4.625, 3, 2.75, 4], "texture": "#1"}, - "east": {"uv": [2.75, 6.75, 2.75, 7.75], "texture": "#1"}, - "south": {"uv": [1.875, 4, 0, 5], "texture": "#1"}, - "west": {"uv": [2.5, 6.75, 2.5, 7.75], "texture": "#1"}, - "up": {"uv": [5.25, 7, 7.125, 7], "texture": "#1"}, - "down": {"uv": [7.25, 2, 9.125, 2], "texture": "#1"} - } - }, - { - "from": [4.78967, 19.22052, 7.90314], - "to": [6.65879, 20.86788, 7.90314], - "rotation": {"angle": 45, "axis": "z", "origin": [5.15399, 20.25012, 7.90282]}, - "faces": { - "north": {"uv": [3.75, 4.25, 2.25, 5.5], "texture": "#1"}, - "east": {"uv": [3.25, 6.75, 3.25, 8], "texture": "#1"}, - "south": {"uv": [5.5, 4.25, 4, 5.5], "texture": "#1"}, - "west": {"uv": [3, 6.75, 3, 8], "texture": "#1"}, - "up": {"uv": [7.25, 2.25, 8.75, 2.25], "texture": "#1"}, - "down": {"uv": [7.25, 2.5, 8.75, 2.5], "texture": "#1"} - } - }, - { - "from": [2.69992, 23.3345, 8.02954], - "to": [4.12552, 24.06314, 8.02954], - "rotation": {"angle": -45, "axis": "z", "origin": [4.12552, 21.71882, 8.02954]}, - "faces": { - "north": {"uv": [1.125, 5.25, 0, 5.875], "texture": "#1"}, - "east": {"uv": [3.75, 6.75, 3.75, 7.375], "texture": "#1"}, - "south": {"uv": [2.625, 5.75, 1.5, 6.375], "texture": "#1"}, - "west": {"uv": [3.5, 6.75, 3.5, 7.375], "texture": "#1"}, - "up": {"uv": [4, 7.25, 5.125, 7.25], "texture": "#1"}, - "down": {"uv": [7.25, 4, 8.375, 4], "texture": "#1"} - } - }, - { - "from": [3.6028, 20.5625, 7.87114], - "to": [5.0284, 21.29114, 7.87114], - "rotation": {"angle": -45, "axis": "z", "origin": [5.01256, 18.93098, 7.87114]}, - "faces": { - "north": {"uv": [4.125, 5.75, 3, 6.375], "texture": "#1"}, - "east": {"uv": [6.25, 7.25, 6.25, 7.875], "texture": "#1"}, - "south": {"uv": [6.875, 4, 5.75, 4.625], "texture": "#1"}, - "west": {"uv": [6, 7.25, 6, 7.875], "texture": "#1"}, - "up": {"uv": [7.25, 4.75, 8.375, 4.75], "texture": "#1"}, - "down": {"uv": [6.5, 7.25, 7.625, 7.25], "texture": "#1"} - } - }, - { - "from": [5.06908, 21.24691, 7.87146], - "to": [5.79772, 23.36947, 7.87146], - "faces": { - "north": {"uv": [5.625, 0, 5, 1.625], "texture": "#1"}, - "east": {"uv": [5.75, 7.25, 5.75, 8.875], "texture": "#1"}, - "south": {"uv": [5.625, 2, 5, 3.625], "texture": "#1"}, - "west": {"uv": [5.5, 7.25, 5.5, 8.875], "texture": "#1"}, - "up": {"uv": [7.25, 4.25, 7.875, 4.25], "texture": "#1"}, - "down": {"uv": [7.25, 4.5, 7.875, 4.5], "texture": "#1"} - } - }, - { - "from": [2.69992, 23.3345, 7.87114], - "to": [4.12552, 24.06314, 7.87114], - "rotation": {"angle": -45, "axis": "z", "origin": [4.12552, 21.71882, 7.87114]}, - "faces": { - "north": {"uv": [1.125, 5.25, 0, 5.875], "texture": "#1"}, - "east": {"uv": [3.75, 6.75, 3.75, 7.375], "texture": "#1"}, - "south": {"uv": [2.625, 5.75, 1.5, 6.375], "texture": "#1"}, - "west": {"uv": [3.5, 6.75, 3.5, 7.375], "texture": "#1"}, - "up": {"uv": [4, 7.25, 5.125, 7.25], "texture": "#1"}, - "down": {"uv": [7.25, 4, 8.375, 4], "texture": "#1"} - } - }, - { - "from": [3.6028, 20.5625, 8.02954], - "to": [5.0284, 21.29114, 8.02954], - "rotation": {"angle": -45, "axis": "z", "origin": [5.01256, 18.93098, 8.02954]}, - "faces": { - "north": {"uv": [4.125, 5.75, 3, 6.375], "texture": "#1"}, - "east": {"uv": [6.25, 7.25, 6.25, 7.875], "texture": "#1"}, - "south": {"uv": [6.875, 4, 5.75, 4.625], "texture": "#1"}, - "west": {"uv": [6, 7.25, 6, 7.875], "texture": "#1"}, - "up": {"uv": [7.25, 4.75, 8.375, 4.75], "texture": "#1"}, - "down": {"uv": [6.5, 7.25, 7.625, 7.25], "texture": "#1"} - } - }, - { - "from": [5.06908, 21.24691, 8.02986], - "to": [5.79772, 23.36947, 8.02986], - "faces": { - "north": {"uv": [5.625, 0, 5, 1.625], "texture": "#1"}, - "east": {"uv": [5.75, 7.25, 5.75, 8.875], "texture": "#1"}, - "south": {"uv": [5.625, 2, 5, 3.625], "texture": "#1"}, - "west": {"uv": [5.5, 7.25, 5.5, 8.875], "texture": "#1"}, - "up": {"uv": [7.25, 4.25, 7.875, 4.25], "texture": "#1"}, - "down": {"uv": [7.25, 4.5, 7.875, 4.5], "texture": "#1"} - } - }, - { - "from": [8.9916, 20.3249, 7.76026], - "to": [10.734, 20.8001, 8.23546], - "rotation": {"angle": 45, "axis": "z", "origin": [8.9916, 20.4833, 7.83946]}, - "faces": { - "north": {"uv": [4.5, 5.75, 5.875, 6.125], "texture": "#1"}, - "east": {"uv": [6.5, 2, 6.875, 2.375], "texture": "#1"}, - "south": {"uv": [5.75, 5, 7.125, 5.375], "texture": "#1"}, - "west": {"uv": [4.5, 6.5, 4.875, 6.875], "texture": "#1"}, - "up": {"uv": [7.375, 0.375, 6, 0], "texture": "#1"}, - "down": {"uv": [7.375, 0.75, 6, 1.125], "texture": "#1"} - } - }, - { - "from": [10.09407, 23.23946, 7.83914], - "to": [11.83647, 23.55626, 8.15594], - "rotation": {"angle": 45, "axis": "z", "origin": [10.09407, 23.23946, 7.83946]}, - "faces": { - "north": {"uv": [6, 3.25, 7.375, 3.5], "texture": "#1"}, - "east": {"uv": [5.25, 6.5, 5.5, 6.75], "texture": "#1"}, - "south": {"uv": [6.25, 5.75, 7.625, 6], "texture": "#1"}, - "west": {"uv": [5.75, 6.5, 6, 6.75], "texture": "#1"}, - "up": {"uv": [7.625, 6.5, 6.25, 6.25], "texture": "#1"}, - "down": {"uv": [7.875, 1.5, 6.5, 1.75], "texture": "#1"} - } - }, - { - "from": [9.90687, 21.71536, 7.83788], - "to": [10.22367, 23.45776, 8.15468], - "faces": { - "north": {"uv": [6, 1.5, 6.25, 2.875], "texture": "#1"}, - "east": {"uv": [0, 6.25, 0.25, 7.625], "texture": "#1"}, - "south": {"uv": [0.5, 6.25, 0.75, 7.625], "texture": "#1"}, - "west": {"uv": [1, 6.25, 1.25, 7.625], "texture": "#1"}, - "up": {"uv": [1.75, 5.5, 1.5, 5.25], "texture": "#1"}, - "down": {"uv": [6.75, 2.75, 6.5, 3], "texture": "#1"} - } - }, - { - "from": [11.55768, 22.09898, 7.90282], - "to": [14.56728, 24.15818, 7.90282], - "rotation": {"angle": 45, "axis": "z", "origin": [11.55768, 21.78218, 7.90282]}, - "faces": { - "north": {"uv": [0, 0, 2.375, 1.625], "texture": "#1"}, - "east": {"uv": [1.5, 6.75, 1.5, 8.375], "texture": "#1"}, - "south": {"uv": [0, 2, 2.375, 3.625], "texture": "#1"}, - "west": {"uv": [1.75, 6.75, 1.75, 8.375], "texture": "#1"}, - "up": {"uv": [8.375, 3.75, 6, 3.75], "texture": "#1"}, - "down": {"uv": [8.625, 6.75, 6.25, 6.75], "texture": "#1"} - } - }, - { - "from": [9.91145, 21.4698, 7.90314], - "to": [12.38249, 23.11716, 7.90314], - "faces": { - "north": {"uv": [2.75, 0, 4.75, 1.25], "texture": "#1"}, - "east": {"uv": [2, 6.75, 2, 8], "texture": "#1"}, - "south": {"uv": [2.75, 1.5, 4.75, 2.75], "texture": "#1"}, - "west": {"uv": [2.25, 6.75, 2.25, 8], "texture": "#1"}, - "up": {"uv": [9, 2.75, 7, 2.75], "texture": "#1"}, - "down": {"uv": [9, 3, 7, 3], "texture": "#1"} - } - }, - { - "from": [9.91145, 20.55108, 7.90346], - "to": [12.35081, 21.84996, 7.90346], - "rotation": {"angle": -22.5, "axis": "z", "origin": [11.41625, 21.58068, 7.90282]}, - "faces": { - "north": {"uv": [2.75, 3, 4.625, 4], "texture": "#1"}, - "east": {"uv": [2.5, 6.75, 2.5, 7.75], "texture": "#1"}, - "south": {"uv": [0, 4, 1.875, 5], "texture": "#1"}, - "west": {"uv": [2.75, 6.75, 2.75, 7.75], "texture": "#1"}, - "up": {"uv": [7.125, 7, 5.25, 7], "texture": "#1"}, - "down": {"uv": [9.125, 2, 7.25, 2], "texture": "#1"} - } - }, - { - "from": [9.34121, 19.22052, 7.90314], - "to": [11.21033, 20.86788, 7.90314], - "rotation": {"angle": -45, "axis": "z", "origin": [10.84601, 20.25012, 7.90282]}, - "faces": { - "north": {"uv": [2.25, 4.25, 3.75, 5.5], "texture": "#1"}, - "east": {"uv": [3, 6.75, 3, 8], "texture": "#1"}, - "south": {"uv": [4, 4.25, 5.5, 5.5], "texture": "#1"}, - "west": {"uv": [3.25, 6.75, 3.25, 8], "texture": "#1"}, - "up": {"uv": [8.75, 2.25, 7.25, 2.25], "texture": "#1"}, - "down": {"uv": [8.75, 2.5, 7.25, 2.5], "texture": "#1"} - } - }, - { - "from": [11.87448, 23.3345, 8.02954], - "to": [13.30008, 24.06314, 8.02954], - "rotation": {"angle": 45, "axis": "z", "origin": [11.87448, 21.71882, 8.02954]}, - "faces": { - "north": {"uv": [0, 5.25, 1.125, 5.875], "texture": "#1"}, - "east": {"uv": [3.5, 6.75, 3.5, 7.375], "texture": "#1"}, - "south": {"uv": [1.5, 5.75, 2.625, 6.375], "texture": "#1"}, - "west": {"uv": [3.75, 6.75, 3.75, 7.375], "texture": "#1"}, - "up": {"uv": [5.125, 7.25, 4, 7.25], "texture": "#1"}, - "down": {"uv": [8.375, 4, 7.25, 4], "texture": "#1"} - } - }, - { - "from": [10.9716, 20.5625, 7.87114], - "to": [12.3972, 21.29114, 7.87114], - "rotation": {"angle": 45, "axis": "z", "origin": [10.98744, 18.93098, 7.87114]}, - "faces": { - "north": {"uv": [3, 5.75, 4.125, 6.375], "texture": "#1"}, - "east": {"uv": [6, 7.25, 6, 7.875], "texture": "#1"}, - "south": {"uv": [5.75, 4, 6.875, 4.625], "texture": "#1"}, - "west": {"uv": [6.25, 7.25, 6.25, 7.875], "texture": "#1"}, - "up": {"uv": [8.375, 4.75, 7.25, 4.75], "texture": "#1"}, - "down": {"uv": [7.625, 7.25, 6.5, 7.25], "texture": "#1"} - } - }, - { - "from": [10.20228, 21.24691, 7.87146], - "to": [10.93092, 23.36947, 7.87146], - "faces": { - "north": {"uv": [5, 0, 5.625, 1.625], "texture": "#1"}, - "east": {"uv": [5.5, 7.25, 5.5, 8.875], "texture": "#1"}, - "south": {"uv": [5, 2, 5.625, 3.625], "texture": "#1"}, - "west": {"uv": [5.75, 7.25, 5.75, 8.875], "texture": "#1"}, - "up": {"uv": [7.875, 4.25, 7.25, 4.25], "texture": "#1"}, - "down": {"uv": [7.875, 4.5, 7.25, 4.5], "texture": "#1"} - } - }, - { - "from": [11.87448, 23.3345, 7.87114], - "to": [13.30008, 24.06314, 7.87114], - "rotation": {"angle": 45, "axis": "z", "origin": [11.87448, 21.71882, 7.87114]}, - "faces": { - "north": {"uv": [0, 5.25, 1.125, 5.875], "texture": "#1"}, - "east": {"uv": [3.5, 6.75, 3.5, 7.375], "texture": "#1"}, - "south": {"uv": [1.5, 5.75, 2.625, 6.375], "texture": "#1"}, - "west": {"uv": [3.75, 6.75, 3.75, 7.375], "texture": "#1"}, - "up": {"uv": [5.125, 7.25, 4, 7.25], "texture": "#1"}, - "down": {"uv": [8.375, 4, 7.25, 4], "texture": "#1"} - } - }, - { - "from": [10.9716, 20.5625, 8.02954], - "to": [12.3972, 21.29114, 8.02954], - "rotation": {"angle": 45, "axis": "z", "origin": [10.98744, 18.93098, 8.02954]}, - "faces": { - "north": {"uv": [3, 5.75, 4.125, 6.375], "texture": "#1"}, - "east": {"uv": [6.25625, 7.25, 6, 8.29375], "texture": "#1"}, - "south": {"uv": [5.75, 4, 6.875, 4.625], "texture": "#1"}, - "west": {"uv": [6.50625, 7.25, 6.25, 8.29375], "texture": "#1"}, - "up": {"uv": [8.375, 4.75, 7.50625, 5.16875], "texture": "#1"}, - "down": {"uv": [7.625, 7.25, 6.75625, 7.66875], "texture": "#1"} - } - }, - { - "from": [10.20228, 21.24691, 8.02986], - "to": [10.93092, 23.36947, 8.02986], - "faces": { - "north": {"uv": [5, 0, 5.625, 1.625], "texture": "#1"}, - "east": {"uv": [5.5, 7.25, 5.5, 8.875], "texture": "#1"}, - "south": {"uv": [5, 2, 5.625, 3.625], "texture": "#1"}, - "west": {"uv": [5.75, 7.25, 5.75, 8.875], "texture": "#1"}, - "up": {"uv": [7.875, 4.25, 7.25, 4.25], "texture": "#1"}, - "down": {"uv": [7.875, 4.5, 7.25, 4.5], "texture": "#1"} - } - }, - { - "from": [9.00728, 15.90762, 7.80282], - "to": [11.06648, 18.91722, 7.80282], - "rotation": {"angle": 45, "axis": "z", "origin": [8.69048, 18.91722, 7.80282]}, - "faces": { - "north": {"uv": [0, 0, 2.375, 1.625], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.375, 3.75, 6, 3.75], "rotation": 90, "texture": "#1"}, - "south": {"uv": [0, 2, 2.375, 3.625], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.625, 6.75, 6.25, 6.75], "rotation": 90, "texture": "#1"}, - "up": {"uv": [1.75, 6.75, 1.75, 8.375], "rotation": 90, "texture": "#1"}, - "down": {"uv": [1.5, 6.75, 1.5, 8.375], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [8.37811, 18.09241, 7.80314], - "to": [10.02547, 20.56345, 7.80314], - "faces": { - "north": {"uv": [2.75, 0, 4.75, 1.25], "rotation": 270, "texture": "#1"}, - "east": {"uv": [9, 2.75, 7, 2.75], "rotation": 90, "texture": "#1"}, - "south": {"uv": [2.75, 1.5, 4.75, 2.75], "rotation": 90, "texture": "#1"}, - "west": {"uv": [9, 3, 7, 3], "rotation": 90, "texture": "#1"}, - "up": {"uv": [2.25, 6.75, 2.25, 8], "rotation": 90, "texture": "#1"}, - "down": {"uv": [2, 6.75, 2, 8], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [10.2428, 17.17482, 7.92954], - "to": [10.97144, 18.60042, 7.92954], - "rotation": {"angle": 45, "axis": "z", "origin": [8.62712, 18.60042, 7.92954]}, - "faces": { - "north": {"uv": [0, 5.25, 1.125, 5.875], "rotation": 270, "texture": "#1"}, - "east": {"uv": [5.625, 7.25, 4, 7.6875], "rotation": 90, "texture": "#1"}, - "south": {"uv": [1.5, 5.75, 2.625, 6.375], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.875, 4, 7.25, 4.4375], "rotation": 90, "texture": "#1"}, - "up": {"uv": [3.75, 6.75, 4.25, 7.8125], "rotation": 90, "texture": "#1"}, - "down": {"uv": [3.5, 6.75, 4, 7.8125], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [8.15522, 19.54399, 7.77146], - "to": [10.27778, 20.27263, 7.77146], - "faces": { - "north": {"uv": [5, 0, 5.625, 1.625], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.01875, 4.25, 7.25, 4.54375], "rotation": 90, "texture": "#1"}, - "south": {"uv": [5, 2, 5.625, 3.625], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.01875, 4.5, 7.25, 4.79375], "rotation": 90, "texture": "#1"}, - "up": {"uv": [5.75, 7.25, 5.89375, 9.16875], "rotation": 90, "texture": "#1"}, - "down": {"uv": [5.5, 7.25, 5.64375, 9.16875], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [10.2428, 17.17482, 7.77114], - "to": [10.97144, 18.60042, 7.77114], - "rotation": {"angle": 45, "axis": "z", "origin": [8.62712, 18.60042, 7.77114]}, - "faces": { - "north": {"uv": [0, 5.25, 1.125, 5.875], "rotation": 270, "texture": "#1"}, - "east": {"uv": [5.3652, 10.89375, 3.33228, 10.38637], "rotation": 90, "texture": "#1"}, - "south": {"uv": [1.5, 5.75, 2.625, 6.375], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.74375, 7.02613, 6.71083, 6.51875], "rotation": 90, "texture": "#1"}, - "up": {"uv": [3.07239, 9.95498, 3.93581, 10.87888], "rotation": 90, "texture": "#1"}, - "down": {"uv": [2.8125, 9.95498, 3.67592, 10.87888], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [8.15522, 19.54399, 7.92986], - "to": [10.27778, 20.27263, 7.92986], - "faces": { - "north": {"uv": [5, 0, 5.625, 1.625], "rotation": 270, "texture": "#1"}, - "east": {"uv": [4.0625, 6.6875, 2.75, 7.44375], "rotation": 90, "texture": "#1"}, - "south": {"uv": [5, 2, 5.625, 3.625], "rotation": 90, "texture": "#1"}, - "west": {"uv": [4.0625, 6.9375, 2.75, 7.69375], "rotation": 90, "texture": "#1"}, - "up": {"uv": [1.25, 9.6875, 1.9375, 12.06875], "rotation": 90, "texture": "#1"}, - "down": {"uv": [1, 9.6875, 1.6875, 12.06875], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [4.93352, 15.90762, 7.80282], - "to": [6.99272, 18.91722, 7.80282], - "rotation": {"angle": -45, "axis": "z", "origin": [7.30952, 18.91722, 7.80282]}, - "faces": { - "north": {"uv": [0, 1.625, 2.375, 0], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.625, 6.75, 6.25, 6.99375], "rotation": 90, "texture": "#1"}, - "south": {"uv": [0, 3.625, 2.375, 2], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.375, 3.75, 6, 3.99375], "rotation": 90, "texture": "#1"}, - "up": {"uv": [1.75, 8.61875, 2.5875, 6.75], "rotation": 90, "texture": "#1"}, - "down": {"uv": [1.5, 8.61875, 2.3375, 6.75], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [5.97453, 18.09241, 7.80314], - "to": [7.62189, 20.56345, 7.80314], - "faces": { - "north": {"uv": [2.75, 1.25, 4.75, 0], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.74375, 7.3, 5.825, 7.90625], "rotation": 90, "texture": "#1"}, - "south": {"uv": [2.75, 2.75, 4.75, 1.5], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.74375, 7.05, 5.825, 7.65625], "rotation": 90, "texture": "#1"}, - "up": {"uv": [1.075, 12.90625, 1.99375, 11.05], "rotation": 90, "texture": "#1"}, - "down": {"uv": [0.825, 12.90625, 1.74375, 11.05], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [5.02856, 17.17482, 7.92954], - "to": [5.7572, 18.60042, 7.92954], - "rotation": {"angle": -45, "axis": "z", "origin": [7.37288, 18.60042, 7.92954]}, - "faces": { - "north": {"uv": [0, 5.875, 1.125, 5.25], "rotation": 270, "texture": "#1"}, - "east": {"uv": [5.46875, 7.25, 3.9375, 7.65625], "rotation": 90, "texture": "#1"}, - "south": {"uv": [1.5, 6.375, 2.625, 5.75], "rotation": 90, "texture": "#1"}, - "west": {"uv": [2.21875, 10.5, 0.6875, 10.90625], "rotation": 90, "texture": "#1"}, - "up": {"uv": [0.4375, 11.03125, 0.84375, 10], "rotation": 90, "texture": "#1"}, - "down": {"uv": [0.1875, 11.03125, 0.59375, 10], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [5.72222, 19.54399, 7.77146], - "to": [7.84478, 20.27263, 7.77146], - "faces": { - "north": {"uv": [5, 1.625, 5.625, 0], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.275, 4.7875, 7.34375, 4.4375], "rotation": 90, "texture": "#1"}, - "south": {"uv": [5, 3.625, 5.625, 2], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.275, 4.5375, 7.34375, 4.1875], "rotation": 90, "texture": "#1"}, - "up": {"uv": [5.75, 8.875, 6.05625, 7.25], "rotation": 90, "texture": "#1"}, - "down": {"uv": [5.5, 8.875, 5.80625, 7.25], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [5.02856, 17.17482, 7.77114], - "to": [5.7572, 18.60042, 7.77114], - "rotation": {"angle": -45, "axis": "z", "origin": [7.37288, 18.60042, 7.77114]}, - "faces": { - "north": {"uv": [0, 5.875, 1.125, 5.25], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.375, 4, 7.25, 4], "rotation": 90, "texture": "#1"}, - "south": {"uv": [1.5, 6.375, 2.625, 5.75], "rotation": 90, "texture": "#1"}, - "west": {"uv": [5.125, 7.25, 4, 7.25], "rotation": 90, "texture": "#1"}, - "up": {"uv": [3.75, 7.375, 3.75, 6.75], "rotation": 90, "texture": "#1"}, - "down": {"uv": [3.5, 7.375, 3.5, 6.75], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [5.72222, 19.54399, 7.92986], - "to": [7.84478, 20.27263, 7.92986], - "faces": { - "north": {"uv": [5, 1.625, 5.625, 0], "rotation": 270, "texture": "#1"}, - "east": {"uv": [7.875, 4.5, 7.25, 4.5], "rotation": 90, "texture": "#1"}, - "south": {"uv": [5, 3.625, 5.625, 2], "rotation": 90, "texture": "#1"}, - "west": {"uv": [7.875, 4.25, 7.25, 4.25], "rotation": 90, "texture": "#1"}, - "up": {"uv": [5.75, 8.875, 5.75, 7.25], "rotation": 90, "texture": "#1"}, - "down": {"uv": [5.5, 8.875, 5.5, 7.25], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [0.4, 15.2, 8], - "to": [15.4, 30.2, 8], - "faces": { - "north": {"uv": [0, 0, 15, 15], "texture": "#3"}, - "east": {"uv": [0, 0, 0, 7.5], "texture": "#3"}, - "south": {"uv": [15, 0, 0, 15], "texture": "#3"}, - "west": {"uv": [0, 0, 0, 7.5], "texture": "#3"}, - "up": {"uv": [7.5, 0, 0, 0], "texture": "#3"}, - "down": {"uv": [7.5, 0, 0, 0], "texture": "#3"} - } - } - ], - "display": { - "thirdperson_righthand": { - "rotation": [0.5, 90, 0], - "translation": [0, 4, 0.75], - "scale": [0.85, 0.85, 0.85] - }, - "thirdperson_lefthand": { - "rotation": [0.25, -90, 0], - "translation": [0, 4, 0.75], - "scale": [0.85, 0.85, 0.85] - }, - "firstperson_righthand": { - "rotation": [-15.71, -76.01, -1.19], - "translation": [3.13, -0.8, 1.13], - "scale": [0.68, 0.68, 0.68] - }, - "firstperson_lefthand": { - "rotation": [-180, 79.75, 167], - "translation": [3.13, -0.05, 1.38], - "scale": [0.68, 0.68, 0.68] - }, - "ground": { - "translation": [0, 15.5, 0] - }, - "gui": { - "rotation": [0, 0, -45], - "scale": [0.5, 0.5, 0.5] - }, - "head": { - "rotation": [0, 0, -31.25], - "translation": [5.25, -12.25, 3.75] - }, - "fixed": { - "rotation": [0, 0, 45], - "scale": [0.65, 0.65, 0.65] - } - } -} \ No newline at end of file diff --git a/src/main/resources/god/assets/useless_mod/models/item/endless_beaf_item.json b/src/main/resources/god/assets/useless_mod/models/item/endless_beaf_item.json deleted file mode 100644 index 4e97a9ce..00000000 --- a/src/main/resources/god/assets/useless_mod/models/item/endless_beaf_item.json +++ /dev/null @@ -1,880 +0,0 @@ -{ - "render_type":"cutout", - "format_version": "1.9.0", - "credit": "Made with _leng", - "texture_size": [32, 32], - "textures": { - "0": "useless_mod:item/endless_beaf_item", - "1": "useless_mod:item/endless_beaf_item_wing", - "2": "useless_mod:item/lable_wireless_transceiver_lighting", - "3": "useless_mod:item/endless_special_efficacy" - }, - "elements": [ - { - "name": "Octagon", - "from": [7.79289, -12.5001, 7.5], - "to": [8.20711, 18.0001, 8.5], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 8, 8]}, - "faces": { - "north": {"uv": [4, 0, 4.125, 7.625], "texture": "#0"}, - "east": {"uv": [0, 0, 0.25, 7.625], "texture": "#0"}, - "south": {"uv": [4.5, 0, 4.625, 7.625], "texture": "#0"}, - "west": {"uv": [0.5, 0, 0.75, 7.625], "texture": "#0"}, - "up": {"uv": [10.875, 8.25, 10.75, 8], "texture": "#0"}, - "down": {"uv": [10.875, 8.5, 10.75, 8.75], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [7.79289, -12.5003, 7.5], - "to": [8.20711, 18.0003, 8.5], - "faces": { - "north": {"uv": [5, 0, 5.125, 7.625], "texture": "#0"}, - "east": {"uv": [1, 0, 1.25, 7.625], "texture": "#0"}, - "south": {"uv": [5.5, 0, 5.625, 7.625], "texture": "#0"}, - "west": {"uv": [1.5, 0, 1.75, 7.625], "texture": "#0"}, - "up": {"uv": [10.875, 9.25, 10.75, 9], "texture": "#0"}, - "down": {"uv": [10.875, 9.5, 10.75, 9.75], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [7.5, -12.5, 7.79289], - "to": [8.5, 18, 8.20711], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 8, 8]}, - "faces": { - "north": {"uv": [2, 0, 2.25, 7.625], "texture": "#0"}, - "east": {"uv": [6, 0, 6.125, 7.625], "texture": "#0"}, - "south": {"uv": [2.5, 0, 2.75, 7.625], "texture": "#0"}, - "west": {"uv": [6.5, 0, 6.625, 7.625], "texture": "#0"}, - "up": {"uv": [11, 10.125, 10.75, 10], "texture": "#0"}, - "down": {"uv": [11, 10.5, 10.75, 10.625], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [7.5, -12.5002, 7.79289], - "to": [8.5, 18.0002, 8.20711], - "faces": { - "north": {"uv": [3, 0, 3.25, 7.625], "texture": "#0"}, - "east": {"uv": [7, 0, 7.125, 7.625], "texture": "#0"}, - "south": {"uv": [3.5, 0, 3.75, 7.625], "texture": "#0"}, - "west": {"uv": [7.5, 0, 7.625, 7.625], "texture": "#0"}, - "up": {"uv": [11.25, 0.125, 11, 0], "texture": "#0"}, - "down": {"uv": [11.25, 0.5, 11, 0.625], "texture": "#0"} - } - }, - { - "name": "duzuo", - "from": [6.869, -14.12, 8], - "to": [9.131, -10.118, 8], - "faces": { - "north": {"uv": [0, 9, 0.625, 10], "texture": "#0"}, - "east": {"uv": [10.25, 6, 10.375, 7], "texture": "#0"}, - "south": {"uv": [0.625, 9, 1.25, 10], "texture": "#0"}, - "west": {"uv": [10.25, 7.25, 10.375, 8.25], "texture": "#0"}, - "up": {"uv": [8, 10.625, 7.5, 10.5], "texture": "#0"}, - "down": {"uv": [8.75, 10.5, 8.25, 10.625], "texture": "#0"} - } - }, - { - "from": [7.4, -11.3, 7.4], - "to": [8.6, -10.1, 8.6], - "rotation": {"angle": -45, "axis": "z", "origin": [8, -10.7, 8]}, - "faces": { - "north": {"uv": [0, 10.75, 0.25, 11], "texture": "#0"}, - "east": {"uv": [0.5, 10.75, 0.75, 11], "texture": "#0"}, - "south": {"uv": [10.75, 1.5, 11, 1.75], "texture": "#0"}, - "west": {"uv": [10.75, 2, 11, 2.25], "texture": "#0"}, - "up": {"uv": [11, 4.75, 10.75, 4.5], "texture": "#0"}, - "down": {"uv": [11, 5, 10.75, 5.25], "texture": "#0"} - } - }, - { - "from": [7.8, -10.9, 7.3], - "to": [8.2, -10.5, 8.7], - "rotation": {"angle": -45, "axis": "z", "origin": [8, -10.7, 8]}, - "faces": { - "north": {"uv": [0.25, 0.75, 0.375, 0.875], "texture": "#0"}, - "east": {"uv": [0.25, 0, 0.5, 0.125], "texture": "#0"}, - "south": {"uv": [0.75, 0.25, 0.875, 0.375], "texture": "#0"}, - "west": {"uv": [0.25, 0.25, 0.5, 0.375], "texture": "#0"}, - "up": {"uv": [0.375, 0.75, 0.25, 0.5], "texture": "#0"}, - "down": {"uv": [0.875, 0, 0.75, 0.25], "texture": "#0"} - } - }, - { - "from": [7.8, 15.12, 7.3], - "to": [8.2, 15.52, 8.7], - "rotation": {"angle": -45, "axis": "z", "origin": [8, 15.32, 8]}, - "faces": { - "north": {"uv": [0.25, 0.75, 0.375, 0.875], "texture": "#0"}, - "east": {"uv": [0.25, 0, 0.5, 0.125], "texture": "#0"}, - "south": {"uv": [0.75, 0.25, 0.875, 0.375], "texture": "#0"}, - "west": {"uv": [0.25, 0.25, 0.5, 0.375], "texture": "#0"}, - "up": {"uv": [0.375, 0.75, 0.25, 0.5], "texture": "#0"}, - "down": {"uv": [0.875, 0, 0.75, 0.25], "texture": "#0"} - } - }, - { - "from": [7.39, -10.31, 7.39], - "to": [8.61, -9.84, 8.61], - "faces": { - "north": {"uv": [11, 1, 11.25, 1.125], "texture": "#0"}, - "east": {"uv": [1.5, 11, 1.75, 11.125], "texture": "#0"}, - "south": {"uv": [2, 11, 2.25, 11.125], "texture": "#0"}, - "west": {"uv": [2.5, 11, 2.75, 11.125], "texture": "#0"}, - "up": {"uv": [5.75, 11, 5.5, 10.75], "texture": "#0"}, - "down": {"uv": [6.25, 10.75, 6, 11], "texture": "#0"} - } - }, - { - "from": [7.39, 15.09, 7.39], - "to": [8.61, 15.56, 8.61], - "faces": { - "north": {"uv": [11, 2.5, 11.25, 2.625], "texture": "#0"}, - "east": {"uv": [3, 11, 3.25, 11.125], "texture": "#0"}, - "south": {"uv": [11, 3, 11.25, 3.125], "texture": "#0"}, - "west": {"uv": [3.5, 11, 3.75, 11.125], "texture": "#0"}, - "up": {"uv": [11, 6.25, 10.75, 6], "texture": "#0"}, - "down": {"uv": [6.75, 10.75, 6.5, 11], "texture": "#0"} - } - }, - { - "from": [7.39, 15.99, 7.39], - "to": [8.61, 17.71, 8.61], - "faces": { - "north": {"uv": [10.25, 8.5, 10.5, 8.875], "texture": "#0"}, - "east": {"uv": [10.25, 9.25, 10.5, 9.625], "texture": "#0"}, - "south": {"uv": [10.5, 0, 10.75, 0.375], "texture": "#0"}, - "west": {"uv": [10.5, 0.75, 10.75, 1.125], "texture": "#0"}, - "up": {"uv": [11, 6.75, 10.75, 6.5], "texture": "#0"}, - "down": {"uv": [7.25, 10.75, 7, 11], "texture": "#0"} - } - }, - { - "from": [6.65, 17.9, 6.68], - "to": [9.15, 20.4, 9.315], - "rotation": {"angle": -45, "axis": "z", "origin": [7.6, 18.9, 8.4]}, - "faces": { - "north": {"uv": [9.25, 1.5, 9.875, 2.125], "texture": "#0"}, - "east": {"uv": [9.25, 2.5, 9.875, 3.125], "texture": "#0"}, - "south": {"uv": [9.25, 6, 9.875, 6.625], "texture": "#0"}, - "west": {"uv": [9.25, 7, 9.875, 7.625], "texture": "#0"}, - "up": {"uv": [9.875, 8.625, 9.25, 8], "texture": "#0"}, - "down": {"uv": [9.875, 9, 9.25, 9.625], "texture": "#0"} - } - }, - { - "from": [7.8, 18.05, 6.48], - "to": [8.6, 18.85, 9.515], - "rotation": {"angle": -45, "axis": "z", "origin": [8.6, 18.9, 8.4]}, - "faces": { - "north": {"uv": [10.75, 7, 11, 7.25], "texture": "#0"}, - "east": {"uv": [10, 5.5, 10.75, 5.75], "texture": "#0"}, - "south": {"uv": [10.75, 7.5, 11, 7.75], "texture": "#0"}, - "west": {"uv": [0, 10.25, 0.75, 10.5], "texture": "#0"}, - "up": {"uv": [1.25, 11, 1, 10.25], "texture": "#0"}, - "down": {"uv": [10.5, 1.5, 10.25, 2.25], "texture": "#0"} - } - }, - { - "from": [8, 16.8, 7.6], - "to": [8, 17.8, 9.6], - "rotation": {"angle": -45, "axis": "x", "origin": [7.9, 17.8, 8.6]}, - "faces": { - "north": {"uv": [4, 11, 3.875, 11.25], "texture": "#0"}, - "east": {"uv": [10.25, 2.5, 10.75, 2.75], "texture": "#0"}, - "south": {"uv": [5.125, 11.25, 5.25, 11.5], "texture": "#0"}, - "west": {"uv": [10.25, 3, 10.75, 3.25], "texture": "#0"}, - "up": {"uv": [5.375, 11.75, 5.5, 11.25], "texture": "#0"}, - "down": {"uv": [5.625, 11.25, 5.75, 11.75], "texture": "#0"} - } - }, - { - "from": [8, 16.8, 6.4], - "to": [8, 17.8, 8.4], - "rotation": {"angle": 45, "axis": "x", "origin": [7.9, 17.8, 7.4]}, - "faces": { - "north": {"uv": [7.625, 10.875, 8, 11.375], "texture": "#0"}, - "east": {"uv": [7.5, 9, 8, 9.25], "texture": "#0"}, - "south": {"uv": [7.9, 7.5, 8.125, 7.75], "texture": "#0"}, - "west": {"uv": [9.25, 3.5, 9.75, 3.75], "texture": "#0"}, - "up": {"uv": [8.875, 11, 9.125, 10.5], "texture": "#0"}, - "down": {"uv": [10.875, 3.5, 11.125, 4], "texture": "#0"} - } - }, - { - "from": [8.10331, 19.03251, 6.9], - "to": [10.78331, 20.70751, 9.1], - "rotation": {"angle": 22.5, "axis": "z", "origin": [8.77331, 19.70251, 7]}, - "faces": { - "north": {"uv": [0, 8, 1, 8.625], "texture": "#0"}, - "east": {"uv": [7.5, 9.5, 8.125, 10.125], "texture": "#0"}, - "south": {"uv": [8, 0, 9, 0.625], "texture": "#0"}, - "west": {"uv": [1.5, 10, 2.125, 10.625], "texture": "#0"}, - "up": {"uv": [9, 1.625, 8, 1], "texture": "#0"}, - "down": {"uv": [2.25, 8, 1.25, 8.625], "texture": "#0"} - } - }, - { - "from": [5.21669, 19.03251, 6.9], - "to": [7.89669, 20.70751, 9.1], - "rotation": {"angle": -22.5, "axis": "z", "origin": [7.22669, 19.70251, 7]}, - "faces": { - "north": {"uv": [8, 2, 9, 2.625], "texture": "#0"}, - "east": {"uv": [2.5, 10, 3.125, 10.625], "texture": "#0"}, - "south": {"uv": [2.5, 8, 3.5, 8.625], "texture": "#0"}, - "west": {"uv": [3.5, 10, 4.125, 10.625], "texture": "#0"}, - "up": {"uv": [9, 3.625, 8, 3], "texture": "#0"}, - "down": {"uv": [4.75, 8, 3.75, 8.625], "texture": "#0"} - } - }, - { - "from": [6.48891, 21.54663, 6.99], - "to": [8.98891, 24.04663, 8.99], - "rotation": {"angle": -45, "axis": "z", "origin": [7.98891, 22.54663, 7.99]}, - "faces": { - "north": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "east": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "south": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "west": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "up": {"uv": [16, 16, 0, 0], "texture": "#2"}, - "down": {"uv": [16, 0, 0, 16], "texture": "#2"} - } - }, - { - "from": [6.48891, 19.04663, 6.99], - "to": [8.98891, 21.54663, 8.99], - "rotation": {"angle": -45, "axis": "z", "origin": [7.98891, 20.04663, 7.991]}, - "faces": { - "north": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "east": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "south": {"uv": [0, 0, 15.9, 16], "texture": "#2"}, - "west": {"uv": [0, 0, 15.9, 15.8], "texture": "#2"}, - "up": {"uv": [15.9, 15.8, 0, 0], "texture": "#2"}, - "down": {"uv": [16, 0, 0, 15.8], "texture": "#2"} - } - }, - { - "from": [6.22114, 20.40018, 6.989], - "to": [9.75668, 22.90018, 8.991], - "faces": { - "north": {"uv": [0, 0, 15.975, 16.025], "texture": "#2"}, - "east": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "south": {"uv": [0, 0, 15.975, 16], "texture": "#2"}, - "west": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "up": {"uv": [15.975, 16, 0, 0], "texture": "#2"}, - "down": {"uv": [16, 0, 0, 16], "texture": "#2"} - } - }, - { - "from": [7.17754, 24.8802, 7.99], - "to": [8.80028, 27.1202, 7.99], - "faces": { - "north": {"uv": [8.25, 7, 8.875, 7.875], "texture": "#0"}, - "east": {"uv": [5, 11, 5.225, 12.1375], "texture": "#0"}, - "south": {"uv": [8.25, 8.25, 8.875, 9.125], "texture": "#0"}, - "west": {"uv": [5.25, 11, 5.475, 12.1375], "texture": "#0"}, - "up": {"uv": [11.85, 4.25, 11, 4.5125], "texture": "#0"}, - "down": {"uv": [11.85, 5.5, 11, 5.7625], "texture": "#0"} - } - }, - { - "name": "cube_outline", - "from": [9.08891, 24.14663, 9.09], - "to": [6.38891, 21.44663, 6.89], - "rotation": {"angle": -45, "axis": "z", "origin": [7.98891, 22.54663, 7.99]}, - "faces": { - "north": {"uv": [0.75, 11.125, 0.875, 11.25], "rotation": 180, "texture": "#0"}, - "east": {"uv": [0.875, 11.125, 1, 11.25], "rotation": 180, "texture": "#0"}, - "south": {"uv": [1, 11.125, 1.125, 11.25], "rotation": 180, "texture": "#0"}, - "west": {"uv": [1.125, 11.125, 1.25, 11.25], "rotation": 180, "texture": "#0"}, - "up": {"uv": [1.375, 11.125, 1.25, 11.25], "rotation": 180, "texture": "#0"}, - "down": {"uv": [1.5, 11.25, 1.375, 11.125], "rotation": 180, "texture": "#0"} - } - }, - { - "name": "cube_outline", - "from": [9.85668, 23.00018, 9.091], - "to": [6.12114, 20.30018, 6.889], - "faces": { - "north": {"uv": [0, 11.125, 0.125, 11.25], "rotation": 180, "texture": "#0"}, - "east": {"uv": [0.125, 11.125, 0.25, 11.25], "rotation": 180, "texture": "#0"}, - "south": {"uv": [0.25, 11.125, 0.375, 11.25], "rotation": 180, "texture": "#0"}, - "west": {"uv": [0.375, 11.125, 0.5, 11.25], "rotation": 180, "texture": "#0"}, - "up": {"uv": [0.625, 11.125, 0.5, 11.25], "rotation": 180, "texture": "#0"}, - "down": {"uv": [0.75, 11.25, 0.625, 11.125], "rotation": 180, "texture": "#0"} - } - }, - { - "name": "cube_outline", - "from": [9.08891, 21.64663, 9.09], - "to": [6.38891, 18.94663, 6.89], - "rotation": {"angle": -45, "axis": "z", "origin": [7.98891, 20.04663, 7.991]}, - "faces": { - "north": {"uv": [1.5, 11.125, 1.625, 11.25], "rotation": 180, "texture": "#0"}, - "east": {"uv": [1.625, 11.125, 1.75, 11.25], "rotation": 180, "texture": "#0"}, - "south": {"uv": [1.75, 11.125, 1.875, 11.25], "rotation": 180, "texture": "#0"}, - "west": {"uv": [1.875, 11.125, 2, 11.25], "rotation": 180, "texture": "#0"}, - "up": {"uv": [2.125, 11.125, 2, 11.25], "rotation": 180, "texture": "#0"}, - "down": {"uv": [2.25, 11.25, 2.125, 11.125], "rotation": 180, "texture": "#0"} - } - }, - { - "from": [7.34391, 24.0552, 7.4525], - "to": [8.63391, 25.3452, 8.5275], - "rotation": {"angle": -45, "axis": "z", "origin": [7.98891, 24.70019, 8.00075]}, - "faces": { - "north": {"uv": [5.75, 8, 5, 8.75], "texture": "#0"}, - "east": {"uv": [4.5, 9.5, 3.875, 8.75], "texture": "#0"}, - "south": {"uv": [8.75, 4, 8, 4.75], "texture": "#0"}, - "west": {"uv": [3.75, 8.75, 3.125, 9.5], "texture": "#0"}, - "up": {"uv": [4.875, 10.75, 4.125, 11.375], "texture": "#0"}, - "down": {"uv": [9.75, 4, 9, 4.625], "texture": "#0"} - } - }, - { - "from": [6.95149, 23.04957, 7.249], - "to": [8.72989, 24.82797, 8.731], - "rotation": {"angle": -45, "axis": "z", "origin": [7.9889, 23.79059, 7.87144]}, - "faces": { - "north": {"uv": [8, 5, 8.75, 5.75], "texture": "#0"}, - "east": {"uv": [4.5, 9, 5.125, 9.75], "texture": "#0"}, - "south": {"uv": [6, 8, 6.75, 8.75], "texture": "#0"}, - "west": {"uv": [9, 5, 9.625, 5.75], "texture": "#0"}, - "up": {"uv": [6.25, 9.625, 5.5, 9], "texture": "#0"}, - "down": {"uv": [7.25, 9, 6.5, 9.625], "texture": "#0"} - } - }, - { - "from": [5.266, 20.3249, 7.76026], - "to": [7.0084, 20.8001, 8.23546], - "rotation": {"angle": -45, "axis": "z", "origin": [7.0084, 20.4833, 7.83946]}, - "faces": { - "north": {"uv": [5.875, 5.75, 4.5, 6.125], "texture": "#1"}, - "east": {"uv": [4.875, 6.5, 4.5, 6.875], "texture": "#1"}, - "south": {"uv": [7.125, 5, 5.75, 5.375], "texture": "#1"}, - "west": {"uv": [6.875, 2, 6.5, 2.375], "texture": "#1"}, - "up": {"uv": [6, 0.375, 7.375, 0], "texture": "#1"}, - "down": {"uv": [6, 0.75, 7.375, 1.125], "texture": "#1"} - } - }, - { - "from": [4.16353, 23.23946, 7.83914], - "to": [5.90593, 23.55626, 8.15594], - "rotation": {"angle": -45, "axis": "z", "origin": [5.90593, 23.23946, 7.83946]}, - "faces": { - "north": {"uv": [7.375, 3.25, 6, 3.5], "texture": "#1"}, - "east": {"uv": [6, 6.5, 5.75, 6.75], "texture": "#1"}, - "south": {"uv": [7.625, 5.75, 6.25, 6], "texture": "#1"}, - "west": {"uv": [5.5, 6.5, 5.25, 6.75], "texture": "#1"}, - "up": {"uv": [6.25, 6.5, 7.625, 6.25], "texture": "#1"}, - "down": {"uv": [6.5, 1.5, 7.875, 1.75], "texture": "#1"} - } - }, - { - "from": [5.77633, 21.71536, 7.83788], - "to": [6.09313, 23.45776, 8.15468], - "faces": { - "north": {"uv": [6.25, 1.5, 6, 2.875], "texture": "#1"}, - "east": {"uv": [1.25, 6.25, 1, 7.625], "texture": "#1"}, - "south": {"uv": [0.75, 6.25, 0.5, 7.625], "texture": "#1"}, - "west": {"uv": [0.25, 6.25, 0, 7.625], "texture": "#1"}, - "up": {"uv": [1.5, 5.5, 1.75, 5.25], "texture": "#1"}, - "down": {"uv": [6.5, 2.75, 6.75, 3], "texture": "#1"} - } - }, - { - "from": [1.43272, 22.09898, 7.90282], - "to": [4.44232, 24.15818, 7.90282], - "rotation": {"angle": -45, "axis": "z", "origin": [4.44232, 21.78218, 7.90282]}, - "faces": { - "north": {"uv": [2.375, 0, 0, 1.625], "texture": "#1"}, - "east": {"uv": [1.75, 6.75, 1.75, 8.375], "texture": "#1"}, - "south": {"uv": [2.375, 2, 0, 3.625], "texture": "#1"}, - "west": {"uv": [1.5, 6.75, 1.5, 8.375], "texture": "#1"}, - "up": {"uv": [6, 3.75, 8.375, 3.75], "texture": "#1"}, - "down": {"uv": [6.25, 6.75, 8.625, 6.75], "texture": "#1"} - } - }, - { - "from": [3.61751, 21.4698, 7.90314], - "to": [6.08855, 23.11716, 7.90314], - "faces": { - "north": {"uv": [4.75, 0, 2.75, 1.25], "texture": "#1"}, - "east": {"uv": [2.25, 6.75, 2.25, 8], "texture": "#1"}, - "south": {"uv": [4.75, 1.5, 2.75, 2.75], "texture": "#1"}, - "west": {"uv": [2, 6.75, 2, 8], "texture": "#1"}, - "up": {"uv": [7, 2.75, 9, 2.75], "texture": "#1"}, - "down": {"uv": [7, 3, 9, 3], "texture": "#1"} - } - }, - { - "from": [3.64919, 20.55108, 7.90346], - "to": [6.08855, 21.84996, 7.90346], - "rotation": {"angle": 22.5, "axis": "z", "origin": [4.58375, 21.58068, 7.90282]}, - "faces": { - "north": {"uv": [4.625, 3, 2.75, 4], "texture": "#1"}, - "east": {"uv": [2.75, 6.75, 2.75, 7.75], "texture": "#1"}, - "south": {"uv": [1.875, 4, 0, 5], "texture": "#1"}, - "west": {"uv": [2.5, 6.75, 2.5, 7.75], "texture": "#1"}, - "up": {"uv": [5.25, 7, 7.125, 7], "texture": "#1"}, - "down": {"uv": [7.25, 2, 9.125, 2], "texture": "#1"} - } - }, - { - "from": [4.78967, 19.22052, 7.90314], - "to": [6.65879, 20.86788, 7.90314], - "rotation": {"angle": 45, "axis": "z", "origin": [5.15399, 20.25012, 7.90282]}, - "faces": { - "north": {"uv": [3.75, 4.25, 2.25, 5.5], "texture": "#1"}, - "east": {"uv": [3.25, 6.75, 3.25, 8], "texture": "#1"}, - "south": {"uv": [5.5, 4.25, 4, 5.5], "texture": "#1"}, - "west": {"uv": [3, 6.75, 3, 8], "texture": "#1"}, - "up": {"uv": [7.25, 2.25, 8.75, 2.25], "texture": "#1"}, - "down": {"uv": [7.25, 2.5, 8.75, 2.5], "texture": "#1"} - } - }, - { - "from": [2.69992, 23.3345, 8.02954], - "to": [4.12552, 24.06314, 8.02954], - "rotation": {"angle": -45, "axis": "z", "origin": [4.12552, 21.71882, 8.02954]}, - "faces": { - "north": {"uv": [1.125, 5.25, 0, 5.875], "texture": "#1"}, - "east": {"uv": [3.75, 6.75, 3.75, 7.375], "texture": "#1"}, - "south": {"uv": [2.625, 5.75, 1.5, 6.375], "texture": "#1"}, - "west": {"uv": [3.5, 6.75, 3.5, 7.375], "texture": "#1"}, - "up": {"uv": [4, 7.25, 5.125, 7.25], "texture": "#1"}, - "down": {"uv": [7.25, 4, 8.375, 4], "texture": "#1"} - } - }, - { - "from": [3.6028, 20.5625, 7.87114], - "to": [5.0284, 21.29114, 7.87114], - "rotation": {"angle": -45, "axis": "z", "origin": [5.01256, 18.93098, 7.87114]}, - "faces": { - "north": {"uv": [4.125, 5.75, 3, 6.375], "texture": "#1"}, - "east": {"uv": [6.25, 7.25, 6.25, 7.875], "texture": "#1"}, - "south": {"uv": [6.875, 4, 5.75, 4.625], "texture": "#1"}, - "west": {"uv": [6, 7.25, 6, 7.875], "texture": "#1"}, - "up": {"uv": [7.25, 4.75, 8.375, 4.75], "texture": "#1"}, - "down": {"uv": [6.5, 7.25, 7.625, 7.25], "texture": "#1"} - } - }, - { - "from": [5.06908, 21.24691, 7.87146], - "to": [5.79772, 23.36947, 7.87146], - "faces": { - "north": {"uv": [5.625, 0, 5, 1.625], "texture": "#1"}, - "east": {"uv": [5.75, 7.25, 5.75, 8.875], "texture": "#1"}, - "south": {"uv": [5.625, 2, 5, 3.625], "texture": "#1"}, - "west": {"uv": [5.5, 7.25, 5.5, 8.875], "texture": "#1"}, - "up": {"uv": [7.25, 4.25, 7.875, 4.25], "texture": "#1"}, - "down": {"uv": [7.25, 4.5, 7.875, 4.5], "texture": "#1"} - } - }, - { - "from": [2.69992, 23.3345, 7.87114], - "to": [4.12552, 24.06314, 7.87114], - "rotation": {"angle": -45, "axis": "z", "origin": [4.12552, 21.71882, 7.87114]}, - "faces": { - "north": {"uv": [1.125, 5.25, 0, 5.875], "texture": "#1"}, - "east": {"uv": [3.75, 6.75, 3.75, 7.375], "texture": "#1"}, - "south": {"uv": [2.625, 5.75, 1.5, 6.375], "texture": "#1"}, - "west": {"uv": [3.5, 6.75, 3.5, 7.375], "texture": "#1"}, - "up": {"uv": [4, 7.25, 5.125, 7.25], "texture": "#1"}, - "down": {"uv": [7.25, 4, 8.375, 4], "texture": "#1"} - } - }, - { - "from": [3.6028, 20.5625, 8.02954], - "to": [5.0284, 21.29114, 8.02954], - "rotation": {"angle": -45, "axis": "z", "origin": [5.01256, 18.93098, 8.02954]}, - "faces": { - "north": {"uv": [4.125, 5.75, 3, 6.375], "texture": "#1"}, - "east": {"uv": [6.25, 7.25, 6.25, 7.875], "texture": "#1"}, - "south": {"uv": [6.875, 4, 5.75, 4.625], "texture": "#1"}, - "west": {"uv": [6, 7.25, 6, 7.875], "texture": "#1"}, - "up": {"uv": [7.25, 4.75, 8.375, 4.75], "texture": "#1"}, - "down": {"uv": [6.5, 7.25, 7.625, 7.25], "texture": "#1"} - } - }, - { - "from": [5.06908, 21.24691, 8.02986], - "to": [5.79772, 23.36947, 8.02986], - "faces": { - "north": {"uv": [5.625, 0, 5, 1.625], "texture": "#1"}, - "east": {"uv": [5.75, 7.25, 5.75, 8.875], "texture": "#1"}, - "south": {"uv": [5.625, 2, 5, 3.625], "texture": "#1"}, - "west": {"uv": [5.5, 7.25, 5.5, 8.875], "texture": "#1"}, - "up": {"uv": [7.25, 4.25, 7.875, 4.25], "texture": "#1"}, - "down": {"uv": [7.25, 4.5, 7.875, 4.5], "texture": "#1"} - } - }, - { - "from": [8.9916, 20.3249, 7.76026], - "to": [10.734, 20.8001, 8.23546], - "rotation": {"angle": 45, "axis": "z", "origin": [8.9916, 20.4833, 7.83946]}, - "faces": { - "north": {"uv": [4.5, 5.75, 5.875, 6.125], "texture": "#1"}, - "east": {"uv": [6.5, 2, 6.875, 2.375], "texture": "#1"}, - "south": {"uv": [5.75, 5, 7.125, 5.375], "texture": "#1"}, - "west": {"uv": [4.5, 6.5, 4.875, 6.875], "texture": "#1"}, - "up": {"uv": [7.375, 0.375, 6, 0], "texture": "#1"}, - "down": {"uv": [7.375, 0.75, 6, 1.125], "texture": "#1"} - } - }, - { - "from": [10.09407, 23.23946, 7.83914], - "to": [11.83647, 23.55626, 8.15594], - "rotation": {"angle": 45, "axis": "z", "origin": [10.09407, 23.23946, 7.83946]}, - "faces": { - "north": {"uv": [6, 3.25, 7.375, 3.5], "texture": "#1"}, - "east": {"uv": [5.25, 6.5, 5.5, 6.75], "texture": "#1"}, - "south": {"uv": [6.25, 5.75, 7.625, 6], "texture": "#1"}, - "west": {"uv": [5.75, 6.5, 6, 6.75], "texture": "#1"}, - "up": {"uv": [7.625, 6.5, 6.25, 6.25], "texture": "#1"}, - "down": {"uv": [7.875, 1.5, 6.5, 1.75], "texture": "#1"} - } - }, - { - "from": [9.90687, 21.71536, 7.83788], - "to": [10.22367, 23.45776, 8.15468], - "faces": { - "north": {"uv": [6, 1.5, 6.25, 2.875], "texture": "#1"}, - "east": {"uv": [0, 6.25, 0.25, 7.625], "texture": "#1"}, - "south": {"uv": [0.5, 6.25, 0.75, 7.625], "texture": "#1"}, - "west": {"uv": [1, 6.25, 1.25, 7.625], "texture": "#1"}, - "up": {"uv": [1.75, 5.5, 1.5, 5.25], "texture": "#1"}, - "down": {"uv": [6.75, 2.75, 6.5, 3], "texture": "#1"} - } - }, - { - "from": [11.55768, 22.09898, 7.90282], - "to": [14.56728, 24.15818, 7.90282], - "rotation": {"angle": 45, "axis": "z", "origin": [11.55768, 21.78218, 7.90282]}, - "faces": { - "north": {"uv": [0, 0, 2.375, 1.625], "texture": "#1"}, - "east": {"uv": [1.5, 6.75, 1.5, 8.375], "texture": "#1"}, - "south": {"uv": [0, 2, 2.375, 3.625], "texture": "#1"}, - "west": {"uv": [1.75, 6.75, 1.75, 8.375], "texture": "#1"}, - "up": {"uv": [8.375, 3.75, 6, 3.75], "texture": "#1"}, - "down": {"uv": [8.625, 6.75, 6.25, 6.75], "texture": "#1"} - } - }, - { - "from": [9.91145, 21.4698, 7.90314], - "to": [12.38249, 23.11716, 7.90314], - "faces": { - "north": {"uv": [2.75, 0, 4.75, 1.25], "texture": "#1"}, - "east": {"uv": [2, 6.75, 2, 8], "texture": "#1"}, - "south": {"uv": [2.75, 1.5, 4.75, 2.75], "texture": "#1"}, - "west": {"uv": [2.25, 6.75, 2.25, 8], "texture": "#1"}, - "up": {"uv": [9, 2.75, 7, 2.75], "texture": "#1"}, - "down": {"uv": [9, 3, 7, 3], "texture": "#1"} - } - }, - { - "from": [9.91145, 20.55108, 7.90346], - "to": [12.35081, 21.84996, 7.90346], - "rotation": {"angle": -22.5, "axis": "z", "origin": [11.41625, 21.58068, 7.90282]}, - "faces": { - "north": {"uv": [2.75, 3, 4.625, 4], "texture": "#1"}, - "east": {"uv": [2.5, 6.75, 2.5, 7.75], "texture": "#1"}, - "south": {"uv": [0, 4, 1.875, 5], "texture": "#1"}, - "west": {"uv": [2.75, 6.75, 2.75, 7.75], "texture": "#1"}, - "up": {"uv": [7.125, 7, 5.25, 7], "texture": "#1"}, - "down": {"uv": [9.125, 2, 7.25, 2], "texture": "#1"} - } - }, - { - "from": [9.34121, 19.22052, 7.90314], - "to": [11.21033, 20.86788, 7.90314], - "rotation": {"angle": -45, "axis": "z", "origin": [10.84601, 20.25012, 7.90282]}, - "faces": { - "north": {"uv": [2.25, 4.25, 3.75, 5.5], "texture": "#1"}, - "east": {"uv": [3, 6.75, 3, 8], "texture": "#1"}, - "south": {"uv": [4, 4.25, 5.5, 5.5], "texture": "#1"}, - "west": {"uv": [3.25, 6.75, 3.25, 8], "texture": "#1"}, - "up": {"uv": [8.75, 2.25, 7.25, 2.25], "texture": "#1"}, - "down": {"uv": [8.75, 2.5, 7.25, 2.5], "texture": "#1"} - } - }, - { - "from": [11.87448, 23.3345, 8.02954], - "to": [13.30008, 24.06314, 8.02954], - "rotation": {"angle": 45, "axis": "z", "origin": [11.87448, 21.71882, 8.02954]}, - "faces": { - "north": {"uv": [0, 5.25, 1.125, 5.875], "texture": "#1"}, - "east": {"uv": [3.5, 6.75, 3.5, 7.375], "texture": "#1"}, - "south": {"uv": [1.5, 5.75, 2.625, 6.375], "texture": "#1"}, - "west": {"uv": [3.75, 6.75, 3.75, 7.375], "texture": "#1"}, - "up": {"uv": [5.125, 7.25, 4, 7.25], "texture": "#1"}, - "down": {"uv": [8.375, 4, 7.25, 4], "texture": "#1"} - } - }, - { - "from": [10.9716, 20.5625, 7.87114], - "to": [12.3972, 21.29114, 7.87114], - "rotation": {"angle": 45, "axis": "z", "origin": [10.98744, 18.93098, 7.87114]}, - "faces": { - "north": {"uv": [3, 5.75, 4.125, 6.375], "texture": "#1"}, - "east": {"uv": [6, 7.25, 6, 7.875], "texture": "#1"}, - "south": {"uv": [5.75, 4, 6.875, 4.625], "texture": "#1"}, - "west": {"uv": [6.25, 7.25, 6.25, 7.875], "texture": "#1"}, - "up": {"uv": [8.375, 4.75, 7.25, 4.75], "texture": "#1"}, - "down": {"uv": [7.625, 7.25, 6.5, 7.25], "texture": "#1"} - } - }, - { - "from": [10.20228, 21.24691, 7.87146], - "to": [10.93092, 23.36947, 7.87146], - "faces": { - "north": {"uv": [5, 0, 5.625, 1.625], "texture": "#1"}, - "east": {"uv": [5.5, 7.25, 5.5, 8.875], "texture": "#1"}, - "south": {"uv": [5, 2, 5.625, 3.625], "texture": "#1"}, - "west": {"uv": [5.75, 7.25, 5.75, 8.875], "texture": "#1"}, - "up": {"uv": [7.875, 4.25, 7.25, 4.25], "texture": "#1"}, - "down": {"uv": [7.875, 4.5, 7.25, 4.5], "texture": "#1"} - } - }, - { - "from": [11.87448, 23.3345, 7.87114], - "to": [13.30008, 24.06314, 7.87114], - "rotation": {"angle": 45, "axis": "z", "origin": [11.87448, 21.71882, 7.87114]}, - "faces": { - "north": {"uv": [0, 5.25, 1.125, 5.875], "texture": "#1"}, - "east": {"uv": [3.5, 6.75, 3.5, 7.375], "texture": "#1"}, - "south": {"uv": [1.5, 5.75, 2.625, 6.375], "texture": "#1"}, - "west": {"uv": [3.75, 6.75, 3.75, 7.375], "texture": "#1"}, - "up": {"uv": [5.125, 7.25, 4, 7.25], "texture": "#1"}, - "down": {"uv": [8.375, 4, 7.25, 4], "texture": "#1"} - } - }, - { - "from": [10.9716, 20.5625, 8.02954], - "to": [12.3972, 21.29114, 8.02954], - "rotation": {"angle": 45, "axis": "z", "origin": [10.98744, 18.93098, 8.02954]}, - "faces": { - "north": {"uv": [3, 5.75, 4.125, 6.375], "texture": "#1"}, - "east": {"uv": [6.25625, 7.25, 6, 8.29375], "texture": "#1"}, - "south": {"uv": [5.75, 4, 6.875, 4.625], "texture": "#1"}, - "west": {"uv": [6.50625, 7.25, 6.25, 8.29375], "texture": "#1"}, - "up": {"uv": [8.375, 4.75, 7.50625, 5.16875], "texture": "#1"}, - "down": {"uv": [7.625, 7.25, 6.75625, 7.66875], "texture": "#1"} - } - }, - { - "from": [10.20228, 21.24691, 8.02986], - "to": [10.93092, 23.36947, 8.02986], - "faces": { - "north": {"uv": [5, 0, 5.625, 1.625], "texture": "#1"}, - "east": {"uv": [5.5, 7.25, 5.5, 8.875], "texture": "#1"}, - "south": {"uv": [5, 2, 5.625, 3.625], "texture": "#1"}, - "west": {"uv": [5.75, 7.25, 5.75, 8.875], "texture": "#1"}, - "up": {"uv": [7.875, 4.25, 7.25, 4.25], "texture": "#1"}, - "down": {"uv": [7.875, 4.5, 7.25, 4.5], "texture": "#1"} - } - }, - { - "from": [9.00728, 15.90762, 7.80282], - "to": [11.06648, 18.91722, 7.80282], - "rotation": {"angle": 45, "axis": "z", "origin": [8.69048, 18.91722, 7.80282]}, - "faces": { - "north": {"uv": [0, 0, 2.375, 1.625], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.375, 3.75, 6, 3.75], "rotation": 90, "texture": "#1"}, - "south": {"uv": [0, 2, 2.375, 3.625], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.625, 6.75, 6.25, 6.75], "rotation": 90, "texture": "#1"}, - "up": {"uv": [1.75, 6.75, 1.75, 8.375], "rotation": 90, "texture": "#1"}, - "down": {"uv": [1.5, 6.75, 1.5, 8.375], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [8.37811, 18.09241, 7.80314], - "to": [10.02547, 20.56345, 7.80314], - "faces": { - "north": {"uv": [2.75, 0, 4.75, 1.25], "rotation": 270, "texture": "#1"}, - "east": {"uv": [9, 2.75, 7, 2.75], "rotation": 90, "texture": "#1"}, - "south": {"uv": [2.75, 1.5, 4.75, 2.75], "rotation": 90, "texture": "#1"}, - "west": {"uv": [9, 3, 7, 3], "rotation": 90, "texture": "#1"}, - "up": {"uv": [2.25, 6.75, 2.25, 8], "rotation": 90, "texture": "#1"}, - "down": {"uv": [2, 6.75, 2, 8], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [10.2428, 17.17482, 7.92954], - "to": [10.97144, 18.60042, 7.92954], - "rotation": {"angle": 45, "axis": "z", "origin": [8.62712, 18.60042, 7.92954]}, - "faces": { - "north": {"uv": [0, 5.25, 1.125, 5.875], "rotation": 270, "texture": "#1"}, - "east": {"uv": [5.625, 7.25, 4, 7.6875], "rotation": 90, "texture": "#1"}, - "south": {"uv": [1.5, 5.75, 2.625, 6.375], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.875, 4, 7.25, 4.4375], "rotation": 90, "texture": "#1"}, - "up": {"uv": [3.75, 6.75, 4.25, 7.8125], "rotation": 90, "texture": "#1"}, - "down": {"uv": [3.5, 6.75, 4, 7.8125], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [8.15522, 19.54399, 7.77146], - "to": [10.27778, 20.27263, 7.77146], - "faces": { - "north": {"uv": [5, 0, 5.625, 1.625], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.01875, 4.25, 7.25, 4.54375], "rotation": 90, "texture": "#1"}, - "south": {"uv": [5, 2, 5.625, 3.625], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.01875, 4.5, 7.25, 4.79375], "rotation": 90, "texture": "#1"}, - "up": {"uv": [5.75, 7.25, 5.89375, 9.16875], "rotation": 90, "texture": "#1"}, - "down": {"uv": [5.5, 7.25, 5.64375, 9.16875], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [10.2428, 17.17482, 7.77114], - "to": [10.97144, 18.60042, 7.77114], - "rotation": {"angle": 45, "axis": "z", "origin": [8.62712, 18.60042, 7.77114]}, - "faces": { - "north": {"uv": [0, 5.25, 1.125, 5.875], "rotation": 270, "texture": "#1"}, - "east": {"uv": [5.3652, 10.89375, 3.33228, 10.38637], "rotation": 90, "texture": "#1"}, - "south": {"uv": [1.5, 5.75, 2.625, 6.375], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.74375, 7.02613, 6.71083, 6.51875], "rotation": 90, "texture": "#1"}, - "up": {"uv": [3.07239, 9.95498, 3.93581, 10.87888], "rotation": 90, "texture": "#1"}, - "down": {"uv": [2.8125, 9.95498, 3.67592, 10.87888], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [8.15522, 19.54399, 7.92986], - "to": [10.27778, 20.27263, 7.92986], - "faces": { - "north": {"uv": [5, 0, 5.625, 1.625], "rotation": 270, "texture": "#1"}, - "east": {"uv": [4.0625, 6.6875, 2.75, 7.44375], "rotation": 90, "texture": "#1"}, - "south": {"uv": [5, 2, 5.625, 3.625], "rotation": 90, "texture": "#1"}, - "west": {"uv": [4.0625, 6.9375, 2.75, 7.69375], "rotation": 90, "texture": "#1"}, - "up": {"uv": [1.25, 9.6875, 1.9375, 12.06875], "rotation": 90, "texture": "#1"}, - "down": {"uv": [1, 9.6875, 1.6875, 12.06875], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [4.93352, 15.90762, 7.80282], - "to": [6.99272, 18.91722, 7.80282], - "rotation": {"angle": -45, "axis": "z", "origin": [7.30952, 18.91722, 7.80282]}, - "faces": { - "north": {"uv": [0, 1.625, 2.375, 0], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.625, 6.75, 6.25, 6.99375], "rotation": 90, "texture": "#1"}, - "south": {"uv": [0, 3.625, 2.375, 2], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.375, 3.75, 6, 3.99375], "rotation": 90, "texture": "#1"}, - "up": {"uv": [1.75, 8.61875, 2.5875, 6.75], "rotation": 90, "texture": "#1"}, - "down": {"uv": [1.5, 8.61875, 2.3375, 6.75], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [5.97453, 18.09241, 7.80314], - "to": [7.62189, 20.56345, 7.80314], - "faces": { - "north": {"uv": [2.75, 1.25, 4.75, 0], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.74375, 7.3, 5.825, 7.90625], "rotation": 90, "texture": "#1"}, - "south": {"uv": [2.75, 2.75, 4.75, 1.5], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.74375, 7.05, 5.825, 7.65625], "rotation": 90, "texture": "#1"}, - "up": {"uv": [1.075, 12.90625, 1.99375, 11.05], "rotation": 90, "texture": "#1"}, - "down": {"uv": [0.825, 12.90625, 1.74375, 11.05], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [5.02856, 17.17482, 7.92954], - "to": [5.7572, 18.60042, 7.92954], - "rotation": {"angle": -45, "axis": "z", "origin": [7.37288, 18.60042, 7.92954]}, - "faces": { - "north": {"uv": [0, 5.875, 1.125, 5.25], "rotation": 270, "texture": "#1"}, - "east": {"uv": [5.46875, 7.25, 3.9375, 7.65625], "rotation": 90, "texture": "#1"}, - "south": {"uv": [1.5, 6.375, 2.625, 5.75], "rotation": 90, "texture": "#1"}, - "west": {"uv": [2.21875, 10.5, 0.6875, 10.90625], "rotation": 90, "texture": "#1"}, - "up": {"uv": [0.4375, 11.03125, 0.84375, 10], "rotation": 90, "texture": "#1"}, - "down": {"uv": [0.1875, 11.03125, 0.59375, 10], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [5.72222, 19.54399, 7.77146], - "to": [7.84478, 20.27263, 7.77146], - "faces": { - "north": {"uv": [5, 1.625, 5.625, 0], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.275, 4.7875, 7.34375, 4.4375], "rotation": 90, "texture": "#1"}, - "south": {"uv": [5, 3.625, 5.625, 2], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.275, 4.5375, 7.34375, 4.1875], "rotation": 90, "texture": "#1"}, - "up": {"uv": [5.75, 8.875, 6.05625, 7.25], "rotation": 90, "texture": "#1"}, - "down": {"uv": [5.5, 8.875, 5.80625, 7.25], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [5.02856, 17.17482, 7.77114], - "to": [5.7572, 18.60042, 7.77114], - "rotation": {"angle": -45, "axis": "z", "origin": [7.37288, 18.60042, 7.77114]}, - "faces": { - "north": {"uv": [0, 5.875, 1.125, 5.25], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.375, 4, 7.25, 4], "rotation": 90, "texture": "#1"}, - "south": {"uv": [1.5, 6.375, 2.625, 5.75], "rotation": 90, "texture": "#1"}, - "west": {"uv": [5.125, 7.25, 4, 7.25], "rotation": 90, "texture": "#1"}, - "up": {"uv": [3.75, 7.375, 3.75, 6.75], "rotation": 90, "texture": "#1"}, - "down": {"uv": [3.5, 7.375, 3.5, 6.75], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [5.72222, 19.54399, 7.92986], - "to": [7.84478, 20.27263, 7.92986], - "faces": { - "north": {"uv": [5, 1.625, 5.625, 0], "rotation": 270, "texture": "#1"}, - "east": {"uv": [7.875, 4.5, 7.25, 4.5], "rotation": 90, "texture": "#1"}, - "south": {"uv": [5, 3.625, 5.625, 2], "rotation": 90, "texture": "#1"}, - "west": {"uv": [7.875, 4.25, 7.25, 4.25], "rotation": 90, "texture": "#1"}, - "up": {"uv": [5.75, 8.875, 5.75, 7.25], "rotation": 90, "texture": "#1"}, - "down": {"uv": [5.5, 8.875, 5.5, 7.25], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [0.4, 15.2, 8], - "to": [15.4, 30.2, 8], - "faces": { - "north": {"uv": [0, 0, 15, 15], "texture": "#3"}, - "east": {"uv": [0, 0, 0, 7.5], "texture": "#3"}, - "south": {"uv": [15, 0, 0, 15], "texture": "#3"}, - "west": {"uv": [0, 0, 0, 7.5], "texture": "#3"}, - "up": {"uv": [7.5, 0, 0, 0], "texture": "#3"}, - "down": {"uv": [7.5, 0, 0, 0], "texture": "#3"} - } - } - ], - "display": { - "thirdperson_righthand": { - "rotation": [0.5, 90, 0], - "translation": [0, 4, 0.75], - "scale": [0.85, 0.85, 0.85] - }, - "thirdperson_lefthand": { - "rotation": [0.25, -90, 0], - "translation": [0, 4, 0.75], - "scale": [0.85, 0.85, 0.85] - }, - "firstperson_righthand": { - "rotation": [-15.71, -76.01, -1.19], - "translation": [3.13, -0.8, 1.13], - "scale": [0.68, 0.68, 0.68] - }, - "firstperson_lefthand": { - "rotation": [-180, 79.75, 167], - "translation": [3.13, -0.05, 1.38], - "scale": [0.68, 0.68, 0.68] - }, - "ground": { - "translation": [0, 15.5, 0] - }, - "gui": { - "rotation": [0, 0, -45], - "scale": [0.5, 0.5, 0.5] - }, - "head": { - "rotation": [0, 0, -31.25], - "translation": [5.25, -12.25, 3.75] - }, - "fixed": { - "rotation": [0, 0, 45], - "scale": [0.65, 0.65, 0.65] - } - } -} \ No newline at end of file diff --git a/src/main/resources/god/assets/useless_mod/models/item/endless_beaf_mallet.json b/src/main/resources/god/assets/useless_mod/models/item/endless_beaf_mallet.json deleted file mode 100644 index 4e97a9ce..00000000 --- a/src/main/resources/god/assets/useless_mod/models/item/endless_beaf_mallet.json +++ /dev/null @@ -1,880 +0,0 @@ -{ - "render_type":"cutout", - "format_version": "1.9.0", - "credit": "Made with _leng", - "texture_size": [32, 32], - "textures": { - "0": "useless_mod:item/endless_beaf_item", - "1": "useless_mod:item/endless_beaf_item_wing", - "2": "useless_mod:item/lable_wireless_transceiver_lighting", - "3": "useless_mod:item/endless_special_efficacy" - }, - "elements": [ - { - "name": "Octagon", - "from": [7.79289, -12.5001, 7.5], - "to": [8.20711, 18.0001, 8.5], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 8, 8]}, - "faces": { - "north": {"uv": [4, 0, 4.125, 7.625], "texture": "#0"}, - "east": {"uv": [0, 0, 0.25, 7.625], "texture": "#0"}, - "south": {"uv": [4.5, 0, 4.625, 7.625], "texture": "#0"}, - "west": {"uv": [0.5, 0, 0.75, 7.625], "texture": "#0"}, - "up": {"uv": [10.875, 8.25, 10.75, 8], "texture": "#0"}, - "down": {"uv": [10.875, 8.5, 10.75, 8.75], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [7.79289, -12.5003, 7.5], - "to": [8.20711, 18.0003, 8.5], - "faces": { - "north": {"uv": [5, 0, 5.125, 7.625], "texture": "#0"}, - "east": {"uv": [1, 0, 1.25, 7.625], "texture": "#0"}, - "south": {"uv": [5.5, 0, 5.625, 7.625], "texture": "#0"}, - "west": {"uv": [1.5, 0, 1.75, 7.625], "texture": "#0"}, - "up": {"uv": [10.875, 9.25, 10.75, 9], "texture": "#0"}, - "down": {"uv": [10.875, 9.5, 10.75, 9.75], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [7.5, -12.5, 7.79289], - "to": [8.5, 18, 8.20711], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 8, 8]}, - "faces": { - "north": {"uv": [2, 0, 2.25, 7.625], "texture": "#0"}, - "east": {"uv": [6, 0, 6.125, 7.625], "texture": "#0"}, - "south": {"uv": [2.5, 0, 2.75, 7.625], "texture": "#0"}, - "west": {"uv": [6.5, 0, 6.625, 7.625], "texture": "#0"}, - "up": {"uv": [11, 10.125, 10.75, 10], "texture": "#0"}, - "down": {"uv": [11, 10.5, 10.75, 10.625], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [7.5, -12.5002, 7.79289], - "to": [8.5, 18.0002, 8.20711], - "faces": { - "north": {"uv": [3, 0, 3.25, 7.625], "texture": "#0"}, - "east": {"uv": [7, 0, 7.125, 7.625], "texture": "#0"}, - "south": {"uv": [3.5, 0, 3.75, 7.625], "texture": "#0"}, - "west": {"uv": [7.5, 0, 7.625, 7.625], "texture": "#0"}, - "up": {"uv": [11.25, 0.125, 11, 0], "texture": "#0"}, - "down": {"uv": [11.25, 0.5, 11, 0.625], "texture": "#0"} - } - }, - { - "name": "duzuo", - "from": [6.869, -14.12, 8], - "to": [9.131, -10.118, 8], - "faces": { - "north": {"uv": [0, 9, 0.625, 10], "texture": "#0"}, - "east": {"uv": [10.25, 6, 10.375, 7], "texture": "#0"}, - "south": {"uv": [0.625, 9, 1.25, 10], "texture": "#0"}, - "west": {"uv": [10.25, 7.25, 10.375, 8.25], "texture": "#0"}, - "up": {"uv": [8, 10.625, 7.5, 10.5], "texture": "#0"}, - "down": {"uv": [8.75, 10.5, 8.25, 10.625], "texture": "#0"} - } - }, - { - "from": [7.4, -11.3, 7.4], - "to": [8.6, -10.1, 8.6], - "rotation": {"angle": -45, "axis": "z", "origin": [8, -10.7, 8]}, - "faces": { - "north": {"uv": [0, 10.75, 0.25, 11], "texture": "#0"}, - "east": {"uv": [0.5, 10.75, 0.75, 11], "texture": "#0"}, - "south": {"uv": [10.75, 1.5, 11, 1.75], "texture": "#0"}, - "west": {"uv": [10.75, 2, 11, 2.25], "texture": "#0"}, - "up": {"uv": [11, 4.75, 10.75, 4.5], "texture": "#0"}, - "down": {"uv": [11, 5, 10.75, 5.25], "texture": "#0"} - } - }, - { - "from": [7.8, -10.9, 7.3], - "to": [8.2, -10.5, 8.7], - "rotation": {"angle": -45, "axis": "z", "origin": [8, -10.7, 8]}, - "faces": { - "north": {"uv": [0.25, 0.75, 0.375, 0.875], "texture": "#0"}, - "east": {"uv": [0.25, 0, 0.5, 0.125], "texture": "#0"}, - "south": {"uv": [0.75, 0.25, 0.875, 0.375], "texture": "#0"}, - "west": {"uv": [0.25, 0.25, 0.5, 0.375], "texture": "#0"}, - "up": {"uv": [0.375, 0.75, 0.25, 0.5], "texture": "#0"}, - "down": {"uv": [0.875, 0, 0.75, 0.25], "texture": "#0"} - } - }, - { - "from": [7.8, 15.12, 7.3], - "to": [8.2, 15.52, 8.7], - "rotation": {"angle": -45, "axis": "z", "origin": [8, 15.32, 8]}, - "faces": { - "north": {"uv": [0.25, 0.75, 0.375, 0.875], "texture": "#0"}, - "east": {"uv": [0.25, 0, 0.5, 0.125], "texture": "#0"}, - "south": {"uv": [0.75, 0.25, 0.875, 0.375], "texture": "#0"}, - "west": {"uv": [0.25, 0.25, 0.5, 0.375], "texture": "#0"}, - "up": {"uv": [0.375, 0.75, 0.25, 0.5], "texture": "#0"}, - "down": {"uv": [0.875, 0, 0.75, 0.25], "texture": "#0"} - } - }, - { - "from": [7.39, -10.31, 7.39], - "to": [8.61, -9.84, 8.61], - "faces": { - "north": {"uv": [11, 1, 11.25, 1.125], "texture": "#0"}, - "east": {"uv": [1.5, 11, 1.75, 11.125], "texture": "#0"}, - "south": {"uv": [2, 11, 2.25, 11.125], "texture": "#0"}, - "west": {"uv": [2.5, 11, 2.75, 11.125], "texture": "#0"}, - "up": {"uv": [5.75, 11, 5.5, 10.75], "texture": "#0"}, - "down": {"uv": [6.25, 10.75, 6, 11], "texture": "#0"} - } - }, - { - "from": [7.39, 15.09, 7.39], - "to": [8.61, 15.56, 8.61], - "faces": { - "north": {"uv": [11, 2.5, 11.25, 2.625], "texture": "#0"}, - "east": {"uv": [3, 11, 3.25, 11.125], "texture": "#0"}, - "south": {"uv": [11, 3, 11.25, 3.125], "texture": "#0"}, - "west": {"uv": [3.5, 11, 3.75, 11.125], "texture": "#0"}, - "up": {"uv": [11, 6.25, 10.75, 6], "texture": "#0"}, - "down": {"uv": [6.75, 10.75, 6.5, 11], "texture": "#0"} - } - }, - { - "from": [7.39, 15.99, 7.39], - "to": [8.61, 17.71, 8.61], - "faces": { - "north": {"uv": [10.25, 8.5, 10.5, 8.875], "texture": "#0"}, - "east": {"uv": [10.25, 9.25, 10.5, 9.625], "texture": "#0"}, - "south": {"uv": [10.5, 0, 10.75, 0.375], "texture": "#0"}, - "west": {"uv": [10.5, 0.75, 10.75, 1.125], "texture": "#0"}, - "up": {"uv": [11, 6.75, 10.75, 6.5], "texture": "#0"}, - "down": {"uv": [7.25, 10.75, 7, 11], "texture": "#0"} - } - }, - { - "from": [6.65, 17.9, 6.68], - "to": [9.15, 20.4, 9.315], - "rotation": {"angle": -45, "axis": "z", "origin": [7.6, 18.9, 8.4]}, - "faces": { - "north": {"uv": [9.25, 1.5, 9.875, 2.125], "texture": "#0"}, - "east": {"uv": [9.25, 2.5, 9.875, 3.125], "texture": "#0"}, - "south": {"uv": [9.25, 6, 9.875, 6.625], "texture": "#0"}, - "west": {"uv": [9.25, 7, 9.875, 7.625], "texture": "#0"}, - "up": {"uv": [9.875, 8.625, 9.25, 8], "texture": "#0"}, - "down": {"uv": [9.875, 9, 9.25, 9.625], "texture": "#0"} - } - }, - { - "from": [7.8, 18.05, 6.48], - "to": [8.6, 18.85, 9.515], - "rotation": {"angle": -45, "axis": "z", "origin": [8.6, 18.9, 8.4]}, - "faces": { - "north": {"uv": [10.75, 7, 11, 7.25], "texture": "#0"}, - "east": {"uv": [10, 5.5, 10.75, 5.75], "texture": "#0"}, - "south": {"uv": [10.75, 7.5, 11, 7.75], "texture": "#0"}, - "west": {"uv": [0, 10.25, 0.75, 10.5], "texture": "#0"}, - "up": {"uv": [1.25, 11, 1, 10.25], "texture": "#0"}, - "down": {"uv": [10.5, 1.5, 10.25, 2.25], "texture": "#0"} - } - }, - { - "from": [8, 16.8, 7.6], - "to": [8, 17.8, 9.6], - "rotation": {"angle": -45, "axis": "x", "origin": [7.9, 17.8, 8.6]}, - "faces": { - "north": {"uv": [4, 11, 3.875, 11.25], "texture": "#0"}, - "east": {"uv": [10.25, 2.5, 10.75, 2.75], "texture": "#0"}, - "south": {"uv": [5.125, 11.25, 5.25, 11.5], "texture": "#0"}, - "west": {"uv": [10.25, 3, 10.75, 3.25], "texture": "#0"}, - "up": {"uv": [5.375, 11.75, 5.5, 11.25], "texture": "#0"}, - "down": {"uv": [5.625, 11.25, 5.75, 11.75], "texture": "#0"} - } - }, - { - "from": [8, 16.8, 6.4], - "to": [8, 17.8, 8.4], - "rotation": {"angle": 45, "axis": "x", "origin": [7.9, 17.8, 7.4]}, - "faces": { - "north": {"uv": [7.625, 10.875, 8, 11.375], "texture": "#0"}, - "east": {"uv": [7.5, 9, 8, 9.25], "texture": "#0"}, - "south": {"uv": [7.9, 7.5, 8.125, 7.75], "texture": "#0"}, - "west": {"uv": [9.25, 3.5, 9.75, 3.75], "texture": "#0"}, - "up": {"uv": [8.875, 11, 9.125, 10.5], "texture": "#0"}, - "down": {"uv": [10.875, 3.5, 11.125, 4], "texture": "#0"} - } - }, - { - "from": [8.10331, 19.03251, 6.9], - "to": [10.78331, 20.70751, 9.1], - "rotation": {"angle": 22.5, "axis": "z", "origin": [8.77331, 19.70251, 7]}, - "faces": { - "north": {"uv": [0, 8, 1, 8.625], "texture": "#0"}, - "east": {"uv": [7.5, 9.5, 8.125, 10.125], "texture": "#0"}, - "south": {"uv": [8, 0, 9, 0.625], "texture": "#0"}, - "west": {"uv": [1.5, 10, 2.125, 10.625], "texture": "#0"}, - "up": {"uv": [9, 1.625, 8, 1], "texture": "#0"}, - "down": {"uv": [2.25, 8, 1.25, 8.625], "texture": "#0"} - } - }, - { - "from": [5.21669, 19.03251, 6.9], - "to": [7.89669, 20.70751, 9.1], - "rotation": {"angle": -22.5, "axis": "z", "origin": [7.22669, 19.70251, 7]}, - "faces": { - "north": {"uv": [8, 2, 9, 2.625], "texture": "#0"}, - "east": {"uv": [2.5, 10, 3.125, 10.625], "texture": "#0"}, - "south": {"uv": [2.5, 8, 3.5, 8.625], "texture": "#0"}, - "west": {"uv": [3.5, 10, 4.125, 10.625], "texture": "#0"}, - "up": {"uv": [9, 3.625, 8, 3], "texture": "#0"}, - "down": {"uv": [4.75, 8, 3.75, 8.625], "texture": "#0"} - } - }, - { - "from": [6.48891, 21.54663, 6.99], - "to": [8.98891, 24.04663, 8.99], - "rotation": {"angle": -45, "axis": "z", "origin": [7.98891, 22.54663, 7.99]}, - "faces": { - "north": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "east": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "south": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "west": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "up": {"uv": [16, 16, 0, 0], "texture": "#2"}, - "down": {"uv": [16, 0, 0, 16], "texture": "#2"} - } - }, - { - "from": [6.48891, 19.04663, 6.99], - "to": [8.98891, 21.54663, 8.99], - "rotation": {"angle": -45, "axis": "z", "origin": [7.98891, 20.04663, 7.991]}, - "faces": { - "north": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "east": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "south": {"uv": [0, 0, 15.9, 16], "texture": "#2"}, - "west": {"uv": [0, 0, 15.9, 15.8], "texture": "#2"}, - "up": {"uv": [15.9, 15.8, 0, 0], "texture": "#2"}, - "down": {"uv": [16, 0, 0, 15.8], "texture": "#2"} - } - }, - { - "from": [6.22114, 20.40018, 6.989], - "to": [9.75668, 22.90018, 8.991], - "faces": { - "north": {"uv": [0, 0, 15.975, 16.025], "texture": "#2"}, - "east": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "south": {"uv": [0, 0, 15.975, 16], "texture": "#2"}, - "west": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "up": {"uv": [15.975, 16, 0, 0], "texture": "#2"}, - "down": {"uv": [16, 0, 0, 16], "texture": "#2"} - } - }, - { - "from": [7.17754, 24.8802, 7.99], - "to": [8.80028, 27.1202, 7.99], - "faces": { - "north": {"uv": [8.25, 7, 8.875, 7.875], "texture": "#0"}, - "east": {"uv": [5, 11, 5.225, 12.1375], "texture": "#0"}, - "south": {"uv": [8.25, 8.25, 8.875, 9.125], "texture": "#0"}, - "west": {"uv": [5.25, 11, 5.475, 12.1375], "texture": "#0"}, - "up": {"uv": [11.85, 4.25, 11, 4.5125], "texture": "#0"}, - "down": {"uv": [11.85, 5.5, 11, 5.7625], "texture": "#0"} - } - }, - { - "name": "cube_outline", - "from": [9.08891, 24.14663, 9.09], - "to": [6.38891, 21.44663, 6.89], - "rotation": {"angle": -45, "axis": "z", "origin": [7.98891, 22.54663, 7.99]}, - "faces": { - "north": {"uv": [0.75, 11.125, 0.875, 11.25], "rotation": 180, "texture": "#0"}, - "east": {"uv": [0.875, 11.125, 1, 11.25], "rotation": 180, "texture": "#0"}, - "south": {"uv": [1, 11.125, 1.125, 11.25], "rotation": 180, "texture": "#0"}, - "west": {"uv": [1.125, 11.125, 1.25, 11.25], "rotation": 180, "texture": "#0"}, - "up": {"uv": [1.375, 11.125, 1.25, 11.25], "rotation": 180, "texture": "#0"}, - "down": {"uv": [1.5, 11.25, 1.375, 11.125], "rotation": 180, "texture": "#0"} - } - }, - { - "name": "cube_outline", - "from": [9.85668, 23.00018, 9.091], - "to": [6.12114, 20.30018, 6.889], - "faces": { - "north": {"uv": [0, 11.125, 0.125, 11.25], "rotation": 180, "texture": "#0"}, - "east": {"uv": [0.125, 11.125, 0.25, 11.25], "rotation": 180, "texture": "#0"}, - "south": {"uv": [0.25, 11.125, 0.375, 11.25], "rotation": 180, "texture": "#0"}, - "west": {"uv": [0.375, 11.125, 0.5, 11.25], "rotation": 180, "texture": "#0"}, - "up": {"uv": [0.625, 11.125, 0.5, 11.25], "rotation": 180, "texture": "#0"}, - "down": {"uv": [0.75, 11.25, 0.625, 11.125], "rotation": 180, "texture": "#0"} - } - }, - { - "name": "cube_outline", - "from": [9.08891, 21.64663, 9.09], - "to": [6.38891, 18.94663, 6.89], - "rotation": {"angle": -45, "axis": "z", "origin": [7.98891, 20.04663, 7.991]}, - "faces": { - "north": {"uv": [1.5, 11.125, 1.625, 11.25], "rotation": 180, "texture": "#0"}, - "east": {"uv": [1.625, 11.125, 1.75, 11.25], "rotation": 180, "texture": "#0"}, - "south": {"uv": [1.75, 11.125, 1.875, 11.25], "rotation": 180, "texture": "#0"}, - "west": {"uv": [1.875, 11.125, 2, 11.25], "rotation": 180, "texture": "#0"}, - "up": {"uv": [2.125, 11.125, 2, 11.25], "rotation": 180, "texture": "#0"}, - "down": {"uv": [2.25, 11.25, 2.125, 11.125], "rotation": 180, "texture": "#0"} - } - }, - { - "from": [7.34391, 24.0552, 7.4525], - "to": [8.63391, 25.3452, 8.5275], - "rotation": {"angle": -45, "axis": "z", "origin": [7.98891, 24.70019, 8.00075]}, - "faces": { - "north": {"uv": [5.75, 8, 5, 8.75], "texture": "#0"}, - "east": {"uv": [4.5, 9.5, 3.875, 8.75], "texture": "#0"}, - "south": {"uv": [8.75, 4, 8, 4.75], "texture": "#0"}, - "west": {"uv": [3.75, 8.75, 3.125, 9.5], "texture": "#0"}, - "up": {"uv": [4.875, 10.75, 4.125, 11.375], "texture": "#0"}, - "down": {"uv": [9.75, 4, 9, 4.625], "texture": "#0"} - } - }, - { - "from": [6.95149, 23.04957, 7.249], - "to": [8.72989, 24.82797, 8.731], - "rotation": {"angle": -45, "axis": "z", "origin": [7.9889, 23.79059, 7.87144]}, - "faces": { - "north": {"uv": [8, 5, 8.75, 5.75], "texture": "#0"}, - "east": {"uv": [4.5, 9, 5.125, 9.75], "texture": "#0"}, - "south": {"uv": [6, 8, 6.75, 8.75], "texture": "#0"}, - "west": {"uv": [9, 5, 9.625, 5.75], "texture": "#0"}, - "up": {"uv": [6.25, 9.625, 5.5, 9], "texture": "#0"}, - "down": {"uv": [7.25, 9, 6.5, 9.625], "texture": "#0"} - } - }, - { - "from": [5.266, 20.3249, 7.76026], - "to": [7.0084, 20.8001, 8.23546], - "rotation": {"angle": -45, "axis": "z", "origin": [7.0084, 20.4833, 7.83946]}, - "faces": { - "north": {"uv": [5.875, 5.75, 4.5, 6.125], "texture": "#1"}, - "east": {"uv": [4.875, 6.5, 4.5, 6.875], "texture": "#1"}, - "south": {"uv": [7.125, 5, 5.75, 5.375], "texture": "#1"}, - "west": {"uv": [6.875, 2, 6.5, 2.375], "texture": "#1"}, - "up": {"uv": [6, 0.375, 7.375, 0], "texture": "#1"}, - "down": {"uv": [6, 0.75, 7.375, 1.125], "texture": "#1"} - } - }, - { - "from": [4.16353, 23.23946, 7.83914], - "to": [5.90593, 23.55626, 8.15594], - "rotation": {"angle": -45, "axis": "z", "origin": [5.90593, 23.23946, 7.83946]}, - "faces": { - "north": {"uv": [7.375, 3.25, 6, 3.5], "texture": "#1"}, - "east": {"uv": [6, 6.5, 5.75, 6.75], "texture": "#1"}, - "south": {"uv": [7.625, 5.75, 6.25, 6], "texture": "#1"}, - "west": {"uv": [5.5, 6.5, 5.25, 6.75], "texture": "#1"}, - "up": {"uv": [6.25, 6.5, 7.625, 6.25], "texture": "#1"}, - "down": {"uv": [6.5, 1.5, 7.875, 1.75], "texture": "#1"} - } - }, - { - "from": [5.77633, 21.71536, 7.83788], - "to": [6.09313, 23.45776, 8.15468], - "faces": { - "north": {"uv": [6.25, 1.5, 6, 2.875], "texture": "#1"}, - "east": {"uv": [1.25, 6.25, 1, 7.625], "texture": "#1"}, - "south": {"uv": [0.75, 6.25, 0.5, 7.625], "texture": "#1"}, - "west": {"uv": [0.25, 6.25, 0, 7.625], "texture": "#1"}, - "up": {"uv": [1.5, 5.5, 1.75, 5.25], "texture": "#1"}, - "down": {"uv": [6.5, 2.75, 6.75, 3], "texture": "#1"} - } - }, - { - "from": [1.43272, 22.09898, 7.90282], - "to": [4.44232, 24.15818, 7.90282], - "rotation": {"angle": -45, "axis": "z", "origin": [4.44232, 21.78218, 7.90282]}, - "faces": { - "north": {"uv": [2.375, 0, 0, 1.625], "texture": "#1"}, - "east": {"uv": [1.75, 6.75, 1.75, 8.375], "texture": "#1"}, - "south": {"uv": [2.375, 2, 0, 3.625], "texture": "#1"}, - "west": {"uv": [1.5, 6.75, 1.5, 8.375], "texture": "#1"}, - "up": {"uv": [6, 3.75, 8.375, 3.75], "texture": "#1"}, - "down": {"uv": [6.25, 6.75, 8.625, 6.75], "texture": "#1"} - } - }, - { - "from": [3.61751, 21.4698, 7.90314], - "to": [6.08855, 23.11716, 7.90314], - "faces": { - "north": {"uv": [4.75, 0, 2.75, 1.25], "texture": "#1"}, - "east": {"uv": [2.25, 6.75, 2.25, 8], "texture": "#1"}, - "south": {"uv": [4.75, 1.5, 2.75, 2.75], "texture": "#1"}, - "west": {"uv": [2, 6.75, 2, 8], "texture": "#1"}, - "up": {"uv": [7, 2.75, 9, 2.75], "texture": "#1"}, - "down": {"uv": [7, 3, 9, 3], "texture": "#1"} - } - }, - { - "from": [3.64919, 20.55108, 7.90346], - "to": [6.08855, 21.84996, 7.90346], - "rotation": {"angle": 22.5, "axis": "z", "origin": [4.58375, 21.58068, 7.90282]}, - "faces": { - "north": {"uv": [4.625, 3, 2.75, 4], "texture": "#1"}, - "east": {"uv": [2.75, 6.75, 2.75, 7.75], "texture": "#1"}, - "south": {"uv": [1.875, 4, 0, 5], "texture": "#1"}, - "west": {"uv": [2.5, 6.75, 2.5, 7.75], "texture": "#1"}, - "up": {"uv": [5.25, 7, 7.125, 7], "texture": "#1"}, - "down": {"uv": [7.25, 2, 9.125, 2], "texture": "#1"} - } - }, - { - "from": [4.78967, 19.22052, 7.90314], - "to": [6.65879, 20.86788, 7.90314], - "rotation": {"angle": 45, "axis": "z", "origin": [5.15399, 20.25012, 7.90282]}, - "faces": { - "north": {"uv": [3.75, 4.25, 2.25, 5.5], "texture": "#1"}, - "east": {"uv": [3.25, 6.75, 3.25, 8], "texture": "#1"}, - "south": {"uv": [5.5, 4.25, 4, 5.5], "texture": "#1"}, - "west": {"uv": [3, 6.75, 3, 8], "texture": "#1"}, - "up": {"uv": [7.25, 2.25, 8.75, 2.25], "texture": "#1"}, - "down": {"uv": [7.25, 2.5, 8.75, 2.5], "texture": "#1"} - } - }, - { - "from": [2.69992, 23.3345, 8.02954], - "to": [4.12552, 24.06314, 8.02954], - "rotation": {"angle": -45, "axis": "z", "origin": [4.12552, 21.71882, 8.02954]}, - "faces": { - "north": {"uv": [1.125, 5.25, 0, 5.875], "texture": "#1"}, - "east": {"uv": [3.75, 6.75, 3.75, 7.375], "texture": "#1"}, - "south": {"uv": [2.625, 5.75, 1.5, 6.375], "texture": "#1"}, - "west": {"uv": [3.5, 6.75, 3.5, 7.375], "texture": "#1"}, - "up": {"uv": [4, 7.25, 5.125, 7.25], "texture": "#1"}, - "down": {"uv": [7.25, 4, 8.375, 4], "texture": "#1"} - } - }, - { - "from": [3.6028, 20.5625, 7.87114], - "to": [5.0284, 21.29114, 7.87114], - "rotation": {"angle": -45, "axis": "z", "origin": [5.01256, 18.93098, 7.87114]}, - "faces": { - "north": {"uv": [4.125, 5.75, 3, 6.375], "texture": "#1"}, - "east": {"uv": [6.25, 7.25, 6.25, 7.875], "texture": "#1"}, - "south": {"uv": [6.875, 4, 5.75, 4.625], "texture": "#1"}, - "west": {"uv": [6, 7.25, 6, 7.875], "texture": "#1"}, - "up": {"uv": [7.25, 4.75, 8.375, 4.75], "texture": "#1"}, - "down": {"uv": [6.5, 7.25, 7.625, 7.25], "texture": "#1"} - } - }, - { - "from": [5.06908, 21.24691, 7.87146], - "to": [5.79772, 23.36947, 7.87146], - "faces": { - "north": {"uv": [5.625, 0, 5, 1.625], "texture": "#1"}, - "east": {"uv": [5.75, 7.25, 5.75, 8.875], "texture": "#1"}, - "south": {"uv": [5.625, 2, 5, 3.625], "texture": "#1"}, - "west": {"uv": [5.5, 7.25, 5.5, 8.875], "texture": "#1"}, - "up": {"uv": [7.25, 4.25, 7.875, 4.25], "texture": "#1"}, - "down": {"uv": [7.25, 4.5, 7.875, 4.5], "texture": "#1"} - } - }, - { - "from": [2.69992, 23.3345, 7.87114], - "to": [4.12552, 24.06314, 7.87114], - "rotation": {"angle": -45, "axis": "z", "origin": [4.12552, 21.71882, 7.87114]}, - "faces": { - "north": {"uv": [1.125, 5.25, 0, 5.875], "texture": "#1"}, - "east": {"uv": [3.75, 6.75, 3.75, 7.375], "texture": "#1"}, - "south": {"uv": [2.625, 5.75, 1.5, 6.375], "texture": "#1"}, - "west": {"uv": [3.5, 6.75, 3.5, 7.375], "texture": "#1"}, - "up": {"uv": [4, 7.25, 5.125, 7.25], "texture": "#1"}, - "down": {"uv": [7.25, 4, 8.375, 4], "texture": "#1"} - } - }, - { - "from": [3.6028, 20.5625, 8.02954], - "to": [5.0284, 21.29114, 8.02954], - "rotation": {"angle": -45, "axis": "z", "origin": [5.01256, 18.93098, 8.02954]}, - "faces": { - "north": {"uv": [4.125, 5.75, 3, 6.375], "texture": "#1"}, - "east": {"uv": [6.25, 7.25, 6.25, 7.875], "texture": "#1"}, - "south": {"uv": [6.875, 4, 5.75, 4.625], "texture": "#1"}, - "west": {"uv": [6, 7.25, 6, 7.875], "texture": "#1"}, - "up": {"uv": [7.25, 4.75, 8.375, 4.75], "texture": "#1"}, - "down": {"uv": [6.5, 7.25, 7.625, 7.25], "texture": "#1"} - } - }, - { - "from": [5.06908, 21.24691, 8.02986], - "to": [5.79772, 23.36947, 8.02986], - "faces": { - "north": {"uv": [5.625, 0, 5, 1.625], "texture": "#1"}, - "east": {"uv": [5.75, 7.25, 5.75, 8.875], "texture": "#1"}, - "south": {"uv": [5.625, 2, 5, 3.625], "texture": "#1"}, - "west": {"uv": [5.5, 7.25, 5.5, 8.875], "texture": "#1"}, - "up": {"uv": [7.25, 4.25, 7.875, 4.25], "texture": "#1"}, - "down": {"uv": [7.25, 4.5, 7.875, 4.5], "texture": "#1"} - } - }, - { - "from": [8.9916, 20.3249, 7.76026], - "to": [10.734, 20.8001, 8.23546], - "rotation": {"angle": 45, "axis": "z", "origin": [8.9916, 20.4833, 7.83946]}, - "faces": { - "north": {"uv": [4.5, 5.75, 5.875, 6.125], "texture": "#1"}, - "east": {"uv": [6.5, 2, 6.875, 2.375], "texture": "#1"}, - "south": {"uv": [5.75, 5, 7.125, 5.375], "texture": "#1"}, - "west": {"uv": [4.5, 6.5, 4.875, 6.875], "texture": "#1"}, - "up": {"uv": [7.375, 0.375, 6, 0], "texture": "#1"}, - "down": {"uv": [7.375, 0.75, 6, 1.125], "texture": "#1"} - } - }, - { - "from": [10.09407, 23.23946, 7.83914], - "to": [11.83647, 23.55626, 8.15594], - "rotation": {"angle": 45, "axis": "z", "origin": [10.09407, 23.23946, 7.83946]}, - "faces": { - "north": {"uv": [6, 3.25, 7.375, 3.5], "texture": "#1"}, - "east": {"uv": [5.25, 6.5, 5.5, 6.75], "texture": "#1"}, - "south": {"uv": [6.25, 5.75, 7.625, 6], "texture": "#1"}, - "west": {"uv": [5.75, 6.5, 6, 6.75], "texture": "#1"}, - "up": {"uv": [7.625, 6.5, 6.25, 6.25], "texture": "#1"}, - "down": {"uv": [7.875, 1.5, 6.5, 1.75], "texture": "#1"} - } - }, - { - "from": [9.90687, 21.71536, 7.83788], - "to": [10.22367, 23.45776, 8.15468], - "faces": { - "north": {"uv": [6, 1.5, 6.25, 2.875], "texture": "#1"}, - "east": {"uv": [0, 6.25, 0.25, 7.625], "texture": "#1"}, - "south": {"uv": [0.5, 6.25, 0.75, 7.625], "texture": "#1"}, - "west": {"uv": [1, 6.25, 1.25, 7.625], "texture": "#1"}, - "up": {"uv": [1.75, 5.5, 1.5, 5.25], "texture": "#1"}, - "down": {"uv": [6.75, 2.75, 6.5, 3], "texture": "#1"} - } - }, - { - "from": [11.55768, 22.09898, 7.90282], - "to": [14.56728, 24.15818, 7.90282], - "rotation": {"angle": 45, "axis": "z", "origin": [11.55768, 21.78218, 7.90282]}, - "faces": { - "north": {"uv": [0, 0, 2.375, 1.625], "texture": "#1"}, - "east": {"uv": [1.5, 6.75, 1.5, 8.375], "texture": "#1"}, - "south": {"uv": [0, 2, 2.375, 3.625], "texture": "#1"}, - "west": {"uv": [1.75, 6.75, 1.75, 8.375], "texture": "#1"}, - "up": {"uv": [8.375, 3.75, 6, 3.75], "texture": "#1"}, - "down": {"uv": [8.625, 6.75, 6.25, 6.75], "texture": "#1"} - } - }, - { - "from": [9.91145, 21.4698, 7.90314], - "to": [12.38249, 23.11716, 7.90314], - "faces": { - "north": {"uv": [2.75, 0, 4.75, 1.25], "texture": "#1"}, - "east": {"uv": [2, 6.75, 2, 8], "texture": "#1"}, - "south": {"uv": [2.75, 1.5, 4.75, 2.75], "texture": "#1"}, - "west": {"uv": [2.25, 6.75, 2.25, 8], "texture": "#1"}, - "up": {"uv": [9, 2.75, 7, 2.75], "texture": "#1"}, - "down": {"uv": [9, 3, 7, 3], "texture": "#1"} - } - }, - { - "from": [9.91145, 20.55108, 7.90346], - "to": [12.35081, 21.84996, 7.90346], - "rotation": {"angle": -22.5, "axis": "z", "origin": [11.41625, 21.58068, 7.90282]}, - "faces": { - "north": {"uv": [2.75, 3, 4.625, 4], "texture": "#1"}, - "east": {"uv": [2.5, 6.75, 2.5, 7.75], "texture": "#1"}, - "south": {"uv": [0, 4, 1.875, 5], "texture": "#1"}, - "west": {"uv": [2.75, 6.75, 2.75, 7.75], "texture": "#1"}, - "up": {"uv": [7.125, 7, 5.25, 7], "texture": "#1"}, - "down": {"uv": [9.125, 2, 7.25, 2], "texture": "#1"} - } - }, - { - "from": [9.34121, 19.22052, 7.90314], - "to": [11.21033, 20.86788, 7.90314], - "rotation": {"angle": -45, "axis": "z", "origin": [10.84601, 20.25012, 7.90282]}, - "faces": { - "north": {"uv": [2.25, 4.25, 3.75, 5.5], "texture": "#1"}, - "east": {"uv": [3, 6.75, 3, 8], "texture": "#1"}, - "south": {"uv": [4, 4.25, 5.5, 5.5], "texture": "#1"}, - "west": {"uv": [3.25, 6.75, 3.25, 8], "texture": "#1"}, - "up": {"uv": [8.75, 2.25, 7.25, 2.25], "texture": "#1"}, - "down": {"uv": [8.75, 2.5, 7.25, 2.5], "texture": "#1"} - } - }, - { - "from": [11.87448, 23.3345, 8.02954], - "to": [13.30008, 24.06314, 8.02954], - "rotation": {"angle": 45, "axis": "z", "origin": [11.87448, 21.71882, 8.02954]}, - "faces": { - "north": {"uv": [0, 5.25, 1.125, 5.875], "texture": "#1"}, - "east": {"uv": [3.5, 6.75, 3.5, 7.375], "texture": "#1"}, - "south": {"uv": [1.5, 5.75, 2.625, 6.375], "texture": "#1"}, - "west": {"uv": [3.75, 6.75, 3.75, 7.375], "texture": "#1"}, - "up": {"uv": [5.125, 7.25, 4, 7.25], "texture": "#1"}, - "down": {"uv": [8.375, 4, 7.25, 4], "texture": "#1"} - } - }, - { - "from": [10.9716, 20.5625, 7.87114], - "to": [12.3972, 21.29114, 7.87114], - "rotation": {"angle": 45, "axis": "z", "origin": [10.98744, 18.93098, 7.87114]}, - "faces": { - "north": {"uv": [3, 5.75, 4.125, 6.375], "texture": "#1"}, - "east": {"uv": [6, 7.25, 6, 7.875], "texture": "#1"}, - "south": {"uv": [5.75, 4, 6.875, 4.625], "texture": "#1"}, - "west": {"uv": [6.25, 7.25, 6.25, 7.875], "texture": "#1"}, - "up": {"uv": [8.375, 4.75, 7.25, 4.75], "texture": "#1"}, - "down": {"uv": [7.625, 7.25, 6.5, 7.25], "texture": "#1"} - } - }, - { - "from": [10.20228, 21.24691, 7.87146], - "to": [10.93092, 23.36947, 7.87146], - "faces": { - "north": {"uv": [5, 0, 5.625, 1.625], "texture": "#1"}, - "east": {"uv": [5.5, 7.25, 5.5, 8.875], "texture": "#1"}, - "south": {"uv": [5, 2, 5.625, 3.625], "texture": "#1"}, - "west": {"uv": [5.75, 7.25, 5.75, 8.875], "texture": "#1"}, - "up": {"uv": [7.875, 4.25, 7.25, 4.25], "texture": "#1"}, - "down": {"uv": [7.875, 4.5, 7.25, 4.5], "texture": "#1"} - } - }, - { - "from": [11.87448, 23.3345, 7.87114], - "to": [13.30008, 24.06314, 7.87114], - "rotation": {"angle": 45, "axis": "z", "origin": [11.87448, 21.71882, 7.87114]}, - "faces": { - "north": {"uv": [0, 5.25, 1.125, 5.875], "texture": "#1"}, - "east": {"uv": [3.5, 6.75, 3.5, 7.375], "texture": "#1"}, - "south": {"uv": [1.5, 5.75, 2.625, 6.375], "texture": "#1"}, - "west": {"uv": [3.75, 6.75, 3.75, 7.375], "texture": "#1"}, - "up": {"uv": [5.125, 7.25, 4, 7.25], "texture": "#1"}, - "down": {"uv": [8.375, 4, 7.25, 4], "texture": "#1"} - } - }, - { - "from": [10.9716, 20.5625, 8.02954], - "to": [12.3972, 21.29114, 8.02954], - "rotation": {"angle": 45, "axis": "z", "origin": [10.98744, 18.93098, 8.02954]}, - "faces": { - "north": {"uv": [3, 5.75, 4.125, 6.375], "texture": "#1"}, - "east": {"uv": [6.25625, 7.25, 6, 8.29375], "texture": "#1"}, - "south": {"uv": [5.75, 4, 6.875, 4.625], "texture": "#1"}, - "west": {"uv": [6.50625, 7.25, 6.25, 8.29375], "texture": "#1"}, - "up": {"uv": [8.375, 4.75, 7.50625, 5.16875], "texture": "#1"}, - "down": {"uv": [7.625, 7.25, 6.75625, 7.66875], "texture": "#1"} - } - }, - { - "from": [10.20228, 21.24691, 8.02986], - "to": [10.93092, 23.36947, 8.02986], - "faces": { - "north": {"uv": [5, 0, 5.625, 1.625], "texture": "#1"}, - "east": {"uv": [5.5, 7.25, 5.5, 8.875], "texture": "#1"}, - "south": {"uv": [5, 2, 5.625, 3.625], "texture": "#1"}, - "west": {"uv": [5.75, 7.25, 5.75, 8.875], "texture": "#1"}, - "up": {"uv": [7.875, 4.25, 7.25, 4.25], "texture": "#1"}, - "down": {"uv": [7.875, 4.5, 7.25, 4.5], "texture": "#1"} - } - }, - { - "from": [9.00728, 15.90762, 7.80282], - "to": [11.06648, 18.91722, 7.80282], - "rotation": {"angle": 45, "axis": "z", "origin": [8.69048, 18.91722, 7.80282]}, - "faces": { - "north": {"uv": [0, 0, 2.375, 1.625], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.375, 3.75, 6, 3.75], "rotation": 90, "texture": "#1"}, - "south": {"uv": [0, 2, 2.375, 3.625], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.625, 6.75, 6.25, 6.75], "rotation": 90, "texture": "#1"}, - "up": {"uv": [1.75, 6.75, 1.75, 8.375], "rotation": 90, "texture": "#1"}, - "down": {"uv": [1.5, 6.75, 1.5, 8.375], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [8.37811, 18.09241, 7.80314], - "to": [10.02547, 20.56345, 7.80314], - "faces": { - "north": {"uv": [2.75, 0, 4.75, 1.25], "rotation": 270, "texture": "#1"}, - "east": {"uv": [9, 2.75, 7, 2.75], "rotation": 90, "texture": "#1"}, - "south": {"uv": [2.75, 1.5, 4.75, 2.75], "rotation": 90, "texture": "#1"}, - "west": {"uv": [9, 3, 7, 3], "rotation": 90, "texture": "#1"}, - "up": {"uv": [2.25, 6.75, 2.25, 8], "rotation": 90, "texture": "#1"}, - "down": {"uv": [2, 6.75, 2, 8], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [10.2428, 17.17482, 7.92954], - "to": [10.97144, 18.60042, 7.92954], - "rotation": {"angle": 45, "axis": "z", "origin": [8.62712, 18.60042, 7.92954]}, - "faces": { - "north": {"uv": [0, 5.25, 1.125, 5.875], "rotation": 270, "texture": "#1"}, - "east": {"uv": [5.625, 7.25, 4, 7.6875], "rotation": 90, "texture": "#1"}, - "south": {"uv": [1.5, 5.75, 2.625, 6.375], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.875, 4, 7.25, 4.4375], "rotation": 90, "texture": "#1"}, - "up": {"uv": [3.75, 6.75, 4.25, 7.8125], "rotation": 90, "texture": "#1"}, - "down": {"uv": [3.5, 6.75, 4, 7.8125], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [8.15522, 19.54399, 7.77146], - "to": [10.27778, 20.27263, 7.77146], - "faces": { - "north": {"uv": [5, 0, 5.625, 1.625], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.01875, 4.25, 7.25, 4.54375], "rotation": 90, "texture": "#1"}, - "south": {"uv": [5, 2, 5.625, 3.625], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.01875, 4.5, 7.25, 4.79375], "rotation": 90, "texture": "#1"}, - "up": {"uv": [5.75, 7.25, 5.89375, 9.16875], "rotation": 90, "texture": "#1"}, - "down": {"uv": [5.5, 7.25, 5.64375, 9.16875], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [10.2428, 17.17482, 7.77114], - "to": [10.97144, 18.60042, 7.77114], - "rotation": {"angle": 45, "axis": "z", "origin": [8.62712, 18.60042, 7.77114]}, - "faces": { - "north": {"uv": [0, 5.25, 1.125, 5.875], "rotation": 270, "texture": "#1"}, - "east": {"uv": [5.3652, 10.89375, 3.33228, 10.38637], "rotation": 90, "texture": "#1"}, - "south": {"uv": [1.5, 5.75, 2.625, 6.375], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.74375, 7.02613, 6.71083, 6.51875], "rotation": 90, "texture": "#1"}, - "up": {"uv": [3.07239, 9.95498, 3.93581, 10.87888], "rotation": 90, "texture": "#1"}, - "down": {"uv": [2.8125, 9.95498, 3.67592, 10.87888], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [8.15522, 19.54399, 7.92986], - "to": [10.27778, 20.27263, 7.92986], - "faces": { - "north": {"uv": [5, 0, 5.625, 1.625], "rotation": 270, "texture": "#1"}, - "east": {"uv": [4.0625, 6.6875, 2.75, 7.44375], "rotation": 90, "texture": "#1"}, - "south": {"uv": [5, 2, 5.625, 3.625], "rotation": 90, "texture": "#1"}, - "west": {"uv": [4.0625, 6.9375, 2.75, 7.69375], "rotation": 90, "texture": "#1"}, - "up": {"uv": [1.25, 9.6875, 1.9375, 12.06875], "rotation": 90, "texture": "#1"}, - "down": {"uv": [1, 9.6875, 1.6875, 12.06875], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [4.93352, 15.90762, 7.80282], - "to": [6.99272, 18.91722, 7.80282], - "rotation": {"angle": -45, "axis": "z", "origin": [7.30952, 18.91722, 7.80282]}, - "faces": { - "north": {"uv": [0, 1.625, 2.375, 0], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.625, 6.75, 6.25, 6.99375], "rotation": 90, "texture": "#1"}, - "south": {"uv": [0, 3.625, 2.375, 2], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.375, 3.75, 6, 3.99375], "rotation": 90, "texture": "#1"}, - "up": {"uv": [1.75, 8.61875, 2.5875, 6.75], "rotation": 90, "texture": "#1"}, - "down": {"uv": [1.5, 8.61875, 2.3375, 6.75], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [5.97453, 18.09241, 7.80314], - "to": [7.62189, 20.56345, 7.80314], - "faces": { - "north": {"uv": [2.75, 1.25, 4.75, 0], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.74375, 7.3, 5.825, 7.90625], "rotation": 90, "texture": "#1"}, - "south": {"uv": [2.75, 2.75, 4.75, 1.5], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.74375, 7.05, 5.825, 7.65625], "rotation": 90, "texture": "#1"}, - "up": {"uv": [1.075, 12.90625, 1.99375, 11.05], "rotation": 90, "texture": "#1"}, - "down": {"uv": [0.825, 12.90625, 1.74375, 11.05], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [5.02856, 17.17482, 7.92954], - "to": [5.7572, 18.60042, 7.92954], - "rotation": {"angle": -45, "axis": "z", "origin": [7.37288, 18.60042, 7.92954]}, - "faces": { - "north": {"uv": [0, 5.875, 1.125, 5.25], "rotation": 270, "texture": "#1"}, - "east": {"uv": [5.46875, 7.25, 3.9375, 7.65625], "rotation": 90, "texture": "#1"}, - "south": {"uv": [1.5, 6.375, 2.625, 5.75], "rotation": 90, "texture": "#1"}, - "west": {"uv": [2.21875, 10.5, 0.6875, 10.90625], "rotation": 90, "texture": "#1"}, - "up": {"uv": [0.4375, 11.03125, 0.84375, 10], "rotation": 90, "texture": "#1"}, - "down": {"uv": [0.1875, 11.03125, 0.59375, 10], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [5.72222, 19.54399, 7.77146], - "to": [7.84478, 20.27263, 7.77146], - "faces": { - "north": {"uv": [5, 1.625, 5.625, 0], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.275, 4.7875, 7.34375, 4.4375], "rotation": 90, "texture": "#1"}, - "south": {"uv": [5, 3.625, 5.625, 2], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.275, 4.5375, 7.34375, 4.1875], "rotation": 90, "texture": "#1"}, - "up": {"uv": [5.75, 8.875, 6.05625, 7.25], "rotation": 90, "texture": "#1"}, - "down": {"uv": [5.5, 8.875, 5.80625, 7.25], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [5.02856, 17.17482, 7.77114], - "to": [5.7572, 18.60042, 7.77114], - "rotation": {"angle": -45, "axis": "z", "origin": [7.37288, 18.60042, 7.77114]}, - "faces": { - "north": {"uv": [0, 5.875, 1.125, 5.25], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.375, 4, 7.25, 4], "rotation": 90, "texture": "#1"}, - "south": {"uv": [1.5, 6.375, 2.625, 5.75], "rotation": 90, "texture": "#1"}, - "west": {"uv": [5.125, 7.25, 4, 7.25], "rotation": 90, "texture": "#1"}, - "up": {"uv": [3.75, 7.375, 3.75, 6.75], "rotation": 90, "texture": "#1"}, - "down": {"uv": [3.5, 7.375, 3.5, 6.75], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [5.72222, 19.54399, 7.92986], - "to": [7.84478, 20.27263, 7.92986], - "faces": { - "north": {"uv": [5, 1.625, 5.625, 0], "rotation": 270, "texture": "#1"}, - "east": {"uv": [7.875, 4.5, 7.25, 4.5], "rotation": 90, "texture": "#1"}, - "south": {"uv": [5, 3.625, 5.625, 2], "rotation": 90, "texture": "#1"}, - "west": {"uv": [7.875, 4.25, 7.25, 4.25], "rotation": 90, "texture": "#1"}, - "up": {"uv": [5.75, 8.875, 5.75, 7.25], "rotation": 90, "texture": "#1"}, - "down": {"uv": [5.5, 8.875, 5.5, 7.25], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [0.4, 15.2, 8], - "to": [15.4, 30.2, 8], - "faces": { - "north": {"uv": [0, 0, 15, 15], "texture": "#3"}, - "east": {"uv": [0, 0, 0, 7.5], "texture": "#3"}, - "south": {"uv": [15, 0, 0, 15], "texture": "#3"}, - "west": {"uv": [0, 0, 0, 7.5], "texture": "#3"}, - "up": {"uv": [7.5, 0, 0, 0], "texture": "#3"}, - "down": {"uv": [7.5, 0, 0, 0], "texture": "#3"} - } - } - ], - "display": { - "thirdperson_righthand": { - "rotation": [0.5, 90, 0], - "translation": [0, 4, 0.75], - "scale": [0.85, 0.85, 0.85] - }, - "thirdperson_lefthand": { - "rotation": [0.25, -90, 0], - "translation": [0, 4, 0.75], - "scale": [0.85, 0.85, 0.85] - }, - "firstperson_righthand": { - "rotation": [-15.71, -76.01, -1.19], - "translation": [3.13, -0.8, 1.13], - "scale": [0.68, 0.68, 0.68] - }, - "firstperson_lefthand": { - "rotation": [-180, 79.75, 167], - "translation": [3.13, -0.05, 1.38], - "scale": [0.68, 0.68, 0.68] - }, - "ground": { - "translation": [0, 15.5, 0] - }, - "gui": { - "rotation": [0, 0, -45], - "scale": [0.5, 0.5, 0.5] - }, - "head": { - "rotation": [0, 0, -31.25], - "translation": [5.25, -12.25, 3.75] - }, - "fixed": { - "rotation": [0, 0, 45], - "scale": [0.65, 0.65, 0.65] - } - } -} \ No newline at end of file diff --git a/src/main/resources/god/assets/useless_mod/models/item/endless_beaf_screwdriver.json b/src/main/resources/god/assets/useless_mod/models/item/endless_beaf_screwdriver.json deleted file mode 100644 index 4e97a9ce..00000000 --- a/src/main/resources/god/assets/useless_mod/models/item/endless_beaf_screwdriver.json +++ /dev/null @@ -1,880 +0,0 @@ -{ - "render_type":"cutout", - "format_version": "1.9.0", - "credit": "Made with _leng", - "texture_size": [32, 32], - "textures": { - "0": "useless_mod:item/endless_beaf_item", - "1": "useless_mod:item/endless_beaf_item_wing", - "2": "useless_mod:item/lable_wireless_transceiver_lighting", - "3": "useless_mod:item/endless_special_efficacy" - }, - "elements": [ - { - "name": "Octagon", - "from": [7.79289, -12.5001, 7.5], - "to": [8.20711, 18.0001, 8.5], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 8, 8]}, - "faces": { - "north": {"uv": [4, 0, 4.125, 7.625], "texture": "#0"}, - "east": {"uv": [0, 0, 0.25, 7.625], "texture": "#0"}, - "south": {"uv": [4.5, 0, 4.625, 7.625], "texture": "#0"}, - "west": {"uv": [0.5, 0, 0.75, 7.625], "texture": "#0"}, - "up": {"uv": [10.875, 8.25, 10.75, 8], "texture": "#0"}, - "down": {"uv": [10.875, 8.5, 10.75, 8.75], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [7.79289, -12.5003, 7.5], - "to": [8.20711, 18.0003, 8.5], - "faces": { - "north": {"uv": [5, 0, 5.125, 7.625], "texture": "#0"}, - "east": {"uv": [1, 0, 1.25, 7.625], "texture": "#0"}, - "south": {"uv": [5.5, 0, 5.625, 7.625], "texture": "#0"}, - "west": {"uv": [1.5, 0, 1.75, 7.625], "texture": "#0"}, - "up": {"uv": [10.875, 9.25, 10.75, 9], "texture": "#0"}, - "down": {"uv": [10.875, 9.5, 10.75, 9.75], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [7.5, -12.5, 7.79289], - "to": [8.5, 18, 8.20711], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 8, 8]}, - "faces": { - "north": {"uv": [2, 0, 2.25, 7.625], "texture": "#0"}, - "east": {"uv": [6, 0, 6.125, 7.625], "texture": "#0"}, - "south": {"uv": [2.5, 0, 2.75, 7.625], "texture": "#0"}, - "west": {"uv": [6.5, 0, 6.625, 7.625], "texture": "#0"}, - "up": {"uv": [11, 10.125, 10.75, 10], "texture": "#0"}, - "down": {"uv": [11, 10.5, 10.75, 10.625], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [7.5, -12.5002, 7.79289], - "to": [8.5, 18.0002, 8.20711], - "faces": { - "north": {"uv": [3, 0, 3.25, 7.625], "texture": "#0"}, - "east": {"uv": [7, 0, 7.125, 7.625], "texture": "#0"}, - "south": {"uv": [3.5, 0, 3.75, 7.625], "texture": "#0"}, - "west": {"uv": [7.5, 0, 7.625, 7.625], "texture": "#0"}, - "up": {"uv": [11.25, 0.125, 11, 0], "texture": "#0"}, - "down": {"uv": [11.25, 0.5, 11, 0.625], "texture": "#0"} - } - }, - { - "name": "duzuo", - "from": [6.869, -14.12, 8], - "to": [9.131, -10.118, 8], - "faces": { - "north": {"uv": [0, 9, 0.625, 10], "texture": "#0"}, - "east": {"uv": [10.25, 6, 10.375, 7], "texture": "#0"}, - "south": {"uv": [0.625, 9, 1.25, 10], "texture": "#0"}, - "west": {"uv": [10.25, 7.25, 10.375, 8.25], "texture": "#0"}, - "up": {"uv": [8, 10.625, 7.5, 10.5], "texture": "#0"}, - "down": {"uv": [8.75, 10.5, 8.25, 10.625], "texture": "#0"} - } - }, - { - "from": [7.4, -11.3, 7.4], - "to": [8.6, -10.1, 8.6], - "rotation": {"angle": -45, "axis": "z", "origin": [8, -10.7, 8]}, - "faces": { - "north": {"uv": [0, 10.75, 0.25, 11], "texture": "#0"}, - "east": {"uv": [0.5, 10.75, 0.75, 11], "texture": "#0"}, - "south": {"uv": [10.75, 1.5, 11, 1.75], "texture": "#0"}, - "west": {"uv": [10.75, 2, 11, 2.25], "texture": "#0"}, - "up": {"uv": [11, 4.75, 10.75, 4.5], "texture": "#0"}, - "down": {"uv": [11, 5, 10.75, 5.25], "texture": "#0"} - } - }, - { - "from": [7.8, -10.9, 7.3], - "to": [8.2, -10.5, 8.7], - "rotation": {"angle": -45, "axis": "z", "origin": [8, -10.7, 8]}, - "faces": { - "north": {"uv": [0.25, 0.75, 0.375, 0.875], "texture": "#0"}, - "east": {"uv": [0.25, 0, 0.5, 0.125], "texture": "#0"}, - "south": {"uv": [0.75, 0.25, 0.875, 0.375], "texture": "#0"}, - "west": {"uv": [0.25, 0.25, 0.5, 0.375], "texture": "#0"}, - "up": {"uv": [0.375, 0.75, 0.25, 0.5], "texture": "#0"}, - "down": {"uv": [0.875, 0, 0.75, 0.25], "texture": "#0"} - } - }, - { - "from": [7.8, 15.12, 7.3], - "to": [8.2, 15.52, 8.7], - "rotation": {"angle": -45, "axis": "z", "origin": [8, 15.32, 8]}, - "faces": { - "north": {"uv": [0.25, 0.75, 0.375, 0.875], "texture": "#0"}, - "east": {"uv": [0.25, 0, 0.5, 0.125], "texture": "#0"}, - "south": {"uv": [0.75, 0.25, 0.875, 0.375], "texture": "#0"}, - "west": {"uv": [0.25, 0.25, 0.5, 0.375], "texture": "#0"}, - "up": {"uv": [0.375, 0.75, 0.25, 0.5], "texture": "#0"}, - "down": {"uv": [0.875, 0, 0.75, 0.25], "texture": "#0"} - } - }, - { - "from": [7.39, -10.31, 7.39], - "to": [8.61, -9.84, 8.61], - "faces": { - "north": {"uv": [11, 1, 11.25, 1.125], "texture": "#0"}, - "east": {"uv": [1.5, 11, 1.75, 11.125], "texture": "#0"}, - "south": {"uv": [2, 11, 2.25, 11.125], "texture": "#0"}, - "west": {"uv": [2.5, 11, 2.75, 11.125], "texture": "#0"}, - "up": {"uv": [5.75, 11, 5.5, 10.75], "texture": "#0"}, - "down": {"uv": [6.25, 10.75, 6, 11], "texture": "#0"} - } - }, - { - "from": [7.39, 15.09, 7.39], - "to": [8.61, 15.56, 8.61], - "faces": { - "north": {"uv": [11, 2.5, 11.25, 2.625], "texture": "#0"}, - "east": {"uv": [3, 11, 3.25, 11.125], "texture": "#0"}, - "south": {"uv": [11, 3, 11.25, 3.125], "texture": "#0"}, - "west": {"uv": [3.5, 11, 3.75, 11.125], "texture": "#0"}, - "up": {"uv": [11, 6.25, 10.75, 6], "texture": "#0"}, - "down": {"uv": [6.75, 10.75, 6.5, 11], "texture": "#0"} - } - }, - { - "from": [7.39, 15.99, 7.39], - "to": [8.61, 17.71, 8.61], - "faces": { - "north": {"uv": [10.25, 8.5, 10.5, 8.875], "texture": "#0"}, - "east": {"uv": [10.25, 9.25, 10.5, 9.625], "texture": "#0"}, - "south": {"uv": [10.5, 0, 10.75, 0.375], "texture": "#0"}, - "west": {"uv": [10.5, 0.75, 10.75, 1.125], "texture": "#0"}, - "up": {"uv": [11, 6.75, 10.75, 6.5], "texture": "#0"}, - "down": {"uv": [7.25, 10.75, 7, 11], "texture": "#0"} - } - }, - { - "from": [6.65, 17.9, 6.68], - "to": [9.15, 20.4, 9.315], - "rotation": {"angle": -45, "axis": "z", "origin": [7.6, 18.9, 8.4]}, - "faces": { - "north": {"uv": [9.25, 1.5, 9.875, 2.125], "texture": "#0"}, - "east": {"uv": [9.25, 2.5, 9.875, 3.125], "texture": "#0"}, - "south": {"uv": [9.25, 6, 9.875, 6.625], "texture": "#0"}, - "west": {"uv": [9.25, 7, 9.875, 7.625], "texture": "#0"}, - "up": {"uv": [9.875, 8.625, 9.25, 8], "texture": "#0"}, - "down": {"uv": [9.875, 9, 9.25, 9.625], "texture": "#0"} - } - }, - { - "from": [7.8, 18.05, 6.48], - "to": [8.6, 18.85, 9.515], - "rotation": {"angle": -45, "axis": "z", "origin": [8.6, 18.9, 8.4]}, - "faces": { - "north": {"uv": [10.75, 7, 11, 7.25], "texture": "#0"}, - "east": {"uv": [10, 5.5, 10.75, 5.75], "texture": "#0"}, - "south": {"uv": [10.75, 7.5, 11, 7.75], "texture": "#0"}, - "west": {"uv": [0, 10.25, 0.75, 10.5], "texture": "#0"}, - "up": {"uv": [1.25, 11, 1, 10.25], "texture": "#0"}, - "down": {"uv": [10.5, 1.5, 10.25, 2.25], "texture": "#0"} - } - }, - { - "from": [8, 16.8, 7.6], - "to": [8, 17.8, 9.6], - "rotation": {"angle": -45, "axis": "x", "origin": [7.9, 17.8, 8.6]}, - "faces": { - "north": {"uv": [4, 11, 3.875, 11.25], "texture": "#0"}, - "east": {"uv": [10.25, 2.5, 10.75, 2.75], "texture": "#0"}, - "south": {"uv": [5.125, 11.25, 5.25, 11.5], "texture": "#0"}, - "west": {"uv": [10.25, 3, 10.75, 3.25], "texture": "#0"}, - "up": {"uv": [5.375, 11.75, 5.5, 11.25], "texture": "#0"}, - "down": {"uv": [5.625, 11.25, 5.75, 11.75], "texture": "#0"} - } - }, - { - "from": [8, 16.8, 6.4], - "to": [8, 17.8, 8.4], - "rotation": {"angle": 45, "axis": "x", "origin": [7.9, 17.8, 7.4]}, - "faces": { - "north": {"uv": [7.625, 10.875, 8, 11.375], "texture": "#0"}, - "east": {"uv": [7.5, 9, 8, 9.25], "texture": "#0"}, - "south": {"uv": [7.9, 7.5, 8.125, 7.75], "texture": "#0"}, - "west": {"uv": [9.25, 3.5, 9.75, 3.75], "texture": "#0"}, - "up": {"uv": [8.875, 11, 9.125, 10.5], "texture": "#0"}, - "down": {"uv": [10.875, 3.5, 11.125, 4], "texture": "#0"} - } - }, - { - "from": [8.10331, 19.03251, 6.9], - "to": [10.78331, 20.70751, 9.1], - "rotation": {"angle": 22.5, "axis": "z", "origin": [8.77331, 19.70251, 7]}, - "faces": { - "north": {"uv": [0, 8, 1, 8.625], "texture": "#0"}, - "east": {"uv": [7.5, 9.5, 8.125, 10.125], "texture": "#0"}, - "south": {"uv": [8, 0, 9, 0.625], "texture": "#0"}, - "west": {"uv": [1.5, 10, 2.125, 10.625], "texture": "#0"}, - "up": {"uv": [9, 1.625, 8, 1], "texture": "#0"}, - "down": {"uv": [2.25, 8, 1.25, 8.625], "texture": "#0"} - } - }, - { - "from": [5.21669, 19.03251, 6.9], - "to": [7.89669, 20.70751, 9.1], - "rotation": {"angle": -22.5, "axis": "z", "origin": [7.22669, 19.70251, 7]}, - "faces": { - "north": {"uv": [8, 2, 9, 2.625], "texture": "#0"}, - "east": {"uv": [2.5, 10, 3.125, 10.625], "texture": "#0"}, - "south": {"uv": [2.5, 8, 3.5, 8.625], "texture": "#0"}, - "west": {"uv": [3.5, 10, 4.125, 10.625], "texture": "#0"}, - "up": {"uv": [9, 3.625, 8, 3], "texture": "#0"}, - "down": {"uv": [4.75, 8, 3.75, 8.625], "texture": "#0"} - } - }, - { - "from": [6.48891, 21.54663, 6.99], - "to": [8.98891, 24.04663, 8.99], - "rotation": {"angle": -45, "axis": "z", "origin": [7.98891, 22.54663, 7.99]}, - "faces": { - "north": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "east": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "south": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "west": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "up": {"uv": [16, 16, 0, 0], "texture": "#2"}, - "down": {"uv": [16, 0, 0, 16], "texture": "#2"} - } - }, - { - "from": [6.48891, 19.04663, 6.99], - "to": [8.98891, 21.54663, 8.99], - "rotation": {"angle": -45, "axis": "z", "origin": [7.98891, 20.04663, 7.991]}, - "faces": { - "north": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "east": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "south": {"uv": [0, 0, 15.9, 16], "texture": "#2"}, - "west": {"uv": [0, 0, 15.9, 15.8], "texture": "#2"}, - "up": {"uv": [15.9, 15.8, 0, 0], "texture": "#2"}, - "down": {"uv": [16, 0, 0, 15.8], "texture": "#2"} - } - }, - { - "from": [6.22114, 20.40018, 6.989], - "to": [9.75668, 22.90018, 8.991], - "faces": { - "north": {"uv": [0, 0, 15.975, 16.025], "texture": "#2"}, - "east": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "south": {"uv": [0, 0, 15.975, 16], "texture": "#2"}, - "west": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "up": {"uv": [15.975, 16, 0, 0], "texture": "#2"}, - "down": {"uv": [16, 0, 0, 16], "texture": "#2"} - } - }, - { - "from": [7.17754, 24.8802, 7.99], - "to": [8.80028, 27.1202, 7.99], - "faces": { - "north": {"uv": [8.25, 7, 8.875, 7.875], "texture": "#0"}, - "east": {"uv": [5, 11, 5.225, 12.1375], "texture": "#0"}, - "south": {"uv": [8.25, 8.25, 8.875, 9.125], "texture": "#0"}, - "west": {"uv": [5.25, 11, 5.475, 12.1375], "texture": "#0"}, - "up": {"uv": [11.85, 4.25, 11, 4.5125], "texture": "#0"}, - "down": {"uv": [11.85, 5.5, 11, 5.7625], "texture": "#0"} - } - }, - { - "name": "cube_outline", - "from": [9.08891, 24.14663, 9.09], - "to": [6.38891, 21.44663, 6.89], - "rotation": {"angle": -45, "axis": "z", "origin": [7.98891, 22.54663, 7.99]}, - "faces": { - "north": {"uv": [0.75, 11.125, 0.875, 11.25], "rotation": 180, "texture": "#0"}, - "east": {"uv": [0.875, 11.125, 1, 11.25], "rotation": 180, "texture": "#0"}, - "south": {"uv": [1, 11.125, 1.125, 11.25], "rotation": 180, "texture": "#0"}, - "west": {"uv": [1.125, 11.125, 1.25, 11.25], "rotation": 180, "texture": "#0"}, - "up": {"uv": [1.375, 11.125, 1.25, 11.25], "rotation": 180, "texture": "#0"}, - "down": {"uv": [1.5, 11.25, 1.375, 11.125], "rotation": 180, "texture": "#0"} - } - }, - { - "name": "cube_outline", - "from": [9.85668, 23.00018, 9.091], - "to": [6.12114, 20.30018, 6.889], - "faces": { - "north": {"uv": [0, 11.125, 0.125, 11.25], "rotation": 180, "texture": "#0"}, - "east": {"uv": [0.125, 11.125, 0.25, 11.25], "rotation": 180, "texture": "#0"}, - "south": {"uv": [0.25, 11.125, 0.375, 11.25], "rotation": 180, "texture": "#0"}, - "west": {"uv": [0.375, 11.125, 0.5, 11.25], "rotation": 180, "texture": "#0"}, - "up": {"uv": [0.625, 11.125, 0.5, 11.25], "rotation": 180, "texture": "#0"}, - "down": {"uv": [0.75, 11.25, 0.625, 11.125], "rotation": 180, "texture": "#0"} - } - }, - { - "name": "cube_outline", - "from": [9.08891, 21.64663, 9.09], - "to": [6.38891, 18.94663, 6.89], - "rotation": {"angle": -45, "axis": "z", "origin": [7.98891, 20.04663, 7.991]}, - "faces": { - "north": {"uv": [1.5, 11.125, 1.625, 11.25], "rotation": 180, "texture": "#0"}, - "east": {"uv": [1.625, 11.125, 1.75, 11.25], "rotation": 180, "texture": "#0"}, - "south": {"uv": [1.75, 11.125, 1.875, 11.25], "rotation": 180, "texture": "#0"}, - "west": {"uv": [1.875, 11.125, 2, 11.25], "rotation": 180, "texture": "#0"}, - "up": {"uv": [2.125, 11.125, 2, 11.25], "rotation": 180, "texture": "#0"}, - "down": {"uv": [2.25, 11.25, 2.125, 11.125], "rotation": 180, "texture": "#0"} - } - }, - { - "from": [7.34391, 24.0552, 7.4525], - "to": [8.63391, 25.3452, 8.5275], - "rotation": {"angle": -45, "axis": "z", "origin": [7.98891, 24.70019, 8.00075]}, - "faces": { - "north": {"uv": [5.75, 8, 5, 8.75], "texture": "#0"}, - "east": {"uv": [4.5, 9.5, 3.875, 8.75], "texture": "#0"}, - "south": {"uv": [8.75, 4, 8, 4.75], "texture": "#0"}, - "west": {"uv": [3.75, 8.75, 3.125, 9.5], "texture": "#0"}, - "up": {"uv": [4.875, 10.75, 4.125, 11.375], "texture": "#0"}, - "down": {"uv": [9.75, 4, 9, 4.625], "texture": "#0"} - } - }, - { - "from": [6.95149, 23.04957, 7.249], - "to": [8.72989, 24.82797, 8.731], - "rotation": {"angle": -45, "axis": "z", "origin": [7.9889, 23.79059, 7.87144]}, - "faces": { - "north": {"uv": [8, 5, 8.75, 5.75], "texture": "#0"}, - "east": {"uv": [4.5, 9, 5.125, 9.75], "texture": "#0"}, - "south": {"uv": [6, 8, 6.75, 8.75], "texture": "#0"}, - "west": {"uv": [9, 5, 9.625, 5.75], "texture": "#0"}, - "up": {"uv": [6.25, 9.625, 5.5, 9], "texture": "#0"}, - "down": {"uv": [7.25, 9, 6.5, 9.625], "texture": "#0"} - } - }, - { - "from": [5.266, 20.3249, 7.76026], - "to": [7.0084, 20.8001, 8.23546], - "rotation": {"angle": -45, "axis": "z", "origin": [7.0084, 20.4833, 7.83946]}, - "faces": { - "north": {"uv": [5.875, 5.75, 4.5, 6.125], "texture": "#1"}, - "east": {"uv": [4.875, 6.5, 4.5, 6.875], "texture": "#1"}, - "south": {"uv": [7.125, 5, 5.75, 5.375], "texture": "#1"}, - "west": {"uv": [6.875, 2, 6.5, 2.375], "texture": "#1"}, - "up": {"uv": [6, 0.375, 7.375, 0], "texture": "#1"}, - "down": {"uv": [6, 0.75, 7.375, 1.125], "texture": "#1"} - } - }, - { - "from": [4.16353, 23.23946, 7.83914], - "to": [5.90593, 23.55626, 8.15594], - "rotation": {"angle": -45, "axis": "z", "origin": [5.90593, 23.23946, 7.83946]}, - "faces": { - "north": {"uv": [7.375, 3.25, 6, 3.5], "texture": "#1"}, - "east": {"uv": [6, 6.5, 5.75, 6.75], "texture": "#1"}, - "south": {"uv": [7.625, 5.75, 6.25, 6], "texture": "#1"}, - "west": {"uv": [5.5, 6.5, 5.25, 6.75], "texture": "#1"}, - "up": {"uv": [6.25, 6.5, 7.625, 6.25], "texture": "#1"}, - "down": {"uv": [6.5, 1.5, 7.875, 1.75], "texture": "#1"} - } - }, - { - "from": [5.77633, 21.71536, 7.83788], - "to": [6.09313, 23.45776, 8.15468], - "faces": { - "north": {"uv": [6.25, 1.5, 6, 2.875], "texture": "#1"}, - "east": {"uv": [1.25, 6.25, 1, 7.625], "texture": "#1"}, - "south": {"uv": [0.75, 6.25, 0.5, 7.625], "texture": "#1"}, - "west": {"uv": [0.25, 6.25, 0, 7.625], "texture": "#1"}, - "up": {"uv": [1.5, 5.5, 1.75, 5.25], "texture": "#1"}, - "down": {"uv": [6.5, 2.75, 6.75, 3], "texture": "#1"} - } - }, - { - "from": [1.43272, 22.09898, 7.90282], - "to": [4.44232, 24.15818, 7.90282], - "rotation": {"angle": -45, "axis": "z", "origin": [4.44232, 21.78218, 7.90282]}, - "faces": { - "north": {"uv": [2.375, 0, 0, 1.625], "texture": "#1"}, - "east": {"uv": [1.75, 6.75, 1.75, 8.375], "texture": "#1"}, - "south": {"uv": [2.375, 2, 0, 3.625], "texture": "#1"}, - "west": {"uv": [1.5, 6.75, 1.5, 8.375], "texture": "#1"}, - "up": {"uv": [6, 3.75, 8.375, 3.75], "texture": "#1"}, - "down": {"uv": [6.25, 6.75, 8.625, 6.75], "texture": "#1"} - } - }, - { - "from": [3.61751, 21.4698, 7.90314], - "to": [6.08855, 23.11716, 7.90314], - "faces": { - "north": {"uv": [4.75, 0, 2.75, 1.25], "texture": "#1"}, - "east": {"uv": [2.25, 6.75, 2.25, 8], "texture": "#1"}, - "south": {"uv": [4.75, 1.5, 2.75, 2.75], "texture": "#1"}, - "west": {"uv": [2, 6.75, 2, 8], "texture": "#1"}, - "up": {"uv": [7, 2.75, 9, 2.75], "texture": "#1"}, - "down": {"uv": [7, 3, 9, 3], "texture": "#1"} - } - }, - { - "from": [3.64919, 20.55108, 7.90346], - "to": [6.08855, 21.84996, 7.90346], - "rotation": {"angle": 22.5, "axis": "z", "origin": [4.58375, 21.58068, 7.90282]}, - "faces": { - "north": {"uv": [4.625, 3, 2.75, 4], "texture": "#1"}, - "east": {"uv": [2.75, 6.75, 2.75, 7.75], "texture": "#1"}, - "south": {"uv": [1.875, 4, 0, 5], "texture": "#1"}, - "west": {"uv": [2.5, 6.75, 2.5, 7.75], "texture": "#1"}, - "up": {"uv": [5.25, 7, 7.125, 7], "texture": "#1"}, - "down": {"uv": [7.25, 2, 9.125, 2], "texture": "#1"} - } - }, - { - "from": [4.78967, 19.22052, 7.90314], - "to": [6.65879, 20.86788, 7.90314], - "rotation": {"angle": 45, "axis": "z", "origin": [5.15399, 20.25012, 7.90282]}, - "faces": { - "north": {"uv": [3.75, 4.25, 2.25, 5.5], "texture": "#1"}, - "east": {"uv": [3.25, 6.75, 3.25, 8], "texture": "#1"}, - "south": {"uv": [5.5, 4.25, 4, 5.5], "texture": "#1"}, - "west": {"uv": [3, 6.75, 3, 8], "texture": "#1"}, - "up": {"uv": [7.25, 2.25, 8.75, 2.25], "texture": "#1"}, - "down": {"uv": [7.25, 2.5, 8.75, 2.5], "texture": "#1"} - } - }, - { - "from": [2.69992, 23.3345, 8.02954], - "to": [4.12552, 24.06314, 8.02954], - "rotation": {"angle": -45, "axis": "z", "origin": [4.12552, 21.71882, 8.02954]}, - "faces": { - "north": {"uv": [1.125, 5.25, 0, 5.875], "texture": "#1"}, - "east": {"uv": [3.75, 6.75, 3.75, 7.375], "texture": "#1"}, - "south": {"uv": [2.625, 5.75, 1.5, 6.375], "texture": "#1"}, - "west": {"uv": [3.5, 6.75, 3.5, 7.375], "texture": "#1"}, - "up": {"uv": [4, 7.25, 5.125, 7.25], "texture": "#1"}, - "down": {"uv": [7.25, 4, 8.375, 4], "texture": "#1"} - } - }, - { - "from": [3.6028, 20.5625, 7.87114], - "to": [5.0284, 21.29114, 7.87114], - "rotation": {"angle": -45, "axis": "z", "origin": [5.01256, 18.93098, 7.87114]}, - "faces": { - "north": {"uv": [4.125, 5.75, 3, 6.375], "texture": "#1"}, - "east": {"uv": [6.25, 7.25, 6.25, 7.875], "texture": "#1"}, - "south": {"uv": [6.875, 4, 5.75, 4.625], "texture": "#1"}, - "west": {"uv": [6, 7.25, 6, 7.875], "texture": "#1"}, - "up": {"uv": [7.25, 4.75, 8.375, 4.75], "texture": "#1"}, - "down": {"uv": [6.5, 7.25, 7.625, 7.25], "texture": "#1"} - } - }, - { - "from": [5.06908, 21.24691, 7.87146], - "to": [5.79772, 23.36947, 7.87146], - "faces": { - "north": {"uv": [5.625, 0, 5, 1.625], "texture": "#1"}, - "east": {"uv": [5.75, 7.25, 5.75, 8.875], "texture": "#1"}, - "south": {"uv": [5.625, 2, 5, 3.625], "texture": "#1"}, - "west": {"uv": [5.5, 7.25, 5.5, 8.875], "texture": "#1"}, - "up": {"uv": [7.25, 4.25, 7.875, 4.25], "texture": "#1"}, - "down": {"uv": [7.25, 4.5, 7.875, 4.5], "texture": "#1"} - } - }, - { - "from": [2.69992, 23.3345, 7.87114], - "to": [4.12552, 24.06314, 7.87114], - "rotation": {"angle": -45, "axis": "z", "origin": [4.12552, 21.71882, 7.87114]}, - "faces": { - "north": {"uv": [1.125, 5.25, 0, 5.875], "texture": "#1"}, - "east": {"uv": [3.75, 6.75, 3.75, 7.375], "texture": "#1"}, - "south": {"uv": [2.625, 5.75, 1.5, 6.375], "texture": "#1"}, - "west": {"uv": [3.5, 6.75, 3.5, 7.375], "texture": "#1"}, - "up": {"uv": [4, 7.25, 5.125, 7.25], "texture": "#1"}, - "down": {"uv": [7.25, 4, 8.375, 4], "texture": "#1"} - } - }, - { - "from": [3.6028, 20.5625, 8.02954], - "to": [5.0284, 21.29114, 8.02954], - "rotation": {"angle": -45, "axis": "z", "origin": [5.01256, 18.93098, 8.02954]}, - "faces": { - "north": {"uv": [4.125, 5.75, 3, 6.375], "texture": "#1"}, - "east": {"uv": [6.25, 7.25, 6.25, 7.875], "texture": "#1"}, - "south": {"uv": [6.875, 4, 5.75, 4.625], "texture": "#1"}, - "west": {"uv": [6, 7.25, 6, 7.875], "texture": "#1"}, - "up": {"uv": [7.25, 4.75, 8.375, 4.75], "texture": "#1"}, - "down": {"uv": [6.5, 7.25, 7.625, 7.25], "texture": "#1"} - } - }, - { - "from": [5.06908, 21.24691, 8.02986], - "to": [5.79772, 23.36947, 8.02986], - "faces": { - "north": {"uv": [5.625, 0, 5, 1.625], "texture": "#1"}, - "east": {"uv": [5.75, 7.25, 5.75, 8.875], "texture": "#1"}, - "south": {"uv": [5.625, 2, 5, 3.625], "texture": "#1"}, - "west": {"uv": [5.5, 7.25, 5.5, 8.875], "texture": "#1"}, - "up": {"uv": [7.25, 4.25, 7.875, 4.25], "texture": "#1"}, - "down": {"uv": [7.25, 4.5, 7.875, 4.5], "texture": "#1"} - } - }, - { - "from": [8.9916, 20.3249, 7.76026], - "to": [10.734, 20.8001, 8.23546], - "rotation": {"angle": 45, "axis": "z", "origin": [8.9916, 20.4833, 7.83946]}, - "faces": { - "north": {"uv": [4.5, 5.75, 5.875, 6.125], "texture": "#1"}, - "east": {"uv": [6.5, 2, 6.875, 2.375], "texture": "#1"}, - "south": {"uv": [5.75, 5, 7.125, 5.375], "texture": "#1"}, - "west": {"uv": [4.5, 6.5, 4.875, 6.875], "texture": "#1"}, - "up": {"uv": [7.375, 0.375, 6, 0], "texture": "#1"}, - "down": {"uv": [7.375, 0.75, 6, 1.125], "texture": "#1"} - } - }, - { - "from": [10.09407, 23.23946, 7.83914], - "to": [11.83647, 23.55626, 8.15594], - "rotation": {"angle": 45, "axis": "z", "origin": [10.09407, 23.23946, 7.83946]}, - "faces": { - "north": {"uv": [6, 3.25, 7.375, 3.5], "texture": "#1"}, - "east": {"uv": [5.25, 6.5, 5.5, 6.75], "texture": "#1"}, - "south": {"uv": [6.25, 5.75, 7.625, 6], "texture": "#1"}, - "west": {"uv": [5.75, 6.5, 6, 6.75], "texture": "#1"}, - "up": {"uv": [7.625, 6.5, 6.25, 6.25], "texture": "#1"}, - "down": {"uv": [7.875, 1.5, 6.5, 1.75], "texture": "#1"} - } - }, - { - "from": [9.90687, 21.71536, 7.83788], - "to": [10.22367, 23.45776, 8.15468], - "faces": { - "north": {"uv": [6, 1.5, 6.25, 2.875], "texture": "#1"}, - "east": {"uv": [0, 6.25, 0.25, 7.625], "texture": "#1"}, - "south": {"uv": [0.5, 6.25, 0.75, 7.625], "texture": "#1"}, - "west": {"uv": [1, 6.25, 1.25, 7.625], "texture": "#1"}, - "up": {"uv": [1.75, 5.5, 1.5, 5.25], "texture": "#1"}, - "down": {"uv": [6.75, 2.75, 6.5, 3], "texture": "#1"} - } - }, - { - "from": [11.55768, 22.09898, 7.90282], - "to": [14.56728, 24.15818, 7.90282], - "rotation": {"angle": 45, "axis": "z", "origin": [11.55768, 21.78218, 7.90282]}, - "faces": { - "north": {"uv": [0, 0, 2.375, 1.625], "texture": "#1"}, - "east": {"uv": [1.5, 6.75, 1.5, 8.375], "texture": "#1"}, - "south": {"uv": [0, 2, 2.375, 3.625], "texture": "#1"}, - "west": {"uv": [1.75, 6.75, 1.75, 8.375], "texture": "#1"}, - "up": {"uv": [8.375, 3.75, 6, 3.75], "texture": "#1"}, - "down": {"uv": [8.625, 6.75, 6.25, 6.75], "texture": "#1"} - } - }, - { - "from": [9.91145, 21.4698, 7.90314], - "to": [12.38249, 23.11716, 7.90314], - "faces": { - "north": {"uv": [2.75, 0, 4.75, 1.25], "texture": "#1"}, - "east": {"uv": [2, 6.75, 2, 8], "texture": "#1"}, - "south": {"uv": [2.75, 1.5, 4.75, 2.75], "texture": "#1"}, - "west": {"uv": [2.25, 6.75, 2.25, 8], "texture": "#1"}, - "up": {"uv": [9, 2.75, 7, 2.75], "texture": "#1"}, - "down": {"uv": [9, 3, 7, 3], "texture": "#1"} - } - }, - { - "from": [9.91145, 20.55108, 7.90346], - "to": [12.35081, 21.84996, 7.90346], - "rotation": {"angle": -22.5, "axis": "z", "origin": [11.41625, 21.58068, 7.90282]}, - "faces": { - "north": {"uv": [2.75, 3, 4.625, 4], "texture": "#1"}, - "east": {"uv": [2.5, 6.75, 2.5, 7.75], "texture": "#1"}, - "south": {"uv": [0, 4, 1.875, 5], "texture": "#1"}, - "west": {"uv": [2.75, 6.75, 2.75, 7.75], "texture": "#1"}, - "up": {"uv": [7.125, 7, 5.25, 7], "texture": "#1"}, - "down": {"uv": [9.125, 2, 7.25, 2], "texture": "#1"} - } - }, - { - "from": [9.34121, 19.22052, 7.90314], - "to": [11.21033, 20.86788, 7.90314], - "rotation": {"angle": -45, "axis": "z", "origin": [10.84601, 20.25012, 7.90282]}, - "faces": { - "north": {"uv": [2.25, 4.25, 3.75, 5.5], "texture": "#1"}, - "east": {"uv": [3, 6.75, 3, 8], "texture": "#1"}, - "south": {"uv": [4, 4.25, 5.5, 5.5], "texture": "#1"}, - "west": {"uv": [3.25, 6.75, 3.25, 8], "texture": "#1"}, - "up": {"uv": [8.75, 2.25, 7.25, 2.25], "texture": "#1"}, - "down": {"uv": [8.75, 2.5, 7.25, 2.5], "texture": "#1"} - } - }, - { - "from": [11.87448, 23.3345, 8.02954], - "to": [13.30008, 24.06314, 8.02954], - "rotation": {"angle": 45, "axis": "z", "origin": [11.87448, 21.71882, 8.02954]}, - "faces": { - "north": {"uv": [0, 5.25, 1.125, 5.875], "texture": "#1"}, - "east": {"uv": [3.5, 6.75, 3.5, 7.375], "texture": "#1"}, - "south": {"uv": [1.5, 5.75, 2.625, 6.375], "texture": "#1"}, - "west": {"uv": [3.75, 6.75, 3.75, 7.375], "texture": "#1"}, - "up": {"uv": [5.125, 7.25, 4, 7.25], "texture": "#1"}, - "down": {"uv": [8.375, 4, 7.25, 4], "texture": "#1"} - } - }, - { - "from": [10.9716, 20.5625, 7.87114], - "to": [12.3972, 21.29114, 7.87114], - "rotation": {"angle": 45, "axis": "z", "origin": [10.98744, 18.93098, 7.87114]}, - "faces": { - "north": {"uv": [3, 5.75, 4.125, 6.375], "texture": "#1"}, - "east": {"uv": [6, 7.25, 6, 7.875], "texture": "#1"}, - "south": {"uv": [5.75, 4, 6.875, 4.625], "texture": "#1"}, - "west": {"uv": [6.25, 7.25, 6.25, 7.875], "texture": "#1"}, - "up": {"uv": [8.375, 4.75, 7.25, 4.75], "texture": "#1"}, - "down": {"uv": [7.625, 7.25, 6.5, 7.25], "texture": "#1"} - } - }, - { - "from": [10.20228, 21.24691, 7.87146], - "to": [10.93092, 23.36947, 7.87146], - "faces": { - "north": {"uv": [5, 0, 5.625, 1.625], "texture": "#1"}, - "east": {"uv": [5.5, 7.25, 5.5, 8.875], "texture": "#1"}, - "south": {"uv": [5, 2, 5.625, 3.625], "texture": "#1"}, - "west": {"uv": [5.75, 7.25, 5.75, 8.875], "texture": "#1"}, - "up": {"uv": [7.875, 4.25, 7.25, 4.25], "texture": "#1"}, - "down": {"uv": [7.875, 4.5, 7.25, 4.5], "texture": "#1"} - } - }, - { - "from": [11.87448, 23.3345, 7.87114], - "to": [13.30008, 24.06314, 7.87114], - "rotation": {"angle": 45, "axis": "z", "origin": [11.87448, 21.71882, 7.87114]}, - "faces": { - "north": {"uv": [0, 5.25, 1.125, 5.875], "texture": "#1"}, - "east": {"uv": [3.5, 6.75, 3.5, 7.375], "texture": "#1"}, - "south": {"uv": [1.5, 5.75, 2.625, 6.375], "texture": "#1"}, - "west": {"uv": [3.75, 6.75, 3.75, 7.375], "texture": "#1"}, - "up": {"uv": [5.125, 7.25, 4, 7.25], "texture": "#1"}, - "down": {"uv": [8.375, 4, 7.25, 4], "texture": "#1"} - } - }, - { - "from": [10.9716, 20.5625, 8.02954], - "to": [12.3972, 21.29114, 8.02954], - "rotation": {"angle": 45, "axis": "z", "origin": [10.98744, 18.93098, 8.02954]}, - "faces": { - "north": {"uv": [3, 5.75, 4.125, 6.375], "texture": "#1"}, - "east": {"uv": [6.25625, 7.25, 6, 8.29375], "texture": "#1"}, - "south": {"uv": [5.75, 4, 6.875, 4.625], "texture": "#1"}, - "west": {"uv": [6.50625, 7.25, 6.25, 8.29375], "texture": "#1"}, - "up": {"uv": [8.375, 4.75, 7.50625, 5.16875], "texture": "#1"}, - "down": {"uv": [7.625, 7.25, 6.75625, 7.66875], "texture": "#1"} - } - }, - { - "from": [10.20228, 21.24691, 8.02986], - "to": [10.93092, 23.36947, 8.02986], - "faces": { - "north": {"uv": [5, 0, 5.625, 1.625], "texture": "#1"}, - "east": {"uv": [5.5, 7.25, 5.5, 8.875], "texture": "#1"}, - "south": {"uv": [5, 2, 5.625, 3.625], "texture": "#1"}, - "west": {"uv": [5.75, 7.25, 5.75, 8.875], "texture": "#1"}, - "up": {"uv": [7.875, 4.25, 7.25, 4.25], "texture": "#1"}, - "down": {"uv": [7.875, 4.5, 7.25, 4.5], "texture": "#1"} - } - }, - { - "from": [9.00728, 15.90762, 7.80282], - "to": [11.06648, 18.91722, 7.80282], - "rotation": {"angle": 45, "axis": "z", "origin": [8.69048, 18.91722, 7.80282]}, - "faces": { - "north": {"uv": [0, 0, 2.375, 1.625], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.375, 3.75, 6, 3.75], "rotation": 90, "texture": "#1"}, - "south": {"uv": [0, 2, 2.375, 3.625], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.625, 6.75, 6.25, 6.75], "rotation": 90, "texture": "#1"}, - "up": {"uv": [1.75, 6.75, 1.75, 8.375], "rotation": 90, "texture": "#1"}, - "down": {"uv": [1.5, 6.75, 1.5, 8.375], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [8.37811, 18.09241, 7.80314], - "to": [10.02547, 20.56345, 7.80314], - "faces": { - "north": {"uv": [2.75, 0, 4.75, 1.25], "rotation": 270, "texture": "#1"}, - "east": {"uv": [9, 2.75, 7, 2.75], "rotation": 90, "texture": "#1"}, - "south": {"uv": [2.75, 1.5, 4.75, 2.75], "rotation": 90, "texture": "#1"}, - "west": {"uv": [9, 3, 7, 3], "rotation": 90, "texture": "#1"}, - "up": {"uv": [2.25, 6.75, 2.25, 8], "rotation": 90, "texture": "#1"}, - "down": {"uv": [2, 6.75, 2, 8], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [10.2428, 17.17482, 7.92954], - "to": [10.97144, 18.60042, 7.92954], - "rotation": {"angle": 45, "axis": "z", "origin": [8.62712, 18.60042, 7.92954]}, - "faces": { - "north": {"uv": [0, 5.25, 1.125, 5.875], "rotation": 270, "texture": "#1"}, - "east": {"uv": [5.625, 7.25, 4, 7.6875], "rotation": 90, "texture": "#1"}, - "south": {"uv": [1.5, 5.75, 2.625, 6.375], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.875, 4, 7.25, 4.4375], "rotation": 90, "texture": "#1"}, - "up": {"uv": [3.75, 6.75, 4.25, 7.8125], "rotation": 90, "texture": "#1"}, - "down": {"uv": [3.5, 6.75, 4, 7.8125], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [8.15522, 19.54399, 7.77146], - "to": [10.27778, 20.27263, 7.77146], - "faces": { - "north": {"uv": [5, 0, 5.625, 1.625], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.01875, 4.25, 7.25, 4.54375], "rotation": 90, "texture": "#1"}, - "south": {"uv": [5, 2, 5.625, 3.625], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.01875, 4.5, 7.25, 4.79375], "rotation": 90, "texture": "#1"}, - "up": {"uv": [5.75, 7.25, 5.89375, 9.16875], "rotation": 90, "texture": "#1"}, - "down": {"uv": [5.5, 7.25, 5.64375, 9.16875], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [10.2428, 17.17482, 7.77114], - "to": [10.97144, 18.60042, 7.77114], - "rotation": {"angle": 45, "axis": "z", "origin": [8.62712, 18.60042, 7.77114]}, - "faces": { - "north": {"uv": [0, 5.25, 1.125, 5.875], "rotation": 270, "texture": "#1"}, - "east": {"uv": [5.3652, 10.89375, 3.33228, 10.38637], "rotation": 90, "texture": "#1"}, - "south": {"uv": [1.5, 5.75, 2.625, 6.375], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.74375, 7.02613, 6.71083, 6.51875], "rotation": 90, "texture": "#1"}, - "up": {"uv": [3.07239, 9.95498, 3.93581, 10.87888], "rotation": 90, "texture": "#1"}, - "down": {"uv": [2.8125, 9.95498, 3.67592, 10.87888], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [8.15522, 19.54399, 7.92986], - "to": [10.27778, 20.27263, 7.92986], - "faces": { - "north": {"uv": [5, 0, 5.625, 1.625], "rotation": 270, "texture": "#1"}, - "east": {"uv": [4.0625, 6.6875, 2.75, 7.44375], "rotation": 90, "texture": "#1"}, - "south": {"uv": [5, 2, 5.625, 3.625], "rotation": 90, "texture": "#1"}, - "west": {"uv": [4.0625, 6.9375, 2.75, 7.69375], "rotation": 90, "texture": "#1"}, - "up": {"uv": [1.25, 9.6875, 1.9375, 12.06875], "rotation": 90, "texture": "#1"}, - "down": {"uv": [1, 9.6875, 1.6875, 12.06875], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [4.93352, 15.90762, 7.80282], - "to": [6.99272, 18.91722, 7.80282], - "rotation": {"angle": -45, "axis": "z", "origin": [7.30952, 18.91722, 7.80282]}, - "faces": { - "north": {"uv": [0, 1.625, 2.375, 0], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.625, 6.75, 6.25, 6.99375], "rotation": 90, "texture": "#1"}, - "south": {"uv": [0, 3.625, 2.375, 2], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.375, 3.75, 6, 3.99375], "rotation": 90, "texture": "#1"}, - "up": {"uv": [1.75, 8.61875, 2.5875, 6.75], "rotation": 90, "texture": "#1"}, - "down": {"uv": [1.5, 8.61875, 2.3375, 6.75], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [5.97453, 18.09241, 7.80314], - "to": [7.62189, 20.56345, 7.80314], - "faces": { - "north": {"uv": [2.75, 1.25, 4.75, 0], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.74375, 7.3, 5.825, 7.90625], "rotation": 90, "texture": "#1"}, - "south": {"uv": [2.75, 2.75, 4.75, 1.5], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.74375, 7.05, 5.825, 7.65625], "rotation": 90, "texture": "#1"}, - "up": {"uv": [1.075, 12.90625, 1.99375, 11.05], "rotation": 90, "texture": "#1"}, - "down": {"uv": [0.825, 12.90625, 1.74375, 11.05], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [5.02856, 17.17482, 7.92954], - "to": [5.7572, 18.60042, 7.92954], - "rotation": {"angle": -45, "axis": "z", "origin": [7.37288, 18.60042, 7.92954]}, - "faces": { - "north": {"uv": [0, 5.875, 1.125, 5.25], "rotation": 270, "texture": "#1"}, - "east": {"uv": [5.46875, 7.25, 3.9375, 7.65625], "rotation": 90, "texture": "#1"}, - "south": {"uv": [1.5, 6.375, 2.625, 5.75], "rotation": 90, "texture": "#1"}, - "west": {"uv": [2.21875, 10.5, 0.6875, 10.90625], "rotation": 90, "texture": "#1"}, - "up": {"uv": [0.4375, 11.03125, 0.84375, 10], "rotation": 90, "texture": "#1"}, - "down": {"uv": [0.1875, 11.03125, 0.59375, 10], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [5.72222, 19.54399, 7.77146], - "to": [7.84478, 20.27263, 7.77146], - "faces": { - "north": {"uv": [5, 1.625, 5.625, 0], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.275, 4.7875, 7.34375, 4.4375], "rotation": 90, "texture": "#1"}, - "south": {"uv": [5, 3.625, 5.625, 2], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.275, 4.5375, 7.34375, 4.1875], "rotation": 90, "texture": "#1"}, - "up": {"uv": [5.75, 8.875, 6.05625, 7.25], "rotation": 90, "texture": "#1"}, - "down": {"uv": [5.5, 8.875, 5.80625, 7.25], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [5.02856, 17.17482, 7.77114], - "to": [5.7572, 18.60042, 7.77114], - "rotation": {"angle": -45, "axis": "z", "origin": [7.37288, 18.60042, 7.77114]}, - "faces": { - "north": {"uv": [0, 5.875, 1.125, 5.25], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.375, 4, 7.25, 4], "rotation": 90, "texture": "#1"}, - "south": {"uv": [1.5, 6.375, 2.625, 5.75], "rotation": 90, "texture": "#1"}, - "west": {"uv": [5.125, 7.25, 4, 7.25], "rotation": 90, "texture": "#1"}, - "up": {"uv": [3.75, 7.375, 3.75, 6.75], "rotation": 90, "texture": "#1"}, - "down": {"uv": [3.5, 7.375, 3.5, 6.75], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [5.72222, 19.54399, 7.92986], - "to": [7.84478, 20.27263, 7.92986], - "faces": { - "north": {"uv": [5, 1.625, 5.625, 0], "rotation": 270, "texture": "#1"}, - "east": {"uv": [7.875, 4.5, 7.25, 4.5], "rotation": 90, "texture": "#1"}, - "south": {"uv": [5, 3.625, 5.625, 2], "rotation": 90, "texture": "#1"}, - "west": {"uv": [7.875, 4.25, 7.25, 4.25], "rotation": 90, "texture": "#1"}, - "up": {"uv": [5.75, 8.875, 5.75, 7.25], "rotation": 90, "texture": "#1"}, - "down": {"uv": [5.5, 8.875, 5.5, 7.25], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [0.4, 15.2, 8], - "to": [15.4, 30.2, 8], - "faces": { - "north": {"uv": [0, 0, 15, 15], "texture": "#3"}, - "east": {"uv": [0, 0, 0, 7.5], "texture": "#3"}, - "south": {"uv": [15, 0, 0, 15], "texture": "#3"}, - "west": {"uv": [0, 0, 0, 7.5], "texture": "#3"}, - "up": {"uv": [7.5, 0, 0, 0], "texture": "#3"}, - "down": {"uv": [7.5, 0, 0, 0], "texture": "#3"} - } - } - ], - "display": { - "thirdperson_righthand": { - "rotation": [0.5, 90, 0], - "translation": [0, 4, 0.75], - "scale": [0.85, 0.85, 0.85] - }, - "thirdperson_lefthand": { - "rotation": [0.25, -90, 0], - "translation": [0, 4, 0.75], - "scale": [0.85, 0.85, 0.85] - }, - "firstperson_righthand": { - "rotation": [-15.71, -76.01, -1.19], - "translation": [3.13, -0.8, 1.13], - "scale": [0.68, 0.68, 0.68] - }, - "firstperson_lefthand": { - "rotation": [-180, 79.75, 167], - "translation": [3.13, -0.05, 1.38], - "scale": [0.68, 0.68, 0.68] - }, - "ground": { - "translation": [0, 15.5, 0] - }, - "gui": { - "rotation": [0, 0, -45], - "scale": [0.5, 0.5, 0.5] - }, - "head": { - "rotation": [0, 0, -31.25], - "translation": [5.25, -12.25, 3.75] - }, - "fixed": { - "rotation": [0, 0, 45], - "scale": [0.65, 0.65, 0.65] - } - } -} \ No newline at end of file diff --git a/src/main/resources/god/assets/useless_mod/models/item/endless_beaf_wrench.json b/src/main/resources/god/assets/useless_mod/models/item/endless_beaf_wrench.json deleted file mode 100644 index 4e97a9ce..00000000 --- a/src/main/resources/god/assets/useless_mod/models/item/endless_beaf_wrench.json +++ /dev/null @@ -1,880 +0,0 @@ -{ - "render_type":"cutout", - "format_version": "1.9.0", - "credit": "Made with _leng", - "texture_size": [32, 32], - "textures": { - "0": "useless_mod:item/endless_beaf_item", - "1": "useless_mod:item/endless_beaf_item_wing", - "2": "useless_mod:item/lable_wireless_transceiver_lighting", - "3": "useless_mod:item/endless_special_efficacy" - }, - "elements": [ - { - "name": "Octagon", - "from": [7.79289, -12.5001, 7.5], - "to": [8.20711, 18.0001, 8.5], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 8, 8]}, - "faces": { - "north": {"uv": [4, 0, 4.125, 7.625], "texture": "#0"}, - "east": {"uv": [0, 0, 0.25, 7.625], "texture": "#0"}, - "south": {"uv": [4.5, 0, 4.625, 7.625], "texture": "#0"}, - "west": {"uv": [0.5, 0, 0.75, 7.625], "texture": "#0"}, - "up": {"uv": [10.875, 8.25, 10.75, 8], "texture": "#0"}, - "down": {"uv": [10.875, 8.5, 10.75, 8.75], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [7.79289, -12.5003, 7.5], - "to": [8.20711, 18.0003, 8.5], - "faces": { - "north": {"uv": [5, 0, 5.125, 7.625], "texture": "#0"}, - "east": {"uv": [1, 0, 1.25, 7.625], "texture": "#0"}, - "south": {"uv": [5.5, 0, 5.625, 7.625], "texture": "#0"}, - "west": {"uv": [1.5, 0, 1.75, 7.625], "texture": "#0"}, - "up": {"uv": [10.875, 9.25, 10.75, 9], "texture": "#0"}, - "down": {"uv": [10.875, 9.5, 10.75, 9.75], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [7.5, -12.5, 7.79289], - "to": [8.5, 18, 8.20711], - "rotation": {"angle": 45, "axis": "y", "origin": [8, 8, 8]}, - "faces": { - "north": {"uv": [2, 0, 2.25, 7.625], "texture": "#0"}, - "east": {"uv": [6, 0, 6.125, 7.625], "texture": "#0"}, - "south": {"uv": [2.5, 0, 2.75, 7.625], "texture": "#0"}, - "west": {"uv": [6.5, 0, 6.625, 7.625], "texture": "#0"}, - "up": {"uv": [11, 10.125, 10.75, 10], "texture": "#0"}, - "down": {"uv": [11, 10.5, 10.75, 10.625], "texture": "#0"} - } - }, - { - "name": "Octagon", - "from": [7.5, -12.5002, 7.79289], - "to": [8.5, 18.0002, 8.20711], - "faces": { - "north": {"uv": [3, 0, 3.25, 7.625], "texture": "#0"}, - "east": {"uv": [7, 0, 7.125, 7.625], "texture": "#0"}, - "south": {"uv": [3.5, 0, 3.75, 7.625], "texture": "#0"}, - "west": {"uv": [7.5, 0, 7.625, 7.625], "texture": "#0"}, - "up": {"uv": [11.25, 0.125, 11, 0], "texture": "#0"}, - "down": {"uv": [11.25, 0.5, 11, 0.625], "texture": "#0"} - } - }, - { - "name": "duzuo", - "from": [6.869, -14.12, 8], - "to": [9.131, -10.118, 8], - "faces": { - "north": {"uv": [0, 9, 0.625, 10], "texture": "#0"}, - "east": {"uv": [10.25, 6, 10.375, 7], "texture": "#0"}, - "south": {"uv": [0.625, 9, 1.25, 10], "texture": "#0"}, - "west": {"uv": [10.25, 7.25, 10.375, 8.25], "texture": "#0"}, - "up": {"uv": [8, 10.625, 7.5, 10.5], "texture": "#0"}, - "down": {"uv": [8.75, 10.5, 8.25, 10.625], "texture": "#0"} - } - }, - { - "from": [7.4, -11.3, 7.4], - "to": [8.6, -10.1, 8.6], - "rotation": {"angle": -45, "axis": "z", "origin": [8, -10.7, 8]}, - "faces": { - "north": {"uv": [0, 10.75, 0.25, 11], "texture": "#0"}, - "east": {"uv": [0.5, 10.75, 0.75, 11], "texture": "#0"}, - "south": {"uv": [10.75, 1.5, 11, 1.75], "texture": "#0"}, - "west": {"uv": [10.75, 2, 11, 2.25], "texture": "#0"}, - "up": {"uv": [11, 4.75, 10.75, 4.5], "texture": "#0"}, - "down": {"uv": [11, 5, 10.75, 5.25], "texture": "#0"} - } - }, - { - "from": [7.8, -10.9, 7.3], - "to": [8.2, -10.5, 8.7], - "rotation": {"angle": -45, "axis": "z", "origin": [8, -10.7, 8]}, - "faces": { - "north": {"uv": [0.25, 0.75, 0.375, 0.875], "texture": "#0"}, - "east": {"uv": [0.25, 0, 0.5, 0.125], "texture": "#0"}, - "south": {"uv": [0.75, 0.25, 0.875, 0.375], "texture": "#0"}, - "west": {"uv": [0.25, 0.25, 0.5, 0.375], "texture": "#0"}, - "up": {"uv": [0.375, 0.75, 0.25, 0.5], "texture": "#0"}, - "down": {"uv": [0.875, 0, 0.75, 0.25], "texture": "#0"} - } - }, - { - "from": [7.8, 15.12, 7.3], - "to": [8.2, 15.52, 8.7], - "rotation": {"angle": -45, "axis": "z", "origin": [8, 15.32, 8]}, - "faces": { - "north": {"uv": [0.25, 0.75, 0.375, 0.875], "texture": "#0"}, - "east": {"uv": [0.25, 0, 0.5, 0.125], "texture": "#0"}, - "south": {"uv": [0.75, 0.25, 0.875, 0.375], "texture": "#0"}, - "west": {"uv": [0.25, 0.25, 0.5, 0.375], "texture": "#0"}, - "up": {"uv": [0.375, 0.75, 0.25, 0.5], "texture": "#0"}, - "down": {"uv": [0.875, 0, 0.75, 0.25], "texture": "#0"} - } - }, - { - "from": [7.39, -10.31, 7.39], - "to": [8.61, -9.84, 8.61], - "faces": { - "north": {"uv": [11, 1, 11.25, 1.125], "texture": "#0"}, - "east": {"uv": [1.5, 11, 1.75, 11.125], "texture": "#0"}, - "south": {"uv": [2, 11, 2.25, 11.125], "texture": "#0"}, - "west": {"uv": [2.5, 11, 2.75, 11.125], "texture": "#0"}, - "up": {"uv": [5.75, 11, 5.5, 10.75], "texture": "#0"}, - "down": {"uv": [6.25, 10.75, 6, 11], "texture": "#0"} - } - }, - { - "from": [7.39, 15.09, 7.39], - "to": [8.61, 15.56, 8.61], - "faces": { - "north": {"uv": [11, 2.5, 11.25, 2.625], "texture": "#0"}, - "east": {"uv": [3, 11, 3.25, 11.125], "texture": "#0"}, - "south": {"uv": [11, 3, 11.25, 3.125], "texture": "#0"}, - "west": {"uv": [3.5, 11, 3.75, 11.125], "texture": "#0"}, - "up": {"uv": [11, 6.25, 10.75, 6], "texture": "#0"}, - "down": {"uv": [6.75, 10.75, 6.5, 11], "texture": "#0"} - } - }, - { - "from": [7.39, 15.99, 7.39], - "to": [8.61, 17.71, 8.61], - "faces": { - "north": {"uv": [10.25, 8.5, 10.5, 8.875], "texture": "#0"}, - "east": {"uv": [10.25, 9.25, 10.5, 9.625], "texture": "#0"}, - "south": {"uv": [10.5, 0, 10.75, 0.375], "texture": "#0"}, - "west": {"uv": [10.5, 0.75, 10.75, 1.125], "texture": "#0"}, - "up": {"uv": [11, 6.75, 10.75, 6.5], "texture": "#0"}, - "down": {"uv": [7.25, 10.75, 7, 11], "texture": "#0"} - } - }, - { - "from": [6.65, 17.9, 6.68], - "to": [9.15, 20.4, 9.315], - "rotation": {"angle": -45, "axis": "z", "origin": [7.6, 18.9, 8.4]}, - "faces": { - "north": {"uv": [9.25, 1.5, 9.875, 2.125], "texture": "#0"}, - "east": {"uv": [9.25, 2.5, 9.875, 3.125], "texture": "#0"}, - "south": {"uv": [9.25, 6, 9.875, 6.625], "texture": "#0"}, - "west": {"uv": [9.25, 7, 9.875, 7.625], "texture": "#0"}, - "up": {"uv": [9.875, 8.625, 9.25, 8], "texture": "#0"}, - "down": {"uv": [9.875, 9, 9.25, 9.625], "texture": "#0"} - } - }, - { - "from": [7.8, 18.05, 6.48], - "to": [8.6, 18.85, 9.515], - "rotation": {"angle": -45, "axis": "z", "origin": [8.6, 18.9, 8.4]}, - "faces": { - "north": {"uv": [10.75, 7, 11, 7.25], "texture": "#0"}, - "east": {"uv": [10, 5.5, 10.75, 5.75], "texture": "#0"}, - "south": {"uv": [10.75, 7.5, 11, 7.75], "texture": "#0"}, - "west": {"uv": [0, 10.25, 0.75, 10.5], "texture": "#0"}, - "up": {"uv": [1.25, 11, 1, 10.25], "texture": "#0"}, - "down": {"uv": [10.5, 1.5, 10.25, 2.25], "texture": "#0"} - } - }, - { - "from": [8, 16.8, 7.6], - "to": [8, 17.8, 9.6], - "rotation": {"angle": -45, "axis": "x", "origin": [7.9, 17.8, 8.6]}, - "faces": { - "north": {"uv": [4, 11, 3.875, 11.25], "texture": "#0"}, - "east": {"uv": [10.25, 2.5, 10.75, 2.75], "texture": "#0"}, - "south": {"uv": [5.125, 11.25, 5.25, 11.5], "texture": "#0"}, - "west": {"uv": [10.25, 3, 10.75, 3.25], "texture": "#0"}, - "up": {"uv": [5.375, 11.75, 5.5, 11.25], "texture": "#0"}, - "down": {"uv": [5.625, 11.25, 5.75, 11.75], "texture": "#0"} - } - }, - { - "from": [8, 16.8, 6.4], - "to": [8, 17.8, 8.4], - "rotation": {"angle": 45, "axis": "x", "origin": [7.9, 17.8, 7.4]}, - "faces": { - "north": {"uv": [7.625, 10.875, 8, 11.375], "texture": "#0"}, - "east": {"uv": [7.5, 9, 8, 9.25], "texture": "#0"}, - "south": {"uv": [7.9, 7.5, 8.125, 7.75], "texture": "#0"}, - "west": {"uv": [9.25, 3.5, 9.75, 3.75], "texture": "#0"}, - "up": {"uv": [8.875, 11, 9.125, 10.5], "texture": "#0"}, - "down": {"uv": [10.875, 3.5, 11.125, 4], "texture": "#0"} - } - }, - { - "from": [8.10331, 19.03251, 6.9], - "to": [10.78331, 20.70751, 9.1], - "rotation": {"angle": 22.5, "axis": "z", "origin": [8.77331, 19.70251, 7]}, - "faces": { - "north": {"uv": [0, 8, 1, 8.625], "texture": "#0"}, - "east": {"uv": [7.5, 9.5, 8.125, 10.125], "texture": "#0"}, - "south": {"uv": [8, 0, 9, 0.625], "texture": "#0"}, - "west": {"uv": [1.5, 10, 2.125, 10.625], "texture": "#0"}, - "up": {"uv": [9, 1.625, 8, 1], "texture": "#0"}, - "down": {"uv": [2.25, 8, 1.25, 8.625], "texture": "#0"} - } - }, - { - "from": [5.21669, 19.03251, 6.9], - "to": [7.89669, 20.70751, 9.1], - "rotation": {"angle": -22.5, "axis": "z", "origin": [7.22669, 19.70251, 7]}, - "faces": { - "north": {"uv": [8, 2, 9, 2.625], "texture": "#0"}, - "east": {"uv": [2.5, 10, 3.125, 10.625], "texture": "#0"}, - "south": {"uv": [2.5, 8, 3.5, 8.625], "texture": "#0"}, - "west": {"uv": [3.5, 10, 4.125, 10.625], "texture": "#0"}, - "up": {"uv": [9, 3.625, 8, 3], "texture": "#0"}, - "down": {"uv": [4.75, 8, 3.75, 8.625], "texture": "#0"} - } - }, - { - "from": [6.48891, 21.54663, 6.99], - "to": [8.98891, 24.04663, 8.99], - "rotation": {"angle": -45, "axis": "z", "origin": [7.98891, 22.54663, 7.99]}, - "faces": { - "north": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "east": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "south": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "west": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "up": {"uv": [16, 16, 0, 0], "texture": "#2"}, - "down": {"uv": [16, 0, 0, 16], "texture": "#2"} - } - }, - { - "from": [6.48891, 19.04663, 6.99], - "to": [8.98891, 21.54663, 8.99], - "rotation": {"angle": -45, "axis": "z", "origin": [7.98891, 20.04663, 7.991]}, - "faces": { - "north": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "east": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "south": {"uv": [0, 0, 15.9, 16], "texture": "#2"}, - "west": {"uv": [0, 0, 15.9, 15.8], "texture": "#2"}, - "up": {"uv": [15.9, 15.8, 0, 0], "texture": "#2"}, - "down": {"uv": [16, 0, 0, 15.8], "texture": "#2"} - } - }, - { - "from": [6.22114, 20.40018, 6.989], - "to": [9.75668, 22.90018, 8.991], - "faces": { - "north": {"uv": [0, 0, 15.975, 16.025], "texture": "#2"}, - "east": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "south": {"uv": [0, 0, 15.975, 16], "texture": "#2"}, - "west": {"uv": [0, 0, 16, 16], "texture": "#2"}, - "up": {"uv": [15.975, 16, 0, 0], "texture": "#2"}, - "down": {"uv": [16, 0, 0, 16], "texture": "#2"} - } - }, - { - "from": [7.17754, 24.8802, 7.99], - "to": [8.80028, 27.1202, 7.99], - "faces": { - "north": {"uv": [8.25, 7, 8.875, 7.875], "texture": "#0"}, - "east": {"uv": [5, 11, 5.225, 12.1375], "texture": "#0"}, - "south": {"uv": [8.25, 8.25, 8.875, 9.125], "texture": "#0"}, - "west": {"uv": [5.25, 11, 5.475, 12.1375], "texture": "#0"}, - "up": {"uv": [11.85, 4.25, 11, 4.5125], "texture": "#0"}, - "down": {"uv": [11.85, 5.5, 11, 5.7625], "texture": "#0"} - } - }, - { - "name": "cube_outline", - "from": [9.08891, 24.14663, 9.09], - "to": [6.38891, 21.44663, 6.89], - "rotation": {"angle": -45, "axis": "z", "origin": [7.98891, 22.54663, 7.99]}, - "faces": { - "north": {"uv": [0.75, 11.125, 0.875, 11.25], "rotation": 180, "texture": "#0"}, - "east": {"uv": [0.875, 11.125, 1, 11.25], "rotation": 180, "texture": "#0"}, - "south": {"uv": [1, 11.125, 1.125, 11.25], "rotation": 180, "texture": "#0"}, - "west": {"uv": [1.125, 11.125, 1.25, 11.25], "rotation": 180, "texture": "#0"}, - "up": {"uv": [1.375, 11.125, 1.25, 11.25], "rotation": 180, "texture": "#0"}, - "down": {"uv": [1.5, 11.25, 1.375, 11.125], "rotation": 180, "texture": "#0"} - } - }, - { - "name": "cube_outline", - "from": [9.85668, 23.00018, 9.091], - "to": [6.12114, 20.30018, 6.889], - "faces": { - "north": {"uv": [0, 11.125, 0.125, 11.25], "rotation": 180, "texture": "#0"}, - "east": {"uv": [0.125, 11.125, 0.25, 11.25], "rotation": 180, "texture": "#0"}, - "south": {"uv": [0.25, 11.125, 0.375, 11.25], "rotation": 180, "texture": "#0"}, - "west": {"uv": [0.375, 11.125, 0.5, 11.25], "rotation": 180, "texture": "#0"}, - "up": {"uv": [0.625, 11.125, 0.5, 11.25], "rotation": 180, "texture": "#0"}, - "down": {"uv": [0.75, 11.25, 0.625, 11.125], "rotation": 180, "texture": "#0"} - } - }, - { - "name": "cube_outline", - "from": [9.08891, 21.64663, 9.09], - "to": [6.38891, 18.94663, 6.89], - "rotation": {"angle": -45, "axis": "z", "origin": [7.98891, 20.04663, 7.991]}, - "faces": { - "north": {"uv": [1.5, 11.125, 1.625, 11.25], "rotation": 180, "texture": "#0"}, - "east": {"uv": [1.625, 11.125, 1.75, 11.25], "rotation": 180, "texture": "#0"}, - "south": {"uv": [1.75, 11.125, 1.875, 11.25], "rotation": 180, "texture": "#0"}, - "west": {"uv": [1.875, 11.125, 2, 11.25], "rotation": 180, "texture": "#0"}, - "up": {"uv": [2.125, 11.125, 2, 11.25], "rotation": 180, "texture": "#0"}, - "down": {"uv": [2.25, 11.25, 2.125, 11.125], "rotation": 180, "texture": "#0"} - } - }, - { - "from": [7.34391, 24.0552, 7.4525], - "to": [8.63391, 25.3452, 8.5275], - "rotation": {"angle": -45, "axis": "z", "origin": [7.98891, 24.70019, 8.00075]}, - "faces": { - "north": {"uv": [5.75, 8, 5, 8.75], "texture": "#0"}, - "east": {"uv": [4.5, 9.5, 3.875, 8.75], "texture": "#0"}, - "south": {"uv": [8.75, 4, 8, 4.75], "texture": "#0"}, - "west": {"uv": [3.75, 8.75, 3.125, 9.5], "texture": "#0"}, - "up": {"uv": [4.875, 10.75, 4.125, 11.375], "texture": "#0"}, - "down": {"uv": [9.75, 4, 9, 4.625], "texture": "#0"} - } - }, - { - "from": [6.95149, 23.04957, 7.249], - "to": [8.72989, 24.82797, 8.731], - "rotation": {"angle": -45, "axis": "z", "origin": [7.9889, 23.79059, 7.87144]}, - "faces": { - "north": {"uv": [8, 5, 8.75, 5.75], "texture": "#0"}, - "east": {"uv": [4.5, 9, 5.125, 9.75], "texture": "#0"}, - "south": {"uv": [6, 8, 6.75, 8.75], "texture": "#0"}, - "west": {"uv": [9, 5, 9.625, 5.75], "texture": "#0"}, - "up": {"uv": [6.25, 9.625, 5.5, 9], "texture": "#0"}, - "down": {"uv": [7.25, 9, 6.5, 9.625], "texture": "#0"} - } - }, - { - "from": [5.266, 20.3249, 7.76026], - "to": [7.0084, 20.8001, 8.23546], - "rotation": {"angle": -45, "axis": "z", "origin": [7.0084, 20.4833, 7.83946]}, - "faces": { - "north": {"uv": [5.875, 5.75, 4.5, 6.125], "texture": "#1"}, - "east": {"uv": [4.875, 6.5, 4.5, 6.875], "texture": "#1"}, - "south": {"uv": [7.125, 5, 5.75, 5.375], "texture": "#1"}, - "west": {"uv": [6.875, 2, 6.5, 2.375], "texture": "#1"}, - "up": {"uv": [6, 0.375, 7.375, 0], "texture": "#1"}, - "down": {"uv": [6, 0.75, 7.375, 1.125], "texture": "#1"} - } - }, - { - "from": [4.16353, 23.23946, 7.83914], - "to": [5.90593, 23.55626, 8.15594], - "rotation": {"angle": -45, "axis": "z", "origin": [5.90593, 23.23946, 7.83946]}, - "faces": { - "north": {"uv": [7.375, 3.25, 6, 3.5], "texture": "#1"}, - "east": {"uv": [6, 6.5, 5.75, 6.75], "texture": "#1"}, - "south": {"uv": [7.625, 5.75, 6.25, 6], "texture": "#1"}, - "west": {"uv": [5.5, 6.5, 5.25, 6.75], "texture": "#1"}, - "up": {"uv": [6.25, 6.5, 7.625, 6.25], "texture": "#1"}, - "down": {"uv": [6.5, 1.5, 7.875, 1.75], "texture": "#1"} - } - }, - { - "from": [5.77633, 21.71536, 7.83788], - "to": [6.09313, 23.45776, 8.15468], - "faces": { - "north": {"uv": [6.25, 1.5, 6, 2.875], "texture": "#1"}, - "east": {"uv": [1.25, 6.25, 1, 7.625], "texture": "#1"}, - "south": {"uv": [0.75, 6.25, 0.5, 7.625], "texture": "#1"}, - "west": {"uv": [0.25, 6.25, 0, 7.625], "texture": "#1"}, - "up": {"uv": [1.5, 5.5, 1.75, 5.25], "texture": "#1"}, - "down": {"uv": [6.5, 2.75, 6.75, 3], "texture": "#1"} - } - }, - { - "from": [1.43272, 22.09898, 7.90282], - "to": [4.44232, 24.15818, 7.90282], - "rotation": {"angle": -45, "axis": "z", "origin": [4.44232, 21.78218, 7.90282]}, - "faces": { - "north": {"uv": [2.375, 0, 0, 1.625], "texture": "#1"}, - "east": {"uv": [1.75, 6.75, 1.75, 8.375], "texture": "#1"}, - "south": {"uv": [2.375, 2, 0, 3.625], "texture": "#1"}, - "west": {"uv": [1.5, 6.75, 1.5, 8.375], "texture": "#1"}, - "up": {"uv": [6, 3.75, 8.375, 3.75], "texture": "#1"}, - "down": {"uv": [6.25, 6.75, 8.625, 6.75], "texture": "#1"} - } - }, - { - "from": [3.61751, 21.4698, 7.90314], - "to": [6.08855, 23.11716, 7.90314], - "faces": { - "north": {"uv": [4.75, 0, 2.75, 1.25], "texture": "#1"}, - "east": {"uv": [2.25, 6.75, 2.25, 8], "texture": "#1"}, - "south": {"uv": [4.75, 1.5, 2.75, 2.75], "texture": "#1"}, - "west": {"uv": [2, 6.75, 2, 8], "texture": "#1"}, - "up": {"uv": [7, 2.75, 9, 2.75], "texture": "#1"}, - "down": {"uv": [7, 3, 9, 3], "texture": "#1"} - } - }, - { - "from": [3.64919, 20.55108, 7.90346], - "to": [6.08855, 21.84996, 7.90346], - "rotation": {"angle": 22.5, "axis": "z", "origin": [4.58375, 21.58068, 7.90282]}, - "faces": { - "north": {"uv": [4.625, 3, 2.75, 4], "texture": "#1"}, - "east": {"uv": [2.75, 6.75, 2.75, 7.75], "texture": "#1"}, - "south": {"uv": [1.875, 4, 0, 5], "texture": "#1"}, - "west": {"uv": [2.5, 6.75, 2.5, 7.75], "texture": "#1"}, - "up": {"uv": [5.25, 7, 7.125, 7], "texture": "#1"}, - "down": {"uv": [7.25, 2, 9.125, 2], "texture": "#1"} - } - }, - { - "from": [4.78967, 19.22052, 7.90314], - "to": [6.65879, 20.86788, 7.90314], - "rotation": {"angle": 45, "axis": "z", "origin": [5.15399, 20.25012, 7.90282]}, - "faces": { - "north": {"uv": [3.75, 4.25, 2.25, 5.5], "texture": "#1"}, - "east": {"uv": [3.25, 6.75, 3.25, 8], "texture": "#1"}, - "south": {"uv": [5.5, 4.25, 4, 5.5], "texture": "#1"}, - "west": {"uv": [3, 6.75, 3, 8], "texture": "#1"}, - "up": {"uv": [7.25, 2.25, 8.75, 2.25], "texture": "#1"}, - "down": {"uv": [7.25, 2.5, 8.75, 2.5], "texture": "#1"} - } - }, - { - "from": [2.69992, 23.3345, 8.02954], - "to": [4.12552, 24.06314, 8.02954], - "rotation": {"angle": -45, "axis": "z", "origin": [4.12552, 21.71882, 8.02954]}, - "faces": { - "north": {"uv": [1.125, 5.25, 0, 5.875], "texture": "#1"}, - "east": {"uv": [3.75, 6.75, 3.75, 7.375], "texture": "#1"}, - "south": {"uv": [2.625, 5.75, 1.5, 6.375], "texture": "#1"}, - "west": {"uv": [3.5, 6.75, 3.5, 7.375], "texture": "#1"}, - "up": {"uv": [4, 7.25, 5.125, 7.25], "texture": "#1"}, - "down": {"uv": [7.25, 4, 8.375, 4], "texture": "#1"} - } - }, - { - "from": [3.6028, 20.5625, 7.87114], - "to": [5.0284, 21.29114, 7.87114], - "rotation": {"angle": -45, "axis": "z", "origin": [5.01256, 18.93098, 7.87114]}, - "faces": { - "north": {"uv": [4.125, 5.75, 3, 6.375], "texture": "#1"}, - "east": {"uv": [6.25, 7.25, 6.25, 7.875], "texture": "#1"}, - "south": {"uv": [6.875, 4, 5.75, 4.625], "texture": "#1"}, - "west": {"uv": [6, 7.25, 6, 7.875], "texture": "#1"}, - "up": {"uv": [7.25, 4.75, 8.375, 4.75], "texture": "#1"}, - "down": {"uv": [6.5, 7.25, 7.625, 7.25], "texture": "#1"} - } - }, - { - "from": [5.06908, 21.24691, 7.87146], - "to": [5.79772, 23.36947, 7.87146], - "faces": { - "north": {"uv": [5.625, 0, 5, 1.625], "texture": "#1"}, - "east": {"uv": [5.75, 7.25, 5.75, 8.875], "texture": "#1"}, - "south": {"uv": [5.625, 2, 5, 3.625], "texture": "#1"}, - "west": {"uv": [5.5, 7.25, 5.5, 8.875], "texture": "#1"}, - "up": {"uv": [7.25, 4.25, 7.875, 4.25], "texture": "#1"}, - "down": {"uv": [7.25, 4.5, 7.875, 4.5], "texture": "#1"} - } - }, - { - "from": [2.69992, 23.3345, 7.87114], - "to": [4.12552, 24.06314, 7.87114], - "rotation": {"angle": -45, "axis": "z", "origin": [4.12552, 21.71882, 7.87114]}, - "faces": { - "north": {"uv": [1.125, 5.25, 0, 5.875], "texture": "#1"}, - "east": {"uv": [3.75, 6.75, 3.75, 7.375], "texture": "#1"}, - "south": {"uv": [2.625, 5.75, 1.5, 6.375], "texture": "#1"}, - "west": {"uv": [3.5, 6.75, 3.5, 7.375], "texture": "#1"}, - "up": {"uv": [4, 7.25, 5.125, 7.25], "texture": "#1"}, - "down": {"uv": [7.25, 4, 8.375, 4], "texture": "#1"} - } - }, - { - "from": [3.6028, 20.5625, 8.02954], - "to": [5.0284, 21.29114, 8.02954], - "rotation": {"angle": -45, "axis": "z", "origin": [5.01256, 18.93098, 8.02954]}, - "faces": { - "north": {"uv": [4.125, 5.75, 3, 6.375], "texture": "#1"}, - "east": {"uv": [6.25, 7.25, 6.25, 7.875], "texture": "#1"}, - "south": {"uv": [6.875, 4, 5.75, 4.625], "texture": "#1"}, - "west": {"uv": [6, 7.25, 6, 7.875], "texture": "#1"}, - "up": {"uv": [7.25, 4.75, 8.375, 4.75], "texture": "#1"}, - "down": {"uv": [6.5, 7.25, 7.625, 7.25], "texture": "#1"} - } - }, - { - "from": [5.06908, 21.24691, 8.02986], - "to": [5.79772, 23.36947, 8.02986], - "faces": { - "north": {"uv": [5.625, 0, 5, 1.625], "texture": "#1"}, - "east": {"uv": [5.75, 7.25, 5.75, 8.875], "texture": "#1"}, - "south": {"uv": [5.625, 2, 5, 3.625], "texture": "#1"}, - "west": {"uv": [5.5, 7.25, 5.5, 8.875], "texture": "#1"}, - "up": {"uv": [7.25, 4.25, 7.875, 4.25], "texture": "#1"}, - "down": {"uv": [7.25, 4.5, 7.875, 4.5], "texture": "#1"} - } - }, - { - "from": [8.9916, 20.3249, 7.76026], - "to": [10.734, 20.8001, 8.23546], - "rotation": {"angle": 45, "axis": "z", "origin": [8.9916, 20.4833, 7.83946]}, - "faces": { - "north": {"uv": [4.5, 5.75, 5.875, 6.125], "texture": "#1"}, - "east": {"uv": [6.5, 2, 6.875, 2.375], "texture": "#1"}, - "south": {"uv": [5.75, 5, 7.125, 5.375], "texture": "#1"}, - "west": {"uv": [4.5, 6.5, 4.875, 6.875], "texture": "#1"}, - "up": {"uv": [7.375, 0.375, 6, 0], "texture": "#1"}, - "down": {"uv": [7.375, 0.75, 6, 1.125], "texture": "#1"} - } - }, - { - "from": [10.09407, 23.23946, 7.83914], - "to": [11.83647, 23.55626, 8.15594], - "rotation": {"angle": 45, "axis": "z", "origin": [10.09407, 23.23946, 7.83946]}, - "faces": { - "north": {"uv": [6, 3.25, 7.375, 3.5], "texture": "#1"}, - "east": {"uv": [5.25, 6.5, 5.5, 6.75], "texture": "#1"}, - "south": {"uv": [6.25, 5.75, 7.625, 6], "texture": "#1"}, - "west": {"uv": [5.75, 6.5, 6, 6.75], "texture": "#1"}, - "up": {"uv": [7.625, 6.5, 6.25, 6.25], "texture": "#1"}, - "down": {"uv": [7.875, 1.5, 6.5, 1.75], "texture": "#1"} - } - }, - { - "from": [9.90687, 21.71536, 7.83788], - "to": [10.22367, 23.45776, 8.15468], - "faces": { - "north": {"uv": [6, 1.5, 6.25, 2.875], "texture": "#1"}, - "east": {"uv": [0, 6.25, 0.25, 7.625], "texture": "#1"}, - "south": {"uv": [0.5, 6.25, 0.75, 7.625], "texture": "#1"}, - "west": {"uv": [1, 6.25, 1.25, 7.625], "texture": "#1"}, - "up": {"uv": [1.75, 5.5, 1.5, 5.25], "texture": "#1"}, - "down": {"uv": [6.75, 2.75, 6.5, 3], "texture": "#1"} - } - }, - { - "from": [11.55768, 22.09898, 7.90282], - "to": [14.56728, 24.15818, 7.90282], - "rotation": {"angle": 45, "axis": "z", "origin": [11.55768, 21.78218, 7.90282]}, - "faces": { - "north": {"uv": [0, 0, 2.375, 1.625], "texture": "#1"}, - "east": {"uv": [1.5, 6.75, 1.5, 8.375], "texture": "#1"}, - "south": {"uv": [0, 2, 2.375, 3.625], "texture": "#1"}, - "west": {"uv": [1.75, 6.75, 1.75, 8.375], "texture": "#1"}, - "up": {"uv": [8.375, 3.75, 6, 3.75], "texture": "#1"}, - "down": {"uv": [8.625, 6.75, 6.25, 6.75], "texture": "#1"} - } - }, - { - "from": [9.91145, 21.4698, 7.90314], - "to": [12.38249, 23.11716, 7.90314], - "faces": { - "north": {"uv": [2.75, 0, 4.75, 1.25], "texture": "#1"}, - "east": {"uv": [2, 6.75, 2, 8], "texture": "#1"}, - "south": {"uv": [2.75, 1.5, 4.75, 2.75], "texture": "#1"}, - "west": {"uv": [2.25, 6.75, 2.25, 8], "texture": "#1"}, - "up": {"uv": [9, 2.75, 7, 2.75], "texture": "#1"}, - "down": {"uv": [9, 3, 7, 3], "texture": "#1"} - } - }, - { - "from": [9.91145, 20.55108, 7.90346], - "to": [12.35081, 21.84996, 7.90346], - "rotation": {"angle": -22.5, "axis": "z", "origin": [11.41625, 21.58068, 7.90282]}, - "faces": { - "north": {"uv": [2.75, 3, 4.625, 4], "texture": "#1"}, - "east": {"uv": [2.5, 6.75, 2.5, 7.75], "texture": "#1"}, - "south": {"uv": [0, 4, 1.875, 5], "texture": "#1"}, - "west": {"uv": [2.75, 6.75, 2.75, 7.75], "texture": "#1"}, - "up": {"uv": [7.125, 7, 5.25, 7], "texture": "#1"}, - "down": {"uv": [9.125, 2, 7.25, 2], "texture": "#1"} - } - }, - { - "from": [9.34121, 19.22052, 7.90314], - "to": [11.21033, 20.86788, 7.90314], - "rotation": {"angle": -45, "axis": "z", "origin": [10.84601, 20.25012, 7.90282]}, - "faces": { - "north": {"uv": [2.25, 4.25, 3.75, 5.5], "texture": "#1"}, - "east": {"uv": [3, 6.75, 3, 8], "texture": "#1"}, - "south": {"uv": [4, 4.25, 5.5, 5.5], "texture": "#1"}, - "west": {"uv": [3.25, 6.75, 3.25, 8], "texture": "#1"}, - "up": {"uv": [8.75, 2.25, 7.25, 2.25], "texture": "#1"}, - "down": {"uv": [8.75, 2.5, 7.25, 2.5], "texture": "#1"} - } - }, - { - "from": [11.87448, 23.3345, 8.02954], - "to": [13.30008, 24.06314, 8.02954], - "rotation": {"angle": 45, "axis": "z", "origin": [11.87448, 21.71882, 8.02954]}, - "faces": { - "north": {"uv": [0, 5.25, 1.125, 5.875], "texture": "#1"}, - "east": {"uv": [3.5, 6.75, 3.5, 7.375], "texture": "#1"}, - "south": {"uv": [1.5, 5.75, 2.625, 6.375], "texture": "#1"}, - "west": {"uv": [3.75, 6.75, 3.75, 7.375], "texture": "#1"}, - "up": {"uv": [5.125, 7.25, 4, 7.25], "texture": "#1"}, - "down": {"uv": [8.375, 4, 7.25, 4], "texture": "#1"} - } - }, - { - "from": [10.9716, 20.5625, 7.87114], - "to": [12.3972, 21.29114, 7.87114], - "rotation": {"angle": 45, "axis": "z", "origin": [10.98744, 18.93098, 7.87114]}, - "faces": { - "north": {"uv": [3, 5.75, 4.125, 6.375], "texture": "#1"}, - "east": {"uv": [6, 7.25, 6, 7.875], "texture": "#1"}, - "south": {"uv": [5.75, 4, 6.875, 4.625], "texture": "#1"}, - "west": {"uv": [6.25, 7.25, 6.25, 7.875], "texture": "#1"}, - "up": {"uv": [8.375, 4.75, 7.25, 4.75], "texture": "#1"}, - "down": {"uv": [7.625, 7.25, 6.5, 7.25], "texture": "#1"} - } - }, - { - "from": [10.20228, 21.24691, 7.87146], - "to": [10.93092, 23.36947, 7.87146], - "faces": { - "north": {"uv": [5, 0, 5.625, 1.625], "texture": "#1"}, - "east": {"uv": [5.5, 7.25, 5.5, 8.875], "texture": "#1"}, - "south": {"uv": [5, 2, 5.625, 3.625], "texture": "#1"}, - "west": {"uv": [5.75, 7.25, 5.75, 8.875], "texture": "#1"}, - "up": {"uv": [7.875, 4.25, 7.25, 4.25], "texture": "#1"}, - "down": {"uv": [7.875, 4.5, 7.25, 4.5], "texture": "#1"} - } - }, - { - "from": [11.87448, 23.3345, 7.87114], - "to": [13.30008, 24.06314, 7.87114], - "rotation": {"angle": 45, "axis": "z", "origin": [11.87448, 21.71882, 7.87114]}, - "faces": { - "north": {"uv": [0, 5.25, 1.125, 5.875], "texture": "#1"}, - "east": {"uv": [3.5, 6.75, 3.5, 7.375], "texture": "#1"}, - "south": {"uv": [1.5, 5.75, 2.625, 6.375], "texture": "#1"}, - "west": {"uv": [3.75, 6.75, 3.75, 7.375], "texture": "#1"}, - "up": {"uv": [5.125, 7.25, 4, 7.25], "texture": "#1"}, - "down": {"uv": [8.375, 4, 7.25, 4], "texture": "#1"} - } - }, - { - "from": [10.9716, 20.5625, 8.02954], - "to": [12.3972, 21.29114, 8.02954], - "rotation": {"angle": 45, "axis": "z", "origin": [10.98744, 18.93098, 8.02954]}, - "faces": { - "north": {"uv": [3, 5.75, 4.125, 6.375], "texture": "#1"}, - "east": {"uv": [6.25625, 7.25, 6, 8.29375], "texture": "#1"}, - "south": {"uv": [5.75, 4, 6.875, 4.625], "texture": "#1"}, - "west": {"uv": [6.50625, 7.25, 6.25, 8.29375], "texture": "#1"}, - "up": {"uv": [8.375, 4.75, 7.50625, 5.16875], "texture": "#1"}, - "down": {"uv": [7.625, 7.25, 6.75625, 7.66875], "texture": "#1"} - } - }, - { - "from": [10.20228, 21.24691, 8.02986], - "to": [10.93092, 23.36947, 8.02986], - "faces": { - "north": {"uv": [5, 0, 5.625, 1.625], "texture": "#1"}, - "east": {"uv": [5.5, 7.25, 5.5, 8.875], "texture": "#1"}, - "south": {"uv": [5, 2, 5.625, 3.625], "texture": "#1"}, - "west": {"uv": [5.75, 7.25, 5.75, 8.875], "texture": "#1"}, - "up": {"uv": [7.875, 4.25, 7.25, 4.25], "texture": "#1"}, - "down": {"uv": [7.875, 4.5, 7.25, 4.5], "texture": "#1"} - } - }, - { - "from": [9.00728, 15.90762, 7.80282], - "to": [11.06648, 18.91722, 7.80282], - "rotation": {"angle": 45, "axis": "z", "origin": [8.69048, 18.91722, 7.80282]}, - "faces": { - "north": {"uv": [0, 0, 2.375, 1.625], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.375, 3.75, 6, 3.75], "rotation": 90, "texture": "#1"}, - "south": {"uv": [0, 2, 2.375, 3.625], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.625, 6.75, 6.25, 6.75], "rotation": 90, "texture": "#1"}, - "up": {"uv": [1.75, 6.75, 1.75, 8.375], "rotation": 90, "texture": "#1"}, - "down": {"uv": [1.5, 6.75, 1.5, 8.375], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [8.37811, 18.09241, 7.80314], - "to": [10.02547, 20.56345, 7.80314], - "faces": { - "north": {"uv": [2.75, 0, 4.75, 1.25], "rotation": 270, "texture": "#1"}, - "east": {"uv": [9, 2.75, 7, 2.75], "rotation": 90, "texture": "#1"}, - "south": {"uv": [2.75, 1.5, 4.75, 2.75], "rotation": 90, "texture": "#1"}, - "west": {"uv": [9, 3, 7, 3], "rotation": 90, "texture": "#1"}, - "up": {"uv": [2.25, 6.75, 2.25, 8], "rotation": 90, "texture": "#1"}, - "down": {"uv": [2, 6.75, 2, 8], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [10.2428, 17.17482, 7.92954], - "to": [10.97144, 18.60042, 7.92954], - "rotation": {"angle": 45, "axis": "z", "origin": [8.62712, 18.60042, 7.92954]}, - "faces": { - "north": {"uv": [0, 5.25, 1.125, 5.875], "rotation": 270, "texture": "#1"}, - "east": {"uv": [5.625, 7.25, 4, 7.6875], "rotation": 90, "texture": "#1"}, - "south": {"uv": [1.5, 5.75, 2.625, 6.375], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.875, 4, 7.25, 4.4375], "rotation": 90, "texture": "#1"}, - "up": {"uv": [3.75, 6.75, 4.25, 7.8125], "rotation": 90, "texture": "#1"}, - "down": {"uv": [3.5, 6.75, 4, 7.8125], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [8.15522, 19.54399, 7.77146], - "to": [10.27778, 20.27263, 7.77146], - "faces": { - "north": {"uv": [5, 0, 5.625, 1.625], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.01875, 4.25, 7.25, 4.54375], "rotation": 90, "texture": "#1"}, - "south": {"uv": [5, 2, 5.625, 3.625], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.01875, 4.5, 7.25, 4.79375], "rotation": 90, "texture": "#1"}, - "up": {"uv": [5.75, 7.25, 5.89375, 9.16875], "rotation": 90, "texture": "#1"}, - "down": {"uv": [5.5, 7.25, 5.64375, 9.16875], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [10.2428, 17.17482, 7.77114], - "to": [10.97144, 18.60042, 7.77114], - "rotation": {"angle": 45, "axis": "z", "origin": [8.62712, 18.60042, 7.77114]}, - "faces": { - "north": {"uv": [0, 5.25, 1.125, 5.875], "rotation": 270, "texture": "#1"}, - "east": {"uv": [5.3652, 10.89375, 3.33228, 10.38637], "rotation": 90, "texture": "#1"}, - "south": {"uv": [1.5, 5.75, 2.625, 6.375], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.74375, 7.02613, 6.71083, 6.51875], "rotation": 90, "texture": "#1"}, - "up": {"uv": [3.07239, 9.95498, 3.93581, 10.87888], "rotation": 90, "texture": "#1"}, - "down": {"uv": [2.8125, 9.95498, 3.67592, 10.87888], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [8.15522, 19.54399, 7.92986], - "to": [10.27778, 20.27263, 7.92986], - "faces": { - "north": {"uv": [5, 0, 5.625, 1.625], "rotation": 270, "texture": "#1"}, - "east": {"uv": [4.0625, 6.6875, 2.75, 7.44375], "rotation": 90, "texture": "#1"}, - "south": {"uv": [5, 2, 5.625, 3.625], "rotation": 90, "texture": "#1"}, - "west": {"uv": [4.0625, 6.9375, 2.75, 7.69375], "rotation": 90, "texture": "#1"}, - "up": {"uv": [1.25, 9.6875, 1.9375, 12.06875], "rotation": 90, "texture": "#1"}, - "down": {"uv": [1, 9.6875, 1.6875, 12.06875], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [4.93352, 15.90762, 7.80282], - "to": [6.99272, 18.91722, 7.80282], - "rotation": {"angle": -45, "axis": "z", "origin": [7.30952, 18.91722, 7.80282]}, - "faces": { - "north": {"uv": [0, 1.625, 2.375, 0], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.625, 6.75, 6.25, 6.99375], "rotation": 90, "texture": "#1"}, - "south": {"uv": [0, 3.625, 2.375, 2], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.375, 3.75, 6, 3.99375], "rotation": 90, "texture": "#1"}, - "up": {"uv": [1.75, 8.61875, 2.5875, 6.75], "rotation": 90, "texture": "#1"}, - "down": {"uv": [1.5, 8.61875, 2.3375, 6.75], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [5.97453, 18.09241, 7.80314], - "to": [7.62189, 20.56345, 7.80314], - "faces": { - "north": {"uv": [2.75, 1.25, 4.75, 0], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.74375, 7.3, 5.825, 7.90625], "rotation": 90, "texture": "#1"}, - "south": {"uv": [2.75, 2.75, 4.75, 1.5], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.74375, 7.05, 5.825, 7.65625], "rotation": 90, "texture": "#1"}, - "up": {"uv": [1.075, 12.90625, 1.99375, 11.05], "rotation": 90, "texture": "#1"}, - "down": {"uv": [0.825, 12.90625, 1.74375, 11.05], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [5.02856, 17.17482, 7.92954], - "to": [5.7572, 18.60042, 7.92954], - "rotation": {"angle": -45, "axis": "z", "origin": [7.37288, 18.60042, 7.92954]}, - "faces": { - "north": {"uv": [0, 5.875, 1.125, 5.25], "rotation": 270, "texture": "#1"}, - "east": {"uv": [5.46875, 7.25, 3.9375, 7.65625], "rotation": 90, "texture": "#1"}, - "south": {"uv": [1.5, 6.375, 2.625, 5.75], "rotation": 90, "texture": "#1"}, - "west": {"uv": [2.21875, 10.5, 0.6875, 10.90625], "rotation": 90, "texture": "#1"}, - "up": {"uv": [0.4375, 11.03125, 0.84375, 10], "rotation": 90, "texture": "#1"}, - "down": {"uv": [0.1875, 11.03125, 0.59375, 10], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [5.72222, 19.54399, 7.77146], - "to": [7.84478, 20.27263, 7.77146], - "faces": { - "north": {"uv": [5, 1.625, 5.625, 0], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.275, 4.7875, 7.34375, 4.4375], "rotation": 90, "texture": "#1"}, - "south": {"uv": [5, 3.625, 5.625, 2], "rotation": 90, "texture": "#1"}, - "west": {"uv": [8.275, 4.5375, 7.34375, 4.1875], "rotation": 90, "texture": "#1"}, - "up": {"uv": [5.75, 8.875, 6.05625, 7.25], "rotation": 90, "texture": "#1"}, - "down": {"uv": [5.5, 8.875, 5.80625, 7.25], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [5.02856, 17.17482, 7.77114], - "to": [5.7572, 18.60042, 7.77114], - "rotation": {"angle": -45, "axis": "z", "origin": [7.37288, 18.60042, 7.77114]}, - "faces": { - "north": {"uv": [0, 5.875, 1.125, 5.25], "rotation": 270, "texture": "#1"}, - "east": {"uv": [8.375, 4, 7.25, 4], "rotation": 90, "texture": "#1"}, - "south": {"uv": [1.5, 6.375, 2.625, 5.75], "rotation": 90, "texture": "#1"}, - "west": {"uv": [5.125, 7.25, 4, 7.25], "rotation": 90, "texture": "#1"}, - "up": {"uv": [3.75, 7.375, 3.75, 6.75], "rotation": 90, "texture": "#1"}, - "down": {"uv": [3.5, 7.375, 3.5, 6.75], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [5.72222, 19.54399, 7.92986], - "to": [7.84478, 20.27263, 7.92986], - "faces": { - "north": {"uv": [5, 1.625, 5.625, 0], "rotation": 270, "texture": "#1"}, - "east": {"uv": [7.875, 4.5, 7.25, 4.5], "rotation": 90, "texture": "#1"}, - "south": {"uv": [5, 3.625, 5.625, 2], "rotation": 90, "texture": "#1"}, - "west": {"uv": [7.875, 4.25, 7.25, 4.25], "rotation": 90, "texture": "#1"}, - "up": {"uv": [5.75, 8.875, 5.75, 7.25], "rotation": 90, "texture": "#1"}, - "down": {"uv": [5.5, 8.875, 5.5, 7.25], "rotation": 90, "texture": "#1"} - } - }, - { - "from": [0.4, 15.2, 8], - "to": [15.4, 30.2, 8], - "faces": { - "north": {"uv": [0, 0, 15, 15], "texture": "#3"}, - "east": {"uv": [0, 0, 0, 7.5], "texture": "#3"}, - "south": {"uv": [15, 0, 0, 15], "texture": "#3"}, - "west": {"uv": [0, 0, 0, 7.5], "texture": "#3"}, - "up": {"uv": [7.5, 0, 0, 0], "texture": "#3"}, - "down": {"uv": [7.5, 0, 0, 0], "texture": "#3"} - } - } - ], - "display": { - "thirdperson_righthand": { - "rotation": [0.5, 90, 0], - "translation": [0, 4, 0.75], - "scale": [0.85, 0.85, 0.85] - }, - "thirdperson_lefthand": { - "rotation": [0.25, -90, 0], - "translation": [0, 4, 0.75], - "scale": [0.85, 0.85, 0.85] - }, - "firstperson_righthand": { - "rotation": [-15.71, -76.01, -1.19], - "translation": [3.13, -0.8, 1.13], - "scale": [0.68, 0.68, 0.68] - }, - "firstperson_lefthand": { - "rotation": [-180, 79.75, 167], - "translation": [3.13, -0.05, 1.38], - "scale": [0.68, 0.68, 0.68] - }, - "ground": { - "translation": [0, 15.5, 0] - }, - "gui": { - "rotation": [0, 0, -45], - "scale": [0.5, 0.5, 0.5] - }, - "head": { - "rotation": [0, 0, -31.25], - "translation": [5.25, -12.25, 3.75] - }, - "fixed": { - "rotation": [0, 0, 45], - "scale": [0.65, 0.65, 0.65] - } - } -} \ No newline at end of file diff --git a/src/main/resources/god/assets/useless_mod/textures/item/endless_beaf_item.png b/src/main/resources/god/assets/useless_mod/textures/item/endless_beaf_item.png deleted file mode 100644 index 4a1cc525..00000000 Binary files a/src/main/resources/god/assets/useless_mod/textures/item/endless_beaf_item.png and /dev/null differ diff --git a/src/main/resources/god/assets/useless_mod/textures/item/endless_beaf_item_wing.png b/src/main/resources/god/assets/useless_mod/textures/item/endless_beaf_item_wing.png deleted file mode 100644 index b1c778ee..00000000 Binary files a/src/main/resources/god/assets/useless_mod/textures/item/endless_beaf_item_wing.png and /dev/null differ diff --git a/src/main/resources/god/assets/useless_mod/textures/item/endless_special_efficacy.png b/src/main/resources/god/assets/useless_mod/textures/item/endless_special_efficacy.png deleted file mode 100644 index cafb5786..00000000 Binary files a/src/main/resources/god/assets/useless_mod/textures/item/endless_special_efficacy.png and /dev/null differ diff --git a/src/main/resources/god/assets/useless_mod/textures/item/endless_special_efficacy.png.mcmeta b/src/main/resources/god/assets/useless_mod/textures/item/endless_special_efficacy.png.mcmeta deleted file mode 100644 index 09015be3..00000000 --- a/src/main/resources/god/assets/useless_mod/textures/item/endless_special_efficacy.png.mcmeta +++ /dev/null @@ -1,5 +0,0 @@ -{ - "animation": { - "frametime": 3 - } -} \ No newline at end of file diff --git a/src/main/resources/god/assets/useless_mod/textures/item/lable_wireless_transceiver_lighting.png b/src/main/resources/god/assets/useless_mod/textures/item/lable_wireless_transceiver_lighting.png deleted file mode 100644 index 412260b8..00000000 Binary files a/src/main/resources/god/assets/useless_mod/textures/item/lable_wireless_transceiver_lighting.png and /dev/null differ diff --git a/src/main/resources/god/assets/useless_mod/textures/item/lable_wireless_transceiver_lighting.png.mcmeta b/src/main/resources/god/assets/useless_mod/textures/item/lable_wireless_transceiver_lighting.png.mcmeta deleted file mode 100644 index 09015be3..00000000 --- a/src/main/resources/god/assets/useless_mod/textures/item/lable_wireless_transceiver_lighting.png.mcmeta +++ /dev/null @@ -1,5 +0,0 @@ -{ - "animation": { - "frametime": 3 - } -} \ No newline at end of file diff --git a/src/main/resources/god/pack.mcmeta b/src/main/resources/god/pack.mcmeta deleted file mode 100644 index 025b9024..00000000 --- a/src/main/resources/god/pack.mcmeta +++ /dev/null @@ -1,6 +0,0 @@ -{ - "pack": { - "pack_format": 15, - "description": "麦淇淋自制uselessmod的全能工具3D材质_测试包1.0.0" - } -} \ No newline at end of file diff --git a/src/main/resources/god/pack.png b/src/main/resources/god/pack.png deleted file mode 100644 index b3783b74..00000000 Binary files a/src/main/resources/god/pack.png and /dev/null differ diff --git a/src/main/resources/kubejs.plugins.txt b/src/main/resources/kubejs.plugins.txt new file mode 100644 index 00000000..3d005170 --- /dev/null +++ b/src/main/resources/kubejs.plugins.txt @@ -0,0 +1 @@ +com.sorrowmist.useless.compat.kubejs.UselessKubeJSPlugin diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta index eca79ae4..69cc8e67 100644 --- a/src/main/resources/pack.mcmeta +++ b/src/main/resources/pack.mcmeta @@ -1,8 +1,8 @@ { "pack": { "description": { - "text": "${mod_id} resources" + "text": "Useless Mod resources" }, "pack_format": 15 } -} \ No newline at end of file +} diff --git a/src/main/resources/useless_mod.compact.mixins.json b/src/main/resources/useless_mod.compact.mixins.json new file mode 100644 index 00000000..77159ef8 --- /dev/null +++ b/src/main/resources/useless_mod.compact.mixins.json @@ -0,0 +1,14 @@ +{ + "required": false, + "package": "com.sorrowmist.useless.mixin.compact", + "compatibilityLevel": "JAVA_17", + "mixins": [ + "GridMachineClassMixin", + "MenuDataCodecsCompactMixin" + ], + "injectors": { + "defaultRequire": 1 + }, + "refmap": "useless_mod.refmap.json", + "minVersion": "0.8" +} diff --git a/src/main/resources/useless_mod.create.mixins.json b/src/main/resources/useless_mod.create.mixins.json new file mode 100644 index 00000000..3bb0156c --- /dev/null +++ b/src/main/resources/useless_mod.create.mixins.json @@ -0,0 +1,17 @@ +{ + "required": false, + "package": "com.sorrowmist.useless.mixin.create", + "compatibilityLevel": "JAVA_17", + "mixins": [ + "KineticBlockEntityMixin", + "KineticNetworkMixin" + ], + "injectors": { + "defaultRequire": 1 + }, + "overwrites": { + "requireAnnotations": true + }, + "refmap": "useless_mod.refmap.json", + "minVersion": "0.8" +} diff --git a/src/main/resources/useless_mod.item_obliterator.mixins.json b/src/main/resources/useless_mod.item_obliterator.mixins.json new file mode 100644 index 00000000..1f8dc4c9 --- /dev/null +++ b/src/main/resources/useless_mod.item_obliterator.mixins.json @@ -0,0 +1,17 @@ +{ + "required": false, + "package": "com.sorrowmist.useless.mixin.itemobliterator", + "compatibilityLevel": "JAVA_17", + "mixins": [ + "ItemObliteratorUtilsMixin", + "ItemObliteratorContainerMixin" + ], + "injectors": { + "defaultRequire": 1 + }, + "overwrites": { + "requireAnnotations": true + }, + "refmap": "useless_mod.refmap.json", + "minVersion": "0.8" +} diff --git a/src/main/resources/useless_mod.jade.mixins.json b/src/main/resources/useless_mod.jade.mixins.json new file mode 100644 index 00000000..fe1f2729 --- /dev/null +++ b/src/main/resources/useless_mod.jade.mixins.json @@ -0,0 +1,14 @@ +{ + "required": false, + "package": "com.sorrowmist.useless.mixin.jade", + "compatibilityLevel": "JAVA_17", + "mixins": [ + "JadeItemStorageProviderMixin", + "JadeItemStorageExtensionMixin" + ], + "injectors": { + "defaultRequire": 1 + }, + "refmap": "useless_mod.refmap.json", + "minVersion": "0.8" +} diff --git a/src/main/resources/useless_mod.malum.mixins.json b/src/main/resources/useless_mod.malum.mixins.json new file mode 100644 index 00000000..f36710c4 --- /dev/null +++ b/src/main/resources/useless_mod.malum.mixins.json @@ -0,0 +1,14 @@ +{ + "required": false, + "package": "com.sorrowmist.useless.mixin.malum", + "compatibilityLevel": "JAVA_17", + "mixins": [ + "SoulHarvestHandlerMixin", + "SoulHarvestDropMixin" + ], + "injectors": { + "defaultRequire": 1 + }, + "refmap": "useless_mod.refmap.json", + "minVersion": "0.8" +} diff --git a/src/main/resources/useless_mod.mekanism.mixins.json b/src/main/resources/useless_mod.mekanism.mixins.json new file mode 100644 index 00000000..af3bb27e --- /dev/null +++ b/src/main/resources/useless_mod.mekanism.mixins.json @@ -0,0 +1,25 @@ +{ + "required": false, + "package": "com.sorrowmist.useless.mixin.mek", + "compatibilityLevel": "JAVA_17", + "mixins": [ + "CachedRecipeMixin", + "DigitalMinerMixin", + "ElectricPumpMixin", + "FluidicPlenisherMixin", + "FormulaicAssemblicatorMixin", + "MekanismUtilsMixin", + "TileComponentEjectorMixin", + "TileEntityDigitalMinerMixin", + "UpgradeMixin", + "UpgradeUtilsMixin" + ], + "injectors": { + "defaultRequire": 1 + }, + "overwrites": { + "requireAnnotations": true + }, + "refmap": "useless_mod.refmap.json", + "minVersion": "0.8" +} diff --git a/src/main/resources/useless_mod.mixins.json b/src/main/resources/useless_mod.mixins.json index 860bdc11..3b78aa3a 100644 --- a/src/main/resources/useless_mod.mixins.json +++ b/src/main/resources/useless_mod.mixins.json @@ -1,43 +1,66 @@ { - "required": true, - "minVersion": "0.8", + "required": false, "package": "com.sorrowmist.useless.mixin", "compatibilityLevel": "JAVA_17", - "refmap": "useless_mod.refmap.json", + "mixins": [ + "BlockGrassWandDropMixin", + "EntityMixin", + "EntityGetterMixin", + "LivingEntityMixin", + "PlayerMixin", + "ServerLevelMixin", + "LevelMixin", + "ServerPlayerGameModeMixin", + "ServerGamePacketListenerImplFlightMixin", + "ae2.CraftingCpuLogicAccessor", + "ae2.CraftingCalculationSmartDoublingMixin", + "ae2.CraftingSimulationStateAccessor", + "ae2.CraftingSimulationStateSmartDoublingMixin", + "ae2.CraftingServiceSmartDoublingMixin", + "ae2.CraftingTreeProcessMixin", + "ae2.ExecutingCraftingJobAccessor", + "ae2.ElapsedTimeTrackerAccessor", + "ae2.CraftingCpuLogicMixin", + "ae2.ExecutingCraftingJobMixin", + "ae2.AEProcessingPatternMixin", + "ae2.PatternEncodingLogicMixin", + "ae2.PatternDetailsHelperOccultismMixin", + "ae2.PatternDetailsHelperSmartDoublingMixin", + "ae2.PathingCalculationMixin", + "ae2.PathingServiceMixin", + "ae2.GridNodeMixin", + "advancedae.AdvancedCraftingCpuAccessor", + "advancedae.CraftingCpuLogicMixin", + "advancedae.ExecutingCraftingJobMixin", + "botanypots.AbstractBotanyPotBlockEntityAccessor", + "botanypots.BotanyPotBlockEntityAccessor", + "botanypots.BotanyPotBlockEntityMixin", + "eae.PatternSlotTrackerMixin", + "eae.TileAssemblerMatrixPatternMixin", + "accessor.BeeAccessor", + "accessor.TurtleAccessor", + "enderio.EnderIOTravelHandlerMixin" + ], + "server": [ + "ServerGamePacketListenerImplMixin" + ], "client": [ - "AbstractContainerScreenMixin", - "GuiAssemblerMatrixPatternInfoMixin" + "ClientLevelMixin", + "ClientPacketListenerMixin", + "LocalPlayerMixin", + "MultiPlayerGameModeMixin", + "botanypots.BotanyPotRendererMixin", + "botanypots.PhasedDisplayStateRendererMixin", + "eae.GuiAssemblerMatrixPatternInfoMixin", + "emi.JemiAEKeyMixin", + "emi.JemiAEKeyRegistrationMixin" ], "injectors": { "defaultRequire": 1 }, - "plugin": "com.sorrowmist.useless.mixin.MixinConfigPlugin", - "mixins": [ - "AEBaseMenuMixin", - "BlockTravelAnchorMixin", - "LevelMixin", - "PatternAccessTermMenuMixin", - "PatternSlotTrackerMixin", - "TileAssemblerMatrixPatternMixin", - "TileComponentEjectorMixin", - "TileEntityDigitalMinerMixin", - "WrenchHookMixin", - "accessor.BlockEntityBotanyPotAccessor", - "accessor.DelayAccessor", - "botanypots.BlockEntityBotanyPotMixin", - "botanypots.BotanyPotsForgeClientMixin", - "mek.CachedRecipeMixin", - "mek.DigitalMinerMixin", - "mek.ElectricPumpMixin", - "mek.FluidicPlenisherMixin", - "mek.FormulaicAssemblicatorMixin", - "mek.MekanismUtilsMixin", - "mek.UpgradeMixin", - "mek.UpgradeUtilsMixin", - "thermal.AugmentableBlockEntityMixin", - "thermal.ItemStorageCoFHMixin", - "thermal.MachineBlockEntityMixin", - "thermal.Reconfigurable4WayBlockEntityMixin", - "thermal.ThermalAugmentRulesMixin" - ] -} \ No newline at end of file + "overwrites": { + "requireAnnotations": true + }, + "refmap": "useless_mod.refmap.json", + "minVersion": "0.8" +} diff --git a/src/main/resources/useless_mod.pneumaticcraft.mixins.json b/src/main/resources/useless_mod.pneumaticcraft.mixins.json new file mode 100644 index 00000000..e97ec26b --- /dev/null +++ b/src/main/resources/useless_mod.pneumaticcraft.mixins.json @@ -0,0 +1,16 @@ +{ + "required": false, + "package": "com.sorrowmist.useless.mixin.pneumaticcraft", + "compatibilityLevel": "JAVA_17", + "mixins": [ + "MachineAirHandlerMixin" + ], + "injectors": { + "defaultRequire": 1 + }, + "overwrites": { + "requireAnnotations": true + }, + "refmap": "useless_mod.refmap.json", + "minVersion": "0.8" +} diff --git a/src/main/resources/xia/assets/useless_mod/models/block/advanced_alloy_furnace.json b/src/main/resources/xia/assets/useless_mod/models/block/advanced_alloy_furnace.json new file mode 100644 index 00000000..732a3eb2 --- /dev/null +++ b/src/main/resources/xia/assets/useless_mod/models/block/advanced_alloy_furnace.json @@ -0,0 +1,389 @@ +{ + "format_version": "1.21.6", + "credit": "Made with blockbench,made by xiabi418", + "render_type": "translucent", + "texture_size": [64, 64], + "textures": { + "12": "useless_mod:block/advanced_alloy_furnace", + "particle": "useless_mod:block/advanced_alloy_furnace" + }, + "elements": [ + { + "from": [3, 0, 0], + "to": [13, 1, 1], + "faces": { + "north": {"uv": [0.75, 3.75, 3.25, 4], "texture": "#12"}, + "up": {"uv": [8, 1.25, 10.5, 1.5], "texture": "#12"}, + "down": {"uv": [1, 11.75, 3, 12], "texture": "#12"} + } + }, + { + "from": [13, 0, 0], + "to": [16, 1, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [3, 0, 0]}, + "faces": { + "north": {"uv": [0, 3.75, 0.75, 4], "texture": "#12"}, + "east": {"uv": [0, 7.75, 0.25, 8], "texture": "#12"}, + "down": {"uv": [0, 11.75, 1, 12], "texture": "#12"} + } + }, + { + "from": [0, 0, 0], + "to": [3, 1, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [-10, 0, 0]}, + "faces": { + "north": {"uv": [3.25, 3.75, 4, 4], "texture": "#12"}, + "west": {"uv": [0, 7.75, 0.25, 8], "texture": "#12"}, + "down": {"uv": [3, 11.75, 4, 12], "texture": "#12"} + } + }, + { + "from": [3, 14, 0], + "to": [13, 16, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [5, 14, 0]}, + "faces": { + "north": {"uv": [0.75, 0, 3.25, 0.5], "texture": "#12"}, + "up": {"uv": [4.75, 4, 7.25, 4.25], "texture": "#12"}, + "down": {"uv": [7, 10.25, 9.5, 10.5], "texture": "#12"} + } + }, + { + "from": [0, 1, 0], + "to": [3, 5, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 1, 0]}, + "faces": { + "north": {"uv": [3.25, 2.75, 4, 3.75], "texture": "#12"}, + "east": {"uv": [10.5, 0, 10.75, 1], "texture": "#12"}, + "west": {"uv": [0, 6.75, 0.25, 7.75], "texture": "#12"} + } + }, + { + "from": [13, 1, 0], + "to": [16, 5, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [14, 1, 0]}, + "faces": { + "north": {"uv": [0, 2.75, 0.75, 3.75], "texture": "#12"}, + "east": {"uv": [0, 6.75, 0.25, 7.75], "texture": "#12"}, + "west": {"uv": [10.5, 0, 10.75, 1], "texture": "#12"} + } + }, + { + "from": [13, 8, 0], + "to": [16, 14, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [14, 8, 0]}, + "faces": { + "north": {"uv": [0, 0.5, 0.75, 2], "texture": "#12"}, + "east": {"uv": [0, 4.5, 0.25, 6], "texture": "#12"}, + "west": {"uv": [9.5, 8.75, 9.75, 10.25], "texture": "#12"} + } + }, + { + "from": [0, 8, 0], + "to": [3, 14, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [1, 8, 0]}, + "faces": { + "north": {"uv": [3.25, 0.5, 4, 2], "texture": "#12"}, + "east": {"uv": [9.5, 8.75, 9.75, 10.25], "texture": "#12"}, + "west": {"uv": [0, 4.5, 0.25, 6], "texture": "#12"} + } + }, + { + "from": [3, 5, 0], + "to": [13, 8, 1], + "faces": { + "north": {"uv": [0.75, 2, 3.25, 2.75], "texture": "#12"}, + "up": {"uv": [7, 10.5, 9.5, 10.75], "texture": "#12"}, + "down": {"uv": [8, 1.75, 10.5, 1.5], "texture": "#12"} + } + }, + { + "from": [0, 5, 0], + "to": [3, 8, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 5, 0]}, + "faces": { + "north": {"uv": [3.25, 2, 4, 2.75], "texture": "#12"}, + "west": {"uv": [0, 6, 0.25, 6.75], "texture": "#12"} + } + }, + { + "from": [13, 5, 0], + "to": [16, 8, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [13, 5, 0]}, + "faces": { + "north": {"uv": [0, 2, 0.75, 2.75], "texture": "#12"}, + "east": {"uv": [0, 6, 0.25, 6.75], "texture": "#12"} + } + }, + { + "from": [0, 14, 0], + "to": [3, 16, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 14, 0]}, + "faces": { + "north": {"uv": [0, 0, 0.75, 0.5], "texture": "#12"}, + "west": {"uv": [0, 4, 0.25, 4.5], "texture": "#12"}, + "up": {"uv": [7.25, 4, 8, 4.25], "texture": "#12"} + } + }, + { + "from": [13, 14, 0], + "to": [16, 16, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [13, 14, 0]}, + "faces": { + "north": {"uv": [0, 0, 0.75, 0.5], "texture": "#12"}, + "east": {"uv": [0, 4, 0.25, 4.5], "texture": "#12"}, + "up": {"uv": [4, 4, 4.75, 4.25], "texture": "#12"} + } + }, + { + "from": [3, 1, 1], + "to": [13, 5, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [3, 1, 1]}, + "faces": { + "north": {"uv": [8, 0, 10.5, 1], "texture": "#12"} + } + }, + { + "from": [3, 8, 1], + "to": [13, 14, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [3, 8, 1]}, + "faces": { + "north": {"uv": [7, 8.75, 9.5, 10.25], "texture": "#12"} + } + }, + { + "from": [12, 0, 1], + "to": [16, 9, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [12, 0, 1]}, + "faces": { + "east": {"uv": [4, 5.75, 0.25, 8], "texture": "#12"}, + "south": {"uv": [7, 1.75, 8, 4], "texture": "#12"}, + "down": {"uv": [3, 8, 4, 11.75], "texture": "#12"} + } + }, + { + "from": [0, 0, 1], + "to": [4, 9, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 0, 1]}, + "faces": { + "south": {"uv": [4, 1.75, 5, 4], "texture": "#12"}, + "west": {"uv": [0.25, 5.75, 4, 8], "texture": "#12"}, + "down": {"uv": [0, 8, 1, 11.75], "texture": "#12"} + } + }, + { + "from": [0, 9, 1], + "to": [3, 16, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 9, 1]}, + "faces": { + "east": {"uv": [16, 3.75, 12.25, 5.75], "texture": "#12"}, + "south": {"uv": [4, 0, 4.75, 1.75], "texture": "#12"}, + "west": {"uv": [0.25, 4, 4, 5.75], "texture": "#12"}, + "up": {"uv": [4, 4.25, 4.75, 8], "texture": "#12"} + } + }, + { + "from": [13, 9, 1], + "to": [16, 16, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [13, 9, 1]}, + "faces": { + "east": {"uv": [4, 4, 0.25, 5.75], "texture": "#12"}, + "south": {"uv": [7.25, 0, 8, 1.75], "texture": "#12"}, + "west": {"uv": [12.25, 3.75, 16, 5.75], "texture": "#12"}, + "up": {"uv": [7.25, 4.25, 8, 8], "texture": "#12"} + } + }, + { + "from": [4, 0, 1], + "to": [12, 4, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [4, 0, 1]}, + "faces": { + "south": {"uv": [5, 3, 7, 4], "texture": "#12"}, + "down": {"uv": [1, 8, 3, 11.75], "texture": "#12"} + } + }, + { + "from": [3, 9.05, 13.05], + "to": [13, 16.05, 16.05], + "rotation": {"angle": 0, "axis": "y", "origin": [3, 14.05, 13.05]}, + "faces": { + "south": {"uv": [4, 8.75, 6.5, 10.5], "texture": "#12"}, + "up": {"uv": [4, 8, 6.5, 8.75], "texture": "#12"} + } + }, + { + "from": [3, 12, 1], + "to": [13, 16, 13], + "rotation": {"angle": 0, "axis": "y", "origin": [3, 14, 1]}, + "faces": { + "south": {"uv": [8.75, 3.75, 11.25, 4.75], "texture": "#12"}, + "up": {"uv": [4.75, 4.25, 7.25, 7.25], "texture": "#12"} + } + }, + { + "from": [4, 4, 12], + "to": [12, 12, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [5, 9, 14]}, + "faces": { + "east": {"uv": [8, 1.75, 9, 3.75], "texture": "#12"}, + "south": {"uv": [5, 1, 7, 3], "texture": "#12"}, + "west": {"uv": [8, 1.75, 9, 3.75], "texture": "#12"}, + "up": {"uv": [8, 2.75, 10, 3.75], "texture": "#12"} + } + }, + { + "from": [5, 7.9, 11.875], + "to": [6, 15.9, 15.875], + "rotation": {"angle": 0, "axis": "y", "origin": [5, 7.9, 10.875]}, + "faces": { + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} + } + }, + { + "from": [7, 7.9, 11.875], + "to": [8, 15.9, 15.875], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 7.9, 10.875]}, + "faces": { + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} + } + }, + { + "from": [10, 7.9, 11.825], + "to": [11, 15.9, 15.825], + "rotation": {"angle": 0, "axis": "y", "origin": [9, 7.9, 10.825]}, + "faces": { + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} + } + }, + { + "from": [12, 7.9, 11.9], + "to": [13, 15.9, 15.9], + "rotation": {"angle": 0, "axis": "y", "origin": [11, 7.9, 10.9]}, + "faces": { + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} + } + }, + { + "from": [3, 7.9, 11.9], + "to": [4, 15.9, 15.9], + "rotation": {"angle": 0, "axis": "y", "origin": [2, 7.9, 10.9]}, + "faces": { + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} + } + }, + { + "from": [8, 7.9, 11.825], + "to": [9, 15.9, 15.825], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 7.9, 10.825]}, + "faces": { + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8.5, 6.75, 8.75, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.5, 6.75, 9.75, 7.75], "texture": "#12"} + } + } + ], + "display": { + "thirdperson_righthand": { + "rotation": [75, 45, 0], + "translation": [0, 2.5, 0], + "scale": [0.375, 0.375, 0.375] + }, + "thirdperson_lefthand": { + "rotation": [75, 45, 0], + "translation": [0, 2.5, 0], + "scale": [0.375, 0.375, 0.375] + }, + "firstperson_righthand": { + "rotation": [0, 45, 0], + "scale": [0.4, 0.4, 0.4] + }, + "firstperson_lefthand": { + "rotation": [0, -135, 0], + "scale": [0.4, 0.4, 0.4] + }, + "ground": { + "translation": [0, 3, 0], + "scale": [0.25, 0.25, 0.25] + }, + "gui": { + "rotation": [30, -135, 0], + "scale": [0.625, 0.625, 0.625] + }, + "head": { + "translation": [0, 15, 0] + }, + "fixed": { + "scale": [0.5, 0.5, 0.5] + } + }, + "groups": [ + { + "name": "allcube", + "origin": [0, 0, 0], + "color": 0, + "children": [ + { + "name": "baseBlock", + "origin": [0, 0, 0], + "color": 0, + "children": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + { + "name": "back", + "origin": [7, 0.025, -2], + "color": 0, + "children": [20] + }, + 21 + ] + }, + { + "name": "connect", + "origin": [5, 10, 14], + "color": 0, + "children": [22] + }, + { + "name": "copper_past", + "origin": [3, 9, -2], + "color": 0, + "children": [23, 24, 25, 26, 27, 28] + } + ] + } + ] +} \ No newline at end of file diff --git a/src/main/resources/xia/assets/useless_mod/models/block/advanced_alloy_furnace_active.json b/src/main/resources/xia/assets/useless_mod/models/block/advanced_alloy_furnace_active.json new file mode 100644 index 00000000..de2242e2 --- /dev/null +++ b/src/main/resources/xia/assets/useless_mod/models/block/advanced_alloy_furnace_active.json @@ -0,0 +1,390 @@ +{ + "format_version": "1.21.6", + "credit": "Made with blockbench,made by xiabi418", + "render_type": "translucent", + "texture_size": [64, 64], + "textures": { + "1": "useless_mod:block/advanced_alloy_furnace_active", + "12": "useless_mod:block/advanced_alloy_furnace", + "particle": "useless_mod:block/advanced_alloy_furnace" + }, + "elements": [ + { + "from": [3, 0, 0], + "to": [13, 1, 1], + "faces": { + "north": {"uv": [0.75, 3.75, 3.25, 4], "texture": "#12"}, + "up": {"uv": [8, 1.25, 10.5, 1.5], "texture": "#12"}, + "down": {"uv": [1, 11.75, 3, 12], "texture": "#12"} + } + }, + { + "from": [13, 0, 0], + "to": [16, 1, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [3, 0, 0]}, + "faces": { + "north": {"uv": [0, 3.75, 0.75, 4], "texture": "#12"}, + "east": {"uv": [0, 7.75, 0.25, 8], "texture": "#12"}, + "down": {"uv": [0, 11.75, 1, 12], "texture": "#12"} + } + }, + { + "from": [0, 0, 0], + "to": [3, 1, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [-10, 0, 0]}, + "faces": { + "north": {"uv": [3.25, 3.75, 4, 4], "texture": "#12"}, + "west": {"uv": [0, 7.75, 0.25, 8], "texture": "#12"}, + "down": {"uv": [3, 11.75, 4, 12], "texture": "#12"} + } + }, + { + "from": [3, 14, 0], + "to": [13, 16, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [5, 14, 0]}, + "faces": { + "north": {"uv": [0.75, 0, 3.25, 0.5], "texture": "#12"}, + "up": {"uv": [4.75, 4, 7.25, 4.25], "texture": "#12"}, + "down": {"uv": [7, 10.25, 9.5, 10.5], "texture": "#12"} + } + }, + { + "from": [0, 1, 0], + "to": [3, 5, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 1, 0]}, + "faces": { + "north": {"uv": [3.25, 2.75, 4, 3.75], "texture": "#12"}, + "east": {"uv": [10.5, 0, 10.75, 1], "texture": "#12"}, + "west": {"uv": [0, 6.75, 0.25, 7.75], "texture": "#12"} + } + }, + { + "from": [13, 1, 0], + "to": [16, 5, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [14, 1, 0]}, + "faces": { + "north": {"uv": [0, 2.75, 0.75, 3.75], "texture": "#12"}, + "east": {"uv": [0, 6.75, 0.25, 7.75], "texture": "#12"}, + "west": {"uv": [10.5, 0, 10.75, 1], "texture": "#12"} + } + }, + { + "from": [13, 8, 0], + "to": [16, 14, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [14, 8, 0]}, + "faces": { + "north": {"uv": [0, 0.5, 0.75, 2], "texture": "#12"}, + "east": {"uv": [0, 4.5, 0.25, 6], "texture": "#12"}, + "west": {"uv": [9.5, 8.75, 9.75, 10.25], "texture": "#12"} + } + }, + { + "from": [0, 8, 0], + "to": [3, 14, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [1, 8, 0]}, + "faces": { + "north": {"uv": [3.25, 0.5, 4, 2], "texture": "#12"}, + "east": {"uv": [9.5, 8.75, 9.75, 10.25], "texture": "#12"}, + "west": {"uv": [0, 4.5, 0.25, 6], "texture": "#12"} + } + }, + { + "from": [3, 5, 0], + "to": [13, 8, 1], + "faces": { + "north": {"uv": [0.75, 2, 3.25, 2.75], "texture": "#12"}, + "up": {"uv": [7, 10.5, 9.5, 10.75], "texture": "#12"}, + "down": {"uv": [8, 1.75, 10.5, 1.5], "texture": "#12"} + } + }, + { + "from": [0, 5, 0], + "to": [3, 8, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 5, 0]}, + "faces": { + "north": {"uv": [3.25, 2, 4, 2.75], "texture": "#12"}, + "west": {"uv": [0, 6, 0.25, 6.75], "texture": "#12"} + } + }, + { + "from": [13, 5, 0], + "to": [16, 8, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [13, 5, 0]}, + "faces": { + "north": {"uv": [0, 2, 0.75, 2.75], "texture": "#12"}, + "east": {"uv": [0, 6, 0.25, 6.75], "texture": "#12"} + } + }, + { + "from": [0, 14, 0], + "to": [3, 16, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 14, 0]}, + "faces": { + "north": {"uv": [0, 0, 0.75, 0.5], "texture": "#12"}, + "west": {"uv": [0, 4, 0.25, 4.5], "texture": "#12"}, + "up": {"uv": [7.25, 4, 8, 4.25], "texture": "#12"} + } + }, + { + "from": [13, 14, 0], + "to": [16, 16, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [13, 14, 0]}, + "faces": { + "north": {"uv": [0, 0, 0.75, 0.5], "texture": "#12"}, + "east": {"uv": [0, 4, 0.25, 4.5], "texture": "#12"}, + "up": {"uv": [4, 4, 4.75, 4.25], "texture": "#12"} + } + }, + { + "from": [3, 1, 1], + "to": [13, 5, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [3, 1, 1]}, + "faces": { + "north": {"uv": [8, 0, 10.5, 1], "texture": "#1"} + } + }, + { + "from": [3, 8, 1], + "to": [13, 14, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [3, 8, 1]}, + "faces": { + "north": {"uv": [7, 8.75, 9.5, 10.25], "texture": "#1"} + } + }, + { + "from": [12, 0, 1], + "to": [16, 9, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [12, 0, 1]}, + "faces": { + "east": {"uv": [4, 5.75, 0.25, 8], "texture": "#12"}, + "south": {"uv": [7, 1.75, 8, 4], "texture": "#12"}, + "down": {"uv": [3, 8, 4, 11.75], "texture": "#12"} + } + }, + { + "from": [0, 0, 1], + "to": [4, 9, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 0, 1]}, + "faces": { + "south": {"uv": [4, 1.75, 5, 4], "texture": "#12"}, + "west": {"uv": [0.25, 5.75, 4, 8], "texture": "#12"}, + "down": {"uv": [0, 8, 1, 11.75], "texture": "#12"} + } + }, + { + "from": [0, 9, 1], + "to": [3, 16, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 9, 1]}, + "faces": { + "east": {"uv": [16, 3.75, 12.25, 5.75], "texture": "#12"}, + "south": {"uv": [4, 0, 4.75, 1.75], "texture": "#12"}, + "west": {"uv": [0.25, 4, 4, 5.75], "texture": "#12"}, + "up": {"uv": [4, 4.25, 4.75, 8], "texture": "#12"} + } + }, + { + "from": [13, 9, 1], + "to": [16, 16, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [13, 9, 1]}, + "faces": { + "east": {"uv": [4, 4, 0.25, 5.75], "texture": "#12"}, + "south": {"uv": [7.25, 0, 8, 1.75], "texture": "#12"}, + "west": {"uv": [12.25, 3.75, 16, 5.75], "texture": "#12"}, + "up": {"uv": [7.25, 4.25, 8, 8], "texture": "#12"} + } + }, + { + "from": [4, 0, 1], + "to": [12, 4, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [4, 0, 1]}, + "faces": { + "south": {"uv": [5, 3, 7, 4], "texture": "#12"}, + "down": {"uv": [1, 8, 3, 11.75], "texture": "#12"} + } + }, + { + "from": [3, 9.05, 13.05], + "to": [13, 16.05, 16.05], + "rotation": {"angle": 0, "axis": "y", "origin": [3, 14.05, 13.05]}, + "faces": { + "south": {"uv": [4, 8.75, 6.5, 10.5], "texture": "#12"}, + "up": {"uv": [4, 8, 6.5, 8.75], "texture": "#12"} + } + }, + { + "from": [3, 12, 1], + "to": [13, 16, 13], + "rotation": {"angle": 0, "axis": "y", "origin": [3, 14, 1]}, + "faces": { + "south": {"uv": [8.75, 3.75, 11.25, 4.75], "texture": "#12"}, + "up": {"uv": [4.75, 4.25, 7.25, 7.25], "texture": "#12"} + } + }, + { + "from": [4, 4, 12], + "to": [12, 12, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [5, 9, 14]}, + "faces": { + "east": {"uv": [8, 1.75, 9, 3.75], "texture": "#12"}, + "south": {"uv": [5, 1, 7, 3], "texture": "#12"}, + "west": {"uv": [8, 1.75, 9, 3.75], "texture": "#12"}, + "up": {"uv": [8, 2.75, 10, 3.75], "texture": "#12"} + } + }, + { + "from": [5, 7.9, 11.875], + "to": [6, 15.9, 15.875], + "rotation": {"angle": 0, "axis": "y", "origin": [5, 7.9, 10.875]}, + "faces": { + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} + } + }, + { + "from": [7, 7.9, 11.875], + "to": [8, 15.9, 15.875], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 7.9, 10.875]}, + "faces": { + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} + } + }, + { + "from": [10, 7.9, 11.825], + "to": [11, 15.9, 15.825], + "rotation": {"angle": 0, "axis": "y", "origin": [9, 7.9, 10.825]}, + "faces": { + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} + } + }, + { + "from": [12, 7.9, 11.9], + "to": [13, 15.9, 15.9], + "rotation": {"angle": 0, "axis": "y", "origin": [11, 7.9, 10.9]}, + "faces": { + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} + } + }, + { + "from": [3, 7.9, 11.9], + "to": [4, 15.9, 15.9], + "rotation": {"angle": 0, "axis": "y", "origin": [2, 7.9, 10.9]}, + "faces": { + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} + } + }, + { + "from": [8, 7.9, 11.825], + "to": [9, 15.9, 15.825], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 7.9, 10.825]}, + "faces": { + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8.5, 6.75, 8.75, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.5, 6.75, 9.75, 7.75], "texture": "#12"} + } + } + ], + "display": { + "thirdperson_righthand": { + "rotation": [75, 45, 0], + "translation": [0, 2.5, 0], + "scale": [0.375, 0.375, 0.375] + }, + "thirdperson_lefthand": { + "rotation": [75, 45, 0], + "translation": [0, 2.5, 0], + "scale": [0.375, 0.375, 0.375] + }, + "firstperson_righthand": { + "rotation": [0, 45, 0], + "scale": [0.4, 0.4, 0.4] + }, + "firstperson_lefthand": { + "rotation": [0, -135, 0], + "scale": [0.4, 0.4, 0.4] + }, + "ground": { + "translation": [0, 3, 0], + "scale": [0.25, 0.25, 0.25] + }, + "gui": { + "rotation": [30, -135, 0], + "scale": [0.625, 0.625, 0.625] + }, + "head": { + "translation": [0, 15, 0] + }, + "fixed": { + "scale": [0.5, 0.5, 0.5] + } + }, + "groups": [ + { + "name": "allcube", + "origin": [0, 0, 0], + "color": 0, + "children": [ + { + "name": "baseBlock", + "origin": [0, 0, 0], + "color": 0, + "children": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + { + "name": "back", + "origin": [7, 0.025, -2], + "color": 0, + "children": [20] + }, + 21 + ] + }, + { + "name": "connect", + "origin": [5, 10, 14], + "color": 0, + "children": [22] + }, + { + "name": "copper_past", + "origin": [3, 9, -2], + "color": 0, + "children": [23, 24, 25, 26, 27, 28] + } + ] + } + ] +} \ No newline at end of file diff --git a/src/main/resources/xia/assets/useless_mod/models/block/advanced_alloy_furnace_locked.json b/src/main/resources/xia/assets/useless_mod/models/block/advanced_alloy_furnace_locked.json new file mode 100644 index 00000000..758e5c33 --- /dev/null +++ b/src/main/resources/xia/assets/useless_mod/models/block/advanced_alloy_furnace_locked.json @@ -0,0 +1,390 @@ +{ + "format_version": "1.21.6", + "credit": "Made with blockbench,made by xiabi418", + "render_type": "translucent", + "texture_size": [64, 64], + "textures": { + "1": "useless_mod:block/advanced_alloy_furnace_locked", + "12": "useless_mod:block/advanced_alloy_furnace_locked", + "particle": "useless_mod:block/advanced_alloy_furnace" + }, + "elements": [ + { + "from": [3, 0, 0], + "to": [13, 1, 1], + "faces": { + "north": {"uv": [0.75, 3.75, 3.25, 4], "texture": "#12"}, + "up": {"uv": [8, 1.25, 10.5, 1.5], "texture": "#12"}, + "down": {"uv": [1, 11.75, 3, 12], "texture": "#12"} + } + }, + { + "from": [13, 0, 0], + "to": [16, 1, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [3, 0, 0]}, + "faces": { + "north": {"uv": [0, 3.75, 0.75, 4], "texture": "#12"}, + "east": {"uv": [0, 7.75, 0.25, 8], "texture": "#12"}, + "down": {"uv": [0, 11.75, 1, 12], "texture": "#12"} + } + }, + { + "from": [0, 0, 0], + "to": [3, 1, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [-10, 0, 0]}, + "faces": { + "north": {"uv": [3.25, 3.75, 4, 4], "texture": "#12"}, + "west": {"uv": [0, 7.75, 0.25, 8], "texture": "#12"}, + "down": {"uv": [3, 11.75, 4, 12], "texture": "#12"} + } + }, + { + "from": [3, 14, 0], + "to": [13, 16, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [5, 14, 0]}, + "faces": { + "north": {"uv": [0.75, 0, 3.25, 0.5], "texture": "#12"}, + "up": {"uv": [4.75, 4, 7.25, 4.25], "texture": "#12"}, + "down": {"uv": [7, 10.25, 9.5, 10.5], "texture": "#12"} + } + }, + { + "from": [0, 1, 0], + "to": [3, 5, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 1, 0]}, + "faces": { + "north": {"uv": [3.25, 2.75, 4, 3.75], "texture": "#12"}, + "east": {"uv": [10.5, 0, 10.75, 1], "texture": "#12"}, + "west": {"uv": [0, 6.75, 0.25, 7.75], "texture": "#12"} + } + }, + { + "from": [13, 1, 0], + "to": [16, 5, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [14, 1, 0]}, + "faces": { + "north": {"uv": [0, 2.75, 0.75, 3.75], "texture": "#12"}, + "east": {"uv": [0, 6.75, 0.25, 7.75], "texture": "#12"}, + "west": {"uv": [10.5, 0, 10.75, 1], "texture": "#12"} + } + }, + { + "from": [13, 8, 0], + "to": [16, 14, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [14, 8, 0]}, + "faces": { + "north": {"uv": [0, 0.5, 0.75, 2], "texture": "#12"}, + "east": {"uv": [0, 4.5, 0.25, 6], "texture": "#12"}, + "west": {"uv": [9.5, 8.75, 9.75, 10.25], "texture": "#12"} + } + }, + { + "from": [0, 8, 0], + "to": [3, 14, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [1, 8, 0]}, + "faces": { + "north": {"uv": [3.25, 0.5, 4, 2], "texture": "#12"}, + "east": {"uv": [9.5, 8.75, 9.75, 10.25], "texture": "#12"}, + "west": {"uv": [0, 4.5, 0.25, 6], "texture": "#12"} + } + }, + { + "from": [3, 5, 0], + "to": [13, 8, 1], + "faces": { + "north": {"uv": [0.75, 2, 3.25, 2.75], "texture": "#12"}, + "up": {"uv": [7, 10.5, 9.5, 10.75], "texture": "#12"}, + "down": {"uv": [8, 1.75, 10.5, 1.5], "texture": "#12"} + } + }, + { + "from": [0, 5, 0], + "to": [3, 8, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 5, 0]}, + "faces": { + "north": {"uv": [3.25, 2, 4, 2.75], "texture": "#12"}, + "west": {"uv": [0, 6, 0.25, 6.75], "texture": "#12"} + } + }, + { + "from": [13, 5, 0], + "to": [16, 8, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [13, 5, 0]}, + "faces": { + "north": {"uv": [0, 2, 0.75, 2.75], "texture": "#12"}, + "east": {"uv": [0, 6, 0.25, 6.75], "texture": "#12"} + } + }, + { + "from": [0, 14, 0], + "to": [3, 16, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 14, 0]}, + "faces": { + "north": {"uv": [0, 0, 0.75, 0.5], "texture": "#12"}, + "west": {"uv": [0, 4, 0.25, 4.5], "texture": "#12"}, + "up": {"uv": [7.25, 4, 8, 4.25], "texture": "#12"} + } + }, + { + "from": [13, 14, 0], + "to": [16, 16, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [13, 14, 0]}, + "faces": { + "north": {"uv": [0, 0, 0.75, 0.5], "texture": "#12"}, + "east": {"uv": [0, 4, 0.25, 4.5], "texture": "#12"}, + "up": {"uv": [4, 4, 4.75, 4.25], "texture": "#12"} + } + }, + { + "from": [3, 1, 1], + "to": [13, 5, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [3, 1, 1]}, + "faces": { + "north": {"uv": [8, 0, 10.5, 1], "texture": "#1"} + } + }, + { + "from": [3, 8, 1], + "to": [13, 14, 1], + "rotation": {"angle": 0, "axis": "y", "origin": [3, 8, 1]}, + "faces": { + "north": {"uv": [7, 8.75, 9.5, 10.25], "texture": "#1"} + } + }, + { + "from": [12, 0, 1], + "to": [16, 9, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [12, 0, 1]}, + "faces": { + "east": {"uv": [4, 5.75, 0.25, 8], "texture": "#12"}, + "south": {"uv": [7, 1.75, 8, 4], "texture": "#12"}, + "down": {"uv": [3, 8, 4, 11.75], "texture": "#12"} + } + }, + { + "from": [0, 0, 1], + "to": [4, 9, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 0, 1]}, + "faces": { + "south": {"uv": [4, 1.75, 5, 4], "texture": "#12"}, + "west": {"uv": [0.25, 5.75, 4, 8], "texture": "#12"}, + "down": {"uv": [0, 8, 1, 11.75], "texture": "#12"} + } + }, + { + "from": [0, 9, 1], + "to": [3, 16, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 9, 1]}, + "faces": { + "east": {"uv": [16, 3.75, 12.25, 5.75], "texture": "#12"}, + "south": {"uv": [4, 0, 4.75, 1.75], "texture": "#12"}, + "west": {"uv": [0.25, 4, 4, 5.75], "texture": "#12"}, + "up": {"uv": [4, 4.25, 4.75, 8], "texture": "#12"} + } + }, + { + "from": [13, 9, 1], + "to": [16, 16, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [13, 9, 1]}, + "faces": { + "east": {"uv": [4, 4, 0.25, 5.75], "texture": "#12"}, + "south": {"uv": [7.25, 0, 8, 1.75], "texture": "#12"}, + "west": {"uv": [12.25, 3.75, 16, 5.75], "texture": "#12"}, + "up": {"uv": [7.25, 4.25, 8, 8], "texture": "#12"} + } + }, + { + "from": [4, 0, 1], + "to": [12, 4, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [4, 0, 1]}, + "faces": { + "south": {"uv": [5, 3, 7, 4], "texture": "#12"}, + "down": {"uv": [1, 8, 3, 11.75], "texture": "#12"} + } + }, + { + "from": [3, 9.05, 13.05], + "to": [13, 16.05, 16.05], + "rotation": {"angle": 0, "axis": "y", "origin": [3, 14.05, 13.05]}, + "faces": { + "south": {"uv": [4, 8.75, 6.5, 10.5], "texture": "#12"}, + "up": {"uv": [4, 8, 6.5, 8.75], "texture": "#12"} + } + }, + { + "from": [3, 12, 1], + "to": [13, 16, 13], + "rotation": {"angle": 0, "axis": "y", "origin": [3, 14, 1]}, + "faces": { + "south": {"uv": [8.75, 3.75, 11.25, 4.75], "texture": "#12"}, + "up": {"uv": [4.75, 4.25, 7.25, 7.25], "texture": "#12"} + } + }, + { + "from": [4, 4, 12], + "to": [12, 12, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [5, 9, 14]}, + "faces": { + "east": {"uv": [8, 1.75, 9, 3.75], "texture": "#12"}, + "south": {"uv": [5, 1, 7, 3], "texture": "#12"}, + "west": {"uv": [8, 1.75, 9, 3.75], "texture": "#12"}, + "up": {"uv": [8, 2.75, 10, 3.75], "texture": "#12"} + } + }, + { + "from": [5, 7.9, 11.875], + "to": [6, 15.9, 15.875], + "rotation": {"angle": 0, "axis": "y", "origin": [5, 7.9, 10.875]}, + "faces": { + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} + } + }, + { + "from": [7, 7.9, 11.875], + "to": [8, 15.9, 15.875], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 7.9, 10.875]}, + "faces": { + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} + } + }, + { + "from": [10, 7.9, 11.825], + "to": [11, 15.9, 15.825], + "rotation": {"angle": 0, "axis": "y", "origin": [9, 7.9, 10.825]}, + "faces": { + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} + } + }, + { + "from": [12, 7.9, 11.9], + "to": [13, 15.9, 15.9], + "rotation": {"angle": 0, "axis": "y", "origin": [11, 7.9, 10.9]}, + "faces": { + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} + } + }, + { + "from": [3, 7.9, 11.9], + "to": [4, 15.9, 15.9], + "rotation": {"angle": 0, "axis": "y", "origin": [2, 7.9, 10.9]}, + "faces": { + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8, 6.75, 8.25, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.25, 6.75, 9.5, 7.75], "texture": "#12"} + } + }, + { + "from": [8, 7.9, 11.825], + "to": [9, 15.9, 15.825], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 7.9, 10.825]}, + "faces": { + "east": {"uv": [9.25, 6.75, 8.25, 8.75], "texture": "#12"}, + "south": {"uv": [8.5, 6.75, 8.75, 8.75], "texture": "#12"}, + "west": {"uv": [8.25, 6.75, 9.25, 8.75], "texture": "#12"}, + "up": {"uv": [9.5, 6.75, 9.75, 7.75], "texture": "#12"} + } + } + ], + "display": { + "thirdperson_righthand": { + "rotation": [75, 45, 0], + "translation": [0, 2.5, 0], + "scale": [0.375, 0.375, 0.375] + }, + "thirdperson_lefthand": { + "rotation": [75, 45, 0], + "translation": [0, 2.5, 0], + "scale": [0.375, 0.375, 0.375] + }, + "firstperson_righthand": { + "rotation": [0, 45, 0], + "scale": [0.4, 0.4, 0.4] + }, + "firstperson_lefthand": { + "rotation": [0, -135, 0], + "scale": [0.4, 0.4, 0.4] + }, + "ground": { + "translation": [0, 3, 0], + "scale": [0.25, 0.25, 0.25] + }, + "gui": { + "rotation": [30, -135, 0], + "scale": [0.625, 0.625, 0.625] + }, + "head": { + "translation": [0, 15, 0] + }, + "fixed": { + "scale": [0.5, 0.5, 0.5] + } + }, + "groups": [ + { + "name": "allcube", + "origin": [0, 0, 0], + "color": 0, + "children": [ + { + "name": "baseBlock", + "origin": [0, 0, 0], + "color": 0, + "children": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + { + "name": "back", + "origin": [7, 0.025, -2], + "color": 0, + "children": [20] + }, + 21 + ] + }, + { + "name": "connect", + "origin": [5, 10, 14], + "color": 0, + "children": [22] + }, + { + "name": "copper_past", + "origin": [3, 9, -2], + "color": 0, + "children": [23, 24, 25, 26, 27, 28] + } + ] + } + ] +} \ No newline at end of file diff --git a/src/main/resources/xia/assets/useless_mod/models/block/supervisor.json b/src/main/resources/xia/assets/useless_mod/models/block/supervisor.json new file mode 100644 index 00000000..09a16f58 --- /dev/null +++ b/src/main/resources/xia/assets/useless_mod/models/block/supervisor.json @@ -0,0 +1,2316 @@ +{ + "format_version": "1.21.11", + "credit": "Made By xiabi with Blockbench", + "loader": "useless_mod:supervisor", + "render_type": "cutout", + "gui_light": "front", + "textures": { + "2": "useless_mod:block/iava_user", + "3": "useless_mod:block/pluto", + "particle": "useless_mod:block/dream_master_xxl" + }, + "display": { + "thirdperson_righthand": { + "rotation": [ + 75, + 45, + 0 + ], + "translation": [ + 0, + 2.5, + 0 + ], + "scale": [ + 0.375, + 0.375, + 0.375 + ] + }, + "thirdperson_lefthand": { + "rotation": [ + 75, + 45, + 0 + ], + "translation": [ + 0, + 2.5, + 0 + ], + "scale": [ + 0.375, + 0.375, + 0.375 + ] + }, + "firstperson_righthand": { + "rotation": [ + 0, + 45, + 0 + ], + "translation": [ + 0, + 4.25, + 0 + ], + "scale": [ + 0.4, + 0.4, + 0.4 + ] + }, + "firstperson_lefthand": { + "rotation": [ + 0, + -135, + 0 + ], + "translation": [ + 0, + 3.75, + 0 + ], + "scale": [ + 0.4, + 0.4, + 0.4 + ] + }, + "ground": { + "translation": [ + 0, + 3, + 0 + ], + "scale": [ + 0.25, + 0.25, + 0.25 + ] + }, + "gui": { + "rotation": [ + 30, + -135, + 0 + ], + "scale": [ + 0.625, + 0.625, + 0.625 + ] + }, + "head": { + "translation": [ + 0, + 14.75, + 0 + ] + }, + "fixed": { + "translation": [ + 0, + -1, + -1.75 + ] + }, + "on_shelf": { + "rotation": [ + 180, + 0, + 180 + ] + } + }, + "supervisor_model": { + "format_version": "1.21.11", + "credit": "Made By xiabi with Blockbench", + "texture_size": [ + 64, + 64 + ], + "textures": { + "2": "iava_user", + "3": "pluto", + "particle": "iava_user" + }, + "elements": [ + { + "name": "Head", + "from": [ + 6, + 13.25, + 7 + ], + "to": [ + 10, + 17.25, + 11 + ], + "faces": { + "north": { + "uv": [ + 2, + 2, + 4, + 4 + ], + "texture": "#3" + }, + "east": { + "uv": [ + 0, + 2, + 2, + 4 + ], + "texture": "#3" + }, + "south": { + "uv": [ + 6, + 2, + 8, + 4 + ], + "texture": "#3" + }, + "west": { + "uv": [ + 4, + 2, + 6, + 4 + ], + "texture": "#3" + }, + "up": { + "uv": [ + 4, + 2, + 2, + 0 + ], + "texture": "#3" + }, + "down": { + "uv": [ + 6, + 0, + 4, + 2 + ], + "texture": "#3" + } + } + }, + { + "name": "Hat Layer", + "from": [ + 5.75, + 13, + 6.75 + ], + "to": [ + 10.25, + 17.5, + 11.25 + ], + "faces": { + "north": { + "uv": [ + 10, + 2, + 12, + 4 + ], + "texture": "#3" + }, + "east": { + "uv": [ + 8, + 2, + 10, + 4 + ], + "texture": "#3" + }, + "south": { + "uv": [ + 14, + 2, + 16, + 4 + ], + "texture": "#3" + }, + "west": { + "uv": [ + 12, + 2, + 14, + 4 + ], + "texture": "#3" + }, + "up": { + "uv": [ + 12, + 2, + 10, + 0 + ], + "texture": "#3" + }, + "down": { + "uv": [ + 14, + 0, + 12, + 2 + ], + "texture": "#3" + } + } + }, + { + "name": "Body", + "from": [ + 6, + 7.25, + 8 + ], + "to": [ + 10, + 13.25, + 10 + ], + "rotation": { + "angle": 0, + "axis": "y", + "origin": [ + 10, + -4.75, + 10 + ] + }, + "faces": { + "north": { + "uv": [ + 5, + 5, + 7, + 8 + ], + "texture": "#3" + }, + "east": { + "uv": [ + 4, + 5, + 5, + 8 + ], + "texture": "#3" + }, + "south": { + "uv": [ + 8, + 5, + 10, + 8 + ], + "texture": "#3" + }, + "west": { + "uv": [ + 7, + 5, + 8, + 8 + ], + "texture": "#3" + }, + "up": { + "uv": [ + 7, + 5, + 5, + 4 + ], + "texture": "#3" + }, + "down": { + "uv": [ + 9, + 4, + 7, + 5 + ], + "texture": "#3" + } + } + }, + { + "name": "Body Layer", + "from": [ + 5.875, + 7.125, + 7.875 + ], + "to": [ + 10.125, + 13.375, + 10.125 + ], + "rotation": { + "angle": 0, + "axis": "y", + "origin": [ + 10, + -4.75, + 10 + ] + }, + "faces": { + "north": { + "uv": [ + 5, + 9, + 7, + 12 + ], + "texture": "#3" + }, + "east": { + "uv": [ + 4, + 9, + 5, + 12 + ], + "texture": "#3" + }, + "south": { + "uv": [ + 8, + 9, + 10, + 12 + ], + "texture": "#3" + }, + "west": { + "uv": [ + 7, + 9, + 8, + 12 + ], + "texture": "#3" + }, + "up": { + "uv": [ + 7, + 9, + 5, + 8 + ], + "texture": "#3" + }, + "down": { + "uv": [ + 9, + 8, + 7, + 9 + ], + "texture": "#3" + } + } + }, + { + "name": "Right Arm", + "from": [ + 12.80853, + 23.48743, + 5.75 + ], + "to": [ + 14.30853, + 29.48743, + 7.75 + ], + "rotation": { + "x": -180, + "y": 0, + "z": -20, + "origin": [ + 14.13494, + 20.36406, + 7.875 + ] + }, + "faces": { + "north": { + "uv": [ + 11, + 5, + 11.75, + 8 + ], + "texture": "#3" + }, + "east": { + "uv": [ + 10, + 5, + 11, + 8 + ], + "texture": "#3" + }, + "south": { + "uv": [ + 12.75, + 5, + 13.5, + 8 + ], + "texture": "#3" + }, + "west": { + "uv": [ + 11.75, + 5, + 12.75, + 8 + ], + "texture": "#3" + }, + "up": { + "uv": [ + 11.75, + 5, + 11, + 4 + ], + "texture": "#3" + }, + "down": { + "uv": [ + 12.5, + 4, + 11.75, + 5 + ], + "texture": "#3" + } + } + }, + { + "name": "Right Arm Layer", + "from": [ + 12.59803, + 23.37751, + 5.625 + ], + "to": [ + 14.34803, + 29.62751, + 7.875 + ], + "rotation": { + "x": -180, + "y": 0, + "z": -20, + "origin": [ + 14.13494, + 20.36406, + 7.875 + ] + }, + "faces": { + "north": { + "uv": [ + 11, + 9, + 11.75, + 12 + ], + "texture": "#3" + }, + "east": { + "uv": [ + 10, + 9, + 11, + 12 + ], + "texture": "#3" + }, + "south": { + "uv": [ + 12.75, + 9, + 13.5, + 12 + ], + "texture": "#3" + }, + "west": { + "uv": [ + 11.75, + 9, + 12.75, + 12 + ], + "texture": "#3" + }, + "up": { + "uv": [ + 11.75, + 9, + 11, + 8 + ], + "texture": "#3" + }, + "down": { + "uv": [ + 12.5, + 8, + 11.75, + 9 + ], + "texture": "#3" + } + } + }, + { + "name": "Left Arm", + "from": [ + 4.5, + 10.27584, + 0.54196 + ], + "to": [ + 6, + 16.27584, + 2.54196 + ], + "rotation": { + "angle": 30, + "axis": "x", + "origin": [ + 0, + 0, + 0 + ] + }, + "faces": { + "north": { + "uv": [ + 9, + 13, + 9.75, + 16 + ], + "texture": "#3" + }, + "east": { + "uv": [ + 8, + 13, + 9, + 16 + ], + "texture": "#3" + }, + "south": { + "uv": [ + 10.75, + 13, + 11.5, + 16 + ], + "texture": "#3" + }, + "west": { + "uv": [ + 9.75, + 13, + 10.75, + 16 + ], + "texture": "#3" + }, + "up": { + "uv": [ + 9.75, + 13, + 9, + 12 + ], + "texture": "#3" + }, + "down": { + "uv": [ + 10.5, + 12, + 9.75, + 13 + ], + "texture": "#3" + } + } + }, + { + "name": "Left Arm Layer", + "from": [ + 4.375, + 10.15084, + 0.41696 + ], + "to": [ + 6.125, + 16.40084, + 2.66696 + ], + "rotation": { + "angle": 30, + "axis": "x", + "origin": [ + 0, + 0, + 0 + ] + }, + "faces": { + "north": { + "uv": [ + 13, + 13, + 13.75, + 16 + ], + "texture": "#3" + }, + "east": { + "uv": [ + 12, + 13, + 13, + 16 + ], + "texture": "#3" + }, + "south": { + "uv": [ + 14.75, + 13, + 15.5, + 16 + ], + "texture": "#3" + }, + "west": { + "uv": [ + 13.75, + 13, + 14.75, + 16 + ], + "texture": "#3" + }, + "up": { + "uv": [ + 13.75, + 13, + 13, + 12 + ], + "texture": "#3" + }, + "down": { + "uv": [ + 14.5, + 12, + 13.75, + 13 + ], + "texture": "#3" + } + } + }, + { + "name": "Right Leg", + "from": [ + 10.9052, + -4.89142, + -8.2155 + ], + "to": [ + 12.9052, + 1.10858, + -6.2155 + ], + "rotation": { + "x": 80, + "y": -45, + "z": 0, + "origin": [ + 0, + 0, + 0 + ] + }, + "faces": { + "north": { + "uv": [ + 1, + 5, + 2, + 8 + ], + "texture": "#3" + }, + "east": { + "uv": [ + 0, + 5, + 1, + 8 + ], + "texture": "#3" + }, + "south": { + "uv": [ + 3, + 5, + 4, + 8 + ], + "texture": "#3" + }, + "west": { + "uv": [ + 2, + 5, + 3, + 8 + ], + "texture": "#3" + }, + "up": { + "uv": [ + 2, + 5, + 1, + 4 + ], + "texture": "#3" + }, + "down": { + "uv": [ + 3, + 4, + 2, + 5 + ], + "texture": "#3" + } + } + }, + { + "name": "Right Leg Layer", + "from": [ + 10.7802, + -5.01642, + -8.3405 + ], + "to": [ + 13.0302, + 1.23358, + -6.0905 + ], + "rotation": { + "x": 80, + "y": -45, + "z": 0, + "origin": [ + 0, + 0, + 0 + ] + }, + "faces": { + "north": { + "uv": [ + 1, + 9, + 2, + 12 + ], + "texture": "#3" + }, + "east": { + "uv": [ + 0, + 9, + 1, + 12 + ], + "texture": "#3" + }, + "south": { + "uv": [ + 3, + 9, + 4, + 12 + ], + "texture": "#3" + }, + "west": { + "uv": [ + 2, + 9, + 3, + 12 + ], + "texture": "#3" + }, + "up": { + "uv": [ + 2, + 9, + 1, + 8 + ], + "texture": "#3" + }, + "down": { + "uv": [ + 3, + 8, + 2, + 9 + ], + "texture": "#3" + } + } + }, + { + "name": "Left Leg", + "from": [ + 6.92843, + -0.9644, + 6.78293 + ], + "to": [ + 8.92843, + 5.0356, + 8.78293 + ], + "rotation": { + "x": 80, + "y": 45, + "z": 0, + "origin": [ + 6.1, + 5.25, + 10 + ] + }, + "faces": { + "north": { + "uv": [ + 5, + 13, + 6, + 16 + ], + "texture": "#3" + }, + "east": { + "uv": [ + 4, + 13, + 5, + 16 + ], + "texture": "#3" + }, + "south": { + "uv": [ + 7, + 13, + 8, + 16 + ], + "texture": "#3" + }, + "west": { + "uv": [ + 6, + 13, + 7, + 16 + ], + "texture": "#3" + }, + "up": { + "uv": [ + 6, + 13, + 5, + 12 + ], + "texture": "#3" + }, + "down": { + "uv": [ + 7, + 12, + 6, + 13 + ], + "texture": "#3" + } + } + }, + { + "name": "Left Leg Layer", + "from": [ + 6.80343, + -1.0894, + 6.65793 + ], + "to": [ + 9.05343, + 5.1606, + 8.90793 + ], + "rotation": { + "x": 80, + "y": 45, + "z": 0, + "origin": [ + 6.1, + 5.25, + 10 + ] + }, + "faces": { + "north": { + "uv": [ + 1, + 13, + 2, + 16 + ], + "texture": "#3" + }, + "east": { + "uv": [ + 0, + 13, + 1, + 16 + ], + "texture": "#3" + }, + "south": { + "uv": [ + 3, + 13, + 4, + 16 + ], + "texture": "#3" + }, + "west": { + "uv": [ + 2, + 13, + 3, + 16 + ], + "texture": "#3" + }, + "up": { + "uv": [ + 2, + 13, + 1, + 12 + ], + "texture": "#3" + }, + "down": { + "uv": [ + 3, + 12, + 2, + 13 + ], + "texture": "#3" + } + } + }, + { + "name": "Head", + "from": [ + 6, + 4, + 1 + ], + "to": [ + 10, + 8, + 5 + ], + "faces": { + "north": { + "uv": [ + 2, + 2, + 4, + 4 + ], + "texture": "#2" + }, + "east": { + "uv": [ + 0, + 2, + 2, + 4 + ], + "texture": "#2" + }, + "south": { + "uv": [ + 6, + 2, + 8, + 4 + ], + "texture": "#2" + }, + "west": { + "uv": [ + 4, + 2, + 6, + 4 + ], + "texture": "#2" + }, + "up": { + "uv": [ + 4, + 2, + 2, + 0 + ], + "texture": "#2" + }, + "down": { + "uv": [ + 6, + 0, + 4, + 2 + ], + "texture": "#2" + } + } + }, + { + "name": "Hat Layer", + "from": [ + 5.75, + 3.75, + 0.75 + ], + "to": [ + 10.25, + 8.25, + 5.25 + ], + "faces": { + "north": { + "uv": [ + 10, + 2, + 12, + 4 + ], + "texture": "#2" + }, + "east": { + "uv": [ + 8, + 2, + 10, + 4 + ], + "texture": "#2" + }, + "south": { + "uv": [ + 14, + 2, + 16, + 4 + ], + "texture": "#2" + }, + "west": { + "uv": [ + 12, + 2, + 14, + 4 + ], + "texture": "#2" + }, + "up": { + "uv": [ + 12, + 2, + 10, + 0 + ], + "texture": "#2" + }, + "down": { + "uv": [ + 14, + 0, + 12, + 2 + ], + "texture": "#2" + } + } + }, + { + "name": "Body", + "from": [ + 6, + 0, + 3 + ], + "to": [ + 10, + 6, + 5 + ], + "rotation": { + "x": -90, + "y": 0, + "z": 0, + "origin": [ + 10, + 6, + 5 + ] + }, + "faces": { + "north": { + "uv": [ + 5, + 5, + 7, + 8 + ], + "texture": "#2" + }, + "east": { + "uv": [ + 4, + 5, + 5, + 8 + ], + "texture": "#2" + }, + "south": { + "uv": [ + 8, + 5, + 10, + 8 + ], + "texture": "#2" + }, + "west": { + "uv": [ + 7, + 5, + 8, + 8 + ], + "texture": "#2" + }, + "up": { + "uv": [ + 7, + 5, + 5, + 4 + ], + "texture": "#2" + }, + "down": { + "uv": [ + 9, + 4, + 7, + 5 + ], + "texture": "#2" + } + } + }, + { + "name": "Body Layer", + "from": [ + 5.875, + -0.125, + 2.875 + ], + "to": [ + 10.125, + 6.125, + 5.125 + ], + "rotation": { + "x": -90, + "y": 0, + "z": 0, + "origin": [ + 10, + 6, + 5 + ] + }, + "faces": { + "north": { + "uv": [ + 5, + 9, + 7, + 12 + ], + "texture": "#2" + }, + "east": { + "uv": [ + 4, + 9, + 5, + 12 + ], + "texture": "#2" + }, + "south": { + "uv": [ + 8, + 9, + 10, + 12 + ], + "texture": "#2" + }, + "west": { + "uv": [ + 7, + 9, + 8, + 12 + ], + "texture": "#2" + }, + "up": { + "uv": [ + 7, + 9, + 5, + 8 + ], + "texture": "#2" + }, + "down": { + "uv": [ + 9, + 8, + 7, + 9 + ], + "texture": "#2" + } + } + }, + { + "name": "Right Arm", + "from": [ + 10, + 0, + 5 + ], + "to": [ + 11.5, + 6, + 7 + ], + "faces": { + "north": { + "uv": [ + 11, + 5, + 11.75, + 8 + ], + "texture": "#2" + }, + "east": { + "uv": [ + 10, + 5, + 11, + 8 + ], + "texture": "#2" + }, + "south": { + "uv": [ + 12.75, + 5, + 13.5, + 8 + ], + "texture": "#2" + }, + "west": { + "uv": [ + 11.75, + 5, + 12.75, + 8 + ], + "texture": "#2" + }, + "up": { + "uv": [ + 11.75, + 5, + 11, + 4 + ], + "texture": "#2" + }, + "down": { + "uv": [ + 12.5, + 4, + 11.75, + 5 + ], + "texture": "#2" + } + } + }, + { + "name": "Right Arm Layer", + "from": [ + 9.875, + -0.125, + 4.875 + ], + "to": [ + 11.625, + 6.125, + 7.125 + ], + "faces": { + "north": { + "uv": [ + 11, + 9, + 11.75, + 12 + ], + "texture": "#2" + }, + "east": { + "uv": [ + 10, + 9, + 11, + 12 + ], + "texture": "#2" + }, + "south": { + "uv": [ + 12.75, + 9, + 13.5, + 12 + ], + "texture": "#2" + }, + "west": { + "uv": [ + 11.75, + 9, + 12.75, + 12 + ], + "texture": "#2" + }, + "up": { + "uv": [ + 11.75, + 9, + 11, + 8 + ], + "texture": "#2" + }, + "down": { + "uv": [ + 12.5, + 8, + 11.75, + 9 + ], + "texture": "#2" + } + } + }, + { + "name": "Left Arm", + "from": [ + 4, + 0, + 5 + ], + "to": [ + 5.5, + 6, + 7 + ], + "rotation": { + "angle": 0, + "axis": "y", + "origin": [ + 13.5, + 18, + 0 + ] + }, + "faces": { + "north": { + "uv": [ + 9, + 13, + 9.75, + 16 + ], + "texture": "#2" + }, + "east": { + "uv": [ + 8, + 13, + 9, + 16 + ], + "texture": "#2" + }, + "south": { + "uv": [ + 10.75, + 13, + 11.5, + 16 + ], + "texture": "#2" + }, + "west": { + "uv": [ + 9.75, + 13, + 10.75, + 16 + ], + "texture": "#2" + }, + "up": { + "uv": [ + 9.75, + 13, + 9, + 12 + ], + "texture": "#2" + }, + "down": { + "uv": [ + 10.5, + 12, + 9.75, + 13 + ], + "texture": "#2" + } + } + }, + { + "name": "Left Arm Layer", + "from": [ + 3.875, + -0.125, + 4.875 + ], + "to": [ + 5.625, + 6.125, + 7.125 + ], + "rotation": { + "angle": 0, + "axis": "y", + "origin": [ + 13.5, + 18, + 0 + ] + }, + "faces": { + "north": { + "uv": [ + 13, + 13, + 13.75, + 16 + ], + "texture": "#2" + }, + "east": { + "uv": [ + 12, + 13, + 13, + 16 + ], + "texture": "#2" + }, + "south": { + "uv": [ + 14.75, + 13, + 15.5, + 16 + ], + "texture": "#2" + }, + "west": { + "uv": [ + 13.75, + 13, + 14.75, + 16 + ], + "texture": "#2" + }, + "up": { + "uv": [ + 13.75, + 13, + 13, + 12 + ], + "texture": "#2" + }, + "down": { + "uv": [ + 14.5, + 12, + 13.75, + 13 + ], + "texture": "#2" + } + } + }, + { + "name": "Right Leg", + "from": [ + 9.9, + 0, + 10 + ], + "to": [ + 11.9, + 6, + 12 + ], + "faces": { + "north": { + "uv": [ + 1, + 5, + 2, + 8 + ], + "texture": "#2" + }, + "east": { + "uv": [ + 0, + 5, + 1, + 8 + ], + "texture": "#2" + }, + "south": { + "uv": [ + 3, + 5, + 4, + 8 + ], + "texture": "#2" + }, + "west": { + "uv": [ + 2, + 5, + 3, + 8 + ], + "texture": "#2" + }, + "up": { + "uv": [ + 2, + 5, + 1, + 4 + ], + "texture": "#2" + }, + "down": { + "uv": [ + 3, + 4, + 2, + 5 + ], + "texture": "#2" + } + } + }, + { + "name": "Right Leg Layer", + "from": [ + 9.775, + -0.125, + 9.875 + ], + "to": [ + 12.025, + 6.125, + 12.125 + ], + "faces": { + "north": { + "uv": [ + 1, + 9, + 2, + 12 + ], + "texture": "#2" + }, + "east": { + "uv": [ + 0, + 9, + 1, + 12 + ], + "texture": "#2" + }, + "south": { + "uv": [ + 3, + 9, + 4, + 12 + ], + "texture": "#2" + }, + "west": { + "uv": [ + 2, + 9, + 3, + 12 + ], + "texture": "#2" + }, + "up": { + "uv": [ + 2, + 9, + 1, + 8 + ], + "texture": "#2" + }, + "down": { + "uv": [ + 3, + 8, + 2, + 9 + ], + "texture": "#2" + } + } + }, + { + "name": "Left Leg", + "from": [ + 4.1, + 0, + 10 + ], + "to": [ + 6.1, + 6, + 12 + ], + "faces": { + "north": { + "uv": [ + 5, + 13, + 6, + 16 + ], + "texture": "#2" + }, + "east": { + "uv": [ + 4, + 13, + 5, + 16 + ], + "texture": "#2" + }, + "south": { + "uv": [ + 7, + 13, + 8, + 16 + ], + "texture": "#2" + }, + "west": { + "uv": [ + 6, + 13, + 7, + 16 + ], + "texture": "#2" + }, + "up": { + "uv": [ + 6, + 13, + 5, + 12 + ], + "texture": "#2" + }, + "down": { + "uv": [ + 7, + 12, + 6, + 13 + ], + "texture": "#2" + } + } + }, + { + "name": "Left Leg Layer", + "from": [ + 3.975, + -0.125, + 9.875 + ], + "to": [ + 6.225, + 6.125, + 12.125 + ], + "faces": { + "north": { + "uv": [ + 1, + 13, + 2, + 16 + ], + "texture": "#2" + }, + "east": { + "uv": [ + 0, + 13, + 1, + 16 + ], + "texture": "#2" + }, + "south": { + "uv": [ + 3, + 13, + 4, + 16 + ], + "texture": "#2" + }, + "west": { + "uv": [ + 2, + 13, + 3, + 16 + ], + "texture": "#2" + }, + "up": { + "uv": [ + 2, + 13, + 1, + 12 + ], + "texture": "#2" + }, + "down": { + "uv": [ + 3, + 12, + 2, + 13 + ], + "texture": "#2" + } + } + } + ], + "gui_light": "front", + "display": { + "thirdperson_righthand": { + "rotation": [ + 75, + 45, + 0 + ], + "translation": [ + 0, + 2.5, + 0 + ], + "scale": [ + 0.375, + 0.375, + 0.375 + ] + }, + "thirdperson_lefthand": { + "rotation": [ + 75, + 45, + 0 + ], + "translation": [ + 0, + 2.5, + 0 + ], + "scale": [ + 0.375, + 0.375, + 0.375 + ] + }, + "firstperson_righthand": { + "rotation": [ + 0, + 45, + 0 + ], + "translation": [ + 0, + 4.25, + 0 + ], + "scale": [ + 0.4, + 0.4, + 0.4 + ] + }, + "firstperson_lefthand": { + "rotation": [ + 0, + -135, + 0 + ], + "translation": [ + 0, + 3.75, + 0 + ], + "scale": [ + 0.4, + 0.4, + 0.4 + ] + }, + "ground": { + "translation": [ + 0, + 3, + 0 + ], + "scale": [ + 0.25, + 0.25, + 0.25 + ] + }, + "gui": { + "rotation": [ + 30, + -135, + 0 + ], + "scale": [ + 0.625, + 0.625, + 0.625 + ] + }, + "head": { + "translation": [ + 0, + 14.75, + 0 + ] + }, + "fixed": { + "translation": [ + 0, + -1, + -1.75 + ] + }, + "on_shelf": { + "rotation": [ + 180, + 0, + 180 + ] + } + }, + "groups": [ + { + "name": "group", + "origin": [ + 0, + 12, + 0 + ], + "scope": 0, + "color": 0, + "children": [ + { + "name": "Waist", + "origin": [ + 0, + 12, + 0 + ], + "scope": 0, + "color": 0, + "children": [ + { + "name": "Head", + "origin": [ + 0, + 24, + 0 + ], + "rotation": [ + 6, + -5, + 0 + ], + "scope": 0, + "color": 0, + "children": [ + 0, + 1 + ] + }, + { + "name": "Body", + "origin": [ + 0, + 24, + 0 + ], + "scope": 0, + "color": 0, + "children": [ + 2, + 3 + ] + }, + { + "name": "Right Arm", + "origin": [ + 5, + 22, + 0 + ], + "rotation": [ + 10, + 0, + 0 + ], + "scope": 0, + "color": 0, + "children": [ + 4, + 5 + ] + }, + { + "name": "Left Arm", + "origin": [ + -5, + 22, + 0 + ], + "rotation": [ + -12, + 0, + 0 + ], + "scope": 0, + "color": 0, + "children": [ + 6, + 7 + ] + } + ] + }, + { + "name": "Right Leg", + "origin": [ + 1.9, + 12, + 0 + ], + "rotation": [ + -11, + 0, + 2 + ], + "scope": 0, + "color": 0, + "children": [ + 8, + 9 + ] + }, + { + "name": "Left Leg", + "origin": [ + -1.9, + 12, + 0 + ], + "rotation": [ + 10, + 0, + -2 + ], + "scope": 0, + "color": 0, + "children": [ + 10, + 11 + ] + } + ] + }, + { + "name": "group", + "origin": [ + 0, + 12, + 0 + ], + "scope": 0, + "color": 0, + "children": [ + { + "name": "Waist", + "origin": [ + 0, + 12, + 0 + ], + "scope": 0, + "color": 0, + "children": [ + { + "name": "Head", + "origin": [ + 0, + 24, + 0 + ], + "rotation": [ + 6, + -5, + 0 + ], + "scope": 0, + "color": 0, + "children": [ + 12, + 13 + ] + }, + { + "name": "Body", + "origin": [ + 0, + 24, + 0 + ], + "scope": 0, + "color": 0, + "children": [ + 14, + 15 + ] + }, + { + "name": "Right Arm", + "origin": [ + 5, + 22, + 0 + ], + "rotation": [ + 10, + 0, + 0 + ], + "scope": 0, + "color": 0, + "children": [ + 16, + 17 + ] + }, + { + "name": "Left Arm", + "origin": [ + -5, + 22, + 0 + ], + "rotation": [ + -12, + 0, + 0 + ], + "scope": 0, + "color": 0, + "children": [ + 18, + 19 + ] + } + ] + }, + { + "name": "Right Leg", + "origin": [ + 1.9, + 12, + 0 + ], + "rotation": [ + -11, + 0, + 2 + ], + "scope": 0, + "color": 0, + "children": [ + 20, + 21 + ] + }, + { + "name": "Left Leg", + "origin": [ + -1.9, + 12, + 0 + ], + "rotation": [ + 10, + 0, + -2 + ], + "scope": 0, + "color": 0, + "children": [ + 22, + 23 + ] + } + ] + } + ] + } +} diff --git a/src/main/resources/xia/assets/useless_mod/models/item/advanced_alloy_furnace_block.json b/src/main/resources/xia/assets/useless_mod/models/item/advanced_alloy_furnace_block.json new file mode 100644 index 00000000..a7b999b7 --- /dev/null +++ b/src/main/resources/xia/assets/useless_mod/models/item/advanced_alloy_furnace_block.json @@ -0,0 +1,3 @@ +{ +"parent": "useless_mod:block/advanced_alloy_furnace" +} \ No newline at end of file diff --git a/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_crowbar.json b/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_crowbar.json new file mode 100644 index 00000000..526d75ea --- /dev/null +++ b/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_crowbar.json @@ -0,0 +1,206 @@ +{ + "format_version": "1.21.6", + "credit": "Made with Blockbench,made by xia_bi418", + "textures": { + "1": "useless_mod:item/beef_tool/stick", + "5": "useless_mod:item/beef_tool/crowbar", + "particle": "advanced_alloy_furnace" + }, + "elements": [ + { + "from": [6, -14, 6], + "to": [10, -9, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [8, -11.5, 8]}, + "faces": { + "north": {"uv": [1.5, 0, 3.5, 2.5], "texture": "#1"}, + "east": {"uv": [1.5, 2.5, 3.5, 5], "texture": "#1"}, + "south": {"uv": [3.5, 0, 5.5, 2.5], "texture": "#1"}, + "west": {"uv": [3.5, 2.5, 1.5, 5], "texture": "#1"}, + "up": {"uv": [5.5, 4.5, 3.5, 2.5], "texture": "#1"}, + "down": {"uv": [5.5, 4.5, 3.5, 6.5], "texture": "#1"} + } + }, + { + "from": [6.025, -15.25, 6.25], + "to": [9.975, -12.45, 7.775], + "rotation": {"angle": -45, "axis": "x", "origin": [8, -13.5, 8.025]}, + "faces": { + "north": {"uv": [3.5, 6.5, 5.5, 8], "texture": "#1"}, + "east": {"uv": [7.5, 7.5, 8.5, 9], "texture": "#1"}, + "south": {"uv": [0, 7, 2, 8.5], "texture": "#1"}, + "west": {"uv": [8.5, 7.5, 7.5, 9], "texture": "#1"}, + "up": {"uv": [9.5, 2.5, 7.5, 1.5], "texture": "#1"}, + "down": {"uv": [9.5, 2.5, 7.5, 3.5], "texture": "#1"} + } + }, + { + "from": [6, -16, 8], + "to": [10, -14, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [8, -15, 9]}, + "faces": { + "north": {"uv": [7.5, 3.5, 9.5, 4.5], "texture": "#1"}, + "east": {"uv": [3.5, 8, 4.5, 9], "texture": "#1"}, + "south": {"uv": [7.5, 4.5, 9.5, 5.5], "texture": "#1"}, + "west": {"uv": [4.5, 8, 3.5, 9], "texture": "#1"}, + "up": {"uv": [9.5, 6.5, 7.5, 5.5], "texture": "#1"}, + "down": {"uv": [9.5, 6.5, 7.5, 7.5], "texture": "#1"} + } + }, + { + "from": [6.5, -9, 6.5], + "to": [9.5, 5, 9.5], + "rotation": {"angle": 0, "axis": "y", "origin": [7, -9, 7]}, + "faces": { + "north": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "east": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "south": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "west": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "up": {"uv": [3.5, 8.5, 2, 7], "texture": "#1"}, + "down": {"uv": [9, 0, 7.5, 1.5], "texture": "#1"} + } + }, + { + "from": [6, 5, 6], + "to": [10, 9, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 5, 7]}, + "faces": { + "north": {"uv": [1.5, 5, 3.5, 7], "texture": "#1"}, + "east": {"uv": [5.5, 0, 7.5, 2], "texture": "#1"}, + "south": {"uv": [5.5, 2, 7.5, 4], "texture": "#1"}, + "west": {"uv": [7.5, 0, 5.5, 2], "texture": "#1"}, + "up": {"uv": [7.5, 6, 5.5, 4], "texture": "#1"}, + "down": {"uv": [7.5, 6, 5.5, 8], "texture": "#1"} + } + }, + { + "from": [6.5, 9, 6.5], + "to": [9.5, 13, 9.5], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 9, 7]}, + "faces": { + "north": {"uv": [4, 10, 7, 14], "texture": "#5"}, + "east": {"uv": [4, 10, 7, 14], "texture": "#5"}, + "south": {"uv": [4, 10, 7, 14], "texture": "#5"}, + "west": {"uv": [4, 10, 7, 14], "texture": "#5"}, + "up": {"uv": [3, 14, 0, 11], "texture": "#5"}, + "down": {"uv": [15, 0, 12, 3], "texture": "#5"} + } + }, + { + "from": [7, 15, 5], + "to": [9, 19, 9], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 17, 5]}, + "faces": { + "north": {"uv": [12, 3, 14, 7], "texture": "#5"}, + "east": {"uv": [8, 0, 12, 4], "texture": "#5"}, + "south": {"uv": [7, 12, 9, 16], "texture": "#5"}, + "west": {"uv": [12, 0, 8, 4], "texture": "#5"}, + "up": {"uv": [14, 11, 12, 7], "texture": "#5"}, + "down": {"uv": [11, 12, 9, 16], "texture": "#5"} + } + }, + { + "from": [6, 18, 1], + "to": [10, 18, 7], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 16, 5]}, + "faces": { + "north": {"uv": [0, 0, 4, 0], "texture": "#5"}, + "east": {"uv": [0, 0, 6, 0], "texture": "#5"}, + "south": {"uv": [0, 0, 4, 0], "texture": "#5"}, + "west": {"uv": [0, 0, 6, 0], "texture": "#5"}, + "up": {"uv": [4, 6, 0, 0], "texture": "#5"}, + "down": {"uv": [0, 0, 4, 6], "texture": "#5"} + } + }, + { + "from": [6, 13, 6], + "to": [10, 18, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [8, 15.5, 8]}, + "faces": { + "north": {"uv": [4, 0, 8, 5], "texture": "#5"}, + "east": {"uv": [4, 5, 8, 10], "texture": "#5"}, + "south": {"uv": [0, 6, 4, 11], "texture": "#5"}, + "west": {"uv": [8, 5, 4, 10], "texture": "#5"}, + "up": {"uv": [12, 8, 8, 4], "texture": "#5"}, + "down": {"uv": [12, 8, 8, 12], "texture": "#5"} + } + } + ], + "gui_light": "front", + "display": { + "thirdperson_righthand": { + "translation": [0, 5, 1], + "scale": [0.55, 0.55, 0.55] + }, + "thirdperson_lefthand": { + "translation": [0, 5.5, 1], + "scale": [0.55, 0.55, 0.55] + }, + "firstperson_righthand": { + "rotation": [19, 16, 0], + "translation": [3.25, 7.25, 1.5], + "scale": [0.4, 0.4, 0.4] + }, + "firstperson_lefthand": { + "rotation": [9.64, 27.12, 0.81], + "translation": [4.5, 7.25, 0], + "scale": [0.4, 0.4, 0.4] + }, + "ground": { + "translation": [0, 3, 0], + "scale": [0.25, 0.25, 0.25] + }, + "gui": { + "rotation": [30, -135, 0], + "translation": [0, 1.75, 0], + "scale": [0.4, 0.4, 0.4] + }, + "head": { + "translation": [0, 15, 0] + }, + "fixed": { + "rotation": [90, -44.5, 90], + "scale": [0.5, 0.5, 0.5] + } + }, + "groups": [ + { + "name": "stick", + "origin": [8, 8, 8], + "color": 0, + "children": [ + { + "name": "stick_bot", + "origin": [8, 8, 8], + "color": 0, + "children": [0, 1, 2] + }, + { + "name": "stick_mid", + "origin": [8, 8, 8], + "color": 0, + "children": [3] + }, + { + "name": "stick_top", + "origin": [7, 0, 7], + "color": 0, + "children": [4] + } + ] + }, + { + "name": "tool_crowbar", + "origin": [8, 8, 8], + "color": 0, + "children": [5, 6, 7, 8] + } + ], + "overrides": [ + { + "predicate": { + "custom_model_data": 1 + }, + "model": "useless_mod:item/endless_beaf_crowbar_silk" + } + ] +} \ No newline at end of file diff --git a/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_crowbar_silk.json b/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_crowbar_silk.json new file mode 100644 index 00000000..951e62ef --- /dev/null +++ b/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_crowbar_silk.json @@ -0,0 +1,198 @@ +{ + "format_version": "1.21.6", + "credit": "Made with Blockbench,made by xia_bi418", + "textures": { + "1": "useless_mod:item/beef_tool/stick", + "6": "useless_mod:item/beef_tool/crowbar_silk", + "particle": "advanced_alloy_furnace" + }, + "elements": [ + { + "from": [6, -14, 6], + "to": [10, -9, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [8, -11.5, 8]}, + "faces": { + "north": {"uv": [1.5, 0, 3.5, 2.5], "texture": "#1"}, + "east": {"uv": [1.5, 2.5, 3.5, 5], "texture": "#1"}, + "south": {"uv": [3.5, 0, 5.5, 2.5], "texture": "#1"}, + "west": {"uv": [3.5, 2.5, 1.5, 5], "texture": "#1"}, + "up": {"uv": [5.5, 4.5, 3.5, 2.5], "texture": "#1"}, + "down": {"uv": [5.5, 4.5, 3.5, 6.5], "texture": "#1"} + } + }, + { + "from": [6.025, -15.25, 6.25], + "to": [9.975, -12.45, 7.775], + "rotation": {"angle": -45, "axis": "x", "origin": [8, -13.5, 8.025]}, + "faces": { + "north": {"uv": [3.5, 6.5, 5.5, 8], "texture": "#1"}, + "east": {"uv": [7.5, 7.5, 8.5, 9], "texture": "#1"}, + "south": {"uv": [0, 7, 2, 8.5], "texture": "#1"}, + "west": {"uv": [8.5, 7.5, 7.5, 9], "texture": "#1"}, + "up": {"uv": [9.5, 2.5, 7.5, 1.5], "texture": "#1"}, + "down": {"uv": [9.5, 2.5, 7.5, 3.5], "texture": "#1"} + } + }, + { + "from": [6, -16, 8], + "to": [10, -14, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [8, -15, 9]}, + "faces": { + "north": {"uv": [7.5, 3.5, 9.5, 4.5], "texture": "#1"}, + "east": {"uv": [3.5, 8, 4.5, 9], "texture": "#1"}, + "south": {"uv": [7.5, 4.5, 9.5, 5.5], "texture": "#1"}, + "west": {"uv": [4.5, 8, 3.5, 9], "texture": "#1"}, + "up": {"uv": [9.5, 6.5, 7.5, 5.5], "texture": "#1"}, + "down": {"uv": [9.5, 6.5, 7.5, 7.5], "texture": "#1"} + } + }, + { + "from": [6.5, -9, 6.5], + "to": [9.5, 5, 9.5], + "rotation": {"angle": 0, "axis": "y", "origin": [7, -9, 7]}, + "faces": { + "north": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "east": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "south": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "west": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "up": {"uv": [3.5, 8.5, 2, 7], "texture": "#1"}, + "down": {"uv": [9, 0, 7.5, 1.5], "texture": "#1"} + } + }, + { + "from": [6, 5, 6], + "to": [10, 9, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 5, 7]}, + "faces": { + "north": {"uv": [1.5, 5, 3.5, 7], "texture": "#1"}, + "east": {"uv": [5.5, 0, 7.5, 2], "texture": "#1"}, + "south": {"uv": [5.5, 2, 7.5, 4], "texture": "#1"}, + "west": {"uv": [7.5, 0, 5.5, 2], "texture": "#1"}, + "up": {"uv": [7.5, 6, 5.5, 4], "texture": "#1"}, + "down": {"uv": [7.5, 6, 5.5, 8], "texture": "#1"} + } + }, + { + "from": [6.5, 9, 6.5], + "to": [9.5, 13, 9.5], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 9, 7]}, + "faces": { + "north": {"uv": [4, 10, 7, 14], "texture": "#6"}, + "east": {"uv": [4, 10, 7, 14], "texture": "#6"}, + "south": {"uv": [4, 10, 7, 14], "texture": "#6"}, + "west": {"uv": [4, 10, 7, 14], "texture": "#6"}, + "up": {"uv": [3, 14, 0, 11], "texture": "#6"}, + "down": {"uv": [15, 0, 12, 3], "texture": "#6"} + } + }, + { + "from": [7, 15, 5], + "to": [9, 19, 9], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 17, 5]}, + "faces": { + "north": {"uv": [12, 3, 14, 7], "texture": "#6"}, + "east": {"uv": [8, 0, 12, 4], "texture": "#6"}, + "south": {"uv": [7, 12, 9, 16], "texture": "#6"}, + "west": {"uv": [12, 0, 8, 4], "texture": "#6"}, + "up": {"uv": [14, 11, 12, 7], "texture": "#6"}, + "down": {"uv": [11, 12, 9, 16], "texture": "#6"} + } + }, + { + "from": [6, 18, 1], + "to": [10, 18, 7], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 16, 5]}, + "faces": { + "north": {"uv": [0, 0, 4, 0], "texture": "#6"}, + "east": {"uv": [0, 0, 6, 0], "texture": "#6"}, + "south": {"uv": [0, 0, 4, 0], "texture": "#6"}, + "west": {"uv": [0, 0, 6, 0], "texture": "#6"}, + "up": {"uv": [4, 6, 0, 0], "texture": "#6"}, + "down": {"uv": [0, 0, 4, 6], "texture": "#6"} + } + }, + { + "from": [6, 13, 6], + "to": [10, 18, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [8, 15.5, 8]}, + "faces": { + "north": {"uv": [4, 0, 8, 5], "texture": "#6"}, + "east": {"uv": [4, 5, 8, 10], "texture": "#6"}, + "south": {"uv": [0, 6, 4, 11], "texture": "#6"}, + "west": {"uv": [8, 5, 4, 10], "texture": "#6"}, + "up": {"uv": [12, 8, 8, 4], "texture": "#6"}, + "down": {"uv": [12, 8, 8, 12], "texture": "#6"} + } + } + ], + "gui_light": "front", + "display": { + "thirdperson_righthand": { + "translation": [0, 5, 1], + "scale": [0.55, 0.55, 0.55] + }, + "thirdperson_lefthand": { + "translation": [0, 5.5, 1], + "scale": [0.55, 0.55, 0.55] + }, + "firstperson_righthand": { + "rotation": [19, 16, 0], + "translation": [3.25, 7.25, 1.5], + "scale": [0.4, 0.4, 0.4] + }, + "firstperson_lefthand": { + "rotation": [9.64, 27.12, 0.81], + "translation": [4.5, 7.25, 0], + "scale": [0.4, 0.4, 0.4] + }, + "ground": { + "translation": [0, 3, 0], + "scale": [0.25, 0.25, 0.25] + }, + "gui": { + "rotation": [30, -135, 0], + "translation": [0, 1.75, 0], + "scale": [0.4, 0.4, 0.4] + }, + "head": { + "translation": [0, 15, 0] + }, + "fixed": { + "rotation": [90, -44.5, 90], + "scale": [0.5, 0.5, 0.5] + } + }, + "groups": [ + { + "name": "stick", + "origin": [8, 8, 8], + "color": 0, + "children": [ + { + "name": "stick_bot", + "origin": [8, 8, 8], + "color": 0, + "children": [0, 1, 2] + }, + { + "name": "stick_mid", + "origin": [8, 8, 8], + "color": 0, + "children": [3] + }, + { + "name": "stick_top", + "origin": [7, 0, 7], + "color": 0, + "children": [4] + } + ] + }, + { + "name": "tool_crowbar", + "origin": [8, 8, 8], + "color": 0, + "children": [5, 6, 7, 8] + } + ] +} \ No newline at end of file diff --git a/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_hammer.json b/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_hammer.json new file mode 100644 index 00000000..fa4aa841 --- /dev/null +++ b/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_hammer.json @@ -0,0 +1,192 @@ +{ + "format_version": "1.21.6", + "credit": "Made with Blockbench,made by xia_bi418", + "texture_size": [32, 32], + "textures": { + "1": "useless_mod:item/beef_tool/stick", + "2": "useless_mod:item/beef_tool/harmmer", + "particle": "advanced_alloy_furnace" + }, + "elements": [ + { + "from": [6, -14, 6], + "to": [10, -9, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [8, -11.5, 8]}, + "faces": { + "north": {"uv": [1.5, 0, 3.5, 2.5], "texture": "#1"}, + "east": {"uv": [1.5, 2.5, 3.5, 5], "texture": "#1"}, + "south": {"uv": [3.5, 0, 5.5, 2.5], "texture": "#1"}, + "west": {"uv": [3.5, 2.5, 1.5, 5], "texture": "#1"}, + "up": {"uv": [5.5, 4.5, 3.5, 2.5], "texture": "#1"}, + "down": {"uv": [5.5, 4.5, 3.5, 6.5], "texture": "#1"} + } + }, + { + "from": [6.025, -15.25, 6.25], + "to": [9.975, -12.45, 7.775], + "rotation": {"angle": -45, "axis": "x", "origin": [8, -13.5, 8.025]}, + "faces": { + "north": {"uv": [3.5, 6.5, 5.5, 8], "texture": "#1"}, + "east": {"uv": [7.5, 7.5, 8.5, 9], "texture": "#1"}, + "south": {"uv": [0, 7, 2, 8.5], "texture": "#1"}, + "west": {"uv": [8.5, 7.5, 7.5, 9], "texture": "#1"}, + "up": {"uv": [9.5, 2.5, 7.5, 1.5], "texture": "#1"}, + "down": {"uv": [9.5, 2.5, 7.5, 3.5], "texture": "#1"} + } + }, + { + "from": [6, -16, 8], + "to": [10, -14, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [8, -15, 9]}, + "faces": { + "north": {"uv": [7.5, 3.5, 9.5, 4.5], "texture": "#1"}, + "east": {"uv": [3.5, 8, 4.5, 9], "texture": "#1"}, + "south": {"uv": [7.5, 4.5, 9.5, 5.5], "texture": "#1"}, + "west": {"uv": [4.5, 8, 3.5, 9], "texture": "#1"}, + "up": {"uv": [9.5, 6.5, 7.5, 5.5], "texture": "#1"}, + "down": {"uv": [9.5, 6.5, 7.5, 7.5], "texture": "#1"} + } + }, + { + "from": [6.5, -9, 6.5], + "to": [9.5, 5, 9.5], + "rotation": {"angle": 0, "axis": "y", "origin": [7, -9, 7]}, + "faces": { + "north": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "east": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "south": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "west": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "up": {"uv": [3.5, 8.5, 2, 7], "texture": "#1"}, + "down": {"uv": [9, 0, 7.5, 1.5], "texture": "#1"} + } + }, + { + "from": [6, 5, 6], + "to": [10, 9, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 5, 7]}, + "faces": { + "north": {"uv": [1.5, 5, 3.5, 7], "texture": "#1"}, + "east": {"uv": [5.5, 0, 7.5, 2], "texture": "#1"}, + "south": {"uv": [5.5, 2, 7.5, 4], "texture": "#1"}, + "west": {"uv": [7.5, 0, 5.5, 2], "texture": "#1"}, + "up": {"uv": [7.5, 6, 5.5, 4], "texture": "#1"}, + "down": {"uv": [7.5, 6, 5.5, 8], "texture": "#1"} + } + }, + { + "from": [3, 8, 11], + "to": [13, 18, 15], + "rotation": {"angle": 0, "axis": "y", "origin": [8, 13, 18]}, + "faces": { + "north": {"uv": [0, 0, 5, 5], "texture": "#2"}, + "east": {"uv": [8, 10, 10, 15], "texture": "#2"}, + "south": {"uv": [0, 5, 5, 10], "texture": "#2"}, + "west": {"uv": [10, 10, 8, 15], "texture": "#2"}, + "up": {"uv": [15, 12, 10, 10], "texture": "#2"}, + "down": {"uv": [16, 0, 11, 2], "texture": "#2"} + } + }, + { + "from": [3, 8, 1], + "to": [13, 18, 5], + "rotation": {"angle": 0, "axis": "y", "origin": [8, 13, 8]}, + "faces": { + "north": {"uv": [0, 5, 5, 10], "texture": "#2"}, + "east": {"uv": [10, 10, 8, 15], "texture": "#2"}, + "south": {"uv": [0, 0, 5, 5], "texture": "#2"}, + "west": {"uv": [8, 10, 10, 15], "texture": "#2"}, + "up": {"uv": [15, 10, 10, 12], "texture": "#2"}, + "down": {"uv": [11, 2, 16, 0], "texture": "#2"} + } + }, + { + "from": [4, 9, 5], + "to": [12, 17, 11], + "rotation": {"angle": 0, "axis": "y", "origin": [8, 13, 8]}, + "faces": { + "north": {"uv": [0, 10, 4, 14], "texture": "#2"}, + "east": {"uv": [6.5, 0, 9.5, 4], "texture": "#2"}, + "south": {"uv": [4, 10, 8, 14], "texture": "#2"}, + "west": {"uv": [6.5, 0, 9.5, 4], "texture": "#2"}, + "up": {"uv": [9, 8.5, 5, 5.5], "texture": "#2"}, + "down": {"uv": [13, 5.5, 9, 8.5], "texture": "#2"} + } + } + ], + "gui_light": "front", + "display": { + "thirdperson_righthand": { + "translation": [0, 5, 1], + "scale": [0.55, 0.55, 0.55] + }, + "thirdperson_lefthand": { + "translation": [0, 5.5, 1], + "scale": [0.55, 0.55, 0.55] + }, + "firstperson_righthand": { + "rotation": [19, 16, 0], + "translation": [3.25, 7.25, 1.5], + "scale": [0.4, 0.4, 0.4] + }, + "firstperson_lefthand": { + "rotation": [9.64, 27.12, 0.81], + "translation": [4.5, 7.25, 0], + "scale": [0.4, 0.4, 0.4] + }, + "ground": { + "translation": [0, 3, 0], + "scale": [0.25, 0.25, 0.25] + }, + "gui": { + "rotation": [30, -135, 0], + "scale": [0.625, 0.625, 0.625] + }, + "head": { + "translation": [0, 15, 0] + }, + "fixed": { + "scale": [0.5, 0.5, 0.5] + } + }, + "groups": [ + { + "name": "stick", + "origin": [8, 8, 8], + "color": 0, + "children": [ + { + "name": "stick_bot", + "origin": [8, 8, 8], + "color": 0, + "children": [0, 1, 2] + }, + { + "name": "stick_mid", + "origin": [8, 8, 8], + "color": 0, + "children": [3] + }, + { + "name": "stick_top", + "origin": [7, 0, 7], + "color": 0, + "children": [4] + } + ] + }, + { + "name": "tool_hammer", + "origin": [8, 8, 8], + "color": 0, + "children": [5, 6, 7] + } + ], + "overrides": [ + { + "predicate": { + "custom_model_data": 1 + }, + "model": "useless_mod:item/endless_beaf_hammer_silk" + } + ] +} \ No newline at end of file diff --git a/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_hammer_silk.json b/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_hammer_silk.json new file mode 100644 index 00000000..c17ab3ad --- /dev/null +++ b/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_hammer_silk.json @@ -0,0 +1,184 @@ +{ + "format_version": "1.21.6", + "credit": "Made with Blockbench,made by xia_bi418", + "texture_size": [32, 32], + "textures": { + "1": "useless_mod:item/beef_tool/stick", + "2": "useless_mod:item/beef_tool/harmmer_silk", + "particle": "advanced_alloy_furnace" + }, + "elements": [ + { + "from": [6, -14, 6], + "to": [10, -9, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [8, -11.5, 8]}, + "faces": { + "north": {"uv": [1.5, 0, 3.5, 2.5], "texture": "#1"}, + "east": {"uv": [1.5, 2.5, 3.5, 5], "texture": "#1"}, + "south": {"uv": [3.5, 0, 5.5, 2.5], "texture": "#1"}, + "west": {"uv": [3.5, 2.5, 1.5, 5], "texture": "#1"}, + "up": {"uv": [5.5, 4.5, 3.5, 2.5], "texture": "#1"}, + "down": {"uv": [5.5, 4.5, 3.5, 6.5], "texture": "#1"} + } + }, + { + "from": [6.025, -15.25, 6.25], + "to": [9.975, -12.45, 7.775], + "rotation": {"angle": -45, "axis": "x", "origin": [8, -13.5, 8.025]}, + "faces": { + "north": {"uv": [3.5, 6.5, 5.5, 8], "texture": "#1"}, + "east": {"uv": [7.5, 7.5, 8.5, 9], "texture": "#1"}, + "south": {"uv": [0, 7, 2, 8.5], "texture": "#1"}, + "west": {"uv": [8.5, 7.5, 7.5, 9], "texture": "#1"}, + "up": {"uv": [9.5, 2.5, 7.5, 1.5], "texture": "#1"}, + "down": {"uv": [9.5, 2.5, 7.5, 3.5], "texture": "#1"} + } + }, + { + "from": [6, -16, 8], + "to": [10, -14, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [8, -15, 9]}, + "faces": { + "north": {"uv": [7.5, 3.5, 9.5, 4.5], "texture": "#1"}, + "east": {"uv": [3.5, 8, 4.5, 9], "texture": "#1"}, + "south": {"uv": [7.5, 4.5, 9.5, 5.5], "texture": "#1"}, + "west": {"uv": [4.5, 8, 3.5, 9], "texture": "#1"}, + "up": {"uv": [9.5, 6.5, 7.5, 5.5], "texture": "#1"}, + "down": {"uv": [9.5, 6.5, 7.5, 7.5], "texture": "#1"} + } + }, + { + "from": [6.5, -9, 6.5], + "to": [9.5, 5, 9.5], + "rotation": {"angle": 0, "axis": "y", "origin": [7, -9, 7]}, + "faces": { + "north": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "east": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "south": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "west": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "up": {"uv": [3.5, 8.5, 2, 7], "texture": "#1"}, + "down": {"uv": [9, 0, 7.5, 1.5], "texture": "#1"} + } + }, + { + "from": [6, 5, 6], + "to": [10, 9, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 5, 7]}, + "faces": { + "north": {"uv": [1.5, 5, 3.5, 7], "texture": "#1"}, + "east": {"uv": [5.5, 0, 7.5, 2], "texture": "#1"}, + "south": {"uv": [5.5, 2, 7.5, 4], "texture": "#1"}, + "west": {"uv": [7.5, 0, 5.5, 2], "texture": "#1"}, + "up": {"uv": [7.5, 6, 5.5, 4], "texture": "#1"}, + "down": {"uv": [7.5, 6, 5.5, 8], "texture": "#1"} + } + }, + { + "from": [3, 8, 11], + "to": [13, 18, 15], + "rotation": {"angle": 0, "axis": "y", "origin": [8, 13, 18]}, + "faces": { + "north": {"uv": [0, 0, 5, 5], "texture": "#2"}, + "east": {"uv": [8, 10, 10, 15], "texture": "#2"}, + "south": {"uv": [0, 5, 5, 10], "texture": "#2"}, + "west": {"uv": [10, 10, 8, 15], "texture": "#2"}, + "up": {"uv": [15, 12, 10, 10], "texture": "#2"}, + "down": {"uv": [16, 0, 11, 2], "texture": "#2"} + } + }, + { + "from": [3, 8, 1], + "to": [13, 18, 5], + "rotation": {"angle": 0, "axis": "y", "origin": [8, 13, 8]}, + "faces": { + "north": {"uv": [0, 5, 5, 10], "texture": "#2"}, + "east": {"uv": [10, 10, 8, 15], "texture": "#2"}, + "south": {"uv": [0, 0, 5, 5], "texture": "#2"}, + "west": {"uv": [8, 10, 10, 15], "texture": "#2"}, + "up": {"uv": [15, 10, 10, 12], "texture": "#2"}, + "down": {"uv": [11, 2, 16, 0], "texture": "#2"} + } + }, + { + "from": [4, 9, 5], + "to": [12, 17, 11], + "rotation": {"angle": 0, "axis": "y", "origin": [8, 13, 8]}, + "faces": { + "north": {"uv": [0, 10, 4, 14], "texture": "#2"}, + "east": {"uv": [6.5, 0, 9.5, 4], "texture": "#2"}, + "south": {"uv": [4, 10, 8, 14], "texture": "#2"}, + "west": {"uv": [6.5, 0, 9.5, 4], "texture": "#2"}, + "up": {"uv": [9, 8.5, 5, 5.5], "texture": "#2"}, + "down": {"uv": [13, 5.5, 9, 8.5], "texture": "#2"} + } + } + ], + "gui_light": "front", + "display": { + "thirdperson_righthand": { + "translation": [0, 5, 1], + "scale": [0.55, 0.55, 0.55] + }, + "thirdperson_lefthand": { + "translation": [0, 5.5, 1], + "scale": [0.55, 0.55, 0.55] + }, + "firstperson_righthand": { + "rotation": [19, 16, 0], + "translation": [3.25, 7.25, 1.5], + "scale": [0.4, 0.4, 0.4] + }, + "firstperson_lefthand": { + "rotation": [9.64, 27.12, 0.81], + "translation": [4.5, 7.25, 0], + "scale": [0.4, 0.4, 0.4] + }, + "ground": { + "translation": [0, 3, 0], + "scale": [0.25, 0.25, 0.25] + }, + "gui": { + "rotation": [30, -135, 0], + "scale": [0.625, 0.625, 0.625] + }, + "head": { + "translation": [0, 15, 0] + }, + "fixed": { + "scale": [0.5, 0.5, 0.5] + } + }, + "groups": [ + { + "name": "stick", + "origin": [8, 8, 8], + "color": 0, + "children": [ + { + "name": "stick_bot", + "origin": [8, 8, 8], + "color": 0, + "children": [0, 1, 2] + }, + { + "name": "stick_mid", + "origin": [8, 8, 8], + "color": 0, + "children": [3] + }, + { + "name": "stick_top", + "origin": [7, 0, 7], + "color": 0, + "children": [4] + } + ] + }, + { + "name": "tool_hammer", + "origin": [8, 8, 8], + "color": 0, + "children": [5, 6, 7] + } + ] +} \ No newline at end of file diff --git a/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_item.json b/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_item.json new file mode 100644 index 00000000..fe05acfd --- /dev/null +++ b/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_item.json @@ -0,0 +1,253 @@ +{ + "format_version": "1.21.6", + "credit": "Made with Blockbench,made by xia_bi418", + "texture_size": [32, 32], + "textures": { + "1": "useless_mod:item/beef_tool/stick", + "11": "useless_mod:item/beef_tool/common_base", + "12": "useless_mod:item/beef_tool/common_layer", + "particle": "advanced_alloy_furnace" + }, + "elements": [ + { + "from": [6, -14, 6], + "to": [10, -9, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [8, -11.5, 8]}, + "faces": { + "north": {"uv": [1.5, 0, 3.5, 2.5], "texture": "#1"}, + "east": {"uv": [1.5, 2.5, 3.5, 5], "texture": "#1"}, + "south": {"uv": [3.5, 0, 5.5, 2.5], "texture": "#1"}, + "west": {"uv": [3.5, 2.5, 1.5, 5], "texture": "#1"}, + "up": {"uv": [5.5, 4.5, 3.5, 2.5], "texture": "#1"}, + "down": {"uv": [5.5, 4.5, 3.5, 6.5], "texture": "#1"} + } + }, + { + "from": [6.025, -15.25, 6.25], + "to": [9.975, -12.45, 7.775], + "rotation": {"angle": -45, "axis": "x", "origin": [8, -13.5, 8.025]}, + "faces": { + "north": {"uv": [3.5, 6.5, 5.5, 8], "texture": "#1"}, + "east": {"uv": [7.5, 7.5, 8.5, 9], "texture": "#1"}, + "south": {"uv": [0, 7, 2, 8.5], "texture": "#1"}, + "west": {"uv": [8.5, 7.5, 7.5, 9], "texture": "#1"}, + "up": {"uv": [9.5, 2.5, 7.5, 1.5], "texture": "#1"}, + "down": {"uv": [9.5, 2.5, 7.5, 3.5], "texture": "#1"} + } + }, + { + "from": [6, -16, 8], + "to": [10, -14, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [8, -15, 9]}, + "faces": { + "north": {"uv": [7.5, 3.5, 9.5, 4.5], "texture": "#1"}, + "east": {"uv": [3.5, 8, 4.5, 9], "texture": "#1"}, + "south": {"uv": [7.5, 4.5, 9.5, 5.5], "texture": "#1"}, + "west": {"uv": [4.5, 8, 3.5, 9], "texture": "#1"}, + "up": {"uv": [9.5, 6.5, 7.5, 5.5], "texture": "#1"}, + "down": {"uv": [9.5, 6.5, 7.5, 7.5], "texture": "#1"} + } + }, + { + "from": [6.5, -9, 6.5], + "to": [9.5, 5, 9.5], + "rotation": {"angle": 0, "axis": "y", "origin": [7, -9, 7]}, + "faces": { + "north": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "east": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "south": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "west": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "up": {"uv": [3.5, 8.5, 2, 7], "texture": "#1"}, + "down": {"uv": [9, 0, 7.5, 1.5], "texture": "#1"} + } + }, + { + "from": [6, 5, 6], + "to": [10, 9, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 5, 7]}, + "faces": { + "north": {"uv": [1.5, 5, 3.5, 7], "texture": "#1"}, + "east": {"uv": [5.5, 0, 7.5, 2], "texture": "#1"}, + "south": {"uv": [5.5, 2, 7.5, 4], "texture": "#1"}, + "west": {"uv": [7.5, 0, 5.5, 2], "texture": "#1"}, + "up": {"uv": [7.5, 6, 5.5, 4], "texture": "#1"}, + "down": {"uv": [7.5, 6, 5.5, 8], "texture": "#1"} + } + }, + { + "from": [7, 9, 7], + "to": [9, 10, 9], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 9, 7]}, + "faces": { + "north": {"uv": [7, 5.5, 8, 6], "texture": "#11"}, + "east": {"uv": [7, 5.5, 8, 6], "texture": "#11"}, + "south": {"uv": [8, 5.5, 7, 6], "texture": "#11"}, + "west": {"uv": [7, 5.5, 8, 6], "texture": "#11"}, + "up": {"uv": [8, 5.5, 7, 4.5], "texture": "#11"}, + "down": {"uv": [6, 7, 5, 8], "texture": "#11"} + } + }, + { + "from": [4.5, 17.5, 4.5], + "to": [11.5, 18.5, 11.5], + "rotation": {"angle": 45, "axis": "y", "origin": [8, 14.5, 8]}, + "faces": { + "north": {"uv": [7, 0, 10.5, 0.5], "texture": "#11"}, + "east": {"uv": [7, 0.5, 10.5, 1], "texture": "#11"}, + "south": {"uv": [7, 1, 10.5, 1.5], "texture": "#11"}, + "west": {"uv": [1.5, 7, 5, 7.5], "texture": "#11"}, + "up": {"uv": [3.5, 3.5, 0, 0], "texture": "#11"}, + "down": {"uv": [3.5, 3.5, 0, 7], "texture": "#11"} + } + }, + { + "from": [4.5, 11, 4.5], + "to": [11.5, 12, 11.5], + "rotation": {"angle": 45, "axis": "y", "origin": [8, 8, 8]}, + "faces": { + "north": {"uv": [7, 1.5, 10.5, 2], "texture": "#11"}, + "east": {"uv": [7, 2, 10.5, 2.5], "texture": "#11"}, + "south": {"uv": [7, 2.5, 10.5, 3], "texture": "#11"}, + "west": {"uv": [7, 3, 10.5, 3.5], "texture": "#11"}, + "up": {"uv": [7, 3.5, 3.5, 0], "texture": "#11"}, + "down": {"uv": [7, 3.5, 3.5, 7], "texture": "#11"} + } + }, + { + "from": [6.5, 18, 6.5], + "to": [9.5, 20, 9.5], + "rotation": {"angle": 45, "axis": "y", "origin": [8, 15, 8]}, + "faces": { + "north": {"uv": [7, 3.5, 8.5, 4.5], "texture": "#11"}, + "east": {"uv": [7, 3.5, 8.5, 4.5], "texture": "#11"}, + "south": {"uv": [7, 3.5, 8.5, 4.5], "texture": "#11"}, + "west": {"uv": [7, 3.5, 8.5, 4.5], "texture": "#11"}, + "up": {"uv": [1.5, 8.5, 0, 7], "texture": "#11"}, + "down": {"uv": [0, 7, 1.5, 8.5], "texture": "#11"} + } + }, + { + "from": [6.5, 9.5, 6.5], + "to": [9.5, 11.5, 9.5], + "rotation": {"angle": 45, "axis": "y", "origin": [8, 10.5, 8]}, + "faces": { + "north": {"uv": [7, 3.5, 8.5, 4.5], "texture": "#11"}, + "east": {"uv": [7, 3.5, 8.5, 4.5], "texture": "#11"}, + "south": {"uv": [7, 3.5, 8.5, 4.5], "texture": "#11"}, + "west": {"uv": [7, 3.5, 8.5, 4.5], "texture": "#11"}, + "up": {"uv": [0, 8.5, 1.5, 7], "texture": "#11"}, + "down": {"uv": [0, 8.5, 1.5, 7], "texture": "#11"} + } + }, + { + "from": [4.75, 11.5, 11.25], + "to": [11.25, 18, 4.75], + "rotation": {"angle": 45, "axis": "y", "origin": [8, 14.75, 8]}, + "faces": { + "north": {"uv": [0, 0, 3.25, 3.25], "texture": "#12"}, + "east": {"uv": [0, 0, 3.25, 3.25], "texture": "#12"}, + "south": {"uv": [0, 0, 3.25, 3.25], "texture": "#12"}, + "west": {"uv": [0, 0, 3.25, 3.25], "texture": "#12"}, + "up": {"uv": [3.25, 6.75, 0, 3.5], "texture": "#12"}, + "down": {"uv": [6.75, 0, 3.5, 3.25], "texture": "#12"} + } + }, + { + "from": [5, 11.75, 5], + "to": [11, 17.75, 11], + "rotation": {"angle": 45, "axis": "y", "origin": [8, 14.75, 8]}, + "faces": { + "north": {"uv": [3.5, 3.5, 6.5, 6.5], "texture": "#12"}, + "east": {"uv": [3.5, 3.5, 6.5, 6.5], "texture": "#12"}, + "south": {"uv": [3.5, 3.5, 6.5, 6.5], "texture": "#12"}, + "west": {"uv": [3.5, 3.5, 6.5, 6.5], "texture": "#12"}, + "up": {"uv": [6.5, 9.5, 3.5, 6.5], "texture": "#12"}, + "down": {"uv": [9.5, 3.5, 6.5, 6.5], "texture": "#12"} + } + } + ], + "gui_light": "front", + "display": { + "thirdperson_righthand": { + "translation": [0, 5, 1], + "scale": [0.55, 0.55, 0.55] + }, + "thirdperson_lefthand": { + "translation": [0, 5.5, 1], + "scale": [0.55, 0.55, 0.55] + }, + "firstperson_righthand": { + "rotation": [19, 16, 0], + "translation": [3.25, 7.25, 1.5], + "scale": [0.4, 0.4, 0.4] + }, + "firstperson_lefthand": { + "rotation": [9.64, 27.12, 0.81], + "translation": [4.5, 7.25, 0], + "scale": [0.4, 0.4, 0.4] + }, + "ground": { + "translation": [0, 3, 0], + "scale": [0.25, 0.25, 0.25] + }, + "gui": { + "rotation": [30, -135, 0], + "translation": [0, 1.75, 0], + "scale": [0.4, 0.4, 0.4] + }, + "head": { + "translation": [0, 15, 0] + }, + "fixed": { + "rotation": [90, -44.5, 90], + "scale": [0.5, 0.5, 0.5] + } + }, + "groups": [ + { + "name": "stick", + "origin": [8, 8, 8], + "color": 0, + "children": [ + { + "name": "stick_bot", + "origin": [8, 8, 8], + "color": 0, + "children": [0, 1, 2] + }, + { + "name": "stick_mid", + "origin": [8, 8, 8], + "color": 0, + "children": [3] + }, + { + "name": "stick_top", + "origin": [7, 0, 7], + "color": 0, + "children": [4] + } + ] + }, + { + "name": "tool_common", + "origin": [8, 8, 8], + "color": 0, + "children": [5, 6, 7, 8, 9] + }, + { + "name": "growing_layer", + "origin": [8, 8, 8], + "color": 0, + "children": [10, 11] + } + ], + "overrides": [ + { + "predicate": { + "custom_model_data": 1 + }, + "model": "useless_mod:item/endless_beaf_item_silk_touch" + } + ] +} \ No newline at end of file diff --git a/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_item_silk_touch.json b/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_item_silk_touch.json new file mode 100644 index 00000000..90e338f6 --- /dev/null +++ b/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_item_silk_touch.json @@ -0,0 +1,245 @@ +{ + "format_version": "1.21.6", + "credit": "Made with Blockbench,made by xia_bi418", + "texture_size": [32, 32], + "textures": { + "1": "useless_mod:item/beef_tool/stick", + "11": "useless_mod:item/beef_tool/common_base", + "12": "useless_mod:item/beef_tool/common_layer_silk", + "particle": "advanced_alloy_furnace" + }, + "elements": [ + { + "from": [6, -14, 6], + "to": [10, -9, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [8, -11.5, 8]}, + "faces": { + "north": {"uv": [1.5, 0, 3.5, 2.5], "texture": "#1"}, + "east": {"uv": [1.5, 2.5, 3.5, 5], "texture": "#1"}, + "south": {"uv": [3.5, 0, 5.5, 2.5], "texture": "#1"}, + "west": {"uv": [3.5, 2.5, 1.5, 5], "texture": "#1"}, + "up": {"uv": [5.5, 4.5, 3.5, 2.5], "texture": "#1"}, + "down": {"uv": [5.5, 4.5, 3.5, 6.5], "texture": "#1"} + } + }, + { + "from": [6.025, -15.25, 6.25], + "to": [9.975, -12.45, 7.775], + "rotation": {"angle": -45, "axis": "x", "origin": [8, -13.5, 8.025]}, + "faces": { + "north": {"uv": [3.5, 6.5, 5.5, 8], "texture": "#1"}, + "east": {"uv": [7.5, 7.5, 8.5, 9], "texture": "#1"}, + "south": {"uv": [0, 7, 2, 8.5], "texture": "#1"}, + "west": {"uv": [8.5, 7.5, 7.5, 9], "texture": "#1"}, + "up": {"uv": [9.5, 2.5, 7.5, 1.5], "texture": "#1"}, + "down": {"uv": [9.5, 2.5, 7.5, 3.5], "texture": "#1"} + } + }, + { + "from": [6, -16, 8], + "to": [10, -14, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [8, -15, 9]}, + "faces": { + "north": {"uv": [7.5, 3.5, 9.5, 4.5], "texture": "#1"}, + "east": {"uv": [3.5, 8, 4.5, 9], "texture": "#1"}, + "south": {"uv": [7.5, 4.5, 9.5, 5.5], "texture": "#1"}, + "west": {"uv": [4.5, 8, 3.5, 9], "texture": "#1"}, + "up": {"uv": [9.5, 6.5, 7.5, 5.5], "texture": "#1"}, + "down": {"uv": [9.5, 6.5, 7.5, 7.5], "texture": "#1"} + } + }, + { + "from": [6.5, -9, 6.5], + "to": [9.5, 5, 9.5], + "rotation": {"angle": 0, "axis": "y", "origin": [7, -9, 7]}, + "faces": { + "north": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "east": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "south": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "west": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "up": {"uv": [3.5, 8.5, 2, 7], "texture": "#1"}, + "down": {"uv": [9, 0, 7.5, 1.5], "texture": "#1"} + } + }, + { + "from": [6, 5, 6], + "to": [10, 9, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 5, 7]}, + "faces": { + "north": {"uv": [1.5, 5, 3.5, 7], "texture": "#1"}, + "east": {"uv": [5.5, 0, 7.5, 2], "texture": "#1"}, + "south": {"uv": [5.5, 2, 7.5, 4], "texture": "#1"}, + "west": {"uv": [7.5, 0, 5.5, 2], "texture": "#1"}, + "up": {"uv": [7.5, 6, 5.5, 4], "texture": "#1"}, + "down": {"uv": [7.5, 6, 5.5, 8], "texture": "#1"} + } + }, + { + "from": [7, 9, 7], + "to": [9, 10, 9], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 9, 7]}, + "faces": { + "north": {"uv": [7, 5.5, 8, 6], "texture": "#11"}, + "east": {"uv": [7, 5.5, 8, 6], "texture": "#11"}, + "south": {"uv": [8, 5.5, 7, 6], "texture": "#11"}, + "west": {"uv": [7, 5.5, 8, 6], "texture": "#11"}, + "up": {"uv": [8, 5.5, 7, 4.5], "texture": "#11"}, + "down": {"uv": [6, 7, 5, 8], "texture": "#11"} + } + }, + { + "from": [4.5, 17.5, 4.5], + "to": [11.5, 18.5, 11.5], + "rotation": {"angle": 45, "axis": "y", "origin": [8, 14.5, 8]}, + "faces": { + "north": {"uv": [7, 0, 10.5, 0.5], "texture": "#11"}, + "east": {"uv": [7, 0.5, 10.5, 1], "texture": "#11"}, + "south": {"uv": [7, 1, 10.5, 1.5], "texture": "#11"}, + "west": {"uv": [1.5, 7, 5, 7.5], "texture": "#11"}, + "up": {"uv": [3.5, 3.5, 0, 0], "texture": "#11"}, + "down": {"uv": [3.5, 3.5, 0, 7], "texture": "#11"} + } + }, + { + "from": [4.5, 11, 4.5], + "to": [11.5, 12, 11.5], + "rotation": {"angle": 45, "axis": "y", "origin": [8, 8, 8]}, + "faces": { + "north": {"uv": [7, 1.5, 10.5, 2], "texture": "#11"}, + "east": {"uv": [7, 2, 10.5, 2.5], "texture": "#11"}, + "south": {"uv": [7, 2.5, 10.5, 3], "texture": "#11"}, + "west": {"uv": [7, 3, 10.5, 3.5], "texture": "#11"}, + "up": {"uv": [7, 3.5, 3.5, 0], "texture": "#11"}, + "down": {"uv": [7, 3.5, 3.5, 7], "texture": "#11"} + } + }, + { + "from": [6.5, 18, 6.5], + "to": [9.5, 20, 9.5], + "rotation": {"angle": 45, "axis": "y", "origin": [8, 15, 8]}, + "faces": { + "north": {"uv": [7, 3.5, 8.5, 4.5], "texture": "#11"}, + "east": {"uv": [7, 3.5, 8.5, 4.5], "texture": "#11"}, + "south": {"uv": [7, 3.5, 8.5, 4.5], "texture": "#11"}, + "west": {"uv": [7, 3.5, 8.5, 4.5], "texture": "#11"}, + "up": {"uv": [1.5, 8.5, 0, 7], "texture": "#11"}, + "down": {"uv": [0, 7, 1.5, 8.5], "texture": "#11"} + } + }, + { + "from": [6.5, 9.5, 6.5], + "to": [9.5, 11.5, 9.5], + "rotation": {"angle": 45, "axis": "y", "origin": [8, 10.5, 8]}, + "faces": { + "north": {"uv": [7, 3.5, 8.5, 4.5], "texture": "#11"}, + "east": {"uv": [7, 3.5, 8.5, 4.5], "texture": "#11"}, + "south": {"uv": [7, 3.5, 8.5, 4.5], "texture": "#11"}, + "west": {"uv": [7, 3.5, 8.5, 4.5], "texture": "#11"}, + "up": {"uv": [0, 8.5, 1.5, 7], "texture": "#11"}, + "down": {"uv": [0, 8.5, 1.5, 7], "texture": "#11"} + } + }, + { + "from": [4.75, 11.5, 11.25], + "to": [11.25, 18, 4.75], + "rotation": {"angle": 45, "axis": "y", "origin": [8, 14.75, 8]}, + "faces": { + "north": {"uv": [0, 0, 3.25, 3.25], "texture": "#12"}, + "east": {"uv": [0, 0, 3.25, 3.25], "texture": "#12"}, + "south": {"uv": [0, 0, 3.25, 3.25], "texture": "#12"}, + "west": {"uv": [0, 0, 3.25, 3.25], "texture": "#12"}, + "up": {"uv": [3.25, 6.75, 0, 3.5], "texture": "#12"}, + "down": {"uv": [6.75, 0, 3.5, 3.25], "texture": "#12"} + } + }, + { + "from": [5, 11.75, 5], + "to": [11, 17.75, 11], + "rotation": {"angle": 45, "axis": "y", "origin": [8, 14.75, 8]}, + "faces": { + "north": {"uv": [3.5, 3.5, 6.5, 6.5], "texture": "#12"}, + "east": {"uv": [3.5, 3.5, 6.5, 6.5], "texture": "#12"}, + "south": {"uv": [3.5, 3.5, 6.5, 6.5], "texture": "#12"}, + "west": {"uv": [3.5, 3.5, 6.5, 6.5], "texture": "#12"}, + "up": {"uv": [6.5, 9.5, 3.5, 6.5], "texture": "#12"}, + "down": {"uv": [9.5, 3.5, 6.5, 6.5], "texture": "#12"} + } + } + ], + "gui_light": "front", + "display": { + "thirdperson_righthand": { + "translation": [0, 5, 1], + "scale": [0.55, 0.55, 0.55] + }, + "thirdperson_lefthand": { + "translation": [0, 5.5, 1], + "scale": [0.55, 0.55, 0.55] + }, + "firstperson_righthand": { + "rotation": [19, 16, 0], + "translation": [3.25, 7.25, 1.5], + "scale": [0.4, 0.4, 0.4] + }, + "firstperson_lefthand": { + "rotation": [9.64, 27.12, 0.81], + "translation": [4.5, 7.25, 0], + "scale": [0.4, 0.4, 0.4] + }, + "ground": { + "translation": [0, 3, 0], + "scale": [0.25, 0.25, 0.25] + }, + "gui": { + "rotation": [30, -135, 0], + "translation": [0, 1.75, 0], + "scale": [0.4, 0.4, 0.4] + }, + "head": { + "translation": [0, 15, 0] + }, + "fixed": { + "rotation": [90, -44.5, 90], + "scale": [0.5, 0.5, 0.5] + } + }, + "groups": [ + { + "name": "stick", + "origin": [8, 8, 8], + "color": 0, + "children": [ + { + "name": "stick_bot", + "origin": [8, 8, 8], + "color": 0, + "children": [0, 1, 2] + }, + { + "name": "stick_mid", + "origin": [8, 8, 8], + "color": 0, + "children": [3] + }, + { + "name": "stick_top", + "origin": [7, 0, 7], + "color": 0, + "children": [4] + } + ] + }, + { + "name": "tool_common", + "origin": [8, 8, 8], + "color": 0, + "children": [5, 6, 7, 8, 9] + }, + { + "name": "growing_layer", + "origin": [8, 8, 8], + "color": 0, + "children": [10, 11] + } + ] +} \ No newline at end of file diff --git a/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_mallet.json b/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_mallet.json new file mode 100644 index 00000000..beec7ef2 --- /dev/null +++ b/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_mallet.json @@ -0,0 +1,194 @@ +{ + "format_version": "1.21.6", + "credit": "Made with Blockbench,made by xia_bi418", + "texture_size": [32, 32], + "textures": { + "1": "useless_mod:item/beef_tool/stick", + "7": "useless_mod:item/beef_tool/mallet", + "particle": "advanced_alloy_furnace" + }, + "elements": [ + { + "from": [6, -14, 6], + "to": [10, -9, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [8, -11.5, 8]}, + "faces": { + "north": {"uv": [1.5, 0, 3.5, 2.5], "texture": "#1"}, + "east": {"uv": [1.5, 2.5, 3.5, 5], "texture": "#1"}, + "south": {"uv": [3.5, 0, 5.5, 2.5], "texture": "#1"}, + "west": {"uv": [3.5, 2.5, 1.5, 5], "texture": "#1"}, + "up": {"uv": [5.5, 4.5, 3.5, 2.5], "texture": "#1"}, + "down": {"uv": [5.5, 4.5, 3.5, 6.5], "texture": "#1"} + } + }, + { + "from": [6.025, -15.25, 6.25], + "to": [9.975, -12.45, 7.775], + "rotation": {"angle": -45, "axis": "x", "origin": [8, -13.5, 8.025]}, + "faces": { + "north": {"uv": [3.5, 6.5, 5.5, 8], "texture": "#1"}, + "east": {"uv": [7.5, 7.5, 8.5, 9], "texture": "#1"}, + "south": {"uv": [0, 7, 2, 8.5], "texture": "#1"}, + "west": {"uv": [8.5, 7.5, 7.5, 9], "texture": "#1"}, + "up": {"uv": [9.5, 2.5, 7.5, 1.5], "texture": "#1"}, + "down": {"uv": [9.5, 2.5, 7.5, 3.5], "texture": "#1"} + } + }, + { + "from": [6, -16, 8], + "to": [10, -14, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [8, -15, 9]}, + "faces": { + "north": {"uv": [7.5, 3.5, 9.5, 4.5], "texture": "#1"}, + "east": {"uv": [3.5, 8, 4.5, 9], "texture": "#1"}, + "south": {"uv": [7.5, 4.5, 9.5, 5.5], "texture": "#1"}, + "west": {"uv": [4.5, 8, 3.5, 9], "texture": "#1"}, + "up": {"uv": [9.5, 6.5, 7.5, 5.5], "texture": "#1"}, + "down": {"uv": [9.5, 6.5, 7.5, 7.5], "texture": "#1"} + } + }, + { + "from": [6.5, -9, 6.5], + "to": [9.5, 5, 9.5], + "rotation": {"angle": 0, "axis": "y", "origin": [7, -9, 7]}, + "faces": { + "north": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "east": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "south": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "west": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "up": {"uv": [3.5, 8.5, 2, 7], "texture": "#1"}, + "down": {"uv": [9, 0, 7.5, 1.5], "texture": "#1"} + } + }, + { + "from": [6, 5, 6], + "to": [10, 9, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 5, 7]}, + "faces": { + "north": {"uv": [1.5, 5, 3.5, 7], "texture": "#1"}, + "east": {"uv": [5.5, 0, 7.5, 2], "texture": "#1"}, + "south": {"uv": [5.5, 2, 7.5, 4], "texture": "#1"}, + "west": {"uv": [7.5, 0, 5.5, 2], "texture": "#1"}, + "up": {"uv": [7.5, 6, 5.5, 4], "texture": "#1"}, + "down": {"uv": [7.5, 6, 5.5, 8], "texture": "#1"} + } + }, + { + "from": [6.5, 9, 6.5], + "to": [9.5, 11, 9.5], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 9, 7]}, + "faces": { + "north": {"uv": [0, 9.5, 1.5, 10.5], "texture": "#7"}, + "east": {"uv": [1.5, 9.5, 3, 10.5], "texture": "#7"}, + "south": {"uv": [3, 9.5, 4.5, 10.5], "texture": "#7"}, + "west": {"uv": [4.5, 10, 6, 11], "texture": "#7"}, + "up": {"uv": [10.5, 8.5, 9, 7], "texture": "#7"}, + "down": {"uv": [10.5, 8.5, 9, 10], "texture": "#7"} + } + }, + { + "from": [6, 11, 3.25], + "to": [10, 16.5, 12.5], + "rotation": {"angle": 0, "axis": "y", "origin": [8, 13.5, 7.5]}, + "faces": { + "north": {"uv": [7, 7, 9, 10], "texture": "#7"}, + "east": {"uv": [5, 3, 0, 6], "texture": "#7"}, + "south": {"uv": [9, 0, 11, 3], "texture": "#7"}, + "west": {"uv": [0, 3, 5, 6], "texture": "#7"}, + "up": {"uv": [7, 5, 5, 0], "texture": "#7"}, + "down": {"uv": [7, 5, 5, 10], "texture": "#7"} + } + }, + { + "from": [5.6, 10.5, 3.2], + "to": [10.35, 17, 6.95], + "rotation": {"angle": 0, "axis": "y", "origin": [7.85, 13.5, 8.2]}, + "faces": { + "north": {"uv": [0, 6, 2.5, 9.5], "texture": "#7"}, + "east": {"uv": [9, 3.5, 7, 7], "texture": "#7"}, + "south": {"uv": [2.5, 6, 5, 9.5], "texture": "#7"}, + "west": {"uv": [7, 3.5, 9, 7], "texture": "#7"}, + "up": {"uv": [11.5, 5, 9, 3], "texture": "#7"}, + "down": {"uv": [11.5, 5, 9, 7], "texture": "#7"} + } + } + ], + "gui_light": "front", + "display": { + "thirdperson_righthand": { + "translation": [0, 5, 1], + "scale": [0.55, 0.55, 0.55] + }, + "thirdperson_lefthand": { + "translation": [0, 5.5, 1], + "scale": [0.55, 0.55, 0.55] + }, + "firstperson_righthand": { + "rotation": [19, 16, 0], + "translation": [3.25, 7.25, 1.5], + "scale": [0.4, 0.4, 0.4] + }, + "firstperson_lefthand": { + "rotation": [9.64, 27.12, 0.81], + "translation": [4.5, 7.25, 0], + "scale": [0.4, 0.4, 0.4] + }, + "ground": { + "translation": [0, 3, 0], + "scale": [0.25, 0.25, 0.25] + }, + "gui": { + "rotation": [30, -135, 0], + "translation": [0, 1.75, 0], + "scale": [0.4, 0.4, 0.4] + }, + "head": { + "translation": [0, 15, 0] + }, + "fixed": { + "rotation": [90, -44.5, 90], + "scale": [0.5, 0.5, 0.5] + } + }, + "groups": [ + { + "name": "stick", + "origin": [8, 8, 8], + "color": 0, + "children": [ + { + "name": "stick_bot", + "origin": [8, 8, 8], + "color": 0, + "children": [0, 1, 2] + }, + { + "name": "stick_mid", + "origin": [8, 8, 8], + "color": 0, + "children": [3] + }, + { + "name": "stick_top", + "origin": [7, 0, 7], + "color": 0, + "children": [4] + } + ] + }, + { + "name": "tool_mallet", + "origin": [8, 8, 8], + "color": 0, + "children": [5, 6, 7] + } + ], + "overrides": [ + { + "predicate": { + "custom_model_data": 1 + }, + "model": "useless_mod:item/endless_beaf_mallet_silk" + } + ] +} \ No newline at end of file diff --git a/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_mallet_silk.json b/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_mallet_silk.json new file mode 100644 index 00000000..a25ecd68 --- /dev/null +++ b/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_mallet_silk.json @@ -0,0 +1,186 @@ +{ + "format_version": "1.21.6", + "credit": "Made with Blockbench,made by xia_bi418", + "texture_size": [32, 32], + "textures": { + "1": "useless_mod:item/beef_tool/stick", + "7": "useless_mod:item/beef_tool/mallet_silk", + "particle": "advanced_alloy_furnace" + }, + "elements": [ + { + "from": [6, -14, 6], + "to": [10, -9, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [8, -11.5, 8]}, + "faces": { + "north": {"uv": [1.5, 0, 3.5, 2.5], "texture": "#1"}, + "east": {"uv": [1.5, 2.5, 3.5, 5], "texture": "#1"}, + "south": {"uv": [3.5, 0, 5.5, 2.5], "texture": "#1"}, + "west": {"uv": [3.5, 2.5, 1.5, 5], "texture": "#1"}, + "up": {"uv": [5.5, 4.5, 3.5, 2.5], "texture": "#1"}, + "down": {"uv": [5.5, 4.5, 3.5, 6.5], "texture": "#1"} + } + }, + { + "from": [6.025, -15.25, 6.25], + "to": [9.975, -12.45, 7.775], + "rotation": {"angle": -45, "axis": "x", "origin": [8, -13.5, 8.025]}, + "faces": { + "north": {"uv": [3.5, 6.5, 5.5, 8], "texture": "#1"}, + "east": {"uv": [7.5, 7.5, 8.5, 9], "texture": "#1"}, + "south": {"uv": [0, 7, 2, 8.5], "texture": "#1"}, + "west": {"uv": [8.5, 7.5, 7.5, 9], "texture": "#1"}, + "up": {"uv": [9.5, 2.5, 7.5, 1.5], "texture": "#1"}, + "down": {"uv": [9.5, 2.5, 7.5, 3.5], "texture": "#1"} + } + }, + { + "from": [6, -16, 8], + "to": [10, -14, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [8, -15, 9]}, + "faces": { + "north": {"uv": [7.5, 3.5, 9.5, 4.5], "texture": "#1"}, + "east": {"uv": [3.5, 8, 4.5, 9], "texture": "#1"}, + "south": {"uv": [7.5, 4.5, 9.5, 5.5], "texture": "#1"}, + "west": {"uv": [4.5, 8, 3.5, 9], "texture": "#1"}, + "up": {"uv": [9.5, 6.5, 7.5, 5.5], "texture": "#1"}, + "down": {"uv": [9.5, 6.5, 7.5, 7.5], "texture": "#1"} + } + }, + { + "from": [6.5, -9, 6.5], + "to": [9.5, 5, 9.5], + "rotation": {"angle": 0, "axis": "y", "origin": [7, -9, 7]}, + "faces": { + "north": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "east": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "south": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "west": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "up": {"uv": [3.5, 8.5, 2, 7], "texture": "#1"}, + "down": {"uv": [9, 0, 7.5, 1.5], "texture": "#1"} + } + }, + { + "from": [6, 5, 6], + "to": [10, 9, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 5, 7]}, + "faces": { + "north": {"uv": [1.5, 5, 3.5, 7], "texture": "#1"}, + "east": {"uv": [5.5, 0, 7.5, 2], "texture": "#1"}, + "south": {"uv": [5.5, 2, 7.5, 4], "texture": "#1"}, + "west": {"uv": [7.5, 0, 5.5, 2], "texture": "#1"}, + "up": {"uv": [7.5, 6, 5.5, 4], "texture": "#1"}, + "down": {"uv": [7.5, 6, 5.5, 8], "texture": "#1"} + } + }, + { + "from": [6.5, 9, 6.5], + "to": [9.5, 11, 9.5], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 9, 7]}, + "faces": { + "north": {"uv": [0, 9.5, 1.5, 10.5], "texture": "#7"}, + "east": {"uv": [1.5, 9.5, 3, 10.5], "texture": "#7"}, + "south": {"uv": [3, 9.5, 4.5, 10.5], "texture": "#7"}, + "west": {"uv": [4.5, 10, 6, 11], "texture": "#7"}, + "up": {"uv": [10.5, 8.5, 9, 7], "texture": "#7"}, + "down": {"uv": [10.5, 8.5, 9, 10], "texture": "#7"} + } + }, + { + "from": [6, 11, 3.25], + "to": [10, 16.5, 12.5], + "rotation": {"angle": 0, "axis": "y", "origin": [8, 13.5, 7.5]}, + "faces": { + "north": {"uv": [7, 7, 9, 10], "texture": "#7"}, + "east": {"uv": [5, 3, 0, 6], "texture": "#7"}, + "south": {"uv": [9, 0, 11, 3], "texture": "#7"}, + "west": {"uv": [0, 3, 5, 6], "texture": "#7"}, + "up": {"uv": [7, 5, 5, 0], "texture": "#7"}, + "down": {"uv": [7, 5, 5, 10], "texture": "#7"} + } + }, + { + "from": [5.6, 10.5, 3.2], + "to": [10.35, 17, 6.95], + "rotation": {"angle": 0, "axis": "y", "origin": [7.85, 13.5, 8.2]}, + "faces": { + "north": {"uv": [0, 6, 2.5, 9.5], "texture": "#7"}, + "east": {"uv": [9, 3.5, 7, 7], "texture": "#7"}, + "south": {"uv": [2.5, 6, 5, 9.5], "texture": "#7"}, + "west": {"uv": [7, 3.5, 9, 7], "texture": "#7"}, + "up": {"uv": [11.5, 5, 9, 3], "texture": "#7"}, + "down": {"uv": [11.5, 5, 9, 7], "texture": "#7"} + } + } + ], + "gui_light": "front", + "display": { + "thirdperson_righthand": { + "translation": [0, 5, 1], + "scale": [0.55, 0.55, 0.55] + }, + "thirdperson_lefthand": { + "translation": [0, 5.5, 1], + "scale": [0.55, 0.55, 0.55] + }, + "firstperson_righthand": { + "rotation": [19, 16, 0], + "translation": [3.25, 7.25, 1.5], + "scale": [0.4, 0.4, 0.4] + }, + "firstperson_lefthand": { + "rotation": [9.64, 27.12, 0.81], + "translation": [4.5, 7.25, 0], + "scale": [0.4, 0.4, 0.4] + }, + "ground": { + "translation": [0, 3, 0], + "scale": [0.25, 0.25, 0.25] + }, + "gui": { + "rotation": [30, -135, 0], + "translation": [0, 1.75, 0], + "scale": [0.4, 0.4, 0.4] + }, + "head": { + "translation": [0, 15, 0] + }, + "fixed": { + "rotation": [90, -44.5, 90], + "scale": [0.5, 0.5, 0.5] + } + }, + "groups": [ + { + "name": "stick", + "origin": [8, 8, 8], + "color": 0, + "children": [ + { + "name": "stick_bot", + "origin": [8, 8, 8], + "color": 0, + "children": [0, 1, 2] + }, + { + "name": "stick_mid", + "origin": [8, 8, 8], + "color": 0, + "children": [3] + }, + { + "name": "stick_top", + "origin": [7, 0, 7], + "color": 0, + "children": [4] + } + ] + }, + { + "name": "tool_mallet", + "origin": [8, 8, 8], + "color": 0, + "children": [5, 6, 7] + } + ] +} \ No newline at end of file diff --git a/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_screwdriver.json b/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_screwdriver.json new file mode 100644 index 00000000..b95289a3 --- /dev/null +++ b/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_screwdriver.json @@ -0,0 +1,207 @@ +{ + "format_version": "1.21.6", + "credit": "Made with Blockbench,made by xia_bi418", + "texture_size": [32, 32], + "textures": { + "1": "useless_mod:item/beef_tool/stick", + "4": "useless_mod:item/beef_tool/screwdriver", + "particle": "advanced_alloy_furnace" + }, + "elements": [ + { + "from": [6, -14, 6], + "to": [10, -9, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [8, -11.5, 8]}, + "faces": { + "north": {"uv": [1.5, 0, 3.5, 2.5], "texture": "#1"}, + "east": {"uv": [1.5, 2.5, 3.5, 5], "texture": "#1"}, + "south": {"uv": [3.5, 0, 5.5, 2.5], "texture": "#1"}, + "west": {"uv": [3.5, 2.5, 1.5, 5], "texture": "#1"}, + "up": {"uv": [5.5, 4.5, 3.5, 2.5], "texture": "#1"}, + "down": {"uv": [5.5, 4.5, 3.5, 6.5], "texture": "#1"} + } + }, + { + "from": [6.025, -15.25, 6.25], + "to": [9.975, -12.45, 7.775], + "rotation": {"angle": -45, "axis": "x", "origin": [8, -13.5, 8.025]}, + "faces": { + "north": {"uv": [3.5, 6.5, 5.5, 8], "texture": "#1"}, + "east": {"uv": [7.5, 7.5, 8.5, 9], "texture": "#1"}, + "south": {"uv": [0, 7, 2, 8.5], "texture": "#1"}, + "west": {"uv": [8.5, 7.5, 7.5, 9], "texture": "#1"}, + "up": {"uv": [9.5, 2.5, 7.5, 1.5], "texture": "#1"}, + "down": {"uv": [9.5, 2.5, 7.5, 3.5], "texture": "#1"} + } + }, + { + "from": [6, -16, 8], + "to": [10, -14, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [8, -15, 9]}, + "faces": { + "north": {"uv": [7.5, 3.5, 9.5, 4.5], "texture": "#1"}, + "east": {"uv": [3.5, 8, 4.5, 9], "texture": "#1"}, + "south": {"uv": [7.5, 4.5, 9.5, 5.5], "texture": "#1"}, + "west": {"uv": [4.5, 8, 3.5, 9], "texture": "#1"}, + "up": {"uv": [9.5, 6.5, 7.5, 5.5], "texture": "#1"}, + "down": {"uv": [9.5, 6.5, 7.5, 7.5], "texture": "#1"} + } + }, + { + "from": [6.5, -9, 6.5], + "to": [9.5, 5, 9.5], + "rotation": {"angle": 0, "axis": "y", "origin": [7, -9, 7]}, + "faces": { + "north": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "east": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "south": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "west": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "up": {"uv": [3.5, 8.5, 2, 7], "texture": "#1"}, + "down": {"uv": [9, 0, 7.5, 1.5], "texture": "#1"} + } + }, + { + "from": [6, 5, 6], + "to": [10, 9, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 5, 7]}, + "faces": { + "north": {"uv": [1.5, 5, 3.5, 7], "texture": "#1"}, + "east": {"uv": [5.5, 0, 7.5, 2], "texture": "#1"}, + "south": {"uv": [5.5, 2, 7.5, 4], "texture": "#1"}, + "west": {"uv": [7.5, 0, 5.5, 2], "texture": "#1"}, + "up": {"uv": [7.5, 6, 5.5, 4], "texture": "#1"}, + "down": {"uv": [7.5, 6, 5.5, 8], "texture": "#1"} + } + }, + { + "from": [4, 9, 3.5], + "to": [12, 17, 12.5], + "rotation": {"angle": 0, "axis": "y", "origin": [8, 12, 8]}, + "faces": { + "north": {"uv": [4.5, 0, 8.5, 4], "texture": "#4"}, + "east": {"uv": [0, 0, 4.5, 4], "texture": "#4"}, + "south": {"uv": [8, 4, 12, 8], "texture": "#4"}, + "west": {"uv": [4.5, 0, 0, 4], "texture": "#4"}, + "up": {"uv": [4, 8.5, 0, 4], "texture": "#4"}, + "down": {"uv": [8, 4, 4, 8.5], "texture": "#4"} + } + }, + { + "from": [5, 10, 12.5], + "to": [11, 16, 13.5], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 9, 10.5]}, + "faces": { + "north": {"uv": [8, 8, 11, 11], "texture": "#4"}, + "east": {"uv": [9, 11, 9.5, 14], "texture": "#4"}, + "south": {"uv": [0, 8.5, 3, 11.5], "texture": "#4"}, + "west": {"uv": [9, 11, 9.5, 14], "texture": "#4"}, + "up": {"uv": [14, 9.5, 11, 9], "texture": "#4"}, + "down": {"uv": [12.5, 11, 9.5, 11.5], "texture": "#4"} + } + }, + { + "from": [5, 10, 1.5], + "to": [11, 16, 3.5], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 9, -0.5]}, + "faces": { + "north": {"uv": [8.5, 0, 11.5, 3], "texture": "#4"}, + "east": {"uv": [8, 11, 9, 14], "texture": "#4"}, + "south": {"uv": [3, 8.5, 6, 11.5], "texture": "#4"}, + "west": {"uv": [8, 11, 9, 14], "texture": "#4"}, + "up": {"uv": [14, 9, 11, 8], "texture": "#4"}, + "down": {"uv": [11, 8, 14, 9], "texture": "#4"} + } + }, + { + "from": [7, 12, -7.5], + "to": [9, 14, 1.5], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 9, -2.5]}, + "faces": { + "north": {"uv": [11, 9.5, 12, 10.5], "texture": "#4"}, + "east": {"uv": [8.5, 3, 13, 4], "texture": "#4"}, + "south": {"uv": [0, 11.5, 1, 12.5], "texture": "#4"}, + "west": {"uv": [13, 3, 8.5, 4], "texture": "#4"}, + "up": {"uv": [7, 13, 6, 8.5], "texture": "#4"}, + "down": {"uv": [8, 8.5, 7, 13], "texture": "#4"} + } + } + ], + "gui_light": "front", + "display": { + "thirdperson_righthand": { + "translation": [0, 5, 1], + "scale": [0.55, 0.55, 0.55] + }, + "thirdperson_lefthand": { + "translation": [0, 5.5, 1], + "scale": [0.55, 0.55, 0.55] + }, + "firstperson_righthand": { + "rotation": [19, 16, 0], + "translation": [3.25, 7.25, 1.5], + "scale": [0.4, 0.4, 0.4] + }, + "firstperson_lefthand": { + "rotation": [9.64, 27.12, 0.81], + "translation": [4.5, 7.25, 0], + "scale": [0.4, 0.4, 0.4] + }, + "ground": { + "translation": [0, 3, 0], + "scale": [0.25, 0.25, 0.25] + }, + "gui": { + "rotation": [30, -135, 0], + "translation": [0, 1.75, 0], + "scale": [0.4, 0.4, 0.4] + }, + "head": { + "translation": [0, 15, 0] + }, + "fixed": { + "rotation": [90, -44.5, 90], + "scale": [0.5, 0.5, 0.5] + } + }, + "groups": [ + { + "name": "stick", + "origin": [8, 8, 8], + "color": 0, + "children": [ + { + "name": "stick_bot", + "origin": [8, 8, 8], + "color": 0, + "children": [0, 1, 2] + }, + { + "name": "stick_mid", + "origin": [8, 8, 8], + "color": 0, + "children": [3] + }, + { + "name": "stick_top", + "origin": [7, 0, 7], + "color": 0, + "children": [4] + } + ] + }, + { + "name": "tool_screwdriver", + "origin": [8, 8, 8], + "color": 0, + "children": [5, 6, 7, 8] + } + ], + "overrides": [ + { + "predicate": { + "custom_model_data": 1 + }, + "model": "useless_mod:item/endless_beaf_screwdriver_silk" + } + ] +} \ No newline at end of file diff --git a/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_screwdriver_silk.json b/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_screwdriver_silk.json new file mode 100644 index 00000000..668f2a05 --- /dev/null +++ b/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_screwdriver_silk.json @@ -0,0 +1,199 @@ +{ + "format_version": "1.21.6", + "credit": "Made with Blockbench,made by xia_bi418", + "texture_size": [32, 32], + "textures": { + "1": "useless_mod:item/beef_tool/stick", + "4": "useless_mod:item/beef_tool/screwdriver_silk", + "particle": "advanced_alloy_furnace" + }, + "elements": [ + { + "from": [6, -14, 6], + "to": [10, -9, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [8, -11.5, 8]}, + "faces": { + "north": {"uv": [1.5, 0, 3.5, 2.5], "texture": "#1"}, + "east": {"uv": [1.5, 2.5, 3.5, 5], "texture": "#1"}, + "south": {"uv": [3.5, 0, 5.5, 2.5], "texture": "#1"}, + "west": {"uv": [3.5, 2.5, 1.5, 5], "texture": "#1"}, + "up": {"uv": [5.5, 4.5, 3.5, 2.5], "texture": "#1"}, + "down": {"uv": [5.5, 4.5, 3.5, 6.5], "texture": "#1"} + } + }, + { + "from": [6.025, -15.25, 6.25], + "to": [9.975, -12.45, 7.775], + "rotation": {"angle": -45, "axis": "x", "origin": [8, -13.5, 8.025]}, + "faces": { + "north": {"uv": [3.5, 6.5, 5.5, 8], "texture": "#1"}, + "east": {"uv": [7.5, 7.5, 8.5, 9], "texture": "#1"}, + "south": {"uv": [0, 7, 2, 8.5], "texture": "#1"}, + "west": {"uv": [8.5, 7.5, 7.5, 9], "texture": "#1"}, + "up": {"uv": [9.5, 2.5, 7.5, 1.5], "texture": "#1"}, + "down": {"uv": [9.5, 2.5, 7.5, 3.5], "texture": "#1"} + } + }, + { + "from": [6, -16, 8], + "to": [10, -14, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [8, -15, 9]}, + "faces": { + "north": {"uv": [7.5, 3.5, 9.5, 4.5], "texture": "#1"}, + "east": {"uv": [3.5, 8, 4.5, 9], "texture": "#1"}, + "south": {"uv": [7.5, 4.5, 9.5, 5.5], "texture": "#1"}, + "west": {"uv": [4.5, 8, 3.5, 9], "texture": "#1"}, + "up": {"uv": [9.5, 6.5, 7.5, 5.5], "texture": "#1"}, + "down": {"uv": [9.5, 6.5, 7.5, 7.5], "texture": "#1"} + } + }, + { + "from": [6.5, -9, 6.5], + "to": [9.5, 5, 9.5], + "rotation": {"angle": 0, "axis": "y", "origin": [7, -9, 7]}, + "faces": { + "north": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "east": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "south": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "west": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "up": {"uv": [3.5, 8.5, 2, 7], "texture": "#1"}, + "down": {"uv": [9, 0, 7.5, 1.5], "texture": "#1"} + } + }, + { + "from": [6, 5, 6], + "to": [10, 9, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 5, 7]}, + "faces": { + "north": {"uv": [1.5, 5, 3.5, 7], "texture": "#1"}, + "east": {"uv": [5.5, 0, 7.5, 2], "texture": "#1"}, + "south": {"uv": [5.5, 2, 7.5, 4], "texture": "#1"}, + "west": {"uv": [7.5, 0, 5.5, 2], "texture": "#1"}, + "up": {"uv": [7.5, 6, 5.5, 4], "texture": "#1"}, + "down": {"uv": [7.5, 6, 5.5, 8], "texture": "#1"} + } + }, + { + "from": [4, 9, 3.5], + "to": [12, 17, 12.5], + "rotation": {"angle": 0, "axis": "y", "origin": [8, 12, 8]}, + "faces": { + "north": {"uv": [4.5, 0, 8.5, 4], "texture": "#4"}, + "east": {"uv": [0, 0, 4.5, 4], "texture": "#4"}, + "south": {"uv": [8, 4, 12, 8], "texture": "#4"}, + "west": {"uv": [4.5, 0, 0, 4], "texture": "#4"}, + "up": {"uv": [4, 8.5, 0, 4], "texture": "#4"}, + "down": {"uv": [8, 4, 4, 8.5], "texture": "#4"} + } + }, + { + "from": [5, 10, 12.5], + "to": [11, 16, 13.5], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 9, 10.5]}, + "faces": { + "north": {"uv": [8, 8, 11, 11], "texture": "#4"}, + "east": {"uv": [9, 11, 9.5, 14], "texture": "#4"}, + "south": {"uv": [0, 8.5, 3, 11.5], "texture": "#4"}, + "west": {"uv": [9, 11, 9.5, 14], "texture": "#4"}, + "up": {"uv": [14, 9.5, 11, 9], "texture": "#4"}, + "down": {"uv": [12.5, 11, 9.5, 11.5], "texture": "#4"} + } + }, + { + "from": [5, 10, 1.5], + "to": [11, 16, 3.5], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 9, -0.5]}, + "faces": { + "north": {"uv": [8.5, 0, 11.5, 3], "texture": "#4"}, + "east": {"uv": [8, 11, 9, 14], "texture": "#4"}, + "south": {"uv": [3, 8.5, 6, 11.5], "texture": "#4"}, + "west": {"uv": [8, 11, 9, 14], "texture": "#4"}, + "up": {"uv": [14, 9, 11, 8], "texture": "#4"}, + "down": {"uv": [11, 8, 14, 9], "texture": "#4"} + } + }, + { + "from": [7, 12, -7.5], + "to": [9, 14, 1.5], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 9, -2.5]}, + "faces": { + "north": {"uv": [11, 9.5, 12, 10.5], "texture": "#4"}, + "east": {"uv": [8.5, 3, 13, 4], "texture": "#4"}, + "south": {"uv": [0, 11.5, 1, 12.5], "texture": "#4"}, + "west": {"uv": [13, 3, 8.5, 4], "texture": "#4"}, + "up": {"uv": [7, 13, 6, 8.5], "texture": "#4"}, + "down": {"uv": [8, 8.5, 7, 13], "texture": "#4"} + } + } + ], + "gui_light": "front", + "display": { + "thirdperson_righthand": { + "translation": [0, 5, 1], + "scale": [0.55, 0.55, 0.55] + }, + "thirdperson_lefthand": { + "translation": [0, 5.5, 1], + "scale": [0.55, 0.55, 0.55] + }, + "firstperson_righthand": { + "rotation": [19, 16, 0], + "translation": [3.25, 7.25, 1.5], + "scale": [0.4, 0.4, 0.4] + }, + "firstperson_lefthand": { + "rotation": [9.64, 27.12, 0.81], + "translation": [4.5, 7.25, 0], + "scale": [0.4, 0.4, 0.4] + }, + "ground": { + "translation": [0, 3, 0], + "scale": [0.25, 0.25, 0.25] + }, + "gui": { + "rotation": [30, -135, 0], + "translation": [0, 1.75, 0], + "scale": [0.4, 0.4, 0.4] + }, + "head": { + "translation": [0, 15, 0] + }, + "fixed": { + "rotation": [90, -44.5, 90], + "scale": [0.5, 0.5, 0.5] + } + }, + "groups": [ + { + "name": "stick", + "origin": [8, 8, 8], + "color": 0, + "children": [ + { + "name": "stick_bot", + "origin": [8, 8, 8], + "color": 0, + "children": [0, 1, 2] + }, + { + "name": "stick_mid", + "origin": [8, 8, 8], + "color": 0, + "children": [3] + }, + { + "name": "stick_top", + "origin": [7, 0, 7], + "color": 0, + "children": [4] + } + ] + }, + { + "name": "tool_screwdriver", + "origin": [8, 8, 8], + "color": 0, + "children": [5, 6, 7, 8] + } + ] +} \ No newline at end of file diff --git a/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_wrench.json b/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_wrench.json new file mode 100644 index 00000000..01a43c68 --- /dev/null +++ b/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_wrench.json @@ -0,0 +1,259 @@ +{ + "format_version": "1.21.6", + "credit": "Made with Blockbench,made by xia_bi418", + "texture_size": [32, 32], + "textures": { + "1": "useless_mod:item/beef_tool/stick", + "9": "useless_mod:item/beef_tool/wrench", + "particle": "advanced_alloy_furnace" + }, + "elements": [ + { + "from": [6, -14, 6], + "to": [10, -9, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [8, -11.5, 8]}, + "faces": { + "north": {"uv": [1.5, 0, 3.5, 2.5], "texture": "#1"}, + "east": {"uv": [1.5, 2.5, 3.5, 5], "texture": "#1"}, + "south": {"uv": [3.5, 0, 5.5, 2.5], "texture": "#1"}, + "west": {"uv": [3.5, 2.5, 1.5, 5], "texture": "#1"}, + "up": {"uv": [5.5, 4.5, 3.5, 2.5], "texture": "#1"}, + "down": {"uv": [5.5, 4.5, 3.5, 6.5], "texture": "#1"} + } + }, + { + "from": [6.025, -15.25, 6.25], + "to": [9.975, -12.45, 7.775], + "rotation": {"angle": -45, "axis": "x", "origin": [8, -13.5, 8.025]}, + "faces": { + "north": {"uv": [3.5, 6.5, 5.5, 8], "texture": "#1"}, + "east": {"uv": [7.5, 7.5, 8.5, 9], "texture": "#1"}, + "south": {"uv": [0, 7, 2, 8.5], "texture": "#1"}, + "west": {"uv": [8.5, 7.5, 7.5, 9], "texture": "#1"}, + "up": {"uv": [9.5, 2.5, 7.5, 1.5], "texture": "#1"}, + "down": {"uv": [9.5, 2.5, 7.5, 3.5], "texture": "#1"} + } + }, + { + "from": [6, -16, 8], + "to": [10, -14, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [8, -15, 9]}, + "faces": { + "north": {"uv": [7.5, 3.5, 9.5, 4.5], "texture": "#1"}, + "east": {"uv": [3.5, 8, 4.5, 9], "texture": "#1"}, + "south": {"uv": [7.5, 4.5, 9.5, 5.5], "texture": "#1"}, + "west": {"uv": [4.5, 8, 3.5, 9], "texture": "#1"}, + "up": {"uv": [9.5, 6.5, 7.5, 5.5], "texture": "#1"}, + "down": {"uv": [9.5, 6.5, 7.5, 7.5], "texture": "#1"} + } + }, + { + "from": [6.5, -9, 6.5], + "to": [9.5, 5, 9.5], + "rotation": {"angle": 0, "axis": "y", "origin": [7, -9, 7]}, + "faces": { + "north": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "east": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "south": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "west": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "up": {"uv": [3.5, 8.5, 2, 7], "texture": "#1"}, + "down": {"uv": [9, 0, 7.5, 1.5], "texture": "#1"} + } + }, + { + "from": [6, 5, 6], + "to": [10, 9, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 5, 7]}, + "faces": { + "north": {"uv": [1.5, 5, 3.5, 7], "texture": "#1"}, + "east": {"uv": [5.5, 0, 7.5, 2], "texture": "#1"}, + "south": {"uv": [5.5, 2, 7.5, 4], "texture": "#1"}, + "west": {"uv": [7.5, 0, 5.5, 2], "texture": "#1"}, + "up": {"uv": [7.5, 6, 5.5, 4], "texture": "#1"}, + "down": {"uv": [7.5, 6, 5.5, 8], "texture": "#1"} + } + }, + { + "from": [6, 9, 5.5], + "to": [10, 13.25, 10.5], + "rotation": {"angle": 0, "axis": "y", "origin": [9, 9, 7]}, + "faces": { + "north": {"uv": [2.5, 0, 4.5, 2], "texture": "#9"}, + "east": {"uv": [0, 0, 2.5, 2], "texture": "#9"}, + "south": {"uv": [4, 2, 6, 4], "texture": "#9"}, + "west": {"uv": [2.5, 0, 0, 2], "texture": "#9"}, + "up": {"uv": [2, 4.5, 0, 2], "texture": "#9"}, + "down": {"uv": [4, 2, 2, 4.5], "texture": "#9"} + } + }, + { + "from": [6.5, 13.3, 9.5], + "to": [9.5, 16.8, 12.5], + "rotation": {"angle": 0, "axis": "y", "origin": [9, 13.8, 9]}, + "faces": { + "north": {"uv": [5.5, 4, 7, 5.5], "texture": "#9"}, + "east": {"uv": [5.5, 5.5, 7, 7], "texture": "#9"}, + "south": {"uv": [6, 0, 7.5, 1.5], "texture": "#9"}, + "west": {"uv": [5.5, 5.5, 7, 7], "texture": "#9"}, + "up": {"uv": [7.5, 3, 6, 1.5], "texture": "#9"}, + "down": {"uv": [4.5, 6, 3, 7.5], "texture": "#9"} + } + }, + { + "from": [6.5, 13.25, 6.5], + "to": [9.5, 16.75, 8.5], + "rotation": {"angle": 0, "axis": "y", "origin": [9, 13.75, 6]}, + "faces": { + "north": {"uv": [4, 4, 5.5, 6], "texture": "#9"}, + "east": {"uv": [4.5, 6, 5.5, 8], "texture": "#9"}, + "south": {"uv": [0, 4.5, 1.5, 6.5], "texture": "#9"}, + "west": {"uv": [4.5, 6, 5.5, 8], "texture": "#9"}, + "up": {"uv": [7.5, 4, 6, 3], "texture": "#9"}, + "down": {"uv": [8.5, 4, 7, 5], "texture": "#9"} + } + }, + { + "from": [6.5, 11.25, 5.125], + "to": [9.5, 15.35, 6.5], + "rotation": {"angle": 0, "axis": "y", "origin": [9, 12.75, 5]}, + "faces": { + "north": {"uv": [4.5, 0, 6, 2], "texture": "#9"}, + "east": {"uv": [7.5, 0, 8, 2], "texture": "#9"}, + "south": {"uv": [1.5, 4.5, 3, 6.5], "texture": "#9"}, + "west": {"uv": [7.5, 0, 8, 2], "texture": "#9"}, + "up": {"uv": [9, 2.5, 7.5, 2], "texture": "#9"}, + "down": {"uv": [9, 2.5, 7.5, 3], "texture": "#9"} + } + }, + { + "from": [6.525, 15.5, 5.875], + "to": [9.475, 16.5, 7.875], + "rotation": {"angle": -45, "axis": "x", "origin": [8, 15, 6.875]}, + "faces": { + "north": {"uv": [3, 7.5, 4.5, 8], "texture": "#9"}, + "east": {"uv": [8, 1, 9, 1.5], "texture": "#9"}, + "south": {"uv": [7.5, 3, 9, 3.5], "texture": "#9"}, + "west": {"uv": [8, 1, 9, 1.5], "texture": "#9"}, + "up": {"uv": [8.5, 6, 7, 5], "texture": "#9"}, + "down": {"uv": [7, 7, 5.5, 8], "texture": "#9"} + } + }, + { + "from": [6.5, 11.875, 6.45], + "to": [9.5, 12.675, 7.25], + "rotation": {"angle": -45, "axis": "x", "origin": [8, 10.375, 7.5]}, + "faces": { + "north": {"uv": [7.5, 3.5, 9, 4], "texture": "#9"}, + "east": {"uv": [8, 1.5, 8.5, 2], "texture": "#9"}, + "south": {"uv": [0, 8, 1.5, 8.5], "texture": "#9"}, + "west": {"uv": [8, 1.5, 8.5, 2], "texture": "#9"}, + "up": {"uv": [9.5, 0.5, 8, 0], "texture": "#9"}, + "down": {"uv": [9.5, 0.5, 8, 1], "texture": "#9"} + } + }, + { + "from": [6.525, 12.125, 9.85], + "to": [9.475, 15.125, 12.025], + "rotation": {"angle": 45, "axis": "x", "origin": [8, 13.625, 10.025]}, + "faces": { + "north": {"uv": [0, 6.5, 1.5, 8], "texture": "#9"}, + "east": {"uv": [3, 4.5, 4, 6], "texture": "#9"}, + "south": {"uv": [1.5, 6.5, 3, 8], "texture": "#9"}, + "west": {"uv": [3, 4.5, 4, 6], "texture": "#9"}, + "up": {"uv": [8.5, 7, 7, 6], "texture": "#9"}, + "down": {"uv": [8.5, 7, 7, 8], "texture": "#9"} + } + }, + { + "from": [6.525, 12.125, 9.1], + "to": [9.475, 15.125, 9.85], + "rotation": {"angle": 45, "axis": "x", "origin": [8, 13.625, 10.025]}, + "faces": { + "north": {"uv": [0, 6.5, 1.5, 8], "texture": "#9"}, + "east": {"uv": [1.5, 8, 2, 9.5], "texture": "#9"}, + "south": {"uv": [1.5, 6.5, 3, 8], "texture": "#9"}, + "west": {"uv": [1.5, 8, 2, 9.5], "texture": "#9"}, + "up": {"uv": [7, 6, 8.5, 7], "texture": "#9"}, + "down": {"uv": [7, 7, 8.5, 8], "texture": "#9"} + } + } + ], + "gui_light": "front", + "display": { + "thirdperson_righthand": { + "translation": [0, 5, 1], + "scale": [0.55, 0.55, 0.55] + }, + "thirdperson_lefthand": { + "translation": [0, 5.5, 1], + "scale": [0.55, 0.55, 0.55] + }, + "firstperson_righthand": { + "rotation": [19, 16, 0], + "translation": [3.25, 7.25, 1.5], + "scale": [0.4, 0.4, 0.4] + }, + "firstperson_lefthand": { + "rotation": [9.64, 27.12, 0.81], + "translation": [4.5, 7.25, 0], + "scale": [0.4, 0.4, 0.4] + }, + "ground": { + "translation": [0, 3, 0], + "scale": [0.25, 0.25, 0.25] + }, + "gui": { + "rotation": [30, -135, 0], + "translation": [0, 1.75, 0], + "scale": [0.4, 0.4, 0.4] + }, + "head": { + "translation": [0, 15, 0] + }, + "fixed": { + "rotation": [90, -44.5, 90], + "scale": [0.5, 0.5, 0.5] + } + }, + "groups": [ + { + "name": "stick", + "origin": [8, 8, 8], + "color": 0, + "children": [ + { + "name": "stick_bot", + "origin": [8, 8, 8], + "color": 0, + "children": [0, 1, 2] + }, + { + "name": "stick_mid", + "origin": [8, 8, 8], + "color": 0, + "children": [3] + }, + { + "name": "stick_top", + "origin": [7, 0, 7], + "color": 0, + "children": [4] + } + ] + }, + { + "name": "tool_wench", + "origin": [8, 8, 8], + "color": 0, + "children": [5, 6, 7, 8, 9, 10, 11, 12] + } + ], + "overrides": [ + { + "predicate": { + "custom_model_data": 1 + }, + "model": "useless_mod:item/endless_beaf_wrench_silk" + } + ] +} \ No newline at end of file diff --git a/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_wrench_silk.json b/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_wrench_silk.json new file mode 100644 index 00000000..31cb7ef4 --- /dev/null +++ b/src/main/resources/xia/assets/useless_mod/models/item/endless_beaf_wrench_silk.json @@ -0,0 +1,251 @@ +{ + "format_version": "1.21.6", + "credit": "Made with Blockbench,made by xia_bi418", + "texture_size": [32, 32], + "textures": { + "1": "useless_mod:item/beef_tool/stick", + "9": "useless_mod:item/beef_tool/wrench_silk", + "particle": "advanced_alloy_furnace" + }, + "elements": [ + { + "from": [6, -14, 6], + "to": [10, -9, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [8, -11.5, 8]}, + "faces": { + "north": {"uv": [1.5, 0, 3.5, 2.5], "texture": "#1"}, + "east": {"uv": [1.5, 2.5, 3.5, 5], "texture": "#1"}, + "south": {"uv": [3.5, 0, 5.5, 2.5], "texture": "#1"}, + "west": {"uv": [3.5, 2.5, 1.5, 5], "texture": "#1"}, + "up": {"uv": [5.5, 4.5, 3.5, 2.5], "texture": "#1"}, + "down": {"uv": [5.5, 4.5, 3.5, 6.5], "texture": "#1"} + } + }, + { + "from": [6.025, -15.25, 6.25], + "to": [9.975, -12.45, 7.775], + "rotation": {"angle": -45, "axis": "x", "origin": [8, -13.5, 8.025]}, + "faces": { + "north": {"uv": [3.5, 6.5, 5.5, 8], "texture": "#1"}, + "east": {"uv": [7.5, 7.5, 8.5, 9], "texture": "#1"}, + "south": {"uv": [0, 7, 2, 8.5], "texture": "#1"}, + "west": {"uv": [8.5, 7.5, 7.5, 9], "texture": "#1"}, + "up": {"uv": [9.5, 2.5, 7.5, 1.5], "texture": "#1"}, + "down": {"uv": [9.5, 2.5, 7.5, 3.5], "texture": "#1"} + } + }, + { + "from": [6, -16, 8], + "to": [10, -14, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [8, -15, 9]}, + "faces": { + "north": {"uv": [7.5, 3.5, 9.5, 4.5], "texture": "#1"}, + "east": {"uv": [3.5, 8, 4.5, 9], "texture": "#1"}, + "south": {"uv": [7.5, 4.5, 9.5, 5.5], "texture": "#1"}, + "west": {"uv": [4.5, 8, 3.5, 9], "texture": "#1"}, + "up": {"uv": [9.5, 6.5, 7.5, 5.5], "texture": "#1"}, + "down": {"uv": [9.5, 6.5, 7.5, 7.5], "texture": "#1"} + } + }, + { + "from": [6.5, -9, 6.5], + "to": [9.5, 5, 9.5], + "rotation": {"angle": 0, "axis": "y", "origin": [7, -9, 7]}, + "faces": { + "north": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "east": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "south": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "west": {"uv": [0, 0, 1.5, 7], "texture": "#1"}, + "up": {"uv": [3.5, 8.5, 2, 7], "texture": "#1"}, + "down": {"uv": [9, 0, 7.5, 1.5], "texture": "#1"} + } + }, + { + "from": [6, 5, 6], + "to": [10, 9, 10], + "rotation": {"angle": 0, "axis": "y", "origin": [7, 5, 7]}, + "faces": { + "north": {"uv": [1.5, 5, 3.5, 7], "texture": "#1"}, + "east": {"uv": [5.5, 0, 7.5, 2], "texture": "#1"}, + "south": {"uv": [5.5, 2, 7.5, 4], "texture": "#1"}, + "west": {"uv": [7.5, 0, 5.5, 2], "texture": "#1"}, + "up": {"uv": [7.5, 6, 5.5, 4], "texture": "#1"}, + "down": {"uv": [7.5, 6, 5.5, 8], "texture": "#1"} + } + }, + { + "from": [6, 9, 5.5], + "to": [10, 13.25, 10.5], + "rotation": {"angle": 0, "axis": "y", "origin": [9, 9, 7]}, + "faces": { + "north": {"uv": [2.5, 0, 4.5, 2], "texture": "#9"}, + "east": {"uv": [0, 0, 2.5, 2], "texture": "#9"}, + "south": {"uv": [4, 2, 6, 4], "texture": "#9"}, + "west": {"uv": [2.5, 0, 0, 2], "texture": "#9"}, + "up": {"uv": [2, 4.5, 0, 2], "texture": "#9"}, + "down": {"uv": [4, 2, 2, 4.5], "texture": "#9"} + } + }, + { + "from": [6.5, 13.3, 9.5], + "to": [9.5, 16.8, 12.5], + "rotation": {"angle": 0, "axis": "y", "origin": [9, 13.8, 9]}, + "faces": { + "north": {"uv": [5.5, 4, 7, 5.5], "texture": "#9"}, + "east": {"uv": [5.5, 5.5, 7, 7], "texture": "#9"}, + "south": {"uv": [6, 0, 7.5, 1.5], "texture": "#9"}, + "west": {"uv": [5.5, 5.5, 7, 7], "texture": "#9"}, + "up": {"uv": [7.5, 3, 6, 1.5], "texture": "#9"}, + "down": {"uv": [4.5, 6, 3, 7.5], "texture": "#9"} + } + }, + { + "from": [6.5, 13.25, 6.5], + "to": [9.5, 16.75, 8.5], + "rotation": {"angle": 0, "axis": "y", "origin": [9, 13.75, 6]}, + "faces": { + "north": {"uv": [4, 4, 5.5, 6], "texture": "#9"}, + "east": {"uv": [4.5, 6, 5.5, 8], "texture": "#9"}, + "south": {"uv": [0, 4.5, 1.5, 6.5], "texture": "#9"}, + "west": {"uv": [4.5, 6, 5.5, 8], "texture": "#9"}, + "up": {"uv": [7.5, 4, 6, 3], "texture": "#9"}, + "down": {"uv": [8.5, 4, 7, 5], "texture": "#9"} + } + }, + { + "from": [6.5, 11.25, 5.125], + "to": [9.5, 15.35, 6.5], + "rotation": {"angle": 0, "axis": "y", "origin": [9, 12.75, 5]}, + "faces": { + "north": {"uv": [4.5, 0, 6, 2], "texture": "#9"}, + "east": {"uv": [7.5, 0, 8, 2], "texture": "#9"}, + "south": {"uv": [1.5, 4.5, 3, 6.5], "texture": "#9"}, + "west": {"uv": [7.5, 0, 8, 2], "texture": "#9"}, + "up": {"uv": [9, 2.5, 7.5, 2], "texture": "#9"}, + "down": {"uv": [9, 2.5, 7.5, 3], "texture": "#9"} + } + }, + { + "from": [6.525, 15.5, 5.875], + "to": [9.475, 16.5, 7.875], + "rotation": {"angle": -45, "axis": "x", "origin": [8, 15, 6.875]}, + "faces": { + "north": {"uv": [3, 7.5, 4.5, 8], "texture": "#9"}, + "east": {"uv": [8, 1, 9, 1.5], "texture": "#9"}, + "south": {"uv": [7.5, 3, 9, 3.5], "texture": "#9"}, + "west": {"uv": [8, 1, 9, 1.5], "texture": "#9"}, + "up": {"uv": [8.5, 6, 7, 5], "texture": "#9"}, + "down": {"uv": [7, 7, 5.5, 8], "texture": "#9"} + } + }, + { + "from": [6.5, 11.875, 6.45], + "to": [9.5, 12.675, 7.25], + "rotation": {"angle": -45, "axis": "x", "origin": [8, 10.375, 7.5]}, + "faces": { + "north": {"uv": [7.5, 3.5, 9, 4], "texture": "#9"}, + "east": {"uv": [8, 1.5, 8.5, 2], "texture": "#9"}, + "south": {"uv": [0, 8, 1.5, 8.5], "texture": "#9"}, + "west": {"uv": [8, 1.5, 8.5, 2], "texture": "#9"}, + "up": {"uv": [9.5, 0.5, 8, 0], "texture": "#9"}, + "down": {"uv": [9.5, 0.5, 8, 1], "texture": "#9"} + } + }, + { + "from": [6.525, 12.125, 9.85], + "to": [9.475, 15.125, 12.025], + "rotation": {"angle": 45, "axis": "x", "origin": [8, 13.625, 10.025]}, + "faces": { + "north": {"uv": [0, 6.5, 1.5, 8], "texture": "#9"}, + "east": {"uv": [3, 4.5, 4, 6], "texture": "#9"}, + "south": {"uv": [1.5, 6.5, 3, 8], "texture": "#9"}, + "west": {"uv": [3, 4.5, 4, 6], "texture": "#9"}, + "up": {"uv": [8.5, 7, 7, 6], "texture": "#9"}, + "down": {"uv": [8.5, 7, 7, 8], "texture": "#9"} + } + }, + { + "from": [6.525, 12.125, 9.1], + "to": [9.475, 15.125, 9.85], + "rotation": {"angle": 45, "axis": "x", "origin": [8, 13.625, 10.025]}, + "faces": { + "north": {"uv": [0, 6.5, 1.5, 8], "texture": "#9"}, + "east": {"uv": [1.5, 8, 2, 9.5], "texture": "#9"}, + "south": {"uv": [1.5, 6.5, 3, 8], "texture": "#9"}, + "west": {"uv": [1.5, 8, 2, 9.5], "texture": "#9"}, + "up": {"uv": [7, 6, 8.5, 7], "texture": "#9"}, + "down": {"uv": [7, 7, 8.5, 8], "texture": "#9"} + } + } + ], + "gui_light": "front", + "display": { + "thirdperson_righthand": { + "translation": [0, 5, 1], + "scale": [0.55, 0.55, 0.55] + }, + "thirdperson_lefthand": { + "translation": [0, 5.5, 1], + "scale": [0.55, 0.55, 0.55] + }, + "firstperson_righthand": { + "rotation": [19, 16, 0], + "translation": [3.25, 7.25, 1.5], + "scale": [0.4, 0.4, 0.4] + }, + "firstperson_lefthand": { + "rotation": [9.64, 27.12, 0.81], + "translation": [4.5, 7.25, 0], + "scale": [0.4, 0.4, 0.4] + }, + "ground": { + "translation": [0, 3, 0], + "scale": [0.25, 0.25, 0.25] + }, + "gui": { + "rotation": [30, -135, 0], + "translation": [0, 1.75, 0], + "scale": [0.4, 0.4, 0.4] + }, + "head": { + "translation": [0, 15, 0] + }, + "fixed": { + "rotation": [90, -44.5, 90], + "scale": [0.5, 0.5, 0.5] + } + }, + "groups": [ + { + "name": "stick", + "origin": [8, 8, 8], + "color": 0, + "children": [ + { + "name": "stick_bot", + "origin": [8, 8, 8], + "color": 0, + "children": [0, 1, 2] + }, + { + "name": "stick_mid", + "origin": [8, 8, 8], + "color": 0, + "children": [3] + }, + { + "name": "stick_top", + "origin": [7, 0, 7], + "color": 0, + "children": [4] + } + ] + }, + { + "name": "tool_wench", + "origin": [8, 8, 8], + "color": 0, + "children": [5, 6, 7, 8, 9, 10, 11, 12] + } + ] +} \ No newline at end of file diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/advanced_alloy_furnace.png b/src/main/resources/xia/assets/useless_mod/textures/block/advanced_alloy_furnace.png new file mode 100644 index 00000000..33a8bfda Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/advanced_alloy_furnace.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/advanced_alloy_furnace_active.png b/src/main/resources/xia/assets/useless_mod/textures/block/advanced_alloy_furnace_active.png new file mode 100644 index 00000000..2368dc57 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/advanced_alloy_furnace_active.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/advanced_alloy_furnace_locked.png b/src/main/resources/xia/assets/useless_mod/textures/block/advanced_alloy_furnace_locked.png new file mode 100644 index 00000000..851e3dda Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/advanced_alloy_furnace_locked.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/creator_doll_block.png b/src/main/resources/xia/assets/useless_mod/textures/block/creator_doll_block.png new file mode 100644 index 00000000..aba56e84 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/creator_doll_block.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/dream_master_xxl.png b/src/main/resources/xia/assets/useless_mod/textures/block/dream_master_xxl.png new file mode 100644 index 00000000..5f60604e Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/dream_master_xxl.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/iava_user.png b/src/main/resources/xia/assets/useless_mod/textures/block/iava_user.png new file mode 100644 index 00000000..6e6d7894 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/iava_user.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/Coil_ctm.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/Coil_ctm.png new file mode 100644 index 00000000..f28a68e2 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/Coil_ctm.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/Coli_base.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/Coli_base.png new file mode 100644 index 00000000..a9ea8042 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/Coli_base.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/Gallery.cache b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/Gallery.cache new file mode 100644 index 00000000..a58b704b --- /dev/null +++ b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/Gallery.cache @@ -0,0 +1 @@ +[{"Path":"/storage/emulated/0/Android/data/com.pixelstudio.pro/files/Storage/useless/coils/Coli_base.png","LastWriteTime":639232982368819097,"Height":16,"Width":16,"Texture":"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAeklEQVQ4EWNgGGjAeL09/b+ilhrYHfev3WIglc24N8DrPyW+YJEylyXZVmRXMi60sP8vI8ENdsSTF18ZSGWzmPlrDLALtE2FGLilxMBe4H/2imQ2y9XT74CaQRgGSGOzILvgK5ILiGUPEhfISPwEBwAkHZDGhoXcwNEAUMej/MWS7qYAAAAASUVORK5CYII="},{"Path":"/storage/emulated/0/Android/data/com.pixelstudio.pro/files/Storage/useless/coils/useless_coil_tier_1.png","LastWriteTime":639232983458859097,"Height":16,"Width":16,"Texture":"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA20lEQVQ4EWPM/db3/+O/VwwgwM8kxgBig+gHf84zKLAYgsXxyTM6XHD7/+fbL7BCxg8/wDSI+C/AAWazcLEx4JWfvl2RQVFLDaz4/rVbCPZBJDayOIz9moEBpJ5xb4DXf7BuMgkWKXNZhK0w04GGobgGjzjjQgv7/zIS3GD7n7z4ykAqm8XMX2OAXaBtKsTALSUG9gL/s1cks1munn4H1AzCMEAamwXZBV+RXEAse5C4QEbiJzgAIOmANDaj76fY/9/ffIKFIJjmFOFjgIkhs2GKkMUYNZLFKMoLAFW21sUW51U4AAAAAElFTkSuQmCC"},{"Path":"/storage/emulated/0/Android/data/com.pixelstudio.pro/files/Storage/useless/coils/Coil_ctm.png","LastWriteTime":639232986264339098,"Height":32,"Width":32,"Texture":"iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABGklEQVRYCWO83p7+X1FLjQEE7l+7xUAKmxw96HYwLrSw/y8jwQ12wJMXXxlIZYM0kqoHWT2Lmb8GSb5G9gFVQmBvgNd/sPcHiGCRMpcd2BAYTQOjITAaAqPlwGgaAIXAAFUDYGsZqWX5j429/5FrSkJsDv9isN0s1HLA0fl7GUDtiWcnH4ONJIYNUsg4kC0isANGdDkADgFyywFQHMMAchuPWDZM/4C2CQdHCGibCjFwS4mBQ5P/2Sui2VLQ8P9Kgh6QFpgdMP3gfgHUrAGhWJBDANk3xLBBLiZGHS41IP2jIcBy9fQ7YN/uJyg0wGU5sWxYPgbpI1YPsh0w/VSrDUF1Cq64xiauWTmTurXhqY03QB4EgncQikg2ABJy7uTf9ldPAAAAAElFTkSuQmCC"},{"Path":"/storage/emulated/0/Android/data/com.pixelstudio.pro/files/Storage/useless/coils/useless_coil_tier_1_ctm.png","LastWriteTime":639232986121779098,"Height":32,"Width":32,"Texture":"iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABYklEQVRYCWO83p7+X1FLjQEE7l+7xUAKmxw96HYwLrSw/y8jwQ12wJMXXxlIZYM0kqoHWT2Lmb8GSb5G9gFVQmBvgNd/sPcHiGCRMpcd2BAYTQOjITAaAqPlwGgaAIXAAFUDYGtZzuzzZ/j47xWYw88kBmaD6Ad/zjMosBiCxYmRn7VXlKT2RKjjBbD5jA4X3P7/+fYLbBHjhx9gGkT8F+AAs1m42BiIkW/s4mIgpT1RX/YNbD7jQLaIQC5gHNHlADgEyC0HQPENA8htPGLZMP0D2iYcHCGgbSrEwC0lBg5N/meviGZLQcP/Kwl6QFpgdsD0g/sFULMGhGJBDgFk3xDDBrmYGHW41ID0j4YAy9XT74B9u5+g0ICW5cSxYfkYpI8S/Yy+n2L/f3/zCWQOHHCK8DHAxJDZMAXIYjD25JUSJKWH3PAXYONYbhfuhJlLEX3qqiZU/zskc3Czn9y5DlYHADwKIiFgYRAAAAAAAElFTkSuQmCC"},{"Path":"/storage/emulated/0/Android/data/com.pixelstudio.pro/files/Storage/useless/coils/useless_coil_tier_2.png","LastWriteTime":639232986616619098,"Height":16,"Width":16,"Texture":"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAy0lEQVQ4EWO8lPvt/8eP/xhAgJ+fiQHEBtH3HvxhUFJgAYvjk2fsc7jw//ufb2CFb/98ANMgQphFAMzmZOFiwCfP4uk+nUFRSw2s+P61WySzGfcGeP0H6yaTYJEylyXZVmQXMy60sP8vI8ENtv/Ji68MpLJZzPw1BtgF2qZCDNxSYmAv8D97RTKb5erpd0DNIAwDpLFZkF3wFckFxLIHiQtkJH6CAwCSDkhjM27w/fT/zfc3sBAE0yKcIgwwMWQ2TBGyGKOWWDJFeQEA6CnWf/N4qL0AAAAASUVORK5CYII="},{"Path":"/storage/emulated/0/Android/data/com.pixelstudio.pro/files/Storage/useless/coils/useless_coil_tier_2_ctm.png","LastWriteTime":639232986803259098,"Height":32,"Width":32,"Texture":"iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABXklEQVRYCWO83p7+X1FLjQEE7l+7xUAKmxw96HYwLrSw/y8jwQ12wJMXXxlIZYM0kqoHWT2Lmb8GSb5G9gFVQmBvgNd/sPcHiGCRMpcd2BAYTQOjITAaAqPlwGgaAIXAAFUDYGsZL+V++//x4z8wh5+fiQHEBtH3HvxhUFJgAYsTI28aPJOk9sSOOalg8xn7HC78//7nG9iit38+gGkQIcwiAGZzsnAxECNvIdLCQEp74sSbGrD5jAPZIgK5gHFElwPgECC3HADFNwwgt/GIZcP0D2ibcHCEgLapEAO3lBg4NPmfvSKaLQUN/68k6AFpgdkB0w/uF0DNGhCKBTkEkH1DDBvkYmLU4VID0j8aAixXT78D9u1+gkIDWpYTx4blY5A+SvQzbvD99P/N9zcgc+BAhFOEASaGzIYpQBaDsa2dO0lKD0f3loONY9QSS6ZKe6Bc6Q7MfUTRnfdUwOoAm/oejupeW+kAAAAASUVORK5CYII="},{"Path":"/storage/emulated/0/Android/data/com.pixelstudio.pro/files/Storage/useless/coils/useless_coil_tier_3.png","LastWriteTime":639232986969259098,"Height":16,"Width":16,"Texture":"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA0ElEQVQ4EWP8ltv2/9/HvwwgwMTPzABig+g/D34zsCiwgsXxybPcuvyB4dufP2CFH/78B9MgQoCFkYHhPQMDFwsLXnkWdvePDBpaamCN96/dYlAkkc24N8ALYS3cfuIZLFLmsiTbiuxKxoUW9v9lJLjBVj558ZWBVDaLmb/GALtA21SIgVtKDOwF/mevSGazXD39DqgZhGGANDYLsgu+IrmAWPYgcYGMxE9wAEDSAWlsxk++jf/ffP8BC0EwLcLJwQATQ2bDFCGLMSaLeVKUFwADW9g0aWtYnwAAAABJRU5ErkJggg=="},{"Path":"/storage/emulated/0/Android/data/com.pixelstudio.pro/files/Storage/useless/coils/useless_coil_tier_3_ctm.png","LastWriteTime":639232987099019098,"Height":32,"Width":32,"Texture":"iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABe0lEQVRYCWO83p7+X1FLjQEE7l+7xUAKmxw96HYwLrSw/y8jwQ12wJMXXxlIZYM0kqoHWT2Lmb8GSb5G9gFVQmBvgNd/sPcHiGCRMpcd2BAYTQOjITAaAqPlwGgaAIXAAFUDYGsZv+XO+f/vI8QNTPyMDCA2iP7zgJGBRQEiTow8S/AnktoTv+YIgM1nvODQ9//bn99g13z48xceGAIszGA2FwsrAzHy30UOMpDSnuB8Yw82n4Xd/SaDBrxFdA9Lzfgf6DOYOAuKL0Em3L8G0++MIofcbsDFBulnHNHlADgEyC0HQPENA8htPGLZMP0D2iYcHCGgbSrEwC0lBg5N/meviGZLQcP/Kwl6QFpgdsD0g/sFULMGhGJBDgFk3xDDBrmYGHW41ID0j4YAy9XT74B9u5+g0ICW5cSxYfkYpI8S/YyffOf+f/P9G8gcOBDh5GKAiSGzYQqQxWDsn85XSEoP7Ht1wMYxJouFUqU9YKf0CuY+ouhD9yBZHwATDR8/pSfJtgAAAABJRU5ErkJggg=="},{"Path":"/storage/emulated/0/Android/data/com.pixelstudio.pro/files/Storage/useless/coils/useless_coil_tier_4.png","LastWriteTime":639232987250259098,"Height":16,"Width":16,"Texture":"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAzElEQVQ4EWPM/fby/8d/nxlAgJ+JlwHEBtEP/jxmUGCRBYvjk2d0uLDs/59v38AK/3x4DaZBBIuAKJjNwsXFgE+e8Xp7+n9FLTWw4vvXbjGQymbcG+D1H6ybTIJFylyWZFuRXcm40ML+v4wEN9j+Jy++MpDKZjHz1xhgF2ibCjFwS4mBvcD/7BXJbJarp98BNYMwDJDGZkF2wVckFxDLHiQukJH4CQ4ASDogjc3o++nM/+9vXsFCEExziogxwMSQ2TBFyGKMYsnmFOUFAFol2G4PQYsKAAAAAElFTkSuQmCC"},{"Path":"/storage/emulated/0/Android/data/com.pixelstudio.pro/files/Storage/useless/coils/useless_coil_tier_4_ctm.png","LastWriteTime":639232987381299098,"Height":32,"Width":32,"Texture":"iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABbUlEQVRYCWO83p7+X1FLjQEE7l+7xUAKmxw96HYwLrSw/y8jwQ12wJMXXxlIZYM0kqoHWT2Lmb8GSb5G9gFVQmBvgNd/sPcHiGCRMpcd2BAYTQOjITAaAqPlwGgaAIXAAFUDYGsZc7+9/P/x32cwh5+JlwHEBtEP/jxmUGCRBYsTIz9r70aS2hOhjvZg81ku39rL8OfbN7BFfz68BtMggkVAlOE9wwUGFi4uouSPzt/LAGpPPDv5GGwGIfZ7UX6w+YwD2SICuZRxRJcD4BAgtxwAxTEMILfxiGXD9A9om3BwhIC2qRADt5QYODT5n70imi0FDf+vJOgBaYHZAdMP7hdAzRoQigU5BJB9Qwwb5GJi1OFSA9I/GgIsV0+/A/btfoJCA1yWE8uG5WOQPmL1INsB08/o++nM/+9vXoHk4IBTRIwBJobMhilAFoOxJ6/cSFJ6yA33BxvHKJZsTpX2QPdVDpj7iKJLtX+A1QEA0GMtWAgkeokAAAAASUVORK5CYII="},{"Path":"/storage/emulated/0/Android/data/com.pixelstudio.pro/files/Storage/useless/coils/useless_coil_tier_5.png","LastWriteTime":639232987587219098,"Height":16,"Width":16,"Texture":"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAvElEQVQ4EWP8f/n/f4aPDBDAD6RAbBD9AIgVgBgE8MizMFwGKvgDVoagQVwWIH4PpfHIs9x4mMGgqKUG0sJw/9otktmMewO8/oN1k0mwSJnLkmwrsosZF1rY/5eR4Abb/+TFVwZS2Sxm/hoD7AJtUyEGbikxsBf4n70imc1y9fQ7oGYQhgHS2CzILviK5AJi2YPEBTISP8EBAEkHpLEZ/28G5sbvsACE0pxAGiaGzIYpQxJjOXLyFEyYLBoA23y94QBGKokAAAAASUVORK5CYII="},{"Path":"/storage/emulated/0/Android/data/com.pixelstudio.pro/files/Storage/useless/coils/useless_coil_tier_5_ctm.png","LastWriteTime":639232987729219098,"Height":32,"Width":32,"Texture":"iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABUUlEQVRYCWO83p7+X1FLjQEE7l+7xUAKmxw96HYwLrSw/y8jwQ12wJMXXxlIZYM0kqoHWT2Lmb8GSb5G9gFVQmBvgNd/sPcHiGCRMpcd2BAYTQOjITAaAqPlwGgaAIXAAFUDYGsZ/1/+/5/hI9QJ/EAaxAbRD4BYAYhBgAj5n2/7SGpPsDMVgc1n/L8M6IA/EHvgNIjLAhUD0UTI71vnzUBKe8IpaCvYAsaBbBGBXMA4ossBcAiQWw6A4hsGkNt4xLJh+ge0TTg4QkDbVIiBW0oMHJr8z14RzZaChv9XEvSAtMDsgOkH9wugZg0IxYIcAsi+IYYNcjEx6nCpAekfDQGWq6ffAft2P0GhAS3LiWPD8jFIHyX6Gf9vBlZG30HGIAFOIBsmhsyGKUEWg7Jv3M0gKT1oKM8Am8Zy5OQpmLEU0ff23IDqf4dkDm72GxeIvQDEwwUJbHgkZAAAAABJRU5ErkJggg=="},{"Path":"/storage/emulated/0/Android/data/com.pixelstudio.pro/files/Storage/useless/coils/useless_coil_tier_6.png","LastWriteTime":639232987868419098,"Height":16,"Width":16,"Texture":"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAzElEQVQ4EWPckPvt/9uP/xhAQJifiQHEBtHXH/xh0FRgAYvjk2fMcrjw/8efb2CFH/58ANMgQoBFAMzmYOFiwCfPeL09/b+ilhpY8f1rtxhIZTPuDfD6D9ZNJsEiZS5Lsq3IrmRcaGH/X0aCG2z/kxdfGUhls5j5awywC7RNhRi4pcTAXuB/9opkNsvV0++AmkEYBkhjsyC74CuSC4hlDxIXyEj8BAcAJB2QxmZs9/30/933N7AQBNNCnCIMMDFkNkwRshijuFgyRXkBAG1f15qPmdtDAAAAAElFTkSuQmCC"},{"Path":"/storage/emulated/0/Android/data/com.pixelstudio.pro/files/Storage/useless/coils/useless_coil_tier_6_ctm.png","LastWriteTime":639232987988059098,"Height":32,"Width":32,"Texture":"iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABXUlEQVRYCWO83p7+X1FLjQEE7l+7xUAKmxw96HYwLrSw/y8jwQ12wJMXXxlIZYM0kqoHWT2Lmb8GSb5G9gFVQmBvgNd/sPcHiGCRMpcd2BAYTQOjITAaAqPlwGgaAIXAAFUDYGtZ+E23M7z9+A/MEeZnArNB9PUHfxg0FVjA4sTIRwfPJKk90T8nFWw+Y5bDhf8//nwDW/ThzwcwDSIEWATAbA4WLgZi5KNFWhhIaU8sfVMDNp9xIFtEIBcwjuhyABwC5JYDoPiGAeQ2HrFsmP4BbRMOjhDQNhVi4JYSA4cm/7NXRLOloOH/lQQ9IC0wO2D6wf0CqFkDQrEghwCyb4hhg1xMjDpcakD6R0OA5erpd8C+3U9QaEDLcuLYsHwM0keJfsZ230//331/AzIHDoQ4RRhgYshsmAJkMRg7ybmTpPQwb2852DhGcbFkqrQHupTuwNxHFF12TwWsDgCreBv7pfsz7AAAAABJRU5ErkJggg=="},{"Path":"/storage/emulated/0/Android/data/com.pixelstudio.pro/files/Storage/useless/coils/useless_coil_tier_7.png","LastWriteTime":639232988155019098,"Height":16,"Width":16,"Texture":"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAyUlEQVQ4EWP08/P7/+nTJwYQ4OPjYwCxQfSjR48Y5OTkwOL45BmlpaX///r1C6zw27dvYBpEcHFxgdlsbGwM+ORZ9uT4MChqqYEV3792i2Q2494Ar/9g3WQSLFLmsiTbiuxixoUW9v9lJLjB9j958ZWBVDaLmb/GALtA21SIgVtKDOwF/mevSGazXD39DqgZhGGANDYLsgu+IrmAWPYgcYGMxE9wAEDSAWlsRgMDg/+fP3+GhSCY5uXlZYCJIbNhipDFGIGCFOUFAAl90kGnakYDAAAAAElFTkSuQmCC"},{"Path":"/storage/emulated/0/Android/data/com.pixelstudio.pro/files/Storage/useless/coils/useless_coil_tier_7_ctm.png","LastWriteTime":639232988300299098,"Height":32,"Width":32,"Texture":"iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABW0lEQVRYCWO83p7+X1FLjQEE7l+7xUAKmxw96HYwLrSw/y8jwQ12wJMXXxlIZYM0kqoHWT2Lmb8GSb5G9gFVQmBvgNd/sPcHiGCRMpcd2BAYTQOjITAaAqPlwGgaAIXAAFUDYGtZ1orxM3z69AnM4ePjA7NB9KNHjxjk5OTA4sTI7yj0Jak9YVG/GGw+o7S09P9fv36BLfr27RuYBhFcXFxgNhsbGwMx8iusTRlIaU9EHD0NNp9xIFtEIBcwjuhyABwC5JYDoPiGAeQ2HrFsmP4BbRMOjhDQNhVi4JYSA4cm/7NXRLOloOH/lQQ9IC0wO2D6wf0CqFkDQrEghwCyb4hhg1xMjDpcakD6R0OA5erpd8C+3U9QaEDLcuLYsHwM0keJfkYDA4P/nz9/BpkDB7y8vAwwMWQ2TAGyGIy9JcWFpPTgM2cP2DhGIEmV9gCwRIW5jyg6/sRBsDoASd8Y76KlEdwAAAAASUVORK5CYII="},{"Path":"/storage/emulated/0/Android/data/com.pixelstudio.pro/files/Storage/useless/coils/useless_coil_tier_8.png","LastWriteTime":639232988572019098,"Height":16,"Width":16,"Texture":"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAxklEQVQ4EWP0zXvy//PnLwwgwMvLwwBig+jnL94ySEoIg8XxyTM6+t38/+H9K7DC79/fgWkQwckpBGYLCIox4JNnvN6e/l9RSw2s+P61Wwykshn3Bnj9B+smk2CRMpcl2VZkVzIutLD/LyPBDbb/yYuvDKSyWcz8NQbYBdqmQgzcUmJgL/A/e0Uym+XqaVDcI+KfVDYLsgu+IrmAWPYgcYGMxE+kdEAam9Ek7OL/tx9+gA2AEcICHAwwMWQ2NnlGLp3JFOUFAK9v1sWmulCyAAAAAElFTkSuQmCC"},{"Path":"/storage/emulated/0/Android/data/com.pixelstudio.pro/files/Storage/useless/coils/useless_coil_tier_8_ctm.png","LastWriteTime":639232988713539098,"Height":32,"Width":32,"Texture":"iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABXklEQVRYCWO83p7+X1FLjQEE7l+7xUAKmxw96HYwLrSw/y8jwQ12wJMXXxlIZYM0kqoHWT2Lmb8GSb5G9gFVQmBvgNd/sPcHiGCRMpcd2BAYTQOjITAaAqPlwGgaAIXAAFUDYGsZffOe/P/8+QuYw8vLwwBig+jnL94ySEoIg8WJkd8RsJWk9oTNUhew+YyOfjf/f3j/CmzR9+/vwDSI4OQUArMFBMUYiJHvEW5nIKU9UfK2Emw+40C2iEAuYBzR5QA4BMgtB0DxDQPIbTxi2TD9A9omHBwhoG0qxMAtJQYOTf5nr4hmS0HD/ysJekBaYHbA9IP7BVCzBoRiQQ4BZN8Qwwa5mBh1uNSA9I+GAMvV0++AfbufoNCAluXEsWH5GKSPEv2MJmEX/7/98ANkDhwIC3AwwMSQ2TAFyGIw9jbHeSSlB6/9SWDjGLl0JlOlPTCdZw3MfUTRmV9CwOoAHd8dOiLLgEAAAAAASUVORK5CYII="},{"Path":"/storage/emulated/0/Android/data/com.pixelstudio.pro/files/Storage/useless/coils/useless_coil_tier_9.png","LastWriteTime":639232988828739098,"Height":16,"Width":16,"Texture":"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA2UlEQVQ4EWP8djf3/79PHxlAgImPnwHEBtF/Hj5gYJFXAIvjk2e8MMvh/7fvf8AKP7yG0CCOgCgLWIyLk4UBnzwL+1t1Bg0tNbDi+99uMSjC2NewsBUYGO6jiTPuDfD6D9ZNJsEiZS6L31agwei2IruScaGF/X8ZCW6w/U9efGUglc1i5q8xwC7QNhVi4JYSA3uB/9krktksV0+/A2oGYRggjc2C7IKvSC4glj1IXCAj8RMcAJB0QBqb8dM+3/9vXn+HhSCYFhHlZICJIbNhipDFGJMNxCjKCwAXvtbknEO04QAAAABJRU5ErkJggg=="},{"Path":"/storage/emulated/0/Android/data/com.pixelstudio.pro/files/Storage/useless/coils/useless_coil_tier_9_ctm.png","LastWriteTime":639232988964099098,"Height":32,"Width":32,"Texture":"iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABeUlEQVRYCWO83p7+X1FLjQEE7l+7xUAKmxw96HYwLrSw/y8jwQ12wJMXXxlIZYM0kqoHWT2Lmb8GSb5G9gFVQmBvgNd/sPcHiGCRMpcd2BAYTQOjITAaAqPlwGgaAIXAAFUDYGsZv93N/f/v00cwh4mPnwHEBtF/Hj5gYJFXAIsTI8/ySJ+k9sQv3gNg8xkvzHL4/+37H7BFH15DaBBHQJQFLMbFycJAjPz3/XwMpLQnOB0/gc1nYX+rzqABaxF9I9AiUkBtNYFMALUPwPoTUeWQ2w242CD9jCO6HACHALnlACi+YQC5jUcsG6Z/QNuEgyMEtE2FGLilxMChyf/sFdFsKWj4fyVBD0gLzA6YfnC/AGrWgFAsyCGA7Bti2CAXE6MOlxqQ/tEQYLl6+h2wb/cTFBrQspw4Niwfg/RRop/x0z7f/29efweZAwciopwMMDFkNkwBshiM/fOeMknpgV3pLtg4xmQDMaq0B+w4NGHuI4o+9OM6WB0Ampwdwfy7/RcAAAAASUVORK5CYII="},{"Path":"/storage/emulated/0/Android/data/com.pixelstudio.pro/files/Storage/useless/coils/useless_coil_active.png","LastWriteTime":639232991040612591,"Height":16,"Width":16,"Texture":"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAd0lEQVQ4EWNgGGjA+P/48v8MWmoQd1y7xUAqm/HvhK7/lPiChclclmRbkV3J+LCi/r+MBDfYEU9efGUglU15GFDugrUT/zNIiUHC8dkrBlLZLI9OvwNqBmEYII3NImcqRLKtyK4cJC6QkfgJDgBIOiCNDQu5gaMBYmmS2q3ArSAAAAAASUVORK5CYII="},{"Path":"/storage/emulated/0/Android/data/com.pixelstudio.pro/files/Storage/useless/coils/useless_coil_active_ctm.png","LastWriteTime":639232991218532591,"Height":32,"Width":32,"Texture":"iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABCklEQVRYCWP8f3z5fwYtNQYwuHaLgSQ2SBOpetDUMz6sqP8vI8ENtv/Ji68MpLJBGknVg6yeccBD4O+Erv9g7w8QwcJkLktavCPHIcjRyHwy2KNpYDQERkOAcbQcGK0LQCEwQNUA2FpGaln+/+PZ/6TUC4z8xmC7WajlgH/z9zKA2xMnH4ONJIYNUjiw7QGQA0Z2OQAKAXLLAVAcwwByG49YNkz/wKeBgQ+BtRP/M0iJQULz2SsGktggXaTqQVYP1A5OAxDbB4ZkkTMVIs3XaD4YDQFKQ4Dl0el3wL7dT3ACAJffRLJh+RikkRL91KsNQb1s5PRBgM1oGUnd2vDRxhvQbPQOSoMowmwAXeSCb79zD5oAAAAASUVORK5CYII="},{"Path":"/storage/emulated/0/Android/data/com.pixelstudio.pro/files/Storage/useless/coils/useful_coil.png","LastWriteTime":639232991909492591,"Height":32,"Width":16,"Texture":"iVBORw0KGgoAAAANSUhEUgAAABAAAAAgCAYAAAAbifjMAAAA+klEQVRIDc1UsQqEMAxNoDgouimC4OLgIIjgJ7g7+xn+j3/l5uokuCk4uPRsDo/eItd26HVoHmnSvqbNw7Zt+bZtIEYQBCCwsPM8Q5qm5H9axyRJ+HmeFHgcB1kxua5L2HEceFrHYRh4nucUPE0TqGLs+55TtubEiqJQPlVmiV3X8TiO6fxlWUAVs6ZpLDMoyxLCMKQrRFGkjNk4jpr1f6cxmcG6rh8Gv+I/YaD69nI8VlXF933/KqTv+3D7ZHwHyT68nGa9cOkBaYDY3YoesLquzXohyzKzGhjrged5lvXAmIHcjTp6gILB/UV1rLEe/AkDub9VsX09eAFEiHGEsJ4ctQAAAABJRU5ErkJggg=="},{"Path":"/storage/emulated/0/Android/data/com.pixelstudio.pro/files/Storage/useless/coils/useful_coil_ctm.png","LastWriteTime":639232992375692591,"Height":64,"Width":32,"Texture":"iVBORw0KGgoAAAANSUhEUgAAACAAAABACAYAAAB7jnWuAAACJElEQVRoBe1YMaoCMRBNIFgoWikKgo2IgqCCYGFrby14B7G0thDvYO0lBO8gVlrYWAlaKVrY5P83EAlb/L+zLkQwA25esplk5pnMTiIXi4WuVqsCst/vBQdH0QnOIQeDgS4UCmTA6XQSXAxFro7dX/V6PZbXtgexMDAejzW57+ih6vW6Wwb8GvAMeAZ8HPBrAAw4+gzQtOrxeIjr9UqVTCZDGOXxeBSlUonaw7yfTqesfGI0GtH4slgs6ufzSRPBGCPJZJJgIpEQYd4Ph0PBySeWyyWNL11mRLBAfnUcIAaixgH830bsHC8sNvpOc8KPYEDOZjOdy+WIzfP5LDgYSlwduz/01Xa7RelMVKPRYHkd9MCuczG89gwQA2H3Ligz8d7sY7S9oy9brZa+3W4Y5yXpdFqYNhubDnabwZPJhLUj5vM5Dac2m40Z961yvV6z9A+HA/WX/X5f12o1qux2O8HBUOLqBPvLVCrl/n6A47XtQSwMlMtltzkh7gecMuDXgGfAM+DjgF8DYIC+xY4edDIKc/6HfX/dH3Q6HVZusFqt4r0fgHEmX4Sx/2HjtGq3286+hjBUfnUcIAaixoG4zgW0C1xlRB/BgOx2uzqbzcIYcblcBAdH0bHngD6dCwBciWo2myyvgx7YdS6G056ByGfD2OJApVJ53QWYiwac+Y3YGG3oY7eZ+wGcsPL5/Gs9hcEYT/7+YskHfiMqxgst9/ud+v4A2eER6ixJ9qYAAAAASUVORK5CYII="},{"Path":"/storage/emulated/0/Android/data/com.pixelstudio.pro/files/Storage/useless/coils/useful_coil_active.png","LastWriteTime":639232992644252591,"Height":16,"Width":16,"Texture":"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAZElEQVQ4EWNgGHBw+/bt/zBADptxw4YN/ynyBTm2IruYccaMGf8lJCTAjnjx4gUDqWyGAXcBi56eHoOoqCjYC2JiYiSzWS5dukRRJKC44PXr13AXEMseJC4gNe6R1VMUgFTRDAAAbBb1b15/JwAAAABJRU5ErkJggg=="},{"Path":"/storage/emulated/0/Android/data/com.pixelstudio.pro/files/Storage/useless/coils/useful_coil_active_ctm.png","LastWriteTime":639232992863292591,"Height":32,"Width":32,"Texture":"iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAA50lEQVRYCe1XQQ6EIAyExC+Y8AGuvtdn6AO4+gASTn6AhEewO00whMtaJelh6YXRdMCOpQV1nmcuxsXgcTmtv17XNRtjFCzGqLgYPC6n9lftF3HU6KLAtm0ZUYiZuAIjB4YCQ4FRB0YOQAGxPtBzYW5nLGtPBbwd932n80QIgabC2eIXJkfJbgjV9F/XAVLgaR3APy5Wn/Hu4osvngPSCujjOPI8z6RmSklxMEhcTu0P/uS9xyhm07IsrKjbCOpnLkbUQwFS4O7ehWTl/njt4++7N3yNSXsY6gknB6y1tHa3buicexTHB4sbDXoXJ2nZAAAAAElFTkSuQmCC"}] \ No newline at end of file diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil.png new file mode 100644 index 00000000..e2f4bd9c Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil.png.mcmeta b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil.png.mcmeta new file mode 100644 index 00000000..00f1d1eb --- /dev/null +++ b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil.png.mcmeta @@ -0,0 +1,7 @@ +{ + "animation": { + "frametime": 40, + "interpolate": true, + "frames": [0, 1] + } +} diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil_active.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil_active.png new file mode 100644 index 00000000..841aef31 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil_active.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil_active_ctm.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil_active_ctm.png new file mode 100644 index 00000000..55822cfb Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil_active_ctm.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil_ctm.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil_ctm.png new file mode 100644 index 00000000..679ec570 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil_ctm.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil_ctm.png.mcmeta b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil_ctm.png.mcmeta new file mode 100644 index 00000000..00f1d1eb --- /dev/null +++ b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useful_coil_ctm.png.mcmeta @@ -0,0 +1,7 @@ +{ + "animation": { + "frametime": 40, + "interpolate": true, + "frames": [0, 1] + } +} diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_active.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_active.png new file mode 100644 index 00000000..92b36ab8 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_active.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_active_ctm.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_active_ctm.png new file mode 100644 index 00000000..ff2251b6 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_active_ctm.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_1.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_1.png new file mode 100644 index 00000000..980791b2 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_1.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_1_ctm.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_1_ctm.png new file mode 100644 index 00000000..c0e7eadc Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_1_ctm.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_2.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_2.png new file mode 100644 index 00000000..88065e4d Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_2.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_2_ctm.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_2_ctm.png new file mode 100644 index 00000000..3893dee1 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_2_ctm.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_3.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_3.png new file mode 100644 index 00000000..c2839fa1 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_3.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_3_ctm.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_3_ctm.png new file mode 100644 index 00000000..2fb87809 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_3_ctm.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_4.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_4.png new file mode 100644 index 00000000..6c093632 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_4.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_4_ctm.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_4_ctm.png new file mode 100644 index 00000000..602d58ba Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_4_ctm.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_5.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_5.png new file mode 100644 index 00000000..03ff559a Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_5.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_5_ctm.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_5_ctm.png new file mode 100644 index 00000000..3b25a67b Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_5_ctm.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_6.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_6.png new file mode 100644 index 00000000..cce1e5b9 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_6.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_6_ctm.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_6_ctm.png new file mode 100644 index 00000000..34c333c3 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_6_ctm.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_7.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_7.png new file mode 100644 index 00000000..ce33c948 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_7.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_7_ctm.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_7_ctm.png new file mode 100644 index 00000000..8a95d0e6 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_7_ctm.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_8.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_8.png new file mode 100644 index 00000000..f484509f Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_8.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_8_ctm.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_8_ctm.png new file mode 100644 index 00000000..ba04ae6a Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_8_ctm.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_9.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_9.png new file mode 100644 index 00000000..98009f5e Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_9.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_9_ctm.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_9_ctm.png new file mode 100644 index 00000000..f3d93146 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/coils/useless_coil_tier_9_ctm.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/controller_idle_overlay.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/controller_idle_overlay.png new file mode 100644 index 00000000..04877ff8 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/controller_idle_overlay.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/controller_run_overlay.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/controller_run_overlay.png new file mode 100644 index 00000000..8fce8074 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/controller_run_overlay.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/controller_wait_overlay.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/controller_wait_overlay.png new file mode 100644 index 00000000..4f1b2096 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/controller_wait_overlay.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/core_base_overlay.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/core_base_overlay.png new file mode 100644 index 00000000..5c304117 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/core_base_overlay.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/furnace_casing.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/furnace_casing.png new file mode 100644 index 00000000..034ee66e Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/furnace_casing.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/furnace_casing_ctm.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/furnace_casing_ctm.png new file mode 100644 index 00000000..045ea8e9 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/furnace_casing_ctm.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/mold_hub_overlay.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/mold_hub_overlay.png new file mode 100644 index 00000000..9d9049d4 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/mold_hub_overlay.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/passive_crafting_hatch_overlay.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/passive_crafting_hatch_overlay.png new file mode 100644 index 00000000..3bbace34 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/passive_crafting_hatch_overlay.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/pattern_assembly_overlay.png b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/pattern_assembly_overlay.png new file mode 100644 index 00000000..28860aa3 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/multiblock_alloy_furnace/pattern_assembly_overlay.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/ore_generator_block.png b/src/main/resources/xia/assets/useless_mod/textures/block/ore_generator_block.png new file mode 100644 index 00000000..db513c3e Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/ore_generator_block.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/plastic_glow_block.png b/src/main/resources/xia/assets/useless_mod/textures/block/plastic_glow_block.png new file mode 100644 index 00000000..63e02761 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/plastic_glow_block.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/plastic_glow_block_ctm.png b/src/main/resources/xia/assets/useless_mod/textures/block/plastic_glow_block_ctm.png new file mode 100644 index 00000000..9fbe4594 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/plastic_glow_block_ctm.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/pluto.png b/src/main/resources/xia/assets/useless_mod/textures/block/pluto.png new file mode 100644 index 00000000..a619ee3f Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/pluto.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/teleport_block.png b/src/main/resources/xia/assets/useless_mod/textures/block/teleport_block.png new file mode 100644 index 00000000..e0e5456b Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/teleport_block.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/teleport_block_2.png b/src/main/resources/xia/assets/useless_mod/textures/block/teleport_block_2.png new file mode 100644 index 00000000..ad85d8ec Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/teleport_block_2.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/teleport_block_3.png b/src/main/resources/xia/assets/useless_mod/textures/block/teleport_block_3.png new file mode 100644 index 00000000..5fe30823 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/teleport_block_3.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/useless_glass_tier_1.png b/src/main/resources/xia/assets/useless_mod/textures/block/useless_glass_tier_1.png new file mode 100644 index 00000000..c41b2c39 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/useless_glass_tier_1.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/useless_glass_tier_2.png b/src/main/resources/xia/assets/useless_mod/textures/block/useless_glass_tier_2.png new file mode 100644 index 00000000..389b27d9 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/useless_glass_tier_2.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/useless_glass_tier_3.png b/src/main/resources/xia/assets/useless_mod/textures/block/useless_glass_tier_3.png new file mode 100644 index 00000000..d47eb584 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/useless_glass_tier_3.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/useless_glass_tier_4.png b/src/main/resources/xia/assets/useless_mod/textures/block/useless_glass_tier_4.png new file mode 100644 index 00000000..89897c9e Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/useless_glass_tier_4.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/useless_glass_tier_5.png b/src/main/resources/xia/assets/useless_mod/textures/block/useless_glass_tier_5.png new file mode 100644 index 00000000..0ac174c3 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/useless_glass_tier_5.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/useless_glass_tier_6.png b/src/main/resources/xia/assets/useless_mod/textures/block/useless_glass_tier_6.png new file mode 100644 index 00000000..d982b59d Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/useless_glass_tier_6.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/useless_glass_tier_7.png b/src/main/resources/xia/assets/useless_mod/textures/block/useless_glass_tier_7.png new file mode 100644 index 00000000..7de372ee Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/useless_glass_tier_7.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/useless_glass_tier_8.png b/src/main/resources/xia/assets/useless_mod/textures/block/useless_glass_tier_8.png new file mode 100644 index 00000000..aa9efa11 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/useless_glass_tier_8.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/useless_glass_tier_9.png b/src/main/resources/xia/assets/useless_mod/textures/block/useless_glass_tier_9.png new file mode 100644 index 00000000..52fc5e63 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/useless_glass_tier_9.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/block/xbai2024.png b/src/main/resources/xia/assets/useless_mod/textures/block/xbai2024.png new file mode 100644 index 00000000..b6647078 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/block/xbai2024.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/gui/advanced_alloy_furnace_tips.png b/src/main/resources/xia/assets/useless_mod/textures/gui/advanced_alloy_furnace_tips.png new file mode 100644 index 00000000..bc23b7db Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/gui/advanced_alloy_furnace_tips.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/gui/jei_advanced_alloy_furnace_gui.png b/src/main/resources/xia/assets/useless_mod/textures/gui/jei_advanced_alloy_furnace_gui.png new file mode 100644 index 00000000..f21a5dad Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/gui/jei_advanced_alloy_furnace_gui.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/gui/jei_arrow_advanced_alloy_furnace_gui.png b/src/main/resources/xia/assets/useless_mod/textures/gui/jei_arrow_advanced_alloy_furnace_gui.png new file mode 100644 index 00000000..c5c16997 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/gui/jei_arrow_advanced_alloy_furnace_gui.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/gui/jei_catalyst_info_gui.png b/src/main/resources/xia/assets/useless_mod/textures/gui/jei_catalyst_info_gui.png new file mode 100644 index 00000000..3714ae14 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/gui/jei_catalyst_info_gui.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/gui/locate_picture.png b/src/main/resources/xia/assets/useless_mod/textures/gui/locate_picture.png new file mode 100644 index 00000000..693805d1 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/gui/locate_picture.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/item/ae2_gift_package.png b/src/main/resources/xia/assets/useless_mod/textures/item/ae2_gift_package.png new file mode 100644 index 00000000..1b440ce6 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/item/ae2_gift_package.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/item/augment/augment_parallel_1.png b/src/main/resources/xia/assets/useless_mod/textures/item/augment/augment_parallel_1.png new file mode 100644 index 00000000..215054e0 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/item/augment/augment_parallel_1.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/item/augment/augment_parallel_2.png b/src/main/resources/xia/assets/useless_mod/textures/item/augment/augment_parallel_2.png new file mode 100644 index 00000000..fe30286b Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/item/augment/augment_parallel_2.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/item/augment/augment_parallel_3.png b/src/main/resources/xia/assets/useless_mod/textures/item/augment/augment_parallel_3.png new file mode 100644 index 00000000..d4284230 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/item/augment/augment_parallel_3.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/common_base.png b/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/common_base.png new file mode 100644 index 00000000..28e931d4 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/common_base.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/common_layer.png b/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/common_layer.png new file mode 100644 index 00000000..d2e0a4cb Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/common_layer.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/common_layer_silk.png b/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/common_layer_silk.png new file mode 100644 index 00000000..9695f356 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/common_layer_silk.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/crowbar.png b/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/crowbar.png new file mode 100644 index 00000000..148a878a Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/crowbar.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/crowbar_silk.png b/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/crowbar_silk.png new file mode 100644 index 00000000..59bd4f78 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/crowbar_silk.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/harmmer.png b/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/harmmer.png new file mode 100644 index 00000000..6c2588f0 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/harmmer.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/harmmer_silk.png b/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/harmmer_silk.png new file mode 100644 index 00000000..4e61746a Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/harmmer_silk.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/mallet.png b/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/mallet.png new file mode 100644 index 00000000..7ad445d6 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/mallet.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/mallet_silk.png b/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/mallet_silk.png new file mode 100644 index 00000000..03607436 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/mallet_silk.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/screwdriver.png b/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/screwdriver.png new file mode 100644 index 00000000..70b3b5ca Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/screwdriver.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/screwdriver_silk.png b/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/screwdriver_silk.png new file mode 100644 index 00000000..de07ebb0 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/screwdriver_silk.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/stick.png b/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/stick.png new file mode 100644 index 00000000..34b88ff7 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/stick.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/wrench.png b/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/wrench.png new file mode 100644 index 00000000..a62eed70 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/wrench.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/wrench_silk.png b/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/wrench_silk.png new file mode 100644 index 00000000..ce5ad84a Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/item/beef_tool/wrench_silk.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/item/ingot/useful_ingot.png b/src/main/resources/xia/assets/useless_mod/textures/item/ingot/useful_ingot.png new file mode 100644 index 00000000..c5b6bfc6 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/item/ingot/useful_ingot.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/item/ingot/useful_ingot.png.mcmeta b/src/main/resources/xia/assets/useless_mod/textures/item/ingot/useful_ingot.png.mcmeta new file mode 100644 index 00000000..88a82d79 --- /dev/null +++ b/src/main/resources/xia/assets/useless_mod/textures/item/ingot/useful_ingot.png.mcmeta @@ -0,0 +1,10 @@ +{ + "animation": { + "frametime": 40, + "interpolate": true, + "frames": [ + {"index": 0, "time": 40}, + {"index": 1, "time": 10} + ] + } +} \ No newline at end of file diff --git a/src/main/resources/xia/assets/useless_mod/textures/item/ingot/useless_ingot_tier_1.png b/src/main/resources/xia/assets/useless_mod/textures/item/ingot/useless_ingot_tier_1.png new file mode 100644 index 00000000..40171e9c Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/item/ingot/useless_ingot_tier_1.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/item/ingot/useless_ingot_tier_2.png b/src/main/resources/xia/assets/useless_mod/textures/item/ingot/useless_ingot_tier_2.png new file mode 100644 index 00000000..1f8f401b Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/item/ingot/useless_ingot_tier_2.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/item/ingot/useless_ingot_tier_3.png b/src/main/resources/xia/assets/useless_mod/textures/item/ingot/useless_ingot_tier_3.png new file mode 100644 index 00000000..9367682c Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/item/ingot/useless_ingot_tier_3.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/item/ingot/useless_ingot_tier_4.png b/src/main/resources/xia/assets/useless_mod/textures/item/ingot/useless_ingot_tier_4.png new file mode 100644 index 00000000..46b25991 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/item/ingot/useless_ingot_tier_4.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/item/ingot/useless_ingot_tier_5.png b/src/main/resources/xia/assets/useless_mod/textures/item/ingot/useless_ingot_tier_5.png new file mode 100644 index 00000000..8a7d5c68 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/item/ingot/useless_ingot_tier_5.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/item/ingot/useless_ingot_tier_6.png b/src/main/resources/xia/assets/useless_mod/textures/item/ingot/useless_ingot_tier_6.png new file mode 100644 index 00000000..0af79d8a Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/item/ingot/useless_ingot_tier_6.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/item/ingot/useless_ingot_tier_7.png b/src/main/resources/xia/assets/useless_mod/textures/item/ingot/useless_ingot_tier_7.png new file mode 100644 index 00000000..26e743d3 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/item/ingot/useless_ingot_tier_7.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/item/ingot/useless_ingot_tier_8.png b/src/main/resources/xia/assets/useless_mod/textures/item/ingot/useless_ingot_tier_8.png new file mode 100644 index 00000000..c1c4fe06 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/item/ingot/useless_ingot_tier_8.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/item/ingot/useless_ingot_tier_9.png b/src/main/resources/xia/assets/useless_mod/textures/item/ingot/useless_ingot_tier_9.png new file mode 100644 index 00000000..95d3b718 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/item/ingot/useless_ingot_tier_9.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/item/mold/metal_mold_block.png b/src/main/resources/xia/assets/useless_mod/textures/item/mold/metal_mold_block.png new file mode 100644 index 00000000..934d255a Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/item/mold/metal_mold_block.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/item/mold/metal_mold_gear.png b/src/main/resources/xia/assets/useless_mod/textures/item/mold/metal_mold_gear.png new file mode 100644 index 00000000..1cd63c66 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/item/mold/metal_mold_gear.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/item/mold/metal_mold_plate.png b/src/main/resources/xia/assets/useless_mod/textures/item/mold/metal_mold_plate.png new file mode 100644 index 00000000..60964dc5 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/item/mold/metal_mold_plate.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/item/mold/metal_mold_rod.png b/src/main/resources/xia/assets/useless_mod/textures/item/mold/metal_mold_rod.png new file mode 100644 index 00000000..8da4af14 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/item/mold/metal_mold_rod.png differ diff --git a/src/main/resources/xia/assets/useless_mod/textures/item/mold/metal_mold_wire.png b/src/main/resources/xia/assets/useless_mod/textures/item/mold/metal_mold_wire.png new file mode 100644 index 00000000..52150e03 Binary files /dev/null and b/src/main/resources/xia/assets/useless_mod/textures/item/mold/metal_mold_wire.png differ diff --git a/src/main/resources/xia/pack.mcmeta b/src/main/resources/xia/pack.mcmeta new file mode 100644 index 00000000..170cbd13 --- /dev/null +++ b/src/main/resources/xia/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "pack_format": 15, + "description": "材质来源:ae2&mek丨部分二改 by 虾比事李跌丨$丨无用模组材质优化V2.6" + } +} \ No newline at end of file diff --git a/src/main/resources/xia/pack.png b/src/main/resources/xia/pack.png new file mode 100644 index 00000000..6652145c Binary files /dev/null and b/src/main/resources/xia/pack.png differ diff --git a/src/main/resources/xia/updatelog.txt b/src/main/resources/xia/updatelog.txt new file mode 100644 index 00000000..5889e6f8 --- /dev/null +++ b/src/main/resources/xia/updatelog.txt @@ -0,0 +1,56 @@ +v1.0 +加入了三种维度方块材质 + +v1.2 +加入了矿石生成器材质 + +v1.3 +修改了塑料方块材质 + +v1.4 +修改了合金炉材质 + +v1.5 +修复合金炉材质bug +修改模具物品材质 + +v1.6 +修改了造化杖的模型方便区分模式 + +v1.7 +修改了所有的无用锭材质 + +v1.8 +修改了有用锭的材质 + +v1.9 +修改了并行控制器的材质 + +v2.0(也许是最终版本) +微调了万象合金炉的gui和jei界面 + +v2.1 +适配了新版的gui,修改了部分物品材质 + +v2.2 +修复了1.21牛排棒无法在切换精准模式时改变颜色的bug + +v2.3 +? + +v2.4 +填坑了多方块万象的外观,修复了手持时万象的模型bug,更改了jei的gui适配多物品/多模具 +(实际上ae风格的多方块完全没有必要做出来,只是书生想要而已) +如何删除多方块的纹理:找到 +/assets/useless_mod/textures/block/multiblock_alloy_furnace/ +路径下的所有文件,删掉即可 + +v2.5 +修复了监工方块书生骑的是压力锅的bug +优化了无用玻璃的材质 +微调了多方块控制器的外观 +更换了材质包封面 + +v2.6 +修复了部分材质bug +修改了牛排棒的名字让他更接近本来的意思 \ No newline at end of file diff --git a/src/main/templates/META-INF/enumextensions.json b/src/main/templates/META-INF/enumextensions.json new file mode 100644 index 00000000..34fb4c05 --- /dev/null +++ b/src/main/templates/META-INF/enumextensions.json @@ -0,0 +1,22 @@ +{ + "entries": [ + { + "enum": "net/minecraft/world/item/Rarity", + "name": "USELESS_MOD_MYTHIC", + "constructor": "(ILjava/lang/String;Ljava/util/function/UnaryOperator;)V", + "parameters": { + "class": "com/sorrowmist/useless/api/enums/RarityExtension", + "field": "MYTHIC" + } + }, + { + "enum": "net/minecraft/world/item/Rarity", + "name": "USELESS_MOD_LEGENDARY", + "constructor": "(ILjava/lang/String;Ljava/util/function/UnaryOperator;)V", + "parameters": { + "class": "com/sorrowmist/useless/api/enums/RarityExtension", + "field": "LEGENDARY" + } + } + ] +} \ No newline at end of file diff --git a/src/main/templates/META-INF/neoforge.mods.toml b/src/main/templates/META-INF/neoforge.mods.toml new file mode 100644 index 00000000..30b65025 --- /dev/null +++ b/src/main/templates/META-INF/neoforge.mods.toml @@ -0,0 +1,222 @@ +# This is an example neoforge.mods.toml file. It contains the data relating to the loading mods. +# There are several mandatory fields (#mandatory), and many more that are optional (#optional). +# The overall format is standard TOML format, v0.5.0. +# Note that there are a couple of TOML lists in this file. +# Find more information on toml format here: https://github.com/toml-lang/toml + +# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml +modLoader="javafml" #mandatory + +# A version range to match for said mod loader - for regular FML @Mod it will be the FML version. This is currently 2. +loaderVersion="${loader_version_range}" #mandatory + +# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties. +# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here. +license="${mod_license}" + +# A URL to refer people to when problems occur with this mod +#issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional + +# A list of mods - how many allowed here is determined by the individual mod loader +[[mods]] #mandatory + +# The modid of the mod +modId="${mod_id}" #mandatory + +# The version number of the mod +version="${mod_version}" #mandatory + +# A display name for the mod +displayName="${mod_name}" #mandatory + +# A URL to query for updates for this mod. See the JSON update specification https://docs.neoforged.net/docs/misc/updatechecker/ +#updateJSONURL="https://change.me.example.invalid/updates.json" #optional + +# A URL for the "homepage" for this mod, displayed in the mod UI +#displayURL="https://change.me.to.your.mods.homepage.example.invalid/" #optional + +# A file name (in the root of the mod JAR) containing a logo for display +#logoFile="examplemod.png" #optional + +# A text field displayed in the mod UI +credits="Art by 虾比、麦淇淋" #optional + +# A text field displayed in the mod UI +authors="C-H716、水幕忘忧" + +# The description text for the mod (multi line!) (#mandatory) +description='''这个mod可能很有用, 但这个mod很有用不太可能。''' + +enumExtensions = "META-INF/enumextensions.json" +# The [[mixins]] block allows you to declare your mixin config to FML so that it gets loaded. +[[mixins]] +config="${mod_id}.mixins.json" + +# 承载「紧凑方块接入 AE2 网格」的关键混入:这里故意设成 required=true, +# 一旦混入失效直接启动报错,避免又出现「看起来正常其实没生效」。 +[[mixins]] +config="${mod_id}.compact.mixins.json" + +[[mixins]] +config="${mod_id}.mekanism.mixins.json" +requiredMods=["mekanism"] + +[[mixins]] +config="${mod_id}.jade.mixins.json" +requiredMods=["jade"] + +[[mixins]] +config="${mod_id}.item_obliterator.mixins.json" +requiredMods=["item_obliterator"] + +[[mixins]] +config="${mod_id}.malum.mixins.json" +requiredMods=["malum"] + +# 无线物流的「应力」介质要在动力学方块的自身转速入口与网络合计处注入, +# 因此这份配置只在动力学模组在场时才加载。 +[[mixins]] +config="${mod_id}.create.mixins.json" +requiredMods=["create"] + +# 无线物流的「气压」介质:被无线绑定的气动方块要"视同已连接"、不再向环境漏气。 +# 注入的是 PneumaticCraft 的内部空气处理器,因此只在气动工艺在场时才加载。 +[[mixins]] +config="${mod_id}.pneumaticcraft.mixins.json" +requiredMods=["pneumaticcraft"] + +# The [[accessTransformers]] block allows you to declare where your AT file is. +# If this block is omitted, a fallback attempt will be made to load an AT from META-INF/accesstransformer.cfg +#[[accessTransformers]] +#file="META-INF/accesstransformer.cfg" + +# The coremods config file path is not configurable and is always loaded from META-INF/coremods.json + +# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional. +[[dependencies.${mod_id}]] #optional + # the modid of the dependency + modId="neoforge" #mandatory + # The type of the dependency. Can be one of "required", "optional", "incompatible" or "discouraged" (case insensitive). + # 'required' requires the mod to exist, 'optional' does not + # 'incompatible' will prevent the game from loading when the mod exists, and 'discouraged' will show a warning + type="required" #mandatory + # Optional field describing why the dependency is required or why it is incompatible + # reason="..." + # The version range of the dependency + versionRange="[21.1.1,)" #mandatory + # An ordering relationship for the dependency. + # BEFORE - This mod is loaded BEFORE the dependency + # AFTER - This mod is loaded AFTER the dependency + ordering="NONE" + # Side this dependency is applied on - BOTH, CLIENT, or SERVER + side="BOTH" + +# Here's another dependency +[[dependencies.${mod_id}]] + modId="minecraft" + type="required" + # This version range declares a minimum of the current minecraft version up to but not including the next major version + versionRange="${minecraft_version_range}" + ordering="NONE" + side="BOTH" + +# AE2 is required by the alloy-furnace crafting provider and omniversal patterns. +[[dependencies.${mod_id}]] + modId="ae2" + type="required" + versionRange="[19.2.17,20)" + ordering="AFTER" + side="BOTH" + +[[dependencies.${mod_id}]] + modId="neoecoae" + type="optional" + versionRange="[21.2.0,)" + ordering="NONE" + side="BOTH" + +[[dependencies.${mod_id}]] + modId="jdte" + type="optional" + versionRange="[0.6.0,)" + ordering="AFTER" + side="BOTH" + +# 兼容层引用 1.4.0 起提供的模糊配方类型(FlexPotRecipe / FlexStockpotRecipe)。 +[[dependencies.${mod_id}]] + modId="kaleidoscope_cookery" + type="optional" + versionRange="[1.4.0,)" + ordering="AFTER" + side="BOTH" + +# Ex Deorum 兼容层引用 3.12 起提供的实例缓存 API(RecipeUtil.getCaches 返回 RecipeCaches)。 +# 3.12 之前为静态查询形态(RecipeUtil.getHammerRecipe 等),与本模组的调用方式不兼容, +[[dependencies.${mod_id}]] + modId="exdeorum" + type="optional" + versionRange="[3.12,)" + ordering="AFTER" + side="BOTH" + +[[dependencies.${mod_id}]] + modId="botanypots" + type="optional" + versionRange="[21.1.34,)" + ordering="AFTER" + side="BOTH" + +# 无线物流的「应力」介质:转速与应力容量都靠动力学方块实体之间的传播机制承载。 +# 缺了它,该介质会从下拉列表里消失、服务端也会拒绝写入。 +[[dependencies.${mod_id}]] + modId="create" + type="optional" + versionRange="[6.0.0,6.1.0)" + ordering="AFTER" + side="BOTH" + +# 无线物流的「气压」介质:方块侧走气动工艺的空气处理器(IAirHandlerMachine)。 +# 缺了它,PRESSURE 会从下拉列表里消失、服务端也会拒绝写入。 +[[dependencies.${mod_id}]] + modId="pneumaticcraft" + type="optional" + versionRange="[8.2.0,)" + ordering="AFTER" + side="BOTH" + +# 无线物流的「AE 气压」介质:空气要进 ME 网络得靠 Applied Pneumatics 的 AirKey。 +# 缺了它,AE_PRESSURE 会从下拉列表里消失、服务端也会拒绝写入。 +[[dependencies.${mod_id}]] + modId="appliedpneumatics" + type="optional" + versionRange="[1.0.0,)" + ordering="AFTER" + side="BOTH" + +# 星辉魔法的祭坛适配器按 2.0.1.26 起的附加输入类型(NeoForge 的 SizedIngredient)编译。 +# 更早版本中附加输入的元素是 AS 内部的 CountIngredient,本模组以 SizedIngredient 访问时会在运行期 +# 抛出 ClassCastException,因此下限锁定在 2.0.1.26。 +[[dependencies.${mod_id}]] + modId="astralsorcery" + type="optional" + versionRange="[2.0.1.26,)" + ordering="AFTER" + side="BOTH" + +# 数据能源集成是软加载:入口走它自己的 @DataEnergisticsEntrypoint 扫描, +# 没装数据能源时 integration.dataenergistics 整包不会被触达,本模组照常运行,因此声明为 optional。 +# +# 下限锁定 3.3.4:本模组按数据能源 3.3.4 的 API 编译(ProcessingMatchMode、 +# BigIntegerCraftingAdmission.count() 抽象化、FastUtil ObjectList 参数),旧版缺这些符号。 +[[dependencies.${mod_id}]] + modId="data_energistics" + type="optional" + versionRange="[3.3.4,)" + ordering="AFTER" + side="BOTH" + +# Features are specific properties of the game environment, that you may want to declare you require. This example declares +# that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't +# stop your mod loading on the server for example. +#[features.${mod_id}] +#openGLVersion="[3.2,)" diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/ItemStackAmountsTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/ItemStackAmountsTest.java new file mode 100644 index 00000000..522697d0 --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/ItemStackAmountsTest.java @@ -0,0 +1,32 @@ +package com.sorrowmist.useless.compat.forge1201; + +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class ItemStackAmountsTest { + @Test void smeltingTwoPerInputDoesNotClampAwayHalfTheOutput() { + RecipeTestBootstrap.initialize(); + ItemStack result=new ItemStack(Items.IRON_INGOT,2); + result.getOrCreateTag().putString("custom","kept"); + var stacks=ItemStackAmounts.multiply(result,64); + assertEquals(128,stacks.stream().mapToInt(ItemStack::getCount).sum()); + assertEquals(2,stacks.size()); + assertTrue(stacks.stream().allMatch(s->s.getCount()<=s.getMaxStackSize())); + assertEquals("kept",stacks.get(1).getTag().getString("custom")); + stacks.get(0).getTag().putString("custom","changed"); + assertEquals("kept",stacks.get(1).getTag().getString("custom")); + assertEquals("kept",result.getTag().getString("custom")); + assertEquals(2,result.getCount()); + } + @Test void partialFinalStackAndZeroInputsAreExact() { + RecipeTestBootstrap.initialize(); + var result=new ItemStack(Items.IRON_INGOT,3); + var stacks=ItemStackAmounts.multiply(result,23); + assertEquals(69,stacks.stream().mapToInt(ItemStack::getCount).sum()); + assertEquals(5,stacks.get(1).getCount()); + assertTrue(ItemStackAmounts.multiply(result,0).isEmpty()); + assertTrue(ItemStackAmounts.multiply(ItemStack.EMPTY,64).isEmpty()); + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/RecipeTestBootstrap.java b/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/RecipeTestBootstrap.java new file mode 100644 index 00000000..ed30923e --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/RecipeTestBootstrap.java @@ -0,0 +1,25 @@ +package com.sorrowmist.useless.compat.forge1201; + +import net.minecraft.SharedConstants; +import net.minecraft.server.Bootstrap; +import net.minecraftforge.fml.loading.LoadingModList; +import java.util.List; +import static org.mockito.Mockito.*; + +public final class RecipeTestBootstrap { + private static boolean initialized; + public static synchronized void initialize() { + if (initialized) return; + SharedConstants.tryDetectVersion(); + try (var loading = mockStatic(LoadingModList.class)) { + var mods = mock(LoadingModList.class); + when(mods.getModFiles()).thenReturn(List.of()); + loading.when(LoadingModList::get).thenReturn(mods); + Bootstrap.bootStrap(); + } + net.minecraftforge.common.crafting.CraftingHelper.register(new net.minecraft.resources.ResourceLocation("forge:nbt"),net.minecraftforge.common.crafting.StrictNBTIngredient.Serializer.INSTANCE); + net.minecraftforge.common.crafting.CraftingHelper.register(new net.minecraft.resources.ResourceLocation("forge:partial_nbt"),net.minecraftforge.common.crafting.PartialNBTIngredient.Serializer.INSTANCE); + net.minecraftforge.common.crafting.CraftingHelper.register(new net.minecraft.resources.ResourceLocation("minecraft:item"),net.minecraftforge.common.crafting.VanillaIngredientSerializer.INSTANCE); + initialized = true; + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/capability/NativeCapabilityTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/capability/NativeCapabilityTest.java new file mode 100644 index 00000000..5e0ebabb --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/capability/NativeCapabilityTest.java @@ -0,0 +1,97 @@ +package com.sorrowmist.useless.compat.forge1201.capability; + +import com.sorrowmist.useless.compat.forge1201.RecipeTestBootstrap; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraftforge.common.capabilities.NativeTestCapabilities; +import net.minecraftforge.common.util.LazyOptional; +import net.minecraftforge.energy.EnergyStorage; +import net.minecraftforge.energy.IEnergyStorage; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class NativeCapabilityTest { + @BeforeAll static void bootstrap() { RecipeTestBootstrap.initialize(); } + @SuppressWarnings("unchecked") private static BlockEntityType type() { + return (BlockEntityType) mock(BlockEntityType.class); + } + private static BlockEntity entity(BlockEntityType type) { + BlockEntity entity=mock(BlockEntity.class); doReturn(type).when(entity).getType(); return entity; + } + + @Test void realNativeCapabilityTokenPreservesSidedAndUnsidedAccess() { + var registry=new BlockCapabilityRegistry(); var type=type(); var entity=entity(type); + var energy=new EnergyStorage(10000); + registry.register(NativeTestCapabilities.ENERGY,type,(be,side)->side==Direction.UP||side==null?energy:null); + var provider=registry.provider(entity); assertNotNull(provider); + var up=provider.getCapability(NativeTestCapabilities.ENERGY,Direction.UP); + assertSame(energy,up.orElseThrow(IllegalStateException::new)); + assertSame(up,provider.getCapability(NativeTestCapabilities.ENERGY,Direction.UP)); + assertSame(energy,provider.getCapability(NativeTestCapabilities.ENERGY,null).orElseThrow(IllegalStateException::new)); + assertFalse(provider.getCapability(NativeTestCapabilities.ENERGY,Direction.DOWN).isPresent()); + assertFalse(provider.getCapability(NativeTestCapabilities.ITEM_HANDLER,null).isPresent()); + assertEquals(1200,up.orElseThrow(IllegalStateException::new).receiveEnergy(1200,false)); assertEquals(1200,energy.getEnergyStored()); + } + + @Test void withdrawingAndReenablingAConfigurableSideInvalidatesOldView() { + var registry=new BlockCapabilityRegistry(); var type=type(); var entity=entity(type); + var active=new AtomicReference(new EnergyStorage(1000)); + registry.register(NativeTestCapabilities.ENERGY,type,(be,side)->active.get()); + var provider=registry.provider(entity); var old=provider.getCapability(NativeTestCapabilities.ENERGY,Direction.NORTH); + var invalidated=new AtomicBoolean(); old.addListener(ignored->invalidated.set(true)); + active.set(null); + assertFalse(provider.getCapability(NativeTestCapabilities.ENERGY,Direction.NORTH).isPresent()); + assertTrue(invalidated.get()); assertFalse(old.isPresent()); + var next=new EnergyStorage(2000); active.set(next); + assertSame(next,provider.getCapability(NativeTestCapabilities.ENERGY,Direction.NORTH).orElseThrow(IllegalStateException::new)); + } + + @Test void unloadInvalidatesAllResolvedOptionsAndRemovalPreventsTransfers() { + var registry=new BlockCapabilityRegistry(); var type=type(); var entity=entity(type); + registry.register(NativeTestCapabilities.ENERGY,type,(be,side)->new EnergyStorage(1000)); + var provider=registry.provider(entity); + var up=provider.getCapability(NativeTestCapabilities.ENERGY,Direction.UP); + var down=provider.getCapability(NativeTestCapabilities.ENERGY,Direction.DOWN); + assertTrue(up.isPresent()); assertTrue(down.isPresent()); + provider.invalidate(); assertFalse(up.isPresent()); assertFalse(down.isPresent()); + when(entity.isRemoved()).thenReturn(true); + assertFalse(provider.getCapability(NativeTestCapabilities.ENERGY,null).isPresent()); + } + + @Test void queryDoesNotLoadChunksAndPreservesRequestedSide() { + var level=mock(Level.class); var entity=mock(BlockEntity.class); var pos=new BlockPos(42,64,7); + assertNull(CapabilityAccess.get(level,NativeTestCapabilities.ENERGY,pos,Direction.WEST)); + verify(level,never()).getBlockEntity(any()); + when(level.isLoaded(pos)).thenReturn(true); when(level.getBlockEntity(pos)).thenReturn(entity); + var energy=new EnergyStorage(1000); + when(entity.getCapability(NativeTestCapabilities.ENERGY,Direction.WEST)).thenReturn(LazyOptional.of(()->energy)); + assertSame(energy,CapabilityAccess.get(level,NativeTestCapabilities.ENERGY,pos,Direction.WEST)); + when(entity.isRemoved()).thenReturn(true); + assertNull(CapabilityAccess.get(level,NativeTestCapabilities.ENERGY,pos,Direction.WEST)); + } + + @Test void staleBlockEntityFromBeforeReplacementIsNeverUsed() { + var level=mock(Level.class); var pos=BlockPos.ZERO; + when(level.isLoaded(pos)).thenReturn(true); + var stale=mock(BlockEntity.class); var current=mock(BlockEntity.class); + when(level.getBlockEntity(pos)).thenReturn(current); + var energy=new EnergyStorage(1000); + when(current.getCapability(NativeTestCapabilities.ENERGY,null)).thenReturn(LazyOptional.of(()->energy)); + assertSame(energy,CapabilityAccess.get(level,NativeTestCapabilities.ENERGY,pos,null,stale,null)); + verify(stale,never()).getCapability(any(),any()); + } + + @Test void duplicateRegistrationsFailRatherThanSilentlyChoosingAnotherHandler() { + var registry=new BlockCapabilityRegistry(); var type=type(); + registry.register(NativeTestCapabilities.ENERGY,type,(be,side)->new EnergyStorage(1)); + assertThrows(IllegalStateException.class,()->registry.register(NativeTestCapabilities.ENERGY,type,(be,side)->new EnergyStorage(2))); + assertNull(registry.provider(entity(type()))); + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/component/ComponentAccessTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/component/ComponentAccessTest.java new file mode 100644 index 00000000..d6ac6223 --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/component/ComponentAccessTest.java @@ -0,0 +1,126 @@ +package com.sorrowmist.useless.compat.forge1201.component; + +import com.mojang.serialization.Codec; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; +import org.junit.jupiter.api.Test; +import java.util.List; +import static org.junit.jupiter.api.Assertions.*; + +class ComponentAccessTest { + private static DataComponentType key(String name, Codec codec) { + DataComponentType key=DataComponentType.builder().persistent(codec).build(); + key.bind(new ResourceLocation("useless_mod",name)); + return key; + } + + @Test void customStateRoundTripsAsVanillaNbt() { + var enabled=key("acceleration",Codec.BOOL); + var molds=key("selected_molds",Codec.STRING.listOf()); + CompoundTag root=new CompoundTag(); + root.putString("OtherModState","keep"); + ComponentAccess.writeTag(root,enabled,true); + ComponentAccess.writeTag(root,molds,List.of("mekanism:crusher","create:mixing")); + CompoundTag restored=root.copy(); + assertEquals(true,ComponentAccess.readTag(restored,enabled)); + assertEquals(List.of("mekanism:crusher","create:mixing"),ComponentAccess.readTag(restored,molds)); + assertEquals("keep",restored.getString("OtherModState")); + } + + @Test void removingOneValuePreservesOtherKeysAndForeignNbt() { + var first=key("first",Codec.INT); + var second=key("second",Codec.INT); + CompoundTag root=new CompoundTag();root.putInt("Count",64); + ComponentAccess.writeTag(root,first,4096); + ComponentAccess.writeTag(root,second,1024); + ComponentAccess.writeTag(root,first,null); + assertNull(ComponentAccess.readTag(root,first)); + assertEquals(1024,ComponentAccess.readTag(root,second)); + ComponentAccess.writeTag(root,second,null); + assertFalse(root.contains(ComponentAccess.ROOT_KEY)); + assertEquals(64,root.getInt("Count")); + } + + @Test void unboundOrReboundKeysCannotSilentlyCollide() { + var key=DataComponentType.builder().persistent(Codec.INT).build(); + assertThrows(NullPointerException.class, key::id); + key.bind(new ResourceLocation("useless_mod","first")); + assertThrows(IllegalStateException.class,()->key.bind(new ResourceLocation("useless_mod","second"))); + } + @Test void deferredKeysResolveAtUseTimeAndDoNotTouchForeignNbt() { + var key=key("deferred",Codec.INT); + java.util.function.Supplier> supplier=()->key; + com.sorrowmist.useless.compat.forge1201.RecipeTestBootstrap.initialize(); + var stack=new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.DIAMOND); + CompoundTag tag=new CompoundTag(); tag.putString("foreign","keep"); + stack.setTag(tag); + assertNull(ComponentAccess.get(stack,supplier)); + ComponentAccess.set(stack,supplier,42); + assertTrue(ComponentAccess.has(stack,supplier)); + ComponentAccess.update(stack,supplier,0,n->n+1); + assertEquals(43,ComponentAccess.getOrDefault(stack,supplier,0)); + assertEquals(43,ComponentAccess.remove(stack,supplier)); + assertFalse(ComponentAccess.has(stack,supplier)); + assertEquals("keep",tag.getString("foreign")); + } + + private static net.minecraft.world.item.ItemStack prototypeStack(DataComponentType key,boolean value) { + com.sorrowmist.useless.compat.forge1201.RecipeTestBootstrap.initialize(); + var item=org.mockito.Mockito.mock(net.minecraft.world.item.Item.class, + org.mockito.Mockito.withSettings().extraInterfaces(ComponentDefaults.class)); + org.mockito.Mockito.when(((ComponentDefaults)item).componentDefault(key)).thenReturn(value); + var stack=org.mockito.Mockito.spy(new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.DIAMOND)); + org.mockito.Mockito.doReturn(item).when(stack).getItem(); + return stack; + } + + @Test void prototypePresenceExplicitFalseAndRemovalAreDistinct() { + var enabled=key("prototype_enabled",Codec.BOOL); + var stack=prototypeStack(enabled,true); + assertTrue(ComponentAccess.has(stack,enabled)); + assertEquals(true,ComponentAccess.get(stack,enabled)); + assertNull(stack.getTag()); // querying never dirties the stack + ComponentAccess.set(stack,enabled,false); + assertEquals(false,ComponentAccess.get(stack,enabled)); + assertTrue(ComponentAccess.has(stack,enabled)); + assertEquals(false,ComponentAccess.remove(stack,enabled)); + assertFalse(ComponentAccess.has(stack,enabled)); + assertNull(ComponentAccess.get(stack,enabled)); + assertEquals(true,ComponentAccess.getOrDefault(stack,enabled,true)); + var restored=prototypeStack(enabled,true); + restored.setTag(stack.getTag().copy()); + assertNull(ComponentAccess.get(restored,enabled)); // removal survives native stack NBT + ComponentAccess.set(restored,enabled,true); + assertTrue(ComponentAccess.has(restored,enabled)); + assertFalse(restored.getTag().contains(ComponentAccess.REMOVED_KEY)); + } + + @Test void nullSetMasksAPrototypeButNotAnUnrelatedNamespace() { + var enabled=key("prototype_null",Codec.BOOL); + var foreign=DataComponentType.builder().persistent(Codec.BOOL).build(); + foreign.bind(new ResourceLocation("another_mod","prototype_null")); + var stack=prototypeStack(enabled,false); + assertTrue(ComponentAccess.has(stack,enabled)); + assertNull(ComponentAccess.get(stack,foreign)); + assertEquals(false,ComponentAccess.set(stack,enabled,null)); + assertNull(ComponentAccess.get(stack,enabled)); + assertFalse(ComponentAccess.has(stack,enabled)); + ComponentAccess.set(stack,enabled,false); + assertEquals(false,ComponentAccess.get(stack,enabled)); + } + + @Test void copyingNativeStatePreservesForeignNbtAndDoesNotAliasTheSource() { + com.sorrowmist.useless.compat.forge1201.RecipeTestBootstrap.initialize(); + var key=key("copied_enabled",Codec.BOOL); + var source=new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.DIAMOND); + source.getOrCreateTag().putString("foreign", "keep"); + ComponentAccess.set(source,key,false); + var target=new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.EMERALD); + ComponentAccess.copyState(source,target,List.of(()->key)); + assertEquals(net.minecraft.world.item.Items.EMERALD,target.getItem()); + assertEquals(false,ComponentAccess.get(target,key)); + assertEquals("keep",target.getTag().getString("foreign")); + target.getTag().putString("foreign","changed"); + assertEquals("keep",source.getTag().getString("foreign")); + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/enderio/NativeSoulPortTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/enderio/NativeSoulPortTest.java new file mode 100644 index 00000000..733c6b5d --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/enderio/NativeSoulPortTest.java @@ -0,0 +1,112 @@ +package com.sorrowmist.useless.compat.forge1201.enderio; + +import com.enderio.api.capability.StoredEntityData; +import com.enderio.base.common.capability.EntityStorageItemStack; +import com.sorrowmist.useless.compat.forge1201.RecipeTestBootstrap; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeCodecs; +import io.netty.buffer.Unpooled; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraftforge.common.crafting.CraftingHelper; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import java.util.List; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class NativeSoulPortTest { + @BeforeAll static void bootstrap() { + RecipeTestBootstrap.initialize(); + CraftingHelper.register(new ResourceLocation("useless_mod:filled_soul"), FilledSoulIngredient.TYPE); + } + + private static StoredEntityData captured() { + CompoundTag entity = new CompoundTag(); + entity.putString("id", "minecraft:zombie"); + entity.putFloat("Health", 13.5F); + CompoundTag other = new CompoundTag(); + other.putLong("foreign:progress", 123456789012L); + entity.put("ForgeData", other); + return new StoredEntityData(entity, 40.0F); + } + + @Test void copiesPreserveAllCapturedDataAndHealthWithoutAliasing() { + var original = captured(); + var soul = NativeSoul.from(original); + var snapshot = original.serializeNBT().copy(); + original.getEntityTag().getCompound("ForgeData").putLong("foreign:progress", 0); + assertEquals(snapshot, soul.storedData().serializeNBT()); + assertEquals(EntityType.ZOMBIE, soul.entityType()); + assertEquals(new ResourceLocation("minecraft:zombie"), soul.entityTypeId()); + soul.storedData().getEntityTag().putString("id", "minecraft:skeleton"); + assertEquals(snapshot, soul.copy().storedData().serializeNBT()); + assertEquals(13.5F, soul.storedData().getHealthState().orElseThrow().getA()); + assertEquals(40.0F, soul.storedData().getHealthState().orElseThrow().getB()); + } + + @Test void realEnderIoStorageReadsWritesAndCopiesItsNativeBlockEntityTag() { + var stack = new ItemStack(Items.GLASS_BOTTLE); + var storage = new EntityStorageItemStack(stack); + var soul = NativeSoul.from(captured()); + assertTrue(NativeSoulStorage.read(storage).isEmpty()); + assertTrue(NativeSoulStorage.bind(storage, soul)); + assertEquals(soul.storedData().serializeNBT(), + stack.getTag().getCompound("BlockEntityTag").getCompound("EntityStorage")); + assertEquals(soul.storedData().serializeNBT(), NativeSoulStorage.read(storage).storedData().serializeNBT()); + var copied = ItemStack.of(stack.save(new CompoundTag())); + assertEquals(soul.storedData().serializeNBT(), + NativeSoulStorage.read(new EntityStorageItemStack(copied)).storedData().serializeNBT()); + NativeSoulStorage.read(storage).storedData().getEntityTag().remove("ForgeData"); + assertEquals(123456789012L, storage.getStoredEntityData().getEntityTag() + .getCompound("ForgeData").getLong("foreign:progress")); + } + + @Test void emptyUnknownAndMissingStorageAreNotInventedAsValidEntities() { + assertTrue(NativeSoulStorage.read(null).isEmpty()); + assertFalse(NativeSoulStorage.bind(null, NativeSoul.of(EntityType.ZOMBIE))); + var storage = new EntityStorageItemStack(new ItemStack(Items.GLASS_BOTTLE)); + assertFalse(NativeSoulStorage.bind(storage, NativeSoul.empty())); + assertFalse(NativeSoulStorage.bind(storage, null)); + assertTrue(NativeSoul.empty().isEmpty()); + assertNull(NativeSoul.empty().entityType()); + var missing = NativeSoul.of(new ResourceLocation("missing_mod:captured_mob")); + assertFalse(missing.isEmpty()); // Retain opaque captured data if its mod is not currently present. + assertNull(missing.entityType()); + assertEquals(new ResourceLocation("missing_mod:captured_mob"), missing.copy().entityTypeId()); + } + + @Test void filledPredicateRoundTripsWithoutFlatteningCapturedEntityNbt() { + // Only the Forge capability lookup boundary is replaced; use real Ender IO data and + // EntityStorageItemStack for the predicate's NBT read on every invocation. + try (var lookup = mockStatic(NativeSoulStorage.class)) { + lookup.when(() -> NativeSoulStorage.getBoundSoul(any(ItemStack.class))).thenAnswer(call -> + NativeSoul.from(new EntityStorageItemStack(call.getArgument(0)).getStoredEntityData())); + var full = new ItemStack(Items.GLASS_BOTTLE); + new EntityStorageItemStack(full).setStoredEntityData(captured()); + var source = FilledSoulIngredient.of(Items.GLASS_BOTTLE); + var json = Ingredient.fromJson(source.toJson()); + var buffer = new FriendlyByteBuf(Unpooled.buffer()); + try { + RecipeCodecs.INGREDIENT_STREAM.encode(buffer, source); + var wire = RecipeCodecs.INGREDIENT_STREAM.decode(buffer); + for (var ingredient : List.of(source, json, wire)) { + assertInstanceOf(FilledSoulIngredient.class, ingredient); + assertTrue(ingredient.test(full.copyWithCount(64))); + var changed = full.copy(); changed.getOrCreateTag().putString("other_mod:label", "anything"); + assertTrue(ingredient.test(changed)); + assertFalse(ingredient.test(new ItemStack(Items.GLASS_BOTTLE))); + assertFalse(ingredient.test(ItemStack.EMPTY)); + assertFalse(ingredient.test(null)); + var wrongItem = new ItemStack(Items.DIAMOND); wrongItem.setTag(full.getTag().copy()); + assertFalse(ingredient.test(wrongItem)); + } + assertEquals(0, buffer.readableBytes()); + } finally { buffer.release(); } + } + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/fluid/FluidIngredientTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/fluid/FluidIngredientTest.java new file mode 100644 index 00000000..1b1564f9 --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/fluid/FluidIngredientTest.java @@ -0,0 +1,69 @@ +package com.sorrowmist.useless.compat.forge1201.fluid; + +import com.google.gson.JsonParser; +import com.mojang.serialization.JsonOps; +import com.sorrowmist.useless.compat.forge1201.RecipeTestBootstrap; +import io.netty.buffer.Unpooled; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fluids.FluidStack; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import java.util.*; +import static org.junit.jupiter.api.Assertions.*; + +class FluidIngredientTest { + @BeforeAll static void bootstrap() { RecipeTestBootstrap.initialize(); } + private static FluidStack water(String value) { + FluidStack stack=new FluidStack(Fluids.WATER,1000); + stack.getOrCreateTag().putString("grade",value); return stack; + } + + @Test void exactAndPartialNbtNeverCollapseToPlainFluid() { + var expected=water("pure"); var extra=expected.copy(); extra.getOrCreateTag().putInt("extra",1); + var exact=FluidIngredient.nbt(true,expected); var partial=FluidIngredient.nbt(false,expected); + expected.getOrCreateTag().putString("grade","mutated"); + assertTrue(exact.test(water("pure"))); assertFalse(exact.test(water("dirty"))); + assertFalse(exact.test(extra)); assertTrue(partial.test(extra)); + var representative=exact.getStacks()[0]; representative.getOrCreateTag().putString("grade","mutated"); + assertTrue(exact.test(water("pure"))); + } + + @Test void compoundKeepsEveryAlternativeAcrossJsonAndNetwork() { + var ingredient=FluidIngredient.any(List.of(FluidIngredient.nbt(true,water("pure")),FluidIngredient.single(Fluids.LAVA))); + var decoded=FluidIngredient.fromJson(ingredient.toJson()); + assertEquals(ingredient,decoded); + FriendlyByteBuf buf=new FriendlyByteBuf(Unpooled.buffer()); + try { + FluidIngredient.STREAM_CODEC.encode(buf,decoded); decoded=FluidIngredient.STREAM_CODEC.decode(buf); + assertTrue(decoded.test(water("pure"))); assertFalse(decoded.test(water("dirty"))); + assertTrue(decoded.test(new FluidStack(Fluids.LAVA,1))); assertEquals(0,buf.readableBytes()); + } finally { buf.release(); } + } + + @Test void fluidTagResolvesAfterBindingAndReloadWithoutSnapshot() { + TagKey tag=TagKey.create(Registries.FLUID,new ResourceLocation("useless_port_test:live_fluid")); + var ingredient=FluidIngredient.tag(tag); + assertFalse(ingredient.isEmpty()); // Unbound tags must not get dropped by recipe normalization. + assertEquals(tag.location().toString(),ingredient.toJson().getAsJsonObject().get("tag").getAsString()); + BuiltInRegistries.FLUID.bindTags(Map.of(tag,List.of(BuiltInRegistries.FLUID.wrapAsHolder(Fluids.WATER)))); + assertTrue(ingredient.test(new FluidStack(Fluids.WATER,1))); + assertFalse(ingredient.test(new FluidStack(Fluids.LAVA,1))); + BuiltInRegistries.FLUID.bindTags(Map.of(tag,List.of(BuiltInRegistries.FLUID.wrapAsHolder(Fluids.LAVA)))); + assertFalse(ingredient.test(new FluidStack(Fluids.WATER,1))); + assertTrue(ingredient.test(new FluidStack(Fluids.LAVA,1))); + assertSame(Fluids.LAVA,ingredient.getStacks()[0].getFluid()); + } + + @Test void invalidOrUnknownPredicatesAreNotAcceptedAsOrdinaryFluid() { + for(String json:List.of("{\"fluid\":\"missing:fluid\"}","{\"type\":\"unknown:predicate\",\"fluid\":\"minecraft:water\"}", + "{\"fluid\":\"minecraft:water\",\"components\":{\"other:state\":1}}")) + assertTrue(FluidIngredient.CODEC.parse(JsonOps.INSTANCE,JsonParser.parseString(json)).error().isPresent()); + assertThrows(IllegalArgumentException.class,()->new SizedFluidIngredient(FluidIngredient.single(Fluids.WATER),0)); + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/mekanism/NativeStrictEnergyHandlerTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/mekanism/NativeStrictEnergyHandlerTest.java new file mode 100644 index 00000000..ae554d6f --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/mekanism/NativeStrictEnergyHandlerTest.java @@ -0,0 +1,84 @@ +package com.sorrowmist.useless.compat.forge1201.mekanism; + +import com.sorrowmist.useless.energy.EnergyManager; +import mekanism.api.Action; +import mekanism.api.energy.IEnergyConversion; +import mekanism.api.math.FloatingLong; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class NativeStrictEnergyHandlerTest { + private static FloatingLong joules(String value) { return FloatingLong.parseFloatingLong(value); } + private static IEnergyConversion converter(String factor,boolean enabled) { + FloatingLong multiplier=joules(factor); + return new IEnergyConversion() { + public boolean isEnabled() {return enabled;} + public String getTranslationKey() {return "test.energy";} + public FloatingLong convertFrom(FloatingLong amount) {return amount.multiply(multiplier);} + public FloatingLong convertInPlaceFrom(FloatingLong amount) {return amount.timesEqual(multiplier);} + public FloatingLong convertTo(FloatingLong amount) {return amount.divide(multiplier);} + public FloatingLong convertInPlaceTo(FloatingLong amount) {return amount.divideEquals(multiplier);} + }; + } + @Test void fractionalJoulesNeverRoundUpToFreeFeOrLoseTheUnacceptedRemainder() { + var energy=new EnergyManager(1000); + var handler=new NativeStrictEnergyHandler(energy,converter("2.5",true)); + var amount=joules("4"); + assertEquals(joules("1.5"),handler.insertEnergy(amount,Action.EXECUTE)); + assertEquals(1,energy.getEnergyStoredLong()); assertEquals(joules("4"),amount); + assertEquals(joules("2.5"),handler.extractEnergy(amount,Action.EXECUTE)); + assertEquals(0,energy.getEnergyStoredLong()); + assertEquals(joules("2.4999"),handler.insertEnergy(joules("2.4999"),Action.EXECUTE)); + assertEquals(0,energy.getEnergyStoredLong()); + } + @Test void longTransfersAreNotTruncatedToAnIntAndSimulationDoesNotMutateStorage() { + long amount=5_000_000_000_000L; + var energy=new EnergyManager(amount*2); + var handler=new NativeStrictEnergyHandler(energy,converter("2.5",true)); + var nativeAmount=FloatingLong.create(amount).multiply(joules("2.5")); + assertEquals(FloatingLong.ZERO,handler.insertEnergy(nativeAmount,Action.SIMULATE)); + assertEquals(0,energy.getEnergyStoredLong()); + assertEquals(FloatingLong.ZERO,handler.insertEnergy(nativeAmount,Action.EXECUTE)); + assertEquals(amount,energy.getEnergyStoredLong()); + assertEquals(nativeAmount,handler.extractEnergy(nativeAmount,Action.SIMULATE)); + assertEquals(amount,energy.getEnergyStoredLong()); + assertEquals(nativeAmount,handler.extractEnergy(nativeAmount,Action.EXECUTE)); + assertEquals(0,energy.getEnergyStoredLong()); + } + @Test void capacityTransferLimitsAndInvalidContainerIndexesAreRespected() { + var energy=new EnergyManager(10,3); + var handler=new NativeStrictEnergyHandler(energy,converter("2.5",true)); + assertEquals(joules("17.5"),handler.insertEnergy(joules("25"),Action.EXECUTE)); + assertEquals(3,energy.getEnergyStoredLong()); + assertEquals(joules("25"),handler.insertEnergy(1,joules("25"),Action.EXECUTE)); + assertEquals(FloatingLong.ZERO,handler.extractEnergy(1,joules("25"),Action.EXECUTE)); + handler.setEnergy(0,joules("100")); assertEquals(10,energy.getEnergyStoredLong()); + assertEquals(joules("25"),handler.getMaxEnergy(0)); + assertEquals(FloatingLong.ZERO,handler.getNeededEnergy(0)); + assertEquals(joules("7.5"),handler.extractEnergy(joules("100"),Action.EXECUTE)); + assertEquals(7,energy.getEnergyStoredLong()); + } + @Test void disabledConversionCannotBypassMekanismConfiguration() { + var energy=new EnergyManager(100); energy.setEnergyStored(50); + var handler=new NativeStrictEnergyHandler(energy,converter("2.5",false)); + assertEquals(joules("25"),handler.insertEnergy(joules("25"),Action.EXECUTE)); + assertEquals(FloatingLong.ZERO,handler.extractEnergy(joules("25"),Action.EXECUTE)); + assertEquals(50,energy.getEnergyStoredLong()); + } + @Test void awkwardConfigurableRatiosConserveEnergyAcrossRepeatedInsertExtractCycles() { + for(String ratio:new String[]{"0.3","1","2.5","3.1415","10000"}) { + var energy=new EnergyManager(100_000_000); + var handler=new NativeStrictEnergyHandler(energy,converter(ratio,true)); + for(int i=1;i<=50;i++) { + var offered=FloatingLong.create(i).multiply(joules(ratio)).add(joules("0.0001")); + var before=offered.copy(); + var remainder=handler.insertEnergy(offered,Action.EXECUTE); + var extracted=handler.extractEnergy(offered,Action.EXECUTE); + assertTrue(extracted.smallerOrEqual(offered)); + assertEquals(offered.subtract(remainder),extracted,ratio+" round "+i); + assertEquals(before,offered); + assertEquals(0,energy.getEnergyStoredLong()); + } + } + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/nbt/NativeStackNbtTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/nbt/NativeStackNbtTest.java new file mode 100644 index 00000000..5d6d9647 --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/nbt/NativeStackNbtTest.java @@ -0,0 +1,49 @@ +package com.sorrowmist.useless.compat.forge1201.nbt; + +import com.sorrowmist.useless.compat.forge1201.RecipeTestBootstrap; +import net.minecraft.core.RegistryAccess; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fluids.FluidStack; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class NativeStackNbtTest { + @BeforeAll static void bootstrap() { RecipeTestBootstrap.initialize(); } + @Test void largeStacksKeepFullCountsAndNestedForeignTagsWithoutSharing() { + var stack = new ItemStack(Items.IRON_INGOT, 2_000_000_000); + var foreign = new CompoundTag(); foreign.putIntArray("vector", new int[]{1, 2, 3}); + stack.getOrCreateTag().put("othermod", foreign); + var saved = NativeStackNbt.saveItem(stack, RegistryAccess.EMPTY); + var restored = NativeStackNbt.readItem(RegistryAccess.EMPTY, saved); + assertEquals(2_000_000_000, restored.getCount()); + assertEquals(stack.getTag(), restored.getTag()); + restored.getTag().getCompound("othermod").putInt("changed", 1); + assertFalse(saved.getCompound("tag").getCompound("othermod").contains("changed")); + assertFalse(stack.getTag().getCompound("othermod").contains("changed")); + var normal = NativeStackNbt.saveItem(new ItemStack(Items.IRON_INGOT, 64), RegistryAccess.EMPTY); + assertFalse(normal.contains("RealCount")); + assertEquals(64, ItemStack.of(normal).getCount()); + } + @Test void fluidIdentityAmountAndCompoundTagsUseNativeForgeFormat() { + var fluid = new FluidStack(Fluids.WATER, 2_000_000_000); + var tag = new CompoundTag(); tag.putString("grade", "ultrapure"); fluid.setTag(tag); + var saved = NativeStackNbt.saveFluid(fluid, RegistryAccess.EMPTY); + assertEquals("minecraft:water", saved.getString("FluidName")); + var restored = NativeStackNbt.readFluid(RegistryAccess.EMPTY, saved); + assertEquals(2_000_000_000, restored.getAmount()); + assertTrue(fluid.isFluidEqual(restored)); + restored.getTag().putString("grade", "changed"); + assertEquals("ultrapure", saved.getCompound("Tag").getString("grade")); + } + @Test void nonemptyNewerComponentPayloadCannotBeSilentlyDiscarded() { + var saved = NativeStackNbt.saveItem(new ItemStack(Items.DIAMOND), RegistryAccess.EMPTY); + var unsupported = new CompoundTag(); unsupported.putInt("foreign:data", 2); + saved.put("components", unsupported); + assertThrows(IllegalArgumentException.class, () -> NativeStackNbt.readItem(RegistryAccess.EMPTY, saved)); + assertThrows(IllegalArgumentException.class, () -> NativeStackNbt.readFluid(RegistryAccess.EMPTY, saved)); + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/network/StreamCodecTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/network/StreamCodecTest.java new file mode 100644 index 00000000..22edde6a --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/network/StreamCodecTest.java @@ -0,0 +1,61 @@ +package com.sorrowmist.useless.compat.forge1201.network; + +import io.netty.buffer.Unpooled; +import net.minecraft.network.FriendlyByteBuf; +import org.junit.jupiter.api.Test; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import static org.junit.jupiter.api.Assertions.*; + +class StreamCodecTest { + record Settings(int speed, boolean enabled, String name) {} + + @Test void compositeRoundTripsInFieldOrder() { + StreamCodec codec = StreamCodec.composite( + ByteBufCodecs.VAR_INT, Settings::speed, + ByteBufCodecs.BOOL, Settings::enabled, + ByteBufCodecs.STRING_UTF8, Settings::name, Settings::new); + FriendlyByteBuf buffer = new FriendlyByteBuf(Unpooled.buffer()); + try { + Settings value = new Settings(1024, true, "范围加速 · 1.20.1"); + codec.encode(buffer, value); + assertEquals(value, codec.decode(buffer)); + assertFalse(buffer.isReadable()); + } finally { buffer.release(); } + } + + @Test void varLongPreservesCountsAboveIntegerRange() { + FriendlyByteBuf buffer = new FriendlyByteBuf(Unpooled.buffer()); + try { + for (long value : new long[]{0, 1, Integer.MAX_VALUE + 1L, Long.MAX_VALUE}) { + ByteBufCodecs.VAR_LONG.encode(buffer, value); + assertEquals(value, ByteBufCodecs.VAR_LONG.decode(buffer)); + } + } finally { buffer.release(); } + } + + @Test void collectionsRoundTripAndRejectInvalidLengths() { + StreamCodec> codec = ByteBufCodecs.collection(ArrayList::new, ByteBufCodecs.VAR_INT); + FriendlyByteBuf buffer = new FriendlyByteBuf(Unpooled.buffer()); + try { + codec.encode(buffer, List.of(1, 128, 4096)); + assertEquals(List.of(1, 128, 4096), codec.decode(buffer)); + buffer.clear(); buffer.writeVarInt(-1); + assertThrows(IllegalArgumentException.class, () -> codec.decode(buffer)); + buffer.clear(); buffer.writeVarInt(65537); + assertThrows(IllegalArgumentException.class, () -> codec.decode(buffer)); + } finally { buffer.release(); } + } + + @Test void optionalAndMappedCodecsRoundTrip() { + StreamCodec> codec = ByteBufCodecs.optional(ByteBufCodecs.STRING_UTF8.cast()); + FriendlyByteBuf buffer = new FriendlyByteBuf(Unpooled.buffer()); + try { + codec.encode(buffer, Optional.empty()); + codec.encode(buffer, Optional.of("样板")); + assertEquals(Optional.empty(), codec.decode(buffer)); + assertEquals(Optional.of("样板"), codec.decode(buffer)); + } finally { buffer.release(); } + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/recipe/RecipeCodecsTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/recipe/RecipeCodecsTest.java new file mode 100644 index 00000000..0cde81b4 --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/recipe/RecipeCodecsTest.java @@ -0,0 +1,71 @@ +package com.sorrowmist.useless.compat.forge1201.recipe; + +import com.google.gson.JsonParser; +import com.mojang.serialization.JsonOps; +import com.sorrowmist.useless.compat.forge1201.RecipeTestBootstrap; +import com.sorrowmist.useless.compat.forge1201.network.PortCodecs; +import io.netty.buffer.Unpooled; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class RecipeCodecsTest { + @BeforeAll static void bootstrap() { RecipeTestBootstrap.initialize(); } + + @Test void itemJsonPreservesNbtAndLargeIntCount() { + var stack=RecipeCodecs.readItem(JsonParser.parseString(""" + {"item":"minecraft:diamond","count":32768,"nbt":"{OtherModState:{value:17},Enchantments:[{id:\"minecraft:fortune\",lvl:3s}]}"} + """.replace("id:\"minecraft", "id:\\\"minecraft").replace("fortune\",lvl", "fortune\\\",lvl"))); + var restored=RecipeCodecs.readItem(RecipeCodecs.writeItem(stack)); + assertEquals(32768,restored.getCount()); + assertEquals(stack.getTag(),restored.getTag()); + assertEquals(17,restored.getTag().getCompound("OtherModState").getInt("value")); + } + + @Test void packetKeepsCountAboveByteRangeAndAllNbt() { + ItemStack stack=new ItemStack(Items.DIAMOND,50000); + stack.getOrCreateTag().putString("ForeignMod","preserved"); + CompoundTag custom=new CompoundTag(); custom.putLong("operations",Long.MAX_VALUE); + stack.getOrCreateTag().put("useless_mod:components",custom); + FriendlyByteBuf buffer=new FriendlyByteBuf(Unpooled.buffer()); + try { + PortCodecs.ITEM_STACK.encode(buffer,stack); + ItemStack decoded=PortCodecs.ITEM_STACK.decode(buffer); + assertEquals(50000,decoded.getCount()); + assertEquals(stack.getTag(),decoded.getTag()); + assertEquals(0,buffer.readableBytes()); + } finally { buffer.release(); } + } + + @Test void unknownComponentsItemsOrFractionalCountsFailInsteadOfDroppingData() { + for(String json:new String[]{ + "{\"item\":\"missing:thing\"}", + "{\"item\":\"minecraft:diamond\",\"count\":1.5}", + "{\"item\":\"minecraft:diamond\",\"count\":2147483648}", + "{\"item\":\"minecraft:diamond\",\"components\":{\"other:state\":true}}"}) + assertTrue(RecipeCodecs.ITEM_STACK.parse(JsonOps.INSTANCE,JsonParser.parseString(json)).error().isPresent(),json); + } + + @Test void nativeNbtItemIngredientKeepsExactAndPartialSemantics() { + ItemStack required=new ItemStack(Items.DIAMOND); required.getOrCreateTag().putString("name","required"); + ItemStack extra=required.copy(); extra.getOrCreateTag().putInt("extra",1); + assertTrue(DataComponentIngredient.of(true,required).test(required.copyWithCount(64))); + assertFalse(DataComponentIngredient.of(true,required).test(extra)); + assertTrue(DataComponentIngredient.of(false,required).test(extra)); + assertFalse(DataComponentIngredient.of(false,required).test(new ItemStack(Items.DIAMOND))); + } + + @Test void nativeCraftingGridCopiesInputsAndPreservesDimensionsAndEmptySlots() { + var original=new ItemStack(Items.DIAMOND,64); + var grid=CraftingInputs.of(2,1,java.util.List.of(original,ItemStack.EMPTY)); + assertEquals(2,grid.getWidth()); assertEquals(1,grid.getHeight()); + grid.removeItem(0,1); + assertEquals(64,original.getCount()); assertEquals(63,grid.getItem(0).getCount()); + assertTrue(grid.getItem(1).isEmpty()); + assertThrows(IllegalArgumentException.class,()->CraftingInputs.of(3,3,java.util.List.of(original))); + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/recipe/RecipeManagerAccessTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/recipe/RecipeManagerAccessTest.java new file mode 100644 index 00000000..080bf029 --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/recipe/RecipeManagerAccessTest.java @@ -0,0 +1,36 @@ +package com.sorrowmist.useless.compat.forge1201.recipe; + +import net.minecraft.core.RegistryAccess; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.Container; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.*; +import net.minecraft.world.level.Level; +import org.junit.jupiter.api.Test; +import java.util.List; +import static org.junit.jupiter.api.Assertions.*; + +class RecipeManagerAccessTest { + private static final RecipeType TYPE=new RecipeType<>() {}; + private record TestRecipe(ResourceLocation id) implements Recipe { + public boolean matches(Container input,Level level) { return false; } + public ItemStack assemble(Container input,RegistryAccess registry) { return ItemStack.EMPTY; } + public boolean canCraftInDimensions(int width,int height) { return true; } + public ItemStack getResultItem(RegistryAccess registry) { return ItemStack.EMPTY; } + public ResourceLocation getId() { return id; } + public RecipeSerializer getSerializer() { return null; } + public RecipeType getType() { return TYPE; } + } + @Test void runtimeIdsArePreservedAndReloadDoesNotReturnStaleRecipes() { + RecipeManager manager=new RecipeManager(); + var original=new TestRecipe(new ResourceLocation("kubejs","pack_generated_original")); + var replacement=new TestRecipe(new ResourceLocation("kubejs","pack_reloaded_replacement")); + manager.replaceRecipes(List.of(original)); + assertEquals(original.id(),RecipeManagerAccess.getAllRecipesFor(manager,TYPE).get(0).id()); + assertSame(original,RecipeManagerAccess.byKey(manager,original.id()).orElseThrow().value()); + manager.replaceRecipes(List.of(replacement)); + assertTrue(RecipeManagerAccess.byKey(manager,original.id()).isEmpty()); + assertEquals(List.of(RecipeHolder.of(replacement)),RecipeManagerAccess.getAllRecipesFor(manager,TYPE)); + assertEquals(1,RecipeManagerAccess.getRecipes(manager).size()); + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/recipe/TopLevelNbtIngredientTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/recipe/TopLevelNbtIngredientTest.java new file mode 100644 index 00000000..f02b0f5e --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/compat/forge1201/recipe/TopLevelNbtIngredientTest.java @@ -0,0 +1,59 @@ +package com.sorrowmist.useless.compat.forge1201.recipe; + +import com.sorrowmist.useless.compat.forge1201.RecipeTestBootstrap; +import io.netty.buffer.Unpooled; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraftforge.common.crafting.CraftingHelper; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class TopLevelNbtIngredientTest { + @BeforeAll static void bootstrap() { + RecipeTestBootstrap.initialize(); + CraftingHelper.register(new ResourceLocation("useless_mod:top_level_nbt"),TopLevelNbtIngredient.TYPE); + } + private static ItemStack matrix() { + var stack=new ItemStack(Items.DIAMOND); var inner=new CompoundTag();inner.putInt("level",3); + stack.getOrCreateTag().put("MatrixState",inner);return stack; + } + @Test void matchesNativeHnnTopLevelNbtRuleNotRecursivePartialNbt() { + var required=matrix(); var ingredient=new TopLevelNbtIngredient(required); + var extraTop=required.copy(); extraTop.getOrCreateTag().putString("Unrelated","allowed"); + assertTrue(ingredient.test(extraTop)); + var extraNested=required.copy(); extraNested.getTag().getCompound("MatrixState").putString("Unrelated","rejected"); + assertFalse(ingredient.test(extraNested)); + assertTrue(DataComponentIngredient.of(false,required).test(extraNested),"Forge partial NBT has different nested semantics"); + var wrongItem=new ItemStack(Items.EMERALD); wrongItem.setTag(required.getTag().copy()); + assertFalse(ingredient.test(wrongItem)); + } + @Test void nativeJsonAndNetworkRoundTripPreservePredicateWithoutFlattening() { + var source=new TopLevelNbtIngredient(matrix()); + var buffer=new FriendlyByteBuf(Unpooled.buffer()); + try { + RecipeCodecs.INGREDIENT_STREAM.encode(buffer,source); + var wire=RecipeCodecs.INGREDIENT_STREAM.decode(buffer); + var json=Ingredient.fromJson(source.toJson()); + for(var restored:new Ingredient[]{wire,json}) { + assertInstanceOf(TopLevelNbtIngredient.class,restored); + assertTrue(restored.test(matrix())); + assertFalse(restored.test(new ItemStack(Items.DIAMOND))); + assertEquals(source,restored); + } + assertEquals(0,buffer.readableBytes()); + } finally {buffer.release();} + } + @Test void missingNbtConstraintAllowsForeignNbtAndInputIsDefensivelyCopied() { + var required=matrix();var ingredient=new TopLevelNbtIngredient(required); + required.getTag().getCompound("MatrixState").putInt("level",4); + ingredient.getItems()[0].setTag(null); + assertTrue(ingredient.test(matrix())); assertFalse(ingredient.test(required)); + assertTrue(new TopLevelNbtIngredient(new ItemStack(Items.DIAMOND)).test(matrix())); + assertThrows(IllegalArgumentException.class,()->new TopLevelNbtIngredient(ItemStack.EMPTY)); + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/compat/jei/AEKeyBookmarkPortTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/compat/jei/AEKeyBookmarkPortTest.java new file mode 100644 index 00000000..c76dc840 --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/compat/jei/AEKeyBookmarkPortTest.java @@ -0,0 +1,161 @@ +package com.sorrowmist.useless.compat.jei; + +import appeng.api.stacks.*; +import com.glodblock.github.appflux.common.me.key.FluxKey; +import com.glodblock.github.appflux.common.me.key.type.*; +import com.sorrowmist.useless.compat.forge1201.RecipeTestBootstrap; +import com.sorrowmist.useless.compat.forge1201.jei.AEKeyBookmarkCatalog; +import mezz.jei.api.constants.VanillaTypes; +import mezz.jei.api.forge.ForgeTypes; +import mezz.jei.api.ingredients.*; +import mezz.jei.api.ingredients.subtypes.UidContext; +import mezz.jei.api.runtime.IIngredientManager; +import mezz.jei.common.config.file.serializers.TypedIngredientSerializer; +import mezz.jei.gui.bookmarks.IngredientBookmark; +import mezz.jei.library.ingredients.IngredientSet; +import mezz.jei.library.ingredients.TypedIngredient; +import net.minecraft.nbt.*; +import net.minecraft.world.item.*; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fluids.FluidStack; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.MockedStatic; +import net.minecraftforge.fml.ModList; +import java.nio.file.*; +import java.util.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +class AEKeyBookmarkPortTest { + @TempDir Path temp; + @BeforeAll static void bootstrap() { RecipeTestBootstrap.initialize(); } + private static MockedStatic types() { + var types = mockStatic(AEKeyTypes.class); + for (var type : List.of(AEKeyType.items(), AEKeyType.fluids(), FluxKeyType.TYPE)) + types.when(() -> AEKeyTypes.get(type.getId())).thenReturn(type); + return types; + } + private static AEItemKey variant(String text) { + var stack = new ItemStack(Items.PAPER); + stack.getOrCreateTag().putString("foreign:data", text); + return AEItemKey.of(stack); + } + private static AEKeyJeiIngredients.KeyIngredient ingredient(AEKey key, long count) { + return new AEKeyJeiIngredients.KeyIngredient(key, count); + } + // JEI storage and serializer are real. Only its lifecycle facade is supplied here; + // no GUI or Minecraft instance is booted in this independent test JVM. + private static IIngredientManager manager(IngredientSet values) { + var manager = mock(IIngredientManager.class); + when(manager.getIngredientHelper(AEKeyJeiIngredients.TYPE)).thenReturn(AEKeyJeiIngredients.HELPER); + when(manager.getIngredientHelper(any(AEKeyJeiIngredients.KeyIngredient.class))).thenReturn(AEKeyJeiIngredients.HELPER); + when(manager.normalizeTypedIngredient(any())).thenAnswer(call -> { + ITypedIngredient value = call.getArgument(0); + return TypedIngredient.createUnvalidated(AEKeyJeiIngredients.TYPE, + AEKeyJeiIngredients.HELPER.normalizeIngredient(value.getIngredient())); + }); + when(manager.getIngredientTypeForUid(AEKeyJeiIngredients.TYPE.getUid())).thenReturn(Optional.of(AEKeyJeiIngredients.TYPE)); + when(manager.getTypedIngredientByUid(eq(AEKeyJeiIngredients.TYPE), anyString())).thenAnswer(call -> + values.getByUid(call.getArgument(1)).map(value -> TypedIngredient.createUnvalidated(AEKeyJeiIngredients.TYPE, value))); + return manager; + } + @Test void nativeItemsAndFluidsRetainTheirActualTypesAndNbt() { + var key = variant("&\n中文"); + var item = GenericStackJeiIngredientProviders.resolve(new GenericStack(key, 17)); + assertSame(VanillaTypes.ITEM_STACK, item.type()); + assertEquals(key, AEItemKey.of((ItemStack)item.value())); + assertEquals(17, ((ItemStack)item.value()).getCount()); + var fluidStack = new FluidStack(Fluids.WATER, 1499); + fluidStack.getOrCreateTag().putString("foreign:quality", "pure"); + var fluidKey = AEFluidKey.of(fluidStack); + var fluid = GenericStackJeiIngredientProviders.resolve(new GenericStack(fluidKey, 1499)); + assertSame(ForgeTypes.FLUID_STACK, fluid.type()); + assertEquals(fluidKey, AEFluidKey.of((FluidStack)fluid.value())); + assertEquals(1499, ((FluidStack)fluid.value()).getAmount()); + assertNull(GenericStackJeiIngredientProviders.resolve(new GenericStack(key, 0))); + } + @Test void emiBridgeKeepsLongAmountsAndNbtBeforeNativeIntConversion() { + var mods = mock(ModList.class); + when(mods.isLoaded("emi")).thenReturn(true); + try (var lifecycle = mockStatic(ModList.class)) { + lifecycle.when(ModList::get).thenReturn(mods); + for (var key : List.of(variant("a&中文"), variant("b&中文"), + AEFluidKey.of(Fluids.WATER), FluxKey.of(EnergyType.FE), FluxKey.of(EnergyType.GTEU))) { + var resolved = GenericStackJeiIngredientProviders.resolve(new GenericStack(key, 4_000_000_007L)); + assertSame(AEKeyJeiIngredients.TYPE, resolved.type()); + var value = (AEKeyJeiIngredients.KeyIngredient) resolved.value(); + assertEquals(key, value.key()); + assertEquals(4_000_000_007L, value.amount()); + } + } + } + @Test void energyUsesResourceIdentityAndKeepsLongAmountsInRecipeSlots() { + var fe = FluxKey.of(EnergyType.FE); var eu = FluxKey.of(EnergyType.GTEU); + var value = GenericStackJeiIngredientProviders.resolve(new GenericStack(fe, 4_000_000_007L)); + assertSame(AEKeyJeiIngredients.TYPE, value.type()); + var resource = (AEKeyJeiIngredients.KeyIngredient)value.value(); + assertEquals(fe, resource.key()); assertEquals(4_000_000_007L, resource.amount()); + assertEquals(fe.getDisplayName().getString(), AEKeyJeiIngredients.HELPER.getDisplayName(resource)); + assertEquals(fe.getId(), AEKeyJeiIngredients.HELPER.getResourceLocation(resource)); + assertEquals(fe.getModId(), AEKeyJeiIngredients.HELPER.getDisplayModId(resource)); + assertNotEquals(AEKeyBookmarkCatalog.uid(fe), AEKeyBookmarkCatalog.uid(eu)); + } + @Test void actualJeiBookmarksDeduplicateCountsButKeepNbtVariantsDistinct() { + var set = new IngredientSet<>(AEKeyJeiIngredients.HELPER, UidContext.Ingredient); + var manager = manager(set); + var first = TypedIngredient.createUnvalidated(AEKeyJeiIngredients.TYPE, ingredient(variant("a"), 4_000_000_000L)); + var same = TypedIngredient.createUnvalidated(AEKeyJeiIngredients.TYPE, ingredient(variant("a"), 3)); + var other = TypedIngredient.createUnvalidated(AEKeyJeiIngredients.TYPE, ingredient(variant("b"), 3)); + var bookmark = IngredientBookmark.create(first, manager); + assertEquals(bookmark, IngredientBookmark.create(same, manager)); + assertNotEquals(bookmark, IngredientBookmark.create(other, manager)); + assertEquals(1, bookmark.getIngredient().getIngredient().amount()); + assertEquals(4_000_000_000L, first.getIngredient().amount()); + } + @Test void actualJeiSerializerRestoresFromFreshDiskCatalogIncludingSeparatorsAndEnergy() throws Exception { + try (var ignored = types()) { + var disk = temp.resolve("keys.nbt"); var catalog = new AEKeyBookmarkCatalog(); + var keys = List.of(variant("x&y\n中文"), FluxKey.of(EnergyType.FE), FluxKey.of(EnergyType.GTEU)); + var original = new IngredientSet<>(AEKeyJeiIngredients.HELPER, UidContext.Ingredient); + for (var key : keys) { catalog.remember(key); original.add(ingredient(key, 1)); } + catalog.save(disk); + var saved = new TypedIngredientSerializer(manager(original)); + var lines = original.stream().map(v -> saved.serialize(TypedIngredient.createUnvalidated(AEKeyJeiIngredients.TYPE, v))).toList(); + var restored = new AEKeyBookmarkCatalog(); restored.load(disk); + var reloaded = new IngredientSet<>(AEKeyJeiIngredients.HELPER, UidContext.Ingredient); + restored.decoded().forEach(s -> reloaded.add(ingredient(s.what(), 1))); + var reader = new TypedIngredientSerializer(manager(reloaded)); + for (int i = 0; i < lines.size(); i++) { + assertEquals(2, lines.get(i).split("&").length); + var value = reader.deserialize(lines.get(i)).getResult().orElseThrow(); + assertEquals(keys.get(i), ((AEKeyJeiIngredients.KeyIngredient)value.getIngredient()).key()); + } + } + } + @Test void unknownAddonTagsSurviveAndOriginalDiskFileIsBackedUp() throws Exception { + var disk = temp.resolve("unknown.nbt"); + var unknown = new CompoundTag(); unknown.putString("#type", "absent_addon:custom"); + unknown.putString("payload", "opaque&value"); unknown.putLong("long", 9_000_000_001L); + var root = new CompoundTag(); var list = new ListTag(); list.add(unknown); root.put("keys", list); + try (var out = Files.newOutputStream(disk)) { NbtIo.writeCompressed(root, out); } + var original = Files.readAllBytes(disk); + var catalog = new AEKeyBookmarkCatalog(); catalog.load(disk); + catalog.remember(variant("known")); catalog.save(disk); + assertArrayEquals(original, Files.readAllBytes(temp.resolve("unknown.nbt.bak"))); + try (var in = Files.newInputStream(disk)) { + assertTrue(NbtIo.readCompressed(in).getList("keys", Tag.TAG_COMPOUND).contains(unknown)); + } + var reloaded = new AEKeyBookmarkCatalog(); reloaded.load(disk); assertEquals(2, reloaded.size()); + } + @Test void missingCatalogStartsFreshAndInvalidDiskDoesNotDestroyLoadedData() throws Exception { + var catalog = new AEKeyBookmarkCatalog(); catalog.remember(variant("old")); + catalog.load(temp.resolve("new-profile.nbt")); assertEquals(0, catalog.size()); + catalog.remember(variant("new")); var broken = temp.resolve("broken.nbt"); + Files.writeString(broken, "invalid compressed NBT"); + assertThrows(java.io.IOException.class, () -> catalog.load(broken)); + assertEquals(1, catalog.size()); assertEquals("invalid compressed NBT", Files.readString(broken)); + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/compat/kubejs/NativeKubeComponentTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/compat/kubejs/NativeKubeComponentTest.java new file mode 100644 index 00000000..e54e5635 --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/compat/kubejs/NativeKubeComponentTest.java @@ -0,0 +1,25 @@ +package com.sorrowmist.useless.compat.kubejs; +import com.sorrowmist.useless.compat.forge1201.RecipeTestBootstrap; +import appeng.api.stacks.*; +import net.minecraft.world.item.*; +import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.*; + +class NativeKubeComponentTest { + @BeforeAll static void init(){RecipeTestBootstrap.initialize();} + @Test void jsonPreservesLongQuantityAndItemNbt(){ + var item=new ItemStack(Items.DIAMOND);item.getOrCreateTag().putString("marker","kubejs"); + var stack=new GenericStack(AEItemKey.of(item),9_000_000_000L); + var component=GenericStackRecipeComponent.GENERIC_STACK; + var json=component.write(null,stack); + // Verify encoded native NBT directly; key-type registration belongs to mod bootstrap. + assertTrue(json.toString().contains("9000000000"));assertTrue(json.toString().contains("kubejs")); + assertSame(stack,component.read(null,stack)); + } + @Test void bothNativeObjectsAndJsonRejectZeroOrNegativeAmounts(){ + var stack=new GenericStack(AEItemKey.of(Items.DIAMOND),0); + var component=GenericStackRecipeComponent.GENERIC_STACK; + assertThrows(IllegalArgumentException.class,()->component.read(null,stack)); + assertThrows(IllegalArgumentException.class,()->component.write(null,stack)); + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/compat/mekanism/NativeChemicalStorageTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/compat/mekanism/NativeChemicalStorageTest.java new file mode 100644 index 00000000..9c93b68d --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/compat/mekanism/NativeChemicalStorageTest.java @@ -0,0 +1,133 @@ +package com.sorrowmist.useless.compat.mekanism; + +import com.sorrowmist.useless.api.enums.FurnaceFaceMode; +import com.sorrowmist.useless.compat.forge1201.RecipeTestBootstrap; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.io.FurnaceFaceAccessor; +import mekanism.api.Action; +import mekanism.api.MekanismAPI; +import mekanism.api.chemical.ChemicalStack; +import mekanism.api.chemical.gas.*; +import mekanism.api.chemical.infuse.*; +import mekanism.api.chemical.pigment.*; +import mekanism.api.chemical.slurry.*; +import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.registries.IForgeRegistry; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class NativeChemicalStorageTest { + @BeforeAll static void bootstrap() throws Exception { + RecipeTestBootstrap.initialize(); + installRegistry("GAS_REGISTRY"); + installRegistry("INFUSE_TYPE_REGISTRY"); + installRegistry("PIGMENT_REGISTRY"); + installRegistry("SLURRY_REGISTRY"); + } + // Forge normally creates these registries before ChemicalStack's EMPTY constants. + // Standalone tests keep native stacks and serialization, supplying only the registry lifecycle. + @SuppressWarnings({"unchecked", "rawtypes"}) + private static void installRegistry(String fieldName) throws Exception { + var keys=new java.util.IdentityHashMap(); + var values=new java.util.HashMap(); + var delegates=new java.util.IdentityHashMap(); + IForgeRegistry registry=mock(IForgeRegistry.class, invocation -> { + Object[] args=invocation.getArguments(); + return switch(invocation.getMethod().getName()) { + case "getKey" -> keys.computeIfAbsent(args[0], value -> { + var key=new ResourceLocation("useless_port_test",fieldName.toLowerCase()+"_"+keys.size()); + values.put(key,value); return key; + }); + case "getDelegateOrThrow" -> delegates.computeIfAbsent(args[0],value -> { + net.minecraft.core.Holder.Reference holder=mock(net.minecraft.core.Holder.Reference.class); + when(holder.value()).thenReturn(value); when(holder.get()).thenReturn(value); return holder; + }); + case "getValue" -> values.get(args[0]); + default -> org.mockito.Answers.RETURNS_DEFAULTS.answer(invocation); + }; + }); + var field=MekanismAPI.class.getDeclaredField(fieldName); + field.setAccessible(true); field.set(null,registry); + } + static GasStack gas(long amount) { return new GasStack(new Gas(GasBuilder.builder()),amount); } + static List> forms(long amount) { + return List.of(gas(amount),new InfusionStack(new InfuseType(InfuseTypeBuilder.builder()),amount), + new PigmentStack(new Pigment(PigmentBuilder.builder()),amount),new SlurryStack(new Slurry(SlurryBuilder.clean()),amount)); + } + @Test void everyNativeFormPreservesLongAmountsAndDetachedCopies() { + long large=(long)Integer.MAX_VALUE+123456; + for(var source:forms(large)) { + var storage=new MekanismChemicalStorage(1,Long.MAX_VALUE,null); + storage.setStackInSlot(0,new MekanismChemicalStackView(source)); + source.setAmount(1); + assertEquals(large,storage.getStackInSlot(0).amount()); + var exposed=storage.getNativeStack(0); exposed.setAmount(2); + assertEquals(large,storage.getStackInSlot(0).amount()); + assertEquals(large-5,storage.extractChemical(0,large-5,true).amount()); + assertEquals(large,storage.getStackInSlot(0).amount()); + assertEquals(large-5,storage.extractChemical(0,large-5,false).amount()); + assertEquals(5,storage.getStackInSlot(0).amount()); + } + } + @Test void simulationCapacityAndCrossFormRejectionConserveMaterials() { + var changed=new AtomicInteger(); + var storage=new MekanismChemicalStorage(1,100,changed::incrementAndGet); + var input=new MekanismChemicalStackView(gas(150)); + assertEquals(50,storage.insertChemical(0,input,true).amount()); + assertTrue(storage.getStackInSlot(0).isEmpty()); assertEquals(0,changed.get()); + assertEquals(50,storage.insertChemical(0,input,false).amount()); + assertEquals(100,storage.getStackInSlot(0).amount()); assertEquals(1,changed.get()); + var pigment=new MekanismChemicalStackView(forms(20).get(2)); + assertEquals(20,storage.insertChemical(0,pigment,false).amount()); + assertEquals(100,storage.getStackInSlot(0).amount()); + assertEquals(150,input.amount()); + } + @Test void wrongNativeCapabilityCannotClearOrExtractAnotherForm() { + var input=new MekanismChemicalStorage(1,100,null); + var output=new MekanismChemicalStorage(1,100,null); + input.setStackInSlot(0,new MekanismChemicalStackView(forms(50).get(2))); + output.setStackInSlot(0,new MekanismChemicalStackView(forms(60).get(2))); + var gas=new NativeFurnaceChemicalHandlers.GasHandler(input,output,null,null); + assertTrue(gas.getChemicalInTank(0).isEmpty()); + gas.setChemicalInTank(0,GasStack.EMPTY); + assertEquals(50,input.getStackInSlot(0).amount()); + assertTrue(gas.extractChemical(1,30,Action.EXECUTE).isEmpty()); + assertEquals(60,output.getStackInSlot(0).amount()); + var pigments=new NativeFurnaceChemicalHandlers.PigmentHandler(input,output,null,null); + assertEquals(30,pigments.extractChemical(1,30,Action.EXECUTE).getAmount()); + assertEquals(30,output.getStackInSlot(0).amount()); + } + @Test void nativeHandlerHonorsConfiguredFaceAndInputOutputSlots() { + var input=new MekanismChemicalStorage(1,100,null); + var output=new MekanismChemicalStorage(1,100,null); + var owner=mock(FurnaceFaceAccessor.class); + when(owner.getFacing()).thenReturn(Direction.NORTH); + when(owner.getFaceMode(any())).thenReturn(FurnaceFaceMode.DISABLED); + var handler=new NativeFurnaceChemicalHandlers.GasHandler(input,output,Direction.NORTH,owner); + var stack=gas(40); + assertEquals(40,handler.insertChemical(0,stack,Action.EXECUTE).getAmount()); + assertTrue(input.getStackInSlot(0).isEmpty()); + when(owner.getFaceMode(any())).thenReturn(FurnaceFaceMode.MATERIAL_INPUT); + assertTrue(handler.insertChemical(0,stack,Action.EXECUTE).isEmpty()); + assertEquals(40,input.getStackInSlot(0).amount()); + assertEquals(40,handler.insertChemical(1,stack,Action.EXECUTE).getAmount()); + assertTrue(handler.extractChemical(0,20,Action.EXECUTE).isEmpty()); + } + @Test @SuppressWarnings("unchecked") void boxedNativeNbtRestoresTypeAndLongAmountAndClearsStaleEmptySlots() { + var source=gas((long)Integer.MAX_VALUE+42); + var storage=new MekanismChemicalStorage(2,Long.MAX_VALUE,null); + storage.setStackInSlot(0,new MekanismChemicalStackView(source)); + CompoundTag tag=new CompoundTag(); storage.save(tag,"tank",null); + var restored=new MekanismChemicalStorage(2,Long.MAX_VALUE,null); restored.load(tag,"tank",null); + assertEquals(source.getAmount(),restored.getStackInSlot(0).amount()); + assertSame(source.getType(),restored.getNativeStack(0).getType()); + storage.extractChemical(0,Long.MAX_VALUE,false); storage.save(tag,"tank",null); + assertFalse(tag.contains("tank0")); + restored.load(tag,"tank",null); assertTrue(restored.getStackInSlot(0).isEmpty()); + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/content/blockentities/HighStackInventoryPortTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/content/blockentities/HighStackInventoryPortTest.java new file mode 100644 index 00000000..09039fcd --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/content/blockentities/HighStackInventoryPortTest.java @@ -0,0 +1,73 @@ +package com.sorrowmist.useless.content.blockentities; + +import com.sorrowmist.useless.compat.forge1201.RecipeTestBootstrap; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import java.util.concurrent.atomic.AtomicInteger; +import static org.junit.jupiter.api.Assertions.*; + +class HighStackInventoryPortTest { + @BeforeAll static void bootstrap() {RecipeTestBootstrap.initialize();} + private static HighStackItemStackHandler inventory(int slots,AtomicInteger changes) { + return new HighStackItemStackHandler(slots,0,1,2,slots-2,slots,slots-1,changes::incrementAndGet,null,null,null); + } + private static ItemStack stack(int count) { + var stack=new ItemStack(Items.DIAMOND,count); + stack.getOrCreateTag().putString("OtherModState","retained"); + var inner=new CompoundTag(); inner.putLong("counter",Long.MAX_VALUE); + stack.getOrCreateTag().put("ComplexState",inner); return stack; + } + @Test void nativeForgeSaveLoadPreservesBillionItemCountsNbtAndSlotsAbove255() { + var changes=new AtomicInteger(); var inventory=inventory(512,changes); + inventory.setStackInSlot(399,stack(2_000_000_000)); + CompoundTag saved=inventory.serializeNBT(); + assertEquals(399,saved.getList("Items",10).getCompound(0).getInt("Slot")); + var restored=inventory(512,new AtomicInteger()); restored.deserializeNBT(saved); + assertEquals(2_000_000_000,restored.getStackInSlot(399).getCount()); + assertEquals(inventory.getStackInSlot(399).getTag(),restored.getStackInSlot(399).getTag()); + assertTrue(restored.getStackInSlot(143).isEmpty()); + assertEquals(512,restored.getSlots()); + } + @Test void machineConsumesActualHighCountBatchInsteadOfVanilla64ItemClamp() { + var changes=new AtomicInteger(); var inventory=inventory(4,changes); + assertTrue(inventory.insertItem(2,stack(500_000),false).isEmpty()); + changes.set(0); + assertEquals(250_000,inventory.extractItem(2,250_000,true).getCount()); + assertEquals(500_000,inventory.getStackInSlot(2).getCount()); assertEquals(0,changes.get()); + var consumed=inventory.extractItem(2,250_000,false); + assertEquals(250_000,consumed.getCount()); assertEquals(stack(1).getTag(),consumed.getTag()); + assertEquals(250_000,inventory.getStackInSlot(2).getCount()); assertEquals(1,changes.get()); + assertEquals(250_000,inventory.extractItem(2,Integer.MAX_VALUE,false).getCount()); + assertTrue(inventory.getStackInSlot(2).isEmpty()); assertEquals(2,changes.get()); + } + @Test void insertionAtIntLimitReturnsExactRemainderWithoutOverflowOrMutationDuringSimulation() { + var inventory=inventory(4,new AtomicInteger()); + inventory.setStackInSlot(2,stack(Integer.MAX_VALUE-10)); + assertEquals(90,inventory.insertItem(2,stack(100),true).getCount()); + assertEquals(Integer.MAX_VALUE-10,inventory.getStackInSlot(2).getCount()); + assertEquals(90,inventory.insertItem(2,stack(100),false).getCount()); + assertEquals(Integer.MAX_VALUE,inventory.getStackInSlot(2).getCount()); + } + @Test void nativeLegacySaveAndUnsignedByteSlotAreRestoredWithoutLosingForeignNbt() { + var inventory=inventory(256,new AtomicInteger()); + var entry=stack(1).save(new CompoundTag()); entry.putByte("Slot",(byte)200); entry.putInt("RealCount",5000); + var items=new ListTag(); items.add(entry); var tag=new CompoundTag(); tag.put("Items",items); + inventory.deserializeNBT(tag); + assertEquals(5000,inventory.getStackInSlot(200).getCount()); + assertEquals(stack(1).getTag(),inventory.getStackInSlot(200).getTag()); + inventory.deserializeNBT(new CompoundTag()); + assertTrue(inventory.getStackInSlot(200).isEmpty()); + } + @Test void savedNegativeCountsAndOutOfBoundsSlotsCannotCreateGhostItems() { + var inventory=inventory(4,new AtomicInteger()); + var items=new ListTag(); var negative=stack(1).save(new CompoundTag()); + negative.putInt("Slot",2); negative.putInt("RealCount",-1); items.add(negative); + var outside=stack(1).save(new CompoundTag()); outside.putInt("Slot",3000); outside.putInt("RealCount",10); items.add(outside); + var tag=new CompoundTag(); tag.put("Items",items); inventory.deserializeNBT(tag); + for(int slot=0;slot types() { + // Unit JVM has no AE2 registry event; keep actual AE2 key types and NBT parsers. + var types = mockStatic(AEKeyTypes.class); + types.when(() -> AEKeyTypes.get(AEKeyType.items().getId())).thenReturn(AEKeyType.items()); + types.when(() -> AEKeyTypes.get(AEKeyType.fluids().getId())).thenReturn(AEKeyType.fluids()); + return types; + } + private static AEProcessingPattern pattern(GenericStack... inputs) { + var stack = new ItemStack(Items.PAPER); + var in = new ListTag(); + for (var input : inputs) in.add(input == null ? new CompoundTag() : GenericStack.writeTag(input)); + var out = new ListTag(); + out.add(GenericStack.writeTag(new GenericStack(AEItemKey.of(Items.DIAMOND), 3_000_000_007L))); + stack.getOrCreateTag().put("in", in); + stack.getOrCreateTag().put("out", out); + return new AEProcessingPattern(AEItemKey.of(stack)); + } + @Test void nativeArrayApiKeepsSparseHolesLongCountsAndDefensiveOutputs() { + try (var ignored = types()) { + var key = AEItemKey.of(named("template")); + var source = pattern(new GenericStack(key, 3_000_000_001L), null, new GenericStack(key, 2)); + var view = PatternStackView.fromPattern(source); + assertEquals(3_000_000_003L, view.inputs().get(0).amount()); + var dynamic = new DynamicComponentPatternDetails(source, Set.of(0), Set.of(0), RegistryAccess.EMPTY); + assertTrue(dynamic.isItemIdInput(0)); assertTrue(dynamic.usesDynamicOutputs()); + assertEquals(3_000_000_007L, dynamic.getOutputs()[0].amount()); + dynamic.getOutputs()[0] = null; + dynamic.getInputs()[0] = null; + assertNotNull(dynamic.getOutputs()[0]); assertNotNull(dynamic.getInputs()[0]); + var actual = AEItemKey.of(named("runtime")); + var held = new KeyCounter(); held.add(actual, 3_000_000_003L); + var pushed = new KeyCounter(); + dynamic.pushInputsToExternalInventory(new KeyCounter[]{held}, pushed::add); + assertEquals(3_000_000_003L, pushed.get(actual)); + assertEquals(3_000_000_003L, held.get(actual)); + } + } + @Test void overlappingDynamicPredicatesAreAllocatedWithoutDoubleCounting() { + try (var ignored = types()) { + var a = AEItemKey.of(named("a")); var b = AEItemKey.of(named("b")); + var source = pattern(new GenericStack(a, 1), null, new GenericStack(b, 1)); + var dynamic = new DynamicComponentPatternDetails(source, Set.of(0, 1), Set.of(), + Map.of(0, key -> key instanceof AEItemKey item && item.getItem() == Items.IRON_INGOT, + 1, key -> key.equals(a)), RegistryAccess.EMPTY); + var held = new KeyCounter(); held.add(a, 1); held.add(b, 1); + var pushed = new KeyCounter(); + dynamic.pushInputsToExternalInventory(new KeyCounter[]{held}, pushed::add); + assertEquals(1, pushed.get(a)); assertEquals(1, pushed.get(b)); + var insufficient = new KeyCounter(); insufficient.add(b, 2); + var rejected = new KeyCounter(); + assertThrows(IllegalStateException.class, () -> dynamic.pushInputsToExternalInventory( + new KeyCounter[]{insufficient}, rejected::add)); + assertTrue(rejected.isEmpty()); // Do not transfer anything before complete allocation. + } + } + @Test void onlyExplicitSlotsRelaxNbtAndInvalidSlotsAreRejected() { + try (var ignored = types()) { + var a = AEItemKey.of(named("a")); var b = AEItemKey.of(named("b")); + var source = pattern(new GenericStack(a, 4), new GenericStack(AEItemKey.of(Items.GOLD_INGOT), 2)); + var dynamic = new DynamicComponentPatternDetails(source, Set.of(0), Set.of(), RegistryAccess.EMPTY); + assertTrue(dynamic.getInputs()[0].isValid(b, null)); + assertFalse(dynamic.getInputs()[1].isValid(AEItemKey.of(Items.DIAMOND), null)); + assertEquals(4, dynamic.getInputs()[0].getMultiplier()); + assertFalse(dynamic.isItemIdInput(1)); + assertThrows(IllegalArgumentException.class, () -> new DynamicComponentPatternDetails( + source, Set.of(2), Set.of(), RegistryAccess.EMPTY)); + } + } + @Test void identityIsCanonicalButRetainsForeignNbtAndLongPatternAmounts() { + try (var ignored = types()) { + var a = new ItemStack(Items.PAPER); var b = new ItemStack(Items.PAPER); + a.getOrCreateTag().putInt("z", 17); a.getOrCreateTag().putLong("a", 3_000_000_007L); + b.getOrCreateTag().putLong("a", 3_000_000_007L); b.getOrCreateTag().putInt("z", 17); + var first = DynamicComponentPatternDetails.definitionFingerprint(AEItemKey.of(a), RegistryAccess.EMPTY); + assertEquals(first, DynamicComponentPatternDetails.definitionFingerprint(AEItemKey.of(b), RegistryAccess.EMPTY)); + b.getOrCreateTag().putLong("a", 7); + assertNotEquals(first, DynamicComponentPatternDetails.definitionFingerprint(AEItemKey.of(b), RegistryAccess.EMPTY)); + } + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/FurnaceRecipePortTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/FurnaceRecipePortTest.java new file mode 100644 index 00000000..39a375d5 --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/FurnaceRecipePortTest.java @@ -0,0 +1,107 @@ +package com.sorrowmist.useless.content.recipe; + +import com.google.gson.JsonParser; +import com.sorrowmist.useless.compat.forge1201.RecipeTestBootstrap; +import com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient; +import com.sorrowmist.useless.compat.forge1201.recipe.DataComponentIngredient; +import io.netty.buffer.Unpooled; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fluids.FluidStack; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import java.util.*; +import static org.junit.jupiter.api.Assertions.*; + +class FurnaceRecipePortTest { + @BeforeAll static void bootstrap() { RecipeTestBootstrap.initialize(); } + @Test void nativeSerializerUsesManagerIdAndPreservesLongQuantitiesAndNbt() { + var json=JsonParser.parseString(""" + {"id":"wrong:ignored","ingredients":[{"ingredient":{"item":"minecraft:iron_ingot"},"count":5000000000}], + "input_fluids":[{"ingredient":{"tag":"forge:water"},"amount":6000000000}], + "outputs":[{"item":"minecraft:diamond","count":32768,"nbt":"{OtherMod:42}"}], + "output_fluids":[{"fluid":"minecraft:water","amount":1000,"nbt":"{grade:2}"}], + "energy":7000000000,"process_time":60,"tier":4, + "molds":[{"item":"minecraft:gold_ingot"},{"item":"minecraft:copper_ingot"}]} + """).getAsJsonObject(); + var id=new ResourceLocation("kubejs:exact_runtime_id"); + var serializer=AdvancedAlloyFurnaceRecipeSerializer.INSTANCE; + var recipe=serializer.fromJson(id,json); + assertEquals(id,recipe.getId()); + assertEquals("wrong:ignored",json.get("id").getAsString()); + assertEquals(5_000_000_000L,recipe.inputs().get(0).count()); + assertEquals(6_000_000_000L,recipe.inputFluids().get(0).amount()); + assertEquals(7_000_000_000L,recipe.energy()); assertEquals(2,recipe.molds().size()); + FriendlyByteBuf buf=new FriendlyByteBuf(Unpooled.buffer()); + try { + serializer.toNetwork(buf,recipe); + var restored=serializer.fromNetwork(id,buf); + assertEquals(recipe.id(),restored.id()); + assertEquals(recipe.inputFluids(),restored.inputFluids()); + assertEquals(32768,restored.outputs().get(0).getCount()); + assertEquals(42,restored.outputs().get(0).getTag().getInt("OtherMod")); + assertEquals(2,restored.outputFluids().get(0).getTag().getInt("grade")); + assertEquals(2,restored.molds().size()); assertEquals(4,restored.tier()); + assertEquals(0,buf.readableBytes()); + } finally { buf.release(); } + } + + @Test void nativeFinishedRecipeRoundTripsAndMalformedMoldShapeFails() { + var id=new ResourceLocation("useless_mod:port_builder_roundtrip"); + List generated=new ArrayList<>(); + AdvancedAlloyFurnaceRecipeBuilder.create().input(Items.IRON_INGOT,12) + .fluidInput(new FluidStack(Fluids.WATER,1000)).output(Items.DIAMOND,8192).save(generated::add,id); + // This suite does not run Forge's mod registry events; assert identity, then serialize recipe data. + assertSame(AdvancedAlloyFurnaceRecipeSerializer.INSTANCE,generated.get(0).getType()); + var json=new com.google.gson.JsonObject(); generated.get(0).serializeRecipeData(json); + var restored=AdvancedAlloyFurnaceRecipeSerializer.INSTANCE.fromJson(id,json); + assertEquals(8192,restored.outputs().get(0).getCount()); + assertEquals(1000,restored.inputFluids().get(0).amount()); + json.add("mold",JsonParser.parseString("{\"item\":\"minecraft:iron_ingot\"}")); + json.add("molds",JsonParser.parseString("[{\"item\":\"minecraft:gold_ingot\"}]")); + assertThrows(RuntimeException.class,()->AdvancedAlloyFurnaceRecipeSerializer.INSTANCE.fromJson(id,json)); + } + + @Test void overlappingItemAndFluidDemandsCannotDoubleSpendSupply() { + var ironOrGold=Ingredient.of(Items.IRON_INGOT,Items.GOLD_INGOT); + var requirements=List.of(new CountedIngredient(ironOrGold,1),new CountedIngredient(Ingredient.of(Items.IRON_INGOT),1)); + assertFalse(ItemIngredientAllocator.matches(requirements,List.of(new ItemStack(Items.IRON_INGOT)),1)); + var allocation=ItemIngredientAllocator.allocate(requirements,List.of(new ItemStack(Items.IRON_INGOT),new ItemStack(Items.GOLD_INGOT)),1); + assertNotNull(allocation); assertEquals(1,allocation.consumedFromInput(0)); assertEquals(1,allocation.consumedFromInput(1)); + var waterOrLava=FluidIngredient.any(List.of(FluidIngredient.single(Fluids.WATER),FluidIngredient.single(Fluids.LAVA))); + var fluids=List.of(new LongSizedFluidIngredient(waterOrLava,1000),new LongSizedFluidIngredient(FluidIngredient.single(Fluids.WATER),1000)); + assertFalse(FluidIngredientAllocator.matchesLong(fluids,List.of(new FluidStack(Fluids.WATER,1000)),1)); + assertTrue(FluidIngredientAllocator.matchesLong(fluids,List.of(new FluidStack(Fluids.WATER,1000),new FluidStack(Fluids.LAVA,1000)),1)); + } + + @Test void fluidMapKeepsAboveIntAmountsAndRejectsOverflowInsteadOfFreeCrafts() { + var fluid=FluidIngredient.single(Fluids.WATER); + Map supplies=Map.of(new FluidStack(Fluids.WATER,1),6_000_000_000L); + var requirement=List.of(new LongSizedFluidIngredient(fluid,3_000_000_000L)); + assertTrue(FluidIngredientAllocator.matchesLong(requirement,supplies,2)); + assertFalse(FluidIngredientAllocator.matchesLong(requirement,supplies,3)); + assertFalse(FluidIngredientAllocator.matchesLong(List.of(new LongSizedFluidIngredient(fluid,Long.MAX_VALUE)), + Map.of(new FluidStack(Fluids.WATER,1),Long.MAX_VALUE),2)); + assertFalse(FluidIngredientAllocator.matchesLong(List.of(new LongSizedFluidIngredient(fluid,Long.MAX_VALUE),new LongSizedFluidIngredient(fluid,1)), + Map.of(new FluidStack(Fluids.WATER,1),Long.MAX_VALUE),1)); + var exact=DataComponentIngredient.of(true,new ItemStack(Items.IRON_INGOT)); + assertFalse(ItemIngredientAllocator.matches(Map.of(exact,Long.MAX_VALUE),Map.of(exact,Long.MAX_VALUE,Ingredient.of(Items.GOLD_INGOT),1L))); + } + + @Test void nativeIngredientPacketRetainsExactNbtPredicate() { + var required=new ItemStack(Items.DIAMOND); required.getOrCreateTag().putInt("grade",3); + var codec=com.sorrowmist.useless.compat.forge1201.recipe.RecipeCodecs.INGREDIENT_STREAM; + FriendlyByteBuf buf=new FriendlyByteBuf(Unpooled.buffer()); + try { + codec.encode(buf,DataComponentIngredient.of(true,required)); + Ingredient restored=codec.decode(buf); + assertTrue(restored.test(required)); assertFalse(restored.test(new ItemStack(Items.DIAMOND))); + var different=required.copy(); different.getOrCreateTag().putInt("grade",2); + assertFalse(restored.test(different)); + } finally { buf.release(); } + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/RecipeFingerprintPortTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/RecipeFingerprintPortTest.java new file mode 100644 index 00000000..1ee81e9b --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/RecipeFingerprintPortTest.java @@ -0,0 +1,59 @@ +package com.sorrowmist.useless.content.recipe; + +import com.sorrowmist.useless.compat.forge1201.RecipeTestBootstrap; +import com.sorrowmist.useless.compat.forge1201.recipe.NbtCanonical; +import com.sorrowmist.useless.compat.forge1201.recipe.NativeIngredients; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeCodecs; +import io.netty.buffer.Unpooled; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.TagParser; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class RecipeFingerprintPortTest { + @BeforeAll static void bootstrap() { RecipeTestBootstrap.initialize(); } + @Test void canonicalNbtPreservesTypesButIgnoresCompoundInsertionOrder() throws Exception { + var first=TagParser.parseTag("{Aa:1,BB:2,nested:{z:3s,a:[L;1L,2L]},ordered:[1,2]}"); + var second=TagParser.parseTag("{BB:2,Aa:1,ordered:[1,2],nested:{a:[L;1L,2L],z:3s}}"); + assertEquals(NbtCanonical.snbt(first),NbtCanonical.snbt(second)); + assertEquals(first,TagParser.parseTag(NbtCanonical.snbt(first))); + second.getCompound("nested").putInt("z",3); + assertNotEquals(NbtCanonical.snbt(first),NbtCanonical.snbt(second)); + } + + @Test void recipeIdentityIncludesAllNbtAndLargeCountWithoutDependingOnInsertionOrder() throws Exception { + var first=new ItemStack(Items.DIAMOND,32768); + first.setTag(TagParser.parseTag("{Aa:1,BB:2,OtherMod:{b:3,a:4}}")); + var second=first.copy(); second.setTag(TagParser.parseTag("{BB:2,Aa:1,OtherMod:{a:4,b:3}}")); + var id=new ResourceLocation("kubejs:pack_recipe"); + var recipe1=AdvancedAlloyFurnaceRecipeBuilder.create().input(Items.IRON_INGOT,2).output(first).build(id); + var recipe2=AdvancedAlloyFurnaceRecipeBuilder.create().input(Items.IRON_INGOT,2).output(second).build(id); + assertEquals(AlloyFurnaceRecipeFingerprint.create(recipe1,RegistryAccess.EMPTY),AlloyFurnaceRecipeFingerprint.create(recipe2,RegistryAccess.EMPTY)); + second.getOrCreateTag().getCompound("OtherMod").putInt("a",5); + var recipe3=AdvancedAlloyFurnaceRecipeBuilder.create().input(Items.IRON_INGOT,2).output(second).build(id); + assertNotEquals(AlloyFurnaceRecipeFingerprint.create(recipe1,RegistryAccess.EMPTY),AlloyFurnaceRecipeFingerprint.create(recipe3,RegistryAccess.EMPTY)); + assertNotEquals(AlloyFurnaceRecipeFingerprint.safeItemStack(first,RegistryAccess.EMPTY), + AlloyFurnaceRecipeFingerprint.safeItemStack(first.copyWithCount(32767),RegistryAccess.EMPTY)); + } + + @Test void ingredientPacketKeepsDirectTagReferenceRatherThanMemberSnapshot() { + var tag=TagKey.create(Registries.ITEM,new ResourceLocation("forge:ingots/iron")); + var ingredient=Ingredient.of(tag); + var buf=new FriendlyByteBuf(Unpooled.buffer()); + try { + RecipeCodecs.INGREDIENT_STREAM.encode(buf,ingredient); + var restored=RecipeCodecs.INGREDIENT_STREAM.decode(buf); + assertEquals(tag,NativeIngredients.directItemTag(restored).orElseThrow()); + assertEquals(ingredient.toJson(),restored.toJson()); + } finally { buf.release(); } + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/ae/extendedae/NativeAeRecipeTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/ae/extendedae/NativeAeRecipeTest.java new file mode 100644 index 00000000..11935c7f --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/ae/extendedae/NativeAeRecipeTest.java @@ -0,0 +1,119 @@ +package com.sorrowmist.useless.content.recipe.adapters.ae.extendedae; + +import appeng.api.stacks.AEFluidKey; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.GenericStack; +import com.glodblock.github.extendedae.recipe.CircuitCutterRecipe; +import com.glodblock.github.extendedae.recipe.util.IngredientStack; +import com.sorrowmist.useless.compat.forge1201.RecipeTestBootstrap; +import com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import com.sorrowmist.useless.content.recipe.adapters.ae.advancedae.ReactionChamberRecipeAdapter; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fluids.FluidStack; +import net.pedroksl.advanced_ae.recipes.ReactionChamberRecipe; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import java.util.List; +import java.util.Map; +import static org.junit.jupiter.api.Assertions.*; + +class NativeAeRecipeTest { + static final ResourceLocation ID=new ResourceLocation("useless_port_test:ae_native"); + @BeforeAll static void bootstrap() { RecipeTestBootstrap.initialize(); } + final CircuitCutterRecipeAdapter cutter=new CircuitCutterRecipeAdapter() { + @Override public ItemStack getMoldItem() { return new ItemStack(Items.PISTON); } + }; + final ReactionChamberRecipeAdapter chamber=new ReactionChamberRecipeAdapter() { + @Override public ItemStack getMoldItem() { return new ItemStack(Items.PISTON); } + }; + static CircuitCutterRecipe cutting(IngredientStack.Fluid fluid) { + return new CircuitCutterRecipe(ID,new ItemStack(Items.GOLD_NUGGET,9), + IngredientStack.of(Ingredient.of(Items.GOLD_BLOCK),1),fluid); + } + @Test void cutterRequiresFullWaterCostAndPreservesNineOutputs() { + var recipe=cutter.convert(new RecipeHolder<>(ID,cutting(IngredientStack.of(new FluidStack(Fluids.WATER,100)))),null); + assertNotNull(recipe); + assertEquals(100,recipe.inputFluids().get(0).amount()); + assertEquals(9,recipe.outputs().get(0).getCount()); + var items=Map.of(Ingredient.of(Items.GOLD_BLOCK),1L); + assertFalse(cutter.matches(recipe,items,Map.of())); + assertFalse(cutter.matches(recipe,items,Map.of(new FluidStack(Fluids.WATER,1),99L))); + assertTrue(cutter.matches(recipe,items,Map.of(new FluidStack(Fluids.WATER,1),100L))); + assertFalse(cutter.matches(recipe,items,Map.of(new FluidStack(Fluids.LAVA,1),100L))); + } + @Test void extendedAeFluidFamiliesAndNbtFollowTheNativePredicate() { + var required=new FluidStack(Fluids.WATER,100); + required.getOrCreateTag().putInt("ignored_by_native",1); + var source=com.glodblock.github.extendedae.recipe.util.FluidIngredient.of(required); + var converted=CircuitCutterRecipeAdapter.fluid(IngredientStack.of(source,100)).ingredient(); + for (var fluid:List.of(Fluids.WATER,Fluids.FLOWING_WATER,Fluids.LAVA)) { + var stack=new FluidStack(fluid,100); + assertEquals(source.test(stack),converted.test(stack)); + stack.getOrCreateTag().putString("extra","ok"); + assertEquals(source.test(stack),converted.test(stack)); + } + var restored=FluidIngredient.fromJson(converted.toJson()); + assertTrue(restored.test(new FluidStack(Fluids.FLOWING_WATER,1))); + } + @Test void extendedAeFluidTagsSurviveReloadAndKeepFlowingFamilySemantics() { + TagKey tag=TagKey.create(Registries.FLUID,new ResourceLocation("useless_port_test:eae_fluid")); + BuiltInRegistries.FLUID.bindTags(Map.of(tag,List.of(BuiltInRegistries.FLUID.wrapAsHolder(Fluids.WATER)))); + var source=com.glodblock.github.extendedae.recipe.util.FluidIngredient.of(tag); + var converted=CircuitCutterRecipeAdapter.fluid(IngredientStack.of(source,100)).ingredient(); + assertEquals(source.test(new FluidStack(Fluids.FLOWING_WATER,1)),converted.test(new FluidStack(Fluids.FLOWING_WATER,1))); + var restored=FluidIngredient.fromJson(converted.toJson()); + BuiltInRegistries.FLUID.bindTags(Map.of(tag,List.of(BuiltInRegistries.FLUID.wrapAsHolder(Fluids.LAVA)))); + assertFalse(restored.test(new FluidStack(Fluids.WATER,1))); + assertTrue(restored.test(new FluidStack(Fluids.FLOWING_LAVA,1))); + } + @Test void cutterRejectsMalformedMandatoryFluidInsteadOfDroppingIt() { + assertNull(cutter.convert(new RecipeHolder<>(ID,cutting(null)),null)); + assertNull(cutter.convert(new RecipeHolder<>(ID,cutting(IngredientStack.of(new FluidStack(Fluids.WATER,0)))),null)); + } + static ReactionChamberRecipe reaction(GenericStack output,int secondCount) { + return new ReactionChamberRecipe(ID,output,List.of( + new net.pedroksl.advanced_ae.recipes.IngredientStack.Item(Ingredient.of(Items.IRON_INGOT),2), + new net.pedroksl.advanced_ae.recipes.IngredientStack.Item(Ingredient.of(Items.IRON_INGOT),secondCount)), + new net.pedroksl.advanced_ae.recipes.IngredientStack.Fluid(Fluids.WATER,500),12000); + } + @Test void reactionAllocatesDuplicateInputsAndFullFluidCost() { + var output=new GenericStack(AEItemKey.of(Items.DIAMOND),3); + var recipe=chamber.convert(new RecipeHolder<>(ID,reaction(output,2)),null); + assertNotNull(recipe); + assertFalse(chamber.matches(recipe,Map.of(Ingredient.of(Items.IRON_INGOT),3L),Map.of(new FluidStack(Fluids.WATER,1),500L))); + assertFalse(chamber.matches(recipe,Map.of(Ingredient.of(Items.IRON_INGOT),4L),Map.of(new FluidStack(Fluids.WATER,1),499L))); + assertTrue(chamber.matches(recipe,Map.of(Ingredient.of(Items.IRON_INGOT),4L),Map.of(new FluidStack(Fluids.WATER,1),500L))); + assertEquals(3,recipe.outputs().get(0).getCount()); + assertEquals(12000,recipe.energy()); + } + @Test void reactionPreservesFluidOutputAndNativeFluidPredicate() { + var source=reaction(new GenericStack(AEFluidKey.of(Fluids.LAVA),2000),2); + var recipe=chamber.convert(new RecipeHolder<>(ID,source),null); + assertNotNull(recipe); + assertTrue(recipe.outputs().isEmpty()); + assertEquals(2000,recipe.outputFluids().get(0).getAmount()); + var nbtWater=new FluidStack(Fluids.WATER,500); nbtWater.getOrCreateTag().putString("arbitrary","yes"); + assertEquals(source.getFluid().test(nbtWater),recipe.inputFluids().get(0).ingredient().test(nbtWater)); + var flowing=new FluidStack(Fluids.FLOWING_WATER,500); + assertEquals(source.getFluid().test(flowing),recipe.inputFluids().get(0).ingredient().test(flowing)); + } + @Test void invalidReactionInputsAndOverflowingOutputsRejectWholeRecipe() { + assertNull(chamber.convert(new RecipeHolder<>(ID,reaction(new GenericStack(AEItemKey.of(Items.DIAMOND),1),-2)),null)); + assertNull(chamber.convert(new RecipeHolder<>(ID,reaction(new GenericStack(AEItemKey.of(Items.DIAMOND),(long)Integer.MAX_VALUE+1),2)),null)); + assertNull(chamber.convert(new RecipeHolder<>(ID,reaction(new GenericStack(AEFluidKey.of(Fluids.WATER),0),2)),null)); + } + @Test void crystalFixingAccountsForRandomProgressAndFuelProbability() { + // 100 / mean(nextInt(5)) calls, each burning fuel with probability 0.1, + // plus nonzero overshoot: round expected cost up to six, never one fuel. + assertEquals(6,CrystalFixerRecipeAdapter.fuelPerRepair()); + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/botanypots/NativeBotanyDropsTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/botanypots/NativeBotanyDropsTest.java new file mode 100644 index 00000000..bcd1599f --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/botanypots/NativeBotanyDropsTest.java @@ -0,0 +1,40 @@ +package com.sorrowmist.useless.content.recipe.adapters.botanypots; +import com.sorrowmist.useless.compat.forge1201.RecipeTestBootstrap; +import com.sorrowmist.useless.content.recipe.ExpectedOutputScaler; +import net.darkhax.botanypots.data.recipes.crop.*; +import net.minecraft.world.item.*; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.resources.ResourceLocation; +import org.junit.jupiter.api.*; +import java.util.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class NativeBotanyDropsTest { + @BeforeAll static void init(){RecipeTestBootstrap.initialize();} + @Test void rollsMultiplyTheEntireNbtStack(){ + var stack=new ItemStack(Items.APPLE,3); stack.getOrCreateTag().putString("marker","crop"); + var drops=BotanyPotsRecipeAdapter.collectDrops(List.of(new HarvestEntry(1,stack,2,4))); + var scaled=ExpectedOutputScaler.scale(drops).orElseThrow(); + assertEquals(1,scaled.operations());assertEquals(9,scaled.outputs().get(0).getCount()); + assertEquals("crop",scaled.outputs().get(0).getTag().getString("marker")); + scaled.outputs().get(0).getTag().putString("marker","changed");assertEquals("crop",stack.getTag().getString("marker")); + } + @Test void probabilisticHarvestScalesWaterTimeAndEnergyTogether(){ + var crop=mock(BasicCrop.class); + when(crop.getSeed()).thenReturn(Ingredient.of(Items.WHEAT_SEEDS)); + when(crop.getGrowthTicks()).thenReturn(100); + when(crop.getResults()).thenReturn(List.of(new HarvestEntry(0.25f,new ItemStack(Items.WHEAT),1,1))); + var recipe=BotanyPotsRecipeAdapter.convertCrop(new ResourceLocation("test:crop"),crop,Ingredient.of(Items.FLOWER_POT)); + assertNotNull(recipe); assertEquals(4000,recipe.inputFluids().get(0).amount()); + assertEquals(400,recipe.processTime()); assertEquals(1,recipe.outputs().get(0).getCount()); + assertTrue(recipe.molds().get(1).test(new ItemStack(Items.WHEAT_SEEDS))); + } + @Test void emptyRequiredPotCannotProduceRecipe(){ + var crop=mock(BasicCrop.class); + when(crop.getSeed()).thenReturn(Ingredient.of(Items.WHEAT_SEEDS)); + when(crop.getGrowthTicks()).thenReturn(100); + when(crop.getResults()).thenReturn(List.of(new HarvestEntry(1,new ItemStack(Items.WHEAT),1,1))); + assertNull(BotanyPotsRecipeAdapter.convertCrop(new ResourceLocation("test:crop"),crop,Ingredient.EMPTY)); + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/create/CreateNativeRecipeTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/create/CreateNativeRecipeTest.java new file mode 100644 index 00000000..fa819e03 --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/create/CreateNativeRecipeTest.java @@ -0,0 +1,136 @@ +package com.sorrowmist.useless.content.recipe.adapters.create; + +import com.simibubi.create.content.kinetics.millstone.MillingRecipe; +import com.simibubi.create.content.kinetics.press.PressingRecipe; +import com.simibubi.create.content.processing.recipe.ProcessingOutput; +import com.simibubi.create.content.processing.sequenced.SequencedAssemblyRecipe; +import com.simibubi.create.content.processing.sequenced.SequencedRecipe; +import com.simibubi.create.foundation.fluid.FluidIngredient; +import com.sorrowmist.useless.compat.forge1201.RecipeTestBootstrap; +import com.sorrowmist.useless.compat.forge1201.fluid.SizedFluidIngredient; +import net.minecraft.core.NonNullList; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fluids.FluidStack; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; +import java.util.List; +import java.util.Map; +import java.util.Set; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class CreateNativeRecipeTest { + private static final ResourceLocation ID=new ResourceLocation("create:test_port_recipe"); + @BeforeAll static void bootstrap() { RecipeTestBootstrap.initialize(); } + + @Test void plainFluidWithEmptyNativeNbtMatchesAnUntaggedStack() { + var source=FluidIngredient.fromFluid(Fluids.WATER,250); + SizedFluidIngredient converted=CreateRecipeAdapterUtils.nativeFluid(source); + assertEquals(250,converted.amount()); + var input=new FluidStack(Fluids.WATER,250); + assertTrue(converted.ingredient().test(input)); + assertFalse(converted.ingredient().test(new FluidStack(Fluids.LAVA,250))); + assertNull(input.getTag()); + } + + @Test void nativeFluidTagsStayLiveInsteadOfBecomingAFluidSnapshot() { + TagKey tag=TagKey.create(Registries.FLUID,new ResourceLocation("useless_port_test:create_liquid")); + var nativeIngredient=FluidIngredient.fromTag(tag,125); + var converted=CreateRecipeAdapterUtils.nativeFluid(nativeIngredient); + assertFalse(converted.ingredient().isEmpty()); + assertEquals(List.of(tag),converted.ingredient().tagOnlyTags().orElseThrow()); + BuiltInRegistries.FLUID.bindTags(Map.of(tag,List.of(BuiltInRegistries.FLUID.wrapAsHolder(Fluids.WATER)))); + assertTrue(converted.ingredient().test(new FluidStack(Fluids.WATER,1))); + BuiltInRegistries.FLUID.bindTags(Map.of(tag,List.of(BuiltInRegistries.FLUID.wrapAsHolder(Fluids.LAVA)))); + assertFalse(converted.ingredient().test(new FluidStack(Fluids.WATER,1))); + assertTrue(converted.ingredient().test(new FluidStack(Fluids.LAVA,1))); + } + + @Test void partialNativeNbtKeepsExactListsAndSurvivesSerialization() { + var required=new FluidStack(Fluids.WATER,100); + required.getOrCreateTag().putString("grade","pure"); + ListTag list=new ListTag(); list.add(StringTag.valueOf("one")); + required.getOrCreateTag().put("list",list); + var nativeIngredient=FluidIngredient.fromFluidStack(required); + var converted=CreateRecipeAdapterUtils.nativeFluid(nativeIngredient).ingredient(); + var extra=required.copy(); extra.getOrCreateTag().putInt("extra",42); + assertTrue(nativeIngredient.test(extra)); assertTrue(converted.test(extra)); + var moreList=extra.copy(); moreList.getTag().getList("list",8).add(StringTag.valueOf("two")); + assertFalse(nativeIngredient.test(moreList)); assertFalse(converted.test(moreList)); + assertFalse(converted.test(new FluidStack(Fluids.WATER,100))); + var restored=com.sorrowmist.useless.compat.forge1201.fluid.FluidIngredient.fromJson(converted.toJson()); + assertTrue(restored.test(extra)); assertFalse(restored.test(moreList)); + } + + @Test void independentChanceScalesEveryItemAndFluidInput() { + MillingRecipe source=mock(MillingRecipe.class); + when(source.getProcessingDuration()).thenReturn(40); + when(source.getIngredients()).thenReturn(NonNullList.of(Ingredient.EMPTY, + Ingredient.of(Items.IRON_INGOT),Ingredient.of(Items.IRON_INGOT))); + when(source.getFluidIngredients()).thenReturn(NonNullList.of(FluidIngredient.EMPTY, + FluidIngredient.fromFluid(Fluids.WATER,100))); + when(source.getRollableResults()).thenReturn(List.of(new ProcessingOutput(new ItemStack(Items.GOLD_INGOT),0.5F))); + when(source.getFluidResults()).thenReturn(NonNullList.create()); + var recipe=CreateRecipeAdapterUtils.convertProcessing(ID,source,List.of(Ingredient.of(Items.PISTON)), + LoggerFactory.getLogger(CreateNativeRecipeTest.class)); + assertNotNull(recipe); + assertEquals(4,recipe.inputs().get(0).count()); + assertEquals(200,recipe.inputFluids().get(0).amount()); + assertEquals(80,recipe.processTime()); + assertEquals(1,recipe.outputs().get(0).getCount()); + } + + private static final class AssemblyFixture extends SequencedAssemblyRecipe { + AssemblyFixture(PressingRecipe step) { + super(ID,null); + ingredient=Ingredient.of(Items.GOLD_INGOT); + loops=5; + sequence.add(new SequencedRecipe<>(step)); + resultPool.add(new ProcessingOutput(new ItemStack(Items.DIAMOND),4.0F)); + resultPool.add(new ProcessingOutput(new ItemStack(Items.DIRT),1.0F)); + } + } + @SuppressWarnings("unchecked") + private static PressingRecipe assemblyStep() { + PressingRecipe step=mock(PressingRecipe.class); + when(step.supportsAssembly()).thenReturn(true); + when(step.getProcessingDuration()).thenReturn(10); + doAnswer(call->{ ((List)call.getArgument(0)).add(Ingredient.of(Items.IRON_NUGGET)); return null; }) + .when(step).addAssemblyIngredients(anyList()); + doAnswer(call->{ ((List)call.getArgument(0)).add(FluidIngredient.fromFluid(Fluids.WATER,100)); return null; }) + .when(step).addAssemblyFluidIngredients(anyList()); + doAnswer(call->{ ((Set)call.getArgument(0)).add(Items.PISTON); return null; }) + .when(step).addRequiredMachines(anySet()); + return step; + } + @Test void assemblyUsesExclusiveWeightsAndScalesAllFiveLoops() { + var source=new AssemblyFixture(assemblyStep()); + var recipe=CreateSequencedAssemblyRecipeAdapter.convert(ID,source); + assertNotNull(recipe); + assertEquals(5,recipe.inputs().stream().filter(i->i.ingredient().test(new ItemStack(Items.GOLD_INGOT))).findFirst().orElseThrow().count()); + assertEquals(25,recipe.inputs().stream().filter(i->i.ingredient().test(new ItemStack(Items.IRON_NUGGET))).findFirst().orElseThrow().count()); + assertEquals(2500,recipe.inputFluids().get(0).amount()); + assertEquals(250,recipe.processTime()); + assertEquals(4,recipe.outputs().stream().filter(s->s.is(Items.DIAMOND)).findFirst().orElseThrow().getCount()); + assertEquals(1,recipe.outputs().stream().filter(s->s.is(Items.DIRT)).findFirst().orElseThrow().getCount()); + } + @Test void unsupportedAssemblyStepRejectsWholeRecipeInsteadOfMakingItCheaper() { + var step=assemblyStep(); when(step.supportsAssembly()).thenReturn(false); + assertNull(CreateSequencedAssemblyRecipeAdapter.convert(ID,new AssemblyFixture(step))); + } + @Test void overflowRejectsWholeRequiredInputSet() { + assertNull(CreateRecipeAdapterUtils.scaleItemInputs(Map.of(Ingredient.of(Items.IRON_INGOT),Long.MAX_VALUE), + 2,ID,LoggerFactory.getLogger(CreateNativeRecipeTest.class))); + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/enderio/EnderIOCapturePortTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/enderio/EnderIOCapturePortTest.java new file mode 100644 index 00000000..d693da2a --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/enderio/EnderIOCapturePortTest.java @@ -0,0 +1,59 @@ +package com.sorrowmist.useless.content.recipe.adapters.enderio; + +import com.enderio.api.capability.StoredEntityData; +import com.enderio.machines.common.blockentity.task.SpawnerMachineTask.SpawnType; +import com.enderio.machines.common.souldata.SpawnerSoul; +import com.enderio.base.common.util.EntityCaptureUtils; +import com.sorrowmist.useless.compat.forge1201.RecipeTestBootstrap; +import com.sorrowmist.useless.compat.forge1201.enderio.NativeSoul; +import com.sorrowmist.useless.compat.forge1201.enderio.NativeSoulStorage; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import java.util.Map; +import java.util.List; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class EnderIOCapturePortTest { + private static final ResourceLocation ZOMBIE = new ResourceLocation("minecraft:zombie"); + @BeforeAll static void bootstrap() { RecipeTestBootstrap.initialize(); } + @Test void nativeSpawnerSoulDataUsesResourceIdsAndNativeFallbackEnergy() { + var original = SpawnerSoul.SPAWNER.map; + try { + SpawnerSoul.SPAWNER.map = Map.of(ZOMBIE, new SpawnerSoul.SoulData(ZOMBIE, 112233, SpawnType.COPY)); + assertEquals(112233, SoulVialRecipeAdapter.energyCost(EntityType.ZOMBIE)); + assertEquals(50_000, SoulVialRecipeAdapter.energyCost(EntityType.SKELETON)); + } finally { SpawnerSoul.SPAWNER.map = original; } + } + @Test void nativeCaptureListAndSpawnEggRequirementAreRetained() { + try (var capture = mockStatic(EntityCaptureUtils.class)) { + capture.when(EntityCaptureUtils::getCapturableEntities).thenReturn(List.of(ZOMBIE)); + assertTrue(SoulVialRecipeAdapter.isSupportedEntity(EntityType.ZOMBIE)); + assertFalse(SoulVialRecipeAdapter.isSupportedEntity(EntityType.SKELETON)); + assertFalse(SoulVialRecipeAdapter.isSupportedEntity(EntityType.ITEM)); + assertFalse(SoulVialRecipeAdapter.isSupportedEntity(null)); + } + } + @Test void copyModeRetainsCapturedNbtAndDoesNotAliasTheMold() { + var original = SpawnerSoul.SPAWNER.map; + try (var storage = mockStatic(NativeSoulStorage.class)) { + SpawnerSoul.SPAWNER.map = Map.of(ZOMBIE, new SpawnerSoul.SoulData(ZOMBIE, 50000, SpawnType.COPY)); + var mold = new ItemStack(Items.ZOMBIE_SPAWN_EGG); + var tag = new CompoundTag(); tag.putString("id", ZOMBIE.toString()); tag.putLong("foreign:level", 77); + var captured = NativeSoul.from(new StoredEntityData(tag, 100.0F)); + storage.when(() -> NativeSoulStorage.getBoundSoul(mold)).thenReturn(captured); + var copy = SoulVialRecipeAdapter.captureSoul(EntityType.ZOMBIE, mold); + assertNotSame(captured, copy); + assertEquals(captured.storedData().serializeNBT(), copy.storedData().serializeNBT()); + copy.storedData().getEntityTag().putLong("foreign:level", 0); + assertEquals(77, captured.storedData().getEntityTag().getLong("foreign:level")); + storage.when(() -> NativeSoulStorage.getBoundSoul(mold)).thenReturn(NativeSoul.empty()); + assertEquals(ZOMBIE, SoulVialRecipeAdapter.captureSoul(EntityType.ZOMBIE, mold).entityTypeId()); + } finally { SpawnerSoul.SPAWNER.map = original; } + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/enderio/EnderIOMachinePortTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/enderio/EnderIOMachinePortTest.java new file mode 100644 index 00000000..c9fd9aa3 --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/enderio/EnderIOMachinePortTest.java @@ -0,0 +1,153 @@ +package com.sorrowmist.useless.content.recipe.adapters.enderio; + +import com.enderio.core.common.recipes.CountedIngredient; +import com.enderio.core.common.recipes.OutputStack; +import com.enderio.machines.common.recipe.AlloySmeltingRecipe; +import com.enderio.machines.common.recipe.SagMillingRecipe; +import com.enderio.machines.common.recipe.SlicingRecipe; +import com.enderio.machines.common.recipe.EnchanterRecipe; +import com.enderio.base.common.util.ExperienceUtil; +import com.sorrowmist.useless.compat.forge1201.RecipeTestBootstrap; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import com.sorrowmist.useless.content.recipe.ItemIngredientAllocator; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.enchantment.Enchantments; +import net.minecraft.world.item.enchantment.EnchantmentHelper; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fluids.FluidStack; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import java.util.List; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** Real 6.2.13 recipe objects, not mocks of a newer Ender IO interface. */ +class EnderIOMachinePortTest { + private static final ResourceLocation ID = new ResourceLocation("kubejs:enderio_port"); + @BeforeAll static void bootstrap() { RecipeTestBootstrap.initialize(); } + + @Test void alloySmeltingPreservesNativeCountsNbtEnergyAndActualConsumption() { + var metal = Ingredient.of(Items.IRON_INGOT); + var output = new ItemStack(Items.DIAMOND, 400); + output.getOrCreateTag().putString("foreign", "retained"); + var source = new AlloySmeltingRecipe(ID, + List.of(new CountedIngredient(metal, 200), new CountedIngredient(metal, 50)), + output, 12000, 0.0F); + var adapter = spy(new AlloySmeltingRecipeAdapter()); + doReturn(new ItemStack(Items.BRICKS)).when(adapter).getMoldItem(); + var result = adapter.convertAll(new RecipeHolder<>(ID, source), null).get(0); + assertEquals(250, result.inputs().get(0).count()); + assertEquals(12000, result.energy()); + assertEquals(400, result.outputs().get(0).getCount()); + assertEquals("retained", result.outputs().get(0).getTag().getString("foreign")); + assertNull(ItemIngredientAllocator.allocate(result.inputs(), + List.of(new ItemStack(Items.IRON_INGOT, 249)), 1)); + var allocation = ItemIngredientAllocator.allocate(result.inputs(), + List.of(new ItemStack(Items.IRON_INGOT, 250)), 1); + assertNotNull(allocation); assertEquals(250, allocation.consumedFromInput(0)); + result.outputs().get(0).getTag().putString("foreign", "changed"); + assertEquals("retained", output.getTag().getString("foreign")); + } + + @Test void sliceAndSpliceMergesRepeatedSlotsButRetainsAlternatives() { + var either = Ingredient.of(Items.IRON_INGOT, Items.GOLD_INGOT); + var exact = Ingredient.of(Items.IRON_INGOT); + var source = new SlicingRecipe(ID, new ItemStack(Items.COMPARATOR, 3), + List.of(either, either, exact, Ingredient.EMPTY), 6000); + var adapter = spy(new SlicingRecipeAdapter()); + doReturn(new ItemStack(Items.BRICKS)).when(adapter).getMoldItem(); + var result = adapter.convertAll(new RecipeHolder<>(ID, source), null).get(0); + assertEquals(6000, result.energy()); + assertEquals(3, result.outputs().get(0).getCount()); + assertFalse(ItemIngredientAllocator.matches(result.inputs(), + List.of(new ItemStack(Items.IRON_INGOT, 2)), 1)); + assertTrue(ItemIngredientAllocator.matches(result.inputs(), + List.of(new ItemStack(Items.GOLD_INGOT, 2), new ItemStack(Items.IRON_INGOT)), 1)); + } + + @Test void sagRetainsChanceOnlyAndOutputMultiplierSemantics() { + var chance = sag(SagMillingRecipe.BonusType.CHANCE_ONLY, 0.25F); + var multiplied = sag(SagMillingRecipe.BonusType.MULTIPLY_OUTPUT, 0.25F); + var multipliers = new SagMillingRecipeAdapter.GrindingBallMultipliers(1.5, 2.0); + var a = converted(chance, multipliers, 0.9); + var b = converted(multiplied, multipliers, 0.9); + assertNotNull(a); assertNotNull(b); + // CHANCE_ONLY: one roll at 50%; MULTIPLY_OUTPUT: 1.5 rolls at 50%. + assertEquals(0.5, (double)a.outputs().get(0).getCount() / a.inputs().get(0).count(), 1e-8); + assertEquals(0.75, (double)b.outputs().get(0).getCount() / b.inputs().get(0).count(), 1e-8); + assertEquals((int)(101 * 0.9F) * a.inputs().get(0).count(), a.energy()); + assertEquals((int)(101 * 0.9F) * b.inputs().get(0).count(), b.energy()); + } + + @Test void sagNoneIgnoresBonusAndBatchesNativeFractionalOutputs() { + var result = converted(sag(SagMillingRecipe.BonusType.NONE, 0.1F), + new SagMillingRecipeAdapter.GrindingBallMultipliers(3, 4), 1); + assertNotNull(result); + assertEquals(10, result.inputs().get(0).count()); + assertEquals(1, result.outputs().get(0).getCount()); + assertEquals(1010, result.energy()); + } + + @Test void sagClampsProbabilityAndDoesNotMutateTaggedSourceOutput() { + var output = new ItemStack(Items.DIAMOND, 2); + output.getOrCreateTag().putInt("marker", 42); + var source = new SagMillingRecipe(ID, Ingredient.of(Items.COAL), + List.of(SagMillingRecipe.OutputItem.of(output, 0.75F, false)), + 101, SagMillingRecipe.BonusType.CHANCE_ONLY); + var result = converted(source, new SagMillingRecipeAdapter.GrindingBallMultipliers(1, 2), 1); + assertNotNull(result); + assertEquals(1, result.inputs().get(0).count()); + assertEquals(2, result.outputs().get(0).getCount()); + assertEquals(42, result.outputs().get(0).getTag().getInt("marker")); + result.outputs().get(0).getTag().putInt("marker", 99); + assertEquals(42, output.getTag().getInt("marker")); + } + + @Test void invalidNativeInputsAndUnsupportedResultKindsAreNotFlattened() { + assertNull(EnderIOAdapterUtils.counted(List.of(new CountedIngredient(Ingredient.of(Items.IRON_INGOT), 0)))); + assertNull(EnderIOAdapterUtils.itemOutputs(List.of(OutputStack.EMPTY))); + assertNull(converted(sag(SagMillingRecipe.BonusType.NONE, Float.NaN), + new SagMillingRecipeAdapter.GrindingBallMultipliers(1, 1), 1)); + assertNull(converted(sag(SagMillingRecipe.BonusType.NONE, 0.25F), + new SagMillingRecipeAdapter.GrindingBallMultipliers(1, 1), Double.POSITIVE_INFINITY)); + } + + @Test void enchanterGeneratesAllLevelsAndConsumesExperienceFluidRatherThanFe() { + var source = spy(new EnchanterRecipe(ID, + new CountedIngredient(Ingredient.of(Items.IRON_INGOT), 2), Enchantments.SHARPNESS, 1)); + // Native config is not loaded in a unit JVM. The running pack supplies these through + // EnchanterRecipe's actual ForgeConfigSpec getters; exercise both costs distinctly. + doAnswer(invocation -> (int)invocation.getArgument(0) * 3).when(source).getXPCostForLevel(anyInt()); + doAnswer(invocation -> (int)invocation.getArgument(0) * 2).when(source).getLapisForLevel(anyInt()); + var adapter = spy(new EnchanterRecipeAdapter()); + doReturn(new ItemStack(Items.BRICKS)).when(adapter).getMoldItem(); + doAnswer(invocation -> new FluidStack(Fluids.WATER, + Math.toIntExact(ExperienceUtil.getFluidFromLevel(invocation.getArgument(0))))) + .when(adapter).experienceFluid(anyInt()); + var recipes = adapter.convertAll(new RecipeHolder<>(ID, source), null); + assertEquals(5, recipes.size()); + assertEquals(5, recipes.stream().map(r -> r.id()).distinct().count()); + for (int level = 1; level <= 5; level++) { + var recipe = recipes.get(level - 1); + assertEquals(2 * level, recipe.inputs().get(1).count()); + assertEquals(2 * level, recipe.inputs().get(2).count()); + assertEquals(0, recipe.energy(), "XP must not be replaced by a trivial FE cost"); + assertEquals(ExperienceUtil.getFluidFromLevel(3 * level), recipe.inputFluids().get(0).amount()); + assertEquals(level, EnchantmentHelper.getEnchantments(recipe.outputs().get(0)).get(Enchantments.SHARPNESS)); + verify(adapter).experienceFluid(3 * level); + } + } + + private static SagMillingRecipe sag(SagMillingRecipe.BonusType bonus, float chance) { + return new SagMillingRecipe(ID, Ingredient.of(Items.COAL), + List.of(SagMillingRecipe.OutputItem.of(new ItemStack(Items.DIAMOND), chance, false)), 101, bonus); + } + private static com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe converted( + SagMillingRecipe source, SagMillingRecipeAdapter.GrindingBallMultipliers multipliers, double power) { + return SagMillingRecipeAdapter.convertRecipe(ID, source, multipliers, power, + List.of(Ingredient.of(Items.BRICKS))); + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/enderio/EnderIOTankPortTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/enderio/EnderIOTankPortTest.java new file mode 100644 index 00000000..92d30dd8 --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/enderio/EnderIOTankPortTest.java @@ -0,0 +1,75 @@ +package com.sorrowmist.useless.content.recipe.adapters.enderio; + +import com.enderio.machines.common.recipe.TankRecipe; +import com.sorrowmist.useless.compat.forge1201.RecipeTestBootstrap; +import com.sorrowmist.useless.compat.forge1201.recipe.DataComponentIngredient; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeHolder; +import com.sorrowmist.useless.content.recipe.FluidIngredientAllocator; +import com.sorrowmist.useless.content.recipe.ItemIngredientAllocator; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fluids.FluidStack; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import java.util.List; +import java.util.Map; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class EnderIOTankPortTest { + private static final ResourceLocation ID = new ResourceLocation("kubejs:native_tank"); + @BeforeAll static void bootstrap() { RecipeTestBootstrap.initialize(); } + private static TankRecipeAdapter adapter() { + var adapter = spy(new TankRecipeAdapter()); + doReturn(Ingredient.of(Items.BRICKS, Items.GLASS)).when(adapter).moldIngredient(); + return adapter; + } + @Test void fillingConsumesExactFluidQuantityAndNbtAndKeepsLargeOutput() { + var fluid = new FluidStack(Fluids.WATER, 250); + fluid.getOrCreateTag().putString("foreign:mixture", "xp"); + var output = new ItemStack(Items.EXPERIENCE_BOTTLE, 200); + output.getOrCreateTag().putInt("foreign:quality", 17); + var source = new TankRecipe(ID, Ingredient.of(Items.GLASS_BOTTLE), output, fluid, false); + var converted = adapter().convertAll(new RecipeHolder<>(ID, source), null).get(0); + assertNull(adapter().getMoldItem()); // Do not index only the ordinary tank. + assertEquals(0, converted.energy()); assertEquals(250, converted.inputFluids().get(0).amount()); + assertTrue(converted.outputFluids().isEmpty()); + assertEquals(200, converted.outputs().get(0).getCount()); + assertEquals(output.getTag(), converted.outputs().get(0).getTag()); + assertFalse(FluidIngredientAllocator.matchesLong(converted.inputFluids(), List.of(new FluidStack(Fluids.WATER, 250)), 1)); + assertFalse(FluidIngredientAllocator.matchesLong(converted.inputFluids(), Map.of(fluid, 249L), 1)); + assertTrue(FluidIngredientAllocator.matchesLong(converted.inputFluids(), Map.of(fluid, 250L), 1)); + var allocated = FluidIngredientAllocator.allocateLong(converted.inputFluids(), List.of(fluid.copy()), 1); + assertNotNull(allocated); assertEquals(250, allocated.consumedFromSupply(0)); + converted.outputs().get(0).getTag().putInt("foreign:quality", 0); + assertEquals(17, output.getTag().getInt("foreign:quality")); + } + @Test void emptyingProducesFluidWithoutRequiringItAndConsumesTheContainer() { + var input = new ItemStack(Items.WET_SPONGE); + input.getOrCreateTag().putString("foreign:state", "keep-predicate"); + var fluid = new FluidStack(Fluids.WATER, 1000); + fluid.getOrCreateTag().putInt("foreign:salinity", 5); + var source = new TankRecipe(ID, DataComponentIngredient.of(true, input), + new ItemStack(Items.SPONGE), fluid, true); + var converted = adapter().convertAll(new RecipeHolder<>(ID, source), null).get(0); + assertTrue(converted.inputFluids().isEmpty()); + assertEquals(1000, converted.outputFluids().get(0).getAmount()); + assertEquals(fluid.getTag(), converted.outputFluids().get(0).getTag()); + assertFalse(ItemIngredientAllocator.matches(converted.inputs(), List.of(new ItemStack(Items.WET_SPONGE)), 1)); + var allocation = ItemIngredientAllocator.allocate(converted.inputs(), List.of(input), 1); + assertNotNull(allocation); assertEquals(1, allocation.consumedFromInput(0)); + converted.outputFluids().get(0).getOrCreateTag().putInt("foreign:salinity", 0); + assertEquals(5, fluid.getTag().getInt("foreign:salinity")); + } + @Test void invalidNativeRecipesAreRejectedInsteadOfCreatingFreeConversions() { + var adapter = adapter(); + assertTrue(adapter.convertAll(null, null).isEmpty()); + assertTrue(adapter.convertAll(new RecipeHolder<>(ID, new TankRecipe(ID, Ingredient.EMPTY, + new ItemStack(Items.DIAMOND), new FluidStack(Fluids.WATER, 1), false)), null).isEmpty()); + assertTrue(adapter.convertAll(new RecipeHolder<>(ID, new TankRecipe(ID, Ingredient.of(Items.COAL), + new ItemStack(Items.DIAMOND), FluidStack.EMPTY, true)), null).isEmpty()); + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/enderio/NativeFireLootResultsTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/enderio/NativeFireLootResultsTest.java new file mode 100644 index 00000000..02759a57 --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/enderio/NativeFireLootResultsTest.java @@ -0,0 +1,42 @@ +package com.sorrowmist.useless.content.recipe.adapters.enderio; + +import com.google.gson.*; +import com.sorrowmist.useless.compat.forge1201.RecipeTestBootstrap; +import net.minecraft.world.item.Items; +import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.*; + +class NativeFireLootResultsTest { + @BeforeAll static void bootstrap() { RecipeTestBootstrap.initialize(); } + private static JsonObject table(String conditions, String functions) { + return JsonParser.parseString(""" + {"pools":[{"rolls":1,"bonus_rolls":0,"conditions":%s, + "entries":[{"type":"minecraft:item","name":"minecraft:diamond","functions":%s}]}]} + """.formatted(conditions,functions)).getAsJsonObject(); + } + @Test void nativeHalfChanceSingleRollKeepsExpectedYield() throws Exception { + var outputs=NativeFireLootResults.parse(table("[{\"condition\":\"minecraft:random_chance\",\"chance\":0.5}]","[]"),1); + assertEquals(1,outputs.size()); + assertTrue(outputs.get(0).result().is(Items.DIAMOND)); + assertEquals(0.5,outputs.get(0).chance()); + assertEquals(1,outputs.get(0).minCount()); + assertEquals(1,outputs.get(0).maxCount()); + } + @Test void uniformCountAndNbtAreBothRetained() throws Exception { + var output=NativeFireLootResults.parse(table("[]",""" + [{"function":"minecraft:set_count","count":{"type":"minecraft:uniform","min":2,"max":4}}, + {"function":"minecraft:set_nbt","tag":"{port_marker:42}"}] + """),4).get(0); + assertEquals(2,output.minCount()); assertEquals(4,output.maxCount()); + assertEquals(42,output.result().getTag().getInt("port_marker")); + } + @Test void contextualConditionsPoolFunctionsAndCombinedCapsAreRejected() { + assertThrows(IllegalArgumentException.class,()->NativeFireLootResults.parse(table( + "[{\"condition\":\"minecraft:killed_by_player\"}]","[]"),1)); + var poolFunction=table("[]","[]"); + poolFunction.getAsJsonArray("pools").get(0).getAsJsonObject().add("functions",new JsonArray()); + assertThrows(IllegalArgumentException.class,()->NativeFireLootResults.parse(poolFunction,1)); + var twoPools=table("[]","[]"); var pools=twoPools.getAsJsonArray("pools"); pools.add(pools.get(0).deepCopy()); + assertThrows(IllegalArgumentException.class,()->NativeFireLootResults.parse(twoPools,1)); + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/enderio/SoulVialSetIngredientPortTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/enderio/SoulVialSetIngredientPortTest.java new file mode 100644 index 00000000..2c7ca5f6 --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/enderio/SoulVialSetIngredientPortTest.java @@ -0,0 +1,54 @@ +package com.sorrowmist.useless.content.recipe.adapters.enderio; + +import com.sorrowmist.useless.compat.forge1201.RecipeTestBootstrap; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeCodecs; +import io.netty.buffer.Unpooled; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraftforge.common.crafting.CraftingHelper; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import java.util.List; +import static org.junit.jupiter.api.Assertions.*; + +class SoulVialSetIngredientPortTest { + @BeforeAll static void bootstrap() { + RecipeTestBootstrap.initialize(); + CraftingHelper.register(new ResourceLocation("useless_mod:soul_vial_set"),SoulVialSetIngredient.TYPE); + } + private static ItemStack vial(String entity) { + var stack=new ItemStack(Items.GLASS_BOTTLE); stack.getOrCreateTag().putString("Entity",entity); return stack; + } + @Test void nativeJsonAndWireKeepExactNbtAndAllAlternatives() { + var zombie=vial("minecraft:zombie"); var skeleton=vial("minecraft:skeleton"); + Ingredient source=SoulVialSetIngredient.of(List.of(zombie,skeleton)); + var json=Ingredient.fromJson(source.toJson()); + var buffer=new FriendlyByteBuf(Unpooled.buffer()); + try { + RecipeCodecs.INGREDIENT_STREAM.encode(buffer,source); + var wire=RecipeCodecs.INGREDIENT_STREAM.decode(buffer); + for(var decoded:List.of(json,wire)) { + assertInstanceOf(SoulVialSetIngredient.class,decoded); + assertTrue(decoded.test(zombie.copyWithCount(64))); assertTrue(decoded.test(skeleton)); + assertFalse(decoded.test(vial("minecraft:creeper"))); + assertFalse(decoded.test(new ItemStack(Items.GLASS_BOTTLE))); + var extra=zombie.copy(); extra.getOrCreateTag().putBoolean("OtherModFlag",true); + assertFalse(decoded.test(extra)); + assertEquals(2,decoded.getItems().length); + } + assertEquals(0,buffer.readableBytes()); + } finally {buffer.release();} + } + @Test void defensiveCopiesPreventDisplayCodeFromMutatingRecipePredicates() { + var stack=vial("minecraft:zombie"); + var ingredient=(SoulVialSetIngredient)SoulVialSetIngredient.of(List.of(stack,stack.copyWithCount(64))); + stack.getOrCreateTag().putString("Entity","minecraft:creeper"); + assertEquals(1,ingredient.getItems().length); + ingredient.getItems()[0].getOrCreateTag().putString("Entity","minecraft:skeleton"); + assertTrue(ingredient.test(vial("minecraft:zombie"))); assertFalse(ingredient.test(stack)); + assertThrows(IllegalArgumentException.class,()->SoulVialSetIngredient.of(List.of(ItemStack.EMPTY))); + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/NativeExDeorumTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/NativeExDeorumTest.java new file mode 100644 index 00000000..11e2fdae --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/exdeorum/NativeExDeorumTest.java @@ -0,0 +1,28 @@ +package com.sorrowmist.useless.content.recipe.adapters.exdeorum; +import com.sorrowmist.useless.compat.forge1201.RecipeTestBootstrap; +import net.minecraft.world.level.storage.loot.providers.number.*; +import net.minecraft.world.item.Items; +import net.minecraft.nbt.CompoundTag; +import thedarkcolour.exdeorum.loot.SummationGenerator; +import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.*; + +class NativeExDeorumTest { + @BeforeAll static void init(){RecipeTestBootstrap.initialize();} + @Test void uniformUsesBothBoundsWithoutNativePrecedenceBug(){ + assertEquals(4,ExDeorumRecipeAdapterUtils.expectedAmount(UniformGenerator.between(2,6)).orElseThrow()); + } + @Test void binomialAndSummationRetainExpectedQuantities(){ + var sum=new SummationGenerator(new NumberProvider[]{ConstantValue.exactly(2),BinomialDistributionGenerator.binomial(8,0.25f)}); + assertEquals(4,ExDeorumRecipeAdapterUtils.expectedAmount(sum).orElseThrow()); + } + @Test void unknownContextProviderIsRejected(){ + assertTrue(NativeNumberExpectations.expected(com.google.gson.JsonParser.parseString("{\"type\":\"minecraft:score\",\"score\":\"test\"}")).isEmpty()); + } + @Test void itemNbtIsCopiedWithoutSharingRecipeState(){ + var tag=new CompoundTag(); tag.putString("marker","original"); + var stack=ExDeorumRecipeAdapterUtils.resultStack(Items.DIAMOND,tag); + assertEquals("original",stack.getTag().getString("marker")); + stack.getTag().putString("marker","changed"); assertEquals("original",tag.getString("marker")); + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/extendedcrafting/NativeTableRemainderTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/extendedcrafting/NativeTableRemainderTest.java new file mode 100644 index 00000000..4018beeb --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/extendedcrafting/NativeTableRemainderTest.java @@ -0,0 +1,66 @@ +package com.sorrowmist.useless.content.recipe.adapters.extendedcrafting; + +import com.blakebr0.extendedcrafting.crafting.recipe.ShapedTableRecipe; +import com.sorrowmist.useless.compat.forge1201.RecipeTestBootstrap; +import net.minecraft.core.NonNullList; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.*; +import net.minecraft.world.item.crafting.Ingredient; +import org.junit.jupiter.api.*; +import java.util.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class NativeTableRemainderTest { + @BeforeAll static void bootstrap() { RecipeTestBootstrap.initialize(); } + private static ShapedTableRecipe recipe(int width,int height,Ingredient ingredient) { + return new ShapedTableRecipe(new ResourceLocation("test:table"),width,height, + NonNullList.withSize(width*height,ingredient),new ItemStack(Items.DIAMOND)); + } + // Official Extended Crafting ends at tier 4. The pack's unofficial build adds tier 5 + // with the same native remainder contract; mock only that version-specific tier answer. + private static ShapedTableRecipe epicRecipe(Ingredient ingredient) { + var recipe = spy(recipe(11, 11, ingredient)); + doReturn(5).when(recipe).getTier(); + return recipe; + } + @Test void repeatedTagAlternativesDoNotMultiplyUnrelatedSlots() { + var recipe=recipe(9,9,Ingredient.of(Items.IRON_INGOT,Items.GOLD_INGOT,Items.COPPER_INGOT)); + assertEquals(List.of(),ExtendedCraftingTableRecipeAdapter.tableRemainders(recipe,recipe.getIngredients()).orElseThrow()); + } + @Test void epicTableSupportsAll121SlotsWithTagAlternatives() { + var recipe=epicRecipe(Ingredient.of(Items.IRON_INGOT,Items.GOLD_INGOT,Items.COPPER_INGOT)); + assertEquals(5,recipe.getTier()); + assertEquals(121,ExtendedCraftingTableRecipeAdapter.tableCapacity(recipe.getTier())); + assertTrue(recipe.getIngredients().size() <= ExtendedCraftingTableRecipeAdapter.tableCapacity(recipe.getTier())); + assertEquals(List.of(),ExtendedCraftingTableRecipeAdapter.tableRemainders(recipe,recipe.getIngredients()).orElseThrow()); + } + @Test void epicTableTransformerPreservesElevenColumnPositions() { + var recipe=epicRecipe(Ingredient.of(Items.IRON_INGOT)); + recipe.setTransformer((x,y,stack) -> new ItemStack(Items.GOLD_NUGGET,y+1)); + var outputs=ExtendedCraftingTableRecipeAdapter.tableRemainders(recipe,recipe.getIngredients()).orElseThrow(); + assertEquals(726,outputs.stream().mapToInt(ItemStack::getCount).sum()); + assertTrue(outputs.stream().allMatch(s -> s.is(Items.GOLD_NUGGET))); + } + @Test void nativeBucketsReturnOneContainerPerConsumedSlot() { + var recipe=recipe(5,5,Ingredient.of(Items.WATER_BUCKET,Items.LAVA_BUCKET)); + var outputs=ExtendedCraftingTableRecipeAdapter.tableRemainders(recipe,recipe.getIngredients()).orElseThrow(); + assertEquals(1,outputs.size());assertTrue(outputs.get(0).is(Items.BUCKET));assertEquals(25,outputs.get(0).getCount()); + } + @Test void candidateDependentContainerIsRejected() { + var recipe=recipe(3,3,Ingredient.of(Items.WATER_BUCKET,Items.IRON_INGOT)); + assertTrue(ExtendedCraftingTableRecipeAdapter.tableRemainders(recipe,recipe.getIngredients()).isEmpty()); + } + @Test void rectangularTransformerUsesRealSquareTableGrid() { + var recipe=recipe(2,3,Ingredient.of(Items.IRON_INGOT)); + recipe.setTransformer((x,y,stack) -> new ItemStack(Items.GOLD_NUGGET,y+1)); + var outputs=ExtendedCraftingTableRecipeAdapter.tableRemainders(recipe,recipe.getIngredients()).orElseThrow(); + assertEquals(1,outputs.size());assertEquals(12,outputs.get(0).getCount()); + } + @Test void customCoupledResolverStillRejectsUnboundedCombinations() { + var slots=Collections.nCopies(9,Ingredient.of(Items.IRON_INGOT,Items.GOLD_INGOT,Items.COPPER_INGOT)); + assertTrue(ExtendedCraftingAdapterUtils.deterministicRemainders(slots,Set.of(), + values -> new net.minecraft.world.SimpleContainer(values.toArray(ItemStack[]::new)), + values -> List.of()).isEmpty()); + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/hostilenetworks/HostileNetworksPortTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/hostilenetworks/HostileNetworksPortTest.java new file mode 100644 index 00000000..7d538671 --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/hostilenetworks/HostileNetworksPortTest.java @@ -0,0 +1,96 @@ +package com.sorrowmist.useless.content.recipe.adapters.hostilenetworks; + +import com.sorrowmist.useless.compat.forge1201.RecipeTestBootstrap; +import com.sorrowmist.useless.compat.forge1201.recipe.RecipeCodecs; +import dev.shadowsoffire.hostilenetworks.HostileConfig; +import dev.shadowsoffire.hostilenetworks.data.DataModel; +import io.netty.buffer.Unpooled; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraftforge.common.crafting.CraftingHelper; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class HostileNetworksPortTest { + @BeforeAll static void bootstrap() { + RecipeTestBootstrap.initialize(); + CraftingHelper.register(new ResourceLocation("useless_mod:data_model_range"),DataModelRangeIngredient.TYPE); + } + @Test void modelRangeJsonAndPacketKeepModelIdAndInclusiveDataBounds() { + var source=new DataModelRangeIngredient(new ResourceLocation("custom:model"),6,53); + var buffer=new FriendlyByteBuf(Unpooled.buffer()); + try { + RecipeCodecs.INGREDIENT_STREAM.encode(buffer,source); + var wire=RecipeCodecs.INGREDIENT_STREAM.decode(buffer); + var json=Ingredient.fromJson(source.toJson()); + assertEquals(source,wire); assertEquals(source,json); + assertEquals(6,((DataModelRangeIngredient)wire).minData()); + assertEquals(53,((DataModelRangeIngredient)wire).maxData()); + assertEquals(new ResourceLocation("custom:model"),((DataModelRangeIngredient)wire).modelId()); + assertEquals(0,buffer.readableBytes()); + } finally {buffer.release();} + } + @Test void disabledSimulationUpgradeDoesNotGenerateAProgressionBypass() throws Exception { + int original=HostileConfig.simModelUpgrade; + try { + HostileConfig.simModelUpgrade=0; + Method training=HostileNetworksRecipeAdapter.class.getDeclaredMethod("addTrainingRecipes",List.class,DataModel.class); + training.setAccessible(true); var recipes=new ArrayList<>(); + // Gate must run before any registry/model access; original pack explicitly sets this to 0. + training.invoke(null,recipes,null); assertTrue(recipes.isEmpty()); + } finally {HostileConfig.simModelUpgrade=original;} + } + @Test void impossibleDataIntervalsAreRejected() { + assertThrows(IllegalArgumentException.class,()->new DataModelRangeIngredient(new ResourceLocation("custom:model"),54,53)); + } + + @Test void continuousAccuracyMatchesInstalledHnnBytecodeAtIntermediateData() throws Exception { + boolean original = HostileConfig.continuousAccuracy; + try (var items = mockStatic(dev.shadowsoffire.hostilenetworks.item.DataModelItem.class)) { + // CachedModel.EMPTY initializes from ItemStack.EMPTY. Supply an unbound holder only + // for that bootstrap; this unit JVM deliberately has no global Forge ModList. + var unbound = mock(dev.shadowsoffire.placebo.reload.DynamicHolder.class); + items.when(() -> dev.shadowsoffire.hostilenetworks.item.DataModelItem.getStoredModel(any())).thenReturn(unbound); + var model = mock(DataModel.class); + var tier = dev.shadowsoffire.hostilenetworks.data.ModelTier.BASIC; + when(model.getTierData(tier)).thenReturn(6); + when(model.getTierData(tier.next())).thenReturn(54); + var nativeModel = mock(dev.shadowsoffire.hostilenetworks.data.CachedModel.class, CALLS_REAL_METHODS); + // Mockito's inline maker preserves the concrete native class. Fields remain on CachedModel. + var clazz = dev.shadowsoffire.hostilenetworks.data.CachedModel.class; + var tierMember = clazz.getDeclaredField("tier"); tierMember.setAccessible(true); tierMember.set(nativeModel, tier); + var dataMember = clazz.getDeclaredField("data"); dataMember.setAccessible(true); + doReturn(6).when(nativeModel).getTierData(); + doReturn(54).when(nativeModel).getNextTierData(); + for (boolean continuous : List.of(false, true)) { + HostileConfig.continuousAccuracy = continuous; + for (int data : new int[]{6, 7, 30, 53}) { + dataMember.setInt(nativeModel, data); + assertEquals(nativeModel.getAccuracy(), HostileNetworksRecipeAdapter.accuracyAt(model, tier, data), + "Mismatch at data=" + data + ", continuous=" + continuous); + } + } + assertTrue(HostileNetworksRecipeAdapter.accuracyAt(model, tier, 30) > tier.accuracy()); + assertTrue(Float.isNaN(HostileNetworksRecipeAdapter.accuracyAt(model, tier, 54))); + assertEquals(0F, HostileNetworksRecipeAdapter.accuracyAt(model, + dev.shadowsoffire.hostilenetworks.data.ModelTier.FAULTY, 0)); + } finally { HostileConfig.continuousAccuracy = original; } + } + + @Test void exactDataPredicateDoesNotWidenToWholeTierDuringPacketRoundTrip() { + var source = new DataModelRangeIngredient(new ResourceLocation("custom:continuous"), 30, 30); + var buf = new FriendlyByteBuf(Unpooled.buffer()); + try { + RecipeCodecs.INGREDIENT_STREAM.encode(buf, source); + var result = (DataModelRangeIngredient)RecipeCodecs.INGREDIENT_STREAM.decode(buf); + assertEquals(30, result.minData()); assertEquals(30, result.maxData()); + assertEquals(0, buf.readableBytes()); + } finally { buf.release(); } + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/DissolutionNativePortTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/DissolutionNativePortTest.java new file mode 100644 index 00000000..e597fc91 --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/industrialforegoing/DissolutionNativePortTest.java @@ -0,0 +1,34 @@ +package com.sorrowmist.useless.content.recipe.adapters.industrialforegoing; + +import com.sorrowmist.useless.compat.forge1201.RecipeTestBootstrap; +import com.sorrowmist.useless.content.recipe.*; +import net.minecraft.world.item.*; +import net.minecraft.world.item.crafting.Ingredient; +import org.junit.jupiter.api.*; +import java.util.*; +import static org.junit.jupiter.api.Assertions.*; + +class DissolutionNativePortTest { + @BeforeAll static void bootstrap() { RecipeTestBootstrap.initialize(); } + @Test void nativeValuesRemainSeparateRequiredSlots() { + var inputs=DissolutionChamberRecipeAdapter.nativeInputs(new Ingredient.Value[]{ + new Ingredient.ItemValue(new ItemStack(Items.IRON_INGOT)), + new Ingredient.ItemValue(new ItemStack(Items.GOLD_INGOT)), + new Ingredient.ItemValue(new ItemStack(Items.IRON_INGOT))}); + var requirements=AdapterUtils.mergeIngredients(inputs); + assertFalse(ItemIngredientAllocator.matches(requirements,List.of(new ItemStack(Items.IRON_INGOT,3)),1)); + var allocation=ItemIngredientAllocator.allocate(requirements, + List.of(new ItemStack(Items.IRON_INGOT,2),new ItemStack(Items.GOLD_INGOT)),1); + assertNotNull(allocation); + assertEquals(2,allocation.consumedFromInput(0)); assertEquals(1,allocation.consumedFromInput(1)); + } + @Test void eachValuesAlternativesStayOrWithinAnAndSlot() { + Ingredient.Value alternative=new Ingredient.Value() { + public Collection getItems() { return List.of(new ItemStack(Items.IRON_INGOT),new ItemStack(Items.GOLD_INGOT)); } + public com.google.gson.JsonObject serialize() { return new com.google.gson.JsonObject(); } + }; + var inputs=AdapterUtils.mergeIngredients(DissolutionChamberRecipeAdapter.nativeInputs(new Ingredient.Value[]{alternative,alternative})); + assertTrue(ItemIngredientAllocator.matches(inputs,List.of(new ItemStack(Items.GOLD_INGOT,2)),1)); + assertFalse(ItemIngredientAllocator.matches(inputs,List.of(new ItemStack(Items.GOLD_INGOT)),1)); + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/NativeMekanismIngredientsTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/NativeMekanismIngredientsTest.java new file mode 100644 index 00000000..def33d3f --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/NativeMekanismIngredientsTest.java @@ -0,0 +1,68 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism; + +import com.google.gson.*; +import com.sorrowmist.useless.compat.forge1201.RecipeTestBootstrap; +import mekanism.api.recipes.ingredients.creator.IngredientCreatorAccess; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.common.crafting.PartialNBTIngredient; +import net.minecraftforge.fluids.FluidStack; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import java.util.Set; +import static org.junit.jupiter.api.Assertions.*; + +class NativeMekanismIngredientsTest { + @BeforeAll static void bootstrap() { RecipeTestBootstrap.initialize(); } + @Test void nativeItemCountAndPartialNbtStayStructural() { + CompoundTag nbt=new CompoundTag(); nbt.putString("quality","pure"); + var predicate=PartialNBTIngredient.of(Items.IRON_INGOT,nbt); + var source=IngredientCreatorAccess.item().from(predicate,4); + var converted=MekanismChemicalRecipeSupport.item(source); + assertEquals(4,converted.count()); + ItemStack matching=new ItemStack(Items.IRON_INGOT,4); matching.setTag(nbt.copy()); matching.getTag().putInt("extra",5); + assertEquals(source.testType(matching),converted.ingredient().test(matching)); + assertFalse(converted.ingredient().test(new ItemStack(Items.IRON_INGOT,4))); + assertEquals(12,MekanismChemicalRecipeSupport.countedItem(source,3).count()); + } + @Test void sameCostNativeAlternativesKeepBothPredicates() { + var source=IngredientCreatorAccess.item().createMulti( + IngredientCreatorAccess.item().from(Ingredient.of(Items.IRON_INGOT),2), + IngredientCreatorAccess.item().from(Ingredient.of(Items.GOLD_INGOT),2)); + var converted=MekanismChemicalRecipeSupport.item(source); + assertEquals(2,converted.count()); + assertTrue(converted.ingredient().test(new ItemStack(Items.IRON_INGOT))); + assertTrue(converted.ingredient().test(new ItemStack(Items.GOLD_INGOT))); + assertFalse(converted.ingredient().test(new ItemStack(Items.DIAMOND))); + } + @Test void nativeFluidExactNbtIncludesAbsenceAndAdditionalTags() { + var source=IngredientCreatorAccess.fluid().from(new FluidStack(Fluids.WATER,500)); + var converted=MekanismChemicalRecipeSupport.fluidIngredients(source).get(0); + assertEquals(500,converted.amount()); + var input=new FluidStack(Fluids.WATER,500); + assertEquals(source.testType(input),converted.ingredient().test(input)); + input.getOrCreateTag().putString("extra","yes"); + assertFalse(source.testType(input)); assertFalse(converted.ingredient().test(input)); + var tagged=IngredientCreatorAccess.fluid().from(input); + var restored=MekanismChemicalRecipeSupport.fluidIngredients(tagged).get(0).ingredient(); + assertTrue(restored.test(input)); + input.getOrCreateTag().putInt("additional",1); + assertFalse(restored.test(input)); + } + @Test void differingAlternativeCountsAndOverflowsNeverBecomeCheaper() { + var source=IngredientCreatorAccess.item().createMulti( + IngredientCreatorAccess.item().from(Ingredient.of(Items.IRON_INGOT),2), + IngredientCreatorAccess.item().from(Ingredient.of(Items.GOLD_INGOT),3)); + assertThrows(JsonSyntaxException.class,()->MekanismChemicalRecipeSupport.item(source)); + assertThrows(JsonSyntaxException.class,()->NativeMekanismIngredients.item( + JsonParser.parseString("{\"ingredient\":{\"item\":\"minecraft:iron_ingot\"},\"amount\":9223372036854775807}"),2)); + assertThrows(JsonSyntaxException.class,()->NativeMekanismIngredients.fluid( + JsonParser.parseString("{\"fluid\":\"minecraft:water\",\"amount\":2147483648}"))); + } + @Test void fractionalEnergyMultiplierRoundsUpInsteadOfTruncating() { + assertEquals(3,MekanismChemicalRecipeSupport.energyCost(1,2,mekanism.api.math.FloatingLong.parseFloatingLong("1.25"))); + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/NativeMekanismQuantityTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/NativeMekanismQuantityTest.java new file mode 100644 index 00000000..16e3a625 --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/mekanism/NativeMekanismQuantityTest.java @@ -0,0 +1,77 @@ +package com.sorrowmist.useless.content.recipe.adapters.mekanism; + +import com.google.gson.JsonParser; +import com.sorrowmist.useless.compat.forge1201.RecipeTestBootstrap; +import com.sorrowmist.useless.content.recipe.AdvancedAlloyFurnaceRecipe; +import mekanism.api.recipes.ingredients.ItemStackIngredient; +import mekanism.api.recipes.ingredients.FluidStackIngredient; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fluids.FluidStack; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import java.util.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class NativeMekanismQuantityTest { + @BeforeAll static void bootstrap(){RecipeTestBootstrap.initialize();} + public static final class Inputs { + ItemStackIngredient item; FluidStackIngredient fluid; + public ItemStackIngredient getItemInput(){return item;} + public FluidStackIngredient getFluidInput(){return fluid;} + } + static Inputs inputs(){ + var inputs=new Inputs(); + inputs.item=mock(ItemStackIngredient.class); + when(inputs.item.serialize()).thenReturn(JsonParser.parseString("[{\"ingredient\":{\"item\":\"minecraft:iron_ingot\"},\"amount\":2},{\"ingredient\":{\"item\":\"minecraft:gold_ingot\"},\"amount\":7}]")); + inputs.fluid=mock(FluidStackIngredient.class); + when(inputs.fluid.serialize()).thenReturn(JsonParser.parseString("[{\"fluid\":\"minecraft:water\",\"amount\":200},{\"fluid\":\"minecraft:lava\",\"amount\":700}]")); + return inputs; + } + static AdvancedAlloyFurnaceRecipe convert(Inputs input){ + return MekanismChemicalRecipeSupport.recipe(new ResourceLocation("test:quantities"), + MekanismChemicalRecipeSupport.items(input.item),MekanismChemicalRecipeSupport.fluidIngredients(input.fluid), + List.of(),List.of(new ItemStack(Items.DIAMOND)),List.of(),List.of(),1,1,null); + } + @Test void differingItemAndFluidQuantitiesProduceIndependentOrVariants(){ + var inputs=inputs(); + assertTrue(NativeMekanismVariants.needsExpansion(inputs)); + var recipes=NativeMekanismVariants.expand(inputs,()->List.of(convert(inputs))); + assertEquals(4,recipes.size()); + var quantities=new HashSet(); var ids=new HashSet(); + for(var recipe:recipes){ + assertEquals(1,recipe.inputs().size()); assertEquals(1,recipe.inputFluids().size()); + quantities.add(recipe.inputs().get(0).count()+":"+recipe.inputFluids().get(0).amount()); ids.add(recipe.id()); + } + assertEquals(Set.of("2:200","2:700","7:200","7:700"),quantities); assertEquals(4,ids.size()); + assertTrue(NativeMekanismVariants.needsExpansion(inputs)); + } + @Test void matchingRespectsEachAlternativesOwnQuantity(){ + var inputs=inputs(); + assertFalse(MekanismChemicalRecipeSupport.matchesItem(Map.of(Ingredient.of(Items.GOLD_INGOT),6L),inputs.item)); + assertTrue(MekanismChemicalRecipeSupport.matchesItem(Map.of(Ingredient.of(Items.GOLD_INGOT),7L),inputs.item)); + assertFalse(MekanismChemicalRecipeSupport.matchesFluid(Map.of(new FluidStack(Fluids.LAVA,1),699L),inputs.fluid)); + assertTrue(MekanismChemicalRecipeSupport.matchesFluid(Map.of(new FluidStack(Fluids.LAVA,1),700L),inputs.fluid)); + } + @Test void failedConversionAlwaysClearsQuantityContext(){ + var inputs=inputs(); + assertThrows(IllegalStateException.class,()->NativeMekanismVariants.expand(inputs,()->{throw new IllegalStateException("test");})); + assertTrue(NativeMekanismVariants.needsExpansion(inputs)); + } + @Test void fluidBatchUsesSameFactorOnBothSidesWithoutSaturation(){ + var recipe=MekanismChemicalRecipeSupport.recipe(new ResourceLocation("test:batch"),List.of(), + List.of(new FluidStack(Fluids.WATER,3_000_000)),List.of(),List.of(), + List.of(new FluidStack(Fluids.LAVA,6_000_000)),List.of(),1,1,null); + assertEquals(1_071_000_000L,recipe.inputFluids().get(0).amount()); + assertEquals(2_142_000_000,recipe.outputFluids().get(0).getAmount()); + } + @Test void invalidRequiredFluidAndArithmeticOverflowCannotCreateFreeOutputs(){ + assertThrows(IllegalArgumentException.class,()->MekanismChemicalRecipeSupport.recipe(new ResourceLocation("test:invalid"), + List.of(),List.of(FluidStack.EMPTY),List.of(),List.of(),List.of(new FluidStack(Fluids.LAVA,1000)),List.of(),1,1,null)); + assertThrows(ArithmeticException.class,()->MekanismChemicalRecipeSupport.saturatingMultiply(Long.MAX_VALUE,2)); + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/minecraft/NativeBrewingPortTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/minecraft/NativeBrewingPortTest.java new file mode 100644 index 00000000..9c3dd36d --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/minecraft/NativeBrewingPortTest.java @@ -0,0 +1,146 @@ +package com.sorrowmist.useless.content.recipe.adapters.minecraft; + +import com.sorrowmist.useless.compat.forge1201.RecipeTestBootstrap; +import com.sorrowmist.useless.compat.forge1201.recipe.DataComponentIngredient; +import com.sorrowmist.useless.content.recipe.ItemIngredientAllocator; +import net.minecraft.core.RegistryAccess; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.alchemy.PotionUtils; +import net.minecraft.world.item.alchemy.Potions; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraftforge.common.brewing.BrewingRecipe; +import net.minecraftforge.common.brewing.BrewingRecipeRegistry; +import net.minecraftforge.common.brewing.IBrewingRecipe; +import net.minecraftforge.common.brewing.VanillaBrewingRecipe; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import java.util.List; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class NativeBrewingPortTest { + @BeforeAll static void bootstrap() { RecipeTestBootstrap.initialize(); } + private static ItemStack potion() { return PotionUtils.setPotion(new ItemStack(Items.POTION), Potions.AWKWARD); } + + @Test void vanillaPotionAndContainerMixesUseNativePotionNbtAndThreeBottleBatch() { + var source = new VanillaBrewingRecipe(); + var input = potion().copyWithCount(3); + var reagent = new ItemStack(Items.BLAZE_POWDER); + var recipes = BrewingRecipeAdapter.findRuntimeRecipes(List.of(source), List.of(input, reagent), RegistryAccess.EMPTY); + assertEquals(1, recipes.size()); + var recipe = recipes.get(0).value().convertedRecipe(); + assertEquals(3, recipe.outputs().get(0).getCount()); + assertEquals(Potions.STRENGTH, PotionUtils.getPotion(recipe.outputs().get(0))); + assertEquals(400, recipe.processTime()); + assertTrue(ItemIngredientAllocator.matches(recipe.inputs(), List.of(input, reagent), 1)); + assertFalse(ItemIngredientAllocator.matches(recipe.inputs(), List.of(input.copyWithCount(2), reagent), 1)); + assertEquals(Potions.AWKWARD, PotionUtils.getPotion(input)); + var container = BrewingRecipeAdapter.findRuntimeRecipes(List.of(source), + List.of(input, new ItemStack(Items.GUNPOWDER)), RegistryAccess.EMPTY); + assertEquals(1, container.size()); + assertTrue(container.get(0).value().convertedRecipe().outputs().get(0).is(Items.SPLASH_POTION)); + assertEquals(Potions.AWKWARD, PotionUtils.getPotion(container.get(0).value().convertedRecipe().outputs().get(0))); + } + + @Test void forgeSimpleRecipeKeepsExactForeignNbtCountsAndActualConsumption() { + var input = new ItemStack(Items.GLASS_BOTTLE); + input.getOrCreateTag().putString("foreign:mixture", "special"); + var reagent = new ItemStack(Items.REDSTONE); + reagent.getOrCreateTag().putInt("foreign:tier", 5); + var output = new ItemStack(Items.DIAMOND, 200); + output.getOrCreateTag().putLong("foreign:progress", 9876543210L); + var source = new BrewingRecipe(DataComponentIngredient.of(true, input), + DataComponentIngredient.of(true, reagent), output); + var recipes = BrewingRecipeAdapter.findRuntimeRecipes(List.of(source), + List.of(input.copyWithCount(3), reagent), RegistryAccess.EMPTY); + assertEquals(1, recipes.size()); + var recipe = recipes.get(0).value().convertedRecipe(); + assertEquals(600, recipe.outputs().get(0).getCount()); + assertEquals(output.getTag(), recipe.outputs().get(0).getTag()); + var allocated = ItemIngredientAllocator.allocate(recipe.inputs(), List.of(input.copyWithCount(3), reagent), 1); + assertNotNull(allocated); assertEquals(3, allocated.consumedFromInput(0)); assertEquals(1, allocated.consumedFromInput(1)); + assertFalse(ItemIngredientAllocator.matches(recipe.inputs(), + List.of(new ItemStack(Items.GLASS_BOTTLE, 3), reagent), 1)); + recipe.outputs().get(0).getTag().putLong("foreign:progress", 0); + assertEquals(9876543210L, output.getTag().getLong("foreign:progress")); + } + + @Test void arbitraryModBrewingIsEvaluatedForActualNbtInsteadOfFlattenedDisplayCandidates() { + IBrewingRecipe source = new IBrewingRecipe() { + @Override public boolean isInput(ItemStack stack) { return stack.is(Items.GLASS_BOTTLE) && stack.hasTag(); } + @Override public boolean isIngredient(ItemStack stack) { return stack.is(Items.REDSTONE); } + @Override public ItemStack getOutput(ItemStack input, ItemStack reagent) { + if (!isInput(input) || !isIngredient(reagent)) return ItemStack.EMPTY; + var output = new ItemStack(Items.EXPERIENCE_BOTTLE); + output.setTag(input.getTag().copy()); + return output; + } + }; + var input = new ItemStack(Items.GLASS_BOTTLE, 3); + input.getOrCreateTag().putLong("foreign:dynamic", 77); + var reagent = new ItemStack(Items.REDSTONE); + var a = BrewingRecipeAdapter.findRuntimeRecipes(List.of(source), List.of(input, reagent), RegistryAccess.EMPTY); + assertEquals(1, a.size()); + assertEquals(77, a.get(0).value().convertedRecipe().outputs().get(0).getTag().getLong("foreign:dynamic")); + input.getOrCreateTag().putLong("foreign:dynamic", 88); + var b = BrewingRecipeAdapter.findRuntimeRecipes(List.of(source), List.of(input, reagent), RegistryAccess.EMPTY); + assertNotEquals(a.get(0).id(), b.get(0).id()); + assertEquals(88, b.get(0).value().convertedRecipe().outputs().get(0).getTag().getLong("foreign:dynamic")); + assertTrue(BrewingRecipeAdapter.findRuntimeRecipes(List.of(source), + List.of(new ItemStack(Items.GLASS_BOTTLE, 3), reagent), RegistryAccess.EMPTY).isEmpty()); + } + + @Test void orderedSnapshotMatchesActualForgeRegistryPriority() throws ReflectiveOperationException { + var first = new BrewingRecipe(Ingredient.of(Items.GLASS_BOTTLE), Ingredient.of(Items.COAL), new ItemStack(Items.DIAMOND)); + var second = new BrewingRecipe(Ingredient.of(Items.GLASS_BOTTLE), Ingredient.of(Items.COAL), new ItemStack(Items.EMERALD)); + var original = List.copyOf(BrewingRecipeRegistry.getRecipes()); + // Forge deliberately exposes an unmodifiable view. Test-only reflective cleanup is + // needed because its public API permits registration but has no removal method. + var field = BrewingRecipeRegistry.class.getDeclaredField("recipes"); + field.setAccessible(true); + @SuppressWarnings("unchecked") var mutable = (List) field.get(null); + // Add and remove only our two real registry entries. The native registry chooses first. + BrewingRecipeRegistry.addRecipe(first); BrewingRecipeRegistry.addRecipe(second); + try { + var nativeResult = BrewingRecipeRegistry.getOutput(new ItemStack(Items.GLASS_BOTTLE), new ItemStack(Items.COAL)); + var result = BrewingRecipeAdapter.findRuntimeRecipes(BrewingRecipeRegistry.getRecipes(), + List.of(new ItemStack(Items.GLASS_BOTTLE, 3), new ItemStack(Items.COAL)), RegistryAccess.EMPTY); + assertEquals(1, result.size()); + assertEquals(nativeResult.getItem(), result.get(0).value().convertedRecipe().outputs().get(0).getItem()); + assertTrue(nativeResult.is(Items.DIAMOND)); + } finally { + mutable.remove(first); mutable.remove(second); + assertEquals(original, BrewingRecipeRegistry.getRecipes()); + } + } + + @Test void sharedInputAndReagentCannotDoubleSpendOneStackAndOutputOverflowIsRejected() { + var source = new BrewingRecipe(Ingredient.of(Items.IRON_INGOT), Ingredient.of(Items.IRON_INGOT), new ItemStack(Items.DIAMOND)); + assertTrue(BrewingRecipeAdapter.findRuntimeRecipes(List.of(source), List.of(new ItemStack(Items.IRON_INGOT, 3)), RegistryAccess.EMPTY).isEmpty()); + var result = BrewingRecipeAdapter.findRuntimeRecipes(List.of(source), List.of(new ItemStack(Items.IRON_INGOT, 4)), RegistryAccess.EMPTY); + assertEquals(1, result.size()); + var consumed = ItemIngredientAllocator.allocate(result.get(0).value().convertedRecipe().inputs(), + List.of(new ItemStack(Items.IRON_INGOT, 4)), 1); + assertNotNull(consumed); assertEquals(4, consumed.consumedFromInput(0)); + var tooLarge = new BrewingRecipe(Ingredient.of(Items.IRON_INGOT), Ingredient.of(Items.COAL), new ItemStack(Items.DIAMOND, Integer.MAX_VALUE)); + assertTrue(BrewingRecipeAdapter.findRuntimeRecipes(List.of(tooLarge), + List.of(new ItemStack(Items.IRON_INGOT, 3), new ItemStack(Items.COAL)), RegistryAccess.EMPTY).isEmpty()); + } + + @Test void staticCatalogueKeepsOnlyNativeAcceptedPairsAndOutputNbtChangesItsStableId() { + var source = new BrewingRecipe(Ingredient.of(Items.GLASS_BOTTLE), Ingredient.of(Items.REDSTONE), new ItemStack(Items.DIAMOND)); + var a = BrewingRecipeAdapter.createStaticRecipes(List.of(source), RegistryAccess.EMPTY, + List.of(new ItemStack(Items.GLASS_BOTTLE)), List.of(new ItemStack(Items.REDSTONE), new ItemStack(Items.COAL))); + assertEquals(1, a.size()); + var b = BrewingRecipeAdapter.createStaticRecipes(List.of(source), RegistryAccess.EMPTY, + List.of(new ItemStack(Items.GLASS_BOTTLE)), List.of(new ItemStack(Items.COAL), new ItemStack(Items.REDSTONE))); + assertEquals(a.get(0).id(), b.get(0).id()); + var output = new ItemStack(Items.DIAMOND); output.getOrCreateTag().putString("foreign:new", "x"); + var changed = new BrewingRecipe(Ingredient.of(Items.GLASS_BOTTLE), Ingredient.of(Items.REDSTONE), output); + var c = BrewingRecipeAdapter.createStaticRecipes(List.of(changed), RegistryAccess.EMPTY, + List.of(new ItemStack(Items.GLASS_BOTTLE)), List.of(new ItemStack(Items.REDSTONE))); + assertNotEquals(a.get(0).id(), c.get(0).id()); + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/summoningrituals/SummoningNativeOutputTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/summoningrituals/SummoningNativeOutputTest.java new file mode 100644 index 00000000..1e68f066 --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/content/recipe/adapters/summoningrituals/SummoningNativeOutputTest.java @@ -0,0 +1,29 @@ +package com.sorrowmist.useless.content.recipe.adapters.summoningrituals; + +import com.sorrowmist.useless.compat.forge1201.RecipeTestBootstrap; +import net.minecraft.nbt.*; +import net.minecraft.world.item.*; +import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.*; + +class SummoningNativeOutputTest { + @BeforeAll static void bootstrap() { RecipeTestBootstrap.initialize(); } + @Test void ordinaryStackNbtIsCopiedIndependently() { + var original=new ItemStack(Items.PAPER,9); original.getOrCreateTag().putInt("quality",5); + var result=SummoningRitualsAltarRecipeAdapter.nativeItemOutput(original,new CompoundTag()); + assertEquals(9,result.getCount()); assertEquals(5,result.getTag().getInt("quality")); + result.getTag().putInt("quality",2); assertEquals(5,original.getTag().getInt("quality")); + } + @Test void itemEntityReplacementUsesNativeFourItemSpawnBatches() { + var replacement=new ItemStack(Items.DIAMOND,2); replacement.getOrCreateTag().putInt("marker",17); + var entityData=new CompoundTag(); entityData.put("Item",replacement.save(new CompoundTag())); + var result=SummoningRitualsAltarRecipeAdapter.nativeItemOutput(new ItemStack(Items.PAPER,9),entityData); + assertTrue(result.is(Items.DIAMOND)); assertEquals(6,result.getCount()); + assertEquals(17,result.getTag().getInt("marker")); + assertEquals(2,entityData.getCompound("Item").getByte("Count")); + } + @Test void entityLifetimeOrOwnershipIsNotMistakenForItemTags() { + var data=new CompoundTag(); data.putInt("Age",5999); + assertNull(SummoningRitualsAltarRecipeAdapter.nativeItemOutput(new ItemStack(Items.PAPER),data)); + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/utils/RateLimitedLoggerTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/utils/RateLimitedLoggerTest.java new file mode 100644 index 00000000..9e2ecb8f --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/utils/RateLimitedLoggerTest.java @@ -0,0 +1,72 @@ +package com.sorrowmist.useless.utils; + +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.IntStream; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.AdditionalMatchers.aryEq; +import static org.mockito.Mockito.*; + +class RateLimitedLoggerTest { + @Test + void repeatsAreSuppressedUntilTheIntervalExpires() { + AtomicLong clock = new AtomicLong(0); + var gate = new RateLimitedLogger.Gate(60, 8, clock::get); + assertTrue(gate.allow("machine", "WARN", "failed {}")); + clock.set(59); + assertFalse(gate.allow("machine", "WARN", "failed {}")); + clock.set(60); + assertTrue(gate.allow("machine", "WARN", "failed {}")); + } + + @Test + void unrelatedMessagesAndSeveritiesRemainIndependent() { + var gate = new RateLimitedLogger.Gate(60, 8, () -> 0); + assertTrue(gate.allow("one", "WARN", "failure")); + assertTrue(gate.allow("one", "ERROR", "failure")); + assertTrue(gate.allow("two", "WARN", "failure")); + assertTrue(gate.allow("one", "WARN", "other failure")); + assertFalse(gate.allow("one", "WARN", "failure")); + } + + @Test + void boundedStorageDoesNotEvictAnActiveSuppressionWindow() { + AtomicLong clock = new AtomicLong(0); + var gate = new RateLimitedLogger.Gate(60, 2, clock::get); + assertTrue(gate.allow("one", "WARN", "first")); + assertTrue(gate.allow("one", "WARN", "second")); + for (int i = 0; i < 100; i++) { + assertFalse(gate.allow("one", "WARN", "overflow " + i)); + } + assertFalse(gate.allow("one", "WARN", "first")); + clock.set(60); + assertTrue(gate.allow("one", "WARN", "new category")); + assertTrue(gate.allow("one", "WARN", "first")); + } + + @Test + void concurrentFailuresOnlyLogOnce() { + var gate = new RateLimitedLogger.Gate(60, 8, () -> 0); + AtomicInteger allowed = new AtomicInteger(); + IntStream.range(0, 1000).parallel().forEach(i -> { + if (gate.allow("machine", "ERROR", "failure")) allowed.incrementAndGet(); + }); + assertEquals(1, allowed.get()); + } + + @Test + void changingArgumentsDoesNotBypassTheLimitAndExceptionsAreRetained() { + Logger logger = mock(Logger.class); + when(logger.getName()).thenReturn("RateLimitedLoggerTest.arguments"); + when(logger.isWarnEnabled()).thenReturn(true); + RuntimeException cause = new RuntimeException("failure"); + RateLimitedLogger.warn(logger, "Failed recipe {}", "one", cause); + RateLimitedLogger.warn(logger, "Failed recipe {}", "two", cause); + verify(logger).warn(eq("Failed recipe {}"), aryEq(new Object[]{"one", cause})); + } +} diff --git a/src/portBridgeTest/java/com/sorrowmist/useless/world/inventory/NativeSavedDataPortTest.java b/src/portBridgeTest/java/com/sorrowmist/useless/world/inventory/NativeSavedDataPortTest.java new file mode 100644 index 00000000..319b8afc --- /dev/null +++ b/src/portBridgeTest/java/com/sorrowmist/useless/world/inventory/NativeSavedDataPortTest.java @@ -0,0 +1,87 @@ +package com.sorrowmist.useless.world.inventory; + +import com.mojang.datafixers.DataFixer; +import com.sorrowmist.useless.compat.forge1201.RecipeTestBootstrap; +import com.sorrowmist.useless.content.blockentities.HighStackItemStackHandler; +import com.sorrowmist.useless.core.component.ExternalInventoryKind; +import com.sorrowmist.useless.core.component.ExternalInventoryReference; +import com.sorrowmist.useless.world.ae.AeConnectLinkSavedData; +import net.minecraft.core.BlockPos; +import net.minecraft.core.RegistryAccess; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.DimensionDataStorage; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.UUID; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** Real native SavedData disk writes/reopens in a disposable directory, never a pack world. */ +class NativeSavedDataPortTest { + @TempDir Path directory; + @BeforeAll static void bootstrap() { RecipeTestBootstrap.initialize(); } + + @Test void nativeSavedDataCallbackWritesAndReloadsLargeInventoryAndOpaquePayload() throws Exception { + var reference = new ExternalInventoryReference(ExternalInventoryKind.FURNACE, UUID.randomUUID()); + var constructor = ExternalInventorySavedData.class.getDeclaredConstructor(ExternalInventoryReference.class); + constructor.setAccessible(true); + var data = constructor.newInstance(reference); + var handler = inventory(); + var stack = new ItemStack(Items.IRON_INGOT, 2_000_000_000); + stack.getOrCreateTag().putString("othermod", "preserved"); + handler.setStackInSlot(399, stack); + data.saveFrom(handler, RegistryAccess.EMPTY); + var payload = new CompoundTag(); payload.putLong("huge", Long.MAX_VALUE); + data.saveRawPayload(payload); + var storage = storage(); storage.set("native_inventory", data); storage.save(); + assertTrue(Files.size(directory.resolve("native_inventory.dat")) > 0); + + var load = ExternalInventorySavedData.class.getDeclaredMethod("load", ExternalInventoryReference.class, CompoundTag.class); + load.setAccessible(true); + var reloaded = storage().get(tag -> { + try { return (ExternalInventorySavedData)load.invoke(null, reference, tag); } + catch (ReflectiveOperationException error) { throw new AssertionError(error); } + }, "native_inventory"); + assertNotNull(reloaded); assertTrue(reloaded.isInitialized()); + var destination = inventory(); + reloaded.loadInto(destination, RegistryAccess.EMPTY); + assertEquals(2_000_000_000, destination.getStackInSlot(399).getCount()); + assertEquals("preserved", destination.getStackInSlot(399).getTag().getString("othermod")); + assertEquals(Long.MAX_VALUE, reloaded.rawPayload().getLong("huge")); + reloaded.rawPayload().putLong("huge", 0); + assertEquals(Long.MAX_VALUE, reloaded.rawPayload().getLong("huge")); + } + + @Test void nativeAeLinkSavedDataKeepsBothDimensionsAcrossDiskReload() throws Exception { + var data = new AeConnectLinkSavedData(); + var link = new AeConnectLinkSavedData.Link(Level.NETHER, Level.OVERWORLD, + new BlockPos(1, 64, 2), new BlockPos(1, 64, 2)); + data.add(link); + var storage = storage(); storage.set("native_links", data); storage.save(); + var load = AeConnectLinkSavedData.class.getDeclaredMethod("load", CompoundTag.class); + load.setAccessible(true); + var restored = storage().get(tag -> { + try { return (AeConnectLinkSavedData)load.invoke(null, tag); } + catch (ReflectiveOperationException error) { throw new AssertionError(error); } + }, "native_links"); + assertNotNull(restored); assertEquals(java.util.List.of(link), restored.snapshot()); + assertTrue(restored.machinesOf(Level.OVERWORLD, Level.NETHER, link.accessPoint()).isEmpty()); + assertEquals(java.util.List.of(link.machine()), restored.machinesOf(Level.NETHER, Level.OVERWORLD, link.accessPoint())); + } + + private DimensionDataStorage storage() { + DataFixer fixer = mock(DataFixer.class); + when(fixer.update(any(), any(), anyInt(), anyInt())).thenAnswer(invocation -> invocation.getArgument(1)); + return new DimensionDataStorage(directory.toFile(), fixer); + } + private static HighStackItemStackHandler inventory() { + return new HighStackItemStackHandler(400, 0, 1, 2, 398, 400, 400, + () -> {}, slot -> {}, slot -> {}, slot -> {}); + } +} diff --git a/src/test/java/com/sorrowmist/useless/InventoryTestBootstrap.java b/src/test/java/com/sorrowmist/useless/InventoryTestBootstrap.java new file mode 100644 index 00000000..f417fd98 --- /dev/null +++ b/src/test/java/com/sorrowmist/useless/InventoryTestBootstrap.java @@ -0,0 +1,25 @@ +package com.sorrowmist.useless; + +import net.minecraft.SharedConstants; +import net.minecraft.server.Bootstrap; +import net.minecraftforge.fml.loading.LoadingModList; + +import java.util.List; + +import static org.mockito.Mockito.*; + +/** Installs the launcher context needed by Minecraft's inventory and fluid classes in unit tests. */ +public final class InventoryTestBootstrap { + private InventoryTestBootstrap() { + } + + public static void initialize() { + SharedConstants.tryDetectVersion(); + try (var loading = mockStatic(LoadingModList.class)) { + var mods = mock(LoadingModList.class); + when(mods.getModFiles()).thenReturn(List.of()); + loading.when(LoadingModList::get).thenReturn(mods); + Bootstrap.bootStrap(); + } + } +} diff --git a/src/test/java/com/sorrowmist/useless/compat/neoecoae/NeoEcoPlanningCompatTest.java b/src/test/java/com/sorrowmist/useless/compat/neoecoae/NeoEcoPlanningCompatTest.java new file mode 100644 index 00000000..b5f2ddd5 --- /dev/null +++ b/src/test/java/com/sorrowmist/useless/compat/neoecoae/NeoEcoPlanningCompatTest.java @@ -0,0 +1,132 @@ +package com.sorrowmist.useless.compat.neoecoae; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.networking.crafting.ICraftingPlan; +import appeng.api.networking.crafting.ICraftingProvider; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import appeng.api.stacks.KeyCounter; +import appeng.crafting.CraftingPlan; +import cn.dancingsnow.neoecoae.api.me.planning.ECOPlanningResultRegistry; +import cn.dancingsnow.neoecoae.crafting.planner.result.ECOExecutionRequirement; +import cn.dancingsnow.neoecoae.crafting.planner.result.ECOPlanningResult; +import cn.dancingsnow.neoecoae.crafting.planner.result.PlanningStatus; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.SmartDoublingPlanner; +import com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.SmartDoublingPlans; +import net.minecraftforge.fml.ModList; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Function; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class NeoEcoPlanningCompatTest { + @Test + void onlyTheConfirmedCompleteVectorBypassesRewriting() { + AEKey output = mock(AEKey.class); + IPatternDetails pattern = mock(IPatternDetails.class); + when(pattern.getInputs()).thenReturn(new IPatternDetails.IInput[0]); + when(pattern.getOutputs()).thenReturn(List.of()); + CraftingPlan confirmed = plan(output, pattern, 4); + CraftingPlan differentTasks = plan(output, pattern, 5); + CraftingPlan differentMaterials = plan(output, pattern, 4); + KeyCounter materials = new KeyCounter(); + materials.add(output, 1); + when(differentMaterials.usedItems()).thenReturn(materials); + ECOPlanningResult result = mock(ECOPlanningResult.class); + when(result.plan()).thenReturn(confirmed); + when(result.status()).thenReturn(PlanningStatus.SUCCESS); + when(result.executionRequirement()).thenReturn(ECOExecutionRequirement.ORDERED); + when(result.planningId()).thenReturn(UUID.randomUUID()); + ModList mods = mock(ModList.class); + when(mods.isLoaded("neoecoae")).thenReturn(true); + + try (var modList = mockStatic(ModList.class); var planner = mockStatic(SmartDoublingPlanner.class)) { + modList.when(ModList::get).thenReturn(mods); + ECOPlanningResultRegistry.withSubmissionAlias(confirmed, result, () -> { + assertSame(confirmed, SmartDoublingPlans.rewriteForSubmission(confirmed, ignored -> { + fail("A confirmed ECO plan must not reach smart doubling"); + return List.of(); + })); + planner.verifyNoInteractions(); + assertFalse(NeoEcoPlanningCompat.shouldPreserveSubmissionPlan(differentTasks)); + assertFalse(NeoEcoPlanningCompat.shouldPreserveSubmissionPlan(differentMaterials)); + return null; + }); + assertFalse(NeoEcoPlanningCompat.shouldPreserveSubmissionPlan(confirmed)); + } + } + + @Test + void foreignPlansKeepTheExistingSmartDoublingPathWithEcoPresentOrAbsent() { + AEKey output = mock(AEKey.class); + IPatternDetails original = mock(IPatternDetails.class); + IPatternDetails scaled = mock(IPatternDetails.class); + CraftingPlan source = plan(output, original, 4); + Function> lookup = ignored -> List.of(); + ModList mods = mock(ModList.class); + try (var modList = mockStatic(ModList.class); var planner = mockStatic(SmartDoublingPlanner.class)) { + modList.when(ModList::get).thenReturn(mods); + planner.when(() -> SmartDoublingPlanner.rewrite(source.patternTimes(), lookup)) + .thenReturn(Map.of(scaled, 1L)); + for (boolean installed : new boolean[]{false, true}) { + when(mods.isLoaded("neoecoae")).thenReturn(installed); + ICraftingPlan rewritten = SmartDoublingPlans.rewriteForSubmission(source, lookup); + assertNotSame(source, rewritten); + assertEquals(Map.of(scaled, 1L), rewritten.patternTimes()); + assertEquals(source.finalOutput(), rewritten.finalOutput()); + assertSame(source.usedItems(), rewritten.usedItems()); + } + planner.verify(() -> SmartDoublingPlanner.rewrite(source.patternTimes(), lookup), times(2)); + } + } + + @Test + void missingPlanningApiDoesNotMakeTheOptionalBridgeUnusable() throws Exception { + ModList mods = mock(ModList.class); + try (var modList = mockStatic(ModList.class)) { + modList.when(ModList::get).thenReturn(mods); + // Load the bridge in isolation while completely denying ECO classes. + ClassLoader isolated = new ClassLoader(getClass().getClassLoader()) { + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (name.startsWith("cn.dancingsnow.neoecoae.")) { + throw new ClassNotFoundException(name); + } + if (name.startsWith(NeoEcoPlanningCompat.class.getName())) { + Class loaded = findLoadedClass(name); + if (loaded != null) return loaded; + try (var input = getParent().getResourceAsStream(name.replace('.', '/') + ".class")) { + if (input == null) throw new ClassNotFoundException(name); + byte[] bytes = input.readAllBytes(); + return defineClass(name, bytes, 0, bytes.length); + } catch (IOException failure) { + throw new ClassNotFoundException(name, failure); + } + } + return super.loadClass(name, resolve); + } + }; + var method = isolated.loadClass(NeoEcoPlanningCompat.class.getName()) + .getMethod("shouldPreserveSubmissionPlan", ICraftingPlan.class); + assertEquals(false, method.invoke(null, mock(ICraftingPlan.class))); + when(mods.isLoaded("neoecoae")).thenReturn(true); + assertEquals(false, method.invoke(null, mock(ICraftingPlan.class))); + } + } + + private static CraftingPlan plan(AEKey output, IPatternDetails pattern, long tasks) { + CraftingPlan plan = mock(CraftingPlan.class); + when(plan.finalOutput()).thenReturn(new GenericStack(output, 1)); + when(plan.patternTimes()).thenReturn(Map.of(pattern, tasks)); + when(plan.usedItems()).thenReturn(new KeyCounter()); + when(plan.emittedItems()).thenReturn(new KeyCounter()); + when(plan.missingItems()).thenReturn(new KeyCounter()); + return plan; + } +} diff --git a/src/test/java/com/sorrowmist/useless/compat/neoecoae/compact/entity/CompactF9FluidHandlerTest.java b/src/test/java/com/sorrowmist/useless/compat/neoecoae/compact/entity/CompactF9FluidHandlerTest.java new file mode 100644 index 00000000..425138a9 --- /dev/null +++ b/src/test/java/com/sorrowmist/useless/compat/neoecoae/compact/entity/CompactF9FluidHandlerTest.java @@ -0,0 +1,71 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.entity; + +import com.sorrowmist.useless.InventoryTestBootstrap; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.capability.IFluidHandler.FluidAction; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.*; + +class CompactF9FluidHandlerTest { + @BeforeAll + static void bootstrap() { + InventoryTestBootstrap.initialize(); + } + + @Test + void eightHostCapacityAndSimulationDoNotDirtyOrConsumeFluid() { + var changes = new AtomicInteger(); + var input = CompactF9FluidHandler.createFleetTank(8, changes::incrementAndGet); + var output = CompactF9FluidHandler.createFleetTank(8, changes::incrementAndGet); + var handler = new CompactF9FluidHandler(input, output); + FluidStack water = new FluidStack(Fluids.WATER, 200_000); + assertEquals(128_000, handler.getTankCapacity(0)); + assertEquals(128_000, handler.getTankCapacity(1)); + assertEquals(128_000, handler.fill(water, FluidAction.SIMULATE)); + assertEquals(0, input.getFluidAmount()); + assertEquals(0, changes.get()); + assertEquals(128_000, handler.fill(water, FluidAction.EXECUTE)); + assertEquals(128_000, input.getFluidAmount()); + assertEquals(1, changes.get()); + assertTrue(handler.drain(1000, FluidAction.EXECUTE).isEmpty()); + assertEquals(128_000, input.getFluidAmount()); + } + + @Test + void outputExtractionSharesThePoolUsedByCoolingAndMarksActualChanges() { + var changes = new AtomicInteger(); + var input = CompactF9FluidHandler.createFleetTank(8, changes::incrementAndGet); + var output = CompactF9FluidHandler.createFleetTank(8, changes::incrementAndGet); + var handler = new CompactF9FluidHandler(input, output); + output.fill(new FluidStack(Fluids.WATER, 64_000), FluidAction.EXECUTE); + int before = changes.get(); + assertEquals(32_000, handler.drain(32_000, FluidAction.SIMULATE).getAmount()); + assertEquals(64_000, output.getFluidAmount()); + assertEquals(before, changes.get()); + assertEquals(32_000, handler.drain(new FluidStack(Fluids.WATER, 32_000), FluidAction.EXECUTE).getAmount()); + assertEquals(32_000, output.getFluidAmount()); + assertEquals(before + 1, changes.get()); + assertEquals(0, input.getFluidAmount()); + } + + @Test + void playerRecoveryViewCanDrainInputWithoutChangingPipeOutputSemantics() { + var changes = new AtomicInteger(); + var input = CompactF9FluidHandler.createFleetTank(8, changes::incrementAndGet); + var output = CompactF9FluidHandler.createFleetTank(8, changes::incrementAndGet); + var handler = new CompactF9FluidHandler(input, output); + input.fill(new FluidStack(Fluids.WATER, 4_000), FluidAction.EXECUTE); + + assertEquals(4_000, handler.inputRecoveryHandler().drain(4_000, FluidAction.SIMULATE).getAmount()); + assertEquals(4_000, input.getFluidAmount()); + assertEquals(4_000, handler.inputRecoveryHandler().drain(4_000, FluidAction.EXECUTE).getAmount()); + assertEquals(0, input.getFluidAmount()); + assertEquals(2, changes.get()); + assertTrue(handler.drain(1_000, FluidAction.SIMULATE).isEmpty()); + } +} diff --git a/src/test/java/com/sorrowmist/useless/compat/neoecoae/compact/exposed/ExposedPatternBusBlockEntityTest.java b/src/test/java/com/sorrowmist/useless/compat/neoecoae/compact/exposed/ExposedPatternBusBlockEntityTest.java new file mode 100644 index 00000000..5828c7b0 --- /dev/null +++ b/src/test/java/com/sorrowmist/useless/compat/neoecoae/compact/exposed/ExposedPatternBusBlockEntityTest.java @@ -0,0 +1,91 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.exposed; + +import appeng.api.networking.GridHelper; +import appeng.api.networking.IGrid; +import appeng.api.networking.IGridNode; +import appeng.api.networking.IManagedGridNode; +import appeng.api.networking.crafting.ICraftingProvider; +import cn.dancingsnow.neoecoae.config.NEConfig; +import cn.dancingsnow.neoecoae.blocks.entity.ECOMachineInterfaceBlockEntity; +import cn.dancingsnow.neoecoae.blocks.entity.crafting.ECOCraftingPatternBusBlockEntity; +import com.sorrowmist.useless.InventoryTestBootstrap; +import com.sorrowmist.useless.compat.neoecoae.compact.cluster.CompactCraftingCluster; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.BlockEntityType; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class ExposedPatternBusBlockEntityTest { + @BeforeAll + static void bootstrap() { + InventoryTestBootstrap.initialize(); + } + + @Test + void domainMembershipKeepsDispatchOnTheHost() throws ReflectiveOperationException { + var bus = createBus(mock(IManagedGridNode.class, RETURNS_SELF)); + var owner = new CompactCraftingCluster(BlockPos.ZERO, BlockPos.ZERO); + bus.joinPatternDomain(owner); + + assertSame(owner, bus.getCluster()); + assertTrue(owner.getPatternBuses().contains(bus)); + try { + var domainBuses = owner.getClass().getMethod("getPatternDomainBuses"); + assertTrue(((List) domainBuses.invoke(owner)).contains(bus)); + } catch (NoSuchMethodException olderEco) { + // The declared ECO dependency predates domain filtering; also validate with newer ECO. + } + assertTrue(bus.isBusy()); + assertTrue(bus.getAvailablePatterns().isEmpty()); + } + + @Test + void shiftingChildOffsetsInvalidatesTheWholeInterfaceEvenAtTheSameTotalSize() { + var node = mock(IManagedGridNode.class, RETURNS_SELF); + var gridNode = mock(IGridNode.class); + var grid = mock(IGrid.class); + when(node.getNode()).thenReturn(gridNode); + when(node.getGrid()).thenReturn(grid); + when(gridNode.getGrid()).thenReturn(grid); + var machineInterface = mock(ECOMachineInterfaceBlockEntity.class); + when(grid.getActiveMachines(ECOMachineInterfaceBlockEntity.class)).thenReturn(java.util.Set.of(machineInterface)); + var bus = createBus(node); + var first = mock(ECOCraftingPatternBusBlockEntity.class); + var second = mock(ECOCraftingPatternBusBlockEntity.class); + when(first.getPatternSlotCount()).thenReturn(2); + when(second.getPatternSlotCount()).thenReturn(3); + bus.bind(List.of(first, second), node); + assertEquals(5, bus.getPatternSlotCount()); + + when(first.getPatternSlotCount()).thenReturn(3); + when(second.getPatternSlotCount()).thenReturn(2); + try (var providers = mockStatic(ICraftingProvider.class)) { + bus.tick(); + assertEquals(5, bus.getPatternSlotCount()); + verify(machineInterface).onPatternBusInventoryChanged(bus); + verify(machineInterface, never()).onPatternBusInventoryChanged(eq(bus), any(int[].class)); + providers.verify(() -> ICraftingProvider.requestUpdate(node)); + clearInvocations(machineInterface); + bus.tick(); + verifyNoInteractions(machineInterface); + } + } + + private static ExposedPatternBusBlockEntity createBus(IManagedGridNode node) { + try (var gridHelper = mockStatic(GridHelper.class); var config = mockStatic(NEConfig.class)) { + gridHelper.when(() -> GridHelper.createManagedNode(any(), any())).thenReturn(node); + config.when(NEConfig::getMaxCraftingPatternBusSlotCount).thenReturn(63); + config.when(NEConfig::getCraftingPatternBusPages).thenReturn(1); + var type = mock(BlockEntityType.class); + when(type.isValid(any())).thenReturn(true); + return new ExposedPatternBusBlockEntity(type, BlockPos.ZERO, + Blocks.AIR.defaultBlockState()); + } + } +} diff --git a/src/test/java/com/sorrowmist/useless/compat/neoecoae/compact/provider/CombinedPatternInventoryTest.java b/src/test/java/com/sorrowmist/useless/compat/neoecoae/compact/provider/CombinedPatternInventoryTest.java new file mode 100644 index 00000000..d5063b29 --- /dev/null +++ b/src/test/java/com/sorrowmist/useless/compat/neoecoae/compact/provider/CombinedPatternInventoryTest.java @@ -0,0 +1,83 @@ +package com.sorrowmist.useless.compat.neoecoae.compact.provider; + +import appeng.api.inventories.InternalInventory; +import appeng.util.inv.AppEngInternalInventory; +import cn.dancingsnow.neoecoae.blocks.entity.crafting.ECOCraftingPatternBusBlockEntity; +import com.sorrowmist.useless.InventoryTestBootstrap; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class CombinedPatternInventoryTest { + @BeforeAll + static void bootstrap() { + InventoryTestBootstrap.initialize(); + } + + @Test + void managementReadsPhysicalSlotsEvenWhenTheTerminalHidesThem() { + var bus = mock(ECOCraftingPatternBusBlockEntity.class); + var raw = new AppEngInternalInventory(null, 2, 1); + ItemStack disk = new ItemStack(Items.PAPER); + raw.setItemDirect(0, disk); + var terminal = mock(InternalInventory.class); + when(terminal.getStackInSlot(0)).thenReturn(ItemStack.EMPTY); + when(bus.getPatternSlotCount()).thenReturn(2); + when(bus.getPatternSlotInventory()).thenReturn(raw); + when(bus.getTerminalPatternInventory()).thenReturn(terminal); + var combined = new CombinedPatternInventory(); + combined.bind(List.of(bus)); + + assertSame(disk, combined.getStackInSlot(0)); + assertEquals(1, combined.getSlotLimit(0)); + combined.setItemDirect(1, disk); + verify(bus).setPatternDirect(1, disk); + verifyNoInteractions(terminal); + } + + @Test + void terminalIncludesAppendedRowsAndDelegatesTheirTakeAndInsertProtocol() { + var first = mock(ECOCraftingPatternBusBlockEntity.class); + var second = mock(ECOCraftingPatternBusBlockEntity.class); + var firstView = mock(InternalInventory.class); + var secondView = mock(InternalInventory.class); + var guardedSlot = mock(InternalInventory.class); + when(first.getPatternSlotCount()).thenReturn(2); + when(second.getPatternSlotCount()).thenReturn(2); + when(first.getTerminalPatternInventory()).thenReturn(firstView); + when(second.getTerminalPatternInventory()).thenReturn(secondView); + when(firstView.size()).thenReturn(3); // Two physical slots plus one disk recipe. + when(secondView.size()).thenReturn(2); + when(firstView.getSlotInv(2)).thenReturn(guardedSlot); + when(firstView.getSlotInv(0)).thenReturn(InternalInventory.empty()); // Hidden disk. + var combined = new CombinedPatternInventory(); + combined.bind(List.of(first, second)); + InternalInventory terminal = combined.createTerminalView(); + ItemStack pattern = new ItemStack(Items.PAPER); + when(firstView.extractItem(2, 1, false)).thenReturn(pattern); + when(firstView.insertItem(2, pattern, false)).thenReturn(pattern); // Guard refuses insertion. + + assertEquals(5, terminal.size()); + assertSame(guardedSlot, terminal.getSlotInv(2)); + assertEquals(0, terminal.getSlotInv(0).size()); + assertSame(pattern, terminal.extractItem(2, 1, false)); + assertSame(pattern, terminal.insertItem(2, pattern, false)); + terminal.setItemDirect(3, pattern); + verify(secondView).setItemDirect(0, pattern); + verify(first, never()).setPatternDirect(anyInt(), any()); + verify(second, never()).setPatternDirect(anyInt(), any()); + + // A real bus gaining pages must not shift the open menu's already published slot IDs. + when(first.getPatternSlotCount()).thenReturn(4); + assertEquals(6, combined.size()); + assertEquals(5, terminal.size()); + terminal.setItemDirect(3, ItemStack.EMPTY); + verify(secondView).setItemDirect(0, ItemStack.EMPTY); + } +} diff --git a/src/test/python/test_ae_crafting_contract.py b/src/test/python/test_ae_crafting_contract.py new file mode 100644 index 00000000..9fcc8043 --- /dev/null +++ b/src/test/python/test_ae_crafting_contract.py @@ -0,0 +1,104 @@ +"""Regression guards for lazy AE2/AdvancedAE mixins against pinned native jars. + +Run after building the main jar. Does not start Minecraft or edit the pack. +""" +from pathlib import Path +import io +import json +import re +import struct +import unittest +import zipfile + +ROOT = Path(__file__).resolve().parents[3] +INPUTS = ROOT / 'build/contract-inputs.json' + + +def class_contract(data): + stream = io.BytesIO(data) + def read(fmt): + return struct.unpack('>' + fmt, stream.read(struct.calcsize('>' + fmt)))[0] + assert read('I') == 0xCAFEBABE + read('H'); read('H') + pool = [None] * read('H') + index = 1 + sizes = {3:4, 4:4, 5:8, 6:8, 7:2, 8:2, 9:4, 10:4, 11:4, + 12:4, 15:3, 16:2, 17:4, 18:4, 19:2, 20:2} + while index < len(pool): + tag = read('B') + if tag == 1: + pool[index] = stream.read(read('H')).decode('utf-8', errors='replace') + else: + stream.read(sizes[tag]) + index += 2 if tag in (5, 6) else 1 + read('H'); read('H'); read('H') + stream.read(2 * read('H')) + def members(): + result = {} + for _ in range(read('H')): + read('H'); name = pool[read('H')]; desc = pool[read('H')] + result[name] = result.get(name, []) + [desc] + for _ in range(read('H')): + read('H'); stream.read(read('I')) + return result + members() + return members(), [value for value in pool if isinstance(value, str)] + + +def load(jar, name): + with zipfile.ZipFile(jar) as archive: + return class_contract(archive.read(name.replace('.', '/') + '.class')) + + +class AeCraftingMixinContractTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + version = re.search(r'(?m)^mod_version=(.+)$', (ROOT / 'gradle.properties').read_text(encoding='utf-8'))[1].strip() + cls.jar = ROOT / 'build/libs' / ('useless_mod-' + version + '.jar') + cls.native = {key: Path(value) for key, value in json.loads(INPUTS.read_text(encoding='utf-8')).items()} + + def check_cpu(self, family, target): + handlers, strings = load(self.jar, 'com.sorrowmist.useless.mixin.' + family + '.CraftingCpuLogicMixin') + methods, _ = load(self.native[family], target) + for hook, native in [('uselessMod$writeDynamicState', 'writeToNBT'), + ('uselessMod$readDynamicState', 'readFromNBT')]: + with self.subTest(family=family, hook=hook): + self.assertIn('(Lnet/minecraft/nbt/CompoundTag;)V', methods[native]) + descriptor = handlers[hook][0].replace( + 'Lorg/spongepowered/asm/mixin/injection/callback/CallbackInfo;', '') + self.assertIn(descriptor, methods[native]) + self.assertIn(native + '(Lnet/minecraft/nbt/CompoundTag;)V', strings) + + def test_ae2_cpu_native_nbt(self): + self.check_cpu('ae2', 'appeng.crafting.execution.CraftingCpuLogic') + + def test_advancedae_cpu_native_nbt(self): + self.check_cpu('advancedae', 'net.pedroksl.advanced_ae.common.logic.AdvCraftingCPULogic') + + def check_restore(self, family, target): + _, strings = load(self.jar, 'com.sorrowmist.useless.mixin.' + family + '.ExecutingCraftingJobMixin') + native, _ = load(self.native[family], target) + descriptors = [s[len(''):] for s in strings if s.startswith('(')] + self.assertEqual(len(descriptors), 1) + self.assertIn(descriptors[0], native['']) + + def test_ae2_restore_constructor(self): + self.check_restore('ae2', 'appeng.crafting.execution.ExecutingCraftingJob') + + def test_advancedae_restore_constructor(self): + self.check_restore('advancedae', 'net.pedroksl.advanced_ae.common.logic.ExecutingCraftingJob') + + def test_no_mixin_synthetic_args_dependency(self): + _, strings = load(self.jar, 'com.sorrowmist.useless.mixin.ae2.CraftingTreeProcessMixin') + self.assertFalse(any('ModifyArgs' in s or 'injection/invoke/arg/Args' in s for s in strings)) + self.assertIn('Lcom/llamalad7/mixinextras/injector/wrapoperation/WrapOperation;', strings) + self.assertIn('appeng/crafting/CraftingTreeNode', strings) + + def test_registry_free_state_storage(self): + methods, _ = load(self.jar, 'com.sorrowmist.useless.content.machines.advanced_alloy_furnace.ae.DynamicPatternCpuStateManager') + self.assertIn('(Ljava/lang/Object;)Lnet/minecraft/nbt/CompoundTag;', methods['writeToTag']) + self.assertIn('(Ljava/lang/Object;Ljava/util/UUID;Lnet/minecraft/nbt/CompoundTag;)V', methods['readFromTag']) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/src/test/python/test_outline_contract.py b/src/test/python/test_outline_contract.py new file mode 100644 index 00000000..e1f01331 --- /dev/null +++ b/src/test/python/test_outline_contract.py @@ -0,0 +1,46 @@ +"""Guard the Forge 1.20.1 world-outline transform, including the same bug's sibling renderers.""" +from pathlib import Path +import re +import unittest +from test_ae_crafting_contract import load + +ROOT = Path(__file__).resolve().parents[3] +RENDER = ROOT / 'src/main/java/com/sorrowmist/useless/client/render' +RENDERERS = ['AeLinkHighlightRenderer', 'StaffLinkHighlightRenderer', + 'ChainMiningHighlightRenderer', 'AlloyFurnaceErrorHighlightRenderer', + 'ConstructionWandPreviewRenderer'] + + +class OutlineContractTest(unittest.TestCase): + def test_current_built_jar_uses_world_stage_for_all_five_renderers(self): + version = re.search(r'(?m)^mod_version=(.+)$', (ROOT / 'gradle.properties').read_text(encoding='utf-8'))[1].strip() + jar = ROOT / 'build/libs' / ('useless_mod-' + version + '.jar') + for name in RENDERERS: + with self.subTest(renderer=name): + _, strings = load(jar, 'com.sorrowmist.useless.client.render.' + name) + self.assertIn('AFTER_PARTICLES', strings) + self.assertNotIn('AFTER_LEVEL', strings) + self.assertNotIn('org/joml/Quaternionf', strings) + + def test_world_view_stage_without_duplicate_rotation_or_projection(self): + for name in RENDERERS: + with self.subTest(renderer=name): + source = (RENDER / (name + '.java')).read_text(encoding='utf-8') + source = re.sub(r'/\*.*?\*/|//[^\n]*', '', source, flags=re.S) + self.assertIn('RenderLevelStageEvent.Stage.AFTER_PARTICLES', source) + self.assertNotIn('RenderLevelStageEvent.Stage.AFTER_LEVEL', source) + self.assertNotIn('Quaternionf', source) + self.assertNotIn('getProjectionMatrix', source) + self.assertNotIn('.mulPose(', source) + self.assertIn('move(-cameraPos.x, -cameraPos.y, -cameraPos.z)', source) + + def test_ae_and_logistics_do_not_draw_late_responses_when_tool_is_disabled(self): + for name in RENDERERS[:2]: + with self.subTest(renderer=name): + source = (RENDER / (name + '.java')).read_text(encoding='utf-8') + render = source[source.index('public static void onRenderStage'):] + self.assertIn('!shouldRequest(minecraft)', render.split('Camera camera')[0]) + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/resources/assets/minecraft/lang/en_us.json b/src/test/resources/assets/minecraft/lang/en_us.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/src/test/resources/assets/minecraft/lang/en_us.json @@ -0,0 +1 @@ +{} diff --git a/wiki/ALLOY_FURNACE_BIGINT_API.md b/wiki/ALLOY_FURNACE_BIGINT_API.md new file mode 100644 index 00000000..90af65ab --- /dev/null +++ b/wiki/ALLOY_FURNACE_BIGINT_API.md @@ -0,0 +1,447 @@ +# Multiblock Alloy Furnace BigInteger Dispatch API Guide + +

+ English | + 简体中文 +

+ +This document is for addon mods that need to dispatch crafting batches **larger than `long`** to the +multiblock Omniversal Alloy Furnace. These APIs replace reflective access to internal classes such as +`MultiblockAlloyFurnaceCoreBlockEntity` and `AdvancedAlloyFurnaceAeManager`. + +This document corresponds to the current source of the NeoForge 1.21.1 branch. + +## Overview + +AE2's native `ICraftingProvider#pushPattern` can only express amounts as `long`; a single physical +submission carries at most `Long.MAX / amountPerCraft` crafts. There are only two ways to dispatch +genuinely large numbers (say `1e22` items): + +1. **The long "counted" path** — dispatch one `long` window per tick and accumulate to a bigint total + across many dispatches. This is the interface-forced fallback: slow, and it consumes scheduling. +2. **The native bigint path** (this API) — a single dispatch can deliver a batch **larger than `long`**. + +This API turns option 2 into Useless Mod's own public contract, so you do **not** need to depend on +any third-party dispatch framework, and you do **not** need to write mixins into AE2's crafting CPU logic. + +> **Scope**: only the **multiblock Omniversal Alloy Furnace** (ME pattern assembly + multiblock core). +> The single-block advanced alloy furnace does not support bigint batches and will not appear in +> `findTargets`. + +## Table of contents + +- [Package layout](#package-layout) +- [General constraints](#general-constraints) +- [1. Discovering machines](#1-discovering-machines) +- [2. Querying capacity](#2-querying-capacity) +- [3. Admission and commit](#3-admission-and-commit) +- [4. Receiving output callbacks (CPU side)](#4-receiving-output-callbacks-cpu-side) +- [5. Complete example](#5-complete-example) +- [FAQ](#faq) + +## Package layout + +| Package | Purpose | +| --- | --- | +| `com.sorrowmist.useless.api.crafting.bigint` | Provider-side contract: discovery, capacity, admission, commit | +| `com.sorrowmist.useless.api.crafting.bigint.cpu` | CPU-side SPI: register an adapter, receive output and terminal callbacks | +| `com.sorrowmist.useless.api.crafting.bigint.example` | Reference implementation (not active; copy from it) | + +| Type | Purpose | +| --- | --- | +| `AlloyFurnaceBigIntegerApi` | Discovery entry point | +| `AlloyFurnaceBigIntegerTarget` | One machine: query capacity / request a batch | +| `AlloyFurnaceBigIntegerCapacity` | Capacity query result | +| `AlloyFurnaceBigIntegerBatch` | One-shot admission ticket | +| `AlloyFurnaceBigIntegerOutput` | A BigInteger-scale output (key + exact amount) | +| `AlloyFurnaceBigIntegerProvider` | Provider-side marker interface (use it for `instanceof` checks) | +| `AlloyFurnaceBigIntegerCpuAdapter` | The callback interface you implement | +| `AlloyFurnaceBigIntegerCpuAdapters` | Explicit registry | +| `AlloyFurnaceBigIntegerCpuBinding` | Binds your adapter id and context to a batch | +| `AlloyFurnaceBigIntegerBatchContext` | Batch context carried by every callback | +| `AlloyFurnaceBigIntegerBatchResult` | Terminal batch state | + +## General constraints + +1. **Server thread only.** This applies to every method and every callback. +2. **Do not cache `AlloyFurnaceBigIntegerTarget`.** It is owned by the machine's block entity; caching it + across ticks turns into a dangling reference once the machine is broken or unloaded. Just call + `findTargets` again each tick — it is only a grid node walk, very cheap. +3. **Do not reflect on or cast to this mod's block entity classes.** To test "is this a bigint + provider", use `instanceof AlloyFurnaceBigIntegerProvider`. +4. **The prototype is the amount for a *single* push.** The remaining `count - 1` copies of material are + withdrawn by **your own ledger**, and you must roll that back if the commit fails. + See [section 3](#3-admission-and-commit). +5. **Never multiply `count` into the prototype.** The count travels only through the `count` argument. +6. **Do not throw from callbacks**, and do not call `admit`/`commit` from them (re-entrancy). Thrown + exceptions are logged and swallowed. + +## 1. Discovering machines + +```java +import com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerApi; +import com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerTarget; + +IGrid grid = /* the grid your CPU is on */; +for (AlloyFurnaceBigIntegerTarget target : AlloyFurnaceBigIntegerApi.findTargets(grid)) { + String machine = target.machineIdentity(); // e.g. minecraft:overworld@128,64,-256 + // ... +} +``` + +- `findTargets(IGrid)` returns every target **currently available** on the grid, de-duplicated by + `machineIdentity()` (one physical machine appears only once). +- `findTarget(IGrid, String machineIdentity)` looks up a machine by identity; useful for + "which machine did I use last time". +- `acceptedPatternKinds()` returns the set of pattern item ids the machine accepts, for a cheap filter + before expensive decoding. + +> **Implementation note**: AE2 19.2.17's `ICraftingService` has no public "enumerate all providers" API +> (only `getCraftingFor(AEKey)` and `getCpus()`), so this API walks `IGrid#getNodes()` + +> `IGridNode#getOwner()`. + +## 2. Querying capacity + +```java +AlloyFurnaceBigIntegerCapacity capacity = + target.capacity(pattern, unitPrototype, requestedCount); + +if (!capacity.isAvailable()) { + String reason = capacity.statusKey(); // a translation key you can show to players; may be empty + return; +} +BigInteger accepted = capacity.accepted(); // may be less than requestedCount, or far beyond long +``` + +Capacity is the minimum of up to four gates: + +| Gate | Meaning | +| --- | --- | +| Recipe availability | Whether the coil tier is high enough and required molds are present (**omniversal patterns only**) | +| Material window | Thread count N ⇒ this batch absorbs at most N × (`Long.MAX` of every material). **This is a balance gate and can be lifted via the machine's `ae_unlimited_bigint_parallelism` config** (once lifted, batch size is decided solely by delivery capacity) | +| Output delivery capacity | A single batch's size is decided by the machine from its **current delivery capacity** and floats over time (see below) | +| **Energy** | `count × per-craft energy` must be affordable (**omniversal patterns only**) | + +**AE2 crafting patterns only have the "material window + delivery capacity" gates**: they are folded +after a single assembly on the virtual 3×3 grid, with no bound recipe and no energy cost, so their +capacity is usually far larger than an omniversal pattern's. + +### Dynamic throttling: `isThrottled()` + +Every bigint path in this mod uses a dynamic "time budget per tick" throttle (modelled on Data +Energistics' submission budget): when measured cost exceeds the budget, subsequent batches are narrowed +proportionally. The granularity is **global** — all machines run on the same server thread, so cost +from multiple machines accumulates and pushes the throttle down together. + +```java +if (target.isThrottled() && !urgent) { + return; // the machine is busy; defer non-urgent batches +} +``` + +Its purpose is to let you **distinguish** two kinds of "small capacity": + +| Situation | Meaning | +| --- | --- | +| Small capacity + `isThrottled() == false` | This is simply the machine's capability (material window / energy / segmentation / configured threads) | +| Small capacity + `isThrottled() == true` | The machine (or another machine on the same server thread) is busy | + +**Throttling never drives capacity to 0**: reporting "no capacity" triggers a +"no capacity → resubmit" spin loop on the scheduler side, so throttling only narrows the batch size. +Whether to stop is your call. + +> **Output return is subject to the same dynamic throttle, but with a different policy.** Admission +> throttling narrows the **batch size** (accept less new work); return throttling narrows the +> **time willing to be spent per tick** (deliver fewer already-finished outputs). Their floors also +> differ: admission **must** accept at least 1 craft (otherwise the spin loop above), while the return +> path may truncate to very little (it is idempotent; the remainder stays queued for the next tick) — +> but it still keeps a floor, so that sustained load cannot starve the queue and let backpressure push +> admission to 0. The visible consequence for you: **the busier the server, the later +> `onBatchOutputs` arrives** — but it always arrives. + +**`capacity` has no side effects** and may be called every tick. + +### A single batch's size floats with delivery capacity + +The machine sizes a single batch according to its **current delivery capacity** — outputs must be +written back to the ME network chunk by chunk, so how much can be written per tick is limited. + +**What this means for you**: + +- during extremely large crafts `capacity` floats within a range — this is **normal**; do **not** treat + it as a fixed measure of machine capability; +- `capacity` **never drops to zero because of backlog** (only hard conditions — invalid recipe, coil tier + too low, missing molds, no energy — can make it zero), so your scheduling loop does not need to + "pause a tick and wait for the queue to drain before restarting"; +- to get larger batches, raise the machine's **output-return time budget** in its config rather than + retrying repeatedly. Raising it also raises the per-tick write-back cost. + +## 3. Admission and commit + +```java +AlloyFurnaceBigIntegerBatch batch = + target.admit(pattern, unitPrototype, capacity.accepted(), binding); +if (batch == null) { + return; // not acceptable right now +} +if (batch.commit(unitPrototype)) { + // success: input ownership has transferred to the machine +} else { + // failure: the machine consumed nothing. Roll back your own (count - 1) withdrawal, then try another machine. +} +``` + +### Call contract (read this fully) + +1. **Commit only once.** A second `commit` call throws `IllegalStateException`. +2. **You must commit with the prototype you prepared**: the very same `KeyCounter[]` array object you + passed to `admit` — no copying, no rebuilding. Committing with another array throws + `IllegalArgumentException`. +3. **The machine consumes only the one prototype it holds.** The prototype passed to `admit` is the + material for a **single push**, not `count` copies. The remaining `count - 1` copies are withdrawn by + **your own BigInteger ledger**, and roll it back yourself when our commit fails. How you keep that + ledger is entirely up to you. +4. **A failed commit does not lose material**: when `commit` returns `false` the machine does not clear + the prototype, so you should roll back your own `count - 1` withdrawal and retry later or try a + different provider. +5. **A `true` commit transfers ownership**: from then on the material in the prototype belongs to the + machine (it clears it), and outputs are delivered back to the network by the machine as + `AlloyFurnaceBigIntegerOutput` / reported to registered adapters. +6. `admit` **consumes no material and charges no energy**; it only performs a snapshot check. The real + checks run again at `commit` (machine state may change in between — the `commit` return value is + authoritative). + +## 4. Receiving output callbacks (CPU side) + +Outputs have **two** delivery paths. By default the first one is used; if you implement an adapter and +are willing to take ownership, you can use the second. + +**1. Default — chunked write-back.** AE2's storage interface accepts at most `long` per call +(`IMEInventory#insert(AEKey, long, Actionable)`), so the machine splits outputs into `Long.MAX_VALUE` +chunks and writes them back to the ME network **across ticks**. You do not need to do anything — +outputs land in the ME network as usual. + +**2. Bulk — `claimOutputs` (optional, skips the chunked write-back).** See section 4.2. + +### 4.1 Implement the adapter + +```java +public final class MyCpuAdapter implements AlloyFurnaceBigIntegerCpuAdapter { + public static final ResourceLocation ID = + ResourceLocation.fromNamespaceAndPath("mymod", "bigint_cpu"); + + @Override public ResourceLocation id() { return ID; } + + @Override + public void onBatchAdmitted(AlloyFurnaceBigIntegerBatchContext context) { + // Ownership has transferred. Record the planned outputs in your ledger / park the job as waiting. + } + + @Override + public void onBatchOutputs(AlloyFurnaceBigIntegerBatchContext context, + List actualOutputs) { + // All outputs have returned to the network. actualOutputs are exact BigInteger values -- + // for dynamic outputs, these keys are the real ones. + } + + @Override + public void onBatchFinished(AlloyFurnaceBigIntegerBatchContext context, + AlloyFurnaceBigIntegerBatchResult result) { + // SUCCESS / CANCELLED / REJECTED + } +} +``` + +### 4.2 Claiming outputs in bulk (optional) + +If your CPU is willing to **take a whole batch directly** (for example because you already keep a +BigInteger ledger and do not need the items to pass through the ME network first), implement this +callback and the machine will skip the chunked write-back: + +```java +@Override +public @NotNull Map claimOutputs( + @NotNull AlloyFurnaceBigIntegerBatchContext context, + @NotNull List outputs) { + // `outputs` is what the machine is about to write back to the network. + // Return how much you take, keyed by AEKey; the machine deducts it from its pending ledger and + // writes only the remainder back to the network. + Map taken = new HashMap<>(); + for (AlloyFurnaceBigIntegerOutput output : outputs) { + BigInteger accept = decideHowMuchToTake(output); // your policy; partial is fine + if (accept.signum() > 0) { + taken.put(output.what(), accept); + } + } + return taken; +} +``` + +**When it is called**: **before** outputs are written to the network, once per key. The default +implementation returns an empty map, so behaviour is **identical** to not implementing it. + +**Applies to both pattern kinds**: omniversal patterns and AE2 crafting patterns share the same output +queue and the same delivery path, so both trigger this callback as long as the batch was admitted with +an adapter binding. The `key` you receive is the **actual key** (an omniversal pattern's dynamic +outputs only resolve to their real keys when the batch is folded), so just book against it. + +> **⚠️ Taking is taking ownership.** Items you accept do **not** enter the ME network — they exist only +> in your own ledger. Therefore: +> - you are responsible for what happens to them next (return them to the network yourself, or consume +> them as intermediates); +> - **the machine cannot refund them** — the ledger is deducted when the call returns. If the batch is +> later cancelled (`onBatchFinished` receives `CANCELLED`), anything you already took is yours to handle; +> - the implementation **must not throw**; if it does, the machine logs it and falls back to the chunked +> write-back (no interruption, but ownership may become inconsistent). + +**Trigger conditions**: only when the batch was bound to an adapter via `admit(..., cpuBinding)` **and** +every entry in the write-back pass is bound to the same adapter. With several CPUs mixed on one machine +it is not called (falling back to the chunked write-back). + +### 4.3 Registering + +Register once during your mod's common init: + +```java +@SubscribeEvent +public static void onCommonSetup(FMLCommonSetupEvent event) { + event.enqueueWork(() -> AlloyFurnaceBigIntegerCpuAdapters.register(new MyCpuAdapter())); +} +``` + +The registry is global and process-wide; it is not cleared by save or world reloads. If your mod can be +disabled at runtime, remove it with `AlloyFurnaceBigIntegerCpuAdapters.unregister(id)`. + +### 4.4 Callback ordering + +| When | Callback | +| --- | --- | +| `commit` succeeds, same tick | `onBatchAdmitted` | +| After **all** outputs have returned to the network | `onBatchOutputs`, immediately followed by `onBatchFinished(SUCCESS)` | +| Machine broken / player cancelled / job aborted | only `onBatchFinished(CANCELLED)` | + +**A batch receives exactly one terminal callback** (`SUCCESS` or `CANCELLED`, never both). When +cancelled, even if the outputs happen to be force-flushed within the same tick, no `SUCCESS` follows. + +> **Large batches return across multiple ticks.** Outputs are aggregated per key and written back in +> `Long.MAX_VALUE` chunks, and a single machine has a per-tick cap on return work (time budget + per-key +> chunk cap). A `1e22`-scale batch may therefore take a few seconds to fully land, and +> `onBatchOutputs` / `onBatchFinished(SUCCESS)` arrive correspondingly later. **This is deliberate** — +> it prevents one enormous write-back from stalling the server thread. If your job is latency-sensitive, +> design for "the callback arrives late but always arrives" and do not set an aggressive timeout after +> dispatching a batch. + +**Cancellation does not lose outputs**: the machine force-writes the remaining queued outputs back to +the ME network, just without the per-tick pacing. On `CANCELLED`, treat it as "the machine delivered +this batch, but I did not get per-item receipts" and do **not** re-record the outputs. + +### 4.5 Save reload + +`cpuToken` is not serializable and is `null` after a reload; the machine's output queue itself is +persisted as usual (**no items are lost**), but **callbacks are not re-sent**. Use the ME network's +actual balance as the source of truth, or reconcile by `batchId()`. + +### 4.6 What happens without an adapter + +There is no degradation risk at all: if you register no adapter (or pass `cpu = null` to `admit`), +outputs are still chunked back into the ME network as usual — the callbacks are simply not sent and `claimOutputs` is never called. + +## 5. Complete example + +`com.sorrowmist.useless.api.crafting.bigint.example.ExampleBigIntegerCpuAdapter` is a reference +implementation you can copy, covering the whole discovery → capacity → admission → commit → output +callback flow. Its dispatch entry point, simplified: + +```java +public static BigInteger dispatch(IGrid grid, IPatternDetails pattern, + KeyCounter[] unitPrototype, BigInteger requested, + Object jobHandle, boolean urgent) { + AlloyFurnaceBigIntegerCpuBinding binding = + new AlloyFurnaceBigIntegerCpuBinding(MyCpuAdapter.ID, jobHandle); + + for (AlloyFurnaceBigIntegerTarget target : AlloyFurnaceBigIntegerApi.findTargets(grid)) { + // Defer non-urgent batches when the machine is busy (throttling never drives capacity to 0; + // whether to stop is your call). + if (target.isThrottled() && !urgent) { + continue; + } + AlloyFurnaceBigIntegerCapacity capacity = target.capacity(pattern, unitPrototype, requested); + if (!capacity.isAvailable()) { + continue; + } + AlloyFurnaceBigIntegerBatch batch = + target.admit(pattern, unitPrototype, capacity.accepted(), binding); + if (batch == null) { + continue; + } + if (batch.commit(unitPrototype)) { + return batch.count(); // input ownership transferred + } + // commit failed: roll back your own (count - 1) withdrawal, then try the next machine + } + return null; +} +``` + +## FAQ + +### Why is capacity much smaller than `long`? + +First check the **energy gate**. Energy billing **depends on the coil tier**: + +| Coil | Billing | Limits the count? | +| --- | --- | --- | +| **Useful coil (tier 10)** | A single **flat** cost per batch, `ceil(recipeEnergy / 1024)`, **independent of count** | **No** — there is no energy gate | +| Other tiers | `count × per-craft energy` | Yes: `count ≤ available energy / per-craft energy` | + +So on the **useful coil, capacity is limited only by the "material window + output segmentation" +budget and can reach the full bigint scale**; lower tiers are narrowed by the internal energy buffer +(its capacity grows with tier; only the top tier is `Long.MAX_VALUE`). + +Other things that can shrink capacity: coil tier too low, missing mold hub or molds, the return queue +being saturated by backpressure, or active dynamic throttling (use `isThrottled()` to identify the +last one). + +### With millions of coil threads, why is capacity still limited? + +Because **outputs must be written back to the ME network chunk by chunk** (AE2's storage interface +accepts at most `long` per call), and the return path can only make so many insert calls per tick. +**Delivery capacity does not grow with thread count.** + +So a single batch's size is decided by "output-return time budget ÷ per-insert cost", not by the thread +count. **Your million threads still determine the material window** (one long window per thread) — +they simply no longer determine a single batch's **delivery size**. + +To get larger batches, raise the machine's **output-return time budget** in its config; raising it also +raises the per-tick write-back cost. + +### Why does the machine return `waiting_mold_hub` / `waiting_tier`? + +`statusKey` is the **full translation key** produced by the machine's recipe-availability check (for +example `gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_tier`); render it with +`Component.translatable(statusKey)`. Common values: + +| Key suffix | Meaning | +| --- | --- | +| `waiting_structure` | The multiblock structure is not formed | +| `waiting_recipe` | The recipe bound to the pattern is no longer valid (changed/removed) | +| `waiting_tier` | Coil tier too low | +| `waiting_mold_hub` | No mold hub | +| `waiting_missing_mold` | The mold hub lacks a mold this recipe needs | + +### Should I retry after `commit` returns `false`? + +Yes, but first roll back your own `count - 1` withdrawal. Common causes: energy was consumed by +something else in the meantime, the return queue is full (backpressure), or the machine was broken / +not formed at that instant. + +### How is an omniversal pattern's bigint batch executed? + +**Folded**: the machine resolves the bound recipe once, multiplies outputs by `count` (accounted in +BigInteger), then chunks them at `Long.MAX_VALUE` back into the network; energy is charged as +`count × per-craft energy`. It does not actually run `count` separate processing operations. + +### Can I dispatch only AE2 crafting patterns? + +Yes. AE2 crafting patterns support bigint batches too, and they **cost no energy** (the machine +assembles once on the virtual 3×3 grid and scales the result). diff --git a/wiki/ALLOY_FURNACE_BIGINT_API_ZH_CN.md b/wiki/ALLOY_FURNACE_BIGINT_API_ZH_CN.md new file mode 100644 index 00000000..068402e0 --- /dev/null +++ b/wiki/ALLOY_FURNACE_BIGINT_API_ZH_CN.md @@ -0,0 +1,406 @@ +# 万象合金炉 BigInteger 发配 API + +

+ English | + 简体中文 +

+ +本文档面向需要把**超过 `long` 的合成批次**发配给多方块万象合金炉的附属模组。 + +这些 API 是 Useless Mod 的公开契约,用于**替代**对 `MultiblockAlloyFurnaceCoreBlockEntity`、 +`AdvancedAlloyFurnaceAeManager` 等内部类的反射访问。 + +对应 NeoForge 1.21.1 分支。 + +## 概述 + +AE2 原生的 `ICraftingProvider#pushPattern` 只能用 `long` 表达数量,一次物理提交最多 +`Long.MAX / 每次消耗量` 份。想发配真正的大数(例如 `1e22` 个物品)只有两条路: + +1. **长版 counted 路径**:每 tick 派发一个 `long` 窗口,靠多次派发累加到 bigint 总量 —— + 这是接口决定的回退路径,慢且占调度; +2. **原生 bigint 路径**(本 API):一次派发就能交付**超过 `long`** 的批次。 + +本 API 把第 2 条做成公开契约,因此你**不需要**任何第三方调度框架, +也不需要往 AE2 的合成 CPU 逻辑里写 mixin。 + +> **适用范围**:只对**多方块万象合金炉**(ME 样板总成 + 多方块核心)有效。 +> 单方块高级合金炉不支持 bigint 批次,它不会出现在 `findTargets` 的结果里。 + +## 目录 + +- [包结构与类型](#包结构与类型) +- [通用约束](#通用约束) +- [1. 发现机器](#1-发现机器) +- [2. 查询容量](#2-查询容量) +- [3. 准入与提交](#3-准入与提交) +- [4. 产物交付(CPU 侧)](#4-产物交付cpu-侧) +- [5. 完整示例](#5-完整示例) +- [常见问题](#常见问题) + +## 包结构与类型 + +| 包 | 用途 | +| --- | --- | +| `com.sorrowmist.useless.api.crafting.bigint` | 供应器侧契约:发现、容量、准入、提交 | +| `com.sorrowmist.useless.api.crafting.bigint.cpu` | CPU 侧 SPI:注册适配器,接收产物与终局回执 | +| `com.sorrowmist.useless.api.crafting.bigint.example` | 参考实现(不参与运行,供照抄) | + +| 类型 | 用途 | +| --- | --- | +| `AlloyFurnaceBigIntegerApi` | 发现入口 | +| `AlloyFurnaceBigIntegerTarget` | 一台机器:查容量 / 申请批次 | +| `AlloyFurnaceBigIntegerCapacity` | 容量查询结果 | +| `AlloyFurnaceBigIntegerBatch` | 一次性准入凭据 | +| `AlloyFurnaceBigIntegerOutput` | BigInteger 规模的产物(键 + 精确数量) | +| `AlloyFurnaceBigIntegerProvider` | 供应器侧标记接口(判定「是不是大数供应器」用) | +| `AlloyFurnaceBigIntegerCpuAdapter` | **你实现的回调接口** | +| `AlloyFurnaceBigIntegerCpuAdapters` | 适配器注册表 | +| `AlloyFurnaceBigIntegerCpuBinding` | 把适配器 id 与你的上下文绑到批次上 | +| `AlloyFurnaceBigIntegerBatchContext` | 每次回调都会带上的批次上下文 | +| `AlloyFurnaceBigIntegerBatchResult` | 批次终局状态 | + +## 通用约束 + +1. **只在服务器线程调用**。所有方法、所有回调都是如此。 +2. **不要缓存 `AlloyFurnaceBigIntegerTarget`**。它由机器的方块实体持有,跨 tick 缓存会在机器 + 被拆 / 卸载后变成野指针。每 tick 重新 `findTargets` 即可(只是网格节点遍历,很轻)。 +3. **不要反射或强转本模组的方块实体类**。判定「是不是大数供应器」请用 + `instanceof AlloyFurnaceBigIntegerProvider`。 +4. **原型是「单次推送」的量**。整批剩下的 `count - 1` 份材料由**你自己的账本**扣, + 提交失败时要回滚。详见 [第 3 节](#3-准入与提交)。 +5. **不要自己往原型里乘 `count`**。次数只通过 `count` 传递。 +6. **回调里不要抛异常**,也不要再调用 `admit` / `commit`(会重入)。抛出的异常会被记录并吞掉。 + +## 1. 发现机器 + +```java +import com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerApi; +import com.sorrowmist.useless.api.crafting.bigint.AlloyFurnaceBigIntegerTarget; + +IGrid grid = /* 你的 CPU 所在网格 */; +for (AlloyFurnaceBigIntegerTarget target : AlloyFurnaceBigIntegerApi.findTargets(grid)) { + String machine = target.machineIdentity(); // 例:minecraft:overworld@128,64,-256 + // ... +} +``` + +- `findTargets(IGrid)` 返回网格里**当前可用**的全部目标,已按 `machineIdentity()` 去重 + (同一台物理机器只出现一次)。 +- `findTarget(IGrid, String machineIdentity)` 按身份精确查找;适合「我上次用的是哪台」这种场景。 +- `acceptedPatternKinds()` 返回机器接受的样板物品 id 集合,可用于在昂贵的解码之前做一次廉价过滤。 + +## 2. 查询容量 + +```java +AlloyFurnaceBigIntegerCapacity capacity = + target.capacity(pattern, unitPrototype, requestedCount); + +if (!capacity.isAvailable()) { + String reason = capacity.statusKey(); // 语言键,可直接显示给玩家;可能为空串 + return; +} +BigInteger accepted = capacity.accepted(); // 可能小于 requestedCount,也可能远超 long +``` + +**`capacity` 无副作用**,可以每次 tick 都调用。 + +容量同时受四道闸限制,取最小值: + +| 闸 | 含义 | +| --- | --- | +| 配方可用性 | 线圈档次是否够、所需模具是否就位(**仅万象样板**) | +| 材料窗口 | 线程数 N ⇒ 本批最多吃下 N 份「每种材料各 `Long.MAX`」的量。**这是一道配平闸,可由机器配置 `ae_unlimited_bigint_parallelism` 解除**(解除后单批规模只由交付能力决定) | +| 产物交付能力 | 单批规模由机器按**当前交付能力**自动决定,会随时间浮动(见下) | +| 能量 | `count × 单份能耗` 必须付得起(**仅万象样板**) | + +**AE2 合成样板只有「材料窗口 + 产物交付能力」两道闸**:它在虚拟 3×3 工作台上装配一次就折叠完, +既没有绑定配方、也不收能量,所以容量通常比万象样板大得多。 + +### 单批规模是浮动的 + +机器按**当前的交付能力**决定单批能有多大(产物必须逐段写回 ME 网络,每 tick 能写多少是有限的)。 +因此: + +- 超大量合成时 `capacity` 会在一个区间里浮动 —— **正常现象**,不要把它当成机器能力的固定值; +- `capacity` **不会因积压而归零**(除非配方失效 / 档次不够 / 缺模具 / 没能量这些硬条件不满足), + 所以你的调度循环不需要「停一拍等队列排空再重启」; +- 想让批次更大,请调大机器配置里的**回网时间预算**(见下节),而不是反复重试。 + +### 动态降频:`isThrottled()` + +机器对每条 bigint 路径都做了「每 tick 时间预算」的动态降频:实测耗时超预算时, +后续批次的容量会被按比例收窄。粒度是**全局**的 —— 所有机器跑在同一个服务端线程上, +多台机器的耗时会累加、共同把降频压下去。 + +```java +if (target.isThrottled() && !urgent) { + return; // 机器正忙,延后非紧急批次 +} +``` + +它的用途是让你**区分**两种「容量很小」: + +| 情况 | 含义 | +| --- | --- | +| 容量小 + `isThrottled() == false` | 机器能力就这样(材料窗口 / 能量 / 交付能力 / 配置的线程数) | +| 容量小 + `isThrottled() == true` | 机器(或同一服务端线程上的其它机器)正忙 | + +**降频不会让容量变成 0**:完全报「无容量」会引发调度侧「无容量 → 重新提交」的空转, +所以降频只收窄批次规模。要不要停手由你决定。 + +> 产物回网也受同一套降频,但策略不同:准入降频收窄**批次规模**(少收新活), +> 回网降频收窄**每 tick 愿意花的时间**(少投递点已完成的产物)。 +> 对你而言的可见结果是:**服务器越忙,大数批次的产物回执到得越晚**,但一定会到。 + +## 3. 准入与提交 + +```java +AlloyFurnaceBigIntegerBatch batch = + target.admit(pattern, unitPrototype, capacity.accepted(), binding); +if (batch == null) { + return; // 此刻不可接受 +} +if (batch.commit(unitPrototype)) { + // 成功:材料所有权已转移给机器 +} else { + // 失败:机器没消费任何材料。请回滚你自己扣掉的那 (count - 1) 份,再试下一台。 +} +``` + +### 调用契约(务必读完) + +1. **只能提交一次**。`commit` 第二次调用抛 `IllegalStateException`。 +2. **提交必须用「准备时那一份原型」**:调用 `admit` 时传入的 `KeyCounter[]` 数组对象本身, + 不能复制、不能重建。用别的数组提交抛 `IllegalArgumentException`。 +3. **机器只消费手里那一份原型**。`admit` 传入的原型是**单次推送**的材料,不是整批的 `count` 倍。 + 整批里剩下的 `count - 1` 份材料由**你自己的 BigInteger 账本**负责扣除, + 并在我们提交失败时回滚。 +4. **提交失败不代表材料丢失**:`commit` 返回 `false` 时机器不会清空原型, + 调用方应据此回滚自己的那 `count - 1` 份扣账,并稍后重试或改走别的供应器。 +5. **提交返回 `true` 即转移所有权**:此后原型里的材料归机器所有(机器会清空它), + 产物由机器交付(见 [第 4 节](#4-产物交付cpu-侧))。 +6. `admit` **不消费材料、不扣能量**,只做一次快照式检查;真正的检查会在 `commit` 时再跑一遍 + (期间机器状态可能变化,最终以 `commit` 的返回值为准)。 + +## 4. 产物交付(CPU 侧) + +产物有**两条**交付路径。默认走第 1 条;若你实现了适配器并愿意接管,可走第 2 条。 + +**① 默认 —— 逐段写回网络。** AE2 的存储接口单次最多接受 `long` +(`IMEInventory#insert(AEKey, long, Actionable)`),所以机器把产物按 `Long.MAX_VALUE` 切段、 +**逐 tick** 写回 ME 网络。**这条路径下你什么都不用做**,产物照常进入 ME 网络。 + +**② 批量 —— `claimOutputs`(可选,可跳过逐段写回)。** 见 4.2 节。 + +### 4.1 默认路径:逐段写回网络 + +产物照常进入 ME 网络,你的 CPU 从网络里取用即可。这条路径**不需要你做任何事**。 + +需要留意的只有时序:**大数批次的交付是分 tick 摊开的**(见 4.5), +所以 `1e22` 量级的批次可能要几秒钟才全部入网。 + +### 4.2 批量路径:`claimOutputs`(可跳过逐段写回) + +如果你的 CPU 愿意**直接收下整批**(例如你本来就用 BigInteger 账本记账、不打算让物品先进网络), +实现这个回调即可让机器跳过逐段写回: + +```java +@Override +public @NotNull Map claimOutputs( + @NotNull AlloyFurnaceBigIntegerBatchContext context, + @NotNull List outputs) { + // outputs 是本趟**准备写回网络**的产物。 + // 返回你收下的量(按 key 计);机器会把这些量从待交付账本中扣除,只把余额写回网络。 + Map taken = new HashMap<>(); + for (AlloyFurnaceBigIntegerOutput output : outputs) { + BigInteger accept = decideHowMuchToTake(output); // 你的策略;可以只收一部分 + if (accept.signum() > 0) { + taken.put(output.what(), accept); + } + } + return taken; +} +``` + +**调用时机**:产物写回网络**之前**,按**键**调用。默认实现返回空 Map ⇒ +行为与不实现该方法**完全一致**(全部写回网络)。 + +**对两类样板都适用**:万象样板与 AE2 合成样板走的是同一条产物队列、同一套交付逻辑, +只要 `admit` 时绑定了适配器,两者都会触发本回调。回调里的 `key` 是**实际键** +(万象样板的动态产物在折叠时才会解析出真实键),直接按它记账即可。 + +> **⚠️ 接收即接管。** 收下的物品**不会进入 ME 网络**,只存在于你自己的账本里。因此: +> - 你必须负责它们后续的去向(自行落网、或作为中间产物消耗); +> - **机器无法退还** —— 账本在调用返回时已被扣减。若批次随后被取消 +> (`onBatchFinished` 收到 `CANCELLED`),已收下的部分需由你自行处理; +> - 实现必须**不抛异常**;真抛了机器会记录并退回逐段写回(不会中断,但可能造成归属不一致)。 + +**触发条件**:只有当本批是通过 `admit(..., cpuBinding)` **绑定了适配器**、 +且整趟写回绑定的都是同一个适配器时才会调用。多 CPU 混绑同一台机器时不会调用(退回逐段写回)。 + +### 4.3 实现适配器 + +```java +public final class MyCpuAdapter implements AlloyFurnaceBigIntegerCpuAdapter { + public static final ResourceLocation ID = + ResourceLocation.fromNamespaceAndPath("mymod", "bigint_cpu"); + + @Override public ResourceLocation id() { return ID; } + + @Override + public void onBatchAdmitted(AlloyFurnaceBigIntegerBatchContext context) { + // 材料所有权已转移。把「本批计划产出」记进自己的账本 / 让任务进入等待状态。 + } + + @Override + public void onBatchOutputs(AlloyFurnaceBigIntegerBatchContext context, + List actualOutputs) { + // 产物已全部交付完成。actualOutputs 是 BigInteger 精确值 —— + // 对动态产物来说,这里的键才是真正的键。 + // 注意:若你实现了 claimOutputs 并收下了全部产物,本回调依然会到(作为「本批交付完成」的信号), + // 但那时物品在你的账本里,不要把 actualOutputs 再加一遍。 + } + + @Override + public void onBatchFinished(AlloyFurnaceBigIntegerBatchContext context, + AlloyFurnaceBigIntegerBatchResult result) { + // SUCCESS / CANCELLED / REJECTED + } +} +``` + +### 4.4 注册 + +在你自己 mod 的公共初始化阶段注册一次: + +```java +@SubscribeEvent +public static void onCommonSetup(FMLCommonSetupEvent event) { + event.enqueueWork(() -> AlloyFurnaceBigIntegerCpuAdapters.register(new MyCpuAdapter())); +} +``` + +注册表是全局的、进程级的,不随存档或世界重载清空。若你的 mod 支持运行期禁用, +用 `AlloyFurnaceBigIntegerCpuAdapters.unregister(id)` 摘掉。 + +### 4.5 回调时序 + +| 时机 | 回调 | +| --- | --- | +| `commit` 成功、同一 tick | `onBatchAdmitted` | +| 产物**全部**交付完成之后 | `onBatchOutputs`,紧接着 `onBatchFinished(SUCCESS)` | +| 机器被拆 / 玩家取消 / 任务中止 | 只调用 `onBatchFinished(CANCELLED)` | + +**一个批次只会有一个终局回调**(`SUCCESS` 或 `CANCELLED`,二选一)。取消时即使产物在同一 tick 内 +被强制刷空,也不会再补一个 `SUCCESS`。 + +> **大数批次的交付是分 tick 摊开的。** 单台机器每 tick 的写回工作量有上限, +> 所以 `1e22` 量级的批次可能要几秒钟才全部入网,`onBatchOutputs` / +> `onBatchFinished(SUCCESS)` 也就相应晚到。**这是刻意的** —— 避免一次巨量写回把服务端线程拖住。 +> 若你的任务对交付延迟敏感,请按「回调会晚到但一定到」设计,不要在发出批次后设过短的超时。 + +**取消时产物不会丢**:机器会把队列里剩余的产物强制写回 ME 网络,只是不再走逐 tick 节奏。 +收到 `CANCELLED` 时请按「这批已由机器交付,但我没拿到逐条回执」处理,**不要**补记产物。 + +### 4.6 存档重载 + +`cpuToken` 不可序列化,重载后为 `null`;机器的产物队列本身照常持久化(**不会丢物品**), +但**不再补发回调**。请以 ME 网络的实际入账为准,或按 `batchId()` 自行对账。 + +### 4.7 没有适配器时会怎样 + +完全没有降级风险:不注册适配器(或 `admit` 时传 `cpu = null`)时,产物依旧照常逐段写回 +ME 网络,只是不发这些回调、也不会调用 `claimOutputs`。 + +## 5. 完整示例 + +`com.sorrowmist.useless.api.crafting.bigint.example.ExampleBigIntegerCpuAdapter` +是一个可直接照抄的参考实现,包含完整的发现 → 容量 → 准入 → 提交 → 产物交付流程。 +其发配入口简化后是这样: + +```java +public static BigInteger dispatch(IGrid grid, IPatternDetails pattern, + KeyCounter[] unitPrototype, BigInteger requested, + Object jobHandle, boolean urgent) { + AlloyFurnaceBigIntegerCpuBinding binding = + new AlloyFurnaceBigIntegerCpuBinding(MyCpuAdapter.ID, jobHandle); + + for (AlloyFurnaceBigIntegerTarget target : AlloyFurnaceBigIntegerApi.findTargets(grid)) { + // 机器正忙时按需延后非紧急批次(降频不会把容量压到 0,停不停手由你决定)。 + if (target.isThrottled() && !urgent) { + continue; + } + AlloyFurnaceBigIntegerCapacity capacity = target.capacity(pattern, unitPrototype, requested); + if (!capacity.isAvailable()) { + continue; + } + AlloyFurnaceBigIntegerBatch batch = + target.admit(pattern, unitPrototype, capacity.accepted(), binding); + if (batch == null) { + continue; + } + if (batch.commit(unitPrototype)) { + return batch.count(); // 材料所有权已转移 + } + // commit 失败:回滚你自己扣掉的 (count - 1) 份,然后试下一台 + } + return null; +} +``` + +## 常见问题 + +### 为什么容量比 `long` 小得多? + +先看是不是**能量闸**。能量的计费方式**随线圈档次不同**: + +| 线圈 | 计费方式 | 是否限制份数 | +| --- | --- | --- | +| **有用线圈(tier 10)** | 整批**一次固定能耗** `ceil(配方能耗 / 1024)`,**与份数无关** | **不限制** —— 没有能量闸 | +| 其它档次 | `count × 单份能耗` | 限制:`count ≤ 可用能量 / 单份能耗` | + +也就是说:**有用线圈上容量只受「材料窗口 + 交付能力」限制,能吃到完整 bigint 规模**; +低档次线圈则会被内部能量缓冲(容量随档次增长,只有满级才是 `Long.MAX_VALUE`)压小。 + +其余可能压小容量的原因:线圈档次不够、缺模具中枢或模具、正在动态降频 +(用 `isThrottled()` 区分最后一种)。 + +### 线圈并行上百万时,容量为什么也被限制? + +因为**产物必须逐段写回 ME 网络**(AE2 存储接口单次只能收 `long`), +而回网每 tick 只能插那么多次 —— **交付能力不随线程数增长**。 + +所以单批规模由「回网时间预算 ÷ 单次插入成本」决定,而不是由线程数决定。 +**你的百万线程仍然决定材料窗口**(每线程一个 long 窗口),只是不再决定单批的交付规模。 + +想加大单批规模,请调大机器配置里的**回网时间预算**;调大后会同步加大每 tick 的写回耗时。 + +### 机器为什么返回 `waiting_mold_hub` / `waiting_tier`? + +`statusKey` 是机器配方可用性检查给出的**完整语言键**(例如 +`gui.useless_mod.advanced_alloy_furnace.ae_task_status.waiting_tier`), +用 `Component.translatable(statusKey)` 就能显示。常见取值: + +| 语言键后缀 | 含义 | +| --- | --- | +| `waiting_structure` | 多方块结构未成形 | +| `waiting_recipe` | 样板绑定的配方已失效(配方被改 / 删) | +| `waiting_tier` | 线圈档次不够 | +| `waiting_mold_hub` | 没有模具中枢 | +| `waiting_missing_mold` | 模具中枢里缺本次配方需要的模具 | + +### 提交返回 `false` 后要不要重试? + +可以,但要先把你自己扣掉的那 `count - 1` 份回滚。返回 `false` 的常见原因是: +能量刚好被别人用掉、机器在这一瞬间被拆或未成形。 + +### 万象样板的大数批次怎么执行? + +**折叠**语义:机器解析一次绑定配方,把产物 ×`count`(用 BigInteger 记账),再逐段写回网络; +能量按 `count × 单份能耗` 扣。它不会真的逐份跑 `count` 次加工。 + +### 我可以只发配 AE2 合成样板吗? + +可以。AE2 合成样板同样支持 bigint 批次,而且**不收能量**(机器在虚拟 3×3 工作台上装配一次 +再整体放大)。 diff --git a/wiki/KUBEJS_ALLOY_FURNACE_GUIDE.md b/wiki/KUBEJS_ALLOY_FURNACE_GUIDE.md new file mode 100644 index 00000000..1b041db7 --- /dev/null +++ b/wiki/KUBEJS_ALLOY_FURNACE_GUIDE.md @@ -0,0 +1,541 @@ +# KubeJS Advanced Alloy Furnace Recipe Guide + +

+ English | + 简体中文 +

+ +## Overview + +The Advanced Alloy Furnace supports adding, replacing, and removing recipes through KubeJS during the server recipe-loading phase. Put scripts in `kubejs/server/`; changes take effect after `/reload`. + +This integration registers the recipe type `useless_mod:advanced_alloy_furnace` and exposes it as: + +```javascript +event.recipes.useless_mod.advanced_alloy_furnace(...) +``` + +Install a KubeJS build for NeoForge 1.21.1. Without KubeJS, Useless Mod remains usable and the optional integration is inactive. + +## Table of Contents + +- [Basic Usage](#basic-usage) +- [Input/Output Combinations](#inputoutput-combinations) +- [AEKey Inputs and Outputs](#aekey-inputs-and-outputs) +- [Field Description](#field-description) +- [Recipe Tiers](#recipe-tiers) +- [Replacing and Removing Recipes](#replacing-and-removing-recipes) +- [Complete Example](#complete-example) +- [Extended JSON](#extended-json) +- [Best Practices](#best-practices) +- [Troubleshooting](#troubleshooting) + +## Basic Usage + +### Minimal Recipe + +```javascript +ServerEvents.recipes(event => { + event.recipes.useless_mod.advanced_alloy_furnace( + 'kubejs:iron_alloy', + [ + { ingredient: 'minecraft:iron_ingot', count: 4 }, + { ingredient: 'minecraft:gold_ingot', count: 2 } + ], + [ + Item.of('kubejs:iron_alloy', 1) + ] + ) +}) +``` + +The positional form takes the recipe data ID, item input list, and item output list. An omitted `count` defaults to 1. For fluid-only outputs, use object syntax and omit `outputs`. + +### Items and Fluids Together + +Items and fluids use separate fields: item inputs use `ingredients`, fluid inputs use `input_fluids`, item outputs use `outputs`, and fluid outputs use `output_fluids`. One recipe may contain item and fluid inputs and outputs together: + +```javascript +ServerEvents.recipes(event => { + event.recipes.useless_mod.advanced_alloy_furnace({ + id: 'kubejs:water_alloy', + ingredients: [ + { ingredient: 'minecraft:iron_ingot', count: 2 } + ], + input_fluids: [ + { ingredient: 'minecraft:water', amount: 1000 } + ], + outputs: [ + Item.of('kubejs:water_alloy', 1) + ], + output_fluids: [ + Fluid.of('minecraft:lava', 250) + ] + }) +}) +``` + +For a fluid-only recipe, use object syntax and omit the unused item list. The `ingredients` field itself remains required by the schema; if there are no item inputs, set `ingredients: []`. + +### Mold and Tier + +```javascript +ServerEvents.recipes(event => { + event.recipes.useless_mod.advanced_alloy_furnace( + 'kubejs:gear_alloy', + [ + { ingredient: '#c:ingots/iron', count: 4 }, + { ingredient: '#c:ingots/copper', count: 4 } + ], + [ + Item.of('kubejs:gear_alloy', 1) + ], + 'useless_mod:metal_mold_gear', + 3 + ) +}) +``` + +The fourth positional argument is a single `mold`; the fifth is `tier`. This recipe requires a coil tier of at least 3. + +### Pentacle Molds (Components Required) + +`mold` / `molds` are plain Ingredients: **a bare item ID or tag carries no NBT/components**, so it degrades to "any ritual blueprint matches". To bind a specific Occultism pentacle you must include the `useless_mod:ritual_blueprint_pentacle` component: + +```javascript +ServerEvents.recipes(event => { + event.recipes.useless_mod.advanced_alloy_furnace({ + id: 'kubejs:foliot_miner', + ingredients: [ + { ingredient: 'occultism:book_of_binding_bound_foliot', count: 1 }, + { ingredient: 'occultism:iesnium_pickaxe', count: 1 }, + { ingredient: 'occultism:magic_lamp_empty', count: 1 } + ], + outputs: [Item.of('occultism:miner_foliot_unspecialized', 1)], + mold: Item.of('useless_mod:ritual_blueprint', { + 'useless_mod:ritual_blueprint_pentacle': ['occultism:craft_foliot'] + }), + tier: 0 + }) +}) +``` + +The equivalent SNBT string form: + +```javascript +mold: 'useless_mod:ritual_blueprint{useless_mod:ritual_blueprint_pentacle:["occultism:craft_foliot"]}' +``` + +Every entry of `molds` accepts the same component form and can be mixed with ordinary molds: + +```javascript +molds: [ + Item.of('useless_mod:ritual_blueprint', { + 'useless_mod:ritual_blueprint_pentacle': ['occultism:craft_foliot'] + }), + 'useless_mod:metal_mold_gear' +] +``` + +> Matching only requires the blueprint's pentacle set to **contain** the required pentacles, so one blueprint imprinted with several pentacles can satisfy multiple recipes. + +### Object Syntax + +Object syntax is recommended when setting a tier without a mold, multiple molds, or several optional fields: + +```javascript +ServerEvents.recipes(event => { + event.recipes.useless_mod.advanced_alloy_furnace({ + id: 'kubejs:press_alloy', + ingredients: [ + { ingredient: 'minecraft:diamond', count: 8 }, + { ingredient: 'minecraft:netherite_ingot', count: 1 } + ], + outputs: [ + Item.of('kubejs:press_alloy', 1) + ], + tier: 5, + energy: 500000, + process_time: 200 + }) +}) +``` + +### Multiple Molds + +Use `molds` to declare multiple independent mold requirements: + +```javascript +ServerEvents.recipes(event => { + event.recipes.useless_mod.advanced_alloy_furnace({ + id: 'kubejs:multi_mold_alloy', + ingredients: [ + { ingredient: 'minecraft:iron_block', count: 2 } + ], + outputs: [ + Item.of('kubejs:multi_mold_alloy', 1) + ], + molds: [ + 'useless_mod:metal_mold_gear', + 'useless_mod:metal_mold_plate' + ], + tier: 4 + }) +}) +``` + +Do not define both `mold` and `molds`. Multiple mold requirements are handled by the Advanced Alloy Furnace mold hub; a regular single-mold slot does not support them. + +## Input/Output Combinations + +The recipe supports these combinations of item/fluid input and output shapes: + +| Test ID suffix | Inputs | Outputs | +|---|---|---| +| `item_to_item` | Item | Item | +| `item_to_fluid` | Item | Fluid | +| `item_fluid_to_item` | Item + Fluid | Item | +| `item_fluid_to_fluid` | Item + Fluid | Fluid | +| `item_to_item_fluid` | Item | Item + Fluid | +| `item_fluid_to_item_fluid` | Item + Fluid | Item + Fluid | +| `fluid_to_item` | Fluid | Item | +| `fluid_to_fluid` | Fluid | Fluid | +| `fluid_to_item_fluid` | Fluid | Item + Fluid | + +For example, an item and fluid input with both output types is: + +```javascript +event.recipes.useless_mod.advanced_alloy_furnace({ + id: 'kubejs:alloy_furnace_item_fluid_to_item_fluid_test', + ingredients: [ + { ingredient: { item: 'minecraft:redstone' }, count: 1 } + ], + input_fluids: [ + { ingredient: { fluid: 'minecraft:water' }, amount: 1000 } + ], + outputs: [ + Item.of('minecraft:gold_nugget', 1) + ], + output_fluids: [ + Fluid.of('minecraft:lava', 250) + ], + tier: 0 +}) +``` + +When a test has no item input, keep the required `ingredients` field and set it to an empty array: + +```javascript +event.recipes.useless_mod.advanced_alloy_furnace({ + id: 'kubejs:alloy_furnace_fluid_to_item_test', + ingredients: [], + input_fluids: [ + { ingredient: { fluid: 'minecraft:water' }, amount: 1000 } + ], + outputs: [Item.of('minecraft:coal', 1)], + tier: 0 +}) +``` + +Mold requirements can contain one, two, or three independent entries: + +```javascript +// One mold +molds: [{ item: 'minecraft:stick' }] + +// Two molds +molds: [ + { item: 'minecraft:stick' }, + { item: 'minecraft:stone' } +] + +// Three molds +molds: [ + { item: 'minecraft:stick' }, + { item: 'minecraft:stone' }, + { item: 'minecraft:cobblestone' } +] +``` + +Each mold entry is an independent requirement. Do not combine `mold` and `molds` in one recipe. + +## AEKey Inputs and Outputs + +The schema exposes AE2 generic stacks through `key_inputs` and `key_outputs`. Each entry is a `GenericStack`: `#t` selects the registered AEKey type and `#` is the amount. This supports custom key types without a hard-coded dependency on their mod. + +For a Data Energistics data flow recipe, install and load Data Energistics so that `data_energistics:data_flow` is registered before the recipe is parsed: + +```javascript +ServerEvents.recipes(event => { + event.recipes.useless_mod.advanced_alloy_furnace({ + id: 'kubejs:data_flow_alloy', + ingredients: [], + key_inputs: [ + { '#t': 'data_energistics:data_flow', '#': 1200 } + ], + key_outputs: [ + { '#t': 'data_energistics:data_flow', '#': 200 } + ], + tier: 0, + energy: 1000, + process_time: 20 + }) +}) +``` + +`data_energistics:data_flow` is an AEKey, not an item and not a normal fluid. Its amount therefore belongs in `key_inputs` or `key_outputs`. The recipe still needs `ingredients: []` when it has no item input, but ordinary `outputs` and `output_fluids` can remain omitted when `key_outputs` supplies the output. + +AE2 item and fluid keys use the same format: + +```javascript +key_inputs: [ + { '#t': 'ae2:i', id: 'minecraft:iron_ingot', '#': 1 }, + { '#t': 'ae2:f', id: 'minecraft:water', '#': 1000 } +], +key_outputs: [ + { '#t': 'ae2:i', id: 'minecraft:gold_ingot', '#': 1 }, + { '#t': 'ae2:f', id: 'minecraft:lava', '#': 250 } +] +``` + +Use `input_fluids` and `output_fluids` for the alloy furnace's ordinary machine fluid ports. Use `ae2:f` inside `key_inputs` and `key_outputs` when the fluid is meant to be transferred through the AE network as an AEKey. + +## Field Description + +| Field | Type | Required | Default | Description | +|---|---|---:|---:|---| +| `id` | String | Yes | - | Recipe data ID, for example `kubejs:iron_alloy` | +| `ingredients` | Object[] | Yes | - | Input list; each entry has `ingredient` and an optional `count` | +| `input_fluids` | Object[] | No | `[]` | Fluid input list; each entry has a fluid `ingredient` and `amount` | +| `key_inputs` | GenericStack[] | No | `[]` | AEKey input list; each entry uses the AE2 generic stack format | +| `outputs` | ItemStack[] | No | `[]` | Item output list; strings and `Item.of(...)` are supported | +| `output_fluids` | FluidStack[] | No | `[]` | Fluid output list; `Fluid.of(...)` is supported | +| `key_outputs` | GenericStack[] | No | `[]` | AEKey output list; each entry uses the AE2 generic stack format | +| `mold` | Ingredient | No | Empty | Single mold requirement: item ID, tag, or `Item.of(...)` with components; binding a pentacle requires `useless_mod:ritual_blueprint_pentacle` | +| `molds` | Ingredient[] | No | Empty | Multiple independent mold requirements; mutually exclusive with `mold`; components are supported per entry | +| `tier` | Integer | No | Unspecified | Minimum coil tier, from `0` to `10` | +| `energy` | Long | No | `2000` | Energy consumed by one operation | +| `process_time` | Integer | No | `200` | Processing time in ticks | +| `catalyst` | Ingredient | No | Empty | Catalyst requirement; the current alloy-furnace runtime does not consume it | + +### Item Input Format + +```javascript +ingredients: [ + { ingredient: 'minecraft:iron_ingot', count: 4 }, + { ingredient: '#c:ingots/copper', count: 2 }, + { ingredient: 'minecraft:diamond' } +] +``` + +`ingredient` uses the standard KubeJS Ingredient syntax. It may be an item ID, a tag, or another Ingredient expression supported by KubeJS. Each input is checked independently. + +NBT-bearing inputs must use `Item.of(...)` or the SNBT string form: + +```javascript +ingredients: [ + // Components: every listed component must exist with the same value; extra components are allowed + { ingredient: Item.of('occultism:book_of_binding_bound_foliot', { + 'occultism:spirit_name': 'Some Name' + }), count: 1 }, + // Equivalent SNBT string form + { ingredient: 'occultism:book_of_binding_bound_foliot{occultism:spirit_name:"Some Name"}', count: 1 }, + // No components: any instance of the item matches + { ingredient: 'minecraft:iron_ingot', count: 4 } +] +``` + +> A bare item ID (for example `'occultism:book_of_binding_bound_foliot'`) does **not** keep NBT, so any instance matches. The same applies to molds; include the components when you need an exact match. + +### Fluid Input Format + +```javascript +input_fluids: [ + { ingredient: 'minecraft:water', amount: 1000 }, + { ingredient: '#c:lava', amount: 250 } +] +``` + +`amount` is measured in mB and supports large integers. `ingredient` may be a fluid ID, a fluid tag, or another fluid Ingredient expression supported by KubeJS. + +### Output Format + +```javascript +outputs: [ + Item.of('minecraft:diamond', 2), + Item.of('kubejs:alloy', 1, { display: { Name: 'Custom alloy' } }) +] +``` + +An item string can also be used: + +```javascript +outputs: [ 'minecraft:iron_ingot' ] +``` + +### Fluid Output Format + +```javascript +output_fluids: [ + Fluid.of('minecraft:water', 1000), + Fluid.of('minecraft:lava', 250) +] +``` + +Item outputs are optional, so a recipe may produce only fluids: + +```javascript +ServerEvents.recipes(event => { + event.recipes.useless_mod.advanced_alloy_furnace({ + id: 'kubejs:fluid_only_alloy', + ingredients: [ + { ingredient: 'minecraft:iron_ingot', count: 1 } + ], + output_fluids: [ + Fluid.of('minecraft:lava', 1000) + ] + }) +}) +``` + +## Recipe Tiers + +`tier` is the minimum Advanced Alloy Furnace coil tier required by the recipe, from `0` to `10`. + +- `tier: 3` requires a coil tier of at least 3. +- `tier: 0` explicitly makes the recipe unrestricted. +- If `tier` is omitted, the server `recipe_tier_rules` configuration is used. +- `recipe_tier_rules` entries use `recipe ID pattern,tier`, for example `kubejs:*,3`. +- An explicit recipe `tier` takes priority over `recipe_tier_rules`. + +Example configuration: + +```toml +recipe_tier_rules = [ + "useless_mod:advanced_alloy/gear/*,4", + "kubejs:*,2" +] +``` + +The configuration only applies to recipes without an explicit `tier`. Reload the configuration or restart the game according to the configuration screen message. + +> ⚠️ In the example above, `kubejs:*,2` forces **every** recipe whose id starts with `kubejs:` to require a coil tier of at least 2. On a low-tier machine those recipes stay visible in JEI but the machine will not start them. Check `recipe_tier_rules` first when a recipe is visible yet refuses to run. + +## Replacing and Removing Recipes + +### Remove One Recipe + +```javascript +ServerEvents.recipes(event => { + event.remove({ id: 'useless_mod:advanced_alloy/gear/useless_gear_tier_1' }) +}) +``` + +### Remove by Type + +```javascript +ServerEvents.recipes(event => { + event.remove({ type: 'useless_mod:advanced_alloy_furnace' }) +}) +``` + +Removing by type removes every Advanced Alloy Furnace recipe, so use it carefully. + +### Replace One Recipe + +```javascript +ServerEvents.recipes(event => { + const id = 'useless_mod:advanced_alloy/gear/useless_gear_tier_1' + + event.remove({ id: id }) + event.recipes.useless_mod.advanced_alloy_furnace({ + id: id, + ingredients: [ + { ingredient: 'minecraft:iron_ingot', count: 8 } + ], + outputs: [ + Item.of('useless_mod:useless_gear_tier_1', 1) + ], + mold: 'useless_mod:metal_mold_gear', + tier: 2 + }) +}) +``` + +## Complete Example + +```javascript +ServerEvents.recipes(event => { + const { useless_mod } = event.recipes + + useless_mod.advanced_alloy_furnace({ + id: 'kubejs:complete_alloy', + ingredients: [ + { ingredient: 'minecraft:iron_block', count: 2 }, + { ingredient: '#c:ingots/gold', count: 8 } + ], + outputs: [ + Item.of('kubejs:complete_alloy', 1) + ], + mold: 'useless_mod:metal_mold_gear', + tier: 3, + energy: 100000, + process_time: 100, + catalyst: 'minecraft:diamond' + }) +}) +``` + +## Extended JSON + +The Schema covers item inputs, fluid inputs, AEKey inputs, item outputs, AEKey outputs, fluid outputs, molds, tiers, and common processing options. For other fields not exposed by the Schema, use `event.custom` and provide the serializer JSON directly. The raw JSON shape for fluid fields is: + +```javascript +ServerEvents.recipes(event => { + event.custom({ + type: 'useless_mod:advanced_alloy_furnace', + id: 'kubejs:fluid_alloy', + ingredients: [ + { + ingredient: { item: 'minecraft:iron_ingot' }, + count: 4 + } + ], + input_fluids: [ + { + ingredient: { fluid: 'minecraft:water' }, + amount: 1000 + } + ], + output_fluids: [ + { id: 'minecraft:water', amount: 1000 } + ], + tier: 2 + }) +}) +``` + +`event.custom` bypasses this Schema's argument conversion. Field names and JSON types must match the Advanced Alloy Furnace recipe serializer. + +## Best Practices + +1. Prefix recipe IDs with your mod ID or `kubejs:` to avoid collisions. +2. Use positive input counts and provide at least one valid item, fluid, or AEKey output. +3. When replacing a recipe, remove the old recipe before adding the replacement with the same `id`. +4. Use object syntax for multiple molds. +5. Only set `tier` when the recipe needs to override the server configuration; otherwise let `recipe_tier_rules` manage it. +6. Run `/reload` after changing scripts; reopening a recipe viewer alone is not enough. + +## Troubleshooting + +If a recipe does not appear or the machine cannot process it: + +1. Check the game log for KubeJS recipe creation errors. +2. Confirm that `id` is a valid resource location in the form `namespace:path`. +3. Confirm that `id` and `ingredients` are present, and that at least one valid entry exists in `outputs`, `output_fluids`, or `key_outputs`. +4. Confirm that `mold` and `molds` are not both defined. +5. Check coil tier, molds, input counts, and catalyst requirements. +6. For ordinary fluids, use `input_fluids`/`output_fluids`; for AEKey fluids or other AE resources, use `key_inputs`/`key_outputs` and verify the `#t` type is registered. +7. Run `/reload` after editing the script and check for script exceptions. +8. A `mold`/`molds` entry written as a bare item ID with no components degrades to "any blueprint" and can collide with other pentacle recipes; follow [Pentacle Molds](#pentacle-molds-components-required) and include the components. +9. When re-registering Occultism rituals with `occultism_kubejs`, note that `event.recipes.occultism.ritual(...)` writes `ritual_type = occultism:craft` by default (the built-in miner rituals use `occultism:craft_miner_spirit`), and the recipe id becomes your own namespace (for example `cdp2:recipes/occultism/ritual/...`). This mod identifies miner-spirit rituals semantically ("is the output a miner spirit / what is the `ritual_type`"), **not by recipe id**, so re-registered miner spirits still convert correctly. diff --git a/wiki/KUBEJS_ALLOY_FURNACE_GUIDE_CN.md b/wiki/KUBEJS_ALLOY_FURNACE_GUIDE_CN.md new file mode 100644 index 00000000..2940b822 --- /dev/null +++ b/wiki/KUBEJS_ALLOY_FURNACE_GUIDE_CN.md @@ -0,0 +1,541 @@ +# KubeJS 万象合金炉配方指南 + +

+ English | + 简体中文 +

+ +## 概述 + +万象合金炉支持通过 KubeJS 在服务器配方加载阶段添加、替换和删除配方。脚本文件放在 `kubejs/server/` 下,修改后执行 `/reload` 生效。 + +本联动注册的配方类型为 `useless_mod:advanced_alloy_furnace`,对应脚本入口为: + +```javascript +event.recipes.useless_mod.advanced_alloy_furnace(...) +``` + +需要安装适用于 NeoForge 1.21.1 的 KubeJS。没有安装 KubeJS 时,Useless Mod 仍然可以正常运行。 + +## 目录 + +- [基础用法](#基础用法) +- [输入输出组合](#输入输出组合) +- [AEKey 输入和输出](#aekey-输入和输出) +- [字段说明](#字段说明) +- [配方等级](#配方等级) +- [替换和删除配方](#替换和删除配方) +- [完整示例](#完整示例) +- [扩展 JSON](#扩展-json) +- [最佳实践](#最佳实践) +- [排错](#排错) + +## 基础用法 + +### 最简单的配方 + +```javascript +ServerEvents.recipes(event => { + event.recipes.useless_mod.advanced_alloy_furnace( + 'kubejs:iron_alloy', + [ + { ingredient: 'minecraft:iron_ingot', count: 4 }, + { ingredient: 'minecraft:gold_ingot', count: 2 } + ], + [ + Item.of('kubejs:iron_alloy', 1) + ] + ) +}) +``` + +位置调用依次接收配方数据 ID、物品输入列表和物品输出列表。`count` 省略时,输入数量默认为 1。只有流体输出时,请使用对象写法并省略 `outputs`。 + +### 同时使用物品和流体 + +物品和流体使用不同字段声明:物品输入使用 `ingredients`,流体输入使用 `input_fluids`;物品输出使用 `outputs`,流体输出使用 `output_fluids`。同一个配方可以同时包含物品和流体输入、输出: + +```javascript +ServerEvents.recipes(event => { + event.recipes.useless_mod.advanced_alloy_furnace({ + id: 'kubejs:water_alloy', + ingredients: [ + { ingredient: 'minecraft:iron_ingot', count: 2 } + ], + input_fluids: [ + { ingredient: 'minecraft:water', amount: 1000 } + ], + outputs: [ + Item.of('kubejs:water_alloy', 1) + ], + output_fluids: [ + Fluid.of('minecraft:lava', 250) + ] + }) +}) +``` + +如果配方只有流体输入或输出,请使用对象写法并省略未使用的物品列表。`ingredients` 本身仍是 Schema 的必填字段;没有物品输入时写成 `ingredients: []` 即可。 + +### 添加模具和等级 + +```javascript +ServerEvents.recipes(event => { + event.recipes.useless_mod.advanced_alloy_furnace( + 'kubejs:gear_alloy', + [ + { ingredient: '#c:ingots/iron', count: 4 }, + { ingredient: '#c:ingots/copper', count: 4 } + ], + [ + Item.of('kubejs:gear_alloy', 1) + ], + 'useless_mod:metal_mold_gear', + 3 + ) +}) +``` + +位置调用的第四个参数是单个 `mold`,第五个参数是 `tier`。上面的配方要求线圈等级至少为 3。 + +### 五芒星模具(必须带组件) + +`mold` / `molds` 是标准 Ingredient:**只写物品 ID 或标签时不包含任何 NBT/组件**,会退化为“任意仪式蓝图都满足”。要绑定特定的神秘学五芒星,必须带上 `useless_mod:ritual_blueprint_pentacle` 组件: + +```javascript +ServerEvents.recipes(event => { + event.recipes.useless_mod.advanced_alloy_furnace({ + id: 'kubejs:foliot_miner', + ingredients: [ + { ingredient: 'occultism:book_of_binding_bound_foliot', count: 1 }, + { ingredient: 'occultism:iesnium_pickaxe', count: 1 }, + { ingredient: 'occultism:magic_lamp_empty', count: 1 } + ], + outputs: [Item.of('occultism:miner_foliot_unspecialized', 1)], + mold: Item.of('useless_mod:ritual_blueprint', { + 'useless_mod:ritual_blueprint_pentacle': ['occultism:craft_foliot'] + }), + tier: 0 + }) +}) +``` + +等价的 SNBT 字符串写法: + +```javascript +mold: 'useless_mod:ritual_blueprint{useless_mod:ritual_blueprint_pentacle:["occultism:craft_foliot"]}' +``` + +`molds` 的每一项目同样需要带组件,可以与普通模具混用: + +```javascript +molds: [ + Item.of('useless_mod:ritual_blueprint', { + 'useless_mod:ritual_blueprint_pentacle': ['occultism:craft_foliot'] + }), + 'useless_mod:metal_mold_gear' +] +``` + +> 匹配时只要玩家蓝图上的五芒星**包含**配方要求的五芒星即算满足,所以一张刻印了多个五芒星的蓝图可以同时满足多条配方。 + +### 对象写法 + +对象写法适合同时指定等级、模具和其他可选字段,也可以在没有模具时直接指定 `tier`: + +```javascript +ServerEvents.recipes(event => { + event.recipes.useless_mod.advanced_alloy_furnace({ + id: 'kubejs:press_alloy', + ingredients: [ + { ingredient: 'minecraft:diamond', count: 8 }, + { ingredient: 'minecraft:netherite_ingot', count: 1 } + ], + outputs: [ + Item.of('kubejs:press_alloy', 1) + ], + tier: 5, + energy: 500000, + process_time: 200 + }) +}) +``` + +### 多个模具 + +使用 `molds` 可以声明多个独立的模具要求: + +```javascript +ServerEvents.recipes(event => { + event.recipes.useless_mod.advanced_alloy_furnace({ + id: 'kubejs:multi_mold_alloy', + ingredients: [ + { ingredient: 'minecraft:iron_block', count: 2 } + ], + outputs: [ + Item.of('kubejs:multi_mold_alloy', 1) + ], + molds: [ + 'useless_mod:metal_mold_gear', + 'useless_mod:metal_mold_plate' + ], + tier: 4 + }) +}) +``` + +`mold` 和 `molds` 不能同时填写。多个模具由万象炉的模具仓/模具中心处理;普通单模具槽机器不支持多个模具要求。 + +## 输入输出组合 + +配方支持以下物品/流体输入和输出组合: + +| 测试 ID 后缀 | 输入 | 输出 | +|---|---|---| +| `item_to_item` | 物品 | 物品 | +| `item_to_fluid` | 物品 | 流体 | +| `item_fluid_to_item` | 物品 + 流体 | 物品 | +| `item_fluid_to_fluid` | 物品 + 流体 | 流体 | +| `item_to_item_fluid` | 物品 | 物品 + 流体 | +| `item_fluid_to_item_fluid` | 物品 + 流体 | 物品 + 流体 | +| `fluid_to_item` | 流体 | 物品 | +| `fluid_to_fluid` | 流体 | 流体 | +| `fluid_to_item_fluid` | 流体 | 物品 + 流体 | + +例如,同时使用物品和流体输入,并同时输出物品和流体: + +```javascript +event.recipes.useless_mod.advanced_alloy_furnace({ + id: 'kubejs:alloy_furnace_item_fluid_to_item_fluid_test', + ingredients: [ + { ingredient: { item: 'minecraft:redstone' }, count: 1 } + ], + input_fluids: [ + { ingredient: { fluid: 'minecraft:water' }, amount: 1000 } + ], + outputs: [ + Item.of('minecraft:gold_nugget', 1) + ], + output_fluids: [ + Fluid.of('minecraft:lava', 250) + ], + tier: 0 +}) +``` + +如果测试配方没有物品输入,`ingredients` 仍然是必填字段,应明确写成空数组: + +```javascript +event.recipes.useless_mod.advanced_alloy_furnace({ + id: 'kubejs:alloy_furnace_fluid_to_item_test', + ingredients: [], + input_fluids: [ + { ingredient: { fluid: 'minecraft:water' }, amount: 1000 } + ], + outputs: [Item.of('minecraft:coal', 1)], + tier: 0 +}) +``` + +模具要求可以分别包含 1、2、3 个独立条目: + +```javascript +// 1 个模具 +molds: [{ item: 'minecraft:stick' }] + +// 2 个模具 +molds: [ + { item: 'minecraft:stick' }, + { item: 'minecraft:stone' } +] + +// 3 个模具 +molds: [ + { item: 'minecraft:stick' }, + { item: 'minecraft:stone' }, + { item: 'minecraft:cobblestone' } +] +``` + +每个模具条目都是独立要求。一个配方中不要同时使用 `mold` 和 `molds`。 + +## AEKey 输入和输出 + +Schema 通过 `key_inputs` 和 `key_outputs` 暴露 AE2 通用堆栈。每一项都是一个 `GenericStack`:`#t` 选择已注册的 AEKey 类型,`#` 表示数量。因此不需要在本模组中硬编码其他模组的 key 类。 + +以 Data Energistics 数据流为例,需要安装并加载 Data Energistics,确保配方解析前已经注册 `data_energistics:data_flow`: + +```javascript +ServerEvents.recipes(event => { + event.recipes.useless_mod.advanced_alloy_furnace({ + id: 'kubejs:data_flow_alloy', + ingredients: [], + key_inputs: [ + { '#t': 'data_energistics:data_flow', '#': 1200 } + ], + key_outputs: [ + { '#t': 'data_energistics:data_flow', '#': 200 } + ], + tier: 0, + energy: 1000, + process_time: 20 + }) +}) +``` + +`data_energistics:data_flow` 是 AEKey,不是物品,也不是普通流体,所以数量必须写在 `key_inputs` 或 `key_outputs` 中。配方没有物品输入时仍需写 `ingredients: []`;如果 `key_outputs` 已经提供输出,普通的 `outputs` 和 `output_fluids` 可以省略。 + +AE2 物品和流体 key 使用相同格式: + +```javascript +key_inputs: [ + { '#t': 'ae2:i', id: 'minecraft:iron_ingot', '#': 1 }, + { '#t': 'ae2:f', id: 'minecraft:water', '#': 1000 } +], +key_outputs: [ + { '#t': 'ae2:i', id: 'minecraft:gold_ingot', '#': 1 }, + { '#t': 'ae2:f', id: 'minecraft:lava', '#': 250 } +] +``` + +普通机器流体口使用 `input_fluids` 和 `output_fluids`;当流体需要作为 AE 网络中的 AEKey 传输时,才在 `key_inputs` 和 `key_outputs` 中使用 `ae2:f`。 + +## 字段说明 + +| 字段 | 类型 | 必需 | 默认值 | 说明 | +|---|---|---:|---:|---| +| `id` | String | 是 | - | 配方数据 ID,例如 `kubejs:iron_alloy` | +| `ingredients` | Object[] | 是 | - | 输入物列表,每项包含 `ingredient` 和可选的 `count` | +| `input_fluids` | Object[] | 否 | `[]` | 流体输入列表,每项包含流体 `ingredient` 和 `amount` | +| `key_inputs` | GenericStack[] | 否 | `[]` | AEKey 输入列表,每项使用 AE2 通用堆栈格式 | +| `outputs` | ItemStack[] | 否 | `[]` | 输出物列表,可使用字符串或 `Item.of(...)` | +| `output_fluids` | FluidStack[] | 否 | `[]` | 流体输出列表,可使用 `Fluid.of(...)` | +| `key_outputs` | GenericStack[] | 否 | `[]` | AEKey 输出列表,每项使用 AE2 通用堆栈格式 | +| `mold` | Ingredient | 否 | 空 | 单个模具要求:物品 ID、标签或带组件的 `Item.of(...)`;绑定五芒星必须带 `useless_mod:ritual_blueprint_pentacle` | +| `molds` | Ingredient[] | 否 | 空 | 多个独立模具要求,不能与 `mold` 同时使用;每项同样支持组件 | +| `tier` | Integer | 否 | 未指定 | 线圈最低等级,范围为 `0-10` | +| `energy` | Long | 否 | `2000` | 一次处理消耗的能量 | +| `process_time` | Integer | 否 | `200` | 处理时间,单位为 tick | +| `catalyst` | Ingredient | 否 | 空 | 催化剂要求;当前万象合金炉运行逻辑不会消耗催化剂 | + +### 物品输入格式 + +```javascript +ingredients: [ + { ingredient: 'minecraft:iron_ingot', count: 4 }, + { ingredient: '#c:ingots/copper', count: 2 }, + { ingredient: 'minecraft:diamond' } +] +``` + +`ingredient` 使用 KubeJS 标准 Ingredient 写法,可以是物品 ID、标签或其他受 KubeJS 支持的 Ingredient 表达式。每种输入物会分别检查数量。 + +需要带 NBT/组件的输入时,必须使用 `Item.of(...)` 或 SNBT 字符串形式: + +```javascript +ingredients: [ + // 带组件:只要求这些组件存在且值相同,其它组件可以额外存在 + { ingredient: Item.of('occultism:book_of_binding_bound_foliot', { + 'occultism:spirit_name': 'Some Name' + }), count: 1 }, + // 等价的 SNBT 字符串写法 + { ingredient: 'occultism:book_of_binding_bound_foliot{occultism:spirit_name:"Some Name"}', count: 1 }, + // 不带组件:任意该物品都满足 + { ingredient: 'minecraft:iron_ingot', count: 4 } +] +``` + +> 只写物品 ID(例如 `'occultism:book_of_binding_bound_foliot'`)**不会**保留 NBT,任何该物品都会匹配;模具同理。需要精确匹配时请带上组件。 + +### 流体输入格式 + +```javascript +input_fluids: [ + { ingredient: 'minecraft:water', amount: 1000 }, + { ingredient: '#c:lava', amount: 250 } +] +``` + +`amount` 的单位是 mB,支持较大的整数。`ingredient` 可以使用流体 ID、流体标签或 KubeJS 支持的流体 Ingredient 表达式。 + +### 输出物格式 + +```javascript +outputs: [ + Item.of('minecraft:diamond', 2), + Item.of('kubejs:alloy', 1, { display: { Name: 'Custom alloy' } }) +] +``` + +也可以直接使用物品字符串: + +```javascript +outputs: [ 'minecraft:iron_ingot' ] +``` + +### 流体输出格式 + +```javascript +output_fluids: [ + Fluid.of('minecraft:water', 1000), + Fluid.of('minecraft:lava', 250) +] +``` + +物品输出不是必填项,因此配方也可以只产出流体: + +```javascript +ServerEvents.recipes(event => { + event.recipes.useless_mod.advanced_alloy_furnace({ + id: 'kubejs:fluid_only_alloy', + ingredients: [ + { ingredient: 'minecraft:iron_ingot', count: 1 } + ], + output_fluids: [ + Fluid.of('minecraft:lava', 1000) + ] + }) +}) +``` + +## 配方等级 + +`tier` 表示配方要求的万象炉线圈等级,范围为 `0-10`。 + +- 填写 `tier: 3` 时,配方最低需要 3 级线圈。 +- 填写 `tier: 0` 时,配方明确表示不限制等级。 +- 不填写 `tier` 时,使用服务器配置中的 `recipe_tier_rules`。 +- `recipe_tier_rules` 的格式为 `配方 ID 通配符,等级`,例如 `kubejs:*,3`。 +- 配方中显式填写的 `tier` 优先于 `recipe_tier_rules`。 + +示例配置: + +```toml +recipe_tier_rules = [ + "useless_mod:advanced_alloy/gear/*,4", + "kubejs:*,2" +] +``` + +配置只对未显式指定 `tier` 的配方生效。修改配置后需要重新加载配置或重启游戏,具体取决于配置界面提示。 + +> ⚠️ 上面示例里的 `kubejs:*,2` 会让**所有** `kubejs:` 开头的配方都要求至少 2 级线圈。低阶线圈的机器上,这些配方在 JEI 里看得见但机器不会启动——排查“配方能看见却不工作”时请先检查 `recipe_tier_rules`。 + +## 替换和删除配方 + +### 删除指定配方 + +```javascript +ServerEvents.recipes(event => { + event.remove({ id: 'useless_mod:advanced_alloy/gear/useless_gear_tier_1' }) +}) +``` + +### 按类型删除 + +```javascript +ServerEvents.recipes(event => { + event.remove({ type: 'useless_mod:advanced_alloy_furnace' }) +}) +``` + +按类型删除会移除所有万象合金炉配方,使用时需要谨慎。 + +### 替换指定配方 + +```javascript +ServerEvents.recipes(event => { + const id = 'useless_mod:advanced_alloy/gear/useless_gear_tier_1' + + event.remove({ id: id }) + event.recipes.useless_mod.advanced_alloy_furnace({ + id: id, + ingredients: [ + { ingredient: 'minecraft:iron_ingot', count: 8 } + ], + outputs: [ + Item.of('useless_mod:useless_gear_tier_1', 1) + ], + mold: 'useless_mod:metal_mold_gear', + tier: 2 + }) +}) +``` + +## 完整示例 + +```javascript +ServerEvents.recipes(event => { + const { useless_mod } = event.recipes + + useless_mod.advanced_alloy_furnace({ + id: 'kubejs:complete_alloy', + ingredients: [ + { ingredient: 'minecraft:iron_block', count: 2 }, + { ingredient: '#c:ingots/gold', count: 8 } + ], + outputs: [ + Item.of('kubejs:complete_alloy', 1) + ], + mold: 'useless_mod:metal_mold_gear', + tier: 3, + energy: 100000, + process_time: 100, + catalyst: 'minecraft:diamond' + }) +}) +``` + +## 扩展 JSON + +当前 Schema 已覆盖物品输入、流体输入、AEKey 输入、物品输出、AEKey 输出、流体输出、模具、等级和常用处理参数。需要其他尚未暴露的字段时,可以用 `event.custom` 直接提交配方 JSON。流体字段的原始 JSON 形状如下: + +```javascript +ServerEvents.recipes(event => { + event.custom({ + type: 'useless_mod:advanced_alloy_furnace', + id: 'kubejs:fluid_alloy', + ingredients: [ + { + ingredient: { item: 'minecraft:iron_ingot' }, + count: 4 + } + ], + input_fluids: [ + { + ingredient: { fluid: 'minecraft:water' }, + amount: 1000 + } + ], + output_fluids: [ + { id: 'minecraft:water', amount: 1000 } + ], + tier: 2 + }) +}) +``` + +`event.custom` 不会经过本 Schema 的参数转换,字段名和 JSON 类型必须符合万象合金炉配方序列化器的定义。 + +## 最佳实践 + +1. 使用自己的模组 ID 或 `kubejs:` 作为配方 ID 前缀,避免覆盖其他配方。 +2. 输入物数量使用正数,并确认 `outputs`、`output_fluids` 或 `key_outputs` 中至少有一个有效输出。 +3. 替换配方时先删除旧配方,再使用相同的 `id` 添加新配方。 +4. 多模具配方使用对象写法,避免依赖位置参数。 +5. 只有需要覆盖配置等级时才填写 `tier`;不填写可以继续由服务器配置统一管理。 +6. 添加或修改脚本后执行 `/reload`,不要只重新打开配方查看器。 + +## 排错 + +如果配方没有出现或机器无法处理: + +1. 检查游戏日志中是否有 KubeJS 配方创建错误。 +2. 确认 `id` 是合法的资源位置格式:`namespace:path`。 +3. 确认 `ingredients` 和 `id` 已填写,并确认 `outputs`、`output_fluids` 或 `key_outputs` 中至少有一个有效输出。 +4. 确认没有同时填写 `mold` 和 `molds`。 +5. 检查线圈等级、模具、输入数量和催化剂要求是否满足配方条件。 +6. 普通机器流体使用 `input_fluids`/`output_fluids`;AEKey 流体或其他 AE 资源使用 `key_inputs`/`key_outputs`,并检查 `#t` 对应的 key 类型已注册。 +7. 修改脚本后执行 `/reload`,并确认日志中没有脚本异常。 +8. `mold`/`molds` 只写了物品 ID 而没有组件时会退化为“任意蓝图”,可能与其它五芒星配方互相冲突;绑定五芒星请按[五芒星模具](#五芒星模具必须带组件)带上组件。 +9. 用 `occultism_kubejs` 重加神秘学仪式时注意:`event.recipes.occultism.ritual(...)` 会把 `ritual_type` 默认写成 `occultism:craft`(原版矿工仪式是 `occultism:craft_miner_spirit`),并且配方 ID 会变成你自己的命名空间(例如 `cdp2:recipes/occultism/ritual/...`)。本模组按“输出物品是否为矿工之灵 / `ritual_type`”语义识别矿工仪式,**不依赖配方 ID**,因此重加后矿工之灵仍可正常转化。